En este último paso del tutorial Prueba contrarreloj: Persecución de pizza, encontrarás el código completo del juego e ideas para explorar por tu cuenta este juego.
Código completo
En este proyecto, hay múltiples archivos de Verse.
- countdown_timer.verse: consulta Cronómetro de cuenta regresiva personalizado para ver el código completo del archivo.
- game_coordinator_device.verse: consulta debajo para ver el código completo del archivo.
- objective_marker.verse: consulta el tutorial Marcador de objetivo móvil para ver el código completo del archivo.
- pickup_delivery_zone.verse: consulta dbajo para ver el código completo del archivo.
- score_manager.verse: Consulta a continuación el código completo del archivo.
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 }
# Etiquetas de zonas de juego
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{}
# Duración del cronómetro de cuenta regresiva.
@editable
InitialCountdownTime<public> : float = 30.0
@editable
EndGame<public> : end_game_device = end_game_device{}
# Cuántos segundos añadir al cronómetro de cuenta regresiva cuando se entrega una recolección.
@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
# Mapea cuántos puntos vale una recolección en función de su nivel de recolección.
PointsForPickupLevel<public> : []int = array{1, 2, 3}
OnBegin<override>()<suspends> : void =
FindPlayer()
SetupZones()
# Después de entrar al vehículo, el jugador podría salir en cualquier momento; nosotros
# queremos detectar esto cada vez que sucede para volver a colocarlo en el vehículo.
VehicleSpawner.AgentExitsVehicleEvent.Subscribe(HandlePlayerExitsVehicle)
# Solo queremos que se nos notifique la primera vez que el jugador entra en el vehículo para iniciar la partida.
# StartGameOnPlayerEntersVehicle esperará ese evento y luego iniciará el bucle de juego.
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 =
# Como esta es una experiencia de jugador único, el primer jugador (0)
# debería ser el único disponible.
Playspace := Self.GetPlayspace()
if (FirstPlayer := Playspace.GetPlayers()[0]):
set MaybePlayer = option{FirstPlayer}
Logger.Print("Jugador encontrado")
else:
# Registra un error si no se puede encontrar el jugador.
# Esto no debería ocurrir porque al menos un jugador siempre está presente.
Logger.Print("No se puede encontrar un jugador válido", ?Level := log_level.Error)
SetupZones<private>() : void =
# Solo hay un tipo de zona de entrega, ya que no escala por nivel de dificultad.
DeliveryZoneSelector.InitZones(delivery_zone_tag{})
# Usamos etiquetas de juego para seleccionar zonas (representadas por dispositivos) en función de su nivel de dificultad.
# El uso de una matriz facilita la modificación de los niveles de dificultad: podemos añadir más
# niveles, aumentar o disminuir su granularidad o cambiar su orden sin tocar el código.
# Crea un tagged_zone_selector para cada etiqueta de nivel de dificultad, de modo que todos los dispositivos con la misma etiqueta (es decir, el mismo nivel de dificultad)
# terminan en el mismo grupo de selección.
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("El jugador entró al vehículo")
set MaybePlayer = option{player[VehiclePlayer]}
StartGame()
HandlePlayerExitsVehicle<private>(VehiclePlayer : agent) : void =
Logger.Print("El jugador salió del vehículo. Reasignando jugador al vehículo")
VehicleSpawner.AssignDriver(VehiclePlayer)
StartGame<private>()<suspends> : void =
Logger.Print("Intentando iniciar el juego...")
<# Creamos un nuevo countdown_timer que realizará la cuenta regresiva desde InitialCountdownTime una vez iniciado.
También construye un nuevo score_manager que hará un seguimiento de la puntuación del jugador y del nivel de recolección.
countdown_timer y score_manager requieren un jugador a quien mostrar su IU.
Ya deberíamos tener un jugador válido: el que entró en el vehículo, lo que activará el inicio del juego. #>
if (ValidPlayer := MaybePlayer?):
Logger.Print("Jugador válido, iniciando juego...")
set ScoreManager = MakeScoreManager(ValidPlayer, ScoreManagerDevice)
ScoreManager.AddScoreManagerToUI()
set CountdownTimer = MakeCountdownTimer(InitialCountdownTime, ValidPlayer)
CountdownTimer.StartCountdown()
# Esperamos que finalice la cuenta regresiva.
# Al mismo tiempo, también ejecutamos el bucle de juego Recolección y entrega que constituye el corazón del juego.
race:
HandleCountdownEnd(ValidPlayer)
PickupDeliveryLoop()
else:
Logger.Print("No se puede encontrar un jugador válido. Cancelando inicio de juego", nivel := 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(){}
<# Aplaza la deshabilitación de MapIndicator para que la terminación de PickupDeliveryLoop siempre termine deshabilitando el marcador.
El aplazamiento también se ejecuta si se cancela PickupDeliveryLoop. #>
defer:
if (ValidPlayer := MaybePlayer?):
PickupMarker.MapIndicator.DeactivateObjectivePulse(ValidPlayer)
PickupMarker.MapIndicator.Disable()
PickupMarker.MapIndicator.Enable()
loop:
var PickupLevel : int = 0
var IsFirstPickup : logic = true
<# Cada vez que se reinicia el bucle, debemos restablecer la IU del nivel de recolección por medio de ScoreManager.
El nivel de recolección en la IU se inicia con 1 (no 0). Algunos jugadores se confundirán si se inicia con 0.
Indexamos desde 0, por lo que PickupLevel=0 es el nivel 1 en la IU. #>
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)
<# Este es el único aplazamiento que necesitamos para cualquier PickupZone que activemos. Desactivará la primera PickupZone al final de cada bucle externo,
o desactivará cualquier PickupZone posterior. Esto se debe a que la expresión se evalúa al final, cuando la variable PickupZone se vincula a una zona más nueva. #>
defer:
PickupZone.DeactivateZone()
PickupZone.ZoneCompletedEvent.Await()
Logger.Print("Recolectado", ?Level:=log_level.Normal)
<# Eliminamos las recolecciones de pizza del inventario del jugador para evitar que se acumulen y se caigan al suelo una vez que se llena la pila. #>
if (RemovingPlayer := MaybePlayer?):
PizzaRemover.Remove(RemovingPlayer)
# Después de la primera recolección, podemos habilitar la zona de entrega.
if (IsFirstPickup?):
set IsFirstPickup = false
FirstPickupZoneCompletedEvent.Signal()
if (PickupPoints := PointsForPickupLevel[PickupLevel]):
ScoreManager.UpdatePendingScore(PickupPoints)
# Actualiza el nivel de recolección y ScoreManager.
if (PickupLevel < MaxPickupLevel):
set PickupLevel += 1
ScoreManager.UpdatePickupLevel(PickupLevel + 1)
else:
Logger.Print("No se puede encontrar la siguiente PickupZone para seleccionar.", ?Level := log_level.Error)
return # Error fuera de PickupDeliveryLoop
block:
FirstPickupZoneCompletedEvent.Await()
if (DeliveryZone := DeliveryZoneSelector.SelectNext[]):
DeliveryZone.ActivateZone()
# Diferimos la desactivación de la zona de manera que al cancelar PickupDeliveryLoop también se desactive cualquier zona de entrega activa.
defer:
Logger.Print("Desactivando zona de entrega.", ?Level:=log_level.Normal)
DeliveryZone.DeactivateZone()
DeliveryZone.ZoneCompletedEvent.Await()
Logger.Print("Entegado", ?Level:=log_level.Normal)
PointsCommitted := ScoreManager.AddPendingScoreToTotalScore()
BonusTime : float = DeliveryBonusSeconds * PointsCommitted
CountdownTimer.AddRemainingTime(BonusTime)
else:
Logger.Print("No se pudo encontrar la siguiente DeliveryZone para seleccionar.", ?Level:=log_level.Error)
return # Error fuera de 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 }
<# Una zona es un área del mapa (representada por un dispositivo) que puede activarse o desactivarse y que proporciona eventos
para indicar cuando la zona se "completó" (no se puede completar más hasta la próxima activación).
La zona "completada" depende del tipo de dispositivo (ActivatorDevice) de la zona.
Uso sugerido: 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()
<# Activa la zona.
Debes habilitar aquí los dispositivos y cualquier indicador visual para la zona. #>
ActivateZone<public>() : void =
# La zona base puede manejar zonas definidas como generadores de objetos o zonas de captura.
# Intenta convertir a cada tipo para ver con cuál nos enfrentamos.
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}) }
<# Desactiva la zona.
Debes deshabilitar aquí los dispositivos y cualquier indicador visual para la zona. #>
DeactivateZone<public>() : void =
if (CaptureArea := capture_area_device[ActivatorDevice]):
CaptureArea.Disable()
else if (ItemSpawner := item_spawner_device[ActivatorDevice]):
ItemSpawner.Disable()
ZoneDeactivatedEvent.Signal()
<# Este evento es necesario para terminar la corrutina WaitForZoneCompleted si la zona se desactiva sin estar completada. #>
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 crea zonas según los activadores etiquetados con la etiqueta que se pasa a InitZones.
tagged_zone_selector<public> := class:
var Zones<protected> : []base_zone = array{}
InitZones<public>(ZoneTag : tag) : void =
<# Al crear un selector de zona, busca todas las zonas disponibles
y las almacena en caché para que no perdamos tiempo buscando dispositivos etiquetados
cada vez que se selecciona la siguiente zona. #>
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:
<# Ya que no recrearemos el lienzo durante la duración del administrador de puntaje, hazlo una vez
cada vez que se cree un objeto de este tipo. #>
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()
<# Añade PendingScore a TotalGameScore y restablece PendingScore a 0.
Devuelve la cantidad total de puntos de recolección añadidos. #>
AddPendingScoreToTotalScore<public>() : int =
set TotalGameScore += PendingScore
defer:
set PendingScore = 0
UpdateUI()
PendingScore
<# Añade la cantidad dada de puntos a los puntos pendientes. #>
UpdatePendingScore<public>(Points : int) : void =
set PendingScore += Points
UpdateUI()
UpdatePickupLevel<public>(Level : int) : void =
set PickupLevel = Level
UpdateUI()
<# Otorga la puntuación al jugador a través de ScoreManagerDevice y lo activa. #>
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))
Por tu cuenta
Al completar esta guía, aprendiste cómo usar Verse para crear un juego completo de prueba contrarreloj Persecución de pizza.
Con lo que has aprendido, intenta lo siguiente:
- Añade más niveles de zona de recolección.
- Añade diferentes tipos de zonas de entrega. Extiende la clase
base_zonepara que el jugador tenga que activar algún otro dispositivo, como un botón, para completar la zona. - Haz que el jugador se baje del carrito y complete una corta pista de obstáculos caminando para entregar la pizza.
- Activa varias zonas al mismo tiempo.
- Modula los criterios de selección de zona en función de la distancia al jugador.