僕の観察日記

僕が感じたことを書いていき、自分のことを観察、記録するブログ

制作の進捗とデザインパターン

四日ぶりの更新だ。

今日は制作の進捗報告とデザインパターンについて書こうと思う。

今現在制作の方はどちらかというと進行は遅い。

プレイヤーキャラがようやく動き始めたところだ。

だが、決してサボっていた訳ではない。

プログラムを組む上での基盤を固めるために時間を費やしていた。

どういう風に組むのがいいのかと先生が皆に配っているソースを全て読んだり、クラス図をastahで記述したりしてから、Android制作の時に参考にした書籍、ソースをちらちら見ながら実際にコードを書き始めた。

そして一週間前ほど前に案の方向性が確定し、そこからプレイヤーの仮モデル作成、プレイヤーのデザイン案を考えてくれる人がいたので依頼をするなど、最近になって大きく動き始めた感じだ。

これからモデルを歩かせたり跳ばせたりする予定だ。

 

今までの段階で基盤を固めたと書いた。

サウンドを鳴らせたりすることもできずまだまだ足りないところもあるが、まず作り始める上で必要な部分の基盤は固めた。

それはどこかと言うと、ゲームのシーン偏移や入力部分、そしてキャラクターの状態偏移だ。

この三つにはデザインパターンを使っていて、シーンとキャラクターの状態偏移には「State」パターン。入力部分には「Adapter」パターンを使っている。

これらを使うのと使わないのとでコードの見易さや保守性が大きく変わるのではないだろうか。

まだまだ開発初期段階だが、それでもありがたみがわかるくらいの威力ではある。

今から自分の中でどれくらいこの二つのパターンについて理解しているかの確認のため、そしてまだデザインパターンとかよくわからないという人のために説明をしようと思う。

 

Stateパターン

これは簡単に言うと状態をクラスにしてしまうというものだ。

例えばゲームの主人公である「Player」クラスを作るとする。

仮にアクションならばきっと歩いたり走ったり跳んだりするだろう。

しかし、ダメージを受け続けて瀕死状態になった時に行動が変わるとする。

これを疑似コードで表してみよう。

 

class Player{

    void Walk()
    {
        if(m_DamageFlag)
        {
            瀕死時の処理
        }
        else{
            通常時の処理
        }
    }

    void Run()
    {
        if(m_DamageFlag)
        {
            瀕死時の処理
        }
        else{
            通常時の処理
        }
    }

    void Jump()
    {
        if(m_DamageFlag)
        {
            瀕死時の処理
        }
        else{
            通常時の処理
        }
    }

}

 

こんな感じだろうか。

まだ許せる人もいるかもしれない。

しかし、これに加えて覚醒状態があったとする。

…もう書きたくない。

しかもアクションゲームだ。

きっと他にも関数が増えるかもしれない。

これは困った。

正しく動いたとしても凄く見辛いし、これに加えてバグが出たらどうだろうか。

大量のif、else文に目を回すだろう。

これを解決するためのデザインパターンが「State」だ。

 

これを使うとどうなるか。

コードを見てもらうとよくわかる。

class Normal{
    void Walk()
    {
        通常時の処理
    }
    void Run()
    {
        通常時の処理
    }
    void Jump()
    {
        通常時の処理
    }
};

class Damage{
    void Walk()
    {
        瀕死時の処理
    }
    void Run()
    {
        瀕死時の処理
    }
    void Jump()
    {
        瀕死時の処理
    }
};

 

どうだろうか。

通常時と瀕死時の状態をわかりやすく表している。

これなら覚醒状態が追加されたり関数が追加されても問題なさそうだ。

何がこんなにも見やすくしているのだろうか。

それは状態チェックのためのif文がないことである。

状態はクラスになっているため、いちいちチェックする必要がなくなりすっきりとしている。

 

ではこのStateパターンをどう使うかだ。

これには継承を使う。

例えばStateクラスと言う基底クラスがあり、派生クラスにNormalクラスやDamageクラスがあると言った感じだ。

このStateクラスのポインタをPlayerクラスのメンバとして保持する。

ポインタであることが重要だ。

ポインタでなければStateパターンは使えない。

何故ならアップキャストが関わってくるからだ。

これに関してはわからなければ各自調べるなりしてほしい。

別に難しい話ではなく、基底クラスのポインタは派生クラスのオブジェクトを指すことができるというだけだ。

仮想関数などにも気を付けよう。

少し話が逸れたが、このポインタを仮にm_Stateとする。

そしてPlayerの初期化部分でNormalクラスをnewしてm_Stateに代入してやればいい。

そうすればm_State->Walk()で通常時のWalk()関数が呼ばれる。

 

ここで一つ疑問が浮かぶ。

状態偏移をどうすればいいのか?

今のままでは通常時の関数は呼べても瀕死時の関数が呼べないじゃないか。

これは、状態を持っているPlayerクラスに訊いてみよう。

つまり、状態偏移はPlayerクラスの仕事である。

Playerクラスに状態偏移関数を追加しよう。

ChangeState(State *_state)とでもしようか。

この引数に新たな状態クラスを渡すことで、その状態に偏移することができる。

この関数の中でNormalクラスのポインタをdeleteし、引数で渡されたDamageクラスのアドレスを代入しよう。

 

また一つ疑問だ。

代入しようと言ったが、どこで代入すればいいのだろう。

 

ここでダメージを受けた時のDamage()関数を追加しよう。

この関数内にダメージを受けた時の処理を記述する。

つまり、ここで瀕死かどうかの判断を行い、瀕死の体力になっていた場合先ほどのChangeState(State *_state)を呼び出すのだ。

もちろん、これも各状態クラスの中に入れてしまおう。

 

おそらくこれで最後の疑問だ。

状態偏移はPlayerクラスの仕事だと言ったにも関わらず、Stateクラスとその派生クラスが持つDamage()関数の中で呼び出すと言っている。

これはどういうことかというと、StateクラスにPlayerクラスのポインタを持たせておき、コンストラクタでPlayerクラスのアドレスを渡してやるのである。

こうすることでStateクラスの中にあるDamage()関数からもChangeState(State *_state)が呼べるのである。

 

これでStateパターンの説明は以上だ。

正直凄く分かりづらいと思う。

だが、説明前半のコードが見やすくなるという部分さえわかってもらえ、そしてこれがデザインパターンを学ぶきっかけになってくれたらと思う。

 

説明の前に二つのパターンについて説明すると言ったが、Stateだけで随分とスペースを取ってしまったので、Adapterはまた次回にする。

「Stateの説明がようわからん!」とか「もっとStateのコードが見たい!」と言った人はぜひ調べてほしい。

ここにもっとコードを書こうかとも考えたが、当然インデントもしれくれないし(VisualStudioからコピペしても崩れる)正直厳しい。

ただコードがすっきりするということだけは伝えたかったので少し頑張って書いた。

少しでも制作に活かしてもらえたらと思う。

今回はこれで以上だ。