Planta é uma experiência de referência onde os jogadores plantam e regam sementes, para que possam mais tarde colher e vender as plantas resultantes.
O projeto se concentra em casos de uso comuns que você pode encontrar ao desenvolver uma experiência no Roblox. Onde Aplicável, você encontrará anotações sobre trocas, compromissos e a racionalidade de várias escolhas de implementação, para que você possa tomar a melhor decisão para suas próprias experiências.
Obter o Arquivo
- Navegue até a página de experiência Plant.
- Clique no botão **** e Editar no Studio .
Caixas de Uso
Planta cobre os seguintes casos de uso:
- Dados de sessão e persistência de dados do jogador
- Gerenciamento de exibição de UI
- Rede cliente-servidor
- Primeira Experiência de Usuário (FTUE)
- Compras de moeda duras e suaves
Além disso, este projeto resolve conjuntos mais estreitos de problemas que são aplicáveis a muitas experiências, incluindo:
- Personalização de uma área no local que está associada a um jogador
- Gerenciando a velocidade de movimento do personagem do jogador
- Criando um objeto que segue os personagens
- Detectando em que parte do mundo um personagem está
Note que existem vários casos de uso nesta experiência que são muito pequenos, muito de nicho ou não demonstram uma solução para um desafio de design interessante; estes não são cobertos.
Estrutura do projeto
A primeira decisão ao criar uma experiência é decidir como estruturar o projeto , que inclui principalmente onde colocar instâncias específicas no modelo de dados e como organizar e estruturar os pontos de entrada para o código do cliente e do servidor.
Modelo de dados
A tabela a seguir descreve em quais serviços de contêiner nas instâncias do modelo de dados são colocados.
Servicio | Tipos de instancias |
---|---|
Workspace | Contiene modelos estáticos que representan el mundo 3D, específicamente partes del mundo que no pertenecen a ningún jugador. No es necesario crear, modificar o destruir dinámicamente estas instancias en tiempo de ejecución, por lo que es aceptable dejarlas aquí. También hay un Folder , al que se agregarán los modelos de granjas de los jugadores en tiempo de ejecución. |
Lighting | Efectos atmosféricos y de iluminación. |
ReplicatedFirst | Contiene el subconjunto más pequeño posible de instancias necesarias para mostrar la pantalla de carga e inicializar el juego. Cuantas más instancias se coloquen en ReplicatedFirst , más larga será la espera para que se repliquen antes de que el código en ReplicatedFirst pueda ejecutarse.
|
ReplicatedStorage | Sirve como contenedor de almacenamiento para todas las instancias a las que se requiere acceso tanto en el cliente como en el servidor.
|
ServerScriptService | Contiene un Script que sirve como punto de entrada para todo el código del lado del servidor en el proyecto. |
ServerStorage | Sirve como contenedor de almacenamiento para todas las instancias que no necesitan ser replicadas al cliente.
|
SoundService | Contiene los Sound objetos usados para efectos de sonido en el juego. Bajo SoundService , estos Sound objetos no tienen posición y no se simulan en el espacio 3D. |
Pontos de Entrada
A maioria dos projetos organiza o código dentro de ModuleScripts reutilizável que pode ser importado em toda a base de código. ModuleScripts são reutilizáveis, mas não são realizados possuir; eles precisam ser importados por um Script ou LocalScript. Muitos projetos do Roblox terão um grande número de Script e LocalScript objetos, cada um relacionado a um comportamento ou sistema específico no jogo, criando vários pontos de entrada.
Para o microjogo Plant, uma abordagem diferente é implementada através de um único LocalScript que é o ponto de entrada para todo o código do cliente e um único Script que é o ponto de entrada para todo o código do servidor. A abordagem correta para o seu projeto depende das suas necessidades, mas um único ponto de entrada fornece maior controle sobre a ordem em que os sistemas são ejecutados.
As seguintes listas descrevem as trocas de ambas as abordagens:
- Um único Script e um único LocalScript cobrem o código do servidor e do cliente, respectivamente.
- Maior controle sobre a ordem em que diferentes sistemas são iniciados porque todo o código é inicializado a partir de um único script.
- Pode passar objetos por referência entre sistemas.
Arquitetura de Sistemas de Alto Nível
Os sistemas de nível superior no projeto são detalhados abaixo. Alguns desses sistemas são substancialmente mais complexos que outros, e em muitos casos sua funcionalidade é abstraída através de uma hierarquia de outras Classes.
Cada um desses sistemas é um "singleton," na medida em que é uma classe não instantável que é inicializada pelo cliente ou servidor relevante start iniciar. Você pode ler mais sobre o padrão singleton mais tarde neste guia.
Servidor
Os seguintes sistemas estão associados ao servidor.
Sistema | Descripción |
---|---|
Red |
|
Servidor de Datos del Jugador |
|
Mercado |
|
Administrador de Grupos de Colisión |
|
Servidor de administrador de granja |
|
Contenedor de Objetos de Jugador |
|
Jugadores de Etiquetas |
|
Servidor FtueManager |
|
Generador de personajes |
|
Cliente
Os seguintes sistemas estão associados ao cliente.
Sistema | Descripción |
---|---|
Red |
|
Cliente de Datos del Jugador |
|
Cliente de mercado |
|
Administrador de salto de caminata local |
|
Cliente de Gerente de Granja |
|
Configuración de UIS |
|
Cliente de FtueManager |
|
Carrera de caracteres |
|
Comunicação Cliente-Servidor
A maioria das experiências do Roblox envolve algum elemento de comunicação entre o cliente e o servidor. Isso pode incluir o cliente solicitando que o servidor realize uma certa ação e o servidor replicando atualizações para o cliente.
Neste projeto, a comunicação cliente-servidor é mantida o mais genérica possível, limitando o uso de RemoteEvent e RemoteFunction objetos, a fim de diminuir a quantidade de regras especiais para acompanhar. Este projeto usa os seguintes métodos, em ordem de preferência:
- Replicação através do sistema de dados do jogador .
- Replicação via tags .
- Mensagens diretamente através do módulo Network .
Replicação através do Player Data System
O sistema de dados do jogador permite que os dados sejam associados ao jogador que persiste entre as sessões de salvamento. Este sistema fornece replicação de cliente para servidor e um conjunto de APIs que podem ser usadas para consultar dados e assinar alterações, tornando-o ideal para replicar mudanças no estado do jogador do servidor para o cliente.
Por exemplo, em vez de lançar um personalizado UpdateCoins``Class.RemoteEvent para dizer ao cliente quantas moedas ele tem, você pode chamar o seguinte e deixar o cliente se inscrever no evento PlayerDataClient.updated.
PlayerDataServer:setValue(player, "coins", 5)
Claro, isso só é útil para a replicação de servidor para cliente e para valores que você deseja persistir entre sessões, mas isso se aplica a um número surpreendente de casos no projeto, incluindo:
- O estágio FTUE atual
- inventáriodo jogador
- A quantidade de moedas que o jogador tem
- O estado da fazenda do jogador
Replicação via Atributos
Em situações em que o servidor precisa replicar um valor personalizado para o cliente que seja específico para um dado Instance , você pode usar atributos . Roblox replica automaticamente os valores dos atributos, então você não precisa manter nenhum caminho de código para replicar o estado associado a um Objeto. Outra vantagem é que essa replicação acontece ao lado da própria instância.
Isso é especialmente útil para instâncias criadas no tempo de execução, pois os atributos configurados em uma nova instância antes que ela seja relacionada ao modelo de dados serão replicados atomicamente com a própria instância. Isso evita qualquer necessidade de escrever código para "esperar" por dados extras a serem replicados via RemoteEvent ou StringValue .
Você também pode ler diretamente atributos do modelo de dados, do cliente ou do servidor, com o GetAttribute() método e assinar alterações com o GetAttributeChangedSignal() método. No projeto Plant, essa abordagem é usada para, entre outras coisas, replicar o status atual das plantas para os clientes.
Replicação através de Tags
CollectionService permite aplicar uma tag de string a um Instance . Isso é útil para categorizar instâncias e replicar essa categorização ao cliente.
Por exemplo, a etiqueta CanPlant é aplicada no servidor para significar ao cliente que um determinado pote pode receber uma planta.
Mensagens Diretamente via Módulo de Rede
Para situações em que nenhuma das opções anteriores se aplicam, você pode usar chamadas de rede personalizadas através do módulo Network . Esta é a única opção no projeto que permite a comunicação cliente-servidor e, portanto, é mais útil para transmitir solicitações de clientes e receber uma resposta do servidor.
A planta usa chamadas de rede diretas para uma variedade de solicitações de clientes, incluindo:
- Regando uma planta
- Plantando uma semente
- Comprando um item
A desvantagem com essa abordagem é que cada mensagem individual requer alguma configuração personalizada que pode aumentar a complexidade do projeto, embora isso tenha sido evitado sempre que possível, especialmente para a comunicação servidor-cliente.
Classes e Singletons
Classes no projeto Plant, como instâncias no Roblox, podem ser criadas e destruídas. Sua sintaxe de classe é inspirada na abordagem idiomática de Lua para programação orientada a objetos com uma série de mudanças para habilitar o Suportea digitação rigorosa .
Instanciação
Muitas classes no projeto estão associadas a uma ou mais Instances. Objetos de uma determinada classe são criados usando um new() método, consistente com a forma como instâncias são criadas no Roblox usando Instance.new().
Este padrão é geralmente usado para objetos onde a classe tem uma representação física no modelo de dados, e a classe estende sua funcionalidade. Um bom exemplo é BeamBetween que cria um Beam objeto entre dois dados Attachment objetos e mantém esses anexos orientados para que o feixe esteja sempre voltado para cima. Essas instâncias podem ser clonadas de uma versão pré-fabricada em ReplicatedStorage ou passadas para new() como argumento e armazenadas dentro do objeto em self .
Instâncias Correspondentes
Como observado acima, muitas classes neste projeto têm uma representação de modelo de dados, uma instância que corresponde à classe e é manipulada por ela.
Em vez de criar essas instâncias quando um objeto de classe é instanciado, o código geralmente opta por Clone(), uma versão pré-fabricada do Instance armazenado em ReplicatedStorage ou ServerStorage. Embora seja possível serializar as propriedades dessas instâncias e criá-las a partir do zero nas funções new() da classe, fazer isso tornaria a edição dos objetos muito complicada e tornaria mais difícil para um leitor analisá-las. Além disso, clonar uma instância é geralmente uma operação mais rápida do que criar uma nova instância e personalizar suas propriedades no tempo de execução.
Composição
Embora a herança seja possível no Lua usando metatables, o projeto opta por permitir que as classes se estendam umas às outras através de composição . Ao combinar classes através da composição, o objeto "filho" é instanciado no método new() da classe e é incluído como membro em self .
Para um exemplo disso em ação, veja a CloseButton classe que envolve a Button classe.
Limpeza
Semelhante a como um Instance pode ser destruído com o Destroy() método, classes que podem ser instanciadas também podem ser destruídas. O método destruidor para classes de projeto é destroy() com uma letra pequena d para camelCase consistência em todos os métodos da base de código, bem como para distinguir entre as classes do projeto e as instâncias do Roblox.
O papel do método destroy() é destruir qualquer instância criada pelo Objeto, desconectar quaisquer conexões e chamar destroy() em qualquer objeto filho. Isso é especialmente importante para conexões, pois instâncias com conexões ativas não são limpas pelo coletor de lixo Lua, mesmo que não permaneçam referências à instância ou conexões com a instância.
Singletons
Singletons, como o nome sugere, são classes para as quais apenas um objeto pode existir. Eles são o equivalente do projeto aos Serviços do Roblox. Em vez de armazenar uma referência ao objeto singleton e passá-lo no código Lua, Plant aproveita o fato de exigir um ModuleScript para armazenar seu valor retornado. Isso significa que exigir o mesmo singleton ModuleScript de lugares diferentes fornece consistentemente o mesmo Objetoretornado. A única exceção a essa regra seria se ambientes diferentes (cliente ou servidor) acessassem o ModuleScript .
Singletons são distinguidos de classes instantáveis pelo fato de que eles não têm um new() método. Em vez disso, o objeto junto com seus métodos e estado é retornado diretamente através do ModuleScript . Como singletons não são instanciados, a sintaxe self não é usada e os métodos são chamados com um ponto (. ) em vez de um ponto (: ).
Inferência de Tipo Estrito
Luau suporta digitação gradual, o que significa que você pode adicionar definições de tipo opcionais a alguns ou todos os seus códigos. Neste projeto, strict a verificação de tipo é usada para todos os scripts. Esta é a opção menos permissiva para a ferramenta de análise de scripts do Roblox e, portanto, é mais provável que capture erros de tipo antes do tempo de execução.
Sintaxe de classe digitada
A abordagem estabelecida para criar classes no Lua é bem documentada , no entanto, não é bem adequada para digitar Luau forte. Em Luau, a abordagem mais simples para obter o tipo de uma classe é o typeof() método:
type ClassType = typeof(Class.new())
Isso funciona, mas não é muito útil quando sua classe é iniciada com valores que só existem no tempo de execução, por exemplo, Player objetos. Além disso, a suposição feita na sintaxe idiomática da classe Lua é que declarar um método em uma classe self sempre será uma instância dessa classe; isso não é uma suposição que o mecanismo de inferência de tipo pode fazer.
Para suportar a inferência de tipo estrita, o projeto Plant usa uma solução que difere da sintaxe idiomática da classe Lua de várias maneiras, algumas das quais podem parecer não intuitivas:
- A definição de self é duplicada, tanto na declaração de tipo quanto no construtor. Isso introduz um fardo de manutenção, mas avisos serão sinalizados se as duas definições não estiverem sincronizadas uma com a outra.
- Métodos de classe são declarados com um ponto, então self pode ser explicitamente declarado como sendo do tipo ClassType. Métodos ainda podem ser chamados com um ponto e vírgula, como esperado.
--!estrito
local MyClass = {}
MyClass.__index = MyClass
export type ClassType = typeof(setmetatable(
{} :: {
property: number,
},
MyClass
))
function MyClass.new(property: number): ClassType
local self = {
property = property,
}
setmetatable(self, MyClass)
return self
end
function MyClass.addOne(self: ClassType)
self.property += 1
end
return MyClass
Tipos de fundição após protetores lógicos
No momento da redação, o tipo de um valor não é restrito após uma declaração condicional de guarda. Por exemplo, seguindo o guarda abaixo, o tipo de optionalParameter não é restrito a number.
--!estrito
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
print(optionalParameter + 1)
end
Para mitigar isso, novas variáveis são criadas após esses guardas com seu tipo explicitamente lançado.
--!estrito
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
local parameter = optionalParameter :: number
print(parameter + 1)
end
Atravessando Hierarquias do DataModel
Em alguns casos, a base de código precisa atravessar a hierarquia do modelo de dados de uma árvore de objetos que são criados no tempo de execução. Isso apresenta um desafio interessante para a verificação de tipo. No momento da redação, não é possível definir uma hierarquia de modelo de dados genérica como um digitar. Como resultado, há casos em que a única informação de tipo disponível para uma estrutura de modelo de dados é o tipo da instância de nível superior.
Uma abordagem para este desafio é lançar para any e depois refinar. Por exemplo:
local function enableVendor(vendor: Model)
local zonePart: BasePart = (vendor :: any).ZonePart
end
O problema com essa abordagem é que ela afeta a legibilidade. Em vez disso, o projeto usa um módulo genérico chamado getInstance para atravessar as hierarquias do modelo de dados que se projeta para any internamente.
local function enableVendor(vendor: Model)
local zonePart: BasePart = getInstance(vendor, "ZonePart")
end
À medida que a compreensão do motor de tipos do modelo de dados evolui, é possível que padrões como esse não sejam mais necessários.
Interface do Usuário
Plant inclui uma variedade de interfaces de usuário 2D complexas e simples. Estas incluem itens de display de cabeça para cima (HUD) não interativos, como o balcão de moedas e menus interativos complexos, como a comprar.
Abordagem de UI
Você pode comparar livremente a interface do usuário do Roblox com o DOM HTML, porque é uma hierarquia de objetos que descreve o que o usuário deve estar vendo. Abordagens para criar e atualizar uma interface do usuário do Roblox são amplamente divididas em imperativas e práticas declarativas .
Enfoque | Ventajas y Desventajas |
---|---|
Imperativo | En el enfoque imperativo, la interfaz de usuario se trata como cualquier otra jerarquía de instancias en Roblox. La estructura de la interfaz de usuario se crea antes del tiempo de ejecución en Studio y se agrega al aplicación de modeladode datos, típicamente directamente en StarterGui . Luego, en el tiempo de ejecución, el código manipula partes específicas de la interfaz de usuario para reflejar el estado que requiere el creador. Este enfoque viene con algunas ventajas. Puede crear la interfaz de usuario desde cero en Studio y guardarla en el aplicación de modeladode datos. Esta es una experiencia de edición simple y visual que puede acelerar la creacionesde la interfaz de usuario. Debido a que el código de interfaz de usuario imperativo solo se ocupa de lo que necesita cambiar, también hace que los cambios simples en la interfaz de usuario sean fáciles de implementar. Un inconveniente notable es que, ya que los enfoques de interfaz de usuario imperativo requieren que el estado |
Declarativo | En el enfoque declarativo, el estado deseado de las instancias de la interfaz de usuario se declara explícitamente, y la implementación eficiente de este estado se abstrae por bibliotecas como Roact o Fusion . La ventaja de este enfoque es que la implementación del estado se vuelve trivial y solo necesita describir cómo desea que se vea su interfaz de usuario. Esto hace que sea mucho más fácil identificar y resolver errores. El principal inconveniente es tener que declarar todo el árbol de la interfaz de usuario en el código. Bibliotecas como Roact y Fusion tienen sintaxis para hacer esto más fácil, pero sigue siendo un proceso que consume mucho tiempo y una experiencia de edición menos intuitiva al componer la interfaz de usuario. |
Plant usa uma abordagem imperativa sob a noção de que mostrar as transformações diretamente dá uma visão geral mais eficaz de como a interface do usuário é criada e manipulada no Roblox. Isso não seria possível com uma abordagem declarativa. Algumas estruturas e lógicas de interface de usuário repetidas também são abstraídas em componentes reutilizáveis para evitar uma armadilha comum no design de interface de usuário imperativo.
Arquitetura de alto nível
Camada e Componentes
Em Plant, todas as estruturas da interface de usuário são Layer ou Component.
- Layer é definido como um singleton de agrupamento de nível superior que envolve estruturas de interface de usuário pré-fabricadas em ReplicatedStorage . Uma camada pode conter vários componentes ou encapsular sua própria lógica. Exemplos de camadas são o menu de inventário ou o indicador do número de moedas na tela de cabeça para cima.
- Component é um elemento de interface reutilizável. Quando um novo objeto de componente é instanciado, ele clona um modelo pré-fabricado de ReplicatedStorage . Os componentes podem conter outros componentes. Exemplos de componentes são uma classe de botão genérica ou o conceito de uma lista de itens.
Ver manobras
Um problema comum de gerenciamento de UI é o gerenciamento de visualizações. Este projeto tem uma variedade de menus e itens HUD, alguns dos quais ouvem a entrada do usuário, e é necessário gerenciar com cuidado quando eles são visíveis ou habilitados.
Plant aborda este problema com seu UIHandler sistema que gerencia quando uma camada de interface de usuário deve ou não ser visível. Todas as camadas de interface de usuário no jogo são categorizadas como HUD ou Menu e sua visibilidade é gerenciada pelas seguintes regras:
- O estado ativado das camadas Menu e HUD pode ser comutado.
- As camadas HUD ativadas só são mostradas se nenhuma das camadas Menu estiver ativada.
- As camadas Menu ativadas são armazenadas em uma pilha, e apenas uma Menu camada é visível por vez. Quando uma Menu camada é ativada, ela é inserida na frente da pilha e mostrada. Quando uma Menu camada é desativada, ela é removida da pilha e a próxima Menu camada ativada na fila é mostrada.
Essa abordagem é intuitiva porque permite que os menus sejam navegados com o histórico. Se um menu for aberto de outro menu, fechar o novo menu mostrará o menu antigo novamente.
Os singletons de camada de UI se registram com o UIHandler e são fornecidos com um sinal que dispara quando sua visibilidade deve mudar.
Mais Leitura
A partir desta visão geral completa do projeto Plant, você pode explorar os seguintes guias que vão mais em profundidade sobre conceitos e tópicos relacionados.
- Modelo Cliente-Servidor Uma visão geral do modelo cliente-servidor no Roblox.
- Eventos Remotos e Callbacks Tudo sobre eventos de rede remota e callbacks para comunicação através do limite cliente-servidor.
- UI Detalhes sobre objetos de interface de usuário e design no Roblox.