ゲームシーケンス遷移
今回はいつも使っている簡単なシーケンス遷移の実装方法を紹介します。
以下に特徴を示します。
- 仮想的な木構造であり、実行時には1つのリストになる
- 木の葉ノードGameLeafクラスと木の内部ノードGameNodeクラス(GameLeafを継承している)からなる
- 実行時にはGameNodeは1つのGameNodeまたは1つのGameLeafをもち、GameLeafのupdate、renderが毎フレーム呼ばれる
- 子ノードは親ノードがもつリソースにアクセスできる
仮想的なシーケンス構造が下のようになっているとすると、
シーケンス構造 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の戻り値は
- thisポインタ
- null
- 同じ親に属するGameLeafまたはGameNode継承クラス
- さらに上の親ノードの仮想的な子
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"文字列が表示され、スタートボタンを押すとタイトル画面に戻ります。