Projeto de referência de planta

*Este conteúdo é traduzido por IA (Beta) e pode conter erros. Para ver a página em inglês, clique aqui.

Planta é uma experiência de referência em que os jogadores plantam e regam sementes, para que possam, mais tarde, colher e vender as plantas resultantes.

Plant project banner

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á notas sobre trocas, compromissos e a razão de várias escolhas de implementação, para que você possa tomar a melhor decisão para suas próprias experiências.

Obtenha o arquivo

  1. Navegue até a página de experiência Planta.
  2. Clique no botão e Editar no Studio .

Usar casos

Planta abrange os seguintes casos de uso:

  • Persistência de dados de sessão e dados do jogador
  • Gerenciamento de visão da UI
  • Rede cliente-servidor
  • Experiência do Usuário pela Primeira Vez (FTUE)
  • Compras de moeda dura e macia

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á associado a um jogador
  • Gerenciando a velocidade de movimento do personagem do jogador
  • Criando um objeto que segue os personagens ao redor
  • Detectar em qual parte do mundo um personagem está

Observe que existem vários casos de uso nesta experiência que são muito pequenos, muito nichados ou não demonstram uma solução para um desafio de design interessante; estes não são abrangidos.

Estrutura do projeto

A primeira decisão ao criar uma experiência é decidir como estruturar o projeto, que principalmente inclui onde colocar instâncias específicas no modelo de dados e como organizar e estruturar pontos de entrada para tanto o código do cliente quanto do servidor.

modelode dados

A tabela a seguir descreve em quais serviços de contêineres nas instâncias do modelo de dados são colocados.

ServiçoTipos de instâncias
Workspace

Contém modelos estáticos que representam o mundo 3D, especificamente partes do mundo que não pertencem a nenhum jogador.Você não precisa Criar, modificar ou destruir dinamicamente essas instâncias em tempo de execução, então é aceitável deixá-las aqui.:

Também há um vazio Folder , ao qual os modelos de fazenda dos jogadores serão adicionados em tempo de execução.

Lighting

Efeitos atmosféricos e de iluminação.

ReplicatedFirst

Contém o menor conjunto possível de instâncias necessárias para exibir a tela de carregamento e inicializar o jogo.Quanto mais instâncias forem colocadas em ReplicatedFirst, mais tempo demorará para que elas se replicam antes que o código em ReplicatedFirst possa ser executar.:

  • Na pasta Instâncias existe a Interface gráfica do usuáriode carregamento.:
  • Na pasta Fonte existe o código da tela de carregamento e o código necessário para esperar que o resto do jogo carregar.O start``Class.LocalScript é o ponto de entrada para todo o código do lado do cliente no projeto.
ReplicatedStorage

Serve como um contêiner de armazenamento para todas as instâncias para as quais é necessário acesso tanto no cliente quanto no servidor.:

  • Na pasta Dependências existe algumas bibliotecas de terceiros usadas pelo projeto.:
  • Na pasta Instâncias existem uma ampla gama de instâncias pré-fabricadas usadas por várias classes no jogo.:
  • Na pasta Fonte existe todo o código não necessário para o processo de carregamento que precisa ser acessível tanto do cliente quanto do servidor.
ServerScriptService

Contém um servindo como ponto de entrada para todo o código do lado do servidor no projeto.

ServerStorage

Serve como um contêiner de armazenamento para todas as instâncias que não precisam ser replicadas para o cliente.:

  • Na pasta Instâncias existe um modelo de modelo Fazenda .Uma cópia disso é colocada em Workspace quando o jogador se junta ao jogo, onde será replicada para todos os jogadores.:
  • Na pasta Fonte existe todo o código que é exclusivo do servidor.
SoundService

Contém os objetos Sound usados para efeitos sonoros no jogo.Abaixo de SoundService , esses objetos Sound não têm posição e não são simulados no espaço 3D.

Pontos de entrada

A maioria dos projetos organiza código dentro de reutilizável ModuleScripts que pode ser importado em toda a base de código.ModuleScripts são reutilizáveis, mas não executam por conta 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 pertencente a um comportamento ou sistema específico no jogo, criando vários pontos de entrada.

Para o microjogo Planta, 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 de seus requisitos, mas um único ponto de entrada fornece maior controle sobre a ordem em que os sistemas são executados.

As listas a seguir descrevem as compensações 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 alto nível de sistemas

Os sistemas de alto nível no projeto estão detalhados abaixo.Alguns desses sistemas são substancialmente mais complexos do que outros, e, em muitos casos, sua funcionalidade é abstrata através de uma hierarquia de outras Classes.

Plant project systems architecture diagram

Cada um desses sistemas é um "único", pois é uma classe não instantânea que é inicializada em vez disso pelo script relevante do cliente ou do servidor start.Você pode ler mais sobre o padrão solteiro mais tarde neste guia.

Servidor

Os seguintes sistemas estão associados ao servidor.

SistemaDescrição
Rede
  • Cria todas as RemoteEvent e RemoteFunction instâncias.:
  • Exibe métodos para enviar e ouvir mensagens do cliente.:
  • Validação de tipo para argumentos recebidos do cliente em tempo de execução.
Servidor de Dados do Jogador
  • Salva e carrega dados persistentes do jogador usando DataStoreService .:
  • Armazena dados do jogador na memória e replica mutações para o cliente.:
  • Exibe sinais e métodos para assinar, consultar e atualizar dados do jogador.
Mercado
  • Gerencia transações de moeda suave do cliente.
  • Exibe um método para vender plantas colhidas. >
Gerente de Grupo de Colisão
  • Atribui modelos de personagem de jogador a grupos de colisão.:
  • Configura grupos de colisão para que os personagens dos jogadores não possam colidir com vagões de plantas.
Servidor de Gerenciamento de Fazenda
  • Recria o modelo de fazenda de um jogador a partir de seus dados de jogador quando eles se juntam ao jogo.:
  • Remove o modelo de fazenda quando um jogador sai.:
  • Atualiza os dados do jogador quando a fazenda de um jogador é alterada.:
  • Exibe um método para acessar a classe Fazenda associada a um dado jogador.
Contêiner de Objetos de Jogador
  • Cria vários objetos associados à vida útil de um jogador e fornece um método para recuperá-los.
Jogadores de Etiqueta
Servidor de Gerenciamento de Ftue
  • Durante o FTUE, executa cada etapa e aguarda que ela se complete.
Gerador de Personagens
  • Respawna personagens quando eles morrem.Observe que Players.CharacterAutoLoads foi desativado para que o spawning seja pausado até que os dados do jogador sejam carregados.

Cliente

Os seguintes sistemas estão associados ao cliente.

SistemaDescrição
Rede
  • Aguarda o servidor criar todas as RemoteEvent e RemoteFunction instâncias.:
  • Exibe métodos para enviar e ouvir mensagens para e do servidor.:
  • Garante a validação do tipo de parâmetro de tempo de execução.:
  • Executa pcall() em funções remotas.
Cliente de Dados de Jogador
  • Armazena os dados do jogador local na memória.:
  • Exibe métodos e sinais para consultar e se inscrever em alterações nos dados do jogador. >
Cliente do Mercado
  • Exibe um método para solicitar ao servidor a compra de um item por moeda suave.
Gerenciador de Pulo de Caminhada Local
  • Exibe métodos para modificar o WalkSpeed ou JumpHeight de um personagem através de multiplicadores para evitar conflitos ao modificar esses valores de vários lugares.
Cliente de Gerenciador de Fazenda
  • Ouve por tags específicas CollectionService sendo aplicadas a instâncias e cria "componentes" que adicionam comportamento a essas instâncias.Um " componente " refere-se a uma classe que é criada quando uma etiqueta CollectionService é adicionada a uma instância e destruída quando é removida; estes são usados para os prompts de CTA na fazenda e várias classes que teletransportam o estado da fazenda para o jogador.
Configuração de UI
  • Inicializa todas as camadas de UI.:
  • Configura certas camadas para serem visíveis apenas em seções físicas do mundo do jogo.:
  • Conecta um efeito de câmera especial para quando os menus estiverem habilitados.
FtueManagerClient
  • Configura estágios de FTUE no cliente.
Corrida de Personagem
  • Usa LocalWalkJumpManager para aumentar WalkSpeed quando um personagem de jogador está fora de sua fazenda.

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 ao servidor uma determinada 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 para 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 atributos.
  • Replicação via tags.
  • Mensageria diretamente através do módulo Rede .

Replicação via sistema de dados do jogador

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 se inscrever em alterações, tornando-o ideal para replicar alterações no estado do jogador do servidor para o cliente.

Por exemplo, em vez de disparar um personalizado UpdateCoins``Class.RemoteEvent para dizer ao cliente quantas moedas ele tem, você pode chamar o seguinte e deixar o cliente se inscrever nele através do evento PlayerDataClient.updated.


PlayerDataServer:setValue(player, "coins", 5)

Claro, isso só é útil para a replicação do servidor para o cliente e para valores que você deseja persistir entre as sessões, mas isso se aplica a um número surpreendente de casos no projeto, incluindo:

  • A fase atual do FTUE
  • O inventário do 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 é específico para um determinado Instance, você pode usar atributos.O Roblox replica automaticamente os valores de 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 ocorre ao lado da própria instância.

Isso é particularmente útil para instâncias criadas em tempo de execução, pois os atributos definidos em uma nova instância antes de serem parentados para o modelo de dados se replicarão atomicamente com a própria instância.Isso contorna qualquer necessidade de escrever código para "esperar" que dados adicionais sejam replicados via um RemoteEvent ou StringValue.

Você também pode ler diretamente os atributos do modelo de dados, do cliente ou do servidor, com o método GetAttribute(), e se inscrever em alterações com o método GetAttributeChangedSignal().No projeto Planta, esta abordagem é usada para, entre outras coisas, replicar o status atual das plantas para os clientes.

Replicação via tags

CollectionService Permite que você aplique uma tag de corda a um Instance . Isso é útil para categorizar instâncias e replicar essa categorização para o cliente.

Por exemplo, a etiqueta CanPlant é aplicada no servidor para sinalizar ao cliente que um determinado vaso é capaz de receber uma planta.

Mensagem diretamente via módulo de rede

Para situações em que nenhuma das opções anteriores se aplique, você pode usar chamadas de rede personalizadas através do módulo Rede .Esta é a única opção no projeto que permite a comunicação cliente-servidor e, portanto, é mais útil para transmitir solicitações do cliente e receber uma resposta do servidor.

Planta usa chamadas diretas de rede para uma variedade de solicitações de clientes, incluindo:

  • Regando uma planta
  • Plantando uma semente
  • Comprar 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 do servidor para o cliente.

Classe e singletons

Classes no projeto Planta podem ser criadas e destruídas.Sua síntese de classe é inspirada pela abordagem idiomática do Lua à programação orientada por objetos com uma série de alterações para habilitar o Suportea verificação de tipo rigorosa.

Instantância

Muitas classes no projeto estão associadas a uma ou mais Instances .Objetos de uma classe dada são criados usando um método new(), consistente com 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 é que cria um objeto entre dois objetos dados e mantém esses anexos orientados para que o feixe sempre esteja voltado para cima.Essas instâncias podem ser clonadas a partir de uma versão pré-fabricada em ReplicatedStorage ou passadas para new() como um argumento e armazenadas dentro do objeto sob 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 armazenada sob ReplicatedStorage ou ServerStorage.Embora fosse possível serializar as propriedades dessas instâncias e criá-las do zero nas funções da classe new(), fazer isso tornaria a edição dos objetos muito complicada e os tornaria mais difíceis de serem analisados por um leitor.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 em tempo de execução.

Composição

Embora a herança seja possível em Luau 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 sob self .

Para um exemplo disso em ação, veja a classe CloseButton que envolve a classe Button.

Limpar

Semelhante ao modo como um Instance pode ser destruído com o método Destroy(), as classes que podem ser instanciadas também podem ser destruídas.O método destrutor para classes de projeto é com uma minúscula para consistência entre os métodos do código-base, bem como para distinguir entre as classes do projeto e as instâncias do Roblox.

O papel do método destroy() é destruir quaisquer instâncias criadas pelo Objeto, desconectar quaisquer conexões e chamar destroy() em qualquer objeto filho.Isso é particularmente importante para conexões porque instâncias com conexões ativas não são limpas pelo coletor de lixo Luau, mesmo que não restem referências à instância ou conexões para 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á-la em volta no código Luau, Planta aproveita o fato de que exigir um ModuleScript cache seu valor retornado.Isso significa que exigir o mesmo singleton ModuleScript de diferentes locais consistentemente fornece o mesmo Objetoretornado.A única exceção a essa regra seria se diferentes ambientes (cliente ou servidor) acessassem o ModuleScript.

Singletons são distintos de classes instáveis pelo fato de não terem um método new() .Em vez disso, o objeto junto com seus métodos e estado é retornado diretamente via o ModuleScript .Como singletons não são instanciados, a síntese self não é usada e os métodos são chamados em vez disso com um ponto ( . ) em vez de um ponto ( : ).

Inferência de tipo rigorosa

Luau suporta digitação gradual, o que significa que você está livre para adicionar definições de tipo opcional a algum ou a todos os seus códigos.Neste projeto, strict verificação de tipo é usada para cada script.Esta é a opção menos permissiva para a ferramenta de análise de scripts do Roblox e, portanto, a mais provável de detectar erros de tipo antes da tempo de execução.

Síntese de classe tipada

A abordagem estabelecida para criar classes em Lua é bem documentada, no entanto, não é bem adequada para tipagem forte de Luau.Em Luau, a abordagem mais simples para obter o tipo de uma classe é o método typeof():


type ClassType = typeof(Class.new())

Isso funciona, mas não é muito útil quando sua classe é iniciada com valores que só existem em tempo de execução, por exemplo Player.Além disso, a suposição feita na síntax de classe idiomática em Lua é que declarar um método em uma classe self sempre será uma instância dessa classe; essa não é uma suposição que o mecanismo de inferência de tipo pode fazer.

A fim de suportar a inferência de tipo rigoroso, o projeto Planta usa uma solução que difere da síntese de classe Lua idiomática 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 manutenibilidade, mas avisos serão marcados se as duas definições saírem de sincronia umas com as outras.
  • Métodos de classe são declarados com um ponto, então self pode ser declarado explicitamente de tipo ClassType.Métodos ainda podem ser chamados com um colon, como esperado.

--!stringente
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 lançamento após guardas lógicas

No momento da escrita, o tipo de um valor não é restringido após uma declaração condicional de guarda.Por exemplo, após o guarda abaixo, o tipo de optionalParameter não é restringido a number.


--!stringente
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 cast.


--!stringente
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
local parameter = optionalParameter :: number
print(parameter + 1)
end

Transpor as hierarquias do Modelo de Dados

Em alguns casos, o código precisa percorrer a hierarquia do modelo de dados de uma árvore de objetos que são criados em tempo de execução.Isso apresenta um desafio interessante para a verificação de tipo.No momento da escrita, não é possível definir uma hierarquia de modelo de dados genérica como um digitar.Como resultado, existem 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 esse 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 impacta a legibilidade.Em vez disso, o projeto usa um módulo genérico chamado getInstance para percorrer hierarquias de modelo de dados que lançam para any internamente.


local function enableVendor(vendor: Model)
local zonePart: BasePart = getInstance(vendor, "ZonePart")
end

À medida que a compreensão do tipo do motor de dados evolui, é possível que padrões como este não sejam mais necessários.

Interface de usuário

Planta inclui uma variedade de interfaces de usuário 2D complexas e simples.Estes incluem itens de exibição não interativa (HUD) como o contador de moedas e menus interativos complexos como a comprar.

Aproximação de UI

Você pode comparar de forma desconectada a interface Roblox UI com o DOM HTML, porque é uma hierarquia de objetos que descreve o que o usuário deve estar vendo.As abordagens para criar e atualizar uma interface de usuário do Roblox estão amplamente divididas em imperativas e declarativas práticas.

AproximaçãoVantagens e desvantagens
Imperativo

Na abordagem imperativa, a UI é tratada como qualquer outra hierarquia de instâncias no Roblox.A estrutura da interface de usuário é criada antes do tempo de execução no Studio e adicionada ao modelo de dados, geralmente diretamente em StarterGui.Então, na hora de executar, o código manipula peças específicas da interface para refletir o estado que o criador requer.:

Essa abordagem vem com algumas vantagens.Você pode criar a interface de usuário do zero no Studio e armazená-la no modelo de dados.Esta é uma experiência de edição simples e visual que pode acelerar a criaçõesde UI.Porque o código de UI imperativo só se preocupa com o que precisa ser alterado, também torna as alterações simples na interface fáceis de implementar.:

Uma desvantagem notável é que, uma vez que abordagens imperativas de UI exigem que o estado seja implementado manualmente na forma de transformações, representações complexas de estado podem se tornar muito difíceis de encontrar e depurar.É comum que erros surjam ao desenvolver código imperativo da interface, especialmente quando o estado e a interface se tornam des sincronizados devido a várias atualizações interagindo em uma ordem inesperada.:

Outro desafio com abordagens imperativas é que é mais difícil dividir a interface em componentes significativos que podem ser declarados uma vez e reutilizados.Como toda a árvore de UI é declarada no momento de edição, padrões comuns podem ser repetidos em várias partes do modelo de dados.

Declarativo

Na abordagem declarativa, o estado desejado de instâncias de UI são declarados explicitamente e a implementação eficiente desse estado é abstrata por bibliotecas como Roact ou Fusion.:

A vantagem dessa abordagem é a implementação do estado se tornar trivial e você só precisa descrever o que você quer que sua interface tenha.Isso torna a identificação e resolução de bugs significativamente mais fácil.:

A principal desvantagem é ter que declarar toda a árvore de UI em código.Bibliotecas como Roact e Fusion têm síntese para tornar isso mais fácil, mas ainda é um processo demorado e uma experiência de edição menos intuitiva ao compor a interface de usuário.

Planta usa uma abordagem imperativa sob a noção de que mostrar as transformações diretamente dá uma visão mais eficaz de como a UI é criada e manipulada no Roblox.Isso não seria possível com uma abordagem declarativa.Algumas estruturas e lógica de UI repetidas também são abstratas em componentes reutilizáveis para evitar um problema comum no design de UI imperativo.

Arquitetura de alto nível

Plant project UI architecture diagram

Camada e componentes

Em Planta, todas as estruturas de UI são um Layer ou um Component.

  • Layer é definido como um grupo de nível superior que envolve estruturas de UI pré-fabricadas em ReplicatedStorage .Uma camada pode conter um número de componentes ou pode encapsular toda a sua própria lógica.Exemplos de camadas são o menu de inventário ou o indicador de número de moedas na exibição de cabeçalho.
  • Component é um elemento de UI reutilizável.Quando um novo objeto de componente é instanciado, ele clona um modelo pré-fabricado de ReplicatedStorage .Os componentes podem, em si, conter outros componentes.Exemplos de componentes são uma classe de botão genérica ou o conceito de uma lista de itens.

Visualizar manipulação

Um problema comum de gerenciamento de UI é o manuseio de visualização.Este projeto tem uma variedade de menus e itens do HUD, alguns dos quais ouvem a entrada do usuário e é necessária uma gestão cuidadosa de quando eles são visíveis ou ativados.

Planta aborda esse problema com seu sistema UIHandler que gerencia quando uma camada de UI deve ou não deve ser visível.Todas as camadas de UI no jogo são categorizadas como HUD ou Menu e sua visibilidade é gerenciada pelas seguintes regras:

  • O estado habilitado de Menu e HUD camadas pode ser alternado.
  • Camadas habilitadas HUD são mostradas somente se nenhuma camada Menu estiver habilitada.
  • Camadas habilitadas Menu são armazenadas em uma pilha e apenas uma camada Menu é visível de cada vez.Quando uma camada Menu é habilitada, ela é inserida na frente da pilha e mostrada.Quando uma camada Menu é desativada, ela é removida da pilha e a próxima camada habilitada Menu na fila é mostrada.

Essa abordagem é intuitiva porque permite que os menus sejam navegados com histórico.Se um menu for aberto a partir de outro menu, fechar o novo menu mostrará o menu antigo novamente.

Singletons de camada de UI se registram com o UIHandler e são fornecidos com um sinal que dispara quando sua visibilidade deve mudar.

Leitura adicional

A partir desta abrangente visão geral do projeto Planta, você pode querer explorar os seguintes guias que vão mais a fundo em conceitos e tópicos relacionados.

  • Modelo Cliente-Servidor — Um resumo do modelo cliente-servidor no Roblox.
  • Luau — Detalhes sobre Luau , o idioma de programação criado pelo Roblox descendente de Lua 5.1.
  • Eventos e chamadas remotas — Tudo sobre eventos e chamadas de rede remotas para comunicação através da fronteira cliente-servidor.
  • UI — Detalhes sobre objetos de interface do usuário e design no Roblox.