En este último paso del tutorial Prueba contrarreloj: Reparto de pizzas, encontrarás el código completada del juego e ideas para explorar por tu cuenta en este juego.
Código completo
Hay varios archivos de Verse en este proyecto:
- countdown_timer.verse: Consulta Cronómetro de cuenta atrás personalizado para ver el código completo del archivo.
- **game_coordinator_device.Verse: Consulta a continuación el código del archivo completo.
- objective_marker.verse: Consulta el tutorial Marcador de objetivo móvil para ver el código completo del archivo.
- **pickup_delivery_zone.Verse: Consulta a continuación el código del archivo completada.
- score_manager.verse: Más abajo encontrarás 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> := clase(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> := clase(tag):
log_pizza_pursuit<internal> := clase(log_channel){}
game_coordinator_device<public> := class(creative_device):
@editable
VehicleSpawner<public> : vehicle_spawner_atk_device = vehicle_spawner_atk_device{}
# A partir de cuánto tiempo empezará la cuenta atrás el cronómetro.
@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 atrás cuando se entrega una recogida.
@editable
DeliveryBonusSeconds<public> : float = 20.0
@editable
PickupMarker<public> : Objective_marker = Objective_marker{}
@editable
ScoreManagerDevice<internal> : score_manager_device = score_manager_device{}
@editable
PizzaRemover<public> : item_remover_device = item_remover_device{}
@editable
# Asigna cuántos puntos vale una recogida en función de su nivel de recogida.
PointsForPickupLevel<public> : []int = array{1, 2, 3}
OnBegin<override>()<suspends> : void =
FindPlayer()
SetupZones()
# Tras entrar en el vehículo, el jugador puede salir en cualquier momento;
# queremos detectar esto cada vez que ocurra para volver a introducirlos en el vehículo.
VehicleSpawner.AgentExitsVehicleEvent.Subscribe(HandlePlayerExitsVehicle)
# Solo queremos que se notifique la primera vez que el jugador entre en el vehículo para iniciar la partida.
# StartGameOnPlayerEntersVehicle esperará ese evento e 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 se trata de una experiencia para un solo jugador, 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 podemos encontrar un jugador.
# Esto no debería ocurrir porque siempre hay al menos un jugador presente.
Logger.Print("No se puede encontrar ningún jugador válido", ?Level := log_level.Error)
SetupZones<private>() : void =
# Solo hay un tipo de zona de entrega, ya que no escalan por nivel de dificultad.
DeliveryZoneSelector.InitZones(delivery_zone_tag{})
# Utilizamos etiquetas de jugabilidad para seleccionar zonas (representadas por dispositivos) en función de su nivel de dificultad.
# Utilizar una matriz facilita la modificación de los niveles de dificultad: podemos añadir más
# niveles, aumentar/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)
# acaben 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 (definir) PickupZones = for (ZoneTag : LevelTags):
NewZone := tagged_zone_selector{}
NewZone.InitZones(ZoneTag)
NewZone
StartGameOnPlayerEntersVehicle<private>()<suspends> : void =
VehiclePlayer := VehicleSpawner.AgentEntersVehicleEvent.Await()
Logger.Print("El jugador ha entrado en el vehículo")
set MaybePlayer = option{player[VehiclePlayer]}
StartGame()
HandlePlayerExitsVehicle<private>(VehiclePlayer : agente) : void =
Logger.Print("El jugador ha salido del vehículo. Reasignando jugador al vehículo")
VehicleSpawner.AssignDriver(VehiclePlayer)
StartGame<private>()<suspends> : void =
Logger.Print("Intentando iniciar la partida...")
<# Construimos un nuevo countdown_timer que contará hacia atrás desde InitialCountdownTime una vez iniciado.
Construye también un nuevo score_manager que lleve la cuenta de la puntuación del jugador y del nivel de recogida.
countdown_timer y score_manager requieren un jugador al que mostrar su IU.
Ya deberíamos tener un jugador válido: el que entró en el vehículo y desencadenó el inicio de la partida. #>
if (ValidPlayer := MaybePlayer?):
Logger.Print("Jugador válido; iniciando la partida...")
set ScoreManager = MakeScoreManager(ValidPlayer, ScoreManagerDevice)
ScoreManager.AddScoreManagerToUI()
set CountdownTimer = MakeCountdownTimer(InitialCountdownTime, ValidPlayer)
CountdownTimer.StartCountdown()
# Esperamos a que termine la cuenta atrás.
# Al mismo tiempo, también ejecutamos el bucle de juego de recogida y entrega que constituye el núcleo del juego.
race:
HandleCountdownEnd(ValidPlayer)
PickupDeliveryLoop()
else:
Logger.Print("No se puede encontrar ningún jugador válido. Anulando inicio de la partida", ?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(){}
<# Aplaza la desactivación de MapIndicator para que la finalización del bucle PickupDeliveryLoop siempre acabe desactivando el marcador.
`defer` también se ejecuta si se cancela el bucle 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 reinicie el bucle, debemos reiniciar la IU del nivel de recogida a través del ScoreManager.
El nivel de recogida en la IU empieza en 1 (no en 0). Algunos jugadores se confundirán si empieza en 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 exterior
o desactivará cualquier PickupZone posterior. Esto se debe a que la expresión se evalúa al final, cuando la variable PickupZone se ha vinculado a una zona más nueva. #>
defer:
PickupZone.DeactivateZone()
PickupZone.ZoneCompletedEvent.Await()
Logger.Print("Recogido", ?Level:=log_level.Normal)
<# Eliminamos las pizzas recogidas del inventario del jugador para evitar apilarlas y que caigan al suelo una vez que la pila esté llena. #>
if (RemovingPlayer := MaybePlayer?):
PizzaRemover.Remove(RemovingPlayer)
# Después de la primera recogida podemos habilitar la zona de entrega.
if (IsFirstPickup?):
set IsFirstPickup = false
FirstPickupZoneCompletedEvent.Signal()
if (PickupPoints := PointsForPickupLevel[PickupLevel]):
ScoreManager.UpdatePendingScore(PickupPoints)
# Actualiza el nivel de recogida y ScoreManager.
if (PickupLevel < MaxPickupLevel):
set PickupLevel += 1
ScoreManager.UpdatePickupLevel(PickupLevel + 1)
else:
Logger.Print("No se encuentra la siguiente zona de recogida para seleccionar.", ?Level := log_level.Error)
return # Error de PickupDeliveryLoop
block:
FirstPickupZoneCompletedEvent.Await()
if (DeliveryZone := DeliveryZoneSelector.SelectNext[]):
DeliveryZone.ActivateZone()
# Diferimos la desactivación de la zona para que la cancelación de PickupDeliveryLoop también acabe desactivando cualquier zona de entrega activa.
defer:
Logger.Print("Desactivando zona de entrega.", ?Level:=log_level.Normal)
DeliveryZone.DeactivateZone()
DeliveryZone.ZoneCompletedEvent.Await()
Logger.Print("Entregado", ?Level:=log_level.Normal)
PointsCommitted := ScoreManager.AddPendingScoreToTotalScore()
BonusTime : float = DeliveryBonusSeconds * PointsCommitted
CountdownTimer.AddRemainingTime(BonusTime)
else:
Logger.Print("No se encuentra la siguiente zona de entrega para seleccionar.", ?Level:=log_level.Error)
return # Error 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 se puede activar/desactivar y que proporciona eventos
para señalar cuando la zona se ha "completado" (ya no se puede completar hasta la siguiente activación).
La zona "completada" depende del tipo de dispositivo (ActivatorDevice) de la zona.
Uso sugerido: ActivateZone() -> ZoneCompletedEvent.Await() -> DeactivateZone() #>
base_zone<public> := clase:
ActivatorDevice<public> : modo Creativo
ZoneCompletedEvent<public> : evento(base_zone) = evento(base_zone){}
GetTransform<public>() : transformar =
ActivatorDevice.GetTransform()
<# Activa la zona.
Aquí debes activar los dispositivos y cualquier indicador visual de la zona. #>
ActivateZone<public>() : void =
# La zona base puede manejar zonas definidas como generadoras de objetos o zonas de captura.
# Prueba a hacer casting cada tipo para ver con cuál estamos tratando.
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.
Aquí debes desactivar los dispositivos y cualquier indicador visual de 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 completarse. #>
ZoneDeactivatedEvent<protected> : evento() = evento(){}
WaitForZoneCompleted<private>(ZoneDeviceCompletionEventOpt : ?awaitable(agente))<suspends> : void =
if (DeviceEvent := ZoneDeviceCompletionEventOpt?):
race:
block:
DeviceEvent.Await()
ZoneCompletedEvent.Signal(Self)
ZoneDeactivatedEvent.Await()
MakeBaseZone<constructor><public>(InActivatorDevice : modo Creativo) := base_zone:
ActivatorDevice := InActivatorDevice
# tagged_zone_selector crea zonas basadas en activadores etiquetados con la etiqueta pasada a InitZones.
tagged_zone_selector<public> := clase:
var Zones<protected> : []base_zone = array{}
InitZones<public>(ZoneTag : tag) : void =
<# Al crear un selector de zona, busca todas las zonas disponibles
y guárdalas en caché para no consumir tiempo buscando dispositivos etiquetados
cada vez que se selecciona la siguiente zona. #>
ZoneDevices := GetCreativeObjectsWithTag(ZoneTag)
set (definir) 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:
<# Como no volveremos a crear el lienzo mientras dure el gestor de puntuación, 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 el número total de puntos de recogida añadidos. #>
AddPendingScoreToTotalScore<public>() : int =
set TotalGameScore += PendingScore
defer:
set PendingScore = 0
UpdateUI()
PendingScore
<# Añade la cantidad de puntos dada a los puntos pendientes. #>
UpdatePendingScore<public>(Points : int) : void =
set PendingScore += Points
UpdateUI()
UpdatePickupLevel<public>(nivel : int) : void =
set PickupLevel = Level
UpdateUI()
<# Adjudica la puntuación al jugador a través de ScoreManagerDevice, activándolo. #>
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) : mensaje = "Nivel de recogida: {InLevel}"
PendingScoreText<private><localizes>(InPoints : int) : mensaje = "Puntos pendientes: {InPoints}"
TotalGameScoreText<private><localizes>(InPoints : int) : mensaje = "Total de puntos: {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, has aprendido a usar Verse para crear el juego Prueba contrarreloj: Reparto de pizzas.
Con lo que has aprendido, intenta lo siguiente:
- Añadir más niveles de zona de recogida.
- Añadir diferentes tipos de zonas de entrega. Amplía la clase
base_zonepara que el jugador tenga que activar algún otro dispositivo, como un botón, para completar la zona. - Hacer que el jugador se baje del kart y complete una carrera corta de obstáculos a pie para entregar la pizza.
- Activar varias zonas al mismo tiempo.
- Modular los criterios de selección de zona en función de la distancia al jugador.