第 13 章 ゲームによる実践 著:高橋憲一、山下武志 13-1 ゲームによる実践 (1) 著:高 橋 憲 一 、山 下 武 志 LESSON ここでは、これまでに学んできたことを組合わせて、ゲーム KEYWORD ゲーム 高速描画 というジャンルでひとつのアプリの形として仕上げることを スプライト 実践します。 一見かなりの入力量があるように感じられます クラスの継承 が、テキストの手順に従って自動補完機能をうまく活用しな がら進めればそれほどではありません。 SurfaceView SurfaceHolder Canvas Thread この節を学ぶとできること 高速描画用のSurfaceViewを用意する ゲームでは、通常のアプリとちがってキャラクターや背景をすばやく 描画する必要があります。そのための仕組みについて学びます。 自分キャラクターを1つ表示する ゲームの基本要素となるキャラクターを表示させてみましょう。 84 13 -1-1 ゲームに必要な要素 ゲームの開発に使われるライブラリ 3Dゲームを開発する際には、最近は「Unity」等のゲームエンジン、2Dのゲームな 第 ら 「Cocos2d-x」や「AndEngine」等のライブラリを使用することが多いのですが (2D 13 でもUnityを使用するケースも増えています)、ここでは基本的な仕組みを学ぶため、 ゲ ー ム に よ る 章 AndroidのSDKのみを使用して開発します。 実 践 リソース(画像、音) ゲームで使用する2Dの画像のことをスプライ トと呼びます。スプライ トとして、自分で 動かすキャラクターの画像や、敵キャラクターのための画像、背景画像などの画像 ファイルが必要となります。また、実際のプレイを盛り上げるためには効果音も重要な 要素ですので、音声ファイルが必要となります。 13 -1-2 基本の仕様を考える スマートフォンで操作するゲームであるという観点から、ここでは片手でプレイできるシ ンプルな操作を考えてみます。横長の画面に自分が操作するキャラクターを表示し、 横から流れてくる障害物をよけるゲームにします。キャラクターの操作は、画面を指で タッチしている間はロケットをブーストして上昇し、指を離すと下降する、という2種類に 限定します。 13 -1-3 ゲームを描画する View の作成(SurfaceView) 新規プロジェクト作成 Android Studioの「File」メニューから、 「New Project...」 を選択して、新しい プロジェクトを作成します。次のようなダイアログが表示されるので、 「Application Name」に「FlyingDroid」 と入力します。 「Company Domain」には受講生のID に続いて「.techinstitute.jp」を入力します (図1)。ここでは「t051.techinstitute. jp」 としています。 85 図1:新規プロジェクトを作成するダイアログ 「Next」ボタンをクリックして、次の画面へ進みましょう。すると 「Target Android Devices」ダイアログに切り替わります。ここでは「Phone and Tablet」にチェックが 入っていることを確認し、 「Minimum SDK」の項目は「API18:Android 4.3 (Jel ly Bean)」 となっている状態で「Next」ボタンをクリックします (図2)。 図2: 「Phone and Tablet」 を選択し、 「API 18」 に設定する 86 次は「Add an activity to Mobile」画面です。ここでは「Blank Activity」 を 選択して「Next」 をクリックします (図3)。 第 13 章 ゲ ー ム に よ る 実 践 図3:Activityには 「Blank Activity」 を選択する 最後は「Customize the Activity」画面です (図4)。ここでは「Activity Name」 として「MainActivity」、 「Layout Name」には「activity_main」が設定 されているはずです。そのまま 「Finish」ボタンをクリックしましょう。 図4:アクティビティとレイアウトの名前を設定する 87 作成されたプロジェクトの確認 ここまでの操作で「FlyingDroid」 というプロジェクトが作成されました。その中味を 確認しておきましょう (図5)。 図5: 「FlyingDroid」 プロジェクト の構成要素 「app」の下の階層が開かれていない場合は、項目名の左側の右向き三角マー クをクリックして開きます。開いている項目には下向きの三角マークが付いています。 ・ Javaのソースコード 「java」 を開くと、 その下にはパッケージ名(この例では「jp.techinstitute.t051.fly ingdroid」) があります。この文字列は、プロジェクト作成時に「Company Doma in」 と 「Application Name」に指定した名前から自動的に生成されたものです。 さらにその下の階層には「MainActivity」があります。これがJavaのソースコード です。その中にコードを記述していくことになります。 ・ レイアウト 一方「res」の下の「layout」 を開くと、 「activity_main.xml」 というファイルがある はずです。これは画面のレイアウトを定義するためのものです。 カスタムViewクラスの作成 カスタムViewについては、第4章「た めしてわかるA n d r o i d のしくみ 」の 4-2-4「独自のViewを利用してみよ ゲーム画面をカスタムViewとして作成します。ここでは高速描画のためにSurface Viewを使います。 う」 も参照してください。 ・ クラスの追加 カスタムViewを作成するには、プロジェクトにクラスを追加します。まず「Project」 ビューのパッケージ名の部分で右ボタンクリックして開くメニューから「New」→ 88 「Java Class」 を選択します (図6)。 第 13 章 ゲ ー ム に よ る 実 践 図6:パッケージ名を右ボタンクリックして 「New」 → 「Java Class」 を選ぶ すると、 「New Java Class」ダイアログが開くので、 「Name」欄に「GameView」 と入力して「OK」 をクリックします。これがカスタムViewの名前になります。 図7:追加するクラスの名前を 「GameView」 に設定する その結果、空の「GameView」 クラスが生成され、ソースコードが編集可能な状 態になります (図8)。ここにクラスにコードを追加します。 図8:追加したクラスのソースコード 「GameView.java」 が自動的に開く まず「public class GameView」の後ろに、 「extends SurfaceView」 という記 述を加えます (図9)。これは「SurfaceView」 クラスを継承する (extends) という意 味です。 89 図9: 「public class GameView」 の後ろに 「extends SurfaceView」 を加える check! クラスの継承について ここでクラスの継承についておさらいしてみましょう。 今回は「android.view.SurfaceView」を継承して「GameView」 という新しいクラスを 作成しました。ベースとなるクラスを継承して新しいクラスを作るという概念を動物と 犬、そして猫の関係で見てみます (図10) 。 図10:動物と犬、猫の継承関係 犬も猫も動物としての基本的な性質を備えており、例えば 「鳴く」 という行為はどちらに も共通しています。ただし犬は「ワン」 と鳴くのに対し、猫は「ニャー」 と鳴くという違いが あります。また、 「四本足で歩く」 というのは両方に共通している行為です。 これを、ベースとなるクラス(Superclass)を継承して新しいクラスを作るという作業 に当てはめて考えてみると、動物は「Superclass」 で、犬や猫はそれを継承して作る新し いクラスということになります。犬と猫のクラスは「四本足で歩く」 という行為(メソッド) については、新たなメソッドを実装することなく、そのSuperclassが持っているものを使 うことができます。一方それぞれでやるべきこと (処理)が異なる「鳴く」 というメソッドで は、それぞれ継承した先で新たに実装することになります。 これを踏まえてandroid.view.SurfaceViewを継承してGameViewというクラスを作る ことを考えると、SurfaceViewが持つ多くの機能をそのまま活用しつつ、今回のゲームの 描画機能やタップに反応する部分の処理を独自に実装していけば良いということになり ます。このようなことは、他にも 「android.app.Activity」を継承して 「MainActivity」クラ スを作る場合にも当てはまります。 今追加したGameViewクラスは、 このままではコンパイルすることができません。なぜ なら、SurfaceViewクラスにはいくつかのコンストラクターを定義することが必要で、そ れらをまだ実装していないからです。 図9を見ると、クラス定義の先頭行に赤い波線が付いて、そこにエラーがあることを 示しています。その行の「SurfaceView」の部分にテキストカーソルを移動すると、そ の行の左上に「!」マークのオレンジ色の電球が表示されます。それをクリックするとメ ニューが表示されるので、 その中のいちばん上の項目「Create constructor matc 90 hing super...」 を選択します (図11)。 第 13 章 ゲ ー ム に よ る 図 1 1:オレンジ 色 の電球をクリックし て「Create constr uctor matching super...」 を選ぶ 実 践 その結果、 「Choose Super Class Constructor」 というダイアログが現れ、生成 するSurfaceViewのコンストラクターの候補が複数個表示されます (図12)。ここで は、 「Shift」キーを押しながら上の3つを同時に選択して「OK」 をクリックします。 図12: 「Shift」 キーを押しながら上の3つのコンストラクターを同時に選択する 「GameView.java」の編集 ここまでの操作で、 「GameView.java」には、選択した3つのコンストラクターが追 加されました (図13)。 91 図13:3つのコンストラクターが追加されたGameView.java このGameViewクラスには、コンストラクター以外にも実装すべきコードがあります。 スレッドは、複数のプログラムを同時 に実 行するための 仕 組みの1つで SurfaceViewは高速に描画するのに適しているのですが、そこに描画するための「ス す。スレッドについては、第12章で詳 しく学びました。 レッド」 を独自に実装する必要があるのです。そのために「GameThread」 クラスの定 義を追加します。 まずクラス定義の先頭と、最初のコンストラクターの間に以下のコードを追加します。 class GameThread extends Thread { SurfaceHolder surfaceHolder; boolean shouldContinue = true; public GameThread(SurfaceHolder surfaceHolder, Context context, Handler handler) { this.surfaceHolder = surfaceHolder; } @Override public void run() { while(shouldContinue) { Canvas c = surfaceHolder.lockCanvas(); draw(c); surfaceHolder.unlockCanvasAndPost(c); } } public void draw(Canvas c) { c.drawARGB(255, 0, 0, 0); } } GameThread gameThread; 92 このコードを記述する際に発生するエラーは、 「Alt」 + 「Enter」 をタイプするなどし て、そのつど解消しておきます。その際「Handler」 クラスについては、インポートする候 補が2つ表示されるので「android.os.Handler」の方を選びます (図14)。 第 13 章 ゲ ー ム に よ る 実 践 図14: Handlerについては 「android.os.Handler」 を選んでインポートする ここまでで、 「GameView.java」の先頭部分は、図15のようになります。 図15: GameThreadクラスを実装した 「GameView.java」 の先頭部分 次にパラメーターが2つのコンストラクター (GameView(Context context, Attr ibuteSet attrs)) の中味を以下のコードのように記述します。 93 public GameView(Context context, AttributeSet attrs) { super(context, attrs); SurfaceHolder holder = getHolder(); holder.addCallback(this); gameThread = new GameThread(holder, context, new Handler() { @Override public void handleMessage(Message msg) { super.handleMessage(msg); } }); } このコードを入力すると、 2行目の「holder.addCallback(this);」の「this」の部分 にエラーを示す赤い波線が表示されるはずです。ここでも、その「this」の部分にテキ ストカーソルを置くと、左上にオレンジ色の電球マークが表示されます。そこで、電球を クリックして表示されるメニューから 「Make 'GameView' implement 'andor id.view.SurfaceHolder.Callback'」 を選びます (図16)。 図16:addCallback(this)部分のエラーは、 「Make 'GameView'...」 選んで解消する その結果、GameViewクラスの定義の先頭行に、 「implements SurfaceHold er.Callback」 という新たな宣言が追加されます (図17)。これは、 「SurfaceHold er.Callback」 というインターフェース (97ページコラム参照) に定義されたメソッドを、 このクラスで実装しています、 という意味です。 図17:GameViewクラスの定義に 「implements SurfaceHolder.Callback」 が追加された 94 ところが、この追加によって、GameViewクラス定義の先頭行全体に赤い波線が 表示されるようになってしまいました。このエラーも、前と同様にしてオレンジ色の電球を 表示し、 それをクリックして解消できます。この場合は、 メニューから 「Implement Me thods」 を選びましょう (図18)。 第 13 章 ゲ ー ム に よ る 実 践 図18:GameViewクラス定義の先頭行のエラーは 「Implement Methods」 を選んで解消する 今回は、 「Select Methods to Implement」 というダイアログが表示されます (図 19)。 そこで、選択肢として表示される3つのメソッドをすべて選んでから 「OK」 をク リックします。 図19: 「Select Methods to Implement」 ダイアログでは3つのメソッドを選んで実装する 以上の操作の結果、GameViewクラス定義の先頭部分には、新たに3つのメソッ ド、 「surfaceCreated()」、 「surfaceChanged()」、 「surfaceDestroyed()」の定 義が挿入されました (図20)。 95 図20:GameViewクラス定義の先頭部分に、 3つのメソッド定義が追加された このうち、surfaceCreated()メソッドの中味としては、以下の1行のコードを記述しま す。 gameThread.start(); これは、SurfaceView が生成された時に描画用スレッドを開始するためのもので す。 また、surfaceDestroyed()メソッドの中味には、以下の1行を記述します。 gameThread = null; これは、SuraceView が破棄される際に、描画用スレッドを停止させるものです。 ここまでの作業で、追加された3つのメソッドのコードは、図21のようになっているは ずです。 96 第 13 章 ゲ ー ム に よ る 実 践 図21:SurfaceHolder.Callbackインターフェースのメソッドをこのように実装する check! インターフェース(Interface)について 継承とはまた別の方法で、同じ名前のメソッドを複数のクラスで実装することを強要で きるしくみがJavaにあります。 強要と書くとあまり良いイメージではないかもしれませんが、同じ名前で同じ引数を取 るメソッドがあるという前提条件が満たされることにより、オブジェクトのやり取りを行う 場合に 「この名前のメソッドを実装してもらえれば、適切なタイミングでこちらからそれを 呼び出しますよ」 ということを明確にできます。 今回のSurfaceViewを使う場合で言うと、 「SurfaceHolder.Callback」 というインター フェースに 「surfaceChanged」 、 「surfaceCreated」 、 「surfaceDestroyed」 の3つのメソッ ドがあり、 「GameView」 というクラスでそのインターフェースの各メソッドを実装しておく と、 「SurfaceView」 が生成された時にはsurfaceCreated、サイズが変更された時はsurfa ceChanged、破棄される時はsurfaceDestroyedが呼び出されるようになります。 レイアウト作成 レイアウトファイル「activity_main.xml」に、上で作成したGameViewを組み込 みます。 「activity_main.xml」は最初から開かれているはずですが、もし見当たらなけれ ば、プロジェクトの中の「res」 フォルダーの「layout」の下にある 「activity_main. xml」 をダブルクリックして開きます。 このファイルの中味がXML表示ではなくグラフィカ ルレイアウトになっている場合は、下のタブ「Text」をクリックして表示を切り替えます (図22)。 97 図22: 「activity_main.xml」 を開いてをXML表示する この中の<TextView>の部分をすべて削除して、代わりに以下のように書き加えま す。先頭のパッケージ名の部分は、各自が設定したパッケージ名に書き換えてくださ い。 <jp.techinstitute.t051.flyingdroid.GameView android:id="@+id/gameView1" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_centerHorizontal="true" /> また<RelativeLayout>のパディングに関する4つの属性(android:paddingXX XX) もすべて削除します。編集後のXMLは、図23のようになるはずです。 図23: 「activity_main.xml」 を、このように編集する 98 フルスクリーンの設定 画面を横向き (landscape) に固定し、フルスクリーン表示にする設定を追加しま す。そのためには「AndroidManifest.xml」 ファイルを開いて編集します (図24)。こ れは「app」 フォルダーの「manifest」の中にあります。 第 13 章 ゲ ー ム に よ る 実 践 図24: 「AndroidManifest.xml」 ファイルを開いて編集する この「AndroidManifest.xml」の<activity>∼</activity>の部分に、次の2 行を追加します。1行目がフルスクリーン、2行目が横向きの設定です。 android:theme="@android:style/Theme.NoTitleBar.Fullscreen" android:screenOrientation="landscape" この結果<activity>∼</activity>の部分は、図25のようになるはずです。 図25:編集後の 「AndroidManifest.xml」 の<activity>∼</activity>部分 99 MainActivity.javaの修正 次に、 「MainActivity.java」 を修正します。 AndroidStudioでは、生成されたActivityは標準的に「ActionBarActivity」 を継承しています (図26)。 図26:変更前のMainActivityはActionBarActivityクラスを継承している このActionBarActivityクラスには、特定のStyleを適用しなければ動作しないと いう特徴があり、 このままMainActivityを起動するとアプリが強制終了してしまいます。 そこで、MainActivityが継承しているクラスを通常の「Activity」へ変更します (図 27)。 図27:変更後のMainActivity。Activityクラスを継承させる 「MainActivity.java」の変更はこれだけです。変更後は、インポートするクラスも 「android.app.Activity」 となるようにしておきます。 100 13 -1-4 スプライト描画 リソースをダウンロードする まず、このプロジェクトで使用する画像ファイルなどをzipファイルにまとめたリソースを 第 13 ダウンロードします。 ダウンロードするファイルのURLは「http://goo.gl/jI1AjJ」です。 章 「jI1AjJ」の最初の小文字の「j」の次は、大文字の「I(アイ)」です。小文字の「l ゲ ー ム に よ る (エル)」ではありませんので注意してください。 ダウンロードしたら、zipファイルを展開し、 6つの画像ファイル (.png) をプロジェクトに 実 践 移動します。その際にはAndroid Studioの「Project」 ビューの左上のメニューで 「Project」 を選んでおく必要があります。 「Android」のままでは、 リソースファイルを追 加することができません。画像ファイルをまとめて選択し、ドラッグして「res」の下の 「drawable-hdpi」の上にドロップします (図28)。 図28:画像ファイ ル を 選 択 してド ラッグし、 「drawa ble-hdpi」 の上に ドロップする すると、 「Move」 というダイアログが表示され、移動先を確認してくるので、そのまま 「OK」 をクリックします (図29)。 図29:移動先を確認して 「OK」 をクリックする 101 さらに「Non-Project Files Access」 というダイアログが表示されます。ここではプ ロジェクト外にある画像ファイルをプロジェクトに追加する際に、それらをアンロックするこ とを確認します。 「Unlock files」が選択されている状態で「OK」 をクリックします (図 30)。 図30:プロジェクト外のファイルのアンロックを確認する 以上の操作で、6つの画像ファイルが、プロジェクトの「drawable-hdpi」フォル ダーに追加されました (図31)。 画像は Android でゲームを開発す るお手本として、Androidがこの世に 出て間もないころからある「 R e l i c a Island」のものを利用させてもらって います (URL https://code.goog le.com/p/replicaisland/)。この ゲームのソースコードは、画像ともに 「Apache 2.0 License」 というオー プンソースのライセンスで公開されて いるため、ここで利用することも可能 になっています。 図31:6つの画像 ファイル が「 d r a wable-hdpi」 に追 加された クラスの追加 はじめに自分のキャラクター「Droid」 を表示してみましょう。それを表示するために 「Droid」 クラスを実装します。ただし、他にも障害物やビームなどの描画オブジェクト を表示することを考えて、 それらに共通する機能は「AbstractGameObject」 クラスと して作成し、それを継承する形で、それぞれのスプライ トを表示するクラスを実装するこ とにします。 そこでまずは「AbstractGameObject」 クラスを、以前にGameViewを作成した 102 時と同様の手順でプロジェクトに追加します (図32)。 第 13 章 ゲ ー ム に よ る 実 践 図32:プロジェクトに 「AbstractGameObject.java」 を追加して編集する クラスの定義ファイル「AbstractGameObject.java」が追加されたら、クラス定 義の中味を以下のように編集します。 public abstract class AbstractGameObject { protected Drawable drawableImg; protected int width; protected int height; protected int x; protected int y; public AbstractGameObject(Context context, int resourceId, int width, int height) { drawableImg = context.getResources().getDrawable(resourceId); this.width = width; this.height = height; } public void draw(Canvas c, int x, int y) { drawableImg.setBounds(x, y, x + width, y + height); drawableImg.draw(c); } } 入力作業の途中で追加されるインポート文も含めて、 「AbstractGameObject. java」は図33のようになるはずです。 103 図33:編集後の 「AbstractGameObject.java」 の中味 先に説明した動物と犬、猫の関係に 置き換えると、ベースとなる動物だけ では 「鳴く」 というメソッドを持つことが 決まってはいても実装を持っていない ので、それだけではオブジェクトを生成 できません。それを継承した犬や猫は 「鳴く」 というメソッドの実装を持って いるのでオブジェクトを生成できる、と いうことになります。 クラス定義の先頭行の「public」 と 「class」の間に「abstract」を追加するのを 忘れないようにしましょう。これは、このAbstractGameObjectを抽象クラスにして、 AbstractGameObjectそのものからは新しいオブジェクトを生成できないようにしま す。新しいオブジェクトは、必ずこのクラスを継承して作ったクラスで生成します。 追加した5つのメンバー変数は、それぞれ画像描画用のDrawableオブジェクトを 保持するためのもの (drawableImg)、画像の幅と高さ (width、height)、描画位 置のX座標とY座標(x、y) を保持するためのものです。 コンストラクター「AbstractGameObject()」には、 4つの引数があります。 それぞれ リソースを読み出すために必要なContext オブジェクト (context)、画像を示すリ ソースID(resourceId)、表示の際に使用する縦横のサイズ (width、height) と なっています。 コンストラクターの内部では、 1行目で画像から描画のためのDrawableオブジェク トを生成し、 2行目と3行目で表示用の幅と高さをメンバー変数に保存しています。 draw()メソッドの中では、1行目で描画位置とサイズを設定し、2行目で実際に描 画しています。 104 Droid クラスの作成 次に、AbstractGameObjectクラスを継承するDroidクラスを新たに作成します。 前と同じようにして、プロジェクトに「Droid.java」 を追加してください。まずクラス定義 の先頭部分に「extends AbstractGameObject」 を追加します。これでAbstrac 第 tGameObjectクラスを継承するようになります。この記述を追加するとその行全体が 13 エラーとなるので、以前と同じようにオレンジ色の電球をクリックして「Create constru ゲ ー ム に よ る 章 ctor matching super」 を選択します (図34)。 実 践 図34: 「extends AbstractGameObject」 を追記してから 「Create constructor...」 を選ぶ その結果、標準的なコンストラクターがソースコードに追加されます (図35)。引数 の仕様は、AbstractGameObjectクラスに記述したものと同じであることに気付くで しょう。 図35:自動的にコンストラクターを追加した 「Droid.java」 ここでは、 「int resourceId」の引数は削除し、さらに、super()を呼び出している 部分の引数を以下のように変更します。2番目の引数として、直接Droid画像のリ ソースID(R.drawable.andou_diag01) を指定するようにしています。 105 public Droid(Context context, int width, int height) { super(context, R.drawable.andou_diag01, width, height); } その結果、Droidクラスの定義は図36のようになるはずです。 図36:編集後の 「Droid.java」 のDroidクラスの定義部分 GameView.javaの修正 今度は「GameView.java」 を修正し、上で作成したDroidクラスを使って描画で きるようにします。これから変更を加える 「GameThread」 クラスの変更前の状態を示 します (図37)。 図37:今回の変更前の 「GameView.java」 の 「GameThread」 クラス 106 ここに、 まず2つのメンバー変数を加えます。 Droid droid; static final int droidSize = 200; 第 1行目はDroidオブジェクトを保持するためのもので、2行目は表示の際のサイズを 13 定義するための定数です。 ゲ ー ム に よ る 章 次にコンストラクターには、上で追加したメンバー変数「droid」に、newで生成し たオブジェクトを代入するコードを追加します。 実 践 droid = new Droid(context, droidSize, droidSize); さらに、draw()メソッドの中に、 ドロイドを描画するためのコードを追加します。引数と して渡す「c」は、描画に使用するCanvasです。 2つ目と3つ目の「100」は、Droidを 仮置きする際のx、y座標です。 droid.draw(c, 100, 100); 以上の編集作業の結果、GameThreadクラスの実装は図38のようになりました。 107 図38:編集後のGameThreadクラス ここまでできたら、 とりあえず実行してみましょう。正しくできていれば、Androidデバイ スの画面の表示は、図39のようになるはずです。 図39:Droidが1つだけ表示されたゲーム画面 108 13-2 ゲームによる実践 (2) 著:高 橋 憲 一 、山 下 武 志 第 13 LESSON 章 KEYWORD ゲ ー ム に よ る フレーム ゲームというからには、ユーザーの操作によって画面上の 当り判定 実 践 オブジェクトが反応するという、インタラクティブな要素が必 要です。この節ではそうした機能を実装していきます。 この節を学ぶとできること 自分のキャラクターを操作できるようにする 画面をタップすることでキャラクターを移動できるようにする方法につい て学んでいきます。 敵のキャラクターを表示して動かす 敵キャラクターを用意し、あるパターンに沿って動くようにしていきます。 自分と敵がぶつかったかどうかを判定する 当たり判定を用いて、 ゲーム として成立するようなプログラム を作成します。 109 13 -2 -1 自分のキャラクターを操作する 自分で操作するキャラクターの動きを考えてみましょう。ここでは単純に、何もしな ければ少しずつ下降していき、場所は関係なく画面のどこかをタップしている間は上 昇するようにします。画面から指を離せば、 また下降します。 スプライトの下降 Droidオブジェクトを初期位置に表示すると、その後は自動的に画面上の下に 落ちていくようにする機能を追加します。 前の節で作成した「Droid.java」を編集します (図1)。 図1:今回の編集前のDroid.java ここに3つのメンバー変数と、 2つのメソッドを追加します。 メンバー変数は、 コンストラクター「Droid()」の前に追加します。 private int defaultX; private int defaultY; private float velocity = 2; defaultXとdefaultYはDroidの初期表示位置のX座標とY座標を保持する ためのものです。また、velocityは移動速度を保持します。 2つのメソッドsetInitialPosition()とdraw()は、コンストラクターの後に追加しま す。 110 public void setInitialPosition(int x, int y) { defaultX = x; defaultY = y; this.x = defaultX; this.y = defaultY; } フレームは、元来は絵の額縁という 意味ですが、コンピューターグラフィッ クでは、演算によって完成された画像 そのもの、アニメーションでは、キャラ public void draw(Canvas c) { draw(c, defaultX, y); y += velocity; } クターの動きを時間軸に沿って分解 して描いた個々のコマを表します。こ のようなゲームでも、1コマずつ描画 して、順に画面に表示しています。フ レームは、その中のすべての要素の 描画が完了するまで表示できないの で、描画内容が複雑になったり、コン ピューターの処理が遅いと、1つのフ 1つめのsetInitialPosition()は、その名の通り、表示の初期位置をX座標とY レームを用意するのに時間がかかり、 座標で指定するためのものです。2つめのdraw()は、毎フレーム呼び出されます。 なってしまいます。1秒間に表示可能 現在のY座標で指定した位置にDroidを描画し、次のフレームのためのY座標を アニメーションがカクカク動くように なフレームの数を 「フレームレート」 と 呼びます。フレームレートが高いほど、 現在のY座標に速度の値を足すことで計算しておきます。 滑らかなアニメーションになります。 以上の変更を加えた「Droid.java」は、図2のようになるはずです。 す。 逆に低いと動きの荒いものになりま メソッド名や変数名には、その機能や 目的を類推しやすいような名前をつけ ることが望ましいです。 図2:今回の変更後のDroid.java ここで画面の座標系を確認しておきましょう。図3のように、左上が原点(X, Y が 0, 0)、横方向は X、縦方向は Yで、Xの値を増やすと右に移動し、Yの値を 増やすと下に移動します。 111 第 13 章 ゲ ー ム に よ る 実 践 原点 ( 0,0 ) X Y 図3:ゲーム画面の座標系 次に「GameView.java」を編集し、上で「Droid.java」に追加した機能を呼 び出すコードを「GameThread」クラスに追加します。今回の編集前の状態を図 4に示します。 図4:今回の編集前のGameView.java まず、GameThreadのコンストラクター内に、Droidの初期表示位置を設定す るメソッドを呼び出すコードを追加します。 112 droid.setInitialPosition(100, 0); 続いてdraw()メソッドでは、droid.draw()メソッドの呼び出し部分を修正します。 第 描画位置を直接設定するものではなく、描画するためのCanvasのみを渡すものに 13 書き換えます。この結果、描画位置の指定はdroidオブジェクトの中での計算に任 ゲ ー ム に よ る 章 せることになります。 実 践 droid.draw(c); 以上の編集の結果、 「GameView.java」は図5のようになるはずです。 図5:今回の変更後のGameView.java ここでいったん実行してみましょう。ここまで正しくできていれば、 ドロイド君の画像が ゆっくりと下に移動していくはずです。しかし、 このままではどこまでも下降し、画面の外 に見えなくなってしまいます。 タッチで上昇させる 次に画面へのタッチ操作を検出して、タッチしている間はドロイド君が上昇し、指 を画面から離すと再び下降を始めるようにします。 113 再び「Droid.java」を編集します。 こんどは、 メンバー変数の定義方法を変更し、 また新たなメソッドを追加します。 private static final float DefaultVelocity = 2; private float velocity = DefaultVelocity; メンバー変数velocityは、定数として新たに定義する 「DefaultVelocity」を通 して初期化するようにしています。 新しいメソッド「uplift()」は、 コンストラクターの後に追加しましょう。 public void uplift(boolean on) { if (on) { velocity = -DefaultVelocity; } else { velocity = DefaultVelocity; } } このメソッドを、引数onにtrueをセットして呼ぶことで、velocityが負の値になりま す。すると、draw()メソッドの中で「y += velocity」で計算されるY座標は、徐々 に減っていきます。動作としてはドロイド君は上昇していくことになります。逆に、onに falseをセットして呼び出せば、velocityは正の値になります。その結果、Y座標は 徐々に増えていき、 ドロイド君は下降することになります。 以上の編集の結果、 「Droid.java」は、図6のようになるはずです。 図6:今回の変更後のDroid.java 114 あとは「GameView.java」の方で、タッチ操作に合わせてこのuplift()メソッドを 呼び出すようにすれば、期待する動作になるでしょう。そのため、 「GameView. java」の内部で定義している 「GameThread」クラスのコンストラクターの下に、新 たなメソッド「upliftDroid()」を追加します。 第 13 章 public void upliftDroid(boolean on) { droid.uplift(on); ゲ ー ム に よ る } 実 践 この結果、 「GameView.java」は図7のようになります。 図7:upliftDroid()メソッドを追加したGameView.java さらに、 このupliftDroid()メソッドを、 タッチ操作のイベントが発生した際に呼び出 すようにします。 そのためには、GameViewクラスに1つのメソッドを追加し、 さらにコン ストラクターを編集して、その追加したメソッドをタッチイベントのリスナーとして設定す るコードを加えます。 今回の編集前の「GameView.java」の後半部分を図8に示します。 115 図8:今回の編集前のGameView.javaの後半部分 まず以下のメソッドを追加実装します。この例では、3つめのコンストラクターの前 に記述しましたが、場所はどこでもかまいません。 private boolean dispatchEvent(MotionEvent event) { switch(event.getAction()) { case MotionEvent.ACTION_DOWN: gameThread.upliftDroid(true); return true; case MotionEvent.ACTION_UP: gameThread.upliftDroid(false); return false; default: return false; } } ここではswitch文を使い、event.getAction()により得られたアクションの種別に よって処理を分岐させています。その値が「MotionEvent.ACTION_DOWN」 なら、指を画面にタッチしているので、upliftDroid()メソッドにtrueをセットして呼び 出します。これはDroidを上昇させます。一方その値が「MotionEvent.ACTI ON_UP」なら、指を画面から離しているのでupliftDroid()メソッドにfalseをセットし て呼び出します。その結果Droidは下降することになります。 コンストラクター内にリスナーを設定する処理を加える作業では、Android Stu dioの自動補完を使って大部分の入力を省略できます。図9のように、 2つめのコン ストラクターの最後の部分に、 「setOnTouchListener(new On」 までを入力する 116 と補完候補がメニューとして表示されます。ここでは、いちばん上の「OnTouchLis tener [...] (android.view.View)」を選びます。補完候補が表示されない場合は 「Ctrl」 とスペースキーを同時に押してみましょう。 第 13 章 ゲ ー ム に よ る 実 践 図9:補完機能を利用してsetOnTouchListner()の呼び出しを記述する このようにして入力されるコードの「return」に続く部分を変更して、以下のように します。 setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { return dispatchEvent(event); } }); これにより、onTouch()メソッドが画面のタッチ操作のイベントが発生した際に呼 び出されるようになり、 さらにそこから上で記述したdispatchEvent()が呼び出されま す。 以上のGameView.javaの変更結果を図10に示します。 117 図10:今回の編集を加えた後のGameView.java ここまでできたら実際にアプリケーションを動かして、 タッチによる反応を確かめてみ ましょう。 移動範囲の制限 実際に動かしてみると、1つの問題に気付くでしょう。ドロイド君は上昇しても、下 降しても、画面の外にはみ出してしまうのです。それを防ぐ対策を施していきます。 具体的にはDroidクラスに、ドロイド君が移動可能な座標の範囲を制限するた めのメソッドを追加します。ただしこのメソッドは、この後に追加する敵キャラクターの クラスでも必要になるはずです。そこで、 そのメソッドを、 それらのクラスの継承元となる クラス 「AbstractGameObject」に追加して、共通に使用できるようにします。 「AbstractGameObject.java」を編集します。まず今回の編集前の状態を 確認しておきましょう (図11)。 118 第 13 章 ゲ ー ム に よ る 実 践 図11:今回の編集前のAbstractGameObject.java ここに4つのメンバー変数と、1つのメソッドを追加します。4つのメンバー変数は、 動作範囲の左、上、右、下の座標値を保持するものです。 protected int left; protected int top; protected int right; protected int bottom; 追加するメソッドは、追加したメンバー変数の値をセットするためのものです。コン ストラクターの下に記述しましょう。 public void setMovingBoundary(int left, int top, int right, int bottom) { this.left = left; this.top = top; this.right = right; this.bottom = bottom; } 以上の編集を加えた後の「AbstractGameObject.java」の状態を図12に 示します。 119 図12:今回の編集を加えたAbstractGameObject.java 実際にドロイド君を描画する際の範囲のチェックは、Droidクラスの中で実行しま す。そのための処理は、Droidクラスのdraw()メソッドの中に記述します。まず変更 前の状態を確認します (図13)。 図13:今回の変更前のDroidク ラスのdraw()メソッド ここに以下のコードを追加します。 if (y < top) { y = top; } else if (y > bottom){ y = bottom; } 120 この処理では、Y座標の計算後に、それが上方向(top)、および下方向(bott om) について範囲を超えるかどうかを調べ、 その場合にはそれぞれの限界値を代入 して、 その範囲外に描画されないようにしています。 以上の変更後のdraw()メソッドは、図14のようになります。 第 13 章 ゲ ー ム に よ る 実 践 図14:今回の変更後のDroidク ラスのdraw()メソッド さらにGameView側では、Droidの動作範囲をDroidオブジェクトにセットする 処理を追加します。 「GameView.java」の中の、やはり 「GameThread」クラスの コードを編集して、 2つのメンバー変数と1つのメソッドを追加します。 変更前の状態を確認しましょう (図15)。 図15:今回の変更前のGameView.javaのGameThreadクラス定義部分 GameThreadクラスのメンバー変数の宣言として、次の2行を追加します。 int width; int height; 121 新たなメソッドsetViewSize()は、 コンストラクターの後ろに追加しましょう。 public void setViewSize(int width, int height) { this.width = width; this.height = height; droid.setMovingBoundary(0, 0, width, height); } この処理では、メンバー変数に値をセットした後、droidオブジェクトのsetMovin gBoundary()メソッドを呼び出して、原点 (0, 0)と幅、高さをセットします。これでドロ イド君の動作範囲は、画面全体の範囲に制限されることになります。 以上の編集結果を確認しておきましょう (図16)。 図16:ここまでの変更を加えたGameView.java もう1つ、画面のサイズが変更されたときに、 このsetViewSize()メソッドが呼び出 されるような処理を記述します。具体的には、第1節でGameViewクラスに追加し たsurfaceChanged()メソッドの中に以下のコードを記述します。このメソッドは、画 面のサイズが変更されたときだけでなく、画面が初期化される際に、その幅と高さが 引数として呼び出されます。 gameThread.setViewSize(width, height); 122 変更を加えたsurfaceChanged()メソッドの部分を図17に示しておきましょう。 第 13 章 ゲ ー ム に よ る 実 践 図17:surfaceChanged()メソッドの中味を実装したGameView.java ここでまたアプリを実行してみましょう。上方向、下方向ともに画面の端でドロイド 君が止まってくれるでしょうか。ここまで正しくコードが入力できていれば、上方向は 問題ないはずです。下方向は...。画面の外に出てしまいますね。これは入力ミスで はなく、処理が1つ足りないため、Droidの画像の高さ分だけ下にはみ出してしまう のです。 「Droid.java」にコードを追加して、 この問題を解消しましょう。ここでは、コードを 手で入力するのではなく、AbstractGameObjectの持っているsetMovingBou ndary()メソッドをオーバーライドするかたちで、自動的にコードを入力してみます。 「Droid.java」を編集している状態でAndroid Studioの「Code」メニューから 「Overide Methods...」を選択します (図18)。 図18: 「Code」 メニューから 「Overide Methods...」 を選ぶ すると 「Select Method to Override/Implement」 というダイアログが開くの で、ここではオーバーライドするメソッドとして「setMovingBoundary()」を選んで 「OK」をクリックします。 123 図19: 「setMovingBoundary()」 を選んでオーバーライドする その結果「Droid.java」には、setMovingBoundary()をオーバーライドする コードが自動的に追加されます。その中味に、以下の1行のコードを加えます。 this.bottom -= height; その結果、setMovingBoundary()メソッドの部分は、図20のようになりました。 図20:自動的に入力されたコードのsetMovingBoundary()メソッドに1行を加えた このメソッドの最初の行にある 「super.setMovingBoundary(left, top, rig ht, bottom)」では、継承元の同名のメソッドを呼び出しています。 その後、 このコー ドによる処理を加えることで、メンバー変数のbottomをDroidクラス独自の値にする ことができます。 図21のように、限界値をbottomに設定すると、左側のドロイド君のように画面か らはみ出してしまいます。しかし、bottomからDroidの画像の高さであるheightを 引いた値(bottom - height) を限界値にすれば、右側のドロイド君のように、 ちょう 124 ど画面の下端で止まるようになります。 第 13 章 ゲ ー ム に よ る 実 践 図21:Droidの描画範囲の設定 これまでの成果を実行して確かめてみましょう。 13 -2 -2 障害物を表示して動かす こんどは障害物を表示するためのクラスを作成します。 Enemyクラスの作成 Droidクラスを作成したときの要領で、Enemyクラスを作成し、プロジェクトに追 加します (図22)。 図22:プロジェクトにEnemyク ラスを追加する Android Studioでは、コンストラクターは自動的に入力されません (図23)。こ こにコンストラクターなどのコードを記述していきます。 図23:プロジェクトに追加された 直後のEnemy.java 125 まずコンストラクターでは、敵キャラクター画像リソースID(R.drawable.ene my_pinkdude_jump) を指定してオブジェクトを作成するよう、以下のようなコード を入力します。 public Enemy(Context context, int width, int height) { super(context, R.drawable.enemy_pinkdude_jump, width, height); } またDroidクラスと同様に、draw()メソッドを記述します。ただし、動きの方向は Droidとは異なり、X座標を変化させています。X, Y座標を指定した描画メソッド を呼び出し、次のフレームのためのX座標を求めます。もしその値が左端の限界値 を超えるなら、右端に移動するようにしています。 public void draw(Canvas c) { draw(c, x, y); x -= 5; if (x < left) { x = right; } } さらに、AbstractGameObject の持っているsetMovingBoundary()メソッド をオーバーライドします。Droidクラスのときと同様に自動入力機能を使えば、記述 の手間がかなり省けます。 その場合に追加するメソッドの中味のコードを以下に示し ます。 ここでも、継承元の同名のメソッドを呼び出してから、画面の左方向の限界値 を補正し、X, Y座標の初期値を設定しています。 left -= width; x = right; y = 300; 以上の変更を加えた結果の「Enemy.java」を図24に示します。 126 第 13 章 ゲ ー ム に よ る 実 践 図24:新たに作成した後、必要なコードを入力したEnemy.java 次に「GameView.java」にEnemyを描画するための処理を加えます。変更 は、やはりGameThreadクラスの記述に加えます。変更前の状態を確認しておきま しょう (図25)。今回変更しないメソッドの部分は、折り畳んで表示しています。 図25:今回の変更前のGameView.javaのGameThreadクラスの定義部分 127 まず、先頭のメンバー変数の部分にEnemyオブジェクトを宣言し、敵キャラク ターのサイズを定数として定義します。 Enemy enemy; static final int enemySize = 200; その部分の編集結果は、図26のようになります。 図26:Enemyオブジェクト用のメン バー変数の定義を追加したGameVi ew.java 次に、GameThreadのコンストラクターの中に、Enemyオブジェクトを生成する コードを1行加えます。 enemy = new Enemy(context, enemySize, enemySize); その部分の編集結果を、図27に示します。 図27:GameThreadのコンストラクターに1行を追加したGameView.java また、setViewSize()メソッドにも、Enemyオブジェクトの移動範囲を設定する コードを追加します。内容は、Droidの場合とまったく同じです。 enemy.setMovingBoundary(0, 0, width, height); 128 その編集結果を、図28に示します。 第 13 章 ゲ ー ム に よ る 図28:GameThreadのsetViewSize()メソッドに1行を追加したGameView.java 実 践 最後に、draw()メソッドに、Enemyオブジェクトenemyを描画するためのコード を追加します。 enemy.draw(c); 編集結果を、図29に示します。 図29:GameThreadのdraw()メソッ ドに1行を追加したGameView.java ここまでできたら、また実行して動作を確かめてみましょう。正しくできていれば、図 30のように障害物が表示され、右から左へ移動してくるはずです。左端まで移動 すると、 また右端から現れます。 図30:自分に加えて敵キャラクターも表示したゲーム画面 129 13 -2 -3 当たり判定 自分と敵のキャラクターが画面上で衝突したかどうかを判定する機能を追加しま しょう。オブジェクト同士の当り判定にはさまざまな手法がありますが、ここでは円と円 との交差を判定する方法を使います。キャラクターの形状を無視したやや大雑把 な判定にはなりますが、仕組みが簡単で計算量も少ないためコード量も少なくて済 むというメリットがあります。 ここでは図31のように、2つのオブジェクトをちょうど包み込むような円を想定しま す。その2つの円の中心と中心の2点間の距離(len) と、2つの円の半径の和(r1 + r2) を比較することで、 2つのオブジェクトが衝突しているかどうかを判定します。こ の図の場合はlen > (r1 + r2)となっているので、 2つの円は離れていて、 オブジェク トは当たっていないと判定されます。 図31:2つの円の中心の距離が両円の半径の和より大きいので当たっていない 一方、 2つのオブジェクトの距離が近づき、len <= (r1 + r2) となると、 2つの円 が接触、 または交差することになり、当たっていると判定されます (図32)。 図32:2つの円の中心の距離が両円の半径の和以下では当たっている 130 当たり判定コードの追加 当り判定のコードはAbstractGameObjectクラスに追加し、それを継承したク ラスのオブジェクトから共通に使えるようにします。 「AbstractGameObject.java」に、メンバー変数、コンストラクター内の処理、 第 13 新たなメソッドを追加します。元の状態を確認しておきましょう (図33)。 章 ゲ ー ム に よ る 実 践 図33:今回の変更前のAbstractGameObject.java メンバー変数としては、キャラクターを包み込む円の半径を保持するためのradi usの宣言を追加します。 protected double radius; また、 コンストラクターには、 その半径を求めるコードを追加します。 radius = width * 0.5; ここでは幅として設定した値を1/2にしたものを円の半径としています。 さらに当り判定のメソッドを、 コンストラクターの後ろに追加します。 131 public boolean isHit(AbstractGameObject obj) { double xlen = (x + radius) - (obj.x + obj.radius); double ylen = (y + radius) - (obj.y + obj.radius); double len = Math.sqrt((xlen * xlen) + (ylen * ylen)); double radiusSum = radius + obj.radius; if (len <= radiusSum) { return true; } else { return false; } } このメソッドの処理の流れは次のようになっています。 ・当たり判定の対象となるオブジェクトを引数として受け取ります ・ 最初の2行で、 自分と対象オブジェクトの距離のX, Y成分を求めます ・ 次の行でX, Yの成分から三平方の定理で2点間の距離を計算します (Math. sqrtは平方根を求める関数です) ・ 次に自分と判定オブジェクトの半径の和を求めます ・2点間の距離が半径の和よりも小さいか等しければ当たっていると判定し、大きけ れば当たっていないと判定します ここまでの編集結果を図34に示します。 図34:以上の編集を加えたAbstractGameObject.java 132 さらに「AbstractGameObject.java」を編集して、当たった場合にスプライト 画像を置き換える処理も記述しておきましょう。そのためにも、メンバー変数、コンスト ラクター内の処理、新たなメソッドを追加します。 メンバー変数としては、リソースの読み込みに必要なContextを保持するconte xtを追加します。 第 13 章 ゲ ー ム に よ る protected Context context; 実 践 コンストラクターの先頭には、渡されてきたcontextを、メンバー変数に代入する 処理を加えます。 this.context = context; さらに、コンストラクターの下に、スプライト画像を置き換えるためのメソッドを追加し ます。 public void setImageResourceId(int resourceId) { drawableImg = context.getResources().getDrawable(resourceId); } 以上の編集結果を図35に示します。 図35:ここまでの編集を加えたAbstractGameObject.java 133 実際に当たり判定を呼び出して、当たった場合に画像を入れ替える処理は、Ga meViewの中に追加します。 「GameView.java」のdraw()メソッドを編集します。 変更前の状態を確認しておきましょう (図36)。 図36:当たり判定を呼び出す処理を 加える前のGameView.java 追加のコードは、 このメソッドの先頭に記述します。 if (enemy.isHit(droid)) { droid.setImageResourceId(R.drawable.andou_die01); } もしenemyとdroidが当たっていたら 「enemy.isHit(droid)」はtrue を返してき ます。その場合はdroidのスプライト画像を置き換える処理を実行します。 以上の編集結果を図37に示します。 図37:当たり判定処理の呼び出しを加えたGameView.java ここまできたら、またアプリを実行してみましょう。障害物は1つだけで、出る位置も 動きも固定されていますが、当たり判定機能も付きました (図38)。これで最低限の インタラクティブな要素が揃ったことになります。 134 第 13 章 ゲ ー ム に よ る 実 践 図38:当たり判定機能が付き、当たるとDroidの画像が変更されるようになった 135 13-3 ゲームによる実践 (3) 著:高 橋 憲 一 、山 下 武 志 KEYWORD LESSON 配列 前の節までで必要なクラスはほぼ揃いました。ここでは複 数の障害物を発生させ、操作や状態によってスプライト画像 を切り替えたり、効果音、BGMを付けるなど、ゲームの面白 みを高めていきます。 乱数 効果音 BGM Hit Point Random SoundPool MediaPlayer この節を学ぶとできること 複数の敵キャラクターを扱う 複数の敵キャラクターを画面に表示させ、 よりゲームらしくしていきます。 状態によってキャラクター画像を変更する キャラクターに変化を付けて、 プレイヤーを飽きさせない工夫をしてみましょう 状況に応じた効果音を付ける ゲームとしての臨場感を高めるために、判定結果に応じて音を鳴らしてみます。 BGMを再生する Hit Pointを常に画面に表示する ゲームに不可欠なステータスを、キャラク ターとは別に常時表示させておく方法を学 びます。 136 13 -3 -1 ランダムに障害物を発生させる 毎回同じ場所から障害物が発生してもゲームとしては面白みがありません。そこ で、障害物の出現位置がランダムに変化するようにしてみましょう。 第 13 章 乱数 ゲ ー ム に よ る プログラムにランダムな動作をさせるには、一般に「乱数」を使います。Javaには 「Random」 というクラスがあり、サイコロを振ると毎回違う目が出てくるように、ランダ 実 践 ムな数=乱数を発生させることができます。障害物を発生させる際のY座標を決め る際に乱数を使うことで、 ゲーム性を高めることができます。 Enemy.javaの修正 Enemyクラスに乱数発生のコードを追加します。 「Enemy.java」を開いて編集 します。編集前の状態を確認しておきましょう (図1)。 図1:今回の変更前のEnemy.java まずクラス定義の先頭部分に、乱数を発生させるためのRandomクラスのオブ ジェクトの宣言と、 その初期化のコードを追加します。ここで「static { }」をつけてい るのは、 この後にEnemyを複数発生させることを踏まえてのものです。 137 static Random random; static { random = new Random(System.currentTimeMillis()); } コンピューターのプログラムで生成す る乱数は、数値演算によって導くた め、数学的には正統な乱数ではなく、 「擬似乱数」 と呼ばれます。シードは 元来「種」 という意味ですが、擬似乱 数を計算する数式に定数として与え る数値です。 同じ種からは同じ花が 咲くように、シードが同じなら、同じ擬 ここでは、 「new Random()」の引数に「System.currentTimeMillis()」を指 定して、現在の時刻をミリ秒で渡しています。それは、アプリを起動する度に異なる 乱数が発生するよう、乱数の「シード」に毎回違うものを与えるためです。 次に1つのメソッド「getY()」を実装します。場所はどこでも構いませんが、この例 ではdraw()メソッドの後ろに記述しました。 似乱数の列が生成されます。シードと して現在時刻などを与えれば、同じ疑 似乱数列が繰り返し生成されること を防ぎ、ゲームなどには十分実用的な 「乱数」 を得ることができます。 private int getY() { return random.nextInt(this.bottom); } このように、 「random.nextInt()」 とすると、そのたびに新しい乱数を得ることがで きます。その際に与える引数は、発生する乱数の最大値です。ここでは、最大値とし て画面の高さを与えることで、画面の高さの範囲の乱数を得るようにしています。 また、draw()メソッドのif文の{ }の最終行に次の1行を加えて、Y座標をランダム に設定します。 y = getY(); さらに、setMovingBoundary()メソッドの中で、Y座標を設定している部分を 以下のように書き換えます。 this.bottom -= height; y = getY(); 以上の編集を加えた「Enemy.java」の状態を図2に示します。 138 第 13 章 ゲ ー ム に よ る 実 践 図2:今回の変更を加えた後のEnemy.java 以上の編集を加えたら、 ここでアプリを実行してみましょう。障害物が画面の左端 に達して消えると、次に右端から出てくる際には、毎回異なる高さに現れるようになっ たはずです (図3)。 図3:敵キャラクターは、毎回異なる高さに出現するようになった 139 13 -3 -2 障害物を複数発生させる 障害物が1つだけでは、まだまだゲームとしての面白みが足りません。画面上に 複数の障害物が発生するようにしましょう。 GameView.javaの修正 これまでは、 「GameView.java」の中で1つのEnemyオブジェクトを生成してい るだけでしたが、この中で配列を定義して複数のEnemyオブジェクトを生成し、管 理できるようにします。 ここでも 「GameView.java」の中のGameThreadクラスを編集します。変更前 の状態を確認しましょう (図4)。 図4:今回の編集前のGameView.javaのGameThreadクラス まずメンバー変数の部分を編集します。Enemyクラスのオブジェクトenemyを宣 言している部分を削除し、 そこに以下のコードを記述します。 long frameNo = 0; long nextGenFrame = 100; Context context; static final int EnemyNum = 5; Enemy[] enemys; 140 この結果、GameThreadクラスの定義の先頭部分は、図5のようになります。 ここで「enemys」にグレーの波線が 付いているのは、配列名に英語とし て存在しない単語を指定しているから です。 「enemy」 は単複同形で、正し くは複数形も 「enemy」 ですが、ここ では元の変数名と区別し、配列であ ることを強調するためにあえて「 ene mys」 としています。 気になる人は 「 enemyArray 」などとするとよいで しょう。 第 13 章 ゲ ー ム に よ る 実 践 図5:GameThreadクラスのメン バー変数の宣言、定義部分を編 集したGameView.java 次に、GameThreadのコンストラクターの中を編集し、Enemyクラスのオブジェ クトの配列を初期化する部分を以下のように記述します。 this.context = context; enemys = new Enemy[EnemyNum]; enemys[0] = new Enemy(context, enemySize, enemySize); ここではEnemyオブジェクトの配列を作成した後、1つだけEnemyオブジェクト を生成して配列の0番目の要素として入れておきます。この編集結果を図6に示し ます。 図6:GameThreadのコンストラクターを編集したGameView.java もう1つ、setViewSize()メソッドの中で、Enemyオブジェクトの動作範囲を設定 している部分を以下のように書き換えます。 for (int i = 0; i < EnemyNum; i++) { if (enemys[i] != null) { enemys[i].setMovingBoundary(0, 0, width, height); } } 141 これによって、配列に含まれるすべてのEnemyオブジェクトの動作範囲を設定し ます。この編集結果を図7に示します。 図7:GameThreadのsetViewSize()メソッドを編集したGameView.java この「GameView.java」には、もう1カ所の変更を加えます。やはりGameThre adクラスの中のdraw()メソッドです。すでに変数enemyがなくなっているので、変 更前はその部分がエラーになっています (図8)。 図8:今回の変更前のGameThreadのdraw()メソッド まず、前半の当たり判定をしている部分を書き換え、配列内の要素をループして すべての障害物について判定するよう修正します。 for (int i = 0; i < EnemyNum; i++) { if (enemys[i] != null && enemys[i].isHit(droid)) { droid.setImageResourceId(R.drawable.andou_die01); } } さらに、後半のEnemyオブジェクトを描画する部分も、同様に配列内のすべて の要素をループして描画するように修正します。 142 for (int i = 0; i < EnemyNum; i++) { if (enemys[i] != null) { enemys[i].draw(c); } } 第 13 if (frameNo == nextGenFrame) { 章 for (int i = 0; i < EnemyNum; i++) { ゲ ー ム に よ る if (enemys[i] == null) { enemys[i] = new Enemy(context, enemySize, enemySize); enemys[i].setMovingBoundary(0, 0, width, height); nextGenFrame += 100; 実 践 break; } } } frameNo++; この部分後半の「if (frameNo == nextGenFrame) {」から始まる部分では、 複数のEnemyオブジェクトを、それぞれに時間差を付けて出現させるためのもので す。 draw()メソッドの編集結果を図9に示します。 図9:GameThreadのdraw()メソッドを編集したGameView.java 143 ここまでできたらまた実行してみましょう。図10のように複数の障害物が発生する ようになっているはずです。 図10:複数の障害物が一定時間ごとランダムな高さに発生するゲーム画面 Droidを生き返らせる これまで、Droidが障害物に当たった後は、スプライト画像を切り替えて死んだ Droidを表示していました。しかし、複数の障害物が飛んでくる状況では、いったん 画像を切り替えた後に、何かしらのタイミングで元に戻すようにすべきでしょう。タイミン グとしては、Droidを上昇、下降させる際にスプライト画像を切り替える処理を入れ、 そこで元の画像に戻るようにしてみます。 「Droid.java」のuplift()メソッドに少し修正を加えます。まず元の状態を確認し ておきましょう (図11)。 図11:今回の編集前のDroid.java のuplift()メソッド ここでは真偽値onの値によってDroidの速度(動く方向) を変えていました。そこ にスプライト画像を切り替える処理も加えます。まずonが真の場合には、以下のコー ドを加えます。 setImageResourceId(R.drawable.andou_diagmore01); 144 onが偽のelseの処理には、以下のコードを加えます。 setImageResourceId(R.drawable.andou_diag01); 第 13 この部分を変更後のuplift()メソッド全体を確認します (図12)。 章 ゲ ー ム に よ る 実 践 図12:uplift()メソッドを編集したDroid.java またここでアプリを実行してみましょう。Droidの画像が状況によって変更されるよ うになり、だいぶゲームらしくなってきたのではないでしょうか (図13)。 図13:タッチ操作によって上昇中のDroid 13 -3 -3 音をつける ゲームを盛り上げる要素の1つとして「音」があります。このゲームにも効果音と BGMを追加してみましょう。 音声ファイルの追加 プロジェクトに音声ファイルをリソースとして追加します。スプライトの画像ファイルを 145 追加した際にダウンロードしたzipファイルの中に「raw」 というフォルダーがあるの で、それをプロジェクトの「res」の下に追加します。この場合も 「Project」 ビューを 「Project」に切り替えた状態で、 「raw」フォルダーごと 「res」の上にドラッグ&ド ロップすれば良いでしょう (図14)。 図14: 「Project」の「res」の 上 に「 r a w 」フォル ダー をド ラッグして追加する その結果は、 「Android」表示でも確認できます (図15)。 「raw」フォルダーに は、全部で5つの音声ファイルが含まれています。 図15:5つの音声ファイルを 含む「raw」 フォルダーが「res」 に追加された 効果音 まずは、障害物とドロイド君がぶつかった時の衝撃音と、ドロイド君が上昇してい る時のロケットの噴射音が鳴るようにしましょう。効果音のように短めの音を出す場合 は「SoundPool」 というクラスが適しています。 この変更は「GameVew.java」の中のGameThreadクラスの記述を編集しま 146 す。例によって元の状態を確認します (図16)。 第 13 章 ゲ ー ム に よ る 実 践 図16:今回の変更前のGameView.javaのGameThreadクラス まずは、GameThreadクラスにさらに4つのメンバー変数を追加します。 SoundPool sound; int hitSoundId; int rocketSoundId; int rocketStreamId; 上から順に、SoundPoolのオブジェクト、障害物と当たったときの効果音用ID、 上昇時のロケットの効果音用ID、上昇時のロケット音のストリームID(再生中の音 を止めるのに必要) となっています。 次にGameThreadのコンストラクター内に、SoundPoolを初期化するメソッドを 呼び出すコードを追加します。 setupSoundPool(); その下には、そのSoundPool関連の初期化メソッドと、音声データを解放するリ リースメソッドを追加します。 147 public void setupSoundPool() { sound = new SoundPool(2, AudioManager.STREAM_MUSIC, 0); hitSoundId = sound.load(context, R.raw.quick_explosion, 1); rocketSoundId = sound.load(context, R.raw.rockets, 1); } public void releaseSoundPool() { sound.release(); } この初期化メソッドでは、SoundPoolオブジェクトを生成し、障害物と当たったと きの音と、上昇時のロケット音をロードしています。リリースメソッドではSoundPoolオ ブジェクトをリリースするメソッドを呼び出しています。 ここまでの編集結果を図17に示します。 ここで、SoundPoolのコンストラク ターの呼び出しに横線が入っている のは、そのメソッドが「デプリケート」 さ れているからです。デプリケートされる とは、そのクラスやメソッドが、将来使 えなくなると宣言されたことを意味しま す。 本来ならその時点で使わないよ うにすべきですが、ここではその状態 を示すためもあって、あえて使ってい ます。現在では、SoundPool.Build e rクラスを使ってS o u n d P o o lオブ ジェクトを作成することになっていま す。 図17:4つのメンバー変数と、 2つのメソッドを追加したGameView.javaのGameThreadクラス さらにGameView.javaの中のGameThreadクラスのuplift()とdraw()の両メ ソッドも編集します。それらの元の状態を確認してください (図18)。 148 第 13 章 ゲ ー ム に よ る 実 践 図18:今回の編集前のGameView.javaのuplift()とdraw()メソッド uplift()メソッドでは、現状の処理の下に、ロケット上昇時の音の再生するため、 以下のコードを追加します。ここでは、onがtrueなら、上昇する音の再生を開始しま す。その際、play()メソッドが返すストリームIDを保持しておきます。onがfalseなら下 降なので、保持しておいたストリームIDを指定して音の再生を止めます。 if (on) { rocketStreamId = sound.play(rocketSoundId, 0.5f, 0.5f, 0, -1, 1.0f); } else { sound.stop(rocketStreamId); } 次にdraw()メソッドの中の当たり判定の部分には、Droidが障害物に当たった と判断された際に音を再生するコードを追加します。 sound.play(hitSoundId, 1.0f, 1.0f, 0, 0, 1.0f); 以上の編集結果を図19に示します。 149 図19:修正を加えたuplift()とdraw()メソッド 当り判定処理の修正 これまでの処理方法では、当り判定でtrue(当たっている) という結果が出た後 も、毎フレームで当たっているという判定が繰り返されていました。この状況のまま効 果音を出すと、続くフレームごと新たに音を鳴らすような処理が実行されてしまいま す。そこで、いったん当たっているという判定が出た障害物に対しては、次のフレー ム以降は当たっているという判定が出ないように修正します。 「AbstractGameObject.java」を編集します。変更を加えるのはメンバー変 数の部分とisHit()メソッドです。その部分の現状を確認しましょう (図20)。 150 第 13 章 ゲ ー ム に よ る 実 践 図20:今回の変更を加える前のAbstractGameObject.java メンバー変数として、 「当たり判定済み」フラグを追加します。 protected boolean alreadyHit = false; isHit()メソッドでは、実際に当たり判定を実行して、その結果を返す部分を以下 のように書き換えます。 if (len <= radiusSum && !alreadyHit) { alreadyHit = true; return true; } else { return false; } これは、当たっているかどうかを判定する条件式に、 「当たり判定済み」フラグが falseならばという条件を加え、2つの条件が両方とも満たされる場合にのみ当たっ ていると判断します。その際は「当たり判定済み」フラグをtrueにしておきます。 以上の編集結果を図21に示します。 151 図21:当たり判定を変更したAbstractGameObject.java ここでtrueに設定した当たり判定済フラグも、どこかでfalseにしなければなりませ ん。それは、Enemy.javaのdraw()メソッドの中で実行します。まずこのメソッドの元 の状態です (図22)。 図22:今回の変更を加える前のEnemy. javaのdraw()メソッド ifの後ろの{ }の中の最後の部分に以下のコードを加えます。 alreadyHit = false; 変更後のdraw()メソッドを図23に示します。 152 第 13 章 ゲ ー ム に よ る 図23:今回の変更を加えたEnemy.java のdraw()メソッド 実 践 この処理により、障害物が画面の右端から再び画面に出るときに、 「当たり判定 済み」フラグがfalseになります。 BGM 効果音だけではさびしいので、プレイ中に流れるBGMも追加しましょう。こんどは 「MainActivity.java」を編集します。クラス定義の先頭部分にメンバー変数を 追加し、Activityクラスのいくつかのメソッドをオーバーライドするかたちで実装しま す。先頭部分の状態を確認しておきましょう (図24)。 図24:今回の編集を加える前のMainActivity.javaの先頭部分 メンバー変数として、MediaPlayerクラスのオブジェクトを宣言します。BGMのよ うに長い音声ファイルの再生にはMediaPlayerが適しています。 MediaPlayer mediaPlayer; 念のために、 この編集後の状態を確認しておきます (図25)。 153 図25:MediaPlayerオブジェクトを保持するメンバー変数を追加したMainActivity.java 続いてBGMを鳴らしたり止めたりするのに必要なタイミングで呼び出されるActi vityクラスのメソッドをオーバーライドします。まず、 「MainActivity.java」を編集し ている状態で「Code」メニューから 「Override Methods...」を選択します (図 26)。 図26: 「MainActivity.java」 を編集中に、 「Code」 メニューから 「Override Methods...」 を選ぶ すると、 「Select Methods to Override/Implement」 というダイアログが開く ので、 「onStart():void」 「onResume():void」 「onPause():void」 「onStop():v oid」の4つのメソッドを選択して「OK」をクリックします (図27)。このように、離れた 項目を同時に選択するには「Ctrl」キーを押しながらクリックすればよいのです。 154 第 13 章 ゲ ー ム に よ る 実 践 図27: 「Ctrl」 を押しながらクリックして4つのメソッドを選ぶ その結果、選択した4つのメソッドの最小限の実装コードが自動的に入力されます (図28)。 図 2 8: 「Override Methods...」機能を 使って自動的に入力 した4つのメソッド 155 これらのメソッドの定義の中に、必要なコードを追加入力していきます。 まず、onStart()メソッドには、 「super.onStart()」の後ろに、以下のコードを追 加します。 これはこのアクティビティの起動時にMediaPlayerのオブジェクトを作成し ます。 mediaPlayer = MediaPlayer.create(this, R.raw.boss); 次にonResume()メソッドには、やはり 「super.onResume()」の後ろに、以下の コードを追加します。これはアクティビティがリジュームする際に、MediaPlayerを起 動し、 その後ループ再生するように設定します。 mediaPlayer.start(); mediaPlayer.setLooping(true); またonPause()メソッドには、 「super.onPause()」の後ろに、以下のコードを追 加します。これはアクティビティがポーズ状態になる際に、MediaPlayerの再生も ポーズさせます。 mediaPlayer.pause(); さらに、onStop()メソッドには、 「super.onStop()」の後ろに、以下のコードを追 加します。これはMediaPlayerを停止し、 そのオブジェクトを解放します。 mediaPlayer.stop(); mediaPlayer.release(); 以上の変更を加えた「MainActivity.java」の状態を確認しておきましょう (図 29)。 156 第 13 章 ゲ ー ム に よ る 実 践 図29:4つのメソッドの中味を実装したMainActivity.java ここでまたアプリを実行してみましょう。アプリが動作を開始するとBGMが流れ始 め、動作に合わせた効果音が鳴ることを確認してください。 13 -3 -4 Hit Point の管理 ゲームでは、何らかの制限がある方がプレイも盛り上がるものです。自分のキャラ クターであるドロイド君にHit Pointの概念を取り入れ、障害物に当たるたびにそれ が減っていくようにします。そしてHit Pointが0になると操作不能になるようにしてみま しょう。 Droid.javaの修正 こんどは「Droid.java」を編集し、メンバー変数と新たなメソッドを追加し、upli ft()メソッドにも変更を加えます。まず元の状態を確認しましょう (図30)。 157 図30:今回の変更を加える前の 「Droid.java」 の先頭部分 まずHit Pointの満タン時の値として100という定数を定義した後、Hit Pointを 保持するためのメンバー変数を宣言し、 その値を使って初期化します。 private static final int FullHitPoint = 100; private int hitPoint = FullHitPoint; またuplift()メソッドの上あたりに、 3つのメソッドの実装を追加します。 public void hit() { if (hitPoint > 0) { hitPoint -= 10; } if (hitPoint <= 0) { velocity = DefaultVelocity; setImageResourceId(R.drawable.andou_explode10); } } public int getHitPoint() { return hitPoint; } public void resume() { hitPoint = FullHitPoint; setImageResourceId(R.drawable.andou_diag01); } 158 最初のhit()は、障害物と当たったと判定された際に呼ばれるメソッドで、Hit Pointの値を10ずつ減らします。そして、もし0以下になった場合は、速度を下降用 の値に固定して、 スプライト画像を焦げた色のものに変更します。 次のgetHitPoint()は、このクラスが持っているHit Pointの値を返すメソッドで す。 第 最後のresume()は、一度Hit Pointが0になって操作不能になった後に、再び 13 プレイできるようにする際に呼び出すメソッドです。Hit Pointを満タンにして、 スプライ ゲ ー ム に よ る 章 ト画像を元に戻しています。 実 践 ここまでの編集結果を図31に示します。 図31:メンバー変数を加え、 3つのメソッドの実装を加えたDroid.java さらにuplift()メソッドも編集します。ここでは、元の中味の全体をHit Pointが0よ り大きい場合だけ実行するようにして、Hit Pointが0になった場合にはまったく操作 ができないようにします。 159 if (hitPoint > 0) { if (on) { velocity = -DefaultVelocity; setImageResourceId(R.drawable.andou_diagmore01); } else { velocity = DefaultVelocity; setImageResourceId(R.drawable.andou_diag01); } } uplift()メソッドの編集結果を図32に示します。 図32:操作可能な条件を絞ったDroid.javaのuplift()メソッド GameView.javaの修正 最後にもう1つ、GameView.javaのGameThreadクラスを編集します。 まず、編集前のGameThreadクラスの状態を確認します (図33)。 図 3 3:今 回 の 編 集 を加える前のGam eView.javaのGam e T h r e a d クラス の 先頭部分 まずはメンバー変数を1つ追加します。これは、いったん操作不能になった後に、 復活する際のタイミングを表すフレーム番号を保持するものです。 160 long resumeFrame = -1; すでに定義してあるフレーム関係のメンバー変数の後ろに付ければ良いでしょう 第 13 (図34)。 章 ゲ ー ム に よ る 実 践 図34:メンバー変数 をさらに1つ追加し たGameThreadク ラス 次にdraw()メソッドを編集しますが、その前に、そのメソッドの上にもう1つ新たなメ ソッドを追加します。念のため、draw()メソッドの現状を確認しておきましょう (図 35)。 図35:今回の変更を加える前のGameThreadクラスのdraw()メソッド 161 新たなメソッド「drawHitPoint()」は、draw()メソッドの前に実装しましょう。これ は、Hit Pointの現在値を、画面の左上角に描画するメソッドです。文字の色をse tColor()で、大きさをsetTextSize()で指定してから、引数で渡されたHit Point の値を、"HP: "という文字列に続けて描画しています。 private void drawHitPoint(Canvas c, int point) { Paint paint = new Paint(); paint.setColor(Color.WHITE); paint.setTextSize(80); c.drawText("HP: " + point, 20, 100, paint); } このメソッドを実装した状態を図36に示します。前後のメソッドは折り畳んでいま す。 図36:Hit Pointを表示するdrawHitPoint()メソッドを新たに実装した その後、上で定義したHit Pointを表示するメソッドを呼び出すコードも含めて、 draw()メソッドを編集します。 まずメソッド定義の先頭部分に、現在のHit Pointの値を取得するコードを追加 します。 int hitPoint = droid.getHitPoint(); 次の最初のforループのifの後ろの{}の中には、障害物と当たったと判定された 場合の処理を加えます。droidオブジェクトのhit()メソッドを呼び出すことでHit Pointを減らし、現在のHit Point値を取得します。それがもし0なら、復活時のフ レーム番号を現在のフレーム番号から300フレームだけ後に設定しています。 162 droid.hit(); hitPoint = droid.getHitPoint(); if (hitPoint == 0) { resumeFrame = frameNo + 300; } 第 13 章 最後に、すべての障害物をforループで描いた後に、Hit Pointを画面に表示 ゲ ー ム に よ る するメソッドを呼び出し、現在のフレーム番号が復活時のフレーム番号と一致する 場合は復活のメソッドを呼び出す処理を記述します。 実 践 drawHitPoint(c, hitPoint); if (frameNo == resumeFrame) { droid.resume(); } 以上の変更を加えたdraw()メソッドの該当部分を図37に示します。 図37:Hit Point関連の処理を追加したGameThreadクラスのdraw()メソッド 163 ここまでできたら完成です。さっそく実行してみましょう。 Hit Pointは最初から画面に表示されるようになりました (図38)。 図38:画面の左上には、Hit Pointが常に表示される また、障害物との衝突によって、Hit Pointは10ポイントずつ減っていきます (図 39)。 図39:衝突するとHit Pointは減っていく そしてHit Pointが0になると、焼けこげたDroidは下に落ちていきます。その間、 操作はできません。 図40:Hit Pointが0になるとDoirは操作不能になって落ちていく 164 演習問題 障害物を撃つ処理を追加してみましょう。 Beamクラスを追加して、 レーザービームがEnemyと逆に左から右へ移動するよ うにしましょう。BeamとEnemyの当り判定を実行して、当たったらそのEnemyを消 第 13 すようにすればよいでしょう。 章 ゲ ー ム に よ る 実 践 165
© Copyright 2024 ExpyDoc