コメント5/22

今回は、アドベンチャーゲームの scene/GameState のデザインパターンに ついて説明します。

アドベンチャーゲームのクラス設計

アドベンチャーゲームでは、様々なシーンがあります。 シーンには、画面とメッセージがあり、質問を生成し、プレイヤーの回答に より、次のシーンに分岐します。

そのため、そのようなインターフェイスを設計します。

scene/Scene.java


package jp.ac.dendai.c.jtp.adventuresample.scene;
import jp.ac.dendai.c.jtp.adventuresample.Handler;
import android.view.View.OnClickListener;
public interface Scene extends OnClickListener {
	GameState next(int no);
	void start(Handler hand);
	int getMessageId();
	int getImageId();
	int getQuestionId();
	int getDateId();
}

つまり、シーンのオブジェクトは画面、メッセージ、質問事項などのデー タをすべて持ち、取得出来るようにしておきます。 基本的にはこの Scene インターフェイスを実装した各クラスで各シーン を定義しますが、共通の部分に関しては各シーンの親クラス AbstractScene を作っておきます。 つまり、第一シーンのクラスを First, 第二シーンのクラスを Second と すると、次のような定義になります。


interface Scene {     
  // 各シーンごとの情報収集用メソッド
}
public abstract class AbstractScene implements Scene {
  // シーン処理の共通部分
}
public class First extends AbstractScene{
  // 第一シーンの具体的なデータを return するメソッドの集まり
}
public class Second extends AbstractScene{
  // 第二シーンの具体的なデータを return するメソッドの集まり
}

さて、これで一応設計が出来たとして、次にゲームの進め方を考えましょ う。

例えば、主ゲームプログラムが Game クラスで定義されていたとします。 ゲームではシーンを移り変わって行くので、その変数を Scene 型の scene と します。 ゲームは scene を参照しながら画面を作り、メッセージを表示させ、そ して、プレイヤーに質問をし、そして質問の答えにより、次のシーンに移 ります。 次のシーンに移るには、 scene.next() などのメッセージ により次の scene に移る必要があります。

ここで、問題なのは、ゲームでシーンを取り扱う場合、変数に入れられる のはクラスではなく、オブジェクトインスタンスであることです。

つまり、先ほど定義したクラス First, Second などに対して、プログラ ムは直接処理をするのではなく、それらのオブジェクトインスタンスで処 理する必要があります。 これは、何らかの形で生成しておいて、そのオブジェクトを参照しなくて はなりません。 そして、各シーンのオブジェクトはそれぞれ使い捨てではなく、永続的に 存在させ、常に管理しておく必要があります。

シングルトンデザインパターン

デザインパターンを習い始めのころ、単純でわかりやすいデザインパター ンとしてシングルトンデザインパターンと言うのを学びまし た。 これは、例えばGUIにおけるメインウィンドウのような、常に単一性を保 持していて、それにアクセス可能にするためのテクニックです。

アイディアは、オブジェクトをクラス変数、 Java で言う static フィー ルドに保管します。 クラスに対して唯一のオブジェクトとなるので、 static メソッドでオブ ジェクトを取得できるようにし、コンストラクタでのオブジェクト生成を 抑制します。

シングルトンデザインパターンの基本形

  
class A {
    private static A instance;
    private A(){}
    public static A getInstance(){
        if(instance == null){
            instance = new A();
        }
        return instance;
    }
}

これをシンプルにしたものとして、プログラミング言語の起動時の特性を利用 したスコット・メイヤのシングルトンがあります。

スコット・メイヤのシングルトン

  
class A {
    private static A instance = new A();
    private A(){}
    public static A getInstance(){
        return instance;
    }
}

アドベンチャーゲームのオブジェクト管理

さて、今の問題は、アドベンチャーのシーンの派生クラスのオブジェクト インスタンスの管理です。 ここで、オブジェクト指向分析的には、このような管理をするクラスを作って しまえば良いという結論になります。 つまり、SceneControl などというようなクラスを作り、そこで、シーン のオブジェクトを管理し、次のシーンを導くという仕組みです。 SceneCotrol で、次のシーンに移るための next メソッドは、内部で現在 のシーンに対して next メソッドを呼び、次のシーンを出させます。 しかし、これでも次のプログラムのように課題が浮かび上がります。


public class SceneControl {
    private static Scene[] scens = {new First(), new Second(), ... }; //スコットメイヤのシングルトン
    private Scene currentScene;
    public void next(){
        currentScene = currentScene.next(); // これではシングルトンにならない
    }
}
interface Scene {     
  // 各シーンごとの情報収集用メソッド
    Scene next();
}
public abstract class AbstractScene implements Scene {
  // シーン処理の共通部分
}
public class First extends AbstractScene{
  // 第一シーンの具体的なデータを return するメソッドの集まり
    @Override
    public Scene next(){
        return ??? //どうやって、SceneControl で生成した唯一
                            //のオブジェクトを返す?
    }
}
public class Second extends AbstractScene{
  // 第二シーンの具体的なデータを return するメソッドの集まり
    @Override
    public Scene next(){
        return ??? //どうやって、SceneControl で生成した唯一
                            //のオブジェクトを返す?
    }
}

つまり、Scene インターフェイスにおける、next メソッドの戻り値が Scene 型だと、SceneControl の冒頭で生成したオブジェクトの参照を返 さないと、シングルトンが成立しません。 つまり、First クラスで勝手に return new Second() としてはいけ ません。 なお、次のようにするのはありだとは思います。


public class First extends AbstractScene{
  // 第一シーンの具体的なデータを return するメソッドの集まり
    private static Scene instance = new First();
    private First(){}
    public static Scene getInstance(){
        return instance;
    }
    @Override
    public Scene next(){
        return Second.getInstance();
    }
}
public class Second extends AbstractScene{
  // 第二シーンの具体的なデータを return するメソッドの集まり
    private static Scene instance = new Second();
    private Second(){}
    public static Scene getInstance(){
        return instance;
    }
    @Override
    public Scene next(){
        return Third.getInstance();
    }
}

しかし、これでは、すべてのシーンの実装において、継承できないシングルト ンを実装しなければなりません。 また、シングルトンを理解せずにシーンを作った場合、安易にオブジェクトを 生成して返してしまいますし、SceneControl で Sceneを管理しているとは言 い難いです。

または、次のように文字列で管理する方法もあるかもしれません。


public class SceneControl {
    private static Scene[] scens = {new First(), new Second(), ... }; //スコットメイヤのシングルトン
    private Scene currentScene;
    private TreeMap<String,Scene> sceneMap;
    public SceneControl(){
        sceneMap = new TreeMap<>();
        for(Scene s : scenes){
            sceneMap.put(s.getClass().getName(),s);
        }
    }
    public void next(){
        currentScene = sceneMap.get(currentScene.next());
    }
}
interface Scene {     
  // 各シーンごとの情報収集用メソッド
    String next();
}
public abstract class AbstractScene implements Scene {
  // シーン処理の共通部分
}
public class First extends AbstractScene{
  // 第一シーンの具体的なデータを return するメソッドの集まり
    @Override
    public String next(){
        return "Second";
    }
}
public class Second extends AbstractScene{
  // 第二シーンの具体的なデータを return するメソッドの集まり
    @Override
    public String next(){
        return "Third";
    }
}

しかし、文字列によるプログラムロジックのコントロールは Stringly Typed Programming などとも呼ばれるアンチパターンなので、他の解決法 を模索したいです。

そこで、列挙型を考えてみますが、単純な使用方法ではうまくいきません。


public enum GameState {
    first, second, third      
}      
public class SceneControl {
    private Scene currentScene;
    private TreeMap<GameState,Scene> sceneMap;
    public SceneControl(){
        sceneMap = new TreeMap<>();
        sceneMap.put(Scene.first,new First());
        sceneMap.put(Scene.second,new Second());
    }
    public void next(){
        currentScene = sceneMap.get(currentScene.next());
    }
}
interface Scene {     
  // 各シーンごとの情報収集用メソッド
    GameState next();
}
public abstract class AbstractScene implements Scene {
  // シーン処理の共通部分
}
public class First extends AbstractScene{
  // 第一シーンの具体的なデータを return するメソッドの集まり
    @Override
    public GameState next(){
        return GameState.second;
    }
}
public class Second extends AbstractScene{
  // 第二シーンの具体的なデータを return するメソッドの集まり
    @Override
    public GameState next(){
        return GameState.third;
    }
}

Java の enum

Java の列挙型は他のプログラミング言語に無い特徴を持っています。 それは、各列挙子にメソッドを記述できることです。 記述方法は、列挙子の宣言の後に;(セミコロン)を書いた後で列挙型のメ ソッドを定義し、必要に応じて各列挙子の宣言の直後に{}(中括弧)でメソッ ドをオーバライド出来ます。

例1

  
enum A {    
  one, two;
  public void hello(){
    System.out.println("Hello World.");
  }
}

例2

  
enum A {    
one
{
  @Override
  public void hello(){
    System.out.println("Hello World. I am One.");
  }
,
two;
  public void hello(){
    System.out.println("Hello World.");
  }
}

すべての列挙子でオーバーライドする場合は、列挙型の宣言の部分を abstract にできます。

例3

  
enum A {    
one
{
  @Override
  public void hello(){
    System.out.println("Hello World. I am One.");
  }
,
two
{
  @Override
  public void hello(){
    System.out.println("Hello World. I am Two.");
  }
;
  public abstact void hello();
}

さて、列挙型でシングルトンを成立させるにはどうすれば良いでしょうか? つまり、前節最後の例の列挙型 を使ったもので、 GameState.first に対して next メソッドで Second のオブジェクトインスンタンスを返すような仕組みを作りたいです。

Java の enum は一般のクラス同様にインスタンス変数やコンストラクタ があり、 コンストラクタ呼び出しは列挙子宣言の直後に()丸括弧で引数を与えます。

例4

  
enum A {    
one(1)
,
two(2)
;
  private int value;
  public A(int x){
    value = x;
  }
  public int getNum(){
    return value;
  }
}

以上より GameState.first.getScene() で唯一の First クラスのオブジェ クトが得られるようにするには次のようにします。

enum を利用した複数クラスのオブジェクトを管理するシングルトン

  
enum GameState {    
first(new First())
,
second(new Second())
,
third(new Third())
;
  private Scene scene;
  public GameState(Scene x){
    scene = x;
  }
  public Scene getScene(){
    return scene;
  }
}

このようにすると、GameControl のような検索の仕組みが必要なく、各シーンで はGameState の列挙子を返せば良いようになります。


interface Scene {     
  // 各シーンごとの情報収集用メソッド
    GameState next();
}
public abstract class AbstractScene implements Scene {
  // シーン処理の共通部分
}
public class First extends AbstractScene{
  // 第一シーンの具体的なデータを return するメソッドの集まり
    @Override
    public GameState next(){
        return GameState.second;
    }
}
public class Second extends AbstractScene{
  // 第二シーンの具体的なデータを return するメソッドの集まり
    @Override
    public GameState next(){
        return GameState.third;
    }
}

坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科