Systèmes de jeu fondamentaux

*Ce contenu est traduit en utilisant l'IA (Beta) et peut contenir des erreurs. Pour consulter cette page en anglais, clique ici.

Les systèmes suivants ont été la base pour établir le gameplay que nous voulions pour Le mystère de Duvall Drive.

Gestionnaire d'état de jeu

Le GameStateManager / GameStateClient est probablement le système le plus compliqué de l'expérience, car il traite de :

  • Démarrer les joueurs dans le lobby, lancer le compte à rebours pour téléporter le groupe vers la zone de jeu principale et téléporter les joueurs sur des serveurs réservés.
  • Clonage de pièces corrompues, streaming en asynchrone et téléportation des joueurs d'une coordonnée spécifique assignée CFrame .
  • Attraper et placer la mécanique des phoques.
  • Verrouiller et déverrouiller des portes.
  • Initialisation de la téléportation finale au foyer et lecture de la scène finale.

Nous l'avons implémenté comme une simple machine d'état (fonction de mise à jour), et les états sont dans DemoConfig (enum GameStates).Certains États traitent du téléportationdu serveur initial/réservé, tandis que d'autres traitent de la recherche d'une mission, du déclenchement du puzzle et de la résolution de la mission.Notez que, à l'exception des phoques, nous avons essayé de ne pas avoir de code spécifique à la mission dans le GameStateManager .

GameStates est principalement côté serveur, mais lorsque le client doit faire quelque chose, comme afficher le compte à rebours, la lore ou désactiver l'interruption de la pause de streaming, le serveur et le client (GameStatesClient) communiquent via un événement distant appelé GameStateEvent .Comme dans la plupart des cas, le paquet d'événement a pour « taper» d'événement (Config.GameEvents) le premier paramètre, et des données spécifiques à l'événement après cela.

États de jeu de téléportation

Il y a un groupe de 3 états de jeu qui exécute trois scènes uniques qui cachent la téléportation vers la salle corrompue : Warmup, InFlight et Cooldown. L'échauffement se déroule pendant toute la durée et se termine par un écran presque noir dans lequel le monde 3D n'est plus visible.Pendant cette période, nous clonons la salle, obtenons les positions souhaitées du joueur dans la salle corrompue pour chaque joueur, appelons Player.RequestStreamAroundAsync , et transportons les joueurs vers une coordonnée spécifique attribuée CFrame dans la salle corrompue.Ce type de téléportation peut déclencher une pause de diffusion en continu.Lorsqu'une pause de lecture se produit, le client affiche un message d'avertissement.Nous avons désactivé cette interface utilisateur par défaut pour garder l'expérience immersif.

Pendant que le streaming est géré, InFlight s'exécute, en maintenant un écran sombre légèrement pulsé.Lorsque les deux Player.RequestStreamAroundAsync retournent et que les clients informent le serveur que la pause de lecture est désactivée et que chaque joueur est suffisamment proche de l'emplacement souhaité, nous annulons la scène de transition en vol et commençons la scène de transition de récupération.L'objectif de Récupération est de rendre le monde 3D visible à nouveau en supprimant doucement l'écran sombre.Si Player.RequestStreamAroundAsync prend trop de temps à renvoyer, ou que le client ne signale pas que la pause de diffusion est off, nous procédons toujours à la scène de récupération après un délai de plusieurs secondes.

Un ensemble similaire de scènes d'échauffement, en vol et de récupération se produit lorsque nous téléportons le joueur de retour à l'état normal de la salle, TeleportWarmupBack , TeleportInFlightBack et TeleportCooldownBack , et à la fin de l'expérience, nous exécutons également TeleportWarmupFinal , TeleportInFlightFinal et TeleportCooldownFinal pour téléporter les joueurs dans le hall pour la scène finale.

États de jeu d'éclairage et d'atmosphère

Nous savions que nous voulions que l'état normal et corrompu de chaque pièce ait une apparence visuelle différente afin qu'il puisse donner aux joueurs un retour visuel clair qu'ils étaient dans une localisation complètement différente.Les états de jeu nous ont permis de modifier les propriétés d'éclairage et d'atmosphère pour les salles normales et corrompues, dans lesquelles le gestionnaire d'état de jeu a sélectionné les instances à utiliser en fonction de si les joueurs se téléportent de l'état normal vers l'état corrompu de la salle ( TeleportWarmup ), ou vice versa ( TeleportWarmupBack ).Les événements qui se jouent pendant le téléport rendent l'ensemble de l'écran soit sombre ou blanc, donc nous avons décidé de modifier les instances Lighting et Atmosphere à ces moments pour cacher le processus aux joueurs.Pour rendre le changement simple, DemoConfig inclut des cartes qui définissent les instances sous ces services qui doivent être modifiées.

Verrouillage des états de jeu des portes

Nous voulions pouvoir garder les joueurs dans certaines pièces pendant qu'ils terminaient des missions, alors nous avons créé des états de jeu pour verrouiller les portes : InMission et CanGetSeal .InMission verrouille les joueurs dans leur salle de mission active, et CanGetSeal garde la porte de la salle de mission verrouillée jusqu'à ce qu'ils prennent le sceau "restauré".Nous avons surtout utilisé ceci pour que les portes se verrouillent lorsque les joueurs reviennent d'une mission afin qu'ils aient un incitant à récupérer le sceau.Après avoir récupéré le sceau, les portes se déverrouillent afin qu'elles puissent le placer à l'emplacement du sceau dans le hall.La dernière mission est unique à ce processus typique, car la porte de la pièce avec son sceau est verrouillée jusqu'à ce que les joueurs résolvent tous les autres puzzles (EnableRegularMissionDoors, EnableOneMissionDoors fonctions).

Responsable d'événement

EventManager nous a permis de lancer des "actions" au fil du temps en utilisant des keyframes, comme :

  • Interpolation des propriétés et des attributs de l'instance.
  • Exécution de scripts.
  • Lecture de l'audio.
  • Secousse de la caméra en cours d'exécution.

Nous utiliserions idéalement un outil avec une interface utilisateur basée sur des pistes, mais pour cette démonstration, nous avons saisi manuellement les clés et les noms des propriétés.Le système Gestionnaire d'événements se compose de plusieurs scripts et d'un événement/fonction, y compris :

  • EventManager - Logique globale pour la création et l'arrêt d'événements, ainsi que des actions côté serveur.
  • EventManagerClient - Actions côté client.
  • EventManagerModule - Code commun pour les actions côté serveur et côté client.
  • EventManagerConfig - Petit fichier avec quelques déclarations de commande.
  • EventManagerDemo - Où tous les événements réels pour cette démonstration sont définis dans un script spécifique au jeu.
  • EventManagerEvent , EventManagerFunc - Événement distant et fonction bindable pour exécuter et arrêter les événements du client ou du serveur.C'est ainsi que d'autres systèmes peuvent configurer, lanceret arrêter des événements.

Chaque événement a un nom, une section avec des informations facultatives sur le temps de recharge, une fonction à exécuter au démarrage ou à la terminer, des paramètres d'événement et des sections avec des interpolants (interpolation de n'importe quel nombre de propriétés ou d'attributs au fil du temps), des scripts (exécution d' scripts enregistrés à des points clés), des secousses de caméra et lecture d'audio.

Interpolisation

L'interpolation permet aux propriétés et aux attributs des objets de changer sans problème d'une valeur à l'autre au lieu de sauter séparément entre les cadres clés.Nous avons défini des interpolants pour modifier une variété d'effets visuels ; par exemple, le fragment de code suivant montre comment nous avons interpolé la propriété sur un objet défini par le paramètre à partir d'une valeur de au début jusqu'à plus tard :


interpolants = {
objectParam = "TextLabel",
property = "TextTransparency",
keys = {
{value = 1},
{time = .5, value = 0},
{time = 2.25, value = 0},
{time = 3, value = 1}
}
}

Bien que nous puissions définir quelle propriété ou attribut d'objet appartient à ce que comme dans l'exemple de code suivant, nous voulions pouvoir réutiliser les mêmes événements sur différents « groupes d'objets » pour qu'ils fonctionnent avec le streaming sur le client, et avec des objets créés au moment de l'exécution.


object = workspace.SomeFolder.SomeModel

Pour y parvenir, nous avons permis la référence par le nom de l'objet et le passage de paramètres nommés au commencerde l'événement.Pour trouver des objets nommés, nous avons permis de spécifier une « racine » pour l'événement, qui permettait aux objets d'être trouvés par leur nom sous cette racine lorsque l'événement a commencé.Par exemple, dans le fragment de code suivant, le Gestionnaire d'événements essaie de trouver un objet nommé "Wander" quelque part sous Workspace.Content.Interior.Foyer["Ritual-DemoVersion"] .


params = {
["RootObject"] = workspace.Content.Interior.Foyer["Ritual-DemoVersion"],
},
interpolants = {
objectName = "Wander",
attribute = "TimeScale",
keys = {
{value = 0.2}
}
}

Nous avons permis de passer des paramètres dans les événements dans la section params, et les scripts s'exécutant au début de l'événement pouvaient soit modifier les paramètres existants, soit ajouter plus de paramètres dans la table « param ».Dans l'exemple suivant, nous avons le paramètre isEnabled avec une valeur par défaut de false, et la propriété "Enabled" sur un objet avec le nom FocuserGlow sera définie sur la valeur de isEnabled .Un script s'exécutant au démarrage de l'événement ou un script invoquant l'événement peut définir isEnabled, de sorte que nous pourrions utiliser la même description d'événement pour activer et désactiver FocuserGlow.


params = {
isEnabled = false
},
interpolants = {
{
objectName = "FocuserGlow",
property = "Enabled",
keys = {
{valueParam = "isEnabled"}
}
}

Les paramètres nous ont permis de nous référer à des objets qui n'existent même pas au début de l'expérience.Par exemple, dans l'exemple de code suivant, une fonction qui s'exécute au début de l'événement créera un objet et définira l'entrée BlackScreenObject dans les paramètres pour pointer vers l'objet créé.


{objectParam = "BlackScreenObject",
property = "BackgroundTransparency",
keys = {
{value = 0},
{time = 19, value = 0},
{value = 1},
}}

Exécutez des événements, des instances d'événement et connectez-vous à des déclencheurs

Pour exécuter un événement, nous utiliserions soit un événement distant des clients, soit une fonction du serveur.Dans l'exemple suivant, nous avons passé quelques paramètres aux événements RootObject et isEnabled.Interne, une instance de la description de l'événement a été créée, les paramètres résolus en objets réels, et la fonction a retourné un ID pour l'instance d'événement.


local params = {
RootObject = workspace.Content.Interior.Foyer["Ritual-DemoVersion"]["SealDropoff_" .. missionName],
isEnabled = enabled
}
local eventId = eventManagerFunc:Invoke("Run", {eventName = "Ritual_Init_Dropoff", eventParams = params} )

Nous pourrions arrêter d'exécuter un événement en appelant la fonction avec "Stop" :


eventManagerFunc:Invoke("Stop", {eventInstId = cooldownId} )

Les interpolants ou d'autres actions qui sont "cosmétiques" (ne pas modifier la simulation pour tous les joueurs) pourraient être exécutés sur les clients, ce qui pourrait entraîner une interpolation plus lisse.Dans la description de l'événement, nous pouvons fournir une valeur par défaut pour toutes les actions comme onServer = true (sans elle, la valeur par défaut est le client).Chaque action peut l'écraser en définissant son propre onServer.

Pour relier facilement l'exécution d'un événement à un déclencheur, nous avons utilisé les fonctions d'aide ConnectTriggerToEvent ou ConnectSpawnedTriggerToEvent, la dernière desquelles trouve le déclencheur par nom.Pour permettre que le même événement soit déclenché en utilisant différents déclencheurs, nous pourrions appeler eventManagerFunc avec une clé « Setup » et un ensemble de volumes déclencheurs.Pour un exemple de volume déclencheur en action, voir Faire évoluer la réserve.

Paramètres d'événement

En plus des paramètres d'événement personnalisés transmis par les scripts, d'autres données qui peuvent être optionnellement transmises lors de la création d'un événement incluent le joueur, le rappel (à appeler lorsque l'événement se termine) et les paramètres de rappel.Certains événements doivent se produire pour un seul joueur (événements avec des actions s'exécutant sur le client), tandis que d'autres doivent se produire pour tout.Pour qu'il fonctionne pour un seul joueur, nous avons utilisé onlyTriggeredPlayer = true dans les paramètres.

Les événements peuvent avoir des temps de recharge définis par minCooldownTime et maxCooldownTime.Le min et le max fournissent une plage pour l'échelle basée sur le nombre de joueurs, mais nous ne l'avons pas utilisée dans cette démonstration.Si nous avions eu besoin de temps de recharge par joueur, nous avions la capacité d'utiliser perPlayerCooldown = true .Chaque événement a une durée en secondes, et les délais de recharge et les rappels se basent dessus.Pour informer de la fin d'un événement, l'invocation du code pourrait passer un rappel et des paramètres qu'il obtiendra.

Appeler les scripts

Nous pourrions appeler Scripts dans des keyframes spécifiques dans la section Scripts . Par exemple :


scripts = {
{startTime = 2, scriptName = "EnablePlayerControls", params = {true}, onServer = false }
}

Dans l'exemple précédent, le EnablePlayerControls Script devrait être enregistré avec le module gestionnaire d'événements, comme ceci :


emModule.RegisterFunction("EnablePlayerControls", EnablePlayerControls)

La fonction RegisterFunction doit être appelée dans le script du client pour les fonctions appelées sur le client, et dans le script du serveur pour onServer = true.La fonction elle-même recevra l'instance d'événement et les paramètres transmis, mais dans ce cas, un seul paramètre est transmis avec une valeur réelle.


local function EnablePlayerControls(eventInst, params)

Jouer de l'audio

Nous avons un support limité pour jouer non audio positionnel à des points clés dans la section Sons , par exemple :


sounds = {
{startTime = 2, name = "VisTech_ethereal_voices-001"},
}

Notez que les appels de rappel de fin d'événement se déclenchent lorsque la durée de l'événement expire, mais les actions audio peuvent encore jouer ensuite.

Exécuter des secousses de caméra

Nous pourrions définir les secousses de caméra dans la section caméraShakes , comme ceci :


cameraShakes = {
{startTime = 15, shake = "small", sustainDuration = 7, targets = emConfig.ShakeTargets.allPlayers, onServer = true},
}

Les « cibles » ne peuvent être initiées que pour le joueur qui a déclenché l'événement, allPlayer, ou les joueursInRadius au joueur déclencheur.Nous avons utilisé un script tiers pour les secousses de la caméra, et les secousses étaient prédéfinies : eventManagerDemo.bigShake et eventManagerDemo.smallShake .sustainDuration pourrait également être passé.

Logique des missions

Il y a 7 missions au total, et seules 6 d'entre elles utilisent des phoques.La plupart des missions ont des paramètres communs, bien que certaines ne soient que pour les missions avec des phoques et la téléportation vers des salles corrompues.Chaque mission a une entrée dans le script DemoConfig avec un ensemble de paramètres dans la carte Config.Missions :

  • MissionRoot : Un dossier de toutes les versions non corrompues d'objets.
  • Portes : Portes à verrouiller jusqu'à ce qu'un joueur prenne un sceau.
  • Nom de la baleine / Nom de la baleine résolu : baleine non corrompue et baleines corrompues nom.
  • SealPlaceName : Lieux où mettre le sceau.
  • PlacedSealPlaceholderName : objet de remplacement à l'endroit où mettre le sceau.
  • Nom du dossier avec des mailles de remplacement pour définir les positions et les rotations de téléportation du joueur lors du déplacement vers la salle corrompue, et de retour dans la zone normale. Le même nom est utilisé dans les deux cas.
  • CorruptRoomName : Noms des dossiers racine (par rapport au stockage du serveur) pour les salles corrompues.Les salles corrompues se clonent sous TempStorage.Cloned lorsque la mission commence, et elles sont détruites lorsque la mission est terminée.
  • MissionCompleteButtonName : Un bouton de tricherie dans les salles corrompues pour terminer la mission immédiatement. C'est pour des fins de débogage.
  • Clé d'arnaque : La même arnaque qu'un nombre ou CtrlShift[Number] .

Une partie de la logique de la mission se trouve dans les GameStateManager scripts, car les phoques et les portes fournissent le principal flux de jeu pour la plupart des missions, mais la plupart de la logique spécifique à la mission se trouve dans les MissionsLogic et MissionsLogicClient scripts qui définissent plusieurs « types » de missions.Le type est défini simplement par la présence de membres nommés spécifiquement dans la description de la mission.Il existe quelques types de missions :

  • Utiliser une clé sur une serrure - La première mission pour ouvrir une porte. Ce type est défini par LockName, KeyName.
  • Correspondre aux articles - 4 missions correspondent aux articles. Ce type est défini par MatchItems .
  • Habiller un mannequin en utilisant un tissu à plusieurs couches - 1 mission dans le grenier a des joueurs qui collectent trois articles. Ce type est défini par DressItemsTagList .
  • Cliquez sur l'élément pour terminer - 1 mission a ce taper, qui est défini par ClickTargetName .

Chaque type de mission a son propre StartMissionFunc et CompleteMissionFunc.La fonction de départ lit généralement les paramètres de la carte MatchItem , résout les noms en objets et configure tout détecteur de clics ou élément d'interface utilisateur.Presque toute la logique est sur un serveur, mais MissionsLogicClient fournit une interface pour afficher le compteur d'objets, utilisée dans de nombreuses missions.MissionLogicEvent l'événement distant est utilisé pour les communications serveur-client, avec un petit type de commandes défini par MissionEvents.Le script MiscGameLogic lie certains déclencheurs à des événements et supprime les objets de débogage dans la version de sortie.

La logique de correspondance des éléments permet d'« utiliser » (cliquer en maintenant) des éléments marqués de PuzzlePieceXX tags sur des éléments avec PuzzleSlotYY tag.Il y a quelques options disponibles en tant que paramètres dans la carte MatchItems (si les pièces doivent être appliquées dans l'ordre, si seule une de chaque est requise).Nous pourrions spécifier des noms pour des effets audio et visuels simples.Lorsque des pièces doivent être placées à des endroits spécifiques, une carte « Placement » supplémentaire fournit une mise en relation des balises de pièces aux noms des parties de remplacement qui définissent des transformations.

Attrape

Nous avons développé un système simple de prise pour maintenir un objet en attachant l'objet au bras droit du personnage.La saisie est implémentée dans GrabServer2 et GrabClient scripts.Elle commence dans ProcessClick , qui lance un rayon à travers le point cliqué/touché.Il vérifie ensuite si nous atteignons un maillage qui peut être saisi, et le coup se situe dans le maxMovingDist où nous pouvons commencer à saisir l'interaction.Si le modèle cliqué a Attachments appelé GrabHint , nous choisissons le plus proche du point cliqué.Nous nous souvenons de la partie saisie, du modèle auquel elle appartient, et soit de la position la plus proche GrabHint ou de la position cliquée dans la structure grabAttempt.Si la distance est supérieure à maxGrabDist, le joueur doit d'abord se déplacer suffisamment près du point de capture tenté, donc nous appelons Humanoid.MoveTo .

Sur chaque cadre, nous vérifions si une tentative de saisie est en cours.Si le joueur se trouve dans reachDist , nous commençons à jouer ToolHoldAnim .Lorsqu'un joueur se trouve dans maxGrabDist , le client envoie une demande au serveur pour réellement saisir un modèle (performGrab).

Le script côté serveur a 2 principales fonctions :

  • Prendre - Gère la demande d'un client de saisir un modèlisation.
  • Libération - Gère la demande de libération d'un modèlisationsaisi.

Les informations sur ce que chaque joueur détient sont conservées dans la carte playerInfos .Dans la fonction de saisie, nous vérifions si ce modèle est déjà saisi par un autre joueur.Si oui - un "EquipWorldFail" est envoyé au client et il annule la tentative de saisie.Notez que nous devions gérer des situations où les joueurs saisissent différentes parties de la même Model, et annuler la saisie dans ce cas.

Si la prise est autorisée, le script crée deux Attachments , l'un à la main droite et l'autre sur l'objet en utilisant un point de prise passé du client.Il crée ensuite un RigidConstraint entre les deux Attachments .Constraints et Attachments sont stockés sous le dossier CurrentGrips sous le personnage du joueur.La saisie joue également un son, désactive les collisions sur l'objet saisi et gère les restaurables, si nécessaire.

Pour libérer un modèlisationsaisi, le script client se connecte au bouton Libérer le bouton dans HUD ScreenGui.Une fonction A Connected lance un événement sur le serveur.Sur le serveur, la libération supprime le Attachments et Constraints , restaure la collision, traite les Restorables applicables, et efface les données d'attrape pour ce client dans playerInfos .