Plant es un juego de referencia donde los jugadores plantan y riegan semillas, para que luego puedan cosechar y vender las plantas resultantes.

El proyecto se centra en casos de uso comunes que podrías encontrar al desarrollar un juego 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 propios juegos.
Obtén el archivo
- Navega a la página del juego 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 del jugador
- Gestión de vistas de UI
- Redes cliente-servidor
- Experiencia de Usuario por Primera Vez (FTUE)
- Compras de moneda dura y blanda
Además, este proyecto resuelve conjuntos más específicos de problemas que son aplicables a muchos juegos, 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 este juego que son demasiado pequeños, demasiado específicos 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 un juego 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 para el código del cliente y del servidor.
Modelo de datos
La siguiente tabla describe en qué servicios de contenedor se colocan las instancias en el 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, por lo que es aceptable dejarlas aquí. También hay una Folder vacía, a la que se agregará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 el juego. Cuantas más instancias se coloquen en ReplicatedFirst, más tiempo se tardará en replicarlas antes de que el código en ReplicatedFirst pueda ejecutarse.
|
| 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 el juego. 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 pueden ser importados en toda la base de código. ModuleScripts son reutilizables pero no se ejecutan por sí solos; necesitan ser importados por un Script o LocalScript. Muchos proyectos de Roblox tendrán un gran número de objetos Script y LocalScript, cada uno relacionado con un comportamiento o sistema particular en el juego, creando múltiples puntos de entrada.
Para el microjuego Plant, se implementa un enfoque diferente a través de un único LocalScript que es el punto de entrada para todo el código del cliente, y un único 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 único 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 único 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 está abstraída 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 es inicializada por el script start relevante 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 los juegos 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 replique actualizaciones al cliente.
En este proyecto, la comunicación cliente-servidor se mantiene lo más genérica posible al limitar el uso de objetos RemoteEvent y RemoteFunction para disminuir la cantidad de reglas especiales que hay que 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 persiste entre sesiones de guardado. Este sistema proporciona replicación del cliente al servidor y un conjunto de APIs que se pueden utilizar 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 número sorprendente de casos en el proyecto, incluyendo:
- La etapa actual de 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 Instance dada, puedes usar atributos. Roblox replica automáticamente los valores de los atributos, por lo que no necesitas mantener ningún camino de código para replicar el estado asociado con un objeto. Otra ventaja es que esta replicación ocurre junto con la instancia misma.
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 atómicamente con la propia instancia. Esto evita cualquier necesidad de escribir código para "esperar" a que se replique información adicional a través de un RemoteEvent o StringValue.
También puedes leer directamente los atributos del modelo de datos, tanto desde el cliente como desde el servidor, con el método GetAttribute() 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 dada puede recibir una planta.
Mensaje directamente a través del módulo de red
Para situaciones donde 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
La desventaja de este enfoque es que cada mensaje individual requiere alguna 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 está inspirada 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 tipos 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 utilizando un método new(), consistente con la forma en que se crean las instancias en Roblox usando 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 usando 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
De manera 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 en 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 quedan referencias a la instancia o conexiones a la instancia.
Singletons
Los singletons, como su nombre indica, son clases para las que solo puede existir un objeto. Son el equivalente del proyecto a los Servicios de Roblox. En lugar de almacenar una referencia al objeto singleton y pasarlo por el código de Luau, Plant aprovecha el hecho de que requerir un ModuleScript almacena en caché su valor devuelto. Esto significa que requerir el mismo ModuleScript singleton desde diferentes lugares proporciona consistentemente el mismo objeto devuelto. La única excepción a esta regla sería si diferentes entornos (cliente o servidor) accedieran al ModuleScript.
Los singletons se distinguen de las clases instanciables por el hecho de que no tienen un método new(). Más bien, el objeto junto con sus métodos y estado se devuelve directamente a través del ModuleScript. Como los singletons no se instancian, la sintaxis self no se utiliza y los métodos se llaman con un punto (.) en lugar de dos puntos (:).
Verificación de tipos estricta
Luau soporta tipado gradual, lo que significa que eres libre de agregar definiciones de tipo opcionales a parte o a todo tu código. En este proyecto, se utiliza verificación de tipos 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 detectar 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 de 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, objetos Player. Además, la suposición hecha en la sintaxis de clase idiomática de Lua es que declarar un método en una clase self siempre será una instancia de esa clase; esta no es una suposición que el motor de inferencia de tipos pueda hacer.
Para 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 formas, 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 mantenimiento, pero se marcarán advertencias si las dos definiciones se desincronizan entre sí.
- Los métodos de clase se declaran con un punto, por lo que self puede ser declarado explícitamente como tipo ClassType. Los métodos aún se pueden llamar 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 MyClassConvertir tipos después de guardas lógicas
En el momento de escribir, el tipo de un valor no se reduce después de una declaración condicional de guarda. Por ejemplo, después de la guarda 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)
endPara mitigar esto, se crean nuevas variables después de estas guardas con su tipo explícitamente convertido.
--!strict
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
local parameter = optionalParameter :: number
print(parameter + 1)
endRecorrer jerarquías del 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 un 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
endEl problema con este enfoque es que impacta la legibilidad. En su lugar, el proyecto utiliza 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")
endA 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 simples. Estas incluyen elementos de visualización no interactivos (HUD) como el contador de monedas y menús interactivos complejos como la tienda.
Enfoque de UI
Puedes comparar de manera aproximada 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 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 de 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 piezas 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 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. Una desventaja notable es que, dado que los enfoques de UI imperativos requieren que el estado se implemente manualmente en forma de transformaciones, representaciones complejas del estado pueden volverse muy difíciles de encontrar y depurar. Es común que surjan errores al desarrollar código de UI imperativo, 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. Debido a que todo el árbol de 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 quieres que se vea 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 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 una trampa 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 varios 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 artículos.
Manejo de vistas
Un problema común en la gestión de UI es el manejo de vistas. Este proyecto tiene una variedad de menús y elementos HUD, algunos de los cuales escuchan la entrada del usuario, y se requiere una gestión cuidadosa 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 debe o no ser visible. Todas las capas de UI en el juego se categorizan como HUD o Menu y su visibilidad se gestiona mediante las siguientes reglas:
- El estado habilitado de las capas Menu y HUD puede ser alternado.
- Las capas HUD habilitadas solo se muestran si no hay capas Menu habilitadas.
- Las capas Menu habilitadas se almacenan en una pila, y solo una capa Menu es visible a la vez. Cuando se habilita una capa Menu, se inserta al frente de la pila y se muestra. Cuando se deshabilita una capa Menu, se elimina de la pila y se muestra la siguiente capa 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ú mostrará nuevamente el menú antiguo.
Los singletons de capas de UI se registran con el UIHandler y se les proporciona una señal que se activa cuando su visibilidad debe cambiar.
Lectura adicional
A partir de esta completa visión general del proyecto Plant, es posible que desees explorar las siguientes guías que profundizan más 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 y diseño de interfaz de usuario en Roblox.