[UEFN][verse] UEFNのカスタムイベント実装を考える[7]ダミーのagentを取得する方法
前回はこちら。
この記事をアップした時、スイッチデバイスにおける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-do
がif-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取得処理は、公式フォーラムの下記の議論を元にしています。
ちなみにこのスレッドを立てたのは土屋です。お知らせ
verse言語とUEFNの記事を他にも書いているので御覧下さい。
最後まで読んで頂きありがとうございました。この記事がお役に立てたようであれば、是非LIKEとフォローをお願いします(今後の執筆のモチベーションに繋がります)。
#Verse #UEFN #Fortnite #Verselang #UnrealEngine
宣伝
「Unityシェーダープログラミングの教科書」シリーズ1~5をBOOTHで頒布中です。
Discussion