Enlace bidireccional de datos en Unity

Josepanaero

¿Hay alguna forma de hacer enlace bidireccional de datos en Unity?

Os cuento mi problema: me dedico al desarrollo del software de forma profesional, pero no al desarrollo de videojuegos. Hace un tiempo empecé un proyecto personal en Unity, simplemente por probar algo nuevo. Como era mi primer videojuego, pensé en hacer algo muy sencillo: un sudoku.

Estoy intentando programar de la forma más clara y limpia posible, y por ello pensé en separar el "motor" del juego de la parte visual. Por eso, mi motor está escrito en C# plano, sin usar ninguna clase de Unity, y luego tengo por separado la parte que sí que depende de Unity (por ejemplo, clases que heredan de MonoBehaviour).

Por poner un ejemplo, en la parte del motor tengo una clase para el tablero, la cual contiene una matriz 9x9 de objetos de clase Celda. Estas celdas tienen atributos como el valor (entero de 0 a 9), un booleano para indicar si es editable o no, etc. Y luego, por otra parte, en el namespace "GUI" tengo otra clase Celda, la cual hereda de MonoBehaviur, y la cual contiene un enlace a la instancia de Celda del motor, además de otras funciones relacionadas con Unity, como por ejemplo una referencia a un objeto TextMeshProUGUI en el que se muestra el valor de la celda, funciones para la rotación de la celda (estoy haciendo la GUI en 3D, por practicar), etc.

El problema que tengo es que cada vez que quiero actualizar el valor de "GUI.Celda", también tengo que actualizar "Motor.Celda", y viceversa. Por ejemplo, digamos que el usuario introduce un valor de una celda. En este caso, tendría que actualizar el valor del objeto TextMeshProUGUI y, además, actualizar el valor de "Motor.Celda". Del mismo modo, me gustaría que cuando yo cambiase el valor de una celda desde el motor, la GUI se actualizase automáticamente.

¿Hay alguna forma de hacer esto? Por un lado he encontrado esta librería, pero parece que está un poco abandonada y no la utiliza mucha gente: https://github.com/Real-Serious-Games/Unity-Weld. Por otro lado, siempre podría usar "GUI.Celda" en mi motor y eso resolvería el problema, pero eso rompería la separación de conceptos (separation of concerns, no sé cuál es el término correcto en español), y el código estaría quizá peor estructurado. O quizá estoy intentando rizar el rizo y lo normal en la industria de los videojuegos es mezclar el backend y el frontend (por así decirlo), ya que los juegos son inherentemente visuales.

El motivo de querer hacer esto así es porque en todos los tutoriales que he visto por internet (tanto en YouTube como escritos), la calidad del código que he visto siempre es bastante baja: clases con demasiadas responsabilidades, código espagueti, los tests ni los mencionan de pasada, etc. Por eso estaba intentando escribir código algo más mantenible de lo que he visto, ya que en todos los tutoriales que he visto se centran mucho en conseguir un resultado rápido, y el código resultante funciona, pero a la larga sería inmantenible.

puntocom

Hola,

¿has mirado el tema de eventos?

Tal como comentas, lo que yo suelo hacer para separar la parte de la lógica de la interfaz, es que una parte del código se encarga de la lógica del juego y envía eventos cuando hay cambios; la interfaz está suscrita y simplemente muestra estos cambios sin tener ninguna referencia a la lógica propia.

1 respuesta
r2d2rigo

Tienes el mismo problema que tendrias en cualquier software que use una capa de GUI: necesitas un mecanismo para sincronizar los valores entre ambas capas.

Dependiendo de en que tengas experiencia, mirate MVC, MVVM o MVP e intenta aplicar la misma funcionalidad a tu abstaccion motor/UI.

2 1 respuesta
Josepanaero

#2 ¿te refieres a los eventos en C#? No tengo experiencia con C#, así que no los he utilizado nunca. Voy a echarle un vistazo, muchas gracias :-)

#3 correcto, de hecho la librería que comentaba en #1 implementa el patrón MVVM. Creo que mi pregunta en #1 se refería más a si había alguna forma "nativa" y sencilla de implementar esto en Unity, o si lo tengo que programar desde cero.

2 respuestas
r2d2rigo

#4 que yo sepa no, no hay implementacion nativa porque generalmente en los juegos no hay tanta abstraccion, lo que muestras por pantalla es lo que tienes almacenado en una variable concreta y no tienes que ir pasando entre vista, controlador, etc.

1 respuesta
puntocom

#4 Sí, tienes eventos propios de Unity, pero no son necesarios, usa los eventos de C#.

El resumen es lo que te decía antes, la parte lógica invoca un evento cada vez que haga cambios que quieras mostrar; la parte gráfica está suscrita a estos eventos, si es necesario el evento puede pasar información. La parte gráfica tiene su propio sistema de encargarse de tratar esta info y mostrarla, de manera independiente de la lógica.

1 respuesta
Josepanaero

#5, #6 gracias a ambos, tiene sentido.

En ese caso, una posible solución podría ser el tener una función en mi motor para cambiar el valor de una casilla. Cuando llame a esta función, lanzo un evento indicando que la casilla ha cambiado de valor, y dicho evento incluye el nuevo valor. Luego tendría que suscribir la interfaz gráfica a ese evento, para que cada vez que se lance un evento se actualice TextMeshProUGUI con el nuevo valor de la casilla. De esta forma, mi motor sería mi única source of truth y la parte lógica y la visual estarían separadas :thumbsup:

1
Buffoncete

mírate este artículo donde se explica como usar delegados en C# y también mira superficialmente los UnityEvent.

https://itchyowl.com/events-in-unity/

como backend durante 15 años, he de decir que la arquitectura de un videojuego dista mucho de la arquitectura de un servidor, utilizar MVC, MVP, MVVM no es utilizar idiomáticamente el motor de Unity y se ha de hacer una transición.

Los UnityEvents se pueden serializar y la Inversion de Control se hace desde la interfaz de usuario y en este sentido, también la gestión de esas celdas en el sudoku, mi recomendación sería tratar este problema como perteneciente a su propio "dominio".

Yo lo que intento siempre es crear mi propia carpeta, llámalo Celda, y gestionar incluidos scrips y toda la lógica relacionada con la celda en ese subconjunto, crear el prefab Celda y que toda la interacción con ese subdominio sea a través de este prefab de manera visual (la API), y para esto los UnityEvents son mejores que usar los delegados de C#.

También tienes la opción de usar FindObjectOfType pero es una llamada muy costosa y acopla innecesariamente el código.

1 1 respuesta
Josepanaero

#8 gracias por el enlace, está todo muy bien explicado.

También mencionar que mi intención final no era necesariamente utilizar patrones MVC, MVVM o de cualquier otro tipo. Lo único que quería conseguir es que mis clases estuviesen poco acopladas, y procurar que mis clases no tuvieran demasiadas responsabilidades (idealmente una responsabilidad). Y mi problema es que tanto Unity como C# son completamente nuevos para mí, y los tutoriales de Unity que he encontrado hasta el momento no han explicado cómo escribir código más mantenible. Así que no sabía cuál podría ser la mejor manera de resolver esto, pero ya veo que con los eventos puedo solucionar esto de forma sencilla y elegante.

1 respuesta
Buffoncete

tienes la misma intriga que tenía yo cuando empecé con Unity y todos los tutoriales que hacía eran basura a nivel de código, pero el objetivo del tutorial era enseñar rápido como hacer ciertas cosas y no enseñar a escribir código bonito.

En Unity, para tener SRP (Single Responsibility Principle) se trabaja con Arquitectura basada en componentes, esto fue nuevo para mi pasar a este modelo.

En la práctica lo que significa, es que si tienes un objeto y quieres darle puntos de vida, normalmente tendrás un script llamado Health que gestionará todo esto y estará desacoplado de cualquier objeto, lo mismo si además quieres que tenga gravedad o se mueva y le metes un RigidBody como componente.

Y poner eventos para separar mejor la lógica y tener el código más mantenible siempre ha sido mi debilidad, pero es mucho más dificil de seguir cuándo pasa qué y muchos developers prefieren antes un:

    GameObject.FindObjectOfType<Health>().damage(damageDone);

que en realidad, es mucho más difícil de seguir o depurar.

2
Buffoncete

#9 mierda google, hablamos de este tema y me recomienda este video que trata justo sobre lo que has puesto en #1

100% recomendable seguir a DapperDino sobre Design Patterns en Unity y buenas prácticas

2 2 respuestas
puntocom

#11 Los vídeos de Dapper Dino están muy bien, sí.
Otra persona que me gusta mucho y que hace las cosas bien es Jason Weimann.
Aquí un vídeo hablando de cuando usar eventos de Unity o C#:

Es complicado encontrar gente que no haga el típico tutorial para sacar cuatro cosas rápidas, Jason habla mucho de como estructurar un proyecto adecuadamente, os recomiendo echar un vistazo a sus vídeos y si podéis también a sus cursos.

3 1 respuesta
Josepanaero

Gracias, al del vídeo de #11 no lo conocía, pero al de #12 sí, es uno de los pocos tutores que he encontrado por YouTube que escriben código decente. Otro que he encontrado que también parece que sabe de lo que habla es Infallible Code, aunque sus vídeos a veces dan un poco de vergüenza ajena xD Para muestra, un botón:

1 mes después
B

Como ya han comentado puedes implementarlo de diferentes modos.

Lo suyo sería ver primero como funciona el loop interno del engine https://docs.unity3d.com/Manual/ExecutionOrder.html

Ten en cuenta que el enlace muestra un esquema teórico.

En función de para que plataforma compiles, lo que hace el engine bajo el capó varía.

Por ejemplo, se supone que fixedupdate se ejecuta a intervalos fijos... Pero no es así.

Lo que hace el engine en el editor o en compilaciones standalone es calcular, promediar cuántos ciclos de fixedupdate debe haber para cada update (update = frame) y los ejecuta en serie sin esperar el "paso fijo" entre llamadas.

En cada llamada a la lógica de la física le pasa un tiempo ficticio, como si realmente hubiera espera entre llamadas y hacer la magia que de aproxime al marco teórico. Por esto, en tu caso, es probable que meter el "gancho" en fixedupdate no sería una buena idea.

Repito, depende de la plataforma lo que sucede bajo el capó varía, el engine busca la mejor optimización en el dispositivo con la aproximación al marco teórico óptima.

Para las funciones engine dependientes es buena idea usar gestores https://blogs.unity3d.com/es/2015/12/23/1k-update-calls/

Suerte

1 1 respuesta
Josepanaero

#14 Gracias por ambos links, muy interesantes, la verdad. Se me va a quedar un Sudoku la mar de eficiente xD

1 respuesta
B

#15 hoy es un sudoku... mañana veremos, el saber no ocupa lugar.

PD: lo veo con modo batalla por tiempo y/o multiplayer competitivo

Usuarios habituales