【Android Studio】ゲームアプリ開発入門 第4回「タイマーで画像を動かす」
ここまでの記事はこちら
前回は、タップするたびに青いボックスが少しだけ上に動くようにしました。
今回はタイマーを使って
- 画面をタップしている間は上に移動
- 画面から指を離すと下に移動
のように、青いボックスを上下に動かしてみます。
解説
1. タイマーでボックスを動かす
毎秒・毎分など、一定間隔で処理を実行するときはタイマーを使います。
MainActivity.java を開いて、4・5・18~28行目を追加します。
必要な import
必要な import は3つです。
import android.os.Handler;
import java.util.Timer;
import java.util.TimerTask;
タイマーについて
タイマーの処理は少し複雑にみえるかも知れませんが、分解して見てみましょう。
まずは timer.schedule(…) の部分です。
schedule(TimerTask task, long delay, long period)
TimerTask task | 実行する処理(タスク) |
long delay | タスクを実行するまでに待機する時間(ミリ秒で指定) |
long period | タスクの実行間隔(ミリ秒で指定) |
このゲームでは「待機時間は無しで、20ミリ秒毎にタスクを実行する」と設定しています。
Timer task
次に、実行するタスク TimerTask task の部分です。
新しい TimerTask を作成して、run() メソッドを用意します。run() メソッドの中には繰り返し実行する処理を書きます。
changePos メソッド
繰り返し実行する処理は changePos メソッドです。(この後書いていきます。)
changePos メソッドには
- 画像の位置を変更
- スコアラベルを更新
をするための処理を書きます。
20 ミリ秒毎に changePos() メソッドを呼び出して画面を更新していきますが、Android 開発では「メインスレッド外で UI を変更することはできない」という決まりがあります。
言い換えると「TimerTask からはスコアラベルを更新できない」ということです。
このとき使うのが Handler です。
Handler
handler.post で UI スレッドに Runnable を渡して UI を更新するという方法で、changePos() メソッドを実行します。
Handler を使わないとOnly the original thread that created a view hierarchy can touch its views. というエラーメッセージが表示されます。
このエラーが出た場合は、タイマー処理を見直してみてください。
*このエラーは API 26 以上の実機やエミュレータでは表示されませんが、API 25 以下の場合はアプリが強制終了となるのでご注意ください。
changePos() メソッドを用意
先ほど用意したタイマーで20ミリ秒毎に実行する changePos() メソッドを書きます。
5~7行目を追加します。* onTouchEvent 内の box.setY(boxY); は削除してください。
タッチイベントの処理を変更
現在は、画面をタップするごとに青いボックスが少しだけ上に移動するようになっています。
ここからは
- 画面をタップしている間は上に
- 画面に触れていない時は下に
青いボックスが動くようにします。
6行目を追加します。
changePos メソッドと onTouchEvent メソッドを以下のように書き換えます。
action_flg は「画面をタップしているか、タップしていないか」を判定するために使います。
onTouchEvent メソッドでは
- 画面をタップしたら action_flg を true
- 画面から指を離したら action_flg を false
にします。
ボックスの位置を変更する changePos メソッドでは
- action_flg が true だったら boxY を上へ
- action_flg が false だったら boxY を下へ
移動させています。
アプリを実行
ここでアプリを実行してみます。
青いボックスが消えてしまいましたね。
onCreate メソッドでタイマーをスタートしているので、画面が描画された時には青いボックスが下に落ち続けてしまっていることが原因です。画面をタップし続けるとまた上がってきます。
画面をタップしてからタイマーが起動するように修正していきましょう。
2. タイマー開始のタイミングを修正
ゲーム開始の判定も boolean 型の変数を使います。
2行目を追加します。
onTouchEvent メソッドを以下のように書き換えます。
* onCreate 内の startLabel.setVisibility(View.INVISIBLE); とタイマー処理は削除してください。
start_flg は
- false の場合はゲーム開始前
- true の場合はゲームプレイ中
となります。
onTouchEvent メソッドでは、初めて画面がタップされた時に
- start_flg を true にする
- startLabel を消す
- タイマーを開始
という処理を行います。
ゲームが始まって start_flg が true になると、21~27行目の action_flg の判定処理が行われるようになります。
startLabel の非表示について
startLabel.setVisibility(View.INVISIBLE);
startLabel.setVisibility(View.GONE);
TextView や ImageView などの要素を非表示にするには setVisibility メソッドを使います。
INVISIBLE は非表示にするだけ、GONE は完全に消すという違いがあります。
どちらを使っても問題ありませんが、ゲームが始まったら startLabel は不要なので GONE で完全に消してしまいましょう。
アプリを実行
アプリを実行して、このように動いていれば成功です。
今のままでは、青いボックスが画面の外に移動できてしまいます。
次はここを修正していきましょう。
3. 青いボックスが画面から出ないようにする
ここで必要になるのが
- frameLayout の高さ
- 青いボックスの Y 座標
- 青いボックスのサイズ
の3つです。
変数の用意
5・6行目を追加します。
7~11行目を追加します。 * onCreate 内の boxY = 500.0f; は削除してください。
必要な import
import android.widget.FrameLayout;
boxSize
青いボックスの画像は正方形なので box.getHeight() で高さだけ取得しています。
なぜ onCreate でサイズを取得しないのか?
「どうして FrameLayout のサイズを onCreate メソッドで取得しないのか?」と思われる方がいるかもしれません。
理由は onCreate メソッド内ではビューの描画が完了していないからです。
ビューの描画が完了していない状態で frameLayout の高さや画像サイズを取得しようとしても 0 が返ってきてしまいます。
そのため、ビューの描画が確実に完了している onTouchEvent メソッド内でサイズを取得しました。
box の Y 座標である boxY も同じ理由で onTouchEvent メソッド内で取得しています。
changePos メソッド
最後に changePos メソッドに 9・11行目を追加します。
9行目
if (boxY < 0) boxY = 0;
boxY が 0 より小さくなった場合は、青いボックスが frame の外に出ている状態です。
frame から出ないように boxY は 0 未満にならないようにします。
11行目
if (boxY > frameHeight – boxSize) boxY = frameHeight – boxSize;
青いボックスが画面の一番下にある時、boxY は frame の高さからボックスの高さを引いた値になります。
これ以上 boxY の値が大きくなると画面から出てしまうので、frameHeight – boxSize よりも大きくならないようにします。
アプリを実行
アプリを実行してください。
青いボックスが画面から消えなければ成功です!
上手く動かない場合は、一番下にここまでのコードを貼っているのでご確認ください。
次に行うこと
少しずつゲームらしくなってきましたね!次回はオレンジ・ピンク・ブラックボールを動かしていきましょう。
ここまでのコード
- MainActivity.java
最近Androidでの開発にチャレンジをはじめて、こちらのサイトで勉強させていただいています
1点疑問なのですが、boxYにはonCreateで値を入れてますが、
onTouchEventで改めてboxY = box.getY()としているのはどのような理由なのでしょうか
試しにコメントアウトしても同じような動きに見えたので質問です
take さん、サイトを見つけていただきありがとうございます
onCreate 内の boxY は以下の記事で「仮の値」と書いているとおり、一時的に 500.0f の位置にしていただけのものです。
https://codeforfun.jp/android-studio-catch-the-ball-3/#4142
onCreate ではビューの描画が完了していないので boxY の位置を取得しようとしても 0 になってしまいます。
ですので、start_flg を実装するまでの間、一時的に仮の値を使っていました。
不要になった onCreate 内の boxY は、次のページの一番最初に削除しているので、そちらもご確認ください。
https://codeforfun.jp/android-studio-catch-the-ball-5/
Saraさん、丁寧な回答ありがとうございます!
もう少し進めた上で質問するべきでした、、、
私の書き方も分かりにくかったので、今後修正させて頂きます!
ぜひ最後までゲーム開発楽しんで頂ければ幸いです
こんにちは、
この記事で紹介されているTimerTaskとクイズアプリを組み合わせて、クイズ画面に制限時間を表示する機能(90から毎秒1ずつ減らしていく処理)を実装しようとしているのですが、クイズ画面に遷移すると、画面が一瞬描画された後にホーム画面に戻されます。一応デスクトップPCでも試して同様のことが起きたので、スペック不足ではないと思います。
調べてみても原因が分からないです…アドバイスいただけると嬉しいです。
以下、ソースコード(一部省略)です。
当サイトを見つけて頂きありがとうございます!
setText メソッドには文字列をセットする必要がありますが、timercount を int 型のままセットしていることが原因かと思います。
timerLabel.setText(timercount);
これを
timerLabel.setText(timercount + "");
としてみてください。
実装できました!
この部分で長時間苦戦していたので本当に助かりました。
丁寧に教えていただきありがとうございます。
ご報告ありがとうございます。
実装できたとのこと良かったです!