サイトのトップへ戻る

Multi-OS Engineドキュメント 日本語訳

サイト内検索

LibGDXを使用する

警告
このページの情報は古くなっています。LibGDXはオープンソース版のMulti-OS Engineをサポートをするためのアップデートが行われています。 アップデート版のLibGDXがリリースされた時にこのページを更新する予定です。

このページでは、 Intelの Multi-OS Engine テクニカルプレビューと libGDX ライブラリを使ってJavaゲームを作る方法について説明します。

目次:

開発環境を設定する

アプリを開発する

アプリをデバッグする

まとめ

以下のスクリーンショットは、Android端末上で動作してるゲームとApple iOS端末で動作しているゲームのものです。 どちらのOS上でも、ゲームは同じ様な見た目と挙動をしています。これはどのようにしてできるのでしょうか?

あなたがlibGDXについてあまり知らないのであれば、このチュートリアルがゼロから学び始める際の助けになるでしょう。それでは始めましょう。画面を下へスクロールしてください!

./img/libgdx1.png ./img/libgdx2.png



開発環境を設定する

まず最初に、以下のアプリケーションをインストールします:

  • Git
  • Android Studio
  • Ant
  • Maven
  • Multi-OS Engine, a plugin for Android Studio

システムパスに Gitと Maven と Ant を追加する必要があります。

初めてのlibGDXアプリケーションを作成する前に、正しいバージョンのlibGDXで開発環境を設定する必要があります。 今回のアプリケーションでは、最新バージョンのlibGDX を使用します(この記事を書いている時点ではバージョン1.7.0)。 最新版のlibGDXを取得するには、GitHubから公式のlibGDXリポジトリをcloneします。 これを行うには、以下のコマンドを実行します:

git clone https://github.com/libgdx/libgdx.git

その後、以下のコマンドを使ってlibGDX ライブラリをビルドします:

ant -f fetch.xml
mvn install

このビルドが完了すると、ローカル maven リポジトリにMulti-OS EngineをサポートしているバージョンのlibGDXが出来ています。これでアプリケーションの開発を解しできます。



アプリを開発する

まず最初に、新規にAndroid Studioプロジェクトを作成しましょう。 Android Studio を起動し、右側のパネルにある Start a new Android Studio project をクリックしてください。

./img/libgdx3.png

次に、プロジェクトの設定をします。例えば、アプリケーション名にはLibGDXMisslecommand と設定します。

./img/libgdx4.png
  1. アプリケーションのフォームファクタには Phone and Tablet を選びます。
  2. また、最小のSDK (Android) バージョンを選びます。
  3. Nextをクリックします。

Android アプリケーションにアクティビティを追加しないでください。 Finishをクリックします。

./img/libgdx5.png

こうしてプロジェクトが作成されました! それでは、アプリケーションの共通部分を開発しましょう。



共通コード部分

アプリの共通部分を作成するには、Java ライブラリのタイプを持つAndroid Studioモジュールを新規に追加する必要があります。Java Libraryを選んでNextをクリックします。

./img/libgdx6.png

新しいモジュールとパッケージと共通クラスの名前を設定します。MissileCommandという名前でJavaクラスを作成しましょう。

./img/libgdx7.png

これが完了すると、以下のようなファイルツリーが表示されます。:

./img/libgdx8.png

まず、メインの build.gradle ファイルを変更しましょう。以下のコードを記述してください:

import groovy.json.JsonSlurper
buildscript {
// Download information about the latest versions of libGDX components
ant.get(src: 'http://libgdx.badlogicgames.com/libgdx-site/service/getVersions?release=false', dest: 'versions.json')
// Get information from file
def versionFile = file('versions.json')
def json
if (versionFile.exists()) {
        json = new JsonSlurper().parseText(versionFile.text)
} else throw new GradleException("Unable to retrieve latest versions, please check your internet connection")
ext {
        // Set versions
        gdxVersion = json.libgdxSnapshot
        roboVMVersion = json.robovmVersion
        roboVMGradleVersion = json.robovmPluginVersion
        androidToolsVersion = json.androidBuildtoolsVersion
        androidSDKVersion = json.androidSDKVersion
        androidGradleToolsVersion = json.androidGradleToolVersion
        gwtVersion = json.gwtVersion
        gwtGradleVersion = json.gwtPluginVersion
}
repositories {
        jcenter()
}
dependencies {
        classpath 'com.android.tools.build:gradle:1.3.0'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
}
}
// Set repositories for all projects
allprojects {
repositories {
        jcenter()
        // Maven local repository necessary for our libGDX version
        mavenLocal()
}
}

アプリの共通部分のコードを記述する前に、common/build.gradleファイルを編集してlibGDXライブラリの依存関係を追加する必要があります。:

apply plugin: 'java'
sourceCompatibility = 1.7
targetCompatibility = 1.7
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
// libGDX common library
compile "com.badlogicgames.gdx:gdx:$gdxVersion"
}

それではまず最初に、共通部分でリソース読み込み用のAssetsクラスを作成します。

package org.moe.libgdxmissilecommand.common;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.audio.Sound;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
public class Assets {
public static final float screenWidth = 800;
public static final float screenHeight = 480;
// Backgrounds
public static TextureRegion backgroundRegionMenu;
public static TextureRegion backgroundLoadGame1TextureRegion;
public static TextureRegion backgroundLoadGame2TextureRegion;
public static TextureRegion[] levelsBackgroundsTextureRegions = new TextureRegion[4];
// Buttons
public static TextureRegion backButtonTextureRegion;
public static TextureRegion pauseButtonTextureRegion;
public static TextureRegion startButtonTextureRegion;
public static TextureRegion creditsButtonTextureRegion;
// Player
public static TextureRegion playerBaseTextureRegion;
public static TextureRegion playerGunTextureRegion;
// Missiles
public static TextureRegion missileTextureRegion;
public static Sound missileSound;
// Explosions
public static Animation explosionAnimation;
public static Sound explosionSound;
// Cities
public static Animation citiesDestroyAnimation;
public static TextureRegion[] citiesTextureRegions = new TextureRegion[6];
// Naves
public static TextureRegion navesTextureRegion;
// Font file
public static FileHandle fontFile;
public static Sound alarmSound;
private static Texture loadTexture (String file) {
        return new Texture(Gdx.files.internal(file));
}
public static void load() {
        Texture backgroundMenuTexture = loadTexture("Splash2.png");
        backgroundRegionMenu = new TextureRegion(backgroundMenuTexture, 0, 0, 800, 480);
        Texture backgroundLoadGame1Texture = loadTexture("aviso1.png");
        backgroundLoadGame1TextureRegion = new TextureRegion(backgroundLoadGame1Texture, 0, 0, 800, 480);
        Texture backgroundLoadGame2Texture = loadTexture("aviso2.png");
        backgroundLoadGame2TextureRegion = new TextureRegion(backgroundLoadGame2Texture, 0, 0, 800, 480);
        for (int i = 0; i < levelsBackgroundsTextureRegions.length; i++) {
        Texture backgroundLevelTexture = loadTexture("bg" + String.valueOf(i + 1) + ".png");
        levelsBackgroundsTextureRegions[i] = new TextureRegion(backgroundLevelTexture, 0, 0, 800, 480);
        }
        Texture startButtonTexture = loadTexture("btStart.png");
        startButtonTextureRegion = new TextureRegion(startButtonTexture, 0, 0, 120, 43);
        Texture creditsButtonTexture = loadTexture("btCredits.png");
        creditsButtonTextureRegion = new TextureRegion(creditsButtonTexture, 0, 0, 144, 43);
        Texture backButtonTexture = loadTexture("btBack.png");
        backButtonTextureRegion = new TextureRegion(backButtonTexture, 0, 0, 106, 43);
        Texture pauseButtonTexture = loadTexture("btPause.png");
        pauseButtonTextureRegion = new TextureRegion(pauseButtonTexture, 0, 0, 81, 22);
        Texture playerBaseTexture = loadTexture("canhaopart1.png");
        playerBaseTextureRegion = new TextureRegion(playerBaseTexture, 0, 0, 75, 45);
        Texture playerGunTexture = loadTexture("canhaopart2.png");
        playerGunTextureRegion = new TextureRegion(playerGunTexture, 0, 0, 8, 45);
        Texture missileTexture = loadTexture("missil.png");
        missileTextureRegion = new TextureRegion(missileTexture, 0, 0, 5, 9);
        for (int i = 0; i < citiesTextureRegions.length; i++) {
        Texture cityTexture = loadTexture("city" + String.valueOf(i + 1) + ".png");
        citiesTextureRegions[i] = new TextureRegion(cityTexture, 0, 0, 80, 80);
        }
        Texture citiesDestroy = loadTexture("cidade.png");
        citiesDestroyAnimation = new Animation(0.2f, new TextureRegion(citiesDestroy,2,53,51,59),
                new TextureRegion(citiesDestroy,55,2,51,15));
        fontFile = Gdx.files.internal("arial.fnt");
        alarmSound = Gdx.audio.newSound(Gdx.files.internal("alarm.mp3"));
        missileSound = Gdx.audio.newSound(Gdx.files.internal("missil.mp3"));
        Texture explosion = loadTexture("explosao1.png");
        explosionAnimation = new Animation(0.2f, new TextureRegion(explosion,8,8,26,22),
                new TextureRegion(explosion,35,8,40,38), new TextureRegion(explosion,76,8,60,56),
                new TextureRegion(explosion,137,8,74,76), new TextureRegion(explosion,8,85,74,68),
                new TextureRegion(explosion,83,85,72,68), new TextureRegion(explosion,156,85,68,62),
                new TextureRegion(explosion,8,154,62,60), new TextureRegion(explosion,71,154,58,56));
        explosionSound = Gdx.audio.newSound(Gdx.files.internal("bomb.mp3"));
        Texture nave = loadTexture("naves.png");
        navesTextureRegion = new TextureRegion(nave, 12, 20);
}
public static void playSound(Sound sound) {
        sound.play(1);
}
}

次に、MissileCommand クラスを変更します。 MissileCommand.java ファイルに以下のコードを記述してください。 コードにはコメントも記述して、処理内容を分かりやすくします。

package org.moe.libgdxmissilecommand.common;
import com.badlogic.gdx.Game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import org.moe.libgdxmissilecommand.common.screens.MainMenuScreen;
// Main game class
public class MissileCommand extends Game {
// used by all screens
public SpriteBatch batcher;
@Override
public void create () {
        Gdx.input.setCatchBackKey(true);
        batcher = new SpriteBatch();
        // Load resources
        Assets.load();
        // Draw main menu
        setScreen(new MainMenuScreen(this));
}
@Override
public void render() {
        super.render();
}
}

次に、全てのゲーム画面用のパッケージを作成し、そしてメインメニュー用の MainMenuScreen クラスを作成します。

package org.moe.libgdxmissilecommand.common.screens;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.ScreenAdapter;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.math.Vector3;
import org.moe.libgdxmissilecommand.common.MissileCommand;
import org.moe.libgdxmissilecommand.common.Assets;
public class MainMenuScreen extends ScreenAdapter {
MissileCommand game;
OrthographicCamera guiCam;
Sprite startButton;
Sprite creditsButton;
Vector3 touchPoint;
public MainMenuScreen (MissileCommand game) {
        this.game = game;
        // initialize camera
        guiCam = new OrthographicCamera(Assets.screenWidth, Assets.screenHeight);
        guiCam.position.set(Assets.screenWidth / 2, Assets.screenHeight / 2, 0);
        // Create buttons
        startButton = new Sprite(Assets.startButtonTextureRegion);
        startButton.setPosition(Assets.screenWidth / 2 - startButton.getWidth() / 2, startButton.getHeight() * 2.5f);
        creditsButton = new Sprite(Assets.creditsButtonTextureRegion);
        creditsButton.setPosition(Assets.screenWidth / 2 - creditsButton.getWidth() / 2, creditsButton.getHeight());
        touchPoint = new Vector3();
}
// Find events
public void update () {
        if (Gdx.input.isKeyPressed(Input.Keys.BACK)) {
        Gdx.app.exit();
        }
        if (Gdx.input.justTouched()) {
        guiCam.unproject(touchPoint.set(Gdx.input.getX(), Gdx.input.getY(), 0));
        // Update screen
        if (startButton.getBoundingRectangle().contains(touchPoint.x, touchPoint.y)) {
                game.setScreen(new MissleCommandGameScreen(game));
                return;
        }
        if (creditsButton.getBoundingRectangle().contains(touchPoint.x, touchPoint.y)) {
                game.setScreen(new CreditsScreen(game));
                return;
        }
        }
}
// Draw elements
public void draw () {
        GL20 gl = Gdx.gl;
        gl.glClearColor(1, 0, 0, 1);
        gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
        guiCam.update();
        game.batcher.setProjectionMatrix(guiCam.combined);
        game.batcher.disableBlending();
        game.batcher.begin();
        game.batcher.draw(Assets.backgroundRegionMenu, 0, 0, Assets.screenWidth, Assets.screenHeight);
        game.batcher.end();
        game.batcher.enableBlending();
        game.batcher.begin();
        startButton.draw(game.batcher);
        creditsButton.draw(game.batcher);
        game.batcher.end();
}
@Override
public void render (float delta) {
        update();
        draw();
}
}

ゲームに関するいくつかの情報を持ったCreditsScreen クラスを作成します。

package org.moe.libgdxmissilecommand.common.screens;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.ScreenAdapter;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.math.Vector3;
import org.moe.libgdxmissilecommand.common.MissileCommand;
import org.moe.libgdxmissilecommand.common.Assets;
public class CreditsScreen extends ScreenAdapter {
MissileCommand game;
OrthographicCamera guiCam;
Sprite backButton;
Vector3 touchPoint;
BitmapFont font;
public CreditsScreen(MissileCommand game) {
        this.game = game;
        font = new BitmapFont(Assets.fontFile);
        font.getData().setScale(0.7f);
        guiCam = new OrthographicCamera(Assets.screenWidth, Assets.screenHeight);
        guiCam.position.set(Assets.screenWidth / 2, Assets.screenHeight / 2, 0);
        backButton = new Sprite(Assets.backButtonTextureRegion);
        backButton.setPosition(Assets.screenWidth / 2 - backButton.getWidth() / 2, backButton.getHeight());
        touchPoint = new Vector3();
}
public void update () {
        if (Gdx.input.justTouched()) {
        guiCam.unproject(touchPoint.set(Gdx.input.getX(), Gdx.input.getY(), 0));
        if (backButton.getBoundingRectangle().contains(touchPoint.x, touchPoint.y)) {
                game.setScreen(new MainMenuScreen(game));
                return;
        }
        }
        if (Gdx.input.isKeyPressed(Input.Keys.BACK)){
        return;
        }
}
public void draw () {
        GL20 gl = Gdx.gl;
        gl.glClearColor(1, 0, 0, 1);
        gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
        guiCam.update();
        game.batcher.setProjectionMatrix(guiCam.combined);
        game.batcher.disableBlending();
        game.batcher.begin();
        game.batcher.draw(Assets.backgroundRegionMenu, 0, 0, Assets.screenWidth, Assets.screenHeight);
        game.batcher.end();
        game.batcher.enableBlending();
        game.batcher.begin();
        backButton.draw(game.batcher);
        font.draw(game.batcher, "Programmer: Matheus Palheta

Game Design: Jucimar Jr", 200, 320);
        game.batcher.end();
}
@Override
public void render (float delta) {
        update();
        draw();
}
@Override
public void pause () {
        //Settings.save();
}
}

メインのゲーム画面となる MissleCommandGameScreenクラスを作成します。

package org.moe.libgdxmissilecommand.common.screens;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.ScreenAdapter;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector3;
import org.moe.libgdxmissilecommand.common.Assets;
import org.moe.libgdxmissilecommand.common.MissileCommand;
import org.moe.libgdxmissilecommand.common.models.City;
import org.moe.libgdxmissilecommand.common.models.GameObject;
import org.moe.libgdxmissilecommand.common.models.Nave;
import java.util.ArrayList;
public class MissleCommandGameScreen extends ScreenAdapter {
private MissileCommand game;
private OrthographicCamera guiCam;
private BitmapFont fontTime;
private BitmapFont fontTitle;
private int levelBackgroung = 0;
private long lastAttackTime = 0;
private float time = 0;
private boolean playing = true;
private Sprite pauseButton;
private Sprite backButton;
private Vector3 touchPoint;
private int wave = 1;
private long verNaves = 0;
private long score = 0;
private ArrayList<org.moe.libgdxmissilecommand.common.models.Missile> missiles = new ArrayList<>();
private ArrayList<Nave> naves = new ArrayList<>();
City[] cities = new City[6];
org.moe.libgdxmissilecommand.common.models.Player player;
public MissleCommandGameScreen(MissileCommand game) {
        this.game = game;
        fontTime = new BitmapFont(Assets.fontFile);
        fontTime.getData().setScale(0.5f);
        fontTitle = new BitmapFont(Assets.fontFile);
        fontTitle.getData().setScale(0.7f);

        // Initialize camera
        guiCam = new OrthographicCamera(Assets.screenWidth, Assets.screenHeight);
        guiCam.position.set(Assets.screenWidth / 2, Assets.screenHeight / 2, 0);

        // Create buttons
        pauseButton = new Sprite(Assets.pauseButtonTextureRegion);
        pauseButton.setPosition(Assets.screenWidth - (pauseButton.getWidth() + 2), 5);
        backButton = new Sprite(Assets.backButtonTextureRegion);
        backButton.setPosition(Assets.screenWidth - (backButton.getWidth() + 2), 2);
        touchPoint = new Vector3();
        // Create player
        player = new org.moe.libgdxmissilecommand.common.models.Player();
        player.setPosition(Assets.screenWidth / 2, Assets.screenHeight / 24);

        // Create cities
        City.resetCounter();
        for (int i = 0; i < cities.length; i++) {
        cities[i] = new City();
        }
}

// Draw elements
private void draw() {
        GL20 gl = Gdx.gl;
        gl.glClearColor(1, 0, 0, 1);
        gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
        guiCam.update();
        game.batcher.setProjectionMatrix(guiCam.combined);
        game.batcher.begin();
        game.batcher.draw(Assets.levelsBackgroundsTextureRegions[levelBackgroung], 0, 0, Assets.screenWidth, Assets.screenHeight);
        game.batcher.end();
        game.batcher.enableBlending();
        if (playing)
        checkCollisions();
        game.batcher.begin();
        float dt = Gdx.graphics.getDeltaTime();
        for (City city : cities) {
        if (playing)
                city.update(dt);
        city.draw(game.batcher);
        }
        if (playing) {
        time -= dt;
        if (time > 20 / wave && (System.currentTimeMillis() - verNaves > 5000) && City.getCityCounter() > 0) {
                addNaves();
        }
        }
        int printTime = (int) time;
        if (printTime < 0)
        printTime = 0;
        fontTime.draw(game.batcher, "Time: " + String.valueOf(printTime) + "s",
                Assets.screenWidth - 150, Assets.screenHeight - 25);
        fontTime.draw(game.batcher, "Score: " + String.valueOf(score),
                2, Assets.screenHeight - 25);
        for(int i = 0; i < missiles.size(); i++) {
        org.moe.libgdxmissilecommand.common.models.Missile missile = missiles.get(i);
        if (missile.isAlive()) {
                if (playing)
                missile.update(dt);
                missile.draw(game.batcher);
        } else {
                missiles.remove(i);
                i--;
        }
        }
        player.draw(game.batcher);
        for(int i = 0; i < naves.size(); i++) {
        Nave nave = naves.get(i);
        if (nave.isAlive()) {
                if (playing)
                nave.update(dt);
                nave.draw(game.batcher);
        } else {
                naves.remove(i);
                i--;
        }
        }
        if (playing) {
        if (time <= 0) {
                fontTitle.draw(game.batcher, "Wave " + String.valueOf(wave) +
                        " Starts in " + String.valueOf((int)(3 + time)) + "s",
                        230, 240);
                if (time < -3) {
                time = 60;
                wave++;
                levelBackgroung = (wave - 2) % 4;
                }
        }
        pauseButton.draw(game.batcher);
        } else {
        if (City.getCityCounter() > 0) {
                fontTitle.draw(game.batcher, "Touch the screen to resuming the game",
                        100, 320 + fontTitle.getLineHeight());
                fontTitle.draw(game.batcher, "Or press back button to exit in the main menu",
                        70, 320 - fontTitle.getLineHeight());
        } else {
                fontTitle.draw(game.batcher, "You Lose!",
                        320, 320 + fontTitle.getLineHeight());
                fontTitle.draw(game.batcher, "Score: " + String.valueOf(score),
                        320, 320 - fontTitle.getLineHeight());
        }
        backButton.draw(game.batcher);
        }
        fontTime.draw(game.batcher, "fps: " + Gdx.graphics.getFramesPerSecond(), 0, 0);
        game.batcher.end();
}

// Check events
private void update() {
        if (City.getCityCounter() <= 0) {
        playing = false;
        }
        if (Gdx.input.justTouched()) {
        guiCam.unproject(touchPoint.set(Gdx.input.getX(), Gdx.input.getY(), 0));
        if (pauseButton.getBoundingRectangle().contains(touchPoint.x, touchPoint.y) && playing) {
                playing = false;
                return;
        }
        if (backButton.getBoundingRectangle().contains(touchPoint.x, touchPoint.y)) {
                game.setScreen(new MainMenuScreen(game));
                return;
        }
        if (!playing) {
                if (City.getCityCounter() > 0)
                playing = true;
                return;
        }
        if (System.currentTimeMillis() - lastAttackTime > 450 && touchPoint.y >= player.getY()) {
                lastAttackTime = System.currentTimeMillis();
                missiles.add(new org.moe.libgdxmissilecommand.common.models.Missile());
                missiles.get(missiles.size() - 1).detectTarget(touchPoint.x, touchPoint.y);
                player.setRotation(missiles.get(missiles.size() - 1).getRotation());
        }
        }
}

// Add new naves
private void addNaves() {
        naves.add(new Nave(wave));
        naves.add(new Nave(wave));
        naves.add(new Nave(wave));
        verNaves = System.currentTimeMillis();
}
@Override
public void render (float delta) {
        update();
        draw();
}
// Check all collisions in the game
private void checkCollisions() {
        for(int i = 0; i < naves.size(); i++) {
        for (City city : cities) {
                if (isCollision(naves.get(i), city)) {
                naves.get(i).kill();
                city.kill();
                }
        }
        for(int j = 0; j < missiles.size(); j++) {
                if (isCollision(naves.get(i), missiles.get(j)) && naves.get(i).kill()) {
                score++;
                missiles.get(j).kill();
                }
        }
        }
}

// Check collision
private <T extends GameObject> boolean isCollision(T obj1, T obj2) {
        Rectangle r1 = obj1.collisionRectangle();
        Rectangle r2 = obj2.collisionRectangle();
        return !(r1 == null || r2 == null) && r2.overlaps(r1);
}
}

ゲームで使う全ての画面を作成しました。今度は、ゲームで使うPlayerやNavesのようなその他いくつかのモデルを作成する必要があります。 新たにmodels モジュールを作成し、ゲームの全オブジェクトの基底クラスとなる GameObjectを作成します。

package org.moe.libgdxmissilecommand.common.models;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.Rectangle;
public class GameObject extends Sprite {
protected boolean alive = true;
protected float explosionTime;
GameObject(TextureRegion region) {
        super(region);
        explosionTime = -1;4
}

// Check object
public boolean isAlive() {
        return alive;
}

// update object
public void update(float deltaTime) {}

// kill object
public boolean kill() {
        if (Float.compare(explosionTime, -1) == 0) {
        explosionTime = -100;
        return true;
        }
        return false;
}

// get collision rectangle for object
public Rectangle collisionRectangle() {
        if (!alive) {
        return null;
        }
        return getBoundingRectangle();
}
}

Player クラスを作成します。

package org.moe.libgdxmissilecommand.common.models;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.g2d.Sprite;
import org.moe.libgdxmissilecommand.common.Assets;
public class Player extends GameObject {
private Sprite playerBase;
public Player() {
        super(Assets.playerGunTextureRegion);
        playerBase = new Sprite(Assets.playerBaseTextureRegion);
        setOrigin(getWidth() / 2, 0);
}
@Override
public void setPosition(float x, float y) {
        // Need override only for playerBase sprite
        super.setPosition(x - getWidth() / 2, y + 10);
        playerBase.setPosition(x - (playerBase.getWidth() / 2), y);
}
@Override
public void draw(Batch batch) {
        // Need override only for playerBase sprite
        super.draw(batch);
        playerBase.draw(batch);
}
}

Missile クラスを作成します。

package org.moe.libgdxmissilecommand.common.models;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.Vector2;
import org.moe.libgdxmissilecommand.common.Assets;
public class Missile extends GameObject {
// Speed of missile
private final float speed = 600.0f;
private float targetY;
public Missile() {
        super(Assets.missileTextureRegion);
        setPosition(Assets.screenWidth / 2, Assets.screenHeight / 24 + 10);

        // Start sound
        Assets.playSound(Assets.missileSound);
        targetY = 0;
}

// Detect missile target
public void detectTarget(float x, float y) {
        targetY = y;
        Vector2 tempVector = new Vector2(x, y);
        setRotation(tempVector.sub(this.getX(), this.getY()).angle() - 90);
}
@Override
public void update(float deltaTime) {
        // Move missile
        if (getY() > targetY || getX() < 0 || getX() > Assets.screenWidth || explosionTime >= 0) {
        explosionTime += deltaTime;
        return;
        }
        float hypotenuse = speed*deltaTime;
        float shiftY = (float) Math.abs((hypotenuse * Math.cos(getRotation() * Math.PI/180)));
        float shiftX = (float) Math.abs((hypotenuse * Math.sin(getRotation() * Math.PI/180)));
        if (Math.sin(getRotation() * Math.PI / 180) > 0) {
        shiftX = -shiftX;
        }
        setPosition(getX() + shiftX, getY() + shiftY);
}
@Override
public void draw(Batch batch) {
        if (getY() > targetY || getX() < 0 || getX() > Assets.screenWidth || Float.compare(explosionTime, -1) != 0) {
                // Draw explosion animation
        if (explosionTime < 0) {
                explosionTime = 0;
                Assets.playSound(Assets.explosionSound);
        }
        TextureRegion keyFrame = Assets.explosionAnimation.getKeyFrame(explosionTime, false);
        batch.draw(keyFrame, getX() - 74 / 2, getY() - 76 / 2, 74, 76);
        if (Assets.explosionAnimation.isAnimationFinished(explosionTime))
                alive = false;
        } else {
                // Draw missile
        super.draw(batch);
        }
}
}

今度は Navesクラスを作成します。

package org.moe.libgdxmissilecommand.common.models;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import org.moe.libgdxmissilecommand.common.Assets;
import java.security.SecureRandom;
public class Nave extends GameObject {
private float velX = 0, velY = 0;
public Nave(int wave) {
        super(Assets.navesTextureRegion);
        SecureRandom random = new SecureRandom();
        // Init position
        int posX = random.nextInt((int) Assets.screenWidth);
        setPosition(posX, Assets.screenHeight);
        // Calculate init rotation
                float angle;
        if (posX < 400) {
        angle = -random.nextFloat() * 45;
        velX = (float) (Math.sin(angle * Math.PI/180) * (wave / 2));
        velY = (float) (Math.cos(angle * Math.PI/180) * (wave / 2));
        } else {
        angle = random.nextFloat() * 45;
        velX = (float) (Math.sin(angle * Math.PI/180) * (wave / 2));
        velY = (float) (Math.cos(angle * Math.PI/180) * (wave / 2));
        }
        setRotation(-angle);
}
@Override
public void update(float deltaTime) {
        // Move nave
        if (getY() < 30 || explosionTime >= 0) {
        explosionTime += deltaTime;
        return;
        }
        setPosition(getX() - velX, getY() - velY);
}
@Override
public void draw(Batch batch) {
        if (getY() < 30 || Float.compare(explosionTime, -1) != 0) {
                // Draw explosion animation
        if (explosionTime < 0) {
                explosionTime = 0;
                Assets.playSound(Assets.explosionSound);
        }
        TextureRegion keyFrame = Assets.explosionAnimation.getKeyFrame(explosionTime, false);
        batch.draw(keyFrame, getX() - 74 / 2, getY() - 76 / 2, 74, 76);
        if (Assets.explosionAnimation.isAnimationFinished(explosionTime))
                alive = false;
        } else {
                // Draw nave
        super.draw(batch);
        }
}
}

そして都市用の City クラスも作成します。

package org.moe.libgdxmissilecommand.common.models;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import org.moe.libgdxmissilecommand.common.Assets;
import java.security.SecureRandom;
public class City extends GameObject {
// Cities counter
static long cityCounter = 0;
public City() {
        // Take random texture
        super(Assets.citiesTextureRegions[new SecureRandom().nextInt(6)]);
        // Init position
        if (cityCounter < 3) {
        setPosition(50  + ((Assets.screenWidth - 200) / 6) * cityCounter, Assets.screenHeight / 24);
        } else {
        setPosition(50  + ((Assets.screenWidth - 200) / 6) * cityCounter + 120, Assets.screenHeight / 24);
        }
        cityCounter++;
}

// Reset counter
public static void resetCounter() {
        cityCounter = 0;
}

// Get counter
public static long getCityCounter() {
        return cityCounter;
}
@Override
public void update(float deltaTime) {
        // Update explosion animation
        if (explosionTime >= 0)
        explosionTime += deltaTime;
}
@Override
public void draw(Batch batch) {
        if (Float.compare(explosionTime, -1) != 0) {
                // Draw animation
        if (explosionTime < 0) {
                explosionTime = 0;
                Assets.playSound(Assets.explosionSound);
        }
        TextureRegion keyFrame = Assets.citiesDestroyAnimation.getKeyFrame(explosionTime, false);
        if (Assets.citiesDestroyAnimation.getKeyFrameIndex(explosionTime) == 1) {
                batch.draw(keyFrame, getX(), getY(), getWidth(), 15);
                if (isAlive())
                cityCounter--;
                alive = false;
        } else {
                batch.draw(keyFrame, getX(), getY(), getWidth(), getHeight());
        }
        } else {
                // Draw city
        super.draw(batch);
        }
}
}

これでアプリケーションの共通部分が完成しました!これで、ゲームのAndroid 部分と iOS 部分の開発が始められます。



Android コード部分

アプリの Android 部分を開発するには、Android プロジェクト用の build.gradle (app/build.gradle)を編集します。 いくつかのlibGDX ライブラリに依存関係を追加する必要があります。 またネイティブのlibGDX ライブラリをプロジェクトに追加する必要もあります。 app/build.gradleに以下のコードを記述してください:

apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.2"
defaultConfig {
        applicationId "org.moe.libgdxmissilecommand.android"
        minSdkVersion 15
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
}
buildTypes {
        release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
}
}
configurations { natives }
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:23.1.1'
compile project(':common')
compile "com.badlogicgames.gdx:gdx-backend-android:$gdxVersion"
natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-x86"
natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-armeabi"
natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-armeabi-v7a"
}
// needed to add JNI shared libraries to APK when compiling on CLI
tasks.withType(com.android.build.gradle.tasks.PackageApplication) { pkgTask ->
pkgTask.jniFolders = new HashSet<File>()
pkgTask.jniFolders.add(new File(projectDir, 'libs'))
}
// called every time gradle gets executed, takes the native dependencies of
// the natives configuration, and extracts them to the proper libs/ folders
// so they get packed with the APK.
task copyAndroidNatives() {
file("libs/armeabi/").mkdirs();
file("libs/armeabi-v7a/").mkdirs();
file("libs/x86/").mkdirs();
configurations.natives.files.each { jar ->
        def outputDir = null
        if(jar.name.endsWith("natives-armeabi-v7a.jar")) outputDir = file("libs/armeabi-v7a")
        if(jar.name.endsWith("natives-armeabi.jar")) outputDir = file("libs/armeabi")
        if(jar.name.endsWith("natives-x86.jar")) outputDir = file("libs/x86")
        if(outputDir != null) {
        copy {
                from zipTree(jar)
                into outputDir
                include "*.so"
        }
        }
}
}

これで、Android アプリケーション用のMain クラスを作成できます。 それではMainという名前のクラスを作成しましょう。 AndroidManifest.xml ファイルを編集してアクティビティ(Main クラス)を追加します。 以下が改定した AndroidManifest.xml です。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.moe.libgdxmissilecommand.android">
<application android:allowBackup="true" android:label="@string/app_name"
        android:icon="@mipmap/ic_launcher" android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
        android:name=".Main"
        android:label="@string/app_name"
        android:screenOrientation="landscape"
        android:configChanges="keyboard|keyboardHidden|orientation">
        <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
        </activity>
</application>
</manifest>

次に、Main クラスを編集して libGDX Android アクティビティを作成します。 これは難しくはありません。 Main クラスの親クラスとしてcom.badlogic.gdx.backends.androidAndroidApplicationを追加し、onCreate メソッドを上書きする必要があります。

package org.moe.libgdxmissilecommand.android;
import android.os.Bundle;
import com.badlogic.gdx.backends.android.AndroidApplication;
import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration;
import org.moe.libgdxmissilecommand.common.MissileCommand;
public class Main extends AndroidApplication {
@Override
protected void onCreate (Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        AndroidApplicationConfiguration config = new AndroidApplicationConfiguration();
        initialize(new MissileCommand(), config);
}
}

最後に、ゲームで使う画像や音声やその他素材を追加します。 これらのファイルは ここからダウンロードできます (全てのリソースはこのプロジェクトから取得でき、MITライセンスで配布されています)。 これらのファイル (アーカイブ)をダウンロードした後、Android アプリケーションのソースディレクトリツリー内でassets という名前のディレクトリを作成します。 これらダウンロードしたファイルをassets フォルダへ配置します。

./img/libgdx9.png

おめでとうございます! Android ゲームが完成しました。これでゲームをビルドして実行できます! アプリを実行すると以下のスクリーンショットが表示されます。

./img/libgdx10.png



iOS コード部分

まずは、Multi-OS Engine モジュールを新規に作成します。 File > New >Multi-OS Engine Moduleの順でクリックしてください。 その後、新規モジュールのテンプレートとして Single View Application を選びます。

./img/libgdx11.png

Nextをクリックします。次のウィンドウでは、Xcode プロジェクト名、プロダクト名、組織名、その他のようないくつかのモジュールパラメータを設定します。 Xcode プロジェクト名とプロダクト名にはLibGDXPlain と設定してください。

./img/libgdx12.png

次のウィンドウでは、新しいモジュールの名前をiosなどのように設定します。Finish ボタンをクリックしてモジュールを作成します。

./img/libgdx13.png

モジュールが作成されたら、リソースから全てのレイアウトとストーリーボードを削除できます。 また、source ディレクトリから全てのクラスのuiパッケージを削除します。

次に、libGDX ライブラリを組み込むことでiOS モジュール用のbuild.gradle ファイルを変更します。

buildscript {
repositories {
        mavenCentral()
        maven {
        url uri(System.getenv("MOE_HOME") + "/gradle")
        }
}
dependencies {
        classpath 'org.intel.gradle:xRTGradlePlugin:1.0'
}
}
apply plugin: 'xrt'
configurations { natives }
// Extracts native libs (*.a) from the native-ios.jar and places them
// under build/libs/ios/.
task copyNatives << {
file("xcode/native/ios/").mkdirs();
configurations.natives.files.each { jar ->
        def outputDir = null
        if (jar.name.endsWith("natives-ios.jar")) outputDir = file("xcode/native/ios")
        if (outputDir != null) {
        FileCollection fileCollection = zipTree(jar)
        for (File libFile : fileCollection) {
                if (libFile.getAbsolutePath().endsWith(".a") && !libFile.getAbsolutePath().contains("/tvos/")) {
                copy {
                        from libFile.getAbsolutePath()
                        into outputDir
                }
                }
        }
        }
}
}
dependencies {
compile fileTree(dir: 'lib', include: '*.jar')
compile project(":common")
compile "com.badlogicgames.gdx:gdx-backend-moe:$gdxVersion"
natives "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-ios"
}
xrt {
}
buildApp.dependsOn copyNatives

またInfo.plist (module_name/xcode/project_name/Info.plist)から、メインストーリボードと画面向きを検出するためのいくつかの情報を削除します。 以下が変更前のオリジナルのInfo.plist です:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
               "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1">
<dict>
        ...
        <key>UISupportedInterfaceOrientations</key>
        <array>
        <string>UIInterfaceOrientationPortrait</string>
        <string>UIInterfaceOrientationLandscapeLeft</string>
        <string>UIInterfaceOrientationLandscapeRight</string>
        </array>
        <key>UISupportedInterfaceOrientations~ipad</key>
        <array>
        <string>UIInterfaceOrientationPortrait</string>
        <string>UIInterfaceOrientationLandscapeLeft</string>
        <string>UIInterfaceOrientationLandscapeRight</string>
        </array>
        ...
        <key>UIMainStoryboardFile</key>
        <string>MainUI</string>
        ...
</dict>
</plist>

以下が変更後の Info.plist です:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
               "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1">
<dict>
        ...
        <key>UISupportedInterfaceOrientations</key>
        <array>
        <string>UIInterfaceOrientationLandscapeLeft</string>
        <string>UIInterfaceOrientationLandscapeRight</string>
        </array>
        <key>UISupportedInterfaceOrientations~ipad</key>
        <array>
        <string>UIInterfaceOrientationLandscapeLeft</string>
        <string>UIInterfaceOrientationLandscapeRight</string>
        </array>
        ...
</dict>
</plist>

Main クラスを変更して libGDX iOS アクティビティを作成します。

package org.moe.libgdxmissilecommand.ios;
import com.badlogic.gdx.backends.iosmoe.IOSApplication;
import com.badlogic.gdx.backends.iosmoe.IOSApplicationConfiguration;
import org.moe.libgdxmissilecommand.common.MissileCommand;
import org.moe.natj.general.Pointer;
import org.moe.natj.objc.ann.ObjCClassName;
import org.moe.natj.objc.ann.Selector;
import ios.uikit.c.UIKit;
@ObjCClassName("Main")
public class Main extends IOSApplication.Delegate {
   protected Main(Pointer peer) {
           super(peer);
    }
        @Selector("alloc")
        public static native Main alloc();
        public static void main(String[] args) {
                UIKit.UIApplicationMain(0, null, null, Main.class.getSimpleName());
        }
        @Override
        protected IOSApplication createApplication() {
                IOSApplicationConfiguration config = new IOSApplicationConfiguration();
                return new IOSApplication(new MissileCommand(), config);
        }
}

今度は Xcode プロジェクトを変更します。プロジェクト名を右クリックして、 Multi-OS Engine > Open Project in Xcodeの順に選びます。

./img/libgdx14.png

Xcode プロジェクトが開き、以下の画面が表示されます。:

./img/libgdx15.png

Resources ディレクトリ内にあるMainUI.storyboardを削除します。

次に、Xcodeへリソースを追加します。 Resources ディレクトリをマウスで右クリックし、プロジェクトへ追加するファイルを選びます。 Next, find our resources in our project (You can use resources from you android module or download from here). All resources were taken from this project and they are distributed under MIT license) and add it to the Xcode project. 結果は以下のようになります。

./img/libgdx16.png

Build Settings > Linking >Other Linker Flags にあるLinker Flagsを変更してXcode プロジェクトへlibGDXLibrary を追加し、そこに以下のパラメータを追加してください。:

-force_load "$(SRCROOT)/native/ios/libObjectAL.a"
-force_load "$(SRCROOT)/native/ios/libgdx.a"
./img/libgdx17.png

最後に、必要なフレームワークをアプリケーションに追加します。以下のフレームワークを追加する必要があります:

  • AudioToolbox
  • AVFoundation
  • CoreFoundation
  • CoreGraphics
  • Foundation
  • OpenAL
  • OpenGLES
  • QuartzCore
  • UIKit

We need to add these to Build Phases > Link Binary With Libraries.

./img/libgdx18.png

おめでとうございます!iOS ゲームとアプリケーションが完成しました。iOS ゲームの以下のような スクリーンショットが表示されます。

./img/libgdx19.png



アプリをデバッグする

これで、libGDX アプリケーションではどのようにデバッグ方法するのかを試すことができるようになりました。 libGDX のデバッグプロセスは特別なものではないので、通常のMulti-OS EngineアプリのようにlibGDX ゲームをデバッグできます。 まず、いくつかブレークポイントを設定します。

例えば、共通コード部分にブレークポイントを二つ設定してみましょう。 まずは15行目のゲーム作成メソッド(create()) の開始時にブレークポイントを設定します(MissleCommand.java:MissleCommand.create():15)。

./img/libgdx20.png

二つ目のブレークポイントはMainMenuScreen.java ファイルの55行目(描写メソッドの開始時)に設定します。(MainMenuScreen. MainMenuScreen.draw():55)。

./img/libgdx21.png

これでデバッグを使ってアプリケーションを実行できます。 デバッグ中は、プログラムのステップオーバーやステップインや再開などをフル活用してコードをデバッグできます。 また、アプリケーションのデバッグに役立つスタックトレースも使用できます。

./img/libgdx22.png

前のステップ時点での変数の値を見るにはスタックトレースを使用します。以下のように値を見ることができます。

./img/libgdx23.png

Resources.zip とMulti-OS のサンプルをダウンロードしてください。

esources.zip はここでダウンロードできます。

Multi-OS Engine の全サンプルは ここ(Java)ここ(Kotlin)でダウンロードできます。



まとめ

Multi-OS Engine を使ったLibGDXの開発では、以下のような多くの利点があります。:

  • アプリケーションの大半は共通部分です。
  • その共通部分では使い慣れた Java の構文を使用します。
  • 強力なiOS シミュレーターとAndroid エミュレーターを使って、二つの対照を平行してデバッグできます。