状態遷移の管理と言われても、最初は何のことか分からない人も多いと思います。そのため、まずは順序立てて説明していきます。理解したら、もうこの状態遷移での管理を使わずにはいられなくなると思います。それほど便利な管理法だと思っています。なお、今回は初心者向けの解説なので、タスク管理等の考え方には触れません。もう少し原始的な泥臭い方法で解説しますので、こちらのほうが便利なのにというのは言わない約束でお願いします。


  • 状態とは
そもそも状態って何なのでしょうか。見た目から、あるいは動作から、分類できる区分…と言っておきましょうか。先にテストで出したプログラムを例にしてみます。起動すると最初に艦船の配置を行いました。続いてその艦船を表示しました。さらに攻撃しています。これら黄色で示した全てが状態です。

 1. 艦船の配置
 2. 艦船を表示
 3. 攻撃の入力

よく考えると攻撃という状態は、さらに内容を細かく分類することが出来ます。少し細かくなりますので、また別途ナンバリングします。

 3-1. 攻撃を促すプロンプトを表示
 3-2. 英字の入力
 3-3. 数字の入力
 3-4. 当たり判定

このように状態は、どんどん細分化していくことが出来ます。どこまで細分していけばよいかは、処理のまとまり毎だと考えると分かりやすいかもです。プログラムを組む時に、コメントを表題のように付けると思いますが、それが最小単位です。


  • 状態の管理
処理を細分化して状態毎に分けたのなら、その状態に対して名前を付けるのが管理しやすいです。ここで役に立ちのが列挙体です。折角なので C 言語で説明していきますが、考え方そのものは言語に依存しません。なんならアセンブラでも使えます。そのため、ご自身の得意な言語に置き換えて見てもらえると嬉しいです。

さて、前回のテストの main 関数内では、3つの状態を順次呼び出していました。配置、表示、攻撃です。そこで、これを列挙体で名前を付けてしまいます。

enum EGameStatus { DEPLOY, DRAW, ATTACK };
さて、一番最初は配置から始まりますので、グローバル変数 gGame の初期値に DEPLOY を設定します。

EGameStatus gGame = DEPLOY;
この gGame の変数の値によって、呼び出す関数を変化させるように main を書き換えます。

/// <summary> /// メイン関数 /// </summary> /// <returns>終了コード</returns> int main(void) { while (true) { switch (gGame) { case DEPLOY: deployShips(); break; // 配置 case DRAW: drawGameFiled(); break; // 表示 case ATTACK: attackPlayer(); break; // 攻撃 } } return EXIT_SUCCESS; }
これだけだと gGame の値が変化しませんので、延々と配置を呼び出します。そのため、呼び出し先の関数内で、次の状態に変わるように gGame の値を変更します。例えば、配置の次は表示なので、gGame の値は DRAW に変更します。

/// <summary> /// 敵船を配置する /// </summary> void deployShips(void) { // 配置処理 // (省略) gGame = DRAW; }
同様に表示処理の最後にも gGame を変化させます。

/// <summary> /// ゲームフィールドを表示する /// </summary> void drawGameFiled(void) { // ゲーム盤面表示処理 // (省略) gGame = ATTACK; }
これで今まで通り処理が流れていくと思います。この状態の移り変わりを状態遷移と呼びます。
※ 手軽に飲めて美味しいですよね。ドライブやゴルフによく飲んでいます。

  • 状態遷移のメリット
状態の管理を変数の値によって制御できるようにしました。そのため、やろうと思えばいとも簡単に流れを変更することが出来ます。例えばランダムで設定された配置が気に入らない場合に、再配置に流れを変えたいとします。以前のように、main 関数内に流れのまま関数呼び出しが記述されていては、流れを変えることは出来ません。しかし、流れを状態遷移管理変数(この場合は gGame)の値で制御していますから、その値を変えるだけでいとも簡単に再配置に戻すことが出来ます。

試してみます。配置確認の状態を SURE として追加します。

enum EGameStatus { DEPLOY, DRAW, SURE, ATTACK };
メイン関数の処理を分岐している switch に配置確認の関数呼び出しを追加します。

switch (gGame) { case DEPLOY: deployShips(); break; // 配置 case DRAW: drawGameFiled(); break; // 表示 case SURE: sureDeploy(); break; // 確認 case ATTACK: attackPlayer(); break; // 攻撃 }
表示の次を確認に変更します。

/// <summary> /// ゲームフィールドを表示する /// </summary> void drawGameFiled(void) { // ゲーム盤面表示処理 // (省略) gGame = SURE; }
最後に確認関数を追加します。

/// <summary> /// 配置を確認する /// </summary> void sureDeploy(void) { char key; printf("%sAre you sure ? : ", FWHITE); do { key = tolower(_getch()); } while (key != 'y' && key != 'n');
printf("%s\n", key == 'y' ? "Yes" : "No"); gGame = key == 'y' ? ATTACK : DEPLOY; }
どうでしょうか。たったこれだけの追加で、いとも簡単に処理を巻き戻すことが出来るようになりました。このように状態遷移を用いると、処理の流れを簡単に制御することが出来ます。これはゲームプログラムに限りません。状態が存在する全てのプログラムに対して有効な手段となります。こちらの考え方を使い続けて、是非自分のモノにしていただければ幸いです。


  • 宿題
さて、実は上記の説明だけでは、処理は正常に流れて動作しますが、ゲームとしては大変問題のある動作になっています。これはプログラム制作における最もバグを作りやすい状態を再現しているとも言えます。宿題は、このバグを取り除く事です。次回C言語講座で答え合わせします。下のリンクから解析用のプログラムを受け取ってください。ではまたー

Homework01
20230520-1930

※ こちらもタンパク質が不足しそうな日によく飲んでます。なによりこれ、凄く美味しいのでお気に入りなんです!