Plant es una experiencia de referencia donde los jugadores siembran y riegan semillas, para poder cosechar y vender las plantas resultantes.

El proyecto se centra en casos de uso comunes que puedes encontrar al desarrollar una experiencia en Roblox. Donde sea aplicable, encontrarás notas sobre compensaciones, compromisos y la lógica detrás de varias elecciones de implementación, para que puedas tomar la mejor decisión para tus propias experiencias.
Obtén el archivo
- Navega a la página de experiencia de Plant.
- Haz clic en el botón ⋯ y Editar en Studio.
Casos de uso
Plant cubre los siguientes casos de uso:
- Persistencia de datos de sesión y datos de jugador
- Gestión de vistas de UI
- Redes cliente-servidor
- Experiencia de Primer Usuario (FTUE)
- Compras de moneda dura y blanda
Además, este proyecto resuelve conjuntos más reducidos de problemas que son aplicables a muchas experiencias, incluyendo:
- Personalización de un área en el lugar que está asociada con un jugador
- Gestión de la velocidad de movimiento del personaje del jugador
- Creación de un objeto que sigue a los personajes
- Detección de en qué parte del mundo se encuentra un personaje
Ten en cuenta que hay varios casos de uso en esta experiencia que son demasiado pequeños, demasiado nicho o no demuestran una solución a un desafío de diseño interesante; estos no están cubiertos.
Estructura del proyecto
La primera decisión al crear una experiencia es decidir cómo estructurar el proyecto, que incluye principalmente dónde colocar instancias específicas en el modelo de datos y cómo organizar y estructurar los puntos de entrada tanto para el código del cliente como para el del servidor.
Modelo de datos
La siguiente tabla describe en qué servicios contenedor se colocan las instancias del modelo de datos.
| 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 necesitas crear, modificar o destruir dinámicamente estas instancias en tiempo de ejecución, así que es aceptable dejarlas aquí. También hay una Folder vacía, a la que se añadirán los modelos de granja 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 la experiencia. Cuantas más instancias se coloquen en ReplicatedFirst, más tiempo se tardará en replicarlas antes de que se ejecute el código en ReplicatedFirst.
|
| ReplicatedStorage | Sirve como un contenedor de almacenamiento para todas las instancias que requieren acceso tanto en el cliente como en el servidor.
|
| ServerScriptService | Contiene un Script que sirve como el punto de entrada para todo el código del lado del servidor en el proyecto. |
| ServerStorage | Sirve como un contenedor de almacenamiento para todas las instancias que no necesitan ser replicadas al cliente.
|
| SoundService | Contiene los objetos Sound utilizados para efectos de sonido en la experiencia. Bajo SoundService, estos objetos Sound no tienen posición y no se simulan en el espacio 3D. |
Puntos de entrada
La mayoría de los proyectos organizan el código dentro de ModuleScripts reutilizables que se pueden importar en toda la base de código. ModuleScripts son reutilizables pero no se ejecutan por sí solos; necesitan ser importados por un Script o un LocalScript. Muchos proyectos de Roblox tendrán un gran número de objetos Script y LocalScript, cada uno referente a un comportamiento o sistema particular en la experiencia, creando múltiples puntos de entrada.
Para el microjuego Plant, se implementa un enfoque diferente a través de un solo LocalScript que es el punto de entrada para todo el código del cliente, y un solo Script que es el punto de entrada para todo el código del servidor. El enfoque correcto para tu proyecto depende de tus requisitos, pero un solo punto de entrada proporciona un mayor control sobre el orden en que se ejecutan los sistemas.
Las siguientes listas describen las compensaciones de ambos enfoques:
- Un único Script y un único LocalScript cubren el código del servidor y del cliente respectivamente.
- Mayor control sobre el orden en que se inician los diferentes sistemas porque todo el código se inicializa desde un solo script.
- Se pueden pasar objetos por referencia entre sistemas.
Arquitectura de sistemas de alto nivel
Los sistemas de nivel superior en el proyecto se detallan a continuación. Algunos de estos sistemas son sustancialmente más complejos que otros, y en muchos casos su funcionalidad se abstrae a través de una jerarquía de otras clases.

Cada uno de estos sistemas es un "singleton", en el sentido de que es una clase no instanciable que se inicializa mediante el script start pertinente del cliente o del servidor. Puedes leer más sobre el patrón singleton más adelante en esta guía.
Servidor
Los siguientes sistemas están asociados con el servidor.
| Sistema | Descripción |
|---|---|
| Red |
|
| PlayerDataServer |
|
| Mercado |
|
| CollisionGroupManager |
|
| FarmManagerServer |
|
| PlayerObjectsContainer |
|
| TagPlayers |
|
| FtueManagerServer |
|
| CharacterSpawner |
|
Cliente
Los siguientes sistemas están asociados con el cliente.
| Sistema | Descripción |
|---|---|
| Red |
|
| PlayerDataClient |
|
| MarketClient |
|
| LocalWalkJumpManager |
|
| FarmManagerClient |
|
| UISetup |
|
| FtueManagerClient |
|
| CharacterSprint |
|
Comunicación cliente-servidor
La mayoría de las experiencias de Roblox involucran algún elemento de comunicación entre el cliente y el servidor. Esto puede incluir que el cliente solicite al servidor realizar una cierta acción y el servidor replicar actualizaciones al cliente.
En este proyecto, la comunicación cliente-servidor se mantiene lo más genérica posible limitando el uso de objetos RemoteEvent y RemoteFunction a fin de disminuir la cantidad de reglas especiales a tener en cuenta. Este proyecto utiliza los siguientes métodos, en orden de preferencia:
- Replicación a través del sistema de datos del jugador.
- Replicación a través de atributos.
- Replicación a través de etiquetas.
- Mensajería directamente a través del módulo Red.
Replicación a través del sistema de datos del jugador
El sistema de datos del jugador permite asociar datos con el jugador que persisten entre sesiones de guardado. Este sistema proporciona replicación del cliente al servidor y un conjunto de API que se pueden usar para consultar datos y suscribirse a cambios, lo que lo hace ideal para replicar cambios en el estado del jugador desde el servidor al cliente.
Por ejemplo, en lugar de disparar un UpdateCoins RemoteEvent para decirle al cliente cuántas monedas tiene, puedes llamar a lo siguiente y dejar que el cliente se suscriba a ello a través del evento PlayerDataClient.updated.
PlayerDataServer.setValue(player, "coins", 5)
Por supuesto, esto solo es útil para la replicación del servidor al cliente y para valores que deseas que persistan entre sesiones, pero esto se aplica a un sorprendente número de casos en el proyecto, incluyendo:
- La etapa actual del FTUE
- El inventario del jugador
- La cantidad de monedas que tiene el jugador
- El estado de la granja del jugador
Replicación a través de atributos
En situaciones donde el servidor necesita replicar un valor personalizado al cliente que es específico de una determinada Instance, puedes usar atributos. Roblox replica automáticamente los valores de atributos, por lo que no necesitas mantener ninguna ruta de código para replicar el estado asociado a un objeto. Otra ventaja es que esta replicación ocurre junto con la propia instancia.
Esto es particularmente útil para instancias creadas en tiempo de ejecución, ya que los atributos establecidos en una nueva instancia antes de que se parenten al modelo de datos se replicarán de forma atómica con la propia instancia. Esto elude cualquier necesidad de escribir código para "esperar" que se replique información adicional a través de un RemoteEvent o StringValue.
También puedes leer atributos directamente desde el modelo de datos, tanto desde el cliente como desde el servidor, con el método GetAttribute() método y suscribirte a cambios con el GetAttributeChangedSignal() método. En el proyecto Plant, este enfoque se utiliza para, entre otras cosas, replicar el estado actual de las plantas a los clientes.
Replicación a través de etiquetas
CollectionService te permite aplicar una etiqueta de cadena a una Instance. Esto es útil para categorizar instancias y replicar esa categorización al cliente.
Por ejemplo, la etiqueta CanPlant se aplica en el servidor para significar al cliente que una maceta determinada puede recibir una planta.
Mensaje directamente a través del módulo de red
Para situaciones en las que ninguna de las opciones anteriores se aplica, puedes usar llamadas de red personalizadas a través del módulo Red. Esta es la única opción en el proyecto que permite la comunicación del cliente al servidor y, por lo tanto, es más útil para transmitir solicitudes del cliente y recibir una respuesta del servidor.
Plant utiliza llamadas de red directas para una variedad de solicitudes del cliente, incluyendo:
- Regar una planta
- Plantar una semilla
- Comprar un artículo
El inconveniente de este enfoque es que cada mensaje individual requiere una configuración personalizada que puede aumentar la complejidad del proyecto, aunque esto se ha evitado siempre que ha sido posible, particularmente para la comunicación del servidor al cliente.
Clases y singletons
Las clases en el proyecto Plant, al igual que las instancias en Roblox, pueden ser creadas y destruidas. Su sintaxis de clase se inspira en el enfoque idiomático de Lua para la programación orientada a objetos con una serie de cambios para habilitar el soporte de verificación de tipo estricta.
Instanciación
Muchas clases en el proyecto están asociadas con una o más Instances. Los objetos de una clase dada se crean usando un método new(), consistente con la forma en que se crean las instancias en Roblox utilizando Instance.new().
Este patrón se utiliza generalmente para objetos donde la clase tiene una representación física en el modelo de datos, y la clase extiende su funcionalidad. Un buen ejemplo es BeamBetween, que crea un objeto Beam entre dos objetos Attachment dados y mantiene esas uniones orientadas para que el haz siempre esté mirando hacia arriba. Estas instancias podrían ser clonadas desde una versión prefabricada en ReplicatedStorage o pasadas a new() como un argumento y almacenadas dentro del objeto bajo self.
Instancias correspondientes
Como se mencionó anteriormente, muchas clases en este proyecto tienen una representación en el modelo de datos, una instancia que corresponde con la clase y es manipulada por ella.
En lugar de crear estas instancias cuando se instancia un objeto de clase, el código generalmente opta por Clone() una versión prefabricada de la Instance almacenada bajo ReplicatedStorage o ServerStorage. Aunque sería posible serializar las propiedades de estas instancias y crearlas desde cero en las funciones new() de la clase, hacerlo haría que editar los objetos fuera muy engorroso y más difícil de entender para un lector. Además, clonar una instancia es generalmente una operación más rápida que crear una nueva instancia y personalizar sus propiedades en tiempo de ejecución.
Composición
Aunque la herencia es posible en Luau utilizando metatables, el proyecto opta por permitir que las clases se extiendan entre sí a través de composición. Al combinar clases a través de composición, el objeto "hijo" se instancia en el método new() de la clase y se incluye como un miembro bajo self.
Para un ejemplo de esto en acción, consulta la clase CloseButton que envuelve la clase Button.
Limpieza
Similar a cómo una Instance puede ser destruida con el método Destroy(), las clases que pueden ser instanciadas también pueden ser destruidas. El método destructor para las clases del proyecto es destroy() con una d minúscula para mantener la consistencia de camelCase a través de los métodos de la base de código, así como para distinguir entre las clases del proyecto y las instancias de Roblox.
El papel del método destroy() es destruir cualquier instancia creada por el objeto, desconectar cualquier conexión y llamar a destroy() en cualquier objeto hijo. Esto es particularmente importante para las conexiones porque las instancias con conexiones activas no son limpiadas por el recolector de basura de Luau, incluso si no hay referencias a la instancia o conexiones a la instancia restantes.
Singletons
Los singletons, como su nombre lo indica, son clases para las cuales solo un objeto puede existir. Son el equivalente del proyecto a los Servicios de Roblox. En lugar de almacenar una referencia al objeto singleton y pasarla en el código de Luau, Plant aprovecha el hecho de que requerir un ModuleScript guarda en caché su valor devuelto. Esto significa que requerir el mismo ModuleScript singleton desde diferentes lugares proporciona de manera consistente el mismo objeto devuelto. La única excepción a esta regla sería si diferentes entornos (cliente o servidor) acceden al ModuleScript.
Los singletons se distinguen de las clases instanciables por el hecho de que no tienen un método new(). En su lugar, el objeto junto con sus métodos y estado se devuelven directamente a través del ModuleScript. Como los singletons no se instancian, no se usa la sintaxis self y los métodos se llaman en su lugar con un punto (.) en lugar de dos puntos (:).
Inferencia de tipos estricta
Luau admite tipado gradual, lo que significa que eres libre de añadir definiciones de tipo opcionales a algunos o todos tus códigos. En este proyecto, se utiliza verificación de tipo strict para cada script. Esta es la opción menos permisiva para la herramienta de Análisis de Scripts de Roblox y, por lo tanto, la más propensa a capturar errores de tipo antes de tiempo de ejecución.
Sintaxis de clase tipada
El enfoque establecido para crear clases en Lua está bien documentado, sin embargo, no se adapta bien al tipado fuerte en Luau. En Luau, el enfoque más simple para obtener el tipo de una clase es el método typeof():
type ClassType = typeof(Class.new())
Esto funciona, pero no es muy útil cuando tu clase se inicia con valores que solo existen en tiempo de ejecución, por ejemplo, los objetos Player. Además, la suposición hecha en la sintaxis de clase idiomática de Lua es que declarar un método en self siempre será una instancia de esa clase; esta no es una suposición que el motor de inferencia de tipos pueda hacer.
Para poder soportar la inferencia de tipos estricta, el proyecto Plant utiliza una solución que difiere de la sintaxis de clase idiomática de Lua en varias maneras, algunas de las cuales pueden parecer no intuitivas:
- La definición de self se duplica, tanto en la declaración de tipo como en el constructor. Esto introduce una carga de mantenibilidad, pero se señalarán advertencias si las dos definiciones no están sincronizadas.
- Los métodos de clase se declaran con un punto, de modo que self puede ser explícitamente declarado como de tipo ClassType. Los métodos aún pueden ser llamados con dos puntos como se espera.
--!strict
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 casting después de guardas lógicas
En el momento de escribir este documento, el tipo de un valor no se reduce después de una declaración de guardia condicional. Por ejemplo, después de la guardia a continuación, el tipo de optionalParameter no se reduce a number.
--!strict
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
print(optionalParameter + 1)
end
Para mitigar esto, se crean nuevas variables después de estas guardas con su tipo expresamente convertido.
--!strict
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
local parameter = optionalParameter :: number
print(parameter + 1)
end
Traversar jerarquías de DataModel
En algunos casos, la base de código necesita recorrer la jerarquía del modelo de datos de un árbol de objetos que se crean en tiempo de ejecución. Esto presenta un desafío interesante para la verificación de tipos. En el momento de escribir, no es posible definir una jerarquía de modelo de datos genérica como tipo. Como resultado, hay casos donde la única información de tipo disponible para una estructura de modelo de datos es el tipo de la instancia de nivel superior.
Un enfoque para este desafío es convertir a any y luego refinar. Por ejemplo:
local function enableVendor(vendor: Model)
local zonePart: BasePart = (vendor :: any).ZonePart
end
El problema con este enfoque es que impacta la legibilidad. En su lugar, el proyecto usa un módulo genérico llamado getInstance para recorrer jerarquías de modelo de datos que convierte a any internamente.
local function enableVendor(vendor: Model)
local zonePart: BasePart = getInstance(vendor, "ZonePart")
end
A medida que la comprensión del motor de tipos sobre el modelo de datos evoluciona, es posible que patrones como este ya no sean necesarios.
Interfaz de usuario
Plant incluye una variedad de interfaces de usuario 2D complejas y sencillas. Estas incluyen elementos de visualización no interactivos como el contador de monedas y menús interactivos complejos como la tienda.
Enfoque de UI
Puedes comparar de forma flexible la UI de Roblox con el DOM de HTML, porque es una jerarquía de objetos que describe lo que el usuario debería estar viendo. Los enfoques para crear y actualizar una UI de Roblox se dividen ampliamente en prácticas imperativas y declarativas.
| Enfoque | Ventajas y desventajas |
|---|---|
| Imperativo | En el enfoque imperativo, la UI se trata como cualquier otra jerarquía de instancias en Roblox. La estructura de la UI se crea antes del tiempo de ejecución en Studio y se agrega al modelo de datos, típicamente directamente en StarterGui. Luego, en tiempo de ejecución, el código manipula partes específicas de la UI para reflejar el estado que el creador requiere. Este enfoque tiene algunas ventajas. Puedes crear la UI desde cero en Studio y almacenarla en el modelo de datos. Esta es una experiencia de edición simple y visual que puede acelerar la creación de la UI. Debido a que el código de UI imperativo solo se ocupa de lo que necesita cambiar, también hace que los cambios simples en la UI sean fáciles de implementar. Un inconveniente notable es que, dado que los enfoques de UI imperativos requieren que el estado sea implementado manualmente en forma de transformaciones, representaciones complejas de estado pueden volverse muy difíciles de encontrar y depurar. Es común que surjan errores al desarrollar código de UI imperativa, especialmente cuando el estado y la UI se desincronizan debido a múltiples actualizaciones que interactúan en un orden inesperado. Otro desafío con los enfoques imperativos es que es más difícil descomponer la UI en componentes significativos que se pueden declarar una vez y reutilizar. Dado que todo el árbol de la UI se declara en el tiempo de edición, patrones comunes pueden repetirse en múltiples partes del modelo de datos. |
| Declarativo | En el enfoque declarativo, el estado deseado de las instancias de UI se declara explícitamente, y la implementación eficiente de este estado se abstrae mediante bibliotecas como Roact o Fusion. La ventaja de este enfoque es que la implementación del estado se vuelve trivial y solo necesitas describir cómo deseas que luzca tu UI. Esto hace que identificar y resolver errores sea significativamente más fácil. La principal desventaja es tener que declarar todo el árbol de la UI en código. Bibliotecas como Roact y Fusion tienen sintaxis para facilitar esto, pero sigue siendo un proceso que consume tiempo y una experiencia de edición menos intuitiva al componer la UI. |
Plant utiliza un enfoque imperativo bajo la noción de que mostrar las transformaciones directamente proporciona una visión más efectiva de cómo se crea y manipula la UI en Roblox. Esto no sería posible con un enfoque declarativo. Algunas estructuras y lógicas de UI repetidas también se abstraen en componentes reutilizables para evitar un problema común en el diseño de UI imperativa.
Arquitectura de alto nivel

Capa y componentes
En Plant, todas las estructuras de UI son o una Layer o un Component.
- Layer se define como un singleton de agrupación de nivel superior que envuelve estructuras de UI prefabricadas en ReplicatedStorage. Una capa puede contener una serie de componentes, o puede encapsular su propia lógica por completo. Ejemplos de capas son el menú de inventario o el indicador de cantidad de monedas en la visualización.
- Component es un elemento de UI reutilizable. Cuando se instancia un nuevo objeto componente, clona una plantilla prefabricada de ReplicatedStorage. Los componentes pueden contener a su vez otros componentes. Ejemplos de componentes son una clase de botón genérico o el concepto de una lista de elementos.
Manejo de vistas
Un problema común en la gestión de UI es el manejo de vistas. Este proyecto tiene una gama de menús y elementos de HUD, algunos de los cuales escuchan la entrada del usuario, y se requiere un manejo cuidadoso de cuándo son visibles o están habilitados.
Plant aborda este problema con su sistema UIHandler que gestiona cuándo una capa de UI debería o no ser visible. Todas las capas de UI en la experiencia están categorizadas como HUD o Menu y su visibilidad se gestiona mediante las siguientes reglas:
- El estado habilitado de las capas de Menu y HUD puede ser alternado.
- Las capas de HUD habilitadas solo se muestran si no hay capas de Menu habilitadas.
- Las capas de Menu habilitadas se almacenan en una pila, y solo una capa de Menu es visible a la vez. Cuando se habilita una capa de Menu, se inserta al frente de la pila y se muestra. Cuando se deshabilita una capa de Menu, se retira de la pila y se muestra la siguiente capa de Menu habilitada en la cola.
Este enfoque es intuitivo porque permite navegar por los menús con historial. Si se abre un menú desde otro menú, cerrar el nuevo menú volverá a mostrar el menú anterior.
Los singletons de la capa de UI se registran con el UIHandler y se les proporciona una señal que se activa cuando su visibilidad debe cambiar.
Lectura adicional
Desde esta exhaustiva visión general del proyecto Plant, es posible que desees explorar las siguientes guías que profundizan en conceptos y temas relacionados.
- Modelo Cliente-Servidor — Una visión general del modelo cliente-servidor en Roblox.
- Eventos Remotos y Callbacks — Todo sobre eventos de red remotos y callbacks para la comunicación a través de la frontera cliente-servidor.
- UI — Detalles sobre objetos de interfaz de usuario y diseño en Roblox.