Un bucle de juego es un código que se ejecuta repetidamente (bucle) para responder a una entrada (normalmente, la interacción del jugador con el control o el mouse), actualizar el estado del juego y proporcionar una salida que muestre al jugador que afectó el estado del juego, como cuando se presiona un botón y se enciende una luz. El bucle normalmente finaliza cuando el juego alcanza un estado de finalización, como cuando el jugador logra un objetivo, o un estado de fracaso, como cuando el jugador se queda sin tiempo antes de lograr el objetivo.
Al completar este paso del tutorial Prueba contrarreloj: Persecución de pizza, aprenderás a crear el bucle de juego y a definir los estados de finalización y fracaso del juego.
El siguiente es el pseudocódigo del bucle de juego de Prueba contrarreloj: Persecución de pizza:
loop:
race:
loop:
SelectNextPickupZone
WaitForPlayerToCompletePickupZone
block:
WaitForFirstPickup
SelectNextDeliveryZone
WaitForPlayerToCompleteDeliveryZone
Este bucle debería finalizar cuando finaliza la cuenta regresiva o si se produce un error inesperado en el juego.
Cómo crear el bucle de juego central
Sigue estos pasos para actualizar el archivo game_coordinator_device.verse:
Crea un nuevo método denominado
PickupDeliveryLoop()que tenga los especificadoresprivateysuspends. Mueve el bucle que creaste anteriormente enOnBegin()a este nuevo método.VerseOnBegin<override>;()<suspends> : void = SetupZones() PickupDeliveryLoop() PickupDeliveryLoop<private>()<suspends> : void = var PickupLevel : int = 0 loop: if (PickupZone : base_zone = PickupZoneSelectors[PickupLevel].SelectNext[]):Determina la cantidad máxima de niveles de recolección a partir de la longitud de la matriz de etiquetas y aumenta el valor de
PickupLevelcada vez que el jugador completa una zona de recolección, siempre y cuando el nivel de recolección no sea mayor que la cantidad máxima de niveles de recolección.VerseOnBegin<override>;()<suspends> : void = SetupZones() PickupDeliveryLoop() 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 var PickupLevel : int = 0La zona de entrega debería activarse después de que el jugador completa su primera recolección, pero el jugador debería poder recoger más elementos si lo desea antes de ir a la zona de entrega. Para ello, los códigos de la zona de recolección y de la zona de entrega deben ocurrir al mismo tiempo. En este ejemplo, se utiliza la expresión de concurrencia race por el siguiente motivo:
El bloque de entrega debería cancelar el bucle de zona de recolección cuando el jugador termina una entrega.
El bucle de zona de recolección debería cancelar el bloque de entrega si hay algún problema con el bucle de recolección.
También necesitas una ligera modificación de la desactivación de zonas. Cuando se cancelan el bucle o el bloque de entrega, no se debe llamar a
DeactivateZone()si la secuencia de comandos esperaba que se completara la zona.Como la línea de desactivación de la zona nunca se ejecutaría, la zona permanecería activa y crearía un error lógico.
Para corregir esto, puedes usar la expresión defer. Una expresión
deferretrasa la ejecución de las expresiones que contiene hasta que finaliza el ámbito en el quedeferaparece. Una expresióndeferse ejecutará una vez que el control del programa se transfiera fuera del ámbito, incluido cuando se abandona normalmente el ámbito (fin de una función), en una salida temprana (como una devolución o interrupción) o debido a cualquier cancelación de una tarea concurrente o expresión asíncrona (comorace). Es como poner en cola operaciones que se ejecutarán al final, independientemente de lo que ocurra. Ajusta cada llamada aDeactivateZoneen una expresióndefery muévela antes de su respectivoZoneCompletedEvent.Await().VersePickupDeliveryLoop<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 var PickupLevel : int = 0 race: loop: if (PickupZone : base_zone = PickupZoneSelectors[PickupLevel].SelectNext[]): PickupZone.ActivateZone()En el ejemplo anterior, se activa la zona de entrega al mismo tiempo que se activa la zona de recolección, pero la activación de la zona de entrega debería esperar hasta que se complete la primera recolección. Para ello, añade un evento y haz que la zona de entrega espere el evento antes de activarse.
VerseOnBegin<override>;()<suspends> : void = SetupZones() PickupDeliveryLoop() 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(){}Repite esta expresión race de zona de recolección/zona de entrega hasta que finalice el juego de manera que el jugador pueda continuar recogiendo y entregando elementos.
VerseOnBegin<override>;()<suspends> : void = SetupZones() PickupDeliveryLoop() 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(){}El archivo game_coordinator_device.verse ahora debería verse de la siguiente manera:
Verseusing { /Verse.org/Simulation } using { /Fortnite.com/Devices } using { /Fortnite.com/Vehicles } using { /Fortnite.com/Characters } using { /Fortnite.com/Playspaces } using { /Verse.org/Random } using { /UnrealEngine.com/Temporary/Diagnostics } using { /UnrealEngine.com/Temporary/SpatialMath } using { /UnrealEngine.com/Temporary/Curves } using { /Verse.org/Simulation/Tags }
Guarda los archivos de Verse, compila el código y realiza una prueba de juego en tu nivel.
Cuando realices una prueba de juego del nivel, uno de los dispositivos generadores de objetos se activará al comienzo del juego y después de que el jugador recoja un elemento. Después de que el jugador recoja su primer objeto, el dispositivo generador de objetos se desactivará y, a continuación, se activará un dispositivo de zona de captura. Esto continúa hasta que finalizas manualmente el juego.
Cómo definir los estados de finalización y fracaso del bucle de juego
Ahora que creaste el bucle de juego central, define el estado de finalización y fracaso del bucle de juego. Se supone que el juego finaliza cuando:
Finaliza la cuenta regresiva.
O bien, se produce un problema con el bucle de juego.
Realiza estos pasos para configurar los estados de finalización y fracaso del juego:
Crea una instancia de la clase countdown_timer en
game_coordinator_deviceque tenga el especificadorprivate.game_coordinator_device<public> := class(creative_device): @editable EndGame<public> : end_game_device = end_game_device{} var CountdownTimer<private> : countdown_timer = countdown_timer{}Versegame_coordinator_device<public> := class(creative_device): @editable EndGame<public> : end_game_device = end_game_device{} var CountdownTimer<private> : countdown_timer = countdown_timer{}Dado que el constructor de
countdown_timerrequiere una referencia de jugador, añade una variable de jugador opcional para almacenar una referencia al jugador en este juego para un solo jugador y crea una función llamadaFindPlayer()para obtener la referencia del jugador. Llama aFindPlayer()enOnBegin()antes de configurar las zonas.Versegame_coordinator_device<public> := class(creative_device): @editable EndGame<public> : end_game_device = end_game_device{} var CountdownTimer<private> : countdown_timer = countdown_timer{} var MaybePlayer<private> : ?player = false OnBegin<override>()<suspends> : void = FindPlayer() SetupZones() FindPlayer<private>() : void = # Since this is a single player experience, the first player (at index 0) # should be the only one available. if (FirstPlayer := GetPlayspace().GetPlayers()[0]): set MaybePlayer = option{FirstPlayer} Logger.Print("Player found") else: # Log an error if we can't find a player. # This shouldn't happen because at least one player is always present. Logger.Print("Can't find valid player", ?Level := log_level.Error)Crea una función denominada
HandleCountdownEnd()que espere a que finalice el cronómetro de cuenta regresiva y active el dispositivo de terminar partida.VerseHandleCountdownEnd<private>(InPlayer : agent)<suspends> : void = CountdownTimer.CountdownEndedEvent.Await() EndGame.Activate(InPlayer)Crea una función denominada
StartGame()y llámala después deSetupZones()enOnBegin(). Esta función debería hacer lo siguiente:Inicializa el cronómetro de cuenta regresiva.
Versegame_coordinator_device<public> := class(creative_device): # How long the countdown timer will start counting down from. @editable InitialCountdownTime<public> : float = 30.0 @editable EndGame<public> : end_game_device = end_game_device{} OnBegin<override>()<suspends> : void = FindPlayer() SetupZones() StartGame() StartGame<private>()<suspends> : void = Logger.Print("Trying to start the game...") <# We construct a new countdown_timer that'll countdown from InitialCountdownTime once started. The countdown_timer requires a player to show their UI to. We should have a valid player by now. #> if (ValidPlayer := MaybePlayer?): Logger.Print("Valid player, starting game...") set CountdownTimer = MakeCountdownTimer(InitialCountdownTime, ValidPlayer) CountdownTimer.StartCountdown() else: Logger.Print("Can't find valid player. Aborting game start", ?Level := log_level.Error)Usa la expresión
racepara llamar aHandleCountdownEnd(ValidPlayer)yPickupDeliveryLoop(), de manera que ocurra lo siguiente:Cuando finalice la cuenta regresiva y se detenga el bucle de juego.
O, si se detiene el bucle de juego, se cancele la cuenta regresiva.
VerseStartGame<private>()<suspends> : void = Logger.Print("Trying to start the game...") <# We construct a new countdown_timer that'll countdown from InitialCountdownTime once started. The countdown_timer requires a player to show their UI to. We should have a valid player by now. #> if (ValidPlayer := MaybePlayer?): Logger.Print("Valid player, starting game...") set CountdownTimer = MakeCountdownTimer(InitialCountdownTime, ValidPlayer) CountdownTimer.StartCountdown() # We wait for the countdown to end. # At the same time, we also run the Pickup and Delivery game loop that constitutes the core gameplay. race: HandleCountdownEnd(ValidPlayer) PickupDeliveryLoop() else: Logger.Print("Can't find valid player. Aborting game start", ?Level := log_level.Error)
Ahora, el archivo **game_coordinate_device.verse** debería verse así:
Verseusing { /Verse.org/Simulation } using { /Fortnite.com/Devices } using { /Fortnite.com/Vehicles } using { /Fortnite.com/Characters } using { /Fortnite.com/Playspaces } using { /Verse.org/Random } using { /UnrealEngine.com/Temporary/Diagnostics } using { /UnrealEngine.com/Temporary/SpatialMath } using { /UnrealEngine.com/Temporary/Curves } using { /Verse.org/Simulation/Tags }Guarda los archivos de Verse, compila el código y realiza una prueba de juego en tu nivel.
Cuando realizas una prueba de juego del nivel, el juego funciona igual que en la sección anterior pero, ahora, hay un temporizador que finalizará el juego cuando finalice la cuenta regresiva o se produzca algún problema en el bucle de juego.