Processing で状態遷移のあるゲーム・アプリを作る

最初はタイトル画面が表示されていて、あるキーを押すとゲームが開始、一定時間経過するとゲームが終了して、結果(エンディング)画面が表示される、といった状態遷移を伴うゲーム・アプリを作る方法を整理してみましょう。

  1. 状態遷移を int 型の変数と if 文で記述する方法
  2. 状態遷移をクラスで記述する方法

状態遷移を int 型の変数と if 文で記述する方法

一番理解しやすい方法は、現在の状態を覚えておくための変数を用意して 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