プログラミング兼読書日記

プログラミングをしていて、はまってしまった事を中心に記事にしています。

ゲームシーケンス遷移

 今回はいつも使っている簡単なシーケンス遷移の実装方法を紹介します。

 以下に特徴を示します。

  1. 仮想的な木構造であり、実行時には1つのリストになる
  2. 木の葉ノードGameLeafクラスと木の内部ノードGameNodeクラス(GameLeafを継承している)からなる
  3. 実行時にはGameNodeは1つのGameNodeまたは1つのGameLeafをもち、GameLeafのupdate、renderが毎フレーム呼ばれる
  4. 子ノードは親ノードがもつリソースにアクセスできる

 仮想的なシーケンス構造が下のようになっているとすると、

シーケンス構造
RootNode-|-TitleLeaf
         |-SelectStageLeaf
         |-PlayNode-------|-LoadLeaf
                          |-PlayLeaf
                          |-PauseLeaf
                          |-ClearLeaf
                          |-GameOverLeaf

 実行時にはRootNode-TitleLeafやRootNode-PlayNode-PlayLeafのようにGameNode継承クラス-GameNode継承クラス...-GameLeaf継承クラスという構造になります。以下では仮想的な子である場合を"仮想的な子"、実際に生成されている子ノードは"子"と表現しています。

 まずはGameLeafとGameNodeの実装

abstract public class GameLeaf {
  private GameNode parent;               //親ノード
  abstract public void init();           //初期化処理
  abstract public GameLeaf update();     //更新処理。戻り値は基本的には次以降フレームで実行するGameLeaf継承クラス。変更しない場合はthisを返すようにする。
  abstract public void render();         //描画処理
  abstract public void destroy();        //終了処理
  abstract public String getID();        //自分を識別する文字列を返す。
  public void setParent(GameNode node){
    parent=node;
  }
  public GameNode getParent(){           //getParentで親ノードを取得し、親のリソースにアクセスする
    return parent;
  }
}
abstract public class GameNode extends GameLeaf{
  private GameLeaf child;//子ノード
  abstract public GameLeaf createFirstChild();//ノードが作られたと同時に作成する子ノードを返す。初期化もここでする。
  abstract public boolean isMyChild(GameLeaf c);//このノードの仮想てきな子ノードかどうか
                                                //GameNode継承クラスは上の2つとgetID、destroyを実装する必要がある。
  @Override
  public void init() {//子を生成する。
    child=createFirstChild();
    child.setParent(this);
    child.init();
  }
  @Override
  public GameLeaf update() {      //ノードの処理を行う。戻り値によって処理をかえる。
    GameLeaf next=child.update();
    if(next==child)return this;   //child.updateの戻り値がchildと等しい場合なにもしない。
    else if(next==null){          //戻り値がnullの場合は再帰的にノードが破棄され終了
      child.destroy();
      child=null;
      return null;
    }else if(isMyChild(next)){                        
      if(isMyChild(next)){        //child.updateの戻り値がchildと等しくないが自分の仮想的な子の場合はchildを破棄し、
        child.destroy();          //nextに変更。自分自身を戻り値として返すので親ノードではなにもしない。
        child=null;
        child=next;
        child.setParent(this);
        child.init();
        return this;
      }else{                      //child.updateの戻り値がchildと等しくない場合はchildを破棄し、nextを親に投げる。        
        child.destroy();          //自分自身を戻り値として返さないのでこのノードは親ノードで破棄される。
        child=null;               //nextが子である親ノードにたどりつくまで、同じ処理が行われる。
        return next;
      }
    }
  }
  @Override
  public void render() {
    child.render();
  }   
}

 C++の場合はdestroyをデストラクタにし、destroy呼び出しをdeleteに変更すればいいです。

 わかりにくいのはGameNodeのcreateFirstChild、updateだと思います。createFirstChildはGameNodeのinit内で呼ばれるので、init終了時には末端のGameLeafの生成、初期化処理まで終わっています。updateではシーケンスの遷移を処理しています。updateの戻り値は

  1. thisポインタ
  2. null
  3. 同じ親に属するGameLeafまたはGameNode継承クラス
  4. さらに上の親ノードの仮想的な子

 1の場合は何もしません。2が返されたら順次クラスを破棄して終了します。3の場合は元の子ノードを破棄してupdateの戻り値を子ノードとしてセットします。自分自身をnewして返すなら再初期化することになります。4の場合は戻り値が仮想的な子ノードである親ノードに出会うまで再帰的にノードが破棄されていき、親ノードにであったら子ノードとしてセットされる。有効な戻り値は例で示すシーケンス構造で考えると、ClearLeafがTitleLeafを返すのは有効ですが、TitleLeafがPlayLeafを返すのは無効です。

 実際には、描画に必要なものをrenderの引数にもたせたり、経過時間をupdateの引数に持たせたりしています。

 GameLeaf、GameNodeを継承したクラスを実装し、根ノードをRootNodeとすると、ゲームループを実装する場合、下のように使います。

int fps=60;
GameLeaf rootNode=new RootNode();//RootNodeは根ノードとする。
rootNode.init();
rootNode.render();
while(true){//ゲームループ
  long ms=System.currentTimeMillis();
  if(rootNode.update()==null){
    break;
  }
  rootNode.render();
  long waitTime=System.currentTimeMillis()-1000/fps;
  //ほかの処理などここで
  //fpsを固定させるため待つ
  if(waitTime>0){
    try{
      Thread.sleep(waitTime);
    }catch(Exception e){
    }
  }
}
rootNode.destroy();

 以下ではよく使う構造で実装してみます。[...]は...が実装されていると考えてください。

 GameLeaf、GameNode継承クラスの実装方法はシーケンスがしたのようになっているとすると、

シーケンス構造
RootNode-|-TitleLeaf
         |-SelectStageLeaf
         |-PlayNode-------|-LoadLeaf
                          |-PlayLeaf
                          |-PauseLeaf
                          |-ClearLeaf
                          |-GameOverLeaf
RootNode:根ノード
TitleLeaf:タイトルを表示する。スタートボタンでステージセレクト画面へ遷移する。
SelectStageLeaf:ステージセレクト画面を表示
PlayNode:ゲームに必要なリソースを持つNode
LoadLeaf:ロード画面を表示しつつ、PlayNodeのリソースをロードする。
PlayLeaf:ゲームプレイ中
PauseLeaf:ポーズ画面表示
GameOverLeaf:ゲームオーバー画面表示。スタートボダンでタイトル画面へ
ClearLeaf:クリア画面表示。スタートボタンでタイトル画面へ

 RootNode.java

public class RootNode extends GameNode{
  public static String id="RootNode";//すべてのGameLeaf、GameNodeクラスに静的文字列idを持たせる
  @Override
  public GameLeaf createFirstChild(){
    return new TitleLeaf();
  }
  @Override
  public boolean isMyChild(GameLeaf leaf){
    String id=leaf.getID();
    return id.equals(TitleLeaf.id)||id.equals(SelectStageLeaf.id)||id.equals(PlayNode.id);
  }
  @Override
  public void destroy(){
  }
  @Override
  public String getId(){
    return id;
  }
}

 TitleLeaf.java

public class TitleLeaf extends GameLeaf{
  public static String id="TitleLeaf";
  @Override
  public void init(){
  }
  @Override
  public GameLeaf update(){
    if([スタートボタンを押した]){
      return new StageSelectLeaf();
    }
    return this;
  }
  @Override
  public void render(){
    [ここでタイトルの表示]
  }
  @Override
  public void destroy(){
  }
}

 StageSelectLeaf.java

public class StageSelectLeaf extends GameLeaf{
  public static String id="StageSelectLeaf";
  private int stageNumber;   //ステージのナンバー
  @Override
  public void init(){
  }
  @Override
  public GameLeaf update(){
    if([方向キーを押した]){
      [stageNumberの変更処理]
    }
    if([スタートボタンを押した]){
      return new PlayNode(stageNumber);
    }
    return this;
  }
  @Override
  public void render(){
    [ステージセレクト画面表示]
  }
  @Override
  public void destroy(){
  }
}

 PlayNode.java

public class PlayNode extends GameNode{
  public static String id="PlayNode";
  private int stageNumber;
  public PlayNode(int stageNumber){
    this.stageNumber=stageNumber;
  }
  @Override
  public GameLeaf createFirstChild(){
    return new LoadLeaf();
  }
  @Override
  public boolean isMyChild(GameLeaf leaf){
    String id=leaf.getID();
    return id.equals(PlayLeaf.id)||id.equals(LoadLeaf.id)||id.equals(ClearLeaf.id)||id.equals(GameOverLeaf.id);
  }
  @Override
  public void destroy(){
  }
  @Override
  public String getId(){
    return id;
  }
  public void load(){          //各種リソースのロードを行う。LoadLeafから呼び出す
  }
  public void updateGameObjects(){   //ゲームオブジェクトの更新を行う。PlayLeafのupdateから呼び出す
  }
  public void renderGameObjects(){   //ゲームオブジェクトの描画を行う。PlayLeafのrenderから呼び出す
  }
}

 LoadLeaf.java

public class LoadLeaf extends GameLeaf{
  public static String id="LoadLeaf";
  @Override
  public void init(){
    PlayNode node=(PlayNode)getParent();
    [スレッドを作成し、別スレッドでPlayNodeのloadを呼び出す]
  }
  @Override
  public GameLeaf update(){
    if([ロードが完了した]){
      return new PlayLeaf();
    }
    return this;
  }
  @Override
  public void render(){
    [ロード画面の表示]
  }
  @Override
  public void destroy(){
    [リソースの解放処理]
  }
}

 PlayLeaf.java

public class PlayLeaf extends GameLeaf{
  public static String id="PlayLeaf";
  @Override
  public void init(){
    
  }
  @Override
  public GameLeaf update(){
    PlayNode node=(PlayNode)getParent();
    node.updateGameObjects();
    if([ポーズボタンを押した]){
      return new PauseLeaf();
    }
    if([クリアした]){
      return new ClearLeaf();
    }else if([ゲームオーバーした]){
      return new GameOverLeaf();
    }
    return this;
  }
  @Override
  public void render(){
    PlayNode node=(PlayNode)getParent();
    node.renderGameObjects();
  }
  @Override
  public void destroy(){
  }
}

 ClearLeaf.java

public class ClearLeaf extends GameLeaf{
  public static String id="ClearLeaf";
  @Override
  public void init(){
    
  }
  @Override
  public GameLeaf update(){//PlayNodeのupdateGameObjectsを呼び出さないのでゲームはとまったまま
    if([スタートボタンを押した]){
      return new TitleLeaf();
    }
    return this;
  }
  @Override
  public void render(){       //通常のプレイ画面の上にClearの文字列を表示する
    PlayNode node=(PlayNode)getParent();
    node.renderGameObjects();
    [Clear文字列の描画]
  }
  @Override
  public void destroy(){
  }
}

 他のクラスは省略します。RootNodeを生成し、ゲームループ内で実行していくと、まずはタイトル画面が表示されます。スタートボタンを押すとステージセレクト画面が表示されます。ステージセレクト画面でステージを選択し、スタートボタンを押すとロード画面が表示され、ロードが開始されます。ロードが完了したらゲーム開始となります。ゲームクリアでプレイ画面の上に"Clear"文字列が表示され、スタートボタンを押すとタイトル画面に戻ります。