横スクロールゲームでプログラムを学ぶ キーボードイベント スクロール 速度の異なるアニメーション管理 ここでは 横スクロールゲームを題材にして Processing でキーイベントを扱う方法と 画面をスクロールする方法 ( アルゴリズム ) について学ぶ ゲームを作る流れ 1. どのようなゲームか ルール ゲーム進行などについて大まかな構想を練る 今回は 下図のような横方向に移動するキャラをキーボードで操作して障害物をよけるゲームとする 2. どのように画面上にゲームを表現するか ユーザーインターフェースをどうするか考える (2D 3D 一人称視点 三人称視点 etc.) 今回は 2D で三人称視点 キーボード操作のみでゲームする ( ア ) ウインドウサイズ (800 400) を決める ( イ ) ウインドウ背景画像 ( 両端が繋がったもの スクロール速度を調整できるよう約数の多い大きさを幅に使う 今回は 1600 400) キャラアニメーション画像 障害物画像 得点用文字 数字画像などを用意する ( ウ ) BGM や効果音を用意する コンピューター基礎 Ⅲa 1
3. ルール ゲーム進行 ( ライフ 時間 フラグやハイスコアなど ) をどう管理するか考える ( ア ) 異なる速度の複数アニメーションをどう管理するか?( アニメーションカウンター ) ( イ ) 衝突したかどうかの判定は? 中心間距離? ( ウ ) 壁の扱いをどうするか? 反射させるか ダメージを与えるか? ( エ ) 操作感をどうするか? 慣性系か? 非現実か? 移動速度制限するか? ( オ ) 生存時間やライフ数計算 4. プログラムを機能別に分けて 部分ごとにプログラムを作成する ( ア ) ウインドウにゲーム背景画面を描く ( スクロール画像 ) ( イ ) ゲーム画面にキャラをアニメーションさせる ( ウ ) キーボードを読み取る ( キーボードイベント処理 ) ( エ ) 衝突判定 ( オ ) ゲーム終了を実現する ( カ ) BGM 効果音を鳴らす 5. 難易度 ユーザビリティを調整する プログラミングの整理 setup() draw() stop() と自前の関数内部と関数外部に何を記述するか決める 大域変数の名前や各機能の関数名決める 1. 欄外に import する機能や大域変数を記述する ( ア ) 音声を使うので ミニオーディオライブラリを import し 音声用の大域変数を宣言する BGM を sndbgm という名前の AudioPlayer クラスにする ( エンドレスで鳴らす必要があるから ) 壁にぶつかった時の音を sndtap という名前の AudioSample クラスにする 障害物にぶつかった音を sndcrash という名前の AudioSample クラスにする ゲーム終了の BGM を sndend という名前の AudioPlayer クラスにする コンピューター基礎 Ⅲa 2
( イ ) ゲーム画面サイズを定数で記述する ( ウ ) ゲーム終了判定フラグを大域変数 isgameover として宣言する ( エ ) ゲームの経過時間 ( 秒 :pasttime) やライフポイントを大域変数として宣言する ( オ ) 今回は PNG 画像をアニメーションに使うので PImage とその配列を用意する ( アニメーション画像数を定数 ANIME_FRAMES として宣言する ) 背景用 文字 得点用画像用 PImage を宣言 (imgback imgtext) 2 枚の画像からのなるアニメーション用に要素数 2 の PImage 配列を用意する 2. setup() のなかに初期化作業を記述する ( ア ) ウインドウを作る ( イ ) フレームレートを設定する ( ウ ) 背景画像を PImage に読み込む 画像縦横サイズを変数に格納する ( エ ) アニメーション画像を順番に PImage 配列に読み込む 画像縦横サイズを変数に格納する ( オ ) サウンドを初期化し サウンドファイルを読み込む BGM をループ再生する 3. draw() に実行順に注意して 繰り返し処理を記述する ( ア ) ゲーム終了フラグを調べ 終わってなかったらゲームを進行 そうでなかったらゲームを停止し終了画面を表示する と書く ( イ ) ゲーム進行の部分には スクロール背景を描く ( 今回は関数 void drawscroll() として書く ) 経過時間 ライフ数など文字情報を描く キャラを描く ( 関数 void drawchara() として 見通しをよくする ) キー操作処理を行う 衝突判定を行う ゲーム終了条件を調べる もし終わっていたらゲーム終了フラグを立て ゲーム進行を止める操作 (BGM を止める 終了効果音を流す ) を行う ( ウ ) ゲーム終了画面は 終了画面や終了メッセージを描く 4. stop() に終了操作を書く ( サウンド系の消去 ) コーディング コンピューター基礎 Ⅲa 3
衝突判定 物体の衝突判定は 図のように物体間の距離と2つの物体サイズを比較することによって行うことが最も簡単である 円や球では この判定は中心間の距離と二つの半径合計との比較となるので計算が簡単である 実際のプログラムでは 各図形中心座標の x 座標の差の二乗と y 座標の差の二乗を加えたものと あらかじめ計算しておいた衝突距離の二乗とを比べる ( ピタゴラスの定理 ) それ以外の形状では 円に近似して考えるとよい 判定が多少甘くなるが ゲームに大きな影響がなければ気にしない どうしても気になるときは 画像の輪郭上で図形が重なっているかどうか ピクセルごとに色を調べればよい しかしこの方法は実行に時間がかかり 非力な PC では処理落ちや駒落ちが生じる原因となることがある スクロールのための画像表示アルゴリズム スクロール画面を作るためには スクロール方向で始まりと終わりが繋がった画像 ( 縦スクロールなら上下 横スクロールなら左右 ) を用意する ここでは 話を簡単にするため 横スクロール用の画像は ウインドウの横幅より大きく高さがウインドウと同じものを用意する スクロールプログラムの基本は 図のようにWindowが背景からはみ出す場合と はみ出さない場合に分けて考えるところにある まずは はみ出しが始まる条件を確定し これを判断文に反映させる はみ出していないときは スクロール画像から左上隅を指定してゲームウインドウへの copy 処理をする はみ出しが始まったら はみ出していない部分のcopy 処理と はみ出しコンピューター基礎 Ⅲa 4
部分に相当する範囲をスクロール画像の反対側から copy する処理を別々に行う コード例 screenpositionx += SCROLL_SPEED; // 画像から取り出す左上隅座標をずらす if(screenpositionx > BACK_WIDTH-W_WIDTH){ // はみ出している if(screenpositionx > BACK_WIDTH){ // はみ出しきった時の処理 screenpositionx = 0; // 画像の取り出し位置を左端に戻す else{ // 一部はみ出している時の処理 // はみ出していない部分を部分コピー copy(imgback, screenpositionx, 0, BACK_WIDTH-screenPositionX, W_HEIGHT, 0, 0, BACK_WIDTH-screenPositionX, W_HEIGHT); // はみ出した範囲と同じ大きさを 元画像の左端から部分コピー copy(imgback, 0, 0, W_WIDTH-(BACK_WIDTH-W_WIDTH- screenpositionx), W_HEIGHT, BACK_WIDTH-screenPositionX, 0, W_WIDTH-(BACK_WIDTH-W_WIDTH - screenpositionx), W_HEIGHT); コンピューター基礎 Ⅲa 5
else{ // はみ出しがまったくないときの処理 copy(imgback, screenpositionx, 0, W_WIDTH, W_HEIGHT, 0, 0, W_WIDTH, W_HEIGHT); キーボードイベント取り出しとキーコードの利用 キーボードイベントを扱うには 以下のカーソルキーを扱ったコード例のように決まり 文句がある if(keypressed){ // キーが押されたか? if(key = = CODED){ // そのキーは予約されたキーか? if(keycode = = UP){ ; // キーは 上向きカーソル キーか? else if(keycode = = DOWN){ ; else if(keycode = = RIGHT){ ; else if(keycode = = LEFT){ ; keypressed は Boolean 型の特別な変数で キーボードのキーが押されると TRUE になる 押されたキーの状態は 変数 key( キーの種類 ) と keycode( キーのコード番号 ) に格納される UP DOWN などの定数を使いたくないときは キーボードのキーコードを実行環境ごとに調べて そのコードを使う必要がある 注意点は draw に上記コードを書くと キーが押されている間はずっと三重目の if 分が 実行され続けることである キーを一度叩くたびに一つ何かをさせる場合は 工夫が必要 コンピューター基礎 Ⅲa 6
である たとえば 一つ前のキーコードを保存しておき 新たに入力されたキーコードが 変化した場合のみ処理を行うようにする ゲーム開始からの時間を計る プログラム中で経過時間を知りたい場合, 単純にFrame 数 (draw() を実行した数 ) を数えればよい. 秒 10フレームで実行中に3 秒後に何かをさせたければ, フレーム数を大域変数として保存しておき, その値が30になったことをif 文などで検出すればよい. この方法はあまり見た目がよろしくないので, 普通はmillis() 関数を使う.millis() は呼び出される度に, プログラム開始時からの時間をミリ秒単位の整数で返してくる. そこで, 特定の時間が経過したら実行するプログラムは, 1. 初めの時間を大域変数 ( 例えばint mystartmillis) に記録する int mystartmillis = millis(); 2. millis() を適当な間隔で繰り返し実行し, 得られた値からmyStartMillisを引いた値を手に入れる. int now = millis() - mystartmilli; 3. 得られた値 (now) をあらかじめ決めておいた時間と比較し, 時間になったらそのとき行うべき命令を実行する. この方法を使う時には, 得られた値の判断を 等しいか? で行ってはならない.draw() にかかる時間が一定とは限らないので, millis() - mystartmillis が狙った値(A) になることが保証できないからである ( つまり狙った値を跨いでしまうことがある ). 確実に処理をおこなうためには, 等しいかどうか now == A ではなく, 通過したか now >= A で判断すべきである. コンピューター基礎 Ⅲa 7