前書き

フォートナイトの仕掛けにはlistenableという、イベント発火用の機能が備わっています。Subscribeを使えば、ボタンを押した際などに関数を呼び出したり、Await関数を使えば、非同期処理内でイベントが起こるまで待機させることができます。

実装を進めていく中で、自作のイベント機能を作りたい場合が出てくるはずです。例えばある関数の中で条件を満たしたら、別の関数の処理を進めるなど。このような望みをかなえてくれるのが、eventと呼ばれる標準で備わっているクラスです。

event概要

event<native><public>(t:type) := class(signalable(t), awaitable(t)):
    # Suspends the current task until another task calls `Signal`.
    # If called during another invocation of `Signal`, the the task will still suspend and resume during the next call to `Signal`.
    Await<native><override>()<suspends>:t

    # Concurrently resumes the tasks that were suspended by `Await` calls before this call to `Signal`.
    # 
    # Tasks are resumed in the order they were suspended. Each task will perform as much work as it can until it encounters a blocking call, whereupon it will transfer control to the next suspended task.
    Signal<native><override>(Val:t):void

# A *recurring*, successively signaled event allowing a simple mechanism to coordinate between concurrent tasks.
event<public>() := event(tuple())

eventはVerse.digest.verseで実装を確認できます。Signal関数を定義するsignableと、Await関数を定義するawaitableを親に持つパラメトリック型のクラスです。

https://dev.epicgames.com/documentation/ja-jp/uefn/parametric-types-in-verse

パラメトリック型とは何ぞやという方は、上記のリンクを見るのをお勧めします。簡単に言うと、呼び出し時に型が決まる関数/クラスのことで、typeという部分にはあらゆる型が入ります。例えばint型を入れると、その時点でそのクラス内のt:typeはint型で決定し、その後の処理はすべてintとして扱われ実行されます。よくわかんなかったら、そんな仕組みで動いてるんだーくらいに思っていただいて大丈夫です。

EmptyEvent:event() = event(){}
AgentEvent:event(agent) = event(agent){}
MultipleInfoEvent:event(tuple(agent, int)) = event(tuple(agent, int)){}

定義の仕方です。イベント用の機能といえど元はクラスなので、変数化してインスタンスの保持を行います。

eventクラスは発火時に情報を受け渡すことができます。その情報はeventの次のカッコ内で指定し、以降の呼び出し時や受け取り時はその情報をもとに処理が行われます。

何も情報を渡さない場合はevent()とカッコ内に何もいれずに定義します。一つだけ情報を渡したい場合はevent(agent)のようにカッコ内に型を一つだけ入れ、複数の情報を渡したい場合はタプルを使用し型をまとめます。

OnCall(Agent:agent):void=
    EmptyEvent.Siganl()
    AgentEvent.Signal(Agnet)

    MultipleInfoEvent.Siganl(Agent, 0)
    MultipleInfoEvent.Siganl(Agent, 1)
    MultipleInfoEvent.Siganl(Agent, 2)

    AgentEvent.Signal() #何も入れていないのでエラー

イベントの発火はeventクラスのインスタンスに対し「Signal」関数を呼び出し行います。

情報を渡す際は定義時のモノと完全に一致する必要があり、欠けていたり、逆に多すぎるのようなものは許容されません。タプルの場合はSignal((Agent, 0))と二重にカッコを入れる必要はなく、関数の第一引数、第二引数、と同じ感覚で呼び出せます。

OnRecieve()<suspends>:void=
    EmptyEvent.Await()
    sync:
        EmptyEvent.Await()
        block:
            EmptyEvent.Await()
            Print("EmptyEventが呼び出されました")
        block:
            Agent := AgentEvent.Await()
            Print("AgentEventが呼び出されました")
            HUDMessage.Show(Agent)
        loop:
            Info := MultipleInfoEvent.Await()
            Print("Index: {Info(1)}のMultipleInfoEventが呼び出されました")
            HUDMessage.Show(Agent)

イベントの受け取り側はAwaitを用いて行います。listenableのように関数を呼び出すSubscribeは用意されていないため、すべて非同期関数の中で行います。

Await関数を呼び出すとその場で処理が一時停止されます。もし、同じインスタンスのeventクラスにSignalが発火された場合は、そこで処理を再開し、次からの処理が実行されます。

Awaitは戻り値としてSignalに入れた情報を受け取ることもできます。そのため、AgentをSignalで渡せば、Await側でも同じプレイヤー/NPCのAgentとして処理に使うことができます。

基本的には

  • 一時停止
  • raceとともに使い中断処理に使用
  • loopで疑似的にSubscribeのように使う

のどれかだと思います。

使用例

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }

coin_collect_device := class(creative_device):

    @editable
    Collectibles:[]collectible_object_device = array{}

    var CollectCount:int = 0
    CompleteEvent:event(agent) = event(agent){}

    OnBegin<override>()<suspends>:void=
        for(CollectibleDevice : Collectibles):
            CollectibleDevice.CollectedEvent.Subscribe(OnCollect)

    OnCollect(Agent:agent):void=
        set CollectCount += 1
        if(CollectCount >= Collectibles.Length):
            CompleteEvent.Signal(Agent)
            set CollectCount = 0

hud_message_controller_device := class(creative_device):

    @editable
    HUDMessageDevice:hud_message_device = hud_message_device{}

    @editable
    CoinCollectDevice:coin_collect_device = coin_collect_device{}

    OnBegin<override>()<suspends>:void=
        AwaitComplete()

    AwaitComplete()<suspends>:void=
        loop:
            Agent := CoinCollectDevice.CompleteEvent.Await()
            HUDMessageDevice.Show(Agent)

使用例です。この量だとあまりメリットは実感しにくいとは思いますが、クラス間の通信を行う際にはeventは非常に便利なものとなっています。

例えば、Verseデバイスに影響されないAnimalクラスを作ったとして、Animalが倒された際にVerseデバイス側で処理を行いたい場合などには、eventを使う( EliminatedEvent:event() = event(){} )ことでSleep(1.0)をループで実行して生きているかの確認をしなくてもよくなります。

関数の引数として

OnRecieve(InEvent:event())<suspends>:void=
    InEvent.Await()

OnCall()<suspends>:void=
    EventInstance := event(){}
    spawn:
        OnRecieve(EventInstance)
    Sleep(5.0)
    EventInstance.Signal()

event自体は単なるクラスであるため、ゲーム内の状況に応じで動的にインスタンスを作成し、関数に渡したりして通信することもできます。