不変でコンパクトならswitch~caseで状態遷移したっていい
switch~case文。場合(case)を切り替える(switch)というそれっぽい名前が付いているのに、状態遷移の世界ではやけに敬遠されちゃう悲運な構文です。なぜそんな疎き目に合うのか。次の「2つのロゴ表示後にタイトルへ遷移」する状態遷移プログラムを見ると多分その理由を感じ取れます:
switch( gameState ) {
case GS_LOGO1:
switch( logo1State ) {
case FADE_IN:
if ( logo1fade( true, fade ) )
logo1State = IDLE;
break;
case IDLE:
time++;
if ( time >= 120 )
logo1State = FADE_OUT;
break;
case FADE_OUT:
if ( logo1fade( false, fade ) ) {
time = 0;
gameState = GS_LOGO2;
}
break;
}
break;
case GS_LOGO2:
switch( logo2State ) {
case FADE_IN:
if ( logo2fade( true, fade ) )
logo2State = IDLE;
break;
case IDLE:
time++;
if ( time >= 120 )
logo2State = FADE_OUT;
break;
case FADE_OUT:
if ( logo2fade( false, fade ) )
gameState = GS_TITLE;
break;
}
break;
case GS_TITLE:
...
}
え?既視感で眩暈ですって…?大丈夫ですか?お気を確かに(-_-;
流れをざざっと説明すると、gameStateには最初GS_LOGO1という最初のロゴを表示する状態値が入っています。switch文でそれを判断してLOGO1の表示スタート。最初にFADE_IN状態なので文字通りフェードインしてきます。完全にフェードインしたらIDLE状態にスイッチ。120フレーム待ってFADE_OUTへ。フェードアウトしきったらgameStateをGS_LOGO2に切り替えて2番目のロゴ表示へ。以下ロゴ1と同じ動作を繰り返し、タイトルへ。ふぅ~
ゲームの状態遷移はこのレベルで細かく制御する必要があります。そのせいでたったこれだけの状態遷移を記述するのにも何だか凄いコードがごちゃごちゃ…。そう、switch~case文が状態遷移で敬遠されるのは、一にも二にもその見た目の悪さなんです。「おいおい、こいつぁー見てくれは悪いが出来る子だぜい」というチャキチャキ江戸っ子なノリで済ませてはいけません。プログラムコードにおいて見た目の悪さは即「保守性と可読性」を犠牲にします。実際のゲームプログラムは数万行を超えます。もしコードが上のノリで書かれていたら…恐ろしくて震えます。
switch~case文を使うか否かの判断は?
これはswitch~case文が悪いのでしょうか?いえ違います。上のような箇所でこれを選択してごちゃーっと書いてしまうプログラマが良くないんです。gameStateはきっとこの後もっと色々追加されるに違いありません。そういう変化が激しくて複雑な状態遷移を管理するにはswitch~case文では荷が重すぎるんです。
ではこの構文を適用する判断基準は何なのか?この構文は非常に固定的です。変化に弱いんです。ですから追加がほぼ無い普遍的な箇所なら可能性があります。でもただ普遍なだけではマズイ局面もあります。もしcase文が100個連なっていたらどうでしょう。「この100個は変わらないんだよ!」と豪語されても、switch(){ ... の閉じ括弧どこだよ!って絶対なります。その段階ですでに保守性は最悪です。よって遷移先が少ない事も条件になるでしょう。僕の経験則ですが、case文による状態遷移は精々5~6個くらいまでが限度かなと思います(書き方によってはもう少しいけますが)。それ以上なら普遍的であっても他の武器に切り替えた方が無難です。
switch~case文のネストは2重でも嫌!
冒頭のコードを見てわかるように、switch~case文内にswich~case文が入る(ネストする)と途端にカオスっぷりが増します。そういう書き方はC言語でも他の言語でも出来ますが、多分人間工学的に「してはいけない」書き方かと思います。個人的にコンパイラフラグでエラーにして欲しいくらいこれは嫌です(-_-;
書き方の選択肢がそれしかないなら仕方無いですが、内側のswitch~case文は別の関数に逃がすなど可読性を上げる回避手段はあります。それをしないのはやっぱりプログラマの怠惰。その証拠に次の書き方をご覧ください。
case文内を関数呼び出しだけにしてしまう
先の状態遷移を次のように書いたらどうでしょうか?
switch( gameState ) {
case GS_LOGO1: showLogo1(); break;
case GS_LOGO2: showLogo2(); break;
case GS_TITLE: showTitle(); break;
}
まぁとってもスッキリ(^-^)。ロゴ1の表示は全部showLogo1関数に任せてしまいます。そのshowLogo1関数内でgameStateをGS_LOGO2に切り替えれば、ここで判断されてshowLogo2関数がスタートする。このノリであれば見た目も可読性も随分と良くなります。
元々switch~case文はこういう感じで手短にささっと書く分には悪くない構文なんです。プログラマが手抜きをして行間にごそごそと書くから、いたずらにきちゃなくなる。それを避けてコンパクトにさえすればswitch~case文を適用しても構わない状態遷移は増えるはずです。
列挙型を作るのが面倒臭くなってくる
コンパクトになれば案外悪くはないswitch~case文。ただ、状態遷移に使う時にswitch文の引数に対応する列挙型をいちいち作る必要があるのがネックです。GS_LOGO1でshowLogo1関数、GS_LOGO2でshowLogo2関数、GS_TITLEでshowTitle関数…って同じ名前やーん!と。遷移の数だけ列挙型。ゲームは凄まじい状態遷移の嵐ですから、この2重化は馬鹿にならない苦痛になってきます。かといってこれはやっちゃいけません!
int gameState = 0;
switch( gameState ) {
case 0: showLogo1(); break;
case 1: showLogo2(); break;
case 2: showTitle(); break;
}
case文に数字。これはいわゆる「マジックナンバー」で、数字に暗黙の意味を持たせてしまう事で保守性を下げてしまうアンチパターンです。これが行き過ぎると「えーと157番は…何だっけ?」みたいな事に絶対なります。こういうのもたまに見かけますが、プログラマの怠惰です(-_-;
でも列挙型メンドクサイ!メンドクサイ!メンドクサーイ!!そう感じてきたら、次の武器である「関数ポインタ」を装備するタイミングです。そのお話は次回で(^-^)/