ホーム < ゲームつくろー! < デザインパターン習得編

Memento
  〜一時的なセーブロードをサポートするオブジェクト


@ オブジェクトのセーブロードのジレンマ

 ゲームで「取り消し」を求める機会は意外と多いと思います。ユーザーが設定を色々と変えた後、やっぱりやめたと取り消したり、RPGでイベントを途中でやめたりすることなどは良くあります。パズルゲームなどでは1つ前の状態に戻すこともゲーム性の1つになることがあります。
 オブジェクトが持つメンバ変数の値の出し入れが可能だと、元に戻す事が可能になります。しかし、オブジェクトが保持しているデータというのは「カプセル化」の概念からして外に公開したくはないのです。でも、外に出さないとセーブが出来ない。そこで、次のように考えます。

「オブジェクトの中で自分の状態を箱の中に梱包してしっかり封をして、外に出してもそれを見られないようにする。必要になったら箱を戻してもらって、自ら開封してメンバ変数を再設定する」

 この郵便配達のようなデータ保存法がMementoパターンです。Mementoというのは「備忘録」、つまり忘れないようにメモしておいた記録の意味です。


↑Caretakerとは世話人・管理人の意味です



A 箱の中身はなんでしょう?

 Mementoパターンを用いるとき、絶対に必要なことがあります。それは、「梱包した箱(Mementoオブジェクト)の中身は自分(Originator)しか見ることが出来ない」ということを保障することです。これは言語サポートがされていない場合もありますが、C++には「friend」というありがたい機能がサポートされているために、実現可能です。

 「friend」というのは、特定のクラスがあるクラスの中で定義されたprivateとprotected関数および変数にアクセスすることを許可するための宣言です。例を挙げましょう。 

class MyFriend;   // 友達(宣言だけ)

class MyTreasureBox
{
private:
   Item m_Treasure;

protected;
   Item ShowMyItem(){ return m_Treasure; }

protected:
   friend class MyFriend;
};

MyTreasueBoxクラスの中でMyFriendをフレンドクラスとしておくと、MyFriendクラス内でMyTreasureBoxのメンバ関数や変数にアクセスできます。MyFirendオブジェクトはShowMyItem関数を呼び出すと、m_Treasure(宝物)を見ることができるわけです。

 Mementoパターンの場合、Mementoクラス(箱)のフレンドクラスは箱を開けることが出来る自分(Originatorクラス)です。他のクラス(Caretakerクラス)はMementoオブジェクトを持ち運ぶことだけできるようにします。

class Memento
{
private:
   friend class Originator;   // 箱を開けることが出来る友達
   State* m_pFriendState;   // 友達(Originator)の状態

private:
   Memento();   // コンストラクタをprivateで宣言(生成の禁止)
   void SetState(State*);   // 状態を格納
   State* GetState();        // 状態を取得

public:
   ~Memento();   // デストラクタだけは公開
};


 コンストラクタがprivateで宣言されているので、フレンドクラス以外は箱を作ることすら出来ません。箱は友達であるOriginatorクラスが作成します。

class Originator
{
private:
   State *m_pState;   // 自分の状態を格納

public:
   Memento* CreateMemento();    // Stateを詰めた箱を「封印して」作成
   void SetMemento(Memento*);   // 箱を受け取って中身を(State)を取り出し格納
};

 外部からCreateMemento関数が呼ばれるたびに、自分の状態を箱につめて、それをしっかり封印して外部の人に渡します。外部の人(Caretakerクラス)はMementoクラスのフレンドではないので、Mementoクラスでprivate宣言された関数や変数に一切アクセスができません。完全封印です。状態を元に戻したいときにはSetMemento関数に封印した箱を渡します。


B パズルゲームの巻き戻し

 例として、パズルゲームの巻き戻しをMementoパターンで実装してみます。10×10の升目に3色の色(赤、青、黄色)が塗られるパズルとします。Managerクラスは升目の色、塗られた枡の数、スコアを管理しているとしましょう。ある状態を一時的に保存しておくのにMementoクラスを用います。

class Manager
{
private:
   int m_Score;             // スコア
   int m_PaintNum;        // 塗られた枡の数
   int m_PaintAry[100];   // 塗られた色

public:
   void SetMemento(Memento *mem);    // 状態を戻す
   Memento* CreateMemento();                // 状態を外部に保存

   void Paint(int x, int y, int color);   // 指定の枡を塗る
   void Reset();                           // 升目をリセット
   void CalcScore();   // 点数を計算
};


MementoクラスはManagerクラスのメンバ変数を格納するようにします。

class Memento
{
private:
   int m_Score;             // スコア
    int m_PaintNum;        // 塗られた枡の数
    int m_PaintAry[100];   // 塗られた色
   
private:
   Memento(){};   // コンストラクタをprivate宣言(生成の制限)

   // メンバ変数の格納と取り出し
   void SetScore(int scr);
   int GetScore();
   int GetPaintNum();
   void SetPaintNum(int num);
   void SetPaintAry(int *Ary, int size);
   void GetPaintAry(int *Ary, int size);

   friend class Manager;   // マネージャクラスをフレンドに

public:
   ~Memento(){};
}

 呼び出し側では、ManagerオブジェクトからMementoオブジェクトを生成します。

Manager m_Mng;
Memento* m_Memento;

// 内部データの保存
m_Memento = m_Mng.GetMemento();

// データの復元
m_Mng.SetMemento(m_Memento);

 簡単です。しかし、呼び出し側クラスはMementoオブジェクトを保持しておくだけしかできません。Mementoオブジェクトのポインタを通しても、データを取り出すことができません。

 Mementoパターンのクラス図を示します。



B Mementoの限界

 Mementoオブジェクトは、兎にも角にもOriginatorのデータを保存しておかなければなりません。ということは、Originatorが複雑になるほど、その保存作業が大変になることがわかります。例えば、Originatorが可変的なビットマップを保持している場合、Mementoオブジェクトを生成するたびに、非常に大きなメモリのコピーが必要になります。これは時間もかかりますし、メモリが足りなくなるかもしれません。

 そういう場合、差分データだけを保持する形式にすると、大幅にメモリと時間の節約になることがあります。ビットマップも10ピクセルくらいしか変更していないのならば、その変更箇所だけを保存しておけば、大きなメモリを占有することもなくなります。ただ、差分データはそれなりのアルゴリズムの工夫が必要になってきます。差分にするか、まるごとデータを保持するか、場合によって使い分けるのが吉ですね。