このページでは、 Intelの Multi-OS Engine テクニカルプレビューと libGDX ライブラリを使ってJavaゲームを作る方法について説明します。
以下のスクリーンショットは、Android端末上で動作してるゲームとApple iOS端末で動作しているゲームのものです。 どちらのOS上でも、ゲームは同じ様な見た目と挙動をしています。これはどのようにしてできるのでしょうか?
あなたがlibGDXについてあまり知らないのであれば、このチュートリアルがゼロから学び始める際の助けになるでしょう。それでは始めましょう。画面を下へスクロールしてください!
まず最初に、以下のアプリケーションをインストールします:
システムパスに 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 をクリックしてください。
次に、プロジェクトの設定をします。例えば、アプリケーション名にはLibGDXMisslecommand と設定します。
Android アプリケーションにアクティビティを追加しないでください。 Finishをクリックします。
こうしてプロジェクトが作成されました! それでは、アプリケーションの共通部分を開発しましょう。
アプリの共通部分を作成するには、Java ライブラリのタイプを持つAndroid Studioモジュールを新規に追加する必要があります。Java Libraryを選んでNextをクリックします。
新しいモジュールとパッケージと共通クラスの名前を設定します。MissileCommandという名前でJavaクラスを作成しましょう。
これが完了すると、以下のようなファイルツリーが表示されます。:
まず、メインの 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 プロジェクト用の 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.androidのAndroidApplicationを追加し、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 フォルダへ配置します。
おめでとうございます! Android ゲームが完成しました。これでゲームをビルドして実行できます! アプリを実行すると以下のスクリーンショットが表示されます。
まずは、Multi-OS Engine モジュールを新規に作成します。 File > New >Multi-OS Engine Moduleの順でクリックしてください。 その後、新規モジュールのテンプレートとして Single View Application を選びます。
Nextをクリックします。次のウィンドウでは、Xcode プロジェクト名、プロダクト名、組織名、その他のようないくつかのモジュールパラメータを設定します。 Xcode プロジェクト名とプロダクト名にはLibGDXPlain と設定してください。
次のウィンドウでは、新しいモジュールの名前をiosなどのように設定します。Finish ボタンをクリックしてモジュールを作成します。
モジュールが作成されたら、リソースから全てのレイアウトとストーリーボードを削除できます。 また、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の順に選びます。
Xcode プロジェクトが開き、以下の画面が表示されます。:
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. 結果は以下のようになります。
Build Settings > Linking >Other Linker Flags にあるLinker Flagsを変更してXcode プロジェクトへlibGDXLibrary を追加し、そこに以下のパラメータを追加してください。:
-force_load "$(SRCROOT)/native/ios/libObjectAL.a" -force_load "$(SRCROOT)/native/ios/libgdx.a"
最後に、必要なフレームワークをアプリケーションに追加します。以下のフレームワークを追加する必要があります:
We need to add these to Build Phases > Link Binary With Libraries.
おめでとうございます!iOS ゲームとアプリケーションが完成しました。iOS ゲームの以下のような スクリーンショットが表示されます。
これで、libGDX アプリケーションではどのようにデバッグ方法するのかを試すことができるようになりました。 libGDX のデバッグプロセスは特別なものではないので、通常のMulti-OS EngineアプリのようにlibGDX ゲームをデバッグできます。 まず、いくつかブレークポイントを設定します。
例えば、共通コード部分にブレークポイントを二つ設定してみましょう。 まずは15行目のゲーム作成メソッド(create()) の開始時にブレークポイントを設定します(MissleCommand.java:MissleCommand.create():15)。
二つ目のブレークポイントはMainMenuScreen.java ファイルの55行目(描写メソッドの開始時)に設定します。(MainMenuScreen. MainMenuScreen.draw():55)。
これでデバッグを使ってアプリケーションを実行できます。 デバッグ中は、プログラムのステップオーバーやステップインや再開などをフル活用してコードをデバッグできます。 また、アプリケーションのデバッグに役立つスタックトレースも使用できます。
前のステップ時点での変数の値を見るにはスタックトレースを使用します。以下のように値を見ることができます。
Resources.zip とMulti-OS のサンプルをダウンロードしてください。
esources.zip はここでダウンロードできます。
Multi-OS Engine の全サンプルは ここ(Java) と ここ(Kotlin)でダウンロードできます。
Multi-OS Engine を使ったLibGDXの開発では、以下のような多くの利点があります。: