Sistemi di gioco di base

*Questo contenuto è tradotto usando AI (Beta) e potrebbe contenere errori. Per visualizzare questa pagina in inglese, clicca qui.

I seguenti sistemi sono stati la base per stabilire il gameplay che volevamo per Il mistero di Duvall Drive.

Gestore di stato di gioco

Il GameStateManager / GameStateClient è probabilmente il sistema più complicato nell'esperienza, poiché tratta con:

  • Avvio dei giocatori all'interno della lobby, avvio del conto alla rovescia per il teletrasporto del gruppo nell'area di gioco principale e teletrasporto dei giocatori su server riservati.
  • Clonare stanze corrotte, streaming asincronico e teletrasportare i giocatori da e verso una coordinata specifica assegnata CFrame.
  • Afferrare e posizionare le meccaniche delle sigillature.
  • Blocco e sblocco delle porte.
  • Inizializzazione del teletrasporto finale al foyer e riproduzione della scena finale.

L'abbiamo implementata come semplice macchina di stato (funzione Aggiorna), e gli stati sono in DemoConfig (enum GameStates).Alcuni stati gestiscono il Teletrasportoiniziale della lobby/server riservato, mentre altri si occupano di trovare una missione, attivare il puzzle e risolvere la missione.Nota che, a parte le foche, abbiamo cercato di non avere codice specifico per la missione nel GameStateManager .

GameStates è per lo più lato server, ma quando il client deve fare qualcosa, come mostrare il conto alla rovescia, la storia o disabilitare l'interruzione della pausa di streaming, server e client (GameStatesClient) comunicano tramite un evento remoto chiamato GameStateEvent .Come nella maggior parte dei casi, il payload dell'evento ha "inserisci / scrivi" evento (Config.GameEvents) come primo parametro e dati specifici dell'evento dopo.

Stati di gioco di teletrasporto

C'è un gruppo di 3 stati di gioco che esegue tre cutscene uniche che nascondono la teleportazione nella stanza corrotta: Warmup, InFlight e Cooldown. Riscaldamento funziona per l'intera durata e termina con uno schermo quasi nero in cui il mondo 3D non è più visibile.Durante questo periodo, cloniamo la stanza, otteniamo le posizioni desiderate del giocatore nella stanza corrotta per ogni Giocatore, chiamiamo Player.RequestStreamAroundAsync , e trasportiamo i giocatori in una posizione specifica assegnata CFrame all'interno della stanza corrotta.Questo tipo di teletrasporto potrebbe attivare una pausa di streaming.Quando si verifica una pausa di streaming, il client visualizza un Messaggiodi avvertimento.Abbiamo disabilitato questa interfaccia utente predefinita per mantenere l'esperienza immersiva.

Mentre lo streaming viene gestito, InFlight esegue, mantenendo uno schermo oscuro pulsante leggermente.Quando entrambi Player.RequestStreamAroundAsync ritorna e i client informano il server che la pausa di streaming è disattivata e ogni giocatore è abbastanza vicino alla posizione desiderata, annulliamo la scena di InFlight e iniziamo la scena di Cooldown.L'obiettivo di Tempo di recupero è rendere nuovamente visibile il mondo 3D rimuovendo lentamente lo schermo oscuro.Se Player.RequestStreamAroundAsync richiede troppo tempo per essere Riportareo il client non segnala che la pausa di streaming è Off, procediamo comunque alla scena di recupero dopo un timeout di diversi secondi.

Un set simile di scene di riscaldamento, in volo e di recupero si verifica quando teletrasportiamo il giocatore al normale stato della stanza, TeleportWarmupBack , TeleportInFlightBack e TeleportCooldownBack , e alla fine dell'esperienza, eseguiamo anche TeleportWarmupFinal , TeleportInFlightFinal e TeleportCooldownFinal per teletrasportare i giocatori nel foyer per la scena finale.

Stati di gioco di illuminazione e atmosfera

Sapevamo che volevamo che lo stato normale e corrotto di ogni stanza avesse un aspetto visivo diverso in modo che potesse dare ai giocatori un feedback visivo chiaro che fossero in una posizione completamente diversa.Gli stati di gioco ci hanno permesso di modificare le proprietà di illuminazione e atmosfera per stanze normali e corrotte, in cui il GameStateManager ha selezionato quali istanze utilizzare in base se i giocatori si stanno teletrasportando dallo stato normale allo stato corrotto della stanza ( TeleportWarmup ), o viceversa ( TeleportWarmupBack ).Gli eventi in riproduzione durante il teletrasporto rendono l'intera schermata sia scura che bianca, quindi abbiamo deciso di cambiare le istanze Lighting e Atmosphere in quei momenti per nascondere il processo ai giocatori.Per renderlo semplice da modificare, DemoConfig include mappe che definiscono quali istanze sotto questi servizi devono essere modificate.

Bloccare gli stati di gioco delle porte

Volevamo essere in grado di mantenere i giocatori in determinate stanze mentre finivano le missioni, quindi abbiamo creato stati di gioco per bloccare le porte: InMission e CanGetSeal .InMission blocca i giocatori nella loro sala missione attiva, e CanGetSeal mantiene la porta della sala missione chiusa fino a quando non prenderanno il sigillo "restaurato".Abbiamo usato principalmente questo per far sì che le porte si bloccassero quando i giocatori tornassero da una missione in modo che abbiano un incentivo a raccogliere il sigillo.Dopo aver raccolto il sigillo, le porte si sbloccano così possono posizionarlo all'interno della posizione del sigillo nel foyer.L'ultima missione è unica a questo processo tipico, poiché la porta della stanza con il suo sigillo è bloccata fino a quando i giocatori non risolvono ogni altro puzzle (EnableRegularMissionDoors, EnableOneMissionDoors funzioni).

Gestore eventi

EventManager ci ha permesso di eseguire "azioni" nel tempo utilizzando keyframe, come:

  • Interpolazione delle proprietà e degli attributi dell'istanza.
  • Esecuzione di script.
  • Riproduzione dell'audio/suono.
  • Scuotimento della fotocamera in esecuzione.

Utilizziamo idealmente uno strumento con interfaccia utente basata su tracce, ma per questa demo, abbiamo digitato manualmente le chiavi e i nomi delle proprietà.Il sistema EventManager consiste in diversi script e un evento/funzione, tra cui:

  • EventManager - Logica generale per la creazione e l'interruzione di eventi, nonché le azioni lato server.
  • EventManagerClient - Azioni lato client.
  • EventManagerModule - Codice comune per azioni sia lato server che lato client.
  • EventManagerConfig - File piccolo con alcune dichiarazioni di comando.
  • EventManagerDemo - Dove tutti gli eventi reali per questa demo sono definiti in uno script specifico per il gioco.
  • EventManagerEvent , EventManagerFunc - Evento remoto e funzione bindabile per eseguire e interrompere gli eventi dal client o dal Server.Questo è il modo in cui altri sistemi possono configurare, Eseguiree interrompere gli eventi.

Ogni evento ha un nome, una sezione con informazioni opzionali sul cooldown, la funzione di esecuzione all'avvio o alla Terminare, i parametri degli eventi, e sezioni con interpolanti (interpolare qualsiasi numero di proprietà o attributi nel tempo), script (esecuzione di script registrati ai keyframe), scuoti della fotocamera e riproduzione di audio/suono.

Interpolazione

L'interpolazione consente alle proprietà e agli attributi dell'oggetto di cambiare senza soluzione di continuità da un valore all'altro invece di saltare separatamente tra i keyframe.Abbiamo definito gli interpolanti per modificare una varietà di effetti visivi; ad esempio, il seguente snippet di codice mostra come abbiamo interpolato la proprietà TextLabel.TextTransparency su un oggetto definito da TextLabel parametro da un valore di 1 all'inizio a 0 , quindi più tardi di nuovo a 1 :


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

Mentre potremmo definire quale proprietà o attributo dell'oggetto appartenga a ciò che in seguito al seguente esempio di codice, volevamo essere in grado di riutilizzare gli stessi eventi su diversi "gruppi di oggetti" per consentirgli di funzionare con lo streaming sul client e con gli oggetti creati al momento dell'esecuzione.


object = workspace.SomeFolder.SomeModel

Per raggiungere questo obiettivo, abbiamo consentito il riferimento al nome dell'oggetto e il passaggio di parametri nominati all'Iniziare, cominciaredell'evento.Per trovare oggetti nominati, abbiamo consentito di specificare un "Radice" per l'evento, che ha permesso agli oggetti di essere trovati per nome sotto questo root quando l'evento è iniziato.Ad esempio, nel seguente snippet di codice, il EventManager cerca di trovare un oggetto chiamato "Wander" da qualche parte sotto Workspace.Content.Interior.Foyer["Ritual-DemoVersion"] .


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

Abbiamo consentito il passaggio di parametri negli eventi nella sezione params e gli script in esecuzione all'avvio dell'evento potrebbero cambiare i parametri esistenti o aggiungere più parametri nella tabella "param".Nell'esempio seguente, abbiamo il parametro isEnabled con un valore predefinito di false e la proprietà "Enabled" su un oggetto con il nome FocuserGlow verrà impostata al valore di isEnabled .Un script in esecuzione all'avvio dell'evento o uno script che invoca l'evento può impostare isEnabled, quindi potremmo utilizzare la stessa descrizione dell'evento per abilitare e disabilitare FocuserGlow.


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

I parametri ci hanno permesso di fare riferimento a oggetti che non esistono nemmeno all'inizio dell'esperienza.Ad esempio, nel seguente esempio di codice una funzione in esecuzione all'avvio dell'evento creerà un oggetto e imposterà l'entrata BlackScreenObject nei parametri per puntare all'oggetto creato.


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

Esegui eventi, istanze di eventi e connetti a trigger

Per eseguire un evento, useremmo un evento remoto dai client o una funzione dal Server.Nell'esempio seguente, abbiamo passato un paio di parametri agli eventi RootObject e isEnabled.A livello interno, è stata creata un'istanza della descrizione dell'evento, i parametri sono stati risolti in oggetti reali e la funzione ha restituito un ID per l'istanza dell'evento.


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

Potremmo smettere di eseguire un evento chiamando la funzione con "Stop":


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

Interpolanti o altre azioni che sono "cosmetiche" (non cambiare la simulazione per tutti i giocatori) potrebbero essere eseguiti su client, il che potrebbe comportare una interpolazione (di stringhe)più liscia.Nella Descrizionedell'evento, potremmo fornire un valore predefinito per tutte le azioni come onServer = true (senza di esso, il predefinito è il client).Ogni azione può sovrascriverlo impostando il proprio onServer.

Per connettersi facilmente all'esecuzione di un evento a un grilletto, abbiamo usato le funzioni ausiliarie ConnectTriggerToEvent o ConnectSpawnedTriggerToEvent, quest'ultima delle quali trova il trigger per nome.Per consentire allo stesso evento di essere attivato utilizzando diversi trigger, potremmo chiamare eventManagerFunc con una chiave "Impostazione" e un set di volumi di trigger.Per un esempio di volume di trigger in azione, vedi Espandere il magazzino.

Parametri dell'evento

Oltre ai parametri evento personalizzati passati dagli script, altri dati che possono essere opzionalmente passati durante la creazione di un evento includono il Giocatore, il richiamo (da chiamare quando termina l'evento) e i parametri del richiamo.Alcuni eventi dovrebbero funzionare solo per un giocatore (eventi con azioni in esecuzione sul client), mentre altri dovrebbero funzionare per tutti/tutte.Per farlo funzionare solo per un Giocatore, abbiamo usato onlyTriggeredPlayer = true nei parametri.

Gli eventi possono avere tempi di recupero definiti da minCooldownTime e maxCooldownTime .Il minimo e il massimo forniscono un intervallo per la scala in base al numero di giocatori, ma non l'abbiamo utilizzato in questa demo.Se avessimo avuto bisogno di tempi di recupero per ogni Giocatore, avevamo la capacità di utilizzare perPlayerCooldown = true .Ogni evento ha una durata in secondi e i tempi di recupero e le richiamate si basano su di esso.Per informare sull'evento di finitura, l'invocazione del codice potrebbe passare un callback e i parametri che otterrà.

Chiama gli script

Potremmo chiamare Scripts in specifici keyframe nella sezione Script . Ad esempio:


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

Nell'esempio precedente, il EnablePlayerControls Script dovrebbe essere registrato con il modulo gestore eventi, come segue:


emModule.RegisterFunction("EnablePlayerControls", EnablePlayerControls)

La funzione RegisterFunction deve essere chiamata nell' script del client per le funzioni richieste sul client e nell' script del server per onServer = true .La funzione stessa riceverà eventInstance e parametri passati, ma in questo caso, solo un parametro viene passato con un valore vero.


local function EnablePlayerControls(eventInst, params)

Riproduci audio/suono

Abbiamo un supporto limitato per riprodurre audio non posizionale ai fotogrammi nella sezione Suoni , ad esempio:


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

Nota che le chiamate di completamento dell'evento si attivano quando scade la durata dell'evento, ma le azioni audio potrebbero ancora essere in riproduzione dopo.

Esegui scuoti della fotocamera

Potremmo definire gli scuotimenti della fotocamera nella sezione scuotimenti della fotocamera , come questo:


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

I "target" possono essere inizializzati solo per il giocatore che ha attivato l'evento, allPlayer, o giocatoriInRadius al Giocatoreattivante.Abbiamo utilizzato uno script di terze parti per le scosse della fotocamera, e le scosse sono state predefinite: eventManagerDemo.bigShake e eventManagerDemo.smallShake .sustainDuration potrebbe anche essere passato.

Logica delle missioni

Ci sono 7 missioni in totale, e solo 6 di esse usano le foche.La maggior parte delle missioni ha parametri comuni, anche se alcune sono solo per le missioni con foche e teletrasporto in stanze corrotte.Ogni missione ha un'entrata nello script DemoConfig con un insieme di parametri nella mappa Config.Missions :

  • MissionRoot : Un cartella di tutte le versioni non corrotte degli oggetti.
  • Porte : Porte per bloccare fino a quando un giocatore non raccoglie un sigillo.
  • Nome del sigillo / Nome del sigillo risolto : Nome del sigillo non corrotto e nomi del sigillo corrotti.
  • Nome luogo del sigillo : Luoghi in cui mettere il sigillo.
  • Nome del luogo di posizionamento del sigillo : oggetto di posizionamento al posto per mettere il sigillo.
  • Nome della posizione di teletrasporto : Nome di una cartella con mesh di riempimento per definire le posizioni e le rotazioni di teletrasporto del giocatore quando si passa alla stanza corrotta e torna nell'area normale.Lo stesso nome viene utilizzato in entrambi i casi.
  • Nome della stanza corrotta CorruptRoomName : Nome dei cartelle radice (rispetto a ServerStorage) per le stanze corrotte.Le stanze corrotte si clonano sotto TempStorage.Cloned quando inizia la missione e vengono distrutte quando la missione termina.
  • MissionCompleteButtonName : Un pulsante di scambio nelle stanze corrotte per terminare immediatamente la missione. Questo è per scopi di debugging .
  • CheatKey : Lo stesso trucco come un numero o CtrlShift[Number] .

Parte della logica della missione è negli script GameStateManager , poiché le foche e le porte forniscono il flusso di gioco principale per la maggior parte delle missioni, ma la maggior parte della logica specifica per la missione è negli script MissionsLogic e MissionsLogicClient che definiscono diversi "tipi" di missioni.Il tipo è definito solo dalla presenza di membri specificamente nominati nella Descrizionedella missione.Ci sono alcuni tipi di missioni:

  • Usa una chiave su una serratura - La prima missione per aprire una porta. Questo tipo è definito da LockName, KeyName .
  • Elementi di corrispondenza - 4 elementi di corrispondenza delle missioni. Questo tipo è definito da MatchItems .
  • Vestire un manichino usando tessuto a strati - 1 missione in soffitta ha giocatori che raccolgono tre oggetti. Questo tipo è definito da DressItemsTagList .
  • Fai clic sull'elemento per terminare - 1 missione ha questo inserisci / scrivi, che è definito da ClickTargetName .

Ogni tipo di missione ha il proprio StartMissionFunc e CompleteMissionFunc.La funzione di avvio di solito legge i parametri dalla mappa MatchItem , risolve i nomi in oggetti e configura qualsiasi rilevatore di clic o elementi dell'interfaccia utente.Quasi tutta la logica è su un Server, ma MissionsLogicClient fornisce un'interfaccia utente per mostrare il contatore degli elementi, utilizzato in molte missioni. MissionLogicEvent l'evento remoto viene utilizzato per le comunicazioni server-client, con un piccolo tipo di comandi definiti MissionEvents.Lo script MiscGameLogic lega alcuni trigger agli eventi e rimuove gli oggetti di debug nella versione di rilascio.

La logica di abbinamento degli elementi consente di "utilizzare" (clicca mentre tieni premuto) gli elementi contrassegnati con PuzzlePieceXX tag su elementi con PuzzleSlotYY tag.Ci sono alcune opzioni disponibili come parametri nella mappa MatchItems (se le parti devono essere applicate in ordine, se ne è richiesta solo una di ciascuna).Potremmo specificare nomi per semplici audio e FX visivi.Quando le parti devono essere posizionate in luoghi specifici, una mappa "Posizionamento" extra fornisce la mappatura delle etichette delle parti di riempimento alle parti di destinazione che definiscono le trasformazioni.

Afferrare

Abbiamo sviluppato un semplice sistema di presa per mantenere un oggetto attaccandolo al braccio destro del personaggio.L'acquisizione è implementata in GrabServer2 e GrabClient script.Inizia in ProcessClick , che lancia un raggio attraverso il punto cliccato/toccato.Verifica quindi se colpiamo una mesh che può essere afferrata e il colpo è all'interno del maxMovingDist dove possiamo iniziare a afferrare l'interazione.Se il modello cliccato ha Attachments chiamato GrabHint , scegliamo il più vicino al punto cliccato.Ricordiamo la parte afferrata, il modello a cui appartiene e o la posizione cliccata nella struttura GrabHint .Se la distanza è superiore a maxGrabDist, il giocatore deve prima camminare abbastanza vicino al punto di presa tentato, quindi chiamiamo Humanoid.MoveTo .

Su ogni frame, controlliamo se un tentativo di presa è in corso.Se il giocatore è all'interno di reachDist , iniziamo a giocare ToolHoldAnim .Quando un giocatore è all'interno di maxGrabDist , il client lancia una richiesta al server per effettivamente afferrare un modello (funzione performGrab).

Lo script lato server ha 2 funzioni principali:

  • Afferra - Gestisce la richiesta di un client di afferrare un modello.
  • Rilascio - Gestisce la richiesta di rilascio di un modello catturato.

Le informazioni su ciò che ogni giocatore detiene sono mantenute nella mappa playerInfos .Nella funzione di presa, controlliamo se questo modello è già stato catturato da un altro Giocatore.Se è così - un "EquipWorldFail" viene inviato al client e annulla il tentativo/tentativadi cattura.Nota che avevamo bisogno di gestire situazioni in cui i giocatori afferrano diverse parti dello stesso Model , e annullare l'afferraggio in questo caso.

Se la cattura è consentita, lo script crea due Attachments , uno a destra e un altro sull'oggetto utilizzando un punto di cattura passato dal client.Crea quindi un RigidConstraint tra i due Attachments . Constraints e Attachments sono memorizzati sotto la cartella Grip attuali sotto il personaggio del Giocatore.Afferrare riproduce anche un suono, disabilita le collisioni sull'oggetto afferrato e si occupa di Restorables, se necessario.

Per rilasciare un modello afferrato, lo script del client si connette al pulsante GrabReleaseButton in HUD ScreenGui.Una funzione Connected lancia un evento al Server.On the Server, release elimina il Attachments e Constraints , ripristina la collisione, tratta qualsiasi Restorables applicabile, e ripulisce i dati di acquisizione per questo client in playerInfos .