[Unity] ¿Cómo organizar los scripts de manera modular y desacoplada?

Weahl

He empezado hace un par de día con Unity y tengo una duda sobre como organizar los scripts de la mejor manera posible para que sea más fácil el desarrollo, y todo lo que veo por Internet me resulta confuso.

En mi caso por ejemplo tengo un objeto Player en un juego tipo runner que:

  • No tiene movimiento, solo salto
  • Puede realizar un doble salto
  • Emite un sonido al saltar
  • Emite partículas al correr por el suelo
  • Debe dejar de emitir partículas al saltar
  • Puede colisionar con obstáculos

He probado dos aproximaciones, ambas incluyen tener código separado en distintos scripts como PlayerManager, PlayerJump, PlayerCollisions, PlayerAudio y PlayerParticles.

Event manager
Los scripts se comunican entre sí emitiendo y escuchando eventos, los cuales harán una acción determinada al ser recibidos.

Es una aproximación con muchas posibilidades, pero deja el código más confuso de entender bajo mi punto de vista ya que tienes que tener una lista de suscripciones y desuscripciones a eventos en OnEnabled y OnDisabled.

Scripts usando GetComponent
Tenerlo todo separado pero el PlayerJump, al detectar el input de salto y ejecutar el método de salto se encarga de llamar al script de PlayerParticles para parar las partículas, de llamar a PlayerAudio para emitir el sonido de salto, de obtener el RigidBody y el Animator para realizar el movimiento y setear un nuevo estado de animación.

Me parece que queda demasiado acoplado cada script entre sí, aunque es la aproximación que más me convence de momento.

¿Tenéis alguna forma mejor de organizar el código o recomendáis algún canal de Youtube o web que visitar para aprender algo relacionado con esto?

B

Es que no hay ABC.
Puedes utilizar un modelo VMC y tener que salirte de ello.

Tú mismo al final eres el que decides que usar o que te metodo.

En el caso de salto, doble salto. Partículas. Sonido, etc...
Es un personaje modulable. Vas a tener un personaje que tenga metodo correr, saltar, disparar o lo que quieras. Pero el no se va a encargar de ello. Lo que el va a hacer es Delegar esas funciones a otros.
Tienes dos saltos. Uno normal y otro doble.

Al personaje o controller le va a dar igual cual tengas. Cuando pulses Salto. Va a llamar a la función. Y quien la tendrá? Un script, interfaces o por herencia que haga que al recibir la petición Salto haga cosas.

1 1 respuesta
Weahl

#2 La cosa es que para mi no tiene sentido que el script principal se encargue de llamar al resto de funciones, si añado un script de salto automáticamente debería de funcionar lo añada al personaje al que lo añada, debería de venir con todo lo necesario como:

  • Comprobar que se pulsa la tecla correspondiente para ejecutar el salto
  • Las variables de salto relacionadas con el impulso o la gravedad
  • Los efectos de sonido
  • Partículas que pueda llegar a generar

Si es el script principal el que se encarga de llamarlo, estaría acoplado y no podría quitar ese script de salto sin tener que retocar código, ¿no?

Querría poder añadir o quitar funcionalidades de esa forma, añadiendo o quitando un script simplemente.

1 respuesta
kesada7

Creo que no hay una forma correcta, cada una tiene sus cosas buenas y cosas malas.

La primera que planteas, por eventos, te da esa flexibilidad de no acoplar componentes. Si en cualquier otro sitio ahora quieres hacer algo cuando saltas, como un sistema de partículas o un sonido solo tienes que suscribirte al salto. Pero como dices puede llegar a complicar el seguimiento de estos delegados.

La segunda pues es más fácil de leer y entender, pero estás acoplando los componentes y en muchas ocasiones terminas escribiendo más código que si simplemente metes esa partícula o sonido directamente en el script del salto. Pero esto llevaría a otro caso. Super clases que hacen demasiadas cosas. Hay gente que lo hace así. Los de Celeste lo hicieron y tienen aquí la clase y un readme explicando sus razones: https://github.com/NoelFB/Celeste/tree/master/Source/Player

Te dejo otra opción más. Modular las clases por comportamientos. En lugar de tener un "PlayerController" que tenga las funciones de correr, saltar, disparar, etc. haz cada una de esta una clase modular.

Tienes la clase "Saltar.cs" y está se encarga de todo lo relacionado con ello y dentro incluye todo lo que necesita, sonidos, partículas etc. Si le añades esta clase a cualquier objeto, este debería de poder saltar con todo lo que necesita, pero no podrá correr. Si quiere correr tendrás "Correr.cs" en otra diferente con sus partículas, sonidos, etc.

2 1 respuesta
B

#3 No me has entendido.

Tienes PlayerController que gracias a un GetComponent, va a buscar el script encargado del salto acoplado al GameObject Player. Tú en tu PlayerController pones

jumpStyle = GetComponent<Jump>();
jumpStyle.jump();

Y luego el GameObject Player va a tener un script añadido que sea el Jump, ya sea Jump, DoubleJump, TripleJump o NoJump. Todos heredan funcionalidades del Padre Jump.
Puedes agregarlos al Objet Player. Cuando el jugador haga la interacción de Saltar, ya sea pulsando Espacio o X o el botón que sea. Todo el entresijo de código llegará a la parte que es jumpStyle.jump().
Y esta mirará el componente que tenga añadido que será la que mire de hacer el salto normal, el salto doble o lo que quieras. Esto viene bien cuando tienes diferentes funcionalidades para el personaje, ya sean buffs, debuffs o características que cambien a lo largo del juego.

Ejemplo: Jugador puede hacer Salto normal. Tienes Objeto Player con script de Jump añadido.
Jugador se acerca a objeto que le permite ganar la habilidad de Doble Salto. La recoge y en este momento, remueves el Script Jump.cs y lo sustituyes por DoubleJump.cs. Player Object y PlayerController van a buscar un Jump, le da igual cual sea y le van a pedir que ejecute el código que esté en el método Jump. Y es el DoubleJump.cs el que te permitirá hacer 2 saltos en vez de uno.

El problema es que tienes muchas clases separadas y que posiblemente cambien unos números o unas lineas entre ellas. Pero te permite cambiar funcionalidades, testeo y ganar escalabilidad.
Ya entra en juego luego crear Interfaces, que a una misma clase añadirle funcionalidades extras sin que afecte a la herencia.


En el caso de Celeste lo explica bien. Tienen un montón de Estados y tener referencia a todos ellos de cara a controlarlos y referenciarlos puede ser un quebradero de cabeza. Por eso el controlador tan inmenso que tienen. Pero dentro de ello, está limitado a lo que hay.
Si Celeste tuviera estados alterables, temporales o buffs/debuffs, no le funcionaría todo eso.

Para FSM es correcto tenerlos separados, pero para movimientos de personajes en escena, hay demasiadas combinaciones realizables.


Al final, es el juego y la experiencia la que te dictamine que hacer en cada ocasión.

1 1 respuesta
Weahl

#4 #5 Entiendo, muchas gracias por las explicaciones, seguiré trasteando a ver si consigo hacer algo parecido a lo que comentáis :)

totespare

No intentes hacerlo perfecto desde el inicio (aparte de que no existe la manera perfecta, siempre vas a tener que traicionar de una manera u otra tus patrones). Prueba de una forma, expandela y aprende cuales son sus debilidades y fortalezas. Luego intentalo con otro tipo de implementación y lo mismo (o cmabiando de feature para no aburrirte con la misma siempre), y vas viendo qué funciona en cada caso y qué no. Ademas es divertido aprender de esa manera (al menos para mi).

Luego mola cuando tienes que enfrentarte a una situación similar a otra que hayas resuelto, y si tienes mas experiencia, seguro que se te ocurren mejores maneras de hacerlo esa vez. Disfruta el proceso del aprendizane, que cuando da sus frutos mola mucho ☺️

2 1 respuesta
Weahl

#7 Me cuesta no intentar ir a la perfección porque soy demasiado perfeccionista, y me he parado antes de seguir aprendiendo nada más para saber como organizar los scripts y no he parado hasta encontrar una manera y LO HE HECHO! Al menos algo que me funciona actualmente y solo estan acoplados dentro de la misma entidad de Player :D

Y sí, está claro que más adelante encontraré formas de mejorar el código, y eso me encanta :smile:

¡Muchas gracias por los consejos!

1 respuesta
totespare

#8 te entiendo jajaja, dejar de ser perfeccionista es una habilidad complicada. Hay que practicar el "let go", te ayudará en tu salud y en tu manejo del tiempo xD

1

Usuarios habituales