見出し画像

方々でよしなに動いてほしくて

どうも、こんばんは。Dollyです。
前回、大雑把に「分離された能力の記述」なんて書いていましたが、実際にはもう少しちゃんと考えた(?)ことをやっていました。
今回はその部分をもう少しだけ掘り下げてみます。
この日記はプログラミング講座みたいなソレではないので、なんも言ってなければ、なんの参考にもならないことはご了承くださいね。

簡単にまとめると、「ゲームへの作用素」という括りを作り、役職やらボタンやらタイマーやら、そのすべてを「ゲームへの作用素」の1つだと考えることにしたわけです。
Modの場合は特に、イベント駆動型のプログラムに落ち着きやすいと思いますが、この場合はイベント発生時に、「全作用素に対して、この関数を実行させる」ようにお願いすればよいわけです。

foreach(var entity in AllEntities){
    entity.OnPlayerDead(deadPlayer);
}

いろいろと端折りましたが、雰囲気こんなようなものをお好きなタイミングでねじ込めば、ボタンだろうとタイマーだろうと能力だろうと、よしなに動いてくれるようになります。
また、このfor文を使った書き方だと非常に見づらいので、実際には、Harmonyが用意してくれている拡張メソッドを使ってこんな感じにしています。

AllEntities.Do(e => e.OnPlayerDead(deadPlayer));

だいぶシンプルになりました。
"Do"というメソッドは、引数に与えた1引数の関数を、コレクション内の全要素に対して実行してくれる拡張メソッドです。
現状のNoSはこの記述がゲーム中に溢れかえっています。

そしてもう一つ、作用素には大事な側面があります。それは、「作用素には寿命がある」ということ。作用素がゲームに追加されたら、ゲームが終わるまでずっと居座るわけではなく、どこかでふっと消えてしまうこともあります。
フリープレイだと役職をころころ変えることができますが、役職を変えれば以前までの役職は「寿命が尽きる」ことになります。
寿命が尽きてもゲームへの作用素として大手を振っていられては困るわけです。(役職を変えても前の役職のボタンが残ってる、とかね。)

Arsonistの場合は、右下のボタン以外に左下のアイコンも一緒に消えてほしいところです。

対策としては、役職の寿命が尽きるタイミングで、役職やボタン類それぞれに対して、それ自身を破棄させるような手続きをさせることを考えるわけですが、これを手で書いていてはたまったものではありません。いつか書き忘れます。

//役職が無効化されたときに呼び出されます。
void OnInactivated(){
    myButton1.Inactivate();
    myButton1Timer.InActivate();
    myButton2.Inactivate();
}

例なので変数名とかはいろいろ適当ですがお気になさらず。
こんなものをぐだぐだ書いていては後々のメンテナンスが大変ったらありません。
理想を言えば、役職を無効化したら、関連するボタンやタイマーが勝手に解放される状況が望ましいわけです。
そうなれば、やるしかないじゃない。

手始めに、寿命というものを明確にしておく必要があります。
今回は、寿命インターフェース(NoS内ではILifespanと名付けています)を用意し、そのオブジェクトがまだ生きているか、もう死んだかの情報を格納できるようにしました。
この寿命は、C#のGC云々とか、UnityのGameObjectのライフサイクルとは全く無関係のものです。
この寿命インターフェースは役職のような寿命がはっきりしているオブジェクトに実装させておきます。
そしてもう一つ。「ゲームへの作用素」に寿命を関連付けさせます。
これによって、「ゲームへの作用素」とは「作用素の実装」と寿命のペアのことだと見直すことができました。

あとは、ゲームの作用素ごとに関連付けされた寿命を参照して、寿命が尽きていればいい感じに削除する処理を作用素をとりまとめている側で実装すればよいだけです。これはひとたび書けばみんないい感じに動いてくれますから、惜しむ手間ではありません。

こういう経緯もあって、NoSのソースにはいわゆる「後片付け」のコードは少ないと思います。もともと僕の頭がポンコツなので、少しでもヒューマンエラーを起こす箇所は無くしていかないとやっていけません。
自慢じゃないですが、後片付け、苦手です。どんどん注意が次のもの、次のものに移っちゃって忘れてしまうんですよね。鳥頭って言うんでしょうか。
ほんとに誰か助けて下さい。

…少し話は逸れましたが、作用素と寿命のペアに話題を戻しまして。
作用素自身に寿命インターフェースを実装すれば、自分の判断で勝手に死んでくれる作用素にもなりますし、ここの選択は結構自由が利きます。
ほかにも、参照している複数の寿命のうちどれかが死んだら死ぬ、みたいなバリエーションなど、寿命インターフェースまわりはいろいろ作り込んだので、多分欲しくなる組み合わせは全部網羅できたんじゃないかと思ってます。こういうのを作るの、楽しいですね。

Evil Trackerのインポスターを指す矢印は、
自身か相手の役職のどちらかの寿命が尽きると死にます。

「ゲームへの作用素」のおかげで、ボタンやらなんやら、ゲームにはいろんな要素が詰め込まれていますが、それら全てを常々気にしなくてもよくなりました。
イベントの発火タイミングで上のようなプログラムがちゃんと実行されているか?だけを確認すればよいので、バグの原因探りもだいぶ楽になりました。

「ゲームへの作用素」という括りを導入した流れで、既存のプログラムにもいろいろ手を加えました。
下の彼らが今回の犠牲者(?)たちです。

  • GuesserやEvil Trackerの会議中のボタン

  • 矢印

  • 能力ボタン

  • ボタンとかで使われているタイマー

  • 役職

  • 分離して記述された能力 (New!!!)

タイマーは以前まではボタンに依存した難儀な書き方をしていましたが、もうちょっとまともな書き方に直せるような気がします。(執筆中に気付きました)
こうやって文章を書いていると、いろいろ思い出すきっかけを頂けます。
読んでくださるみなさまに何か差し上げられている気はしませんが…。

以前、ボタンや能力がいわゆる "is-a" の関係なのか "has-a" の関係なのか議論をお見掛けしたことがありました。("is-a" は継承による実装、"has-a" は委譲による実装を意味します。ここではあまり深くご紹介しませんが、ご興味を持ってくださいましたらぜひ。)
まぁ随分熱も冷めたころだと思いますが、僕からは「そのどちらでもない」という意見をこっそりと、出させていただきます。まぁ、結構 "has-a" に近い見た目にはなりますが、寿命のあり方の柔軟さを考慮すれば、"has-a" とはまた違うことが伺えると思います。
クラス設計って、無理に型に落とし込もうとすると逆に嵌ること、あると思います。基本の概念に立ち返ることだと思います。難しいものですね。

とはいえ、折角こういう場所を設けたわけですし、アドオンを作ってくださっている方向けの機能紹介とかもしてみたいところです。
特にドキュメントなんて、フルに活用して下さっている方はまだいらっしゃらないんじゃないでしょうか。
いろんな機能が作っては化石のように眠ってを繰り返しているので、感覚が麻痺してきていますが、ちゃんとみなさまに使っていただけることも一つ、大事なポイントですね。

関係ない話を広げ過ぎてしまう前に、今回はここまでです。
最新のスナップショットは今夜公開予定です、長引いててごめんね。
最後までお付き合いくださり、ありがとうございました。