見出し画像

状態遷移事始め。まずは闇を知ろう

ゲームプログラムに限らず、プログラムでは大概状態遷移が必要になります。例えば「ボタンを押したら攻撃する」。企画がさらっと書きそうなこんな短い文章を見た時、プログラマの脳みそには下のようなイメージが思い浮かびます:

画像1

で、これをプログラムで実現しなくちゃいけないと。ここでプログラム初学者は「どうやったらいいのかな?」と悩み、ベテランは「どの状態遷移の実装方法を適用すべきか?」で悩みます。そう、状態遷移ってプログラムでアホ程沢山出現するのに、初学者もベテランも毎度悩まされるんです。

状態遷移に対応する「武器」を増やそう

状態遷移を実現する方法は沢山あります。ささっと簡単に書ける物から物凄く重厚なシステム上で動く多機能な物まで。それは差し詰めRPGの武器のように多彩です。「敵はとある状態遷移。その敵に効果的な武器はこの炎の剣か…それとも氷結の斧か…。適切な武器を選ばないと痛い目に合う…ふむぅ…」。ベテランは選択をミスった時の痛みを知っています。だから武器選びに悩みます。でも初学者はこん棒しか持っていません。「こちとら選択肢なんて無いんでい、ちくしょー」とこん棒でポカスカ挑みます。ダメージは多少与えられますが、敵によってはこちらのダメージの方が甚大に。下手をすると死亡(制御不可)してしまうかも…。その時思うんです「くそっ…俺にも剣があれば…ぐふぅ」と。そう、だから死亡に至らないように、まずは状態遷移に対応する武器(実装方法)を増やしましょう。

if文というこん棒で闇に挑むとどうなるか?

多分状態遷移で誰しもが一度は使うのがif文です。ほぼすべてのプログラム言語に備わっています。先程の状態遷移をC++のif文で書くとこんな感じです:

if( state == ST_IDLE ) {
    idle();    // 待機中の振る舞い
} else if ( state == ST_ATTACK ) {
    attack();  // 攻撃中の振る舞い
}

stateは状態を表す値(列挙型)です。識別できれば何でもOK。このコードを日本語で語るなら「もしstateがST_IDLEだったらidle関数を実行。そうでなくてST_ATTACKだったらattack関数を実行」となります。if文は誰でも簡単に扱えてコストもやすい、こん棒みたいな武器ですね。

ここで気を付けたいのが、上のコードは1秒間に何十回も通るという事。ほとんどのプログラムは時間を物凄い短い間隔に区切り、毎回「次何する?」と様々な箇所で尋ねるスタイルを取ります。そうしないと、沢山の事を同時に行えないからです。でも実はその同時性こそが状態遷移の闇の元凶なんです。良く考えてみて下さい。僕らが日常過ごすこの世界で、同時にどれだけ多くの事が判断されているのかを。その縮図である上のコードをそのまま拡張して世界を表現するとどうなるか…。もちろん、恐ろしい事になります。

驚愕if文10000行!

主人公が待機(IDLE)と攻撃(ATTACK)だけじゃゲームになりません。せめて移動もしたいので「歩く(WALK)」を追加しましょう。OK~簡単だぜ:

if( state == ST_IDLE ) {
    idle();    // 待機中の振る舞い
} else if ( state == ST_ATTACK ) {
    attack();  // 攻撃中の振る舞い
} else if ( state == ST_WALK ) {
    walk();    // 歩行中の振る舞い
}

ST_WALKという状態変数を追加して、先程のif文で判定すれば終わりです。追加も楽々、この調子でどんどん状態を記述していけばいいじゃんね、と思う訳です。でも、敵はどんどん強く(=複雑に)なっているのに、あなたが手に持っているのは未だこん棒のまま。大丈夫なわけがありません。その恐怖はエネミーを追加した時に片鱗を出し始めます。

攻撃対象のゴブリンを追加します。ゴブリンも主人公と同じように待機するし歩くし攻撃します。ゴブリンの状態遷移を記述してみましょう:

// 主人公の状態
if( state == ST_IDLE ) {
    idle();    // 待機中の振る舞い
} else if ( state == ST_ATTACK ) {
    attack();  // 攻撃中の振る舞い
} else if ( state == ST_WALK ) {
    walk();    // 歩行中の振る舞い
}

// ゴブリンの状態
if ( state == ST_IDLE ) {
    gb_idle();   // ゴブリン待機中
} else if ( state == ST_ATTACK ) {
    gb_attack();    // ゴブリン攻撃中
} else if ( state == ST_WALK ) {
    gb_walk();     // ゴブリン歩行中
}

一気にコードが倍に増えました。でも状態遷移は実現出来ています。動けば正義だ、ガハハ。でも、皆さんはもうお気付きでしょうが、このまま突き進むととんでもない事態になってしまいます。主人公は一人かもしれませんが、ゴブリンは多分沢山いるでしょう。それらゴブリン全員分をこうやって記述するの…?え、マジで?うん、この方式を取るなら「マジ」です。もちろんfor文を使って回避は可能ですが、エネミーの種類が増えたらその分同様の記述をしていく事になります。for文を使ったとしてもヤバさの本質は大して変わりません。

これヤバイですよねぇ…。そうヤバいと普通思うんですよ。でもね、僕は本業でゲームプログラマをしていますが、嘘みたいな本当の話、上のif文大連結で書かれた状態遷移のコードを現場で見た事があります。その行数なんと1万行!しかも、その規模のコードファイルが何十個もあるという…。恐ろしい闇を見て開いた口が塞がっていない傍らで「このゲーム○○に移植して機能追加したいんだけど?」と言われてミリ秒で「無理!(心:ヤダ!)」と断りました。その案件はそのままお蔵入りになりましたが、闇もここまで来ると漆黒だなと鳥肌が立ったのを覚えています。

プロの現場でもそんな事あるの?と思ったかもしれませんが、あります!それはですね、まぁ色々な事情が絡むのですが、根源的には最初に状態遷移を記述した人が武器を見誤ったからなんです。「if文で分けていけばいいや」というノリでコピペを繰り返して規模を拡大してしまった。そして一度そういうルールで突き進むと、もはや後任の人もそのルールに従わざるを得ない。もちろん「これヤバイよねぇ」と思ってはいるけども、奇跡のバランスで動いている状態遷移を書き直すなんて怖い事はもう出来ません。仕方なしの追従。それが延々と受け継がれての1万行…という事なんです。

if文が悪いんじゃない、選んだ奴が悪い!

if文の名誉のために敢えて念を押しますが、状態遷移にif文を使っても勿論構いません。先のコード、正直直感的に分かりやすいですよね。そう、規模が小さくて殆ど拡張しないような所であれば、別にif文で状態遷移を書いても何の問題も無いんです。むしろ良いくらい。よく「if文とかぁswitch文でさぁ状態遷移書くとぉ、カオスになるから駄目だよな」という頭ごなしな全否定を目にしますが、「違う!カオスになるのはif文のせいじゃねぇ、使っちゃダメな所で使うプログラマが悪いんじゃ!!」とぐーパンしたくなります。そう、悪いのは武器を見誤ったプログラマなんです。ですから、状態遷移という敵に挑んで勝利するために、プログラマとしての武器を増やし選択肢を広げる事が本当に大事なんです!

とまぁ与太話が過ぎて3000文字越えてしまいましたので、状態遷移の武器のお話はまた次回に(^-^)/

この記事が気に入ったらサポートをしてみませんか?