🦁

[UEFN][verse] UEFNのカスタムイベント実装を考える[7]ダミーのagentを取得する方法

2023/09/07に公開

前回はこちら。

https://zenn.dev/t_tutiya/articles/2f7b4ca32f70ff

この記事をアップした時、スイッチデバイスにおけるagentの取得周りの処理を盛大に勘違いしていた事に気づきまして、遅れて記事を修正しました。

せっかくなので、今回はどういう勘違いをしていたのかをまとめておこうと思います。イベント処理には直接関係しませんが、応用が効くんじゃないかなと思います。

先に結論:何を間違えていたのか&どうすれば良いのか

間違っていたのは、Listener()メソッドの実装です。

Listener()<suspends>:void =
    var SwitchStatus :logic = false
    TeamCollection := GetPlayspace().GetTeamCollection() # agent取得処理
    loop:
        for:
            Team : TeamCollection.GetTeams() # agent取得処理
            Agent : TeamCollection.GetAgents[Team] # agent取得処理
            NowhStatus := logic{MySwitch.GetCurrentState[Agent]}
            SwitchStatus <> NowhStatus
        do:
            set SwitchStatus = NowhStatus
            for:
                SwitchHandle : SwitchHandelers
            do:
                SwitchHandle(Agent)
        Sleep(0.0)

このコードは正しく動くのですが適切な実装とは言えません。以下のように実装すべきです。

Listener()<suspends>:void =
    var SwitchStatus :logic = false
    loop:
        if:
            NowhStatus := logic{MySwitch.GetCurrentState[]}
            SwitchStatus <> NowhStatus
            Agent := GetPlayspace().GetPlayers()[0] # agent取得処理
        then:
            set SwitchStatus = NowhStatus
            for:
                SwitchHandle : SwitchHandelers
            do:
                SwitchHandle(Agent)
        Sleep(0.0)

3行あった"# agent取得処理"が1行のみになり、また、loop式内のfor-doif-thenに変わりました。GetCurrentState()メソッドは引数無しの物を呼びだしています。

関数全体の挙動については前回の記事を参照してもらうとして、ここでは主にagent取得周りの処理を解説します。

前提:スイッチデバイスの「プレイヤーごとに状態を保存(Store State Per Player)」

スイッチデバイス(switch_device)には「プレイヤーごとに状態を保存(Store State Per Player)」というオプション(有効/無効)があります。

オプションが無効(デフォルト)の場合、そのスイッチの状態(ON/OFF)は、プレイヤー全体で共有されます。

オプションが有効の場合、そのスイッチの状態(ON/OFF)は、プレイヤー毎に固有の物となります。同じスイッチでも、プレイヤー毎に異なる状態を持つようになるわけです。スイッチの見た目の状態もプレイヤー毎に変わるのでしょう[1]

スイッチデバイスにはこのようなオプションがあるという事を覚えておいてください。

スイッチデバイスにおける状態の取得と更新

改めてスイッチデバイスの仕様を確認します。スイッチデバイスはONとOFFの状態を持ち、インタラクトする度に状態がトグルします(ONならOFFに、OFFならONになる)。

Verseでは、主に以下のメソッドでスイッチの状態を更新します。

#状態の更新
TurnOn<public>(Agent:agent):void = external {} #オンにする
TurnOff<public>(Agent:agent):void = external {} #オフにする
ToggleState<public>(Agent:agent):void = external {} #トグルする

どのメソッドも引数にagentを取ることが分かります。これは、先程のStore State Per Playerオプションが有効の場合に、どのプレイヤーの状態を更新するかを設定する必要があるためです。

スイッチの状態の取得には以下のメソッドを使用します。

#状態の取得
GetCurrentState<public>()<transacts><decides>:void = external {}
GetCurrentState<public>(Agent:agent)<transacts><decides>:void = external {}

GetCurrentState()メソッドは失敗許容関数で、スイッチがONなら成功、OFFなら失敗します。

GetCurrentState()メソッドには、引数が無い物と、agentを取る物の2種類があります。公式にはStore State Per Playerオプションが無効であれば前者、有効であれば後者を使うように指示されています。オプションが無効の時はプレイヤー全体でスイッチの状態を共有しているので、引数にagentを設定する必要が無いのは納得です。

謎の非対称性とagent不在問題

さて、ここに謎の非対称性がある事に気づきます。GetCurrentState()メソッドには引数にagent無し版がありますが、TurnOn()/TurnOff()/ToggleState()の各メソッドにはagent無し版が無いのです! これ、正直API設計時の見落としでは? と思っています(注:ダイレクトイベントバインディングの仕様上、発信者(insitgator)の情報が常に必要なのかもしれないが、未調査なのでここでは触れません)。

とはいえ、引数無し版が無い以上、どうにもagent情報が必要です。この場合、agentはどこから引っ張ってくれば良いのでしょうか? というのも、今回のサンプルコードは「スイッチAの状態が変化したらスイッチBの状態をトグルする」という物で、スイッチのトグルに対して特定のagentが介在しないのです。

ここで「スイッチAの状態を変化させたプレイヤーをagentにすれば良いのでは?」と考えるかもしれません。実は、当初のサンプルコードはそのフローを意図した物でした。しかし、これは上手くいきません(これについては後で簡単に説明します)。また、例えば「ゲーム開始から10秒後にスイッチをトグルする」など、明確なプレイヤーが最初から存在しない場合もあります。

ダミーのagentを取得する方法

じゃあどうするかというと「ゲームに参加しているプレイヤーの中から適当に一人選ぶしかないよね」という事になります。つまり、ダミーでagentを用意するわけです。これが正しいかどうかは別にして、そうするしかありません。

agentを取得する方法は幾つかありますが、下記の方法が一番シンプルだと思います。

if:
    Agent := GetPlayspace().GetPlayers()[0]
then:
    SwitchDevice.ToggleState(Agent)

GetPlayspace()は、creative_deviceが継承しているcreative_object_interfaceインターフェイスのメソッドです。fort_playspaceというオブジェクト(実際にはインターフェイス)を返します。fort_playspaceは、現在のプレイ空間(playspace[2])にいるプレイヤーやチームについての情報を管理しています。

fort_playspace.GetPlayers()メソッドはプレイ空間に存在するplayerの配列を返します。ここでは、その配列のゼロ番目の要素を取得してagent型のAgentとして定義しています。GetPlayers()[0]player型を返しますが、agent型はスーパクラスなので透過的に処理されます。

セッション中少なくとも常に1人はプレイヤーがいるので、このif式が失敗する事は無いでしょう[3]

何を勘違いしていたのか

当初、土屋は「状態を更新する時にagentが必要なのであれば、スイッチデバイスは『最後に状態を更新したagent』の情報を持っている筈だ」と考えました。これは単なる思い込みだったのですが、組み込みのイベントを使う時にはagentも渡されるので、さほど不自然では無いと考えたのです。

それで、GetCurrentState()というメソッドは、引数にagentを指定すると「そのagentが状態を更新したのであれば値が返る(でなければ失敗する)」という物だと考え、プレイ空間にいるプレイヤー全員についてGetCurrentState()しまくるというコードを書いたのでした。

今考えるとこれは理屈が成立しておらず[4]、どうして自分がこのように考えたのかも分からないんですが、思い込みは視野を狭まらせるので気を付けましょうという話でした(文字通り自戒)。

改めて修正前/後のコードを比較する。

改めて修正前/後のコードを比較しておきましょう。

#修正前
TeamCollection := GetPlayspace().GetTeamCollection() #1 agent取得処理
for:
    Team : TeamCollection.GetTeams() #2 agent取得処理
    Agent : TeamCollection.GetAgents[Team] #3 agent取得処理
    NowhStatus := logic{MySwitch.GetCurrentState[Agent]}
    SwitchStatus <> NowhStatus
do:

#修正後
if:
    NowhStatus := logic{MySwitch.GetCurrentState[]}
    SwitchStatus <> NowhStatus
    Agent := GetPlayspace().GetPlayers()[0] # agent取得処理
then:

修正前のコードでは、for式でプレイ空間中の全プレイヤーを巡回していました。このループはまったく不要なのでif式に差し替えています。

修正前のコードでは、agentを取得するためにまずプレイ空間中のチーム一覧を取得し(#1)、そのチーム一覧を巡回し(#2)、チーム毎のagent一覧を巡回してagentを取得する(#3)という、酷く回りくどいことをしています。これには深い意味が無く、単にその時GetPlayers()メソッドの存在を知らなかっただけです(恥ずかしい……)。

なお、修正後のコードではAgent定数を定義する処理が不等式より後に移動しています。不等式が成立しない場合(つまり、スイッチが更新されていない場合)はthen節が実行されず、結果Agent定数は使用しないため、処理フローとしてこの方が望ましいでしょう。

おわりに

土屋がなにをどう勘違いしたかという話でしたが、今回記事を書いている最中に別の勘違いをしていた事に気づき(つまり、2重の勘違いが起きていた)、ほぼ丸ごと書き直す必要があって、思いのほか大変でした……。

参考リンク

今回紹介したダミーagent取得処理は、公式フォーラムの下記の議論を元にしています。
https://forums.unrealengine.com/t/where-can-i-get-the-agent-to-be-used-here-switch-device-ver/1277212/4
ちなみにこのスレッドを立てたのは土屋です。

お知らせ

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. プレイヤーがいない状態でcreative_device.OnBegin()が実行される事って有り得るんだろうか。絶対無いと言い切れる自信はない。 ↩︎

  4. GetCurrentState()が返すのは「スイッチのオンオフ状態」であり、同時に「そのagentが値を更新したかどうか」を返すのは困難 ↩︎

Discussion