NEWS / BLOG
2022.12.15
社内勉強会の紹介:コーディング勉強会(4)
#テックネタ #プログラマー

はじめに
こんにちは。プログラマーのTです。
株式会社ジーンでは、社内でいろいろな勉強会が行われています。前回は、プログラム構造について簡単に説明しました。今回はその続きです。

プログラムの複雑さ
プログラムの複雑さを決める要素はなんでしょうか?
今回は、状態について着目し、プログラムの複雑さについて考えてみましょう。

状態とは何か
プログラムを行う際には、ほとんどの場合変数が必要です。
変数の内容によって、プログラムの動作に変化がない場合、プログラムの複雑さはあまり上昇しません。
例えば、プレイヤーの名前が自由に決められるゲームで、プレイヤーの名前によってゲームには何も影響がない場合、名前の変数によってプログラムの複雑さはほとんど上昇しません。このような変数は状態を表す変数ではありません。武器の攻撃力など、計算にしか使われないような値も同様です。

img_20221215_01.png

一方で、プログラムの動作が変わる変数があります。ゲームでは、プレイヤーがいまどういう動作をしているかを変数で表します。例えば以下のようなプログラムです。

img_20221215_02.png

この例では、isJumping_ という変数がtrueかfalseかによって、プログラムの動作が大きく異なります。Playerクラスは、isJumping_がtrueのときとfalseのときと、2つの状態を持つことになります。

状態の組み合わせの問題
さらに、isWalking_ という変数を増やして、立っているか、歩いているかを判定しようとしたとします。そうするとPlayerの状態はisJumping_とisWalking_のそれぞれtrue, false で4つの状態を持つことになります。
さらにbool変数を増やすと、組み合わせの状態は8, 16, ... と増えていきます。
このように、プログラムの動作にかかわる変数を増やすと、組み合わせが爆発的に増えます。プログラムの制御を行う変数を作るときには十分注意する必要があることがわかります。

不正な状態とその削減
状態を増やすと、本来は到達するはずのない不正な状態を作り出してしまうことがあります。例えば先ほどの例で、isJumping_ == true かつisWalking_ == trueというのは実際にはありえない状態です。
この例ではenumを導入して、stateを(standing, walking, jumping)と定義すると、3パターンに集約でき、不正な状態がなくなります。このように、bool値の組み合わせから、列挙型へ変換することによって、状態を減らし、不正なものをなくすことができます。

状態が与える影響
あるオブジェクトが状態を持つ場合、内部だけでなく、それに関連する外部のプログラムが影響を受ける場合があります。例えば、宝箱には開封状態があり、すでに開封された宝箱はプレイヤーが再度開けられないようにする必要があります。プレイヤーがジャンプしているとき、NPCに話しかけられないようにしたい、といったことも同様の問題です。
そういう意味で、状態とそれが及ぼす影響は、プログラムの複雑さの重要な部分を占めていると言えるでしょう。

内部状態を公開することの問題点
通常、オブジェクトはクラス単位で分かれています。つまり、外部プログラムとの連携は、クラス同士のやりとりということになります。このとき、クラスの状態に関する変数を、外部から参照する、また、状態を外部から直接変更できるというのはできる限り避けるべきです。

例えば次のようなプログラムには様々な問題があります。

img_20221215_03.png

まず、パラメータと違い、プログラムの動作が変わってしまうにもかかわらず、そのクラス内だけでは、いつ状態が更新されるか判断できなくなります。

外部のプログラムが、Player::Stateに依存するようなコードも、問題があります。以下はプレイヤーの状態を見て、イベントを発生させるか判断するようなプログラムです。

img_20221215_04.png

上記のプログラムの問題は、Playerとは関係ないプログラムがPlayerの内部状態に依存しているという点です。Playerの状態が増減したり、Stateの名前が変わったり、Playerのいろいろな変更にたいして、このプログラムも対応する必要があります。
また、Playerをプログラミングする際にも、EventManagerをはじめ、この関数を使っている様々なクラスからどのように使われているかを考慮する必要があります。
ゲーム開発では、複数人でプログラムを行うことが一般的です。PlayerとEventManagerを別の人が担当した場合、それぞれの担当箇所で変更に対するリスクを負うことになります。そのような状態でプログラミングを行うことは、現実的ではありません。

状態を集約する、保護する
内部状態は内部状態として、外に見せることは避けるようにしましょう。代わりに、抽象化された状態と、抽象化された操作を提供するように変更します。まずはプレイヤーです。停止を行う関数と、イベントが実施可能かを返す関数を用意しました。

img_20221215_05.png

canStartEvent()はイベントを開始できる状態にあるかどうかを返します。このプログラムでは単にstate_との比較を返しているだけですが、例えば、走っているときに一定以上の加速度がある場合はfalseを返す、攻撃モーション中は除外するなど、state_以外の条件が追加されても、EventManagerへの影響が全くありません。
また、stop()関数も抽象的な操作を提供する関数です。こちらも同様で、Playerの内部状態を考慮する必要がありません。

EventManagerは上記の関数を使って実装を行います。

img_20221215_06.png

これにより、他クラスの内部状態に依存した状態は解消されました。
外部から見たときの状態は整理され、プログラムの複雑さが軽減しているのがわかります。
また、このような抽象的な関数を挟むことで、拡張や変更に強くなっています。

おわりに
今回は、プログラム構造のうち、状態について説明しました。
最初に、フラグによる管理は複雑化しやすく、誤った状態を表してしまう可能性があること、組み合わせにより複雑さが増加しやすいこと、フラグをenumに置きかえることで状態をまとめることができることを示しました。
次に、内部状態を公開し設定できることの問題点、プログラムの複雑さや相互に干渉しあう問題について確認しました。内部の状態と外部からの状態を分け、抽象的な取り扱いを行うことにより、プログラムの複雑さを減らすことができるということを説明しました。
また、そのようにすることは複数人のプログラマーで開発を行う場合は特に重要になります。
今回の内容も、少しでも参考になればと思います。

解説は以上です。ありがとうございました。