Os seguintes sistemas foram a base para estabelecer o jogo que queríamos para O Mistério da Avenida Duvall.
Gerenciador de Estado de Jogo
O Gerenciador de Estado de Jogo / Cliente de Estado de Jogo provavelmente é o sistema mais complicado na experiência, pois lida com:
- Começar jogadores dentro do lobby, iniciando a contagem regressiva para teletransportar o grupo para a área de jogabilidade principal e teletransportar jogadores para servidores reservados.
- Clonando salas corrompidas, transmitindo-as de forma assíncrona e teletransportando os jogadores para e de uma coordenada específica designada CFrame.
- Agarração e colocação de mecânicas de selos.
- Trancar e destrancar portas.
- Inicializando o teleporte final para o foyer e tocando a cena de encerramento.
Implementamos-la como uma máquina de estado simples (função de atualização), e os estados estão em DemoConfig (enumeração de estados de jogo).Alguns estados lidam com o teleportarinicial de lobby/servidor reservado, enquanto outros lidam com encontrar uma missão, ativar o quebra-cabeça e resolver a missão.Observe que, além de focas, tentamos não ter código específico de missão no Gerenciador de Estado de Jogo .
GameStates é principalmente do lado do servidor, mas quando o cliente precisa fazer algo, como mostrar contagem regressiva, história ou desativar a interrupção de streaming da interface, servidor e cliente (GameStatesClient) se comunicam por meio de um evento remoto chamado GameStateEvent.Como na maioria dos casos, o payload do evento tem "digitar" de evento (Config.GameEvents) como o primeiro parâmetro e dados específicos de evento após isso.
Estados do jogo de teletransporte
Há um grupo de 3 estados de jogo que executa três cutscenes únicas que escondem a teletransportação para a sala corrupta: Warmup, InFlight e Cooldown. Aquecimento é executado durante toda a duração e termina com uma tela quase preta na qual o mundo 3D não é mais visível.Durante este tempo, clonamos a sala, obter as posições desejadas do jogador na sala corrompida para cada jogador, chamamos Player.RequestStreamAroundAsync , e transportamos os jogadores para uma coordenada específica designada CFrame dentro da sala corrompida.Esse tipo de teletransporte pode desencadear uma pausa de streaming.Quando uma pausa de streaming ocorre, o cliente exibe uma mensagem de aviso.Desativamos essa interface padrão para manter a experiência imersivo.
Enquanto o streaming está sendo tratado, InFlight executa, mantendo uma tela escura pulsante levemente.Quando ambos Player.RequestStreamAroundAsync retornam e os clientes informam ao servidor que a pausa de streaming está desativada e cada jogador está perto o suficiente da localização desejada, cancelamos a cena de sobrevivência no voo e iniciamos a cena de espera.O objetivo de Tempo de espera é tornar o mundo 3D visível novamente removendo suavemente a tela escura.Se Player.RequestStreamAroundAsync demorar muito para retornar ou o cliente não relatar que a pausa de streaming está desligado, ainda procedemos para a cena de espera após um limite de tempo de vários segundos.
Um conjunto semelhante de cenas de aquecimento, voo e espera ocorre quando teletransportamos o jogador de volta ao estado normal da sala, TeleportWarmupBack , TeleportInFlightBack e TeleportCooldownBack , e no final da experiência, também executamos TeleportWarmupFinal , TeleportInFlightFinal e TeleportCooldownFinal para teletransportar os jogadores para o foyer para a cena final.
Estados de jogo de iluminação e atmosfera
Sabíamos que queríamos que o estado normal e corrupto de cada sala tivesse uma aparência visual diferente para que pudesse dar aos jogadores um feedback visual claro de que eles estavam em um local completamente diferente.Estados de jogo nos permitiram alterar as propriedades de iluminação e atmosfera para salas normais e corrompidas, nas quais o Gerenciador de Estado de Jogo selecionou quais instâncias usar com base se os jogadores estão sendo teletransportados do estado normal para o estado corrompido da sala ( TeleportWarmup ), ou vice-versa ( TeleportWarmupBack ).Eventos tocando durante o teletransporte tornam toda a tela escura ou branca, então decidimos mudar as instâncias Lighting e Atmosphere nesses momentos para ocultar o processo dos jogadores.Para tornar simples de alterar, DemoConfig inclui mapas que definem quais instâncias sob esses serviços precisam ser alteradas.
Estados de jogo de portas de bloqueio
Queríamos ser capazes de manter os jogadores em determinadas salas enquanto terminavam missões, então criamos estados de jogo para trancar portas: InMission e CanGetSeal.InMission trava os jogadores em sua sala de missão ativa, e CanGetSeal mantém a porta da sala de missão trancada até que eles peguem o selo "restaurado".Usamos isso principalmente para que as portas sejam trancadas quando os jogadores retornam de uma missão para que eles tenham um incentivo para pegar o selo.Depois de pegar o selo, as portas se desbloqueiam para que possam colocá-lo dentro da localização do selo no foyer.A última missão é única para esse processo típico, pois a porta para a sala com seu selo está trancada até que os jogadores resolvam todos os outros quebra-cabeças (EnableRegularMissionDoors , EnableOneMissionDoors funções).
Gerenciador de Eventos
Gerenciador de eventos nos permitiu executar "ações" ao longo do tempo usando quadros-chave, como:
- Interpolando propriedades e atributos da instância.
- Executando scripts.
- Reproduzindo áudio.
- Tremer a câmera em execução.
Idealmente, usaríamos uma ferramenta com interface baseada em faixas, mas para esta demonstração, digitamos as chaves e os nomes das propriedades manualmente.O sistema Gerenciador de Eventos consiste em vários scripts e um evento/função, incluindo:
- EventManager - Lógica geral para criar e interromper eventos, bem como ações do lado do servidor.
- EventManagerClient - Ações do lado do cliente.
- EventManagerModule - Código comum para ações de ambos os lados do servidor e do cliente.
- EventManagerConfig - Arquivo pequeno com algumas declarações de comando.
- EventManagerDemo - Onde todos os eventos reais para este demo são definidos em um script específico do jogo.
- EventManagerEvent , EventManagerFunc - Evento remoto e função vinculável para executar e interromper eventos do cliente ou do servidor.É assim que outros sistemas podem configurar, executar e interromper eventos.
Cada evento tem um nome, uma seção com informações opcionais sobre tempo de espera, função para executar no início ou no terminar/parar/sair, parâmetros de evento, e seções com interpolantes (interpolando qualquer número de propriedades ou atributos ao longo do tempo), scripts (executando scripts registrados em keyframes), agitações de câmera e reprodução de áudio.
Interpolação
A interpolação permite que as propriedades e atributos do objeto mudem sem problemas de um valor para outro, em vez de saltar de forma desconectada entre quadros-chave.Definimos interpolantes para alterar uma variedade de efeitos visuais; por exemplo, o seguinte trecho de código mostra como interpolamos a propriedade TextLabel.TextTransparency em um objeto definido pelo parâmetro TextLabel de um valor de 1 no início para 0 , e depois mais tarde de volta para 1 novamente:
interpolants = {objectParam = "TextLabel",property = "TextTransparency",keys = {{value = 1},{time = .5, value = 0},{time = 2.25, value = 0},{time = 3, value = 1}}}
Embora pudéssemos definir qual propriedade ou atributo de objeto pertence a algo como no seguinte exemplo de código, queríamos ser capazes de reutilizar os mesmos eventos em diferentes "grupos de objetos" para permitir que ele funcione com streaming no cliente e com objetos criados na hora de executar.
object = workspace.SomeFolder.SomeModel
Para realizar isso, permitimos a referência pelo nome do objeto e o passe de parâmetros nomeados no iniciardo evento.Para encontrar objetos nomeados, permitimos especificar um "raíz" para o evento, que deixou os objetos serem encontrados por nome sob este root quando o evento começou.Por exemplo, no seguinte trecho de código, o Gerenciador de Eventos tenta encontrar um objeto chamado "Wander" em algum lugar abaixo 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 que os parâmetros fossem passados a eventos na seção de parâmetros, e os scripts executados no início do evento poderiam alterar os parâmetros existentes ou adicionar mais parâmetros à tabela "param".No seguinte exemplo, temos o parâmetro isEnabled com um valor padrão de falso e a propriedade "Ativado" em um objeto com o nome FocuserGlow será definida para o valor de isEnabled.Um script executando no início do evento ou um script que invoca o evento pode definir isEnabled, então poderíamos usar a mesma descrição de evento para ativar e desativar o FocuserGlow.
params = {isEnabled = false},interpolants = {{objectName = "FocuserGlow",property = "Enabled",keys = {{valueParam = "isEnabled"}}}
Os parâmetros nos permitiram referir-nos a objetos que nem sequer existem no início da experiência.Por exemplo, no seguinte código de exemplo, uma função executando no início do evento criará um Objetoe definirá a entrada BlackScreenObject nos parâmetros para apontar para o Objetocriado.
{objectParam = "BlackScreenObject",property = "BackgroundTransparency",keys = {{value = 0},{time = 19, value = 0},{value = 1},}}
Execute eventos, instâncias de evento e conecte-se a gatilhos
Para executar um evento, usaríamos um evento remoto de clientes ou uma função do servidor.No seguinte exemplo, passamos alguns parâmetros aos eventos RootObject e isEnabled.Internamente, uma instância da descrição do evento foi criada, os parâmetros resolvidos para objetos reais e a função retornou um ID para a instância do 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} )
Podemos parar de executar um evento chamando a função com "Stop":
eventManagerFunc:Invoke("Stop", {eventInstId = cooldownId} )
Interpolantes ou outras ações que sejam "cosméticas" (não alterem a simulação para todos os jogadores) podem ser executadas em clientes, o que pode resultar em uma interpolação mais suave.Na descrição do evento, poderíamos fornecer um valor padrão para todas as ações como onServer = true (sem ele, o padrão é o cliente).Cada ação pode substituí-la definindo seu próprio onServer.
Para conectar facilmente a execução de um evento a um gatilho, usamos funções de auxílio ConnectTriggerToEvent ou ConnectSpawnedTriggerToEvent, a última das quais encontra o gatilho por nome.Para permitir que o mesmo evento seja acionado usando diferentes gatilhos, poderíamos chamar eventManagerFunc com uma chave "Configurar" e um conjunto de volumes de gatilho.Para um exemplo de volume de gatilho em ação, veja Fazendo a Expansão da Panela.
Parâmetros de evento
Além dos parâmetros de evento personalizados passados de scripts, outros dados que podem ser opcionalmente passados ao criar um evento incluem jogador, chamada de retorno (a ser chamada quando o evento terminar) e parâmetros de chamada de retorno.Alguns eventos devem ocorrer apenas para um jogador (eventos com ações rodando no cliente), enquanto outros devem ocorrer para todas / todos.Para fazer isso funcionar apenas para um jogador, usamos onlyTriggeredPlayer = true na parametrização.
Eventos podem ter tempos de espera definidos por minCooldownTime e maxCooldownTime.O mínimo e o máximo fornecem um alcance para escalonamento com base no número de jogadores, mas não o usamos nesta demonstração.Se precisássemos ter necessidades de espera por jogador, tínhamos a capacidade de usar perPlayerCooldown = true.Cada evento tem uma duração em segundos, e os tempos de espera e os retornos são baseados nela.Para informar sobre o término de um evento, invocar código poderia passar um retorno de chamada e parâmetros que ele obteria.
Chamando scripts
Podemos chamar Scripts na seção Scripts de quadros-chave específicos. Por exemplo:
scripts = {{startTime = 2, scriptName = "EnablePlayerControls", params = {true}, onServer = false }}
No exemplo anterior, o HabilitarControles de Jogador Script precisaria ser registrado com o módulo gerenciador de eventos, como assim:
emModule.RegisterFunction("EnablePlayerControls", EnablePlayerControls)
A função RegisterFunction deve ser chamada no script do cliente para funções chamadas no cliente e no script do servidor para onServer = true.A própria função receberá eventInstance e parâmetros passados, mas neste caso, apenas um parâmetro é passado com um valor verdadeiro.
local function EnablePlayerControls(eventInst, params)
Reproduzir áudio
Temos suporte limitado para tocar áudio não posicional em quadros-chave na seção Sons , por exemplo:
sounds = {{startTime = 2, name = "VisTech_ethereal_voices-001"},}
Observe que os chamados de retorno ao final do evento são acionados quando a duração do evento expira, mas as ações de áudio podem ainda estar tocando depois.
Executar agitações da câmera
Podemos definir agitações de câmera na seção agitações de câmera , como assim:
cameraShakes = {{startTime = 15, shake = "small", sustainDuration = 7, targets = emConfig.ShakeTargets.allPlayers, onServer = true},}
Os "alvos" podem ser iniciados apenas para o jogador que disparou o evento, allPlayer ou jogadoresInRadius para o jogador disparador.Usamos um script de terceiros para agitações de câmera, e as agitações foram pré-definidas: eventManagerDemo.bigShake e eventManagerDemo.smallShake.sustainDuration também poderia ser passado.
Lógica de Missões
Há 7 missões no total, e apenas 6 delas usam focas.A maioria das missões tem parâmetros comuns, embora algumas sejam apenas para missões com focas e teletransporte para salas corruptas.Cada missão tem uma entrada no script DemoConfig com um conjunto de parâmetros no mapa Config.Missions :
- Raiz de Missão : Um diretório de todas as versões não corrompidas de objetos.
- Portas : Portas para trancar até que um jogador pegue uma vedação.
- Nome da Baleia / Nome da Baleia Resolvido : Baleia não corrompida e nomes de baleia corrompidos.
- Nome do Local de Selo : Locais para colocar o selo.
- Nome do espaço reservado para colocar o selo : objeto de espaço reservado no lugar para colocar o selo.
- Nome do posicionamento de teletransporte : Nome de uma pasta com malhas de preenchimento para definir posições e rotações de teletransporte do jogador ao se mover para a sala corrompida e de volta para a área normal.O mesmo nome é usado em ambos os casos.
- CorruptRoomName : Nomes dos diretórios raiz (em relação ao Armazenamento do Servidor) para as salas corrompidas.Salas corrompidas clonam sob TempStorage.Cloned quando a missão começa e são destruídas quando a missão termina.
- Nome do Botão de Missão Completa : Um botão de truque nas salas corrompidas para terminar a missão imediatamente. Isso é para fins de depuração.
- Tecla de truque : O mesmo truque que um número ou CtrlShift[Number] .
Parte da lógica da missão está nos scripts GameStateManager , pois os golfinhos e as portas fornecem o fluxo principal da missão para a maioria das missões, mas a maior parte da lógica específica da missão está nos scripts MissionsLogic e MissionsLogicClient que definem vários "tipos" de missões.O tipo é definido apenas pela presença de membros especificamente nomeados na descrição da missão.Existem alguns tipos de missões:
- Use uma chave em uma fechadura - A primeira missão para abrir uma porta. Este tipo é definido por LockName, KeyName.
- Itens de correspondência - 4 itens de correspondência de missões. Este tipo é definido por MatchItems .
- Vestir um manequim usando tecido em camadas - 1 missão no sótão tem jogadores coletando três itens. Este tipo é definido por DressItemsTagList .
- Clique no item para terminar - 1 missão tem esse digitar, que é definido por ClickTargetName .
Cada tipo de missão tem seu próprio StartMissionFunc e CompleteMissionFunc.A função inicial geralmente lê parâmetros do mapa MatchItem , resolve nomes para objetos e configura quaisquer detectores de cliques ou elementos de interface do usuário.Quase toda a lógica está em um servidor, mas o MissionsLogicClient fornece uma interface para mostrar o contador de itens, usado em muitas missões. MissionLogicEvent O evento remoto é usado para comunicações de servidor - cliente, com um pequeno tipo de comando definindo tipos de comandos passados.O script MiscGameLogic vincula alguns gatilhos a eventos e remove objetos de depuração na versão de lançamento.
A lógica de correspondência de itens permite "usar" (clicar enquanto segura) itens marcados com PuzzlePieceXX tags sobre itens com PuzzleSlotYY tag.Há algumas opções disponíveis como parâmetros no mapa MatchItems (se as peças precisam ser aplicadas em ordem, se apenas uma de cada for necessária).Podemos especificar nomes para áudio e FX visuais simples.Quando peças precisam ser colocadas em locais específicos, um mapa extra de "Colocação" fornece mapeamento de tags de peças para nomes de peças de espaço reservado que definem transformações.
Agarração
Desenvolvemos um simples sistema de agarramento para segurar um objeto ao prender o objeto ao braço direito do personagem.A captura é implementada em GrabServer2 e GrabClient scripts.Começa em ProcessClick, que dispara um raio através do ponto clicado/tocado.Verifica então se atingimos uma malha que pode ser agarrada e o golpe está dentro do maxMovingDist onde podemos começar a agarrar interação.Se o modelo clicou em tem Attachments chamado GrabHint , escolhemos o mais próximo do clique.Lembramos da peça capturada, do modelo a que ela pertence e, seja o GrabHint mais próximo ou a posição clicada na estrutura .Se a distância for maior que maxGrabDist, o jogador primeiro precisa caminhar o suficiente perto do ponto de agarrar tentado, então chamamos Humanoid.MoveTo.
Em cada quadro, verificamos se uma tentativa de captura está em andamento.Se o jogador estiver dentro de reachDist , começamos a tocar ToolHoldAnim .Quando um jogador está dentro de maxGrabDist , o cliente envia um pedido ao servidor para realmente pegar um modelo (função performGrab).
O script do lado do servidor tem 2 funções principais:
- Capturar - Gerencia o pedido de um cliente para capturar um modelo.
- Liberação - Gerencia a solicitação de liberação de um modelo capturado.
Informações sobre o que cada jogador possui são mantidas no mapa playerInfos .Na função de captura, verificamos se esse modelo já foi capturado por outro jogador.Se assim for - um "EquipWorldFail" é enviado ao cliente e ele cancela a tentarde captura.Observe que precisávamos lidar com situações em que os jogadores pegam diferentes partes do mesmo Model, e cancelam a pegada neste caso.
Se a captura for permitida, o script cria dois Attachments, um à direita e outro no objeto usando um ponto de captura passado do cliente.Então, cria um RigidConstraint entre os dois Attachments.Constraints e Attachments são armazenados na pasta Grips Atuais sob o personagem do jogador.A captura também toca um som, desabilita colisões no Objetocapturado e lida com Restorables, se necessário.
Para liberar um modelo capturado, o script do cliente se conecta ao botão GrabReleaseButton na tela HUD ScreenGui.Uma função Connected lança um evento para o servidor.No servidor, liberar exclui o Attachments e Constraints , restaura a colisão, lida com quaisquer Restorables aplicáveis e remove os dados de gravação para este cliente em playerInfos .