「タイムトライアル:ピザ配送」チュートリアルのこの最後のステップでは、ゲームの完全なコードを示し、このゲームを自力でさらに進化させるためのアイデアを紹介します。
完全なコード
このプロジェクトには複数の Verse ファイルがあります。
- countdown_timer.verse:このファイルの完全なコードについては、「カスタム カウントダウン タイマー」を参照してください。
- game_coordinator_device.verse:このファイルの完全なコードについては、以下を参照してください。
- objective_marker.verse:このファイルの完全なコードについては、「移動する目標マーカー」チュートリアルを参照してください。
- pickup_delivery_zone.verse:このファイルの完全なコードについては、以下を参照してください。
- score_manager.verse:このファイルの完全なコードについては、以下を参照してください。
game_coordinate_device.verse
using { /Verse.org/Simulation}
using { /Fortnite.com/Devices}
using { /Fortnite.com/Vehicles}
using { /Fortnite.com/Characters}
using { /Fortnite.com/Playspaces}
using { /Verse.org/Native}
using { /Verse.org/Random}
using { /UnrealEngine.com/Temporary/Diagnostics}
using { /UnrealEngine.com/Temporary/SpatialMath}
using { /UnrealEngine.com/Temporary/Curves}
using { /Verse.org/Simulation/Tags}
# ゲーム ゾーン タグ
pickup_zone_tag<public> := class(tag):
pickup_zone_level_1_tag<public> := class(pickup_zone_tag):
pickup_zone_level_2_tag<public> := class(pickup_zone_tag):
pickup_zone_level_3_tag<public> := class(pickup_zone_tag):
delivery_zone_tag<public> := class(tag):
log_pizza_pursuit<internal> := class(log_channel){}
game_coordinator_device<public> := class(creative_device):
@editable
VehicleSpawner<public> : vehicle_spawner_atk_device = vehicle_spawner_atk_device{}
# カウントダウン タイマーの初期値。
@editable
InitialCountdownTime<public> : float = 30.0
@editable
EndGame<public> : end_game_device = end_game_device{}
# ピックアップしたアイテムを配送したときにカウントダウン タイマーに追加する秒数。
@editable
DeliveryBonusSeconds<public> : float = 20.0
@editable
PickupMarker<public> : objective_marker = objective_marker{}
@editable
ScoreManagerDevice<public> : score_manager_device = score_manager_device{}
@editable
PizzaRemover<public> : item_remover_device = item_remover_device{}
@editable
# ピックアップ レベルに応じて、ピックアップに与えるポイント数をマッピングします。
PointsForPickupLevel<public> : []int = array{1, 2, 3}
OnBegin<override>()<suspends> : void =
FindPlayer()
SetupZones()
# 乗り物に乗り込んだ後に、プレイヤーはいつでもその乗り物から降りることができますが、
# 降りるたびにそれを検知して、プレイヤーを乗り物に戻すようにします。
VehicleSpawner.AgentExitsVehicleEvent.Subscribe(HandlePlayerExitsVehicle)
# プレイヤーがこの乗り物に初めて乗り込んで、ゲームが開始されたときにのみ通知されるようにします。
# StartGameOnPlayerEntersVehicle はそのイベントを待ってから、ゲームプレイ ループを開始します。
StartGameOnPlayerEntersVehicle()
Logger<private> : log = log{Channel := log_pizza_pursuit}
var MaybePlayer<private> : ?player = false
var CountdownTimer<private> : countdown_timer = countdown_timer{}
var ScoreManager<private> : score_manager = score_manager{}
DeliveryZoneSelector<private> : tagged_zone_selector = tagged_zone_selector{}
var PickupZones<private> : []tagged_zone_selector = array{}
FindPlayer<private>() : void =
# これはシングル プレイヤー ゲームであるため、取得可能なプレイヤーは最初のプレイヤー (0)
# だけのはずです。
Playspace := Self.GetPlayspace()
if (FirstPlayer := Playspace.GetPlayers()[0]):
set MaybePlayer = option{FirstPlayer}
Logger.Print("Player found")
else:
# プレイヤーが見つからない場合は、エラーをログに記録します。
# 少なくとも 1 人のプレイヤーが必ず存在するため、これは起こらないはずです。
Logger.Print("Can't find valid player", ?Level := log_level.Error)
SetupZones<private>() : void =
# 配送ゾーンは難易度レベルでスケールされないため、配送ゾーンのタイプは 1 つのみです。
DeliveryZoneSelector.InitZones(delivery_zone_tag{})
# 難易度レベルに応じて、ゲームプレイ タグを使用して (仕掛けによって表される) ゾーンを選択します。
# 配列を使用することで、難易度レベルを変更しやすくなります。つまり、コードを変更することなく、
# レベルの追加、難易度レベルのきめ細かさの増減、順序の変更などを行うことができます。
# 難易度レベル タグごとに tagged_zone_selector を 1 つずつ作成し、同じタグが付いている (つまり、同じ難易度レベルの) すべての仕掛けが
# 同じ選択プールに入るようにします。
LevelTags : []pickup_zone_tag = array{pickup_zone_level_1_tag{}, pickup_zone_level_2_tag{}, pickup_zone_level_3_tag{}}
set PickupZones = for (ZoneTag : LevelTags):
NewZone := tagged_zone_selector{}
NewZone.InitZones(ZoneTag)
NewZone
StartGameOnPlayerEntersVehicle<private>()<suspends> : void =
VehiclePlayer := VehicleSpawner.AgentEntersVehicleEvent.Await()
Logger.Print("Player entered the vehicle")
set MaybePlayer = option{player[VehiclePlayer]}
StartGame()
HandlePlayerExitsVehicle<private>(VehiclePlayer : agent) : void =
Logger.Print("Player exited the vehicle.Reassigning player to vehicle")
VehicleSpawner.AssignDriver(VehiclePlayer)
StartGame<private>()<suspends> : void =
Logger.Print("Trying to start the game...")
<# 開始時に InitialCountdownTime からカウントダウンする新しい countdown_timer を作成します。
さらに、プレイヤーのスコアとピックアップ レベルをトラックする新しいスコア マネージャー (score_manager) も構築します。
この countdown_timer と score_manager には、その UI を表示する先のプレイヤーが必要になります。
この時点で、乗り物に乗り込んで、ゲームの開始をトリガーした有効なプレイヤーが 1 名存在するはずです。#>
if (ValidPlayer := MaybePlayer?):
Logger.Print("Valid player, starting game...")
set ScoreManager = MakeScoreManager(ValidPlayer, ScoreManagerDevice)
ScoreManager.AddScoreManagerToUI()
set CountdownTimer = MakeCountdownTimer(InitialCountdownTime, ValidPlayer)
CountdownTimer.StartCountdown()
# カウントダウンが終了するのを待ちます。
# 同時に、コア ゲームプレイを構成するピックアップと配送のゲーム ループも実行します。
race:
HandleCountdownEnd(ValidPlayer)
PickupDeliveryLoop()
else:
Logger.Print("Can't find valid player.Aborting game start", ?Level := log_level.Error)
HandleCountdownEnd<private>(InPlayer : player)<suspends> : void =
TotalTime := CountdownTimer.CountdownEndedEvent.Await()
ScoreManager.AwardScore()
EndGame.Activate(InPlayer)
PickupDeliveryLoop<private>()<suspends> : void =
PickupZonesTags : []pickup_zone_tag = array{pickup_zone_level_1_tag{}, pickup_zone_level_2_tag{}, pickup_zone_level_3_tag{}}
MaxPickupLevel := PickupZonesTags.Length - 1
FirstPickupZoneCompletedEvent := event(){}
<# PickupDeliveryLoop の終了時に必ずマーカーが無効になるように、MapIndicator の無効化を遅らせます。
PickupDeliveryLoop がキャンセルされた場合も実行を遅らせます。#>
defer:
if (ValidPlayer := MaybePlayer?):
PickupMarker.MapIndicator.DeactivateObjectivePulse(ValidPlayer)
PickupMarker.MapIndicator.Disable()
PickupMarker.MapIndicator.Enable()
loop:
var PickupLevel : int = 0
var IsFirstPickup : logic = true
<# ループが再スタートするたびに、ScoreManager を通じてピックアップ レベルの UI をリセットする必要があります。
UI のピックアップ レベルは 0 ではなく 1 から始まります。 0 から始めると、プレイヤーが混乱する可能性があります。
インデックスは 0 から始まるため、PickupLevel=0 は UI でのレベル 1 です。#>
ScoreManager.UpdatePickupLevel(PickupLevel + 1)
race:
loop:
if (PickupZone:base_zone = PickupZones[PickupLevel].SelectNext[]):
PickupZone.ActivateZone()
Sleep(0.0)
PickupMarker.MoveMarker(PickupZone.GetTransform(), ?OverTime := 0.0)
if (ValidPlayer := MaybePlayer?):
PickupMarker.MapIndicator.ActivateObjectivePulse(ValidPlayer)
<# これは、アクティブ化する PickupZone に対して必要な唯一の遅延です。これにより、それぞれの外部ループの終了時に最初の PickupZone が無効になるか、
後のあらゆる PickupZone が無効になります。これは、PickupZone 変数がより新しいゾーンにバインドされている場合は、この式が最後に評価されるためです。#>
defer:
PickupZone.DeactivateZone()
PickupZone.ZoneCompletedEvent.Await()
Logger.Print("Picked up", ?Level:=log_level.Normal)
<# ピックアップされたピザがスタックに溜まってスタックが満杯になったときにピザが地面に落ちないように、プレイヤーのインベントリから取り除きます。#>
if (RemovingPlayer := MaybePlayer?):
PizzaRemover.Remove(RemovingPlayer)
# 最初のピックアップの後に、配送ゾーンを有効にすることができます。
if (IsFirstPickup?):
set IsFirstPickup = false
FirstPickupZoneCompletedEvent.Signal()
if (PickupPoints := PointsForPickupLevel[PickupLevel]):
ScoreManager.UpdatePendingScore(PickupPoints)
# ピックアップ レベルと ScoreManager を更新します。
if (PickupLevel < MaxPickupLevel):
set PickupLevel += 1
ScoreManager.UpdatePickupLevel(PickupLevel + 1)
else:
Logger.Print("Can't find next PickupZone to select.",?Level := log_level.Error)
return # Error out of the PickupDeliveryLoop
block:
FirstPickupZoneCompletedEvent.Await()
if (DeliveryZone := DeliveryZoneSelector.SelectNext[]):
DeliveryZone.ActivateZone()
# PickupDeliveryLoop がキャンセルされた場合にもアクティブな配送ゾーンが非アクティブ化されるように、ゾーンの非アクティブ化を遅らせます。
defer:
Logger.Print("Deactivating delivery zone.",?Level:=log_level.Normal)
DeliveryZone.DeactivateZone()
DeliveryZone.ZoneCompletedEvent.Await()
Logger.Print("Delivered", ?Level:=log_level.Normal)
PointsCommitted := ScoreManager.AddPendingScoreToTotalScore()
BonusTime : float = DeliveryBonusSeconds * PointsCommitted
CountdownTimer.AddRemainingTime(BonusTime)
else:
Logger.Print("Can't find next DeliveryZone to select.",?Level:=log_level.Error)
return # Error out of the PickupDeliveryLoop
pickup_delivery_zone.verse
using { /Verse.org/Simulation}
using { /Verse.org/Random}
using { /Verse.org/Concurrency}
using { /Verse.org/Simulation/Tags}
using { /UnrealEngine.com/Temporary/SpatialMath}
using { /Fortnite.com/Devices}
<# ゾーンは (仕掛けによって表される) マップのエリアであり、アクティブ化および非アクティブ化でき、イベントを提供します。
ゾーンが「完了」した場合 (次のアクティベーションまでこれ以上完了できない状態) に通知するイベントを提供します。
ゾーンの「完了」は、そのゾーンの仕掛けのタイプ (ActivatorDevice) に依存します。
Suggested usage: ActivateZone() -> ZoneCompletedEvent.Await() -> DeactivateZone() #>
base_zone<public> := class:
ActivatorDevice<public> : creative_object_interface
ZoneCompletedEvent<public> : event(base_zone) = event(base_zone){}
GetTransform<public>() : transform =
ActivatorDevice.GetTransform()
<# ゾーンをアクティブ化します。
ゾーンの仕掛けとビジュアル インジケータをここで有効にします。#>
ActivateZone<public>() : void =
# ベース ゾーンは、アイテム スポナーまたはキャプチャー エリアとして定義されているゾーンを処理できます。
# それぞれの型に型変換してみて、どちらを扱っているかを確認します。
if (CaptureArea := capture_area_device[ActivatorDevice]):
CaptureArea.Enable()
spawn { WaitForZoneCompleted(option{CaptureArea.AgentEntersEvent}) }
else if (ItemSpawner := item_spawner_device[ActivatorDevice]):
ItemSpawner.Enable()
spawn { WaitForZoneCompleted(option{ItemSpawner.ItemPickedUpEvent}) }
<# ゾーンを非アクティブ化します。
ゾーンの仕掛けとビジュアル インジケータをここで無効にします。#>
DeactivateZone<public>() : void =
if (CaptureArea := capture_area_device[ActivatorDevice]):
CaptureArea.Disable()
else if (ItemSpawner := item_spawner_device[ActivatorDevice]):
ItemSpawner.Disable()
ZoneDeactivatedEvent.Signal()
<# このイベントは、ゾーンが完了される前に非アクティブ化された場合に、WaitForZoneCompleted コルーチンを終了するために必要です。#>
ZoneDeactivatedEvent<protected> : event() = event(){}
WaitForZoneCompleted<private>(ZoneDeviceCompletionEventOpt : ?awaitable(agent))<suspends> : void =
if (DeviceEvent := ZoneDeviceCompletionEventOpt?):
race:
block:
DeviceEvent.Await()
ZoneCompletedEvent.Signal(Self)
ZoneDeactivatedEvent.Await()
MakeBaseZone<constructor><public>(InActivatorDevice : creative_object_interface) := base_zone:
ActivatorDevice := InActivatorDevice
# tagged_zone_selector は、InitZones に渡されたタグでタグ付けされているトリガーに基づいてゾーンを作成します。
tagged_zone_selector<public> := class:
var Zones<protected> : []base_zone = array{}
InitZones<public>(ZoneTag : tag) : void =
<# ゾーン セレクターの作成時に、次のゾーンが選択されるたびに、
それらをキャッシュして、毎回次のゾーンが選択されたときに、タグ付けされた仕掛けを
利用可能なすべてのゾーンを見つけます。#>
ZoneDevices := GetCreativeObjectsWithTag(ZoneTag)
set Zones = for (ZoneDevice : ZoneDevices):
MakeBaseZone(ZoneDevice)
SelectNext<public>()<transacts><decides> : base_zone =
Zones[GetRandomInt(0, Zones.Length-1)]
score_manager.verse
using { /UnrealEngine.com/Temporary/UI}
using { /Fortnite.com/UI}
using { /Fortnite.com/Devices}
using { /Verse.org/Simulation}
MakeScoreManager<constructor><public>(InPlayer : player, InScoreManagerDevice : score_manager_device) := score_manager:
MaybePlayer := option{InPlayer}
MaybePlayerUI := option{GetPlayerUI[InPlayer]}
score_manager := class:
<# スコア マネージャーの存続期間中にキャンバスを再作成しなくて済むように、
この型のオブジェクトが作成されたときに、これを 1 回実行します。#>
block:
set Canvas = canvas:
Slots := array:
canvas_slot:
Widget := stack_box:
Orientation := orientation.Vertical
Slots := array:
stack_box_slot:
Widget := TotalGameScoreWidget
stack_box_slot:
Widget := PendingScoreWidget
stack_box_slot:
Widget := PickupLevelWidget
Offsets := margin{ Top:=0.0,Left:=500.0}
AddScoreManagerToUI<public>() : void =
if (PlayerUI := MaybePlayerUI?):
PlayerUI.AddWidget(Canvas)
UpdateUI()
<# PendingScore を TotalGameScore に加算し、PendingScore を 0 にリセットします。
加算されたピックアップ ポイントの総数を返します。#>
AddPendingScoreToTotalScore<public>() : int =
set TotalGameScore += PendingScore
defer:
set PendingScore = 0
UpdateUI()
PendingScore
<# 与えられた数のポイントを保留中のポイントに加算します。#>
UpdatePendingScore<public>(Points : int) : void =
set PendingScore += Points
UpdateUI()
UpdatePickupLevel<public>(Level : int) : void =
set PickupLevel = Level
UpdateUI()
<# ScoreManagerDevice を介してプレイヤーにスコアを付与し、それをアクティブ化します。#>
AwardScore<public>() : void =
ScoreManagerDevice.SetScoreAward(TotalGameScore)
if (AwardedPlayer := MaybePlayer?):
ScoreManagerDevice.Activate(AwardedPlayer)
MaybePlayer<internal> : ?player = false
MaybePlayerUI<internal> : ?player_ui = false
ScoreManagerDevice<internal> : score_manager_device = score_manager_device{}
var Canvas<internal> : canvas = canvas{}
TotalGameScoreWidget<internal> : text_block = text_block{}
PendingScoreWidget<internal> : text_block = text_block{}
PickupLevelWidget<internal> : text_block = text_block{}
PickupLevelText<private><localizes>(InLevel : int) : message = "Pickup Level: {InLevel}"
PendingScoreText<private><localizes>(InPoints : int) : message = "Pending Points: {InPoints}"
TotalGameScoreText<private><localizes>(InPoints : int) : message = "Total Points: {InPoints}"
var TotalGameScore<private> : int = 0
var PendingScore<private> : int = 0
var PickupLevel<private> : int = 0
UpdateUI<private>() : void =
if (PlayerUI := MaybePlayerUI?):
PickupLevelWidget.SetText(PickupLevelText(PickupLevel))
PendingScoreWidget.SetText(PendingScoreText(PendingScore))
TotalGameScoreWidget.SetText(TotalGameScoreText(TotalGameScore))
応用編
このガイドを完了し、Verse を使って完全な タイムトライアル:ピザ配送ゲームを作成する方法を習得しました。
ここで学習したことを活かして以下を試してみてください。
- ピックアップ ゾーンのレベルを増やす。
- さまざまなタイプの配送ゾーンを追加する。
base_zoneクラスを拡張して、ゾーンを完了するために、プレイヤーは他の仕掛け (ボタンなど) を有効にしなければならないようにする。 - ピザを配送するために、プレイヤーがカートから降りて短い障害物コースを歩いて完了しなければならないようにする。
- 複数のゾーンを同時にアクティブ化する。
- プレイヤーからの距離に基づいてゾーンの選択基準を調整する。