AndroidStudioのKotlinバージョンアップ?でビルドは通るけどテストしようとすると落ちるように……

Actorの話をする予定だったのがupdateのサジェスチョンをホイホイ通したらスクリーンショットをとろうとして起動するとエラーを吐くように。

 

エラーメッセージは以下

Error:Error converting bytecode to dex:
Cause: Dex cannot parse version 52 byte code.
This is caused by library dependencies that have been compiled using Java 8 or above.
If you are using the 'java' gradle plugin in a library submodule add
targetCompatibility = '1.7'
sourceCompatibility = '1.7'
to that submodule's build.gradle file.
...while parsing kotlin/collections/CollectionsJRE8Kt.class

 

build.gradleがjre8だと失敗するのでjre7を指定すれば直る

// compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"

 

本来のネタはまた明日…

とりあえずサンプルをKotlinに変換する

westplain.sakuraweb.com

のソースをそのまま使ってしまいましょう。実行画面はこんな感じです。

f:id:turanukimaru:20170915231347p:plain

さてこのサンプルは当然ながらJavaなのでKotlinに変換する必要があります。

とはいっても、対象のファイルを選択してからメニュー>Code>convert Java File to Kotlin Fileを選択するだけです。

 

f:id:turanukimaru:20170915231703p:plain

無事Kotlinに変換されました。ソースは最下段。

 

とはいえ、このサンプルはそのまま使えません。FEHのコピーを作るには盤面の駒を操作しなければならず、タッチしたところにキャラを表示するだけのこのコードはほとんど役に立たないからです。

 if (Gdx.input.isTouched) {
val touchPos = Vector3()
touchPos.set(Gdx.input.x.toFloat(), Gdx.input.y.toFloat(), 0f) //タッチしたところの取得
camera!!.unproject(touchPos) // タッチしたところをLibGDX座標系(上下逆)に変換
bucket!!.x = touchPos.x - 64 / 2 //Imageの座標を変更する。
}

これがシューティングだったら雨の代わりに敵を表示して、バケツを自機にして弾が出るようにすればそれでいいのですが。

 

そこでActorを使います。

Actorとは文字通り俳優です。

Actorを使うことで、

「男優さん右に移動して」とか「男優さんと女優さんは同じ動きをして」とか「1秒右に移動してから一回転して」とか指示を出すように操作することができます。駒を動かすには駒がタッチされたとかドラッグされてどこに置かれたか、とかの判定が必要になりますが、これもActorに対してEventListenerを追加するだけでよくなります。というわけで次回はきっとActorの話です。

 

package com.mygdx.game

import ..
class MyGdxGame : ApplicationAdapter() {
private var dropImage: Texture? = null
private var bucketImage: Texture? = null
// private Sound dropSound;
// private Music rainMusic;
private var batch: SpriteBatch? = null
private var camera: OrthographicCamera? = null
private var bucket: Rectangle? = null
private var raindrops: Array<Rectangle>? = null
private var lastDropTime: Long = 0

override fun create() {
// load the images for the droplet and the bucket, 64x64 pixels each
dropImage = Texture(Gdx.files.internal("droplet.png"))
bucketImage = Texture(Gdx.files.internal("bucket.png"))

// load the drop sound effect and the rain background "music"
// dropSound = Gdx.audio.newSound(Gdx.files.internal("drop.wav"));
// rainMusic = Gdx.audio.newMusic(Gdx.files.internal("rain.mp3"));

// start the playback of the background music immediately
// rainMusic.setLooping(true);
// rainMusic.play();

// create the camera and the SpriteBatch
camera = OrthographicCamera()
camera!!.setToOrtho(false, 800f, 480f)
batch = SpriteBatch()

// create a Rectangle to logically represent the bucket
bucket = Rectangle()
bucket!!.x = (800 / 2 - 64 / 2).toFloat() // center the bucket horizontally
bucket!!.y = 20f // bottom left corner of the bucket is 20 pixels above the bottom screen edge
bucket!!.width = 64f
bucket!!.height = 64f

// create the raindrops array and spawn the first raindrop
raindrops = Array()
spawnRaindrop()
}

private fun spawnRaindrop() {
val raindrop = Rectangle()
raindrop.x = MathUtils.random(0, 800 - 64).toFloat()
raindrop.y = 480f
raindrop.width = 64f
raindrop.height = 64f
raindrops!!.add(raindrop)
lastDropTime = TimeUtils.nanoTime()
}

override fun render() {
// clear the screen with a dark blue color. The
// arguments to glClearColor are the red, green
// blue and alpha component in the range [0,1]
// of the color to be used to clear the screen.
Gdx.gl.glClearColor(0f, 0f, 0.2f, 1f)
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT)

// tell the camera to update its matrices.
camera!!.update()

// tell the SpriteBatch to render in the
// coordinate system specified by the camera.
batch!!.projectionMatrix = camera!!.combined

// begin a new batch and draw the bucket and
// all drops
batch!!.begin()
batch!!.draw(bucketImage!!, bucket!!.x, bucket!!.y)
for (raindrop in raindrops!!) {
batch!!.draw(dropImage!!, raindrop.x, raindrop.y)
}
batch!!.end()

// process user input
if (Gdx.input.isTouched) {
val touchPos = Vector3()
touchPos.set(Gdx.input.x.toFloat(), Gdx.input.y.toFloat(), 0f)
camera!!.unproject(touchPos)
bucket!!.x = touchPos.x - 64 / 2
}
if (Gdx.input.isKeyPressed(Keys.LEFT)) bucket!!.x -= 200 * Gdx.graphics.deltaTime
if (Gdx.input.isKeyPressed(Keys.RIGHT)) bucket!!.x += 200 * Gdx.graphics.deltaTime

// make sure the bucket stays within the screen bounds
if (bucket!!.x < 0) bucket!!.x = 0f
if (bucket!!.x > 800 - 64) bucket!!.x = (800 - 64).toFloat()

// check if we need to create a new raindrop
if (TimeUtils.nanoTime() - lastDropTime > 1000000000) spawnRaindrop()

// move the raindrops, remove any that are beneath the bottom edge of
// the screen or that hit the bucket. In the later case we play back
// a sound effect as well.
val iter = raindrops!!.iterator()
while (iter.hasNext()) {
val raindrop = iter.next()
raindrop.y -= 200 * Gdx.graphics.deltaTime
if (raindrop.y + 64 < 0) iter.remove()
if (raindrop.overlaps(bucket!!)) {
// dropSound.play();
iter.remove()
}
}
}

override fun dispose() {
// dispose of all the native resources
dropImage!!.dispose()
bucketImage!!.dispose()
// dropSound.dispose();
// rainMusic.dispose();
batch!!.dispose()
}
}

 

一応Javaのソース

package com.mygdx.game;
import ..
public class MyGdxGame extends ApplicationAdapter {
private Texture dropImage;
private Texture bucketImage;
// private Sound dropSound;
// private Music rainMusic;
private SpriteBatch batch;
private OrthographicCamera camera;
private Rectangle bucket;
private Array<Rectangle> raindrops;
private long lastDropTime;

@Override
public void create() {
// load the images for the droplet and the bucket, 64x64 pixels each
dropImage = new Texture(Gdx.files.internal("droplet.png"));
bucketImage = new Texture(Gdx.files.internal("bucket.png"));

// load the drop sound effect and the rain background "music"
// dropSound = Gdx.audio.newSound(Gdx.files.internal("drop.wav"));
// rainMusic = Gdx.audio.newMusic(Gdx.files.internal("rain.mp3"));

// start the playback of the background music immediately
// rainMusic.setLooping(true);
// rainMusic.play();

// create the camera and the SpriteBatch
camera = new OrthographicCamera();
camera.setToOrtho(false, 800, 480);
batch = new SpriteBatch();

// create a Rectangle to logically represent the bucket
bucket = new Rectangle();
bucket.x = 800 / 2 - 64 / 2; // center the bucket horizontally
bucket.y = 20; // bottom left corner of the bucket is 20 pixels above the bottom screen edge
bucket.width = 64;
bucket.height = 64;

// create the raindrops array and spawn the first raindrop
raindrops = new Array<Rectangle>();
spawnRaindrop();
}

private void spawnRaindrop() {
Rectangle raindrop = new Rectangle();
raindrop.x = MathUtils.random(0, 800-64);
raindrop.y = 480;
raindrop.width = 64;
raindrop.height = 64;
raindrops.add(raindrop);
lastDropTime = TimeUtils.nanoTime();
}

@Override
public void render() {
// clear the screen with a dark blue color. The
// arguments to glClearColor are the red, green
// blue and alpha component in the range [0,1]
// of the color to be used to clear the screen.
Gdx.gl.glClearColor(0, 0, 0.2f, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

// tell the camera to update its matrices.
camera.update();

// tell the SpriteBatch to render in the
// coordinate system specified by the camera.
batch.setProjectionMatrix(camera.combined);

// begin a new batch and draw the bucket and
// all drops
batch.begin();
batch.draw(bucketImage, bucket.x, bucket.y);
for(Rectangle raindrop: raindrops) {
batch.draw(dropImage, raindrop.x, raindrop.y);
}
batch.end();

// process user input
if(Gdx.input.isTouched()) {
Vector3 touchPos = new Vector3();
touchPos.set(Gdx.input.getX(), Gdx.input.getY(), 0);
camera.unproject(touchPos);
bucket.x = touchPos.x - 64 / 2;
}
if(Gdx.input.isKeyPressed(Keys.LEFT)) bucket.x -= 200 * Gdx.graphics.getDeltaTime();
if(Gdx.input.isKeyPressed(Keys.RIGHT)) bucket.x += 200 * Gdx.graphics.getDeltaTime();

// make sure the bucket stays within the screen bounds
if(bucket.x < 0) bucket.x = 0;
if(bucket.x > 800 - 64) bucket.x = 800 - 64;

// check if we need to create a new raindrop
if(TimeUtils.nanoTime() - lastDropTime > 1000000000) spawnRaindrop();

// move the raindrops, remove any that are beneath the bottom edge of
// the screen or that hit the bucket. In the later case we play back
// a sound effect as well.
Iterator<Rectangle> iter = raindrops.iterator();
while(iter.hasNext()) {
Rectangle raindrop = iter.next();
raindrop.y -= 200 * Gdx.graphics.getDeltaTime();
if(raindrop.y + 64 < 0) iter.remove();
if(raindrop.overlaps(bucket)) {
// dropSound.play();
iter.remove();
}
}
}

@Override
public void dispose() {
// dispose of all the native resources
dropImage.dispose();
bucketImage.dispose();
// dropSound.dispose();
// rainMusic.dispose();
batch.dispose();
}
}

とりあえずLibGDXを取ってきてセットアップします。

libgdx

セットアップと言ってもgdx-setup.jarを落として実行するだけです。

f:id:turanukimaru:20170913234521p:plain

AndroidStudio、つまりIDE用のセットアップ項目もあるのでこれもチェック。

f:id:turanukimaru:20170913234900p:plain

Generateを押してプロジェクトを作成。

 

AndroidStudioを起動して

f:id:turanukimaru:20170913235335p:plain

作成したプロジェクトを開けるだけ

f:id:turanukimaru:20170913235700p:plain

まずはタッチしてキャラを動かすために公式のサンプルを作成するところですかね。

 

公式ドキュメントの非公式翻訳。実にありがたい。

westplain.sakuraweb.com

あと電子書籍の解説本も買いましたがいまいちそのまま使えるような本ではなかったので省略。

むしろQiitaのLibGDX記事のほうが役に立ちました。

まずはユースケースシナリオ

いざゲームを作ると言ってもどこから手を付けていいか分からないものです。

そこで、まずシナリオを考えます。ゲームのストーリーではなくユースケースシナリオ、つまりユーザがこのゲームとどう関わるのか、どういう手順で遊ぶのか、というシナリオです。

 

これは遊び方をそのまま書くだけでいいので簡単ですね。

 

・事前条件

 最大4ユニットから成る部隊を編成してMAPに挑む

 

・事後条件

 勝利するか敗北するかして戦闘が終わり

 

・メインシナリオ

 画面にMAPを表示して、敵味方それぞれのユニットを配置する。

 ユーザ側のターンを開始する。

 ユーザはユニットを移動させる。移動できる範囲は地形とユニットの移動タイプによる。また、移動範囲から更に攻撃範囲が広がり、攻撃範囲でもよい。

 ユニットが移動した先に別のユニットがいた場合、敵ならば攻撃し味方ならば補助スキルを発動させる。このとき攻撃・補助可能位置にユニットを移動させる。攻撃や補助スキルが使えないときは移動できない。

 ユニットが移動したら行動済みとなる。

 ユーザの全ユニットが行動済みになるかターン終了ボタンを押した場合はユーザのターンが終了し、敵のターンになり同じように敵のユニットを移動させる。

 ユニットの攻撃によりHPが0になったユニットはマップから排除する。

 敵のユニットを全て排除したらユーザの勝利であり、逆にユーザのユニットを全て失った場合はユーザの敗北とする。

 

情報の表示は後に回すとして、ユーザが直接かかわるのはこんな感じですかね。

これにより、次にやることが解ります。それはもちろんユーザがユニットを操作する準備、つまりUIの準備です。

これは直接書いてもいいのですがとても大変なのでUI用のフレームワークを使うことにしましょう。Unityでもいいのですが今回はLibGDXを使います。UnityだとAndroidじゃなくてUnityになってしまいますからね。

FEHのシミュレータを作る

ファイアーエムブレムヒーローズ(以下FEH)というスマホゲームがあります。つい先月リリース1/2周年を迎えました。

fire-emblem-heroes.com

 

このFEHと言うゲームがどういったゲームであるのかはまあ遊んでみればわかるとして、このゲームは

  • ルール・ダメージなどの計算方法が明記されている
  • キャラクターのデータもほぼ固定であり個体差も決まっている
  • 確率がない

つまり将棋やチェスなんかと同じ完全情報ゲームです。完全情報ゲームは全てが公開されているのでそれをそのまま実装すれば見た目以外は同じものが作れるはずです。今のところこんな感じ。

f:id:turanukimaru:20170911220616p:plain

画像はネットで拾ってきたものなのでこのまま公開すると著作権違反になってしまいますがまあ後で差し替えればいいでしょう。実際、この画面でもマルスの画像はいらすとやのメジェドに置き換えています。ルキナの陰になってて見えませんが。

 

まだまだ造り途中ですが製作の手順みたいなものを書いていこうと思います。ゲームを作りたいけどどこから手を付けていいかわからない、という人には参考になるかもしれませんしひょっとしたら詳しい人から突っ込みがもらえるかもしれません。

 

なお私はゲームプログラマではなくAndroidもKotlinも初めて扱うので記事やコードの正しさ自体はあまり信頼がおけないものとなるはずです。指摘いただいたら都度修正しようとは思いますが。