이 타임 트라이얼 피자 배달 게임 튜토리얼의 마지막 단계에서는 게임의 완성된 코드와 직접 게임에서 탐구해 볼 아이디어를 살펴봅니다.
완성된 코드
이 프로젝트에는 다수의 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 =
# 배달 구역의 유형은 난이도 레벨에 따라 스케일 조절되지 않으므로 하나뿐입니다.
DeliveryZoneSelector.InitZones(delivery_zone_tag{})
# 난이도 레벨에 따라 (장치로 표현되는) 구역을 선택하는 게임플레이 태그를 사용합니다.
# 배열을 사용하면 난이도 레벨을 보다 쉽게 조정할 수 있습니다.
# 코드를 건드리지 않고도 레벨을 더 추가하고, 상세도를 높이거나 낮추고, 순서를 변경할 수 있습니다.
# 각 난이도 레벨 태그마다 tagged_zone_selector를 하나씩 생성하여 태그가 같은(즉, 난이도 레벨이 같은)
# 모든 장치가 같은 선택 풀에 들어가게 합니다.
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를 보여줄 플레이어가 필요합니다.
이제 유효한 플레이어가 있습니다. 탈것에 들어간 플레이어는 게임 시작을 트리거합니다. #>
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(){}
<# MapIndicator 비활성화를 defer로 지연시켜 PickupDeliveryLoop를 종료하면 항상 마커가 비활성화되게 합니다.
defer는 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에 필요한 유일한 defer입니다. 외부 루프의 끝에서 첫 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 # PickupDeliveryLoop에서 에러 발생
block:
FirstPickupZoneCompletedEvent.Await()
if (DeliveryZone := DeliveryZoneSelector.SelectNext[]):
DeliveryZone.ActivateZone()
# 구역 비활성화를 defer로 지연시켜 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 # 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)에 따라 달라집니다.
권장 사용 방법: 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:
<# 점수 관리 장치의 수명 동안 캔버스를 재생성하지 않을 것이므로,
이 타입의 오브젝트가 생성될 때 한 번만 수행합니다. #>
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()
<# TotalGameScore에 PendingScore를 더하고 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클래스를 확장하여 플레이어가 버튼 등 다른 장치를 활성화하고 구역을 완료하도록 만들어 보세요. - 플레이어가 카트에서 내려서 피자를 배달하기 위해 짧은 장애물 코스를 완료하게 하기
- 다수의 구역을 동시에 활성화하기
- 구역 선택 기준을 플레이어와의 거리에 따라 변조하기