最初はタイトル画面が表示されていて、あるキーを押すとゲームが開始、一定時間経過するとゲームが終了して、結果(エンディング)画面が表示される、といった状態遷移を伴うゲーム・アプリを作る方法を整理してみましょう。
一番理解しやすい方法は、現在の状態を覚えておくための変数を用意して if 文で各状態の処理に分岐することだと思います。 現在の状態に遷移してからの時刻を計算しておくと、アニメーションを描画したり、時間経過で状態遷移したりするプログラムを書くときに便利です。
int state; // 現在の状態 (0=タイトル, 1=ゲーム, 2=エンディング) long t_start; // 現在の状態になった時刻[ミリ秒] float t; // 現在の状態になってからの経過時間[秒] void setup(){ size(400, 400); textSize(32); textAlign(CENTER); fill(255); state = 0; t_start = millis(); } void draw(){ background(0); t = (millis() - t_start) / 1000.0; // 経過時間を更新 text(nf(t, 1, 3) + "sec.", width * 0.5, height * 0.9); // 経過時間を表示 int nextState= 0; if(state == 0){ nextState = title(); } else if(state == 1){ nextState = game(); } else if(state == 2){ nextState = ending(); } if(state != nextState){ t_start = millis(); } // 状態が遷移するので、現在の状態になった時刻を更新する state = nextState; } int title(){ text("Game Title", width * 0.5, height * 0.3); text("Press 'z' key to start", width * 0.5, height * 0.7); if(keyPressed && key == 'z'){ // if 'z' key is pressed return 1; // start game } return 0; } int game(){ text("Game (for 5 seconds)", width * 0.5, height * 0.5); if(t > 5){ // if ellapsed time is larger than 5 seconds return 2; // go to ending } return 1; } int ending(){ text("Ending", width * 0.5, height * 0.5); if(t > 3){ text("Press 'a' to restart.", width * 0.5, height * 0.7); if(keyPressed && key == 'a') return 0; } return 2; }
取りうる状態の数があまり多くなければ上の方法でも十分ですが、状態の数だけ分岐が必要だったり、状態を数字で表しているので意味がわかりにくくなったりすることを考えると、 状態数が多い場合にはあまり良い書き方とは言えません。別の方法として、クラスを利用して状態遷移を記述する方法を見てみましょう。
ここではまず、すべての状態の共通部分を担当する State クラスを作成し、各状態はそのクラスを継承することにします。
State state; void setup() { size(400, 400); textSize(32); textAlign(CENTER); fill(255); state = new TitleState(); } void draw() { background(0); state = state.doState(); } abstract class State { long t_start; float t; State() { t_start = millis(); } State doState() { t = (millis() - t_start) / 1000.0; text(nf(t, 1, 3) + "sec.", width * 0.5, height * 0.9); drawState(); return decideState(); } abstract void drawState(); // 状態に応じた描画を行う abstract State decideState(); // 次の状態を返す }
State クラスのメンバー関数 drawState, decideState
には中身がありません。
これらの関数は各状態を表す子クラスで実装することになります。こういった中身がないメンバー関数には abstract
を頭につける必要があります。
また、一つでも abstract
なメンバー関数があるクラスには abstract class State
というように頭に abstract
を付ける必要があります。
この State クラスを使って先のプログラムを書き直すと、状態は State クラスのオブジェクトとして表され、draw
関数の中にあった分岐が必要なくなります。
次に State クラスを継承する各状態のクラスを見てみましょう。親クラスで abstract
と指定されていて中身のなかった2つの関数 drawState, decideState
が実装されています。
TitleState, GameState, EndingState
でそれぞれ異なる実装となっていることがわかるでしょうか。これによって、状態ごとに異なる処理を (if 文なしで) 実行できるという仕掛けです。
class TitleState extends State { void drawState() { text("Game Title", width * 0.5, height * 0.3); text("Press 'z' key to start", width * 0.5, height * 0.7); } State decideState() { if (keyPressed && key == 'z') { // if 'z' key is pressed return new GameState(); // start game } return this; } } class GameState extends State { void drawState() { text("Game (for 5 seconds)", width * 0.5, height * 0.5); } State decideState() { if (t > 5) { // if ellapsed time is larger than return new EndingState(); // go to ending } return this; } } class EndingState extends State { void drawState() { text("Ending", width * 0.5, height * 0.5); if (t > 3) { text("Press 'a' to restart.", width * 0.5, height * 0.7); } } State decideState() { if (t > 3 && keyPressed && key == 'a') { return new EndingState(); } return this; } }
© 2015- Takeshi NISHIDA