Los siguientes sistemas fueron la base para establecer el juego que queríamos para El misterio de Duvall Drive.
Gestor de estado del juego
El GameStateManager / GameStateClient es probablemente el sistema más complicado de la experiencia, ya que trata con:
- Iniciar jugadores dentro del vestíbulo, iniciar la cuenta regresiva para teletransportar el grupo a la zona de juego principal y teletransportar jugadores a servidores reservados.
- Clonar habitaciones corruptas, transmitirlas de forma asíncrona y teletransportar a los jugadores a y desde una coordenada específica asignada CFrame.
- Agarrar y colocar mecánicas de sellos.
- Bloquear y desbloquear puertas.
- Inicializando el teletransporte final al vestíbulo y reproduciendo la escena final.
Lo implementamos como una máquina de estado simple (función de actualización), y los estados están en DemoConfig ( enumeración de estados de juego).Algunos estados se ocupan del teletransportacióninicial del lobby/servidor reservado, mientras que otros se ocupan de encontrar una misión, activar el rompecabezas y resolver la misión.Tenga en cuenta que, además de las focas, tratamos de no tener código específico de la misión en el GameStateManager .
GameStates es principalmente del lado del servidor, pero cuando el cliente necesita hacer algo, como mostrar la cuenta regresiva, la sabiduría o deshabilitar la interfaz de pausa de transmisión, el servidor y el cliente (GameStatesClient) se comunican a través de un evento remoto llamado GameStateEvent .Como en la mayoría de los casos, el pago del evento tiene el "introducir" de evento (Config.GameEvents) como primer parámetro, y datos específicos del evento después de eso.
Estados del juego de teletransportación
Hay un grupo de 3 estados de juego que ejecutan tres escenas únicas que ocultan la teletransportación a la habitación corrupta: Warmup, InFlight y Cooldown. Calentamiento se ejecuta durante toda la duración y termina con una pantalla casi negra en la que el mundo 3D ya no es visible.Durante este tiempo, clonamos la sala, obtenemos las posiciones deseadas del jugador en la sala corrupta para cada jugador, llamamos Player.RequestStreamAroundAsync , y transportamos a los jugadores a una coordenada específica asignada CFrame dentro de la sala corrupta.Este tipo de teletransportación podría desencadenar una pausa de transmisión.Cuando ocurre una pausa de transmisión, el cliente muestra un mensaje de advertencia.Desactivamos esta interfaz de usuario predeterminada para mantener la experiencia inmersivo.
Mientras se maneja el streaming, InFlight se ejecuta, manteniendo una pantalla oscura pulsante ligeramente.Cuando ambos Player.RequestStreamAroundAsync regresan y los clientes informan al servidor que la pausa de transmisión está desactivada y cada jugador está lo suficientemente cerca de la ubicación deseada, cancelamos la escena de transición en vuelo y comenzamos la escena de espera de tiempo.El objetivo de Enfriamiento es hacer el mundo 3D visible nuevamente al eliminar suavemente la pantalla oscura.Si Player.RequestStreamAroundAsync tarda demasiado en devolverse, o el cliente no informa de que la pausa de transmisión está desactivado, aún procedemos a la escena de enfriamiento después de un tiempo de espera de varios segundos.
Un conjunto similar de escenas de calentamiento, en vuelo y de enfriamiento ocurre cuando teletransportamos al jugador de vuelta al estado normal de la habitación, TeleportWarmupBack , TeleportInFlightBack y TeleportCooldownBack , y al final de la experiencia, también ejecutamos TeleportWarmupFinal , TeleportInFlightFinal y TeleportCooldownFinal para teletransportar a los jugadores al vestíbulo para la escena final.
Estados de juego de iluminación y atmósfera
Sabíamos que queríamos que el estado normal y corrupto de cada habitación tuviera una apariencia visual diferente para que pudiera dar a los jugadores una retroalimentación visual clara de que estaban en una ubicación completamente diferente.Los estados del juego nos permitieron cambiar las propiedades de iluminación y atmósfera para habitaciones normales y corruptas, en las que el administrador de estado de juego seleccionó qué instancias usar según si los jugadores se teletransportan del estado normal al estado corrupto de la habitación (TeleportWarmup), o viceversa (TeleportWarmupBack).Los eventos que se reproducen durante el teletransporte hacen que toda la pantalla sea oscura o blanca, por lo que decidimos cambiar las instancias Lighting y Atmosphere en esos momentos para ocultar el proceso a los jugadores.Para hacer que sea fácil cambiar, DemoConfig incluye mapas que definen qué instancias bajo estos servicios deben cambiar.
Estados de juego de puertas bloqueadas
Queríamos poder mantener a los jugadores en ciertas habitaciones mientras terminaban misiones, así que creamos estados de juego para cerrar puertas: InMission y CanGetSeal .InMission bloquea a los jugadores en su sala de misión activa, y CanGetSeal mantiene la puerta de la sala de misión bloqueada hasta que recojan el sello "restaurado".Principalmente usamos esto para que las puertas se cierren cuando los jugadores regresen de una misión para que tengan un incentivo para recoger el sello.Después de que recojan el sello, las puertas se desbloquean para que puedan colocarlo dentro de la ubicación del sello en el vestíbulo.La última misión es única para este proceso típico, ya que la puerta de la habitación con su sello está bloqueada hasta que los jugadores resuelvan todos los otros puzles (EnableRegularMissionDoors, EnableOneMissionDoors funciones).
Gestor de eventos
EventManager nos permitió ejecutar "acciones" con el tiempo usando marcos clave, como:
- Interpolación de propiedades e atributos de instancias.
- Ejecutando scripts.
- Reproduciendo sonido, audio.
- Vibraciones de la cámara en ejecución.
Idealmente usaríamos una herramienta con interfaz de usuario basada en pistas, pero para esta demostración, escribimos las teclas y los nombres de las propiedades manualmente.El sistema Gestor de eventos consiste en varios scripts y un evento/función, incluyendo:
- EventManager - Lógica general para crear y detener eventos, así como acciones del lado del servidor.
- EventManagerClient - Acciones del lado del cliente.
- EventManagerModule - Código común para acciones tanto del lado del servidor como del lado del cliente.
- EventManagerConfig - Archivo pequeño con algunas declaraciones de comando.
- EventManagerDemo - Donde todos los eventos reales para esta demostración se definen en un scriptespecífico del juego.
- EventManagerEvent , EventManagerFunc - Evento remoto y función vinculable para ejecutar y detener eventos desde el cliente o el servidor.Así es como otros sistemas pueden configurar, ejecutar y detener eventos.
Cada evento tiene un nombre, una sección con información opcional sobre el tiempo de reutilización, una función para ejecutarse al inicio o al finalizar, parámetros de evento, y secciones con interpolantes (interpolando cualquier número de propiedades o atributos con el tiempo), scripts (ejecutando scripts registrados en puntos clave), sacudidas de cámara y reproducción de sonido, audio.
Interpolación
La interpolación permite que las propiedades y atributos de los objetos cambien sin problemas de un valor a otro en lugar de saltar de forma desigual entre marcos clave.Definimos interpoladores para cambiar una variedad de efectos visuales; por ejemplo, el siguiente fragmento de código muestra cómo interpolamos la propiedad TextLabel.TextTransparency en un objeto definido por el parámetro TextLabel de un valor de 1 al principio a 0 , luego más tarde de nuevo a 1 :
interpolants = {objectParam = "TextLabel",property = "TextTransparency",keys = {{value = 1},{time = .5, value = 0},{time = 2.25, value = 0},{time = 3, value = 1}}}
Si bien podríamos definir qué propiedad o atributo de objeto pertenece a qué como en el siguiente ejemplo de código, queríamos poder volver a usar los mismos eventos en diferentes "grupos de objetos" para permitir que funcione con la transmisión en el cliente y con objetos creados en tiempo de ejecución.
object = workspace.SomeFolder.SomeModel
Para lograr esto, permitimos la referencia por nombre de objeto y el paso de parámetros nominales al iniciardel evento.Para encontrar objetos nombrados, permitimos especificar un "raíz" para el evento, que permitió que los objetos se encontraran por nombre bajo este root cuando comenzó el evento.Por ejemplo, en el siguiente fragmento de código, el EventManager intenta encontrar un objeto llamado "Wander" en algún lugar debajo de Workspace.Content.Interior.Foyer["Ritual-DemoVersion"] .
params = {["RootObject"] = workspace.Content.Interior.Foyer["Ritual-DemoVersion"],},interpolants = {objectName = "Wander",attribute = "TimeScale",keys = {{value = 0.2}}}
Permitimos pasar parámetros a eventos en la sección de parámetros, y los scripts que se ejecutan al comienzo del evento pueden cambiar los parámetros existentes o agregar más parámetros a la tabla "param".En el siguiente ejemplo, tenemos el parámetro isEnabled con un valor predeterminado de false, y la propiedad "Enabled" en un objeto con el nombre FocuserGlow se establecerá con el valor de isEnabled .Un script que se ejecuta al comienzo del evento o un script que invoca el evento puede establecer isEnabled, por lo que podríamos usar la misma descripción de evento para habilitar y deshabilitar FocuserGlow.
params = {isEnabled = false},interpolants = {{objectName = "FocuserGlow",property = "Enabled",keys = {{valueParam = "isEnabled"}}}
Los parámetros nos permitieron referirnos a objetos que ni siquiera existen al comienzo de la experiencia.Por ejemplo, en el siguiente ejemplo de código, una función que se ejecuta al comienzo del evento creará un objeto y establecerá la entrada BlackScreenObject en los parámetros para apuntar al objeto creado.
{objectParam = "BlackScreenObject",property = "BackgroundTransparency",keys = {{value = 0},{time = 19, value = 0},{value = 1},}}
Ejecutar eventos, instancias de eventos y conectarse a gatillos
Para ejecutar un evento, usaríamos un evento remoto de los clientes o una función del servidor.En el siguiente ejemplo, pasamos un par de parámetros a los eventos RootObject y isEnabled.Internamente, se creó una instancia de la descripción del evento, los parámetros se resolvieron a objetos reales y la función devolvió un ID para la instancia del 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} )
Podríamos dejar de ejecutar un evento llamando a la función con "Stop":
eventManagerFunc:Invoke("Stop", {eventInstId = cooldownId} )
Los interpolantes o otras acciones que sean "cosméticas" (no cambien la simulación para todos los jugadores) se pueden ejecutar en clientes, lo que podría resultar en una interpolación más suave.En la descripción del evento, podríamos proporcionar un valor predeterminado para todas las acciones como onServer = true (sin él, el predeterminado es el cliente).Cada acción puede reemplazarla al establecer su propio onServer.
Para conectar fácilmente el ejecución de un evento a un desencadenador, usamos funciones auxiliares ConnectTriggerToEvent o ConnectSpawnedTriggerToEvent, la última de las cuales encuentra el gatillo por nombre.Para permitir que el mismo evento se active usando diferentes gatillos, podríamos llamar eventManagerFunc con una clave de "Configuración" y un conjunto de volúmenes de gatillo.Para un ejemplo de un volumen de gatillo en acción, vea Hacer la panadería expandida.
Parámetros de evento
Además de los parámetros de evento personalizados pasados desde los scripts, otros datos que se pueden pasar opcionalmente al crear un evento incluyen al jugador, la llamada de devolución (a llamarse cuando finalice el evento) y los parámetros de llamada de devolución.Algunos eventos deben ejecutarse solo para un jugador (eventos con acciones que se ejecutan en el cliente), mientras que otros deben ejecutarse para todos/todas.Para hacerlo funcionar solo para un jugador, usamos onlyTriggeredPlayer = true en los parámetros.
Los eventos pueden tener tiempos de reutilización definidos por minCooldownTime y maxCooldownTime.El mínimo y el máximo proporcionan un rango para escalar en función del número de jugadores, pero no lo usamos en esta demostración.Si tuviéramos que tener necesidades de enfriamiento por jugador, teníamos la capacidad de usar perPlayerCooldown = true.Cada evento tiene una duración en segundos, y los tiempos de reutilización y las llamadas de devolución se basan en él.Para informar sobre la finalización de un evento, invocar código podría pasar una llamada de devolución de llamada y parámetros que obtendrá.
Llamar scripts
Podríamos llamar Scripts en puntos clave específicos en la sección Guiones . Por ejemplo:
scripts = {{startTime = 2, scriptName = "EnablePlayerControls", params = {true}, onServer = false }}
En el ejemplo anterior, el Habilitar controles de jugador Script tendría que registrarse con el módulo de gestor de eventos, como así:
emModule.RegisterFunction("EnablePlayerControls", EnablePlayerControls)
La función RegisterFunction debe llamarse en el script del cliente para las funciones llamadas en el cliente, y en el script del servidor para onServer = true.La propia función recibirá eventInstance y parámetros pasados, pero en este caso, solo se pasa un parámetro con un valor verdadero.
local function EnablePlayerControls(eventInst, params)
Reproducir sonido, audio
Tenemos soporte limitado para reproducir audio no posicional en marcos clave en la sección Sonidos , por ejemplo:
sounds = {{startTime = 2, name = "VisTech_ethereal_voices-001"},}
Tenga en cuenta que las llamadas de devolución de llamada de evento se disparan cuando expira la duración del evento, pero las acciones de audio pueden seguir reproducándose después.
Ejecutar sacudidas de cámara
Podríamos definir sacudidas de cámara en la sección camerShakes , como así:
cameraShakes = {{startTime = 15, shake = "small", sustainDuration = 7, targets = emConfig.ShakeTargets.allPlayers, onServer = true},}
Los "objetivos" se pueden iniciar solo para el jugador que inició el evento, allPlayer, o jugadoresInRadius al jugador que activó el evento.Utilizamos un script de terceros para sacudidas de cámara, y las sacudidas estaban predefinidas: eventManagerDemo.bigShake y eventManagerDemo.smallShake .sustainDuration también se podría pasar.
Lógica de misiones
Hay 7 misiones en total, y solo 6 de ellas usan sellos.La mayoría de las misiones tienen parámetros comunes, aunque algunas son solo para misiones con focas y teletransportarse a habitaciones corruptas.Cada misión tiene una entrada en el script DemoConfig con un conjunto de parámetros en el mapa Config.Missions :
- Raíz de misión : Un directorio de todas las versiones no corruptas de objetos.
- Puertas : Puertas para bloquear hasta que un jugador recoja una insignia.
- Nombre de sello / Nombre de sello solucionado : Sellos no corruptos y nombres de sello corruptos.
- SealPlaceName : Lugares para poner el sello.
- PlacedSealPlaceholderName : objeto de marcador en el lugar para poner el sello.
- TeleportPositionsName : Nombre de una carpeta con mallas de reemplazo para definir posiciones y rotaciones de teletransporte del jugador al moverse a la habitación corrupta y de vuelta a la zona normal.El mismo nombre se usa en ambos casos.
- CorruptRoomName : Nombres de las carpetas raíz (en relación con ServerStorage) para las habitaciones corruptas.Las salas corruptas se clonan bajo TempStorage.Cloned cuando comienza la misión, y se destruyen cuando termina la misión.
- Nombre del botón de misión completa : Un botón de truco en las habitaciones corruptas para terminar la misión de inmediato. Esto es para fines de depuración .
- Tecla de truco : La misma trampa que un número o CtrlShift[Number] .
Parte de la lógica de la misión está en los GameStateManager scripts, ya que las focas y las puertas proporcionan el flujo principal del juego para la mayoría de las misiones, pero la mayor parte de la lógica específica de la misión está en los MissionsLogic y MissionsLogicClient scripts que definen varios "tipos" de misiones.El tipo se define solo por la presencia de miembros con nombres específicos en la descripción de la misión.Hay algunos tipos de misiones:
- Usa una llave en una cerradura - La primera misión para abrir una puerta. Este tipo se define por LockName, KeyName.
- Elementos de coincidencia - 4 elementos de coincidencia de misiones. Este tipo se define por MatchItems .
- Vestir a un maniquí usando tela en capas - 1 misión en el ático tiene jugadores que recogen tres artículos. Este tipo se define por DressItemsTagList .
- Haga clic en el elemento para terminar - 1 misión tiene este introducir, que se define por ClickTargetName .
Cada tipo de misión tiene su propio StartMissionFunc y CompleteMissionFunc.La función de inicio suele leer parámetros del mapa MatchItem , resolver nombres a objetos y configurar cualquier detector de clics o elementos de interfaz de usuario.Casi toda la lógica está en un servidor, pero MissionsLogicClient proporciona una interfaz de usuario para mostrar el contraofertade elementos, utilizado en muchas misiones. MissionLogicEvent el evento remoto se usa para las comunicaciones del servidor - cliente, con un pequeño tipo de comandos definidos por MissionEvents pequeños.El script MiscGameLogic vincula algunos gatillos a eventos y elimina objetos de depuración en la versión de lanzamiento.
La lógica de emparejamiento de elementos permite "usar" (haga clic mientras mantiene presionado) elementos marcados con etiquetas PuzzlePieceXX sobre elementos con etiqueta PuzzleSlotYY .Hay algunas opciones disponibles como parámetros en el mapa MatchItems (si las piezas deben aplicarse en orden, si solo se requiere una de cada una).Podríamos especificar nombres para efectos de audio y visuales simples.Cuando se necesitan piezas en ubicaciones específicas, un mapa adicional de "Colocación" proporciona la correspondencia de etiquetas de piezas a nombres de piezas de reemplazo que definen transformaciones.
Agarrando
Desarrollamos un sencillo sistema de agarre para sostener un objeto al unir el objeto al brazo derecho del personaje.La captura se implementa en GrabServer2 y GrabClient scripts.Comienza en ProcessClick , que dispara un rayo a través del punto clicado/tocado.Luego comprueba si golpeamos una malla que se puede agarrar, y el golpe está dentro del maxMovingDist donde podemos comenzar a agarrar la interacción.Si el modelo que hizo clic tiene Attachments llamado GrabHint , elegimos el más cercano al punto clicado.Recordamos la parte agarrada, el modelo al que pertenece y, ya sea el GrabHint más cercano o la posición clicada en la estructura .Si la distancia es mayor que maxGrabDist, el jugador primero debe caminar lo suficientemente cerca del punto de agarre intentado, así que llamamos Humanoid.MoveTo .
En cada marco, comprobamos si se está llevando a cabo un intento de agarre.Si el jugador está dentro de reachDist , comenzamos a jugar ToolHoldAnim .Cuando un jugador está dentro de maxGrabDist , el cliente envía una solicitud al servidor para realmente agarrar un modelo (función performGrab).
El script del lado del servidor tiene 2 funciones principales:
- Agarra - Maneja la solicitud de un cliente para agarrar un aplicación de modelado.
- Liberación - Maneja la solicitud de liberar un aplicación de modeladoagarrado.
La información sobre lo que cada jugador sostiene se mantiene en el mapa playerInfos .En la función de agarre, comprobamos si este modelo ya está agarrado por otro jugador.Si es así - un "EquipWorldFail" se envía al cliente y cancela la intentode agarre.Tenga en cuenta que necesitábamos manejar situaciones en las que los jugadores agarran diferentes partes de la misma Model , y cancelar la agarre en este caso.
Si se permite agarrar, el script crea dos Attachments , uno a la derecha y otro en el objeto usando un punto de agarre pasado del cliente.Luego crea un RigidConstraint entre los dos Attachments. Constraints y Attachments se almacenan bajo la carpeta Agarres actuales bajo el personaje del jugador.Agarrar también reproduce un sonido, desactiva las colisiones en el objeto agarrado y trata con Restorables, si es necesario.
Para liberar un aplicación de modeladoagarrado, el script del cliente se conecta al botón Liberar botón en la pantalla HUD de ScreenGui.Una función Connected lanza un evento al servidor.En el servidor, liberar elimina el Attachments y Constraints , restaura la colisión, trata con cualquier Restorables aplicable, y limpia los datos de agarre para este cliente en playerInfos .