Canvas だけでもアプリはできる!
ノート PC に、Visual Studio Community 2022 の C++ によるモバイル開発機能のインストールと、 その時点で最新だった Android Studio のインストールを完了しましたので、 Android アプリの開発環境が整ったということになりました。
「Android Studio で文字を自由に描画する」 で、独自のビューを作成して Empty Activity に設定し文字を描画、 続く「Android Studio で画像を自由に描画する」 で、PNG 画像をプロジェクトに取り込んで、描画関数 onDraw が引数で受け取った Canvas に画像を描画しました。 拡大・縮小もできていますし、画面タッチの検出もできました。
「Android Studio でタイマーによる自動画面更新」ではタイマーをセットし、 一定間隔で独自に作成したビューを無効化して、ユーザーの操作がなくても画面を更新できるようにし、 「Android Studio で効果音を鳴らす」では、クイズで正解したときなどに鳴らす 短い効果音の再生処理も実装しました。
「Android Studio で画面を準備する」では画面の事前準備として、 アクションバーの非表示、 画面を縦固定、 背景画像の描画、 そして画面のタッチからブラウザでウェブサイトを開く、といった項目を実装し、 「Android Studio でボタン表示&入力」で、 座標管理クラスを作成してボタンを表示し、どのボタンがタッチされたかを認識できました。
「Android Studio でゲームクラスを更新」では 「Android Studio でボタン表示&入力」で作成したゲームクラスに処理を追加し、 正解の選択肢をタッチしたときに正解音を鳴らし、赤い丸を表示し、自動的に次の問題に進む処理を実装しました。
ここではさらにコードを追加し、スタートボタンを押すとゲームが始まり、規定の問題数が解答されれば終了と判断します。 また、開始から終了まで時間を測り、ゲーム中画面に進行状況と使用タイムを表示するようにします。
ここまで作成したコードへの追記の形となっていますが、ゲームクラス以外は、すべて必要と言うわけではありません。
過去すでに Android Studio を別のマシンで使用して 「素因数分解トレーニングアプリ」 を開発していますが、改めてちゃんとアプリを作りたいな、と思い、リスタートしています。
なお、使用している Android Studio は、2022 年 5 月中旬にインストールした、2021.2.1 Patch 1 の Chipmunk です。
なお、本サイトの
ご利用に際しては、必ずプライバシーポリシー(免責事項等)をご参照ください。
投稿 August 18, 2022
ここまで「Canvas だけでもアプリはできる!」シリーズを追ってずっとコードを入れてきた場合、 アプリを起動するといきなり問題が表示されています。 4 つのボタンが表示され、その中から素数をタップすれば正解判定が行われます。 正解であれば音が鳴り、赤い丸が表示されて次の問題に進みますが、 いくつ答えても終わりはありません。
少なくともゲームとしては、スタートとエンドがあるべきです。
まずはいつものように、
Android Studio の左端にある Resource Manager を選択し、 Drawable タブが選ばれていない場合は Drawable をクリックします。
エクスプローラーを開き、Drawable に追加したいファイルを
もしすぐに Drawable にあるリストが更新されない場合は、いったん Color など別のタブを選んでから Drawable に戻ると見えます。
ゲーム中かどうかの判定に使うため、ゲームクラス saGame に boolean 型の変数 bInPlay を定義していました。 その様子は「ゲームクラスを作成しておく」に書いています。
しかし、描画を行うのは独自のビュー FlashSosuuView ですので、bInPlay の定義をビューに移動しようと思います。 FlashSosuuView のコンストラクタの前に、定義を移動します。
本来は、 (今回のように超シンプルな画面構成ではなく)もっと複雑になるなら、saGame のように別クラスで描画を行うか、 あるいは描画専用のクラスを作成して 「開始前画面の描画関数」とか「問題描画関数」とか「終了画面描画関数」とわけて実装するほうがわかりやすいとは思います。
今回のように View で描画すると、小規模でない場合は View の onDraw 関数が大きくなって読みにくくなりますので、 なんでもそこで描画しないほうがいいのですが、今は強行します。
まずはゲーム中かどうかを判断するための変数 bInPlay の定義を View に移動しました。 これでこのビューからしかゲームの状態がわからないことに注意しておきましょう。 どうしても手段がないわけではありませんが、せめて public で定義すれば(正統的ではない)アクセス(参照)は可能です。
bInPlay は値を false で初期化していますので、アプリ起動時は「ゲーム中ではない」です。
まずは View の最初の部分で、start ボタン画像を bmpStart に読み込んでおきます。 大きな画像だったり、たまにしか使わない画像はずっと保持しておくとよくありませんが、 スタートボタンはアプリを通して利用しますので、保持しておきます。
描画関数 onDraw で、bInPlay を参照して、画面に描画するものを切り替えます。 画面全体を黒で消し、背景を描画するまでは共通としました。
背景描画のあと、bInPlay が false であれば、if 文内の処理で画面を描画し(このあと記述します)、return しています。 ゲーム中で bInPlay が true ならこの if 文には入りませんので、今までのコードで描画を行うことになります。
タッチの判定にスタートボタンの座標が必要ですので、追加します。
onWindowFocusChanged 関数で、座標を設定しておきます。
それを使って、描画関数 onDraw の bInPlay が false の分岐で start ボタンを描画します。
ここまでで画面にスタートボタンが表示されますので、タッチでゲームを開始できるようにします。 タッチイベント処理関数 onTouchEvent に、bInPlay の判断を追加します。
タッチ終了時の処理 ACTION_UP で、まずウェブサイトを開く位置の判断をしていますが、 これはゲーム状態に関係なく有効にしたいので、このままです。 ただ、そのあと else if でゲーム中に表示される 4 選択肢ボタンかどうかの判定をしていますので、 ウェブサイトを開くタッチの最後に break を入れて ACTION_UP の処理を終了するようにして、else を外せるようにしました。
そしてそのあとに、bInPlay を参照してゲーム中かそうではないかで分岐するようにしました。
スタートボタン上でのタッチかどうかを判定するコードを追加しました。 x 座標と y 座標を別の if 文にしていますが、読みやすくするためだけで、それ以上の意味はありません。
スタートボタンのタッチであれば、bInPlay を true にしてゲームが開始されたこととし、
ちなみに、ゲーム開始前でも 200ms タイマーが効いていますが、正解ボタンを選択した時刻 tmStart が 0 ですので、 onTimerProc は何もせず、悪さはしません。
これで、ゲームの終わりはないものの、始まりはできたことになります。 実機を接続して試します。
アプリを起動するとスタートボタンが表示され、タッチするとゲームが始まるようになりました。
投稿 August 18, 2022
次はゲームの終わりを実装します。
連続 10 問出題して、それをクリアするまでの時間を測ります。
時間を計測する前に、10 問出題したら終わりである判断までをまず入れます。 が、開発中にテストで 10 問だと時間がかかりますので、まずは 3 問として、 出題数はいつでも簡単に変えられるようにします。
現在のゲームの流れとしては、 タッチイベント処理関数 onTouchEvent で選択肢ボタンのタッチを認識し、 正解なら opCorrect 関数を呼び出しています。
opCorrect 関数は赤い丸の描画を指示、正解の効果音を鳴らし、 tmStart にその時刻を設定することにより正解処理の終わりを判定できるようにしています。 そして 200ms ごとに呼び出される、独自のビューの onTimerProc 関数で 2000 ミリ秒経過したかを判断し、 経過した場合には次の問題を作成し、赤い丸を非表示にするようなコードが入っています。
ですので、onTimerProc 関数の正解処理の終わり時刻で今何問目かを把握し、 全 10 問なら 10 問目が終わった時には次の問題に進まずに、終了の処理を行う、ということになります。
必要なのは現在の問題番号と、全部で何問出題するか(定数)の 2 つです。 本来はゲームクラス saGame に入れるべきかとは思いますが、ビュー 1 つのみの小規模なゲームですので、 唯一の独自のビュー FlashSosuuView で定義します。
nNum は出題済み(解答済み)の問題数で、0 から始まる予定です。 出題数 nTotal は final で定義されていますので、あとで書き換えはできません。
タッチイベント処理関数 onTouchEvent に実装したスタートボタン処理で、問題番号 nNum を 0 にします。 nNum は 0 のときは 1 問目を出題中、1 のときは 2 問目を出題中・・・となります。
正解して次の問題に進む処理は onTimerProc 関数にありますので、 最後の部分で問題番号をプラスします。 このときすでに終わりであれば、bInPlay を false にすることで、スタートボタンのある画面に戻します。
ゲーム中であるかを表す bInPlay を false に設定して invalidate() により画面が更新されると、選択肢ボタンは表示されません。 なお、invalidate 関数は即時の画面描画指示ではなく、画面更新の予約です。 ですのでこの順でも問題なく、次の描画のタイミングでスタートボタンが描画されることになります。
実機で試すと、無事に 3 問でスタートボタンのある画面に戻りました。
投稿 August 18, 2022
スタートボタンのタッチでゲームが始まり、 onTimerProc 関数で全問題出題済みの判断ができるようになりましたので、 その間の時間を計測するのは、難しくはありません(正解処理の 2 秒も使用時間に含めます)。
まずはゲームを開始した時刻を保持するための変数 tmGameStart を、 独自のビュー FlashSosuuView で定義します。 基準時刻からの経過時間ミリ秒値を入れますので、long 型です。
onTouchEvent 関数にあるスタートボタンのタッチ処理で、 スタートボタンをタッチした時刻を tmGameStart に記録します。
Date クラスをこのように作成すると、 date に現在日時が設定されますので、getTime 関数で規定の時刻からの経過ミリ秒数を取得できます。
200ms ごとに呼び出される onTimerProc 関数にある、正解して次の問題に進む処理の最終問題終了処理で、 ゲーム中かどうかを示すフラグ bInPlay を false にすると同時に、 tmGameStart をゲームで使用した時間(ミリ秒)に更新します。 tmNow はこの処理に入った時刻がすでに設定されていて、tmGameStart はスタートボタンを押した時刻ですので、その差がゲームで使用した時間となります。
つまり tmGameStart は、アプリ起動時には 0、 ゲーム開始で開始時刻が設定されますので bInPlay が true のときはゲーム開始時刻、 そしてゲーム終了時に bInPlay を false にしつつゲームで使用した時間を設定していることになります。 このような組み合わせに慣れていない場合は、別変数を用意するほうが安全かもしれません。
投稿 August 18, 2022
規定の問題数を完了すると、「おめでとう」表示などなくスタートボタンのある画面に戻っていますので、 せっかくクリアまでの時間を計測しても、確認できません。
スタートボタンの下に、最終プレーのタイムと、ハイスコアを表示してみましょう。
描画関数 onDraw で、最終プレーのタイムを描画します。 tmGameStart には、クリア後には使用した時間(ミリ秒)が設定されています。 1 回もプレーしていない場合は、初期値 0 のままですので、0 のときは描画しないことにします。
スタートボタンの下に、適当なサイズで、センタリングして描画しています。
ここにはプレー中ではないとき、つまり bInPlay が false のときしか来ませんから、 tmGameStart に 1 以上の値があるときは、前回のプレー時間となります。 0 のときはアプリ起動直後ですので、描画を行いません。
paint で描画設定を行い、rcBounds に必要なサイズを取得しています。 それを画面の横サイズ cxView から横方向にはセンタリング、 縦方向はスタートボタンの底辺 rcStart.bottom より「2 行下」に(テキストの下部のラインを)設定しています。
なお、描画する文字列 str を作成する部分のコードが図からはみだして見えませんが、 この部分については「ミリ秒の描画」で改善しています。 読み進めると到達しますが、お急ぎの場合はリンクから確認してください。
実機で試すと、ちゃんとクリア後に表示されました。
ハイスコアはどうでしょう?
やはり独自のビュー FlashSosuuView で tmHighScore を定義し、0 で初期化しておいて、クリア時に更新処理を入れます。
200ms ごとに呼び出される onTimerProc 関数にある、正解して次の問題に進む処理の最終問題終了処理で、 ゲームに使用した時間を tmGameStart を設定した後、ハイスコアかどうかの判断を行います。
ハイスコアを保持する変数 tmHighScore は初期値が 0 ですので、値が 0 のときは初回プレーであり、常にハイスコアになります。
tmHighScore にすでに値が設定されている場合は、今回のスコア tmGameStart と比較して、 今回のスコアがハイスコアを上回っている場合、tmHighScore を tmGameStart で上書きします。
続いて描画です。 前回の成績の描画のコードのあとに、ハイスコア描画用のコードを追加しました。
最後のプレーの秒数が有効な場合はハイスコアも有効ですので、if 文を別に用意する必要はありません。 「ハイスコア」の文字の次の行に、ハイスコア記録を描画しています。
なお、ここでも描画する文字列 str を作成する部分のコードが図からはみだして見えませんが、 この部分についても「ミリ秒の描画」で改善しています。 読み進めると到達しますが、お急ぎの場合はリンクから確認してください。
実機で試しました。
うまく機能しました。 ただ、ハイスコアはどこにも保存されていませんので、アプリを終了すると忘れられます。
なお、ハイスコアを保存した場合は、今は考えられていない「ハイスコアあり、最終プレータイムなし」の状況が生まれます。 このときには if 文を別々に用意する必要がでてきます。
投稿 August 18, 2022
一番下のボタンとウェブサイトへのリンクの間に少しスペースがありますので、今何問目が表示されているのかと、 その時点での経過時間を表示しましょう。
ゲージは画面の横幅いっぱいに、赤いバーを表示し、正解した分だけ左から暗くすることとします。 描画関数 onDraw で Canvas を受け取っていますから、Canvas の関数で簡単にいけますね。
経過時間を上に描画し、その下にバーを表示することとします。
まずは描画関数 onDraw の最後、選択肢 4 ボタンの数字を描画したあとにコードを追加します。 現在の経過秒数を tmPassed に計算し、選択肢 4 ボタンの下に描画を行います。
秒 + 3 桁のミリ秒で表示するようにしています。 変数 ms に、経過時間 tmPassed の下 3 桁を設定しています。 % 演算子は、そのあとの 1000 で割った余りを計算するものです。 ですので、ms は 0 〜 999 の値となります。
続く if はいったんとばして、最後の 4 行、描画したい文字列 str に必要なサイズを計算して rcBounds に取得し、 左右方向にはセンタリングされるよう、上下方向には選択肢 4 ボタンの下になるよう、描画しています。
上の図の if の部分、ミリ秒の描画についてです。
ミリ秒の部分を描画するのに、tmPassed % 1000 のように経過時間の下 3 桁を取り出して、 String.valueOf(ms) のように文字列に変換して描画しようとしていました。
ミリ秒部分が 100 より大きい時は問題ないのですが、例えば 1 秒 001 だと tmPassed は 1001 で、 tmPassed % 1000 は 1 になりますので、String.valueOf(1) は 1、 画面上では 1.1 秒となってしまいますが、実際には 1.001 秒とならなくてはいけません。
そこで桁数により切り分けて文字列を作成するようにしたのが、上のコードです。
スタートボタンの下に表示する最終プレーの使用時間やハイスコア表示でも同じようになりますから、 いつもこの if による切り分けはよくありませんので、関数を作成しました。
getMillisecStr という名前を付けた関数です。 今は独自のビュー内で定義していますが、だんだん規模が大きくなったら、ユーティリティクラスのようなどこからでもアクセスできるクラスに定義すべきでしょう。
内容としては同じで、引数で受けた tm のミリ秒部分を取り出して ms にセットしています。 その桁数に応じてあたまに 0 を付与した文字列にして返しています。
C/C++ では sprintf 関数で指定するフォーマットに "%03d" のように 「あたまを 0 で埋めた 3 桁の数値」という指定ができますが、 もしかすると Java でも同じような指定があるかもしれませんので、気がついたら追記します。
この関数を使うと、次のように書き換えられますから、すっきりします。
ゲーム開始前の画面、スタートボタンの下に表示するタイムも書き換えられます。
ハイスコア描画も書き換えます。
リアルタイムで描画しなければ、ハイスコアなどが "15.28 秒" のように表示され、違和感がありませんから気付かなかったかもしれません。
また、バーも同じように描画しますが、 ひとまずは出題番号 nNum や全問題数 nTotal に関係なくバーを描画してみます。 コードの位置は、上記経過時間のすぐ次です。
Canvas の drawRect 関数で、簡単に四角形を描画できます。 直前で色指定、不透明(最初の 2 文字、16 進数で FF)、赤(次の 2 文字、16 進数で FF)を指定しています。 続く 2 文字は緑要素、最後の 2 文字は青要素ですが、00 ですから「なし」ということです。
実機で試しました。
表示は問題ないのですが、タイマーで画面を自動更新しなくなったので、ボタンをタッチした時にしか経過時間が更新されません。
タイマー処理で、ゲーム中 bInPlay に限り、画面を無効化する invalidate() を実行することにします。 これで画面書き換えになりますので、およそ 200ms ごとに経過時間が更新されることになります。
実装済みのタイマー処理関数 onTimerProc で invalidate() を呼び出すこととしています。 この関数は独自に実装したものですから、このシリーズを順に追っていない場合には見つかりません。 「Android Studio でタイマーによる自動画面更新」で作成しています。
関数の先頭で、「ゲーム中なら画面の書き換えを指示」という流れになっていますが、 この invalidate 関数は指示するだけで書き換えは即時実行ではありませんので、 関数内のどこで実行しても問題ありません。
最後に、画面の横幅いっぱいに赤で表示した出題数バーを、クリアした分をグレー表示するようにします。 グレーにする範囲はクリア済みの問題数 nNum と全出題数 nTotal を使用すれば、nNum * cxView / nTotal で計算できます。 rcBounds のその他の値はそのままですので、赤で塗った部分を上書きするイメージです。 黒を指定しつつ透過度を低めていますので、「グレー」ではなく暗い赤となるはずです。
実機で動作させます。 全 3 問の 1 問クリアしたときにはこのようになり、OK です。
なお、paint.setColor(0xC0000000)」で半透明の黒で上書きして暗い表示を実現していますが、 このあと setColor(0xFF000000) のように不透明に戻さないと、 次回未設定の描画時にも半透明が適用されてしまいます。
具体的には、背景描画の前に不透明を設定すべきでした。
「使ったら戻す」よりは「使う前に設定する」のほうが正統でしょうね。
では、nTotal を 10 に書き換えて試します。
もし出題数をコードに直接数字で書き込んでいた場合は、今これだけでも 正解後に次に進むかの判断部分と、バーの描画部分 の 2 か所を書き換える必要があります。 このように定数として定義していれば、更新し忘れ部分が残ることによるバグを防ぐことができます。
なお、問題数を選択できるようにするなら、 final 定義すると書き換えができませんから、final ではなくして、nTotal を書き換えればよいでしょう。 ただ、問題数によりハイスコアの扱いが面倒になりますので、注意が必要です。
Canvas だけでもアプリはできる! #1
Android Studio で文字を自由に描画する
このシリーズの最初です。 アプリ独自のビューを作成し、テキストを描画するまでのコードを作成しています。
Canvas だけでもアプリはできる! #2
Android Studio で画像を自由に描画する
用意した PNG 画像を表示したり、画面タッチに追従したりするコードを作成しています。
Canvas だけでもアプリはできる! #3
Android Studio でタイマーによる自動画面更新
MainActivity にタイマーを設定して、一定間隔で処理を呼び出すコードを作成しています。
Canvas だけでもアプリはできる! #4
Android Studio で効果音を鳴らす
効果音のような短い音を鳴らすコードを作成しています。
Canvas だけでもアプリはできる! #5
Android Studio で画面を準備する
アクションバーの消去、画面の縦固定指定、背景画像描画、そしてタッチでウェブサイトを開くコードを作成しています。
Canvas だけでもアプリはできる! #6
Android Studio でボタン表示&入力
座標管理クラスを作成して画面に適切なサイズのボタンを表示し、どれが押されたか判断するタッチ処理を実装しています。 トースト表示や独自クラスの作成についても書いています。
Canvas だけでもアプリはできる! #7
Android Studio でゲームクラスを更新
正解のボタンをタッチしたとき効果音を鳴らす等の正解処理を行い、2 秒後に次の問題に自動的に進む処理を実装しています。 不正解ならトーストで理由を表示します。
Android Studio で全画面プロジェクトを作成する
Empty Activity では画面上部に邪魔になり得るタイトル表示領域がありますが、 Fullscreen Ativity なら、それがなくなるのか、新規プロジェクトを作成して確認しています。
Android Studio で ActionBar を非表示にする
Empty Activity では画面上部に邪魔になり得るタイトル表示領域がありますが、 プロジェクト内の設定の書き換えにより、別プロジェクトにしなくても非表示にできるようです。
Android Studio で Native C++ プロジェクトを作成する
目的としている C++ ネイティブコードを併用する Activity は、 Native C++ プロジェクトを作成すれば、比較的簡単に着手できそうです。
Android Studio で AdMob プロジェクトを作成する
もうひとつの目的としては、AdMob でアプリ内に広告を入れる、です。 AdMob Ads Activity なら簡単に実装できるのでしょうか?
C++ ネイティブアプリでアプリ名を設定する方法の詳細を検討しています。
Android 開発に関する記事をまとめた Android 開発トップ もご覧ください。
以降の数学の基本となる素因数分解の基本部分を、ひたすらトレーニングするための JavaScript コードについて書いています。
因数分解の公式の一部について、ひたすらトレーニングするための JavaScript コードについて書いています。