【UEFN】UEFNのVerseで疎結合のコンポジションを行う(Verseの学習⑬)
クラス構成をシンプルにする
オブジェクト指向プログラミング(以下OOP)では、プログラムの機能をクラスに分けることで、一つ一つのプログラムを小さくしていきます。
一つ一つのクラスが正しい形で独立していれば、小さな単位でプログラムを作成、保守することが出来るため、バグの出にくいプログラミングを行うことができます。
OOPでもっとも使うプログラミング手法の一つがコンポジションです。
今回はUEFNのVerseで自分が採用しているコンポジションを行う手法について簡単に紹介します。
そもそもコンポジションとは?
例として、作成するゲームに必要な機能を次のようなものと考えます。
- プレイヤー管理
- 敵キャラクター管理
- イベント管理
コンポジションを用いたプログラム方法としては、これらをそれぞれ部品のクラスとして作成し、その部品クラスを全体管理の親クラスに管理させることになります。
Verseの書き方としてはこんな形です。
# 部品クラス ringo_event_manager := class{(クラスの記述)} ringo_player_manager := class{(クラスの記述)} ringo_enemy_manager := class{(クラスの記述)} # 全体管理クラス ringo_game_manager := class: EventManager : ringo_event_manager = ringo_event_manager{} PlayerManager : ringo_player_manager = ringo_player_manager{} PlayerManager : ringo_player_manager = ringo_player_manager{} (クラスの記述)
このような形で、全体管理の親クラスの中に、部品クラスを実体(インスタンス)として保有する形が、コンポジションの基本形となります。
親クラスと部品クラスは、相互にやり取りをしながらプログラムを実行していきます。
親クラスから部品クラスを参照する際は、親クラスが部品クラスのインスタンスを持っているため、簡単に実現することができます。
ringo_game_manager := class:
EventManager : ringo_event_manager = ringo_event_manager{}
DoSomething() : void=
EventManager.DoSomethingInEvent()
では、部品が親を参照する場合は、どうすればいいでしょうか?まず、考えられるのは、部品に親の参照を渡しておくことです。
ringo_event_manager := class:
GameManager : ringo_game_manager
DoSomethingInEvent() : void=
GameManager.DoSomethingInGameManager()
ただ、この相互に参照しあう形だと、以下のような問題が発生してきます。
部品クラス内で親クラスの関数が直接実行できてしまうとどこでどういった処理が行われているか把握しずらくなる。(オブジェクト指向の分割統治が壊れやすい)
部品クラスの中で、親クラスの関数を実行しているということは、部品クラスの中に親の情報も入ってしまうため、この部品だけ他のもので使用するということができなくなってしまい再利用が行いづらくなる
コンポジションを行うときのコツは、部品が親を直接参照するような密な関係ではなく、疎な関係が組んでいくことです。 互いに疎な関係を構築できると保守のしやすいバグの生まれにくいコードとなります。
この疎な関係を実現するために、多くの開発環境では、様々な仕組みが導入されています。
- UnrealEngineにおけるEventDispatcher
- QtのSignal/Slot機構
- UnityのUnityEvent
などは、こうした疎な関係を実現するために存在しているものです。
ただ、UEFNでは、そうした機能が非常に乏しいのが現状です。そこで今回はTriggerデバイスを使用して、コンポジションの疎結合を行います。
コンポジションサンプル
結合が密な場合と疎な場合を以下の例で示していきます。
- シネマティックシーケンスを再生するイベント管理クラスを作成し、それをゲームマネージャーで管理する
- ゲームマネージャーでは、シネマティックシーケンスイベント管理クラスのインスタンスをリストで保有し、前のイベントが終了したら、次のシネマティックシーケンスイベントを開始するようにする
- アルファベット一つ一つの動きをそれぞれ1つづつのシネマティックシーケンスで作成しており、4つのイベントを管理する(もちろん、この表現ならこんなことする必要はありませんので、あくまでも例です)
クラスが密につながった例
まず、最初にクラスが密につながった例を紹介します。
ゲームマネージャークラスは以下のようにしました。
ringo_game_manager := class(creative_device): @editable CinematicEvents : []ringo_cinematic_event = array{} @editable StartButton : button_device = button_device{} var _CurrentEventIndex<private> : int = 0 OnStartButtonInteracted(Agent : agent) : void= set _CurrentEventIndex = 0 if: CinematicEvent := CinematicEvents[0] then: CinematicEvent.Start() return OnBegin<override>()<suspends>:void= StartButton.InteractedWithEvent.Subscribe(OnStartButtonInteracted) for(CinematicEvent : CinematicEvents): CinematicEvent.Initialize(Self) return DoNextEvent() : void= set _CurrentEventIndex += 1 if: _CurrentEventIndex = CinematicEvents.Length then: set _CurrentEventIndex = 0 if: CinematicEvent := CinematicEvents[_CurrentEventIndex] then: CinematicEvent.Start() return
次に、部品となるシネマティックシーケンスイベント管理クラスです。
ringo_cinematic_event<public> := class<concrete>: @editable CinematicSequenceDevice : cinematic_sequence_device = cinematic_sequence_device{} var _MaybeGameManager : ?ringo_game_manager = false Initialize<public>(GameManager : ringo_game_manager) : void= set _MaybeGameManager = option{GameManager} CinematicSequenceDevice.StoppedEvent.Subscribe(OnStoppedSequence) return Start<public>() : void= CinematicSequenceDevice.Play() return OnStoppedSequence() : void= if: GameManager := _MaybeGameManager? then: GameManager.DoNextEvent() return
この例で問題になりそうなのは、以下の部分です。
var _MaybeGameManager : ?ringo_game_manager = false OnStoppedSequence() : void= if: GameManager := _MaybeGameManager? then: GameManager.DoNextEvent() return
ここで、部品クラスから親クラスへの参照が行われています。 そのため、この部品を他の場所で活用するためには親も同伴しないといけません。
本来的には、部品が親から独立したコードとなっていれば、別の場所にも使用可能なのですが、この書き方の場合、部品クラスの実装は、このゲームのマネージャーに深く依存した密な関係となっています。
この例でも正しく動作しますが、親クラスが部品クラスからも使えてしまうことで、どこでどういった動作が行われているのか管理のしにくい構成となります。
クラスのつながりを疎にした例
つながりを疎にするために、今回はTriggerDeviceを使用します。
TriggerDeviceをイベント管理クラスに追加し、シネマティックシーケンス再生後にトリガー発信するように変更します。
シネマティックシーケンスイベント管理クラスを以下の部分を追加します。
@editable EndedTrigger<public> : trigger_device = trigger_device{} OnStoppedSequence() : void= EndedTrigger.Trigger() return
全体管理クラスは次のようになります。
OnBegin<override>()<suspends>:void= StartButton.InteractedWithEvent.Subscribe(OnStartButtonInteracted) for(CinematicEvent : CinematicEvents): #変更 CinematicEvent.Initialize() #追加 CinematicEvent.EndedTrigger.TriggeredEvent.Subscribe(OnCinematicEventEnded) return # 追加 OnCinematicEventEnded(MaybeAgent : ?agent) : void= DoNextEvent() return
TriggerDeviceクラスの信号を受け取るための記述を親側のゲームマネージャークラスに記述することで、 全体管理クラスの方では、このイベント送信を受信するようにコード変更しています。
デバイス設定はこのようにしています。
このように変更することで、全体管理クラスからは、イベント管理クラスを参照していますが、イベント管理クラスからは、ゲームマネージャークラスを参照することがなくなりました。 部品として独立することができたため、別のゲームのゲームマネージャーでもこのクラスを使用することができるようになります。
まとめ
今回はUEFNでのコンポジションの作り方について説明しました。
基本方針として、部品クラスからは親クラスを直接参照しないというコード設計をすると、見通しがよく、保守性の高いプログラムになります。
VerseにはListenableというインターフェースがあり、将来的には、このインターフェースを実装することで、こうしたイベント処理ができるようになるのではないかと思われますが、現状はまだ未完成なようです。
将来的には、もう少しスマートなやり方が登場しそうですが、現状としてはTriggerDeviceなどを使うしかないのですが、自由な変数をトリガーに与えることができないため、ベストなやり方ではありません。
他の方法では、関数を変数として使用し、処理が終わった場合のコールバックにする方法などもあり、自分はその手法を多用しています。
このあたりは、また機会があれば紹介できればと思います。
UEFN関連の投稿がかなりたまってきたため、UEFN投稿のまとめページを作ってみました。
ぜひほかの記事も見ていただけると嬉しいです。
RingoGamesではUEFNに関するさまざまな情報を発信していきます。Twitterでお知らせしていきますので、よろしければ、Twitterのフォローをしていただけると幸いです。
Twitterはこちら