【Verse】撃破マネージャー(自滅有効設定)と途中離脱イベントの併用
はじめに
もしゲームの勝利条件を「残りのプレイヤー数が〇人以下」とする場合、次のようなタイミングでチェックを行うかと思います。
(a)参加中のプレイヤーが途中離脱した時
(b)参加中のプレイヤーが撃破された時
(c)参加中のプレイヤーが自滅・リスポーンした時
そのイベントを受け取る方法は、たとえば以下の方法があります。
(a)fort_playspace.PlayerRemovedEvent
(b)elimination_manager_device(※1).EliminatedEvent
(c)elimination_manager_device(※2).EliminatedEvent
※1 デバイス側の設定で、「Valid on Self Elimination」(自滅の有効)はオン/オフのいずれもOK
※2 デバイス側の設定で、「Valid on Self Elimination」(自滅の有効)はオン
今回、プレイヤーの自滅とリスポーンも検知するため、撃破マネージャー(elimination_manager_device)の自滅の有効をオンにしてみました。
ここで問題が。。途中離脱時にもEliminatedEventを受け取るようになりました。途中離脱すると、PlayerRemovedEventとEliminatedEventの両方が発生するのです。。納得感はあります。
撃破マネージャー(自滅有効)と途中離脱イベントの併用をする際に、処理を重複して行わないようにするための対策を考えました。
なお、本記事はフォートナイトエコシステム v31.10で検証しています。
プログラム
2人プレイヤーで動作確認をしています。ここでは2つの方法を紹介しますが、確認方法は同じです。
トリガー(StartTrigger)を起動したあと、一方のプレイヤーが途中離脱します。
出力されたログを見て、結果を確かめていきます。
その1 EliminatedEvent、PlayerRemovedEventのSubscribe と、自作 Event
sample_player_elimed_and_player_removed := class(creative_device):
@editable
ElimManager:elimination_manager_device = elimination_manager_device{}
@editable
StartTrigger:trigger_device = trigger_device{}
var PlayerIdMap:[agent]int = map{}
# PlayerIdMapが1人以下になった時シグナルし、GameLoop_A()を終了する
WinEvent:event()=event(){}
OnBegin<override>()<suspends>:void=
GetPlayspace().PlayerRemovedEvent().Subscribe(OnPlayerRemoved)
ElimManager.EliminatedEvent.Subscribe(OnPlayerEliminated)
loop:
GameLoop_A()
Sleep(1.0)
GameLoop_A()<suspends>:void=
Print("GameLoop_A() Start")
StartTrigger.TriggeredEvent.Await()
set PlayerIdMap = map{}
for(I -> Player : GetPlayspace().GetPlayers()):
if(set PlayerIdMap[Player] = I):
Print("GameLoop_A() {Player}" + "'s PlayerId={I}")
# CheckWinConditionの成功を待つ。
WinEvent.Await()
Print("GameLoop_A() End PlayerIdMap.Length={PlayerIdMap.Length}")
OnPlayerRemoved(Player:player):void=
Print("OnPlayerRemoved() {Player}")
if(CheckWinCondition[("PlayerRemovedEvent", Player)]):
WinEvent.Signal()
OnPlayerEliminated(Agent:agent):void=
Print("OnPlayerEliminated() {Agent}")
if(CheckWinCondition[("PlayerEliminatedEvent", Agent)]):
WinEvent.Signal()
WinEvent.Await()
GameLoop_A関数では、StartTriggerのトリガー後、WinEventのシグナルを待機します。
WinEvent.Signal()
OnBegin関数では、EliminatedEvent とPlayerRemovedEvent関数をサブスクライブしています。それぞれ、プレイヤーが撃破されたときにOnPlayerEliminated関数を、プレイヤーが途中離脱したときにOnPlayerRemoved関数を実行します。
いずれの関数も処理内容は同じで、CheckWinCondition関数を呼び出して勝利判定を行い、勝利が決まるとWinEventをシグナルします。WinEventのシグナルによって、GameLoop_A関数のWinEvent.Await() が完了し、GameLoop_A関数の処理は完了します。
# 対象のプレイヤーをmapから削除し、残りのプレイヤー数をチェックする。
# プレイヤー数が1人以下になると成功。
CheckWinCondition(EventName:string, Agent:agent)<transacts><decides>:void=
Print("CheckWinCondition() {EventName}")
var NewPlayerIdMap:[agent]int = map{}
for(Key -> Value : PlayerIdMap, Key <> Agent):
set NewPlayerIdMap = ConcatenateMaps(NewPlayerIdMap, map{Key => Value})
set PlayerIdMap = NewPlayerIdMap
if(PlayerIdMap.Length > 1):
Print("CheckWinCondition() {EventName} PlayerIdMap.Length={PlayerIdMap.Length}")
false?
ToMessage<public><localizes>(Agent:agent)<transacts>:message = "{Agent}"
ToString<public>(Agent:agent):string = "{Localize(ToMessage(Agent))}"
LogVerse: : GameLoop_A() Start
LogVerse: : GameLoop_A() Anonymous[256]'s PlayerId=0
LogVerse: : GameLoop_A() Anonymous[257]'s PlayerId=1
LogVerse: : OnPlayerEliminated() Anonymous[257]
LogVerse: : CheckWinCondition() PlayerEliminatedEvent
LogVerse: : GameLoop_A() End PlayerIdMap.Length=1
LogVerse: : OnPlayerRemoved() Anonymous[257]
LogVerse: : CheckWinCondition() PlayerRemovedEvent
LogVerse: : GameLoop_A() Start
LogVerse: : CheckWinCondition() PlayerEliminatedEvent
LogVerse: : CheckWinCondition() PlayerRemovedEvent
CheckWinCondition関数(チェック)を2回実行していますが、「LogVerse: : GameLoop_A() End」は1度になりました。つまり 1度だけ実行したい処理は、GameLoop_A関数の WinEvent.Await()のあとに記述するとよいでしょう。
その2 EliminatedEvent と PlayerRemovedEvent の race
sample_player_elimed_and_player_removed := class(creative_device):
@editable
ElimManager:elimination_manager_device = elimination_manager_device{}
@editable
StartTrigger:trigger_device = trigger_device{}
var PlayerIdMap:[agent]int = map{}
OnBegin<override>()<suspends>:void=
loop:
GameLoop_B()
Sleep(1.0)
GameLoop_B()<suspends>:void=
Print("GameLoop_B() Start")
StartTrigger.TriggeredEvent.Await()
set PlayerIdMap = map{}
for(I -> Player : GetPlayspace().GetPlayers()):
if(set PlayerIdMap[Player] = I):
Print("GameLoop_B() {Player}" + "'s PlayerId={I}")
Print("GameLoop_B() PlayerIdMap.Length={PlayerIdMap.Length}")
# プレイヤーが途中離脱した時(自滅・リスポーン含む)または撃破された時、プレイヤー数をチェック。
# プレイヤー数が1人以下になったら、ループを抜ける。
loop:
Winner := race:
block:
Player := GetPlayspace().PlayerRemovedEvent().Await()
("PlayerRemovedEvent", Player)
block:
Agent := ElimManager.EliminatedEvent.Await()
("EliminatedEvent", Agent)
if(CheckWinCondition[Winner]):
break
Print("GameLoop_B() End PlayerIdMap.Length={PlayerIdMap.Length}")
loop:
プレイヤーが途中離脱する時(自滅・リスポーン含む)または撃破される時たびに、loop式内の処理を実行します。
Winner := race:
PlayerRemovedEvent関数、 EliminatedEvent を待機して、いずれかのイベントの発生を待機します。
if(CheckWinCondition[Winner]):
break
勝利判定に成功することでloop式を抜け、ログ出力後、GameLoop_B関数を完了します。
LogVerse: : GameLoop_B() Start
LogVerse: : GameLoop_B() Anonymous[256]'s PlayerId=0
LogVerse: : GameLoop_B() Anonymous[257]'s PlayerId=1
LogVerse: : GameLoop_B() PlayerIdMap.Length=2
LogVerse: : CheckWinCondition() EliminatedEvent
LogVerse: : GameLoop_B() End PlayerIdMap.Length=1
LogVerse: : GameLoop_B() Start
LogVerse: : CheckWinCondition() EliminatedEvent
撃破マネージャーのEliminatedEventによって、CheckWinCondition関数を1回だけ実行しました。「LogVerse: : GameLoop_B() End」も1度になりました。思い描いていたかたちです。
まとめ
撃破マネージャーで自滅を有効にすると、途中離脱時にも通知を受け取れてしまうことは、小さいけれど大きな気づきでした。自滅を有効にしなければいいのですが プレイヤーの自滅も拾いたいこともあると思います。
たとえば、今回のようにプログラムで対策して、撃破マネージャー(自滅有効)と、途中離脱時のイベントを併用しましょう。
Discussion