Dans cette dernière étape du tutoriel Contre-la-montre : Pizza Pursuit, vous allez trouver le code complet du jeu et des idées à explorer par vous-même pour développer ce jeu.
Code complet
Ce projet contient plusieurs fichiers Verse :
- countdown_timer.verse : Consultez la rubrique Compte à rebours personnalisé pour obtenir le code complet du fichier.
- game_coordinator_device.verse : consultez la rubrique ci-dessous pour obtenir le code complet du fichier.
- objective_marker.verse : Consultez le tutoriel Déplacer le marqueur d’objectif pour obtenir le code complet du fichier.
- pickup_delivery_zone.verse : consultez la rubrique ci-dessous pour obtenir le code complet du fichier.
- score_manager.verse : consultez la rubrique ci-dessous pour obtenir le code complet du fichier.
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 }
# Balises de zones de jeux
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{}
# Durée à partir de laquelle le compte à rebours démarre.
@editable
InitialCountdownTime<public> : float = 30.0
@editable
EndGame<public> : end_game_device = end_game_device{}
# Nombre de secondes à ajouter au compte à rebours lors de la livraison d'un objet collecté.
@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
# Indique le nombre de points que vaut une collecte en fonction de son niveau.
PointsForPickupLevel<public> : []int = array{1, 2, 3}
OnBegin<override>()<suspends> : void =
FindPlayer()
SetupZones()
# Après être entré dans le véhicule, le joueur peut en sortir à tout moment ; il
# est nécessaire de détecter cet événement chaque fois qu’il se produit pour remettre le joueur dans le véhicule.
VehicleSpawner.AgentExitsVehicleEvent.Subscribe(HandlePlayerExitsVehicle)
# Nous souhaitons uniquement obtenir une notification la première fois que le joueur entre dans le véhicule pour démarrer le jeu.
# StartGameOnPlayerEntersVehicle attend cet événement et lance la boucle de jeu.
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 =
# Étant donné qu'il s'agit d'un jeu solo, le premier joueur (0)
# doit être le seul disponible.
Playspace := Self.GetPlayspace()
if (FirstPlayer := Playspace.GetPlayers()[0]):
set MaybePlayer = option{FirstPlayer}
Logger.Print("Player found")
else:
# Consigner une erreur si un joueur est introuvable.
# Aucune erreur ne devrait se produire, car il existe toujours au moins un joueur.
Logger.Print("Can't find valid player", ?Level := log_level.Error)
SetupZones<private>() : void =
# Il n’existe qu'un seul type de zone de livraison, car les zones de livraison ne sont pas modulées en fonction du niveau de difficulté.
DeliveryZoneSelector.InitZones(delivery_zone_tag{})
# Nous utilisons les étiquettes de jeu pour sélectionner les zones (représentées par des appareils) en fonction de leur niveau de difficulté.
# L'utilisation d'une matrice permet de modifier plus facilement les niveaux de difficulté : il est possible d’ajouter d'autres
# niveaux, d’augmenter/de diminuer leur granularité ou de changer leur ordre sans toucher au code.
# Créer un sélecteur tagged_zone_ pour chaque balise de niveau de difficulté afin que tous les appareils portant la même balise (c'est-à-dire le même niveau de difficulté)
# se retrouve dans le même pool de sélection.
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...")
<# Nous créons un nouveau compte à rebours qui démarre à partir de InitialCountdownTime une fois lancé.
Créez également un nouveau gestionnaire de score qui effectuera le suivi du score du joueur et du niveau de collecte.
countdown_timer et score_manager ont besoin d'un joueur à qui montrer leur interface utilisateur.
Nous devons maintenant disposer d’un joueur valide, à savoir celui qui est entré dans le véhicule et a donc déclenché le jeu. #>
if (ValidPlayer := MaybePlayer?):
Logger.Print("Valid player, starting game...")
set ScoreManager = MakeScoreManager(ValidPlayer, ScoreManagerDevice)
ScoreManager.AddScoreManagerToUI()
set CountdownTimer = MakeCountdownTimer(InitialCountdownTime, ValidPlayer)
CountdownTimer.StartCountdown()
# Nous attendons la fin du compte à rebours.
# En même temps, nous gérons la boucle de jeu de collecte et de livraison qui constitue le cœur du jeu.
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(){}
<# Reporter la désactivation de l’indicateur MapIndicator afin que la fin de la boucle PickupDeliveryLoop finisse toujours par désactiver le marqueur.
Le report a lieu également si la boucle PickupDeliveryLoop est annulée. #>
defer:
if (ValidPlayer := MaybePlayer?):
PickupMarker.MapIndicator.DeactivateObjectivePulse(ValidPlayer)
PickupMarker.MapIndicator.Disable()
PickupMarker.MapIndicator.Enable()
loop:
var PickupLevel : int = 0
var IsFirstPickup : logic = true
<# Chaque fois que la boucle redémarre, réinitialiser l'interface utilisateur du niveau de collecte via le gestionnaire de score.
Le niveau de collecte dans l'interface utilisateur commence à 1 (et non à 0). S’il commençait à 0, cela risquerait de dérouter certains joueurs.
Étant donné que l’index comment à partir de 0, PickupLevel=0 est le niveau 1 dans l'interface utilisateur. #>
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)
<# C'est la seule expression defer dont nous avons besoin pour toutes les zones de collecte que nous activons. Soit elle désactive la première zone de collecte à la fin de chaque boucle externe,
ou toute PickupZone ultérieure sera désactivée. Cela est dû au fait que l'expression est évaluée à la fin, une fois la variable PickupZone liée à une zone plus récente. #>
defer:
PickupZone.DeactivateZone()
PickupZone.ZoneCompletedEvent.Await()
Logger.Print("Picked up", ?Level:=log_level.Normal)
<# Retirer les pizzas collectées de l'inventaire du joueur pour éviter de les empiler et de les faire tomber au sol une fois la pile pleine. #>
if (RemovingPlayer := MaybePlayer?):
PizzaRemover.Remove(RemovingPlayer)
# Après la première collecte, il est possible d’activer la zone de livraison.
if (IsFirstPickup?):
set IsFirstPickup = false
FirstPickupZoneCompletedEvent.Signal()
if (PickupPoints := PointsForPickupLevel[PickupLevel]):
ScoreManager.UpdatePendingScore(PickupPoints)
# Mettre à jour le niveau de collecte et le gestionnaire de score.
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()
# La désactivation de zone est reportée afin que l'annulation de la boucle PickupDeliveryLoop entraîne également la désactivation de toute zone de livraison active.
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 }
<# Une zone est un secteur de la carte (représenté par un appareil) qui peut être activé/désactivé et qui fournit des événements
pour signaler que la zone a été "terminée" (ne peut plus être terminée jusqu'à la prochaine activation).
La zone "terminée" dépend du type d'appareil (ActivatorDevice) de la zone.
Utilisation suggérée : 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()
<# Active la zone.
Vous devez activer les appareils et les indicateurs visuels de la zone ici. #>
ActivateZone<public>() : void =
# La zone de base peut gérer les zones définies comme générateurs d'objets ou zones de capture.
# Essayez de convertir chaque type pour savoir duquel il s’agit.
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}) }
<# Désactive la zone.
Vous devez désactiver les appareils et tous les indicateurs visuels de la zone ici. #>
DeactivateZone<public>() : void =
if (CaptureArea := capture_area_device[ActivatorDevice]):
CaptureArea.Disable()
else if (ItemSpawner := item_spawner_device[ActivatorDevice]):
ItemSpawner.Disable()
ZoneDeactivatedEvent.Signal()
<# Cet événement est nécessaire pour terminer la coroutine WaitForZoneCompleted si la zone est désactivée sans être terminée. #>
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
# Le sélecteur tagged_zone_selector crée des zones basées sur des déclencheurs portant la balise transmise à InitZones.
tagged_zone_selector<public> := class:
var Zones<protected> : []base_zone = array{}
InitZones<public>(ZoneTag : tag) : void =
<# Lors de la création d'un sélecteur de zone, rechercher toutes les zones disponibles
et les mettre en cache pour ne pas perdre de temps à rechercher les appareils étiquetés
chaque fois que la zone suivante est sélectionnée. #>
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:
<# Étant donné que la zone de dessin ne sera pas recréée pendant la durée de vie du gestionnaire de score, le recréer une fois
chaque fois qu'un objet de ce type est créé. #>
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()
<# Ajoute PendingScore à TotalGameScore et remet PendingScore à 0.
Renvoie le nombre total de points de collecte ajoutés. #>
AddPendingScoreToTotalScore<public>() : int =
set TotalGameScore += PendingScore
defer:
set PendingScore = 0
UpdateUI()
PendingScore
<# Ajoute la quantité de points donnée aux points en attente. #>
UpdatePendingScore<public>(Points : int) : void =
set PendingScore += Points
UpdateUI()
UpdatePickupLevel<public>(Level : int) : void =
set PickupLevel = Level
UpdateUI()
<# Attribue le score au joueur via l’appareil ScoreManagerDevice, en l'activant. #>
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))
À vous de jouer
Au cours de ce tutoriel, vous avez appris à utiliser Verse pour créer l’exemple de jeu complet Pizza Pursuit.
Utilisez vos connaissances pour vous essayer aux défis suivants :
- Ajouter d’autres niveaux de zones de collecte.
- Ajouter différents types de zones de livraison. Étendez la classe
base_zonepour que le joueur soit obligé d’activer un autre appareil, comme un bouton, pour terminer la zone. - Le joueur doit sortir du kart et effectuer une brève course d'obstacles à pied pour livrer la pizza.
- Activer plusieurs zones en même temps.
- Moduler les critères de sélection des zones en fonction de la distance du joueur.