🤖

[UEFN][verse] UEFNのカスタムイベント実装を考える[6] イベントリスナの実装

2023/08/26に公開

前回はこちら

https://zenn.dev/t_tutiya/articles/b413668767a612

カスタムイベント実装を考える6回目。今回はイベントリスナをカスタム実装してみます。

イベントリスナとは

イベントリスナとは、特定のイベントの発生を監視するメカニズムです。イベントが発生したら[1]、あらかじめ登録されているイベントハンドラを実行します。「イベント駆動」と言った場合、この「イベント発生→処理を実行」という仕組みを指します。

サンプルコードの挙動の説明

サンプルコードでは、スイッチデバイスを4つ用意し、1つのスイッチをトグルにするたびに、残りの3つのスイッチがトグルするという物です。完全なサンプルコードは記事の最後にアップしてあります。

pub_sub_device2というデバイスをレベルに配置し、detailタブ上で1つのスイッチと3つのトグルスイッチを設定します。

前回のサンプルコードでは「ボタン1個+スイッチ3個」の組み合わせだったのですが、Verseから「ボタンを押したタイミング」を監視する方法が無い[2]ため、全部スイッチになっています。わかりにくいので「監視スイッチ(1個)」と「対象スイッチ(3個)」と呼び分けることにします。

イベントリスナの起動とイベントハンドラの登録

まずはpub_sub_device2.OnBegin()メソッドから見て行きます。

    OnBegin<override>()<suspends>:void=
        #1
        spawn{Listener()}

        #2
        for:
            Switch : Switches
        do:
            EventObj:switch_event = switch_event{Switch := Switch} #2-1
            set SwitchHandelers += array{EventObj.HandleSwitchInteraction} #2-2

#1 イベントリスナの役割を持つListner()メソッド(後述します)を実行します。Listner()メソッドはspawn式によって別タスクで並行処理で実行され、OnBegin()側の処理は継続します。
#2 Detailタブで設定した対象スイッチをデリゲートとして登録します。
#2-1 イベント発生時に対象スイッチを制御するデリゲートを生成します。デリゲートについては下記記事を参照して下さい。

https://zenn.dev/t_tutiya/articles/26e2da5a010b39

#2-2 生成したデリゲートのメソッド(≒イベントハンドラ)を配列に追加します。この追加作業は、以前のサンプルで言う下記の処理に対応しています。

MyButton.InteractedWithEvent.Subscribe(HandleButtonInteraction) 

余談ですが、本来は、イベントハンドラが登録されて初めてイベントリスナによる監視が始まる構造にすべきかもしれません。

イベントハンドラ(デリゲート)

switch_event := class():
    Switch :switch_device

    HandleSwitchInteraction(Agent : agent) :void =
        Switch.ToggleState(Agent)

監視スイッチがトグルするとHandleSwitchInteraction()メソッドが実行されます。このメソッドでは割り当てられている対象スイッチをトグルします。

イベントリスナ

#イベント発生を監視する
Listener()<suspends>:void =
    #1 監視スイッチの状態初期化
    var SwitchStatus :logic = false
    #2 
    loop:
        if:
            #3 監視スイッチの状態を取得
            NowhStatus := logic{MySwitch.GetCurrentState[]}
            #4 監視スイッチが変化した場合
            SwitchStatus <> NowhStatus
            #5 仮のAgent情報を取得
            Agent := GetPlayspace().GetPlayers()[0]
        then:
            #6 監視スイッチの状態を保存
            set SwitchStatus = NowhStatus
            #7 登録されている対象スイッチの処理を呼びだす
            for:
                SwitchHandle : SwitchHandelers
            do:
                SwitchHandle(Agent)
        #8 待機
        Sleep(0.0)

Listener()メソッドがイベントリスナ本体になります。このメソッドはタスクで並行処理で実行されています。
#1 スイッチがトグルしたことを判定する為に、前回のスイッチの状態をlogic型で保持します。ここではfalseで初期化していますが、本来はゲーム開始時点のスイッチの状態を改めて取得すべきかもしれません。
#2 loop式でこのタスクを回し続けます。
#3 switch_device.GetCurrentState()メソッドは失敗許容式で、スイッチのON/OFFを成功/失敗で返します。logic{}を使うと、失敗許容式の結果をlogic型に変換出来ます[3]
#4 監視スイッチの現在の状態を以前の状態と比較します。変化している場合のみthen節が実行されます。
#5 スイッチをトグルする為にagentオブジェクトが必要な為、ここで適当なagentを取得しています。このコードの意味については別記事で解説予定です。
#6 監視スイッチの状態を更新します
#7 登録されているイベントハンドラを実行します。スイッチのトグルにはagentオブジェクトが必要なので、イベントハンドラの引数に設定しています。
#8 Sleep(0.0)は、次のupdateまでAwaitするイディオムだと考えて良いです。これが無いとシステムに処理が戻らないため無限ループになってしまいます[4]

サンプルコード(全体)

using { /Verse.org/Simulation/Tags }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /Verse.org/Simulation }
using { /Fortnite.com/Devices }

custom_event2 := class():
    Switch :switch_device

    HandleSwitchInteraction(Agent : agent) :void =
        Switch.ToggleState(Agent)
            
pub_test_device2 := class(creative_device):

    @editable MySwitch : switch_device = switch_device{}
    @editable Switches : []switch_device = array{switch_device{}}

    var SwitchHandelers: [](agent->void) = array{}

    OnBegin<override>()<suspends>:void=
        #イベントリスナを起動
        spawn{Listener()}

        #対象スイッチを登録する
        for:
            Switch : Switches
        do:
            EventObj:custom_event2 = custom_event2{Switch := Switch}
            set SwitchHandelers += array{EventObj.HandleSwitchInteraction}

    #イベント発生を監視する
    Listener()<suspends>:void =
        #監視スイッチの状態初期化
        var SwitchStatus :logic = false
        loop:
            if:
                #監視スイッチの状態を取得
                NowhStatus := logic{MySwitch.GetCurrentState[]}
                #監視スイッチが変化した場合
                SwitchStatus <> NowhStatus
                #仮のAgent情報を取得
                Agent := GetPlayspace().GetPlayers()[0]
            then:
                #監視スイッチの状態を保存
                set SwitchStatus = NowhStatus
                #登録されている対象スイッチの処理を呼びだす
                for:
                    SwitchHandle : SwitchHandelers
                do:
                    SwitchHandle(Agent)
            #待機
            Sleep(0.0)

余談

ちなみになんですが、今回はイベントリスナを実装する事が目的だったので長めのコードを書いていますが、通常は組み込みのイベントリスナを使った方が良いです。方法については下の記事を参照。

https://zenn.dev/t_tutiya/articles/c5203d67739a4a

なお、switch_device.TurnedOnEvent.Await()の戻り値がagentなので、これを使ってデバイスを制御出来ます。

補足

当初、どう書いてもなぜかしっくり来なくてなんでだろうと悩んでいたんですが、最終的に、これまでサンプルコードで使っていたeventクラスが今回については必要が無いという事に気づきようやく完成しました。まあこういう事もある。

一点気になってるのは、今のサンプルコード、イベントリスナをspawnで並行実行してるんですが、これ正しいのかなあ? 公式ドキュメントでは、できるだけbranchを使うように案内しているんですが、どうもbranchで上手く実装する方法がイメージできません。

おわりに

イベントまわりについてはほぼほぼ説明できたように思います。やり残した事があるとしたらsubscribable/cancelableインターフェイス互換性を持つ実装かなあ(そこまでやる必要があるのかという気もしている)。これについては上手いサンプルコードが思いついたら考えます。

続き

https://zenn.dev/t_tutiya/articles/d8db92924bc6ba

お知らせ

verse言語とUEFNの記事を他にも書いているので御覧下さい。
https://zenn.dev/t_tutiya

最後まで読んで頂きありがとうございました。この記事がお役に立てたようであれば、是非LIKEとフォローをお願いします(今後の執筆のモチベーションに繋がります)。

#Verse #UEFN #Fortnite #Verselang #UnrealEngine

宣伝

「Unityシェーダープログラミングの教科書」シリーズ1~5をBOOTHで頒布中です。
https://s-games.booth.pm/

脚注
  1. 昔は「イベントが発火する」とも言いましたが、最近は使われない表現のようです ↩︎

  2. 多分 ↩︎

  3. Verseでは失敗許容式の結果を真偽値に変換するのは極力避けるべきに思えますが、ここでは他に方法が無かった ↩︎

  4. ただし、その場合はコンパイルエラーになります。偉いなVerseコンパイラ! ↩︎

Discussion