Unity3D: Arquitectura de juego, Objetos de secuencias de comandos, Singletones

Hoy hablaremos sobre cómo almacenar, recibir y transmitir datos dentro del juego. Sobre lo maravilloso llamado ScriptableObject, y por qué es maravilloso. Hablemos un poco sobre los beneficios de los singletones al organizar escenas y transiciones entre ellos.

Este artículo describe una parte de una forma larga y dolorosa de desarrollar un juego, varios enfoques utilizados en el proceso. Lo más probable es que haya mucha información útil para principiantes y nada nuevo para los "veteranos".

Enlaces entre guiones y objetos


La primera pregunta a la que se enfrenta un desarrollador novato es cómo vincular todas las clases escritas y configurar las interacciones entre ellas.

La forma más fácil es especificar un enlace a la clase directamente:

public class MyScript : MonoBehaviour { public OtherScript otherScript; } 

Y luego, enlaza manualmente el script a través del inspector.

Este enfoque tiene al menos un inconveniente significativo: cuando el número de scripts excede varias decenas, y cada uno de ellos requiere dos o tres enlaces entre sí, el juego se convierte rápidamente en una red. Una mirada a ella es suficiente para causar dolor de cabeza.

Es mucho mejor (en mi opinión) organizar un sistema de mensajes y suscripciones, dentro del cual nuestros objetos recibirán la información que necesitan, ¡y solo eso! - sin requerir media docena de enlaces entre sí.

Sin embargo, después de probar el tema, descubrí que las soluciones ya preparadas en Unity son regañadas por todos los que no son flojos. Me pareció una tarea no trivial escribir un sistema de este tipo desde cero y, por lo tanto, buscaré soluciones más simples.

Objeto programable


Básicamente, hay dos cosas que debes saber sobre ScriptableObject:

  • Son parte de la funcionalidad implementada dentro de Unity, como MonoBehaviour.
  • A diferencia de MonoBehaviour, no están vinculados a objetos de escena, sino que existen como activos separados y pueden almacenar y transferir datos entre sesiones de juego .

Inmediatamente me enamoré de su amor ardiente. Ellos, en cierto modo, se han convertido en mi panacea para cualquier problema:

  • ¿Necesitas almacenar la configuración del juego? ScriptableObject!
  • ¿Crear un inventario? ScriptableObject!
  • Escribir AI? ScriptableObject!
  • ¿Registrar información sobre un personaje, enemigo, elemento? ¡ScriptableObject nunca te decepcionará!

Sin pensarlo dos veces, creé varias clases de tipo ScriptableObject, y luego un repositorio para ellos:

 public class Database: ScriptableObject { public PlayerData playerData; public GameSettings gameSettings; public SpellController spellController; } 

Cada uno de ellos almacena en sí mismo toda la información útil y, posiblemente, enlaces a otros objetos. Cada uno de ellos es suficiente para vincular una vez al inspector; no irán a ningún otro lado.

¡Ahora no necesito especificar un número infinito de enlaces entre scripts! Para cada script, una vez puedo especificar un enlace a mi repositorio, y recibirá toda la información de allí.

Por lo tanto, el cálculo de la velocidad del personaje tiene un aspecto muy elegante:

 //   float speed = database.playerData.speed; //    if (database.spellController.haste.active) speed = speed * database.spellController.haste.speedModifier; // ,     if (database.playerData.health<database.playerData.healthThreshold) speed = speed * database.playerData.woundedModifier; 

Y si, por ejemplo, una trampa solo debe disparar sobre un personaje en ejecución:

 if (database.playerData.isSprinting) Activate(); 

Además, el personaje no necesita saber nada sobre hechizos o trampas. Solo recupera datos del almacenamiento. No esta mal No esta mal.

Pero casi de inmediato me encuentro con un problema. ScriptableOnjects no puede almacenar enlaces a objetos de escena directamente. En otras palabras, no puedo crear un enlace al jugador, vincularlo a través del inspector y olvidarme de la cuestión de las coordenadas del jugador para siempre.

Y si lo piensas, ¡tiene sentido! Los activos existen fuera del escenario y se puede acceder en cualquiera de las escenas. ¿Y qué sucede si deja un enlace a un objeto ubicado en otra escena dentro del activo?

Nada bueno

Durante mucho tiempo, una muleta funcionó para mí: se crea un enlace público en el repositorio, y luego cada objeto, el enlace al que desea recordar, llenó este enlace:

 public class PlayerController : MonoBehaviour { void Awake() { database.playerData.player = this.gameObject; } } 

Por lo tanto, independientemente de la escena, mi repositorio primero recibe un enlace al jugador y lo recuerda. Ahora, digamos, el enemigo no debe almacenar un enlace al jugador, no debe buscarlo a través de FindWithTag () (que es un proceso que requiere muchos recursos). Todo lo que hace es acceder al repositorio:

 public Database database; Vector3 destination; void Update () { destination = database.playerData.player.transform.position; } 

Parecería: ¡el sistema es perfecto! Pero no Todavía tenemos 2 problemas.

  1. Todavía tengo que especificar manualmente un enlace al repositorio para cada script.
  2. Es incómodo asignar referencias a objetos de escena dentro de un ScriptableObject.

Sobre el segundo con más detalle. Supongamos que un jugador tiene un hechizo ligero. El jugador lo lanza, y el juego le dice a la tienda: ¡se emite la luz!

 database.spellController.light.CastSpell(); 

Y esto da lugar a una serie de reacciones:

  • Se crea una luz de objeto de juego nueva (o antigua) en el punto del cursor.
  • Se inicia un módulo GUI que nos dice que la luz está activa.
  • Los enemigos obtienen, por ejemplo, una bonificación temporal por encontrar un jugador.

¿Cómo hacer todo esto?

Es posible para cada objeto que está interesado en la luz, directamente en Update () y escribir, dicen, de esta manera y que, cada cuadro sigue la luz (if (database.spellController.light.isActive)), y cuando se ilumina, ¡reacciona! Y no le importa que el 90% de las veces este cheque funcione inactivo. En varios cientos de objetos.

O bien, organice todo esto en forma de enlaces preparados. Resulta que la simple función CastSpell () debería tener acceso a enlaces al jugador, a la luz y a la lista de enemigos. Y esto es lo mejor. Un montón de enlaces, ¿eh?

Puede, por supuesto, guardar todo lo importante en nuestro repositorio cuando comience la escena, dispersar enlaces en activos, que, en general, no están destinados a esto ... Pero, una vez más, viole el principio de un repositorio único, convirtiéndolo en una red de enlaces.

Singleton


Aquí es donde entra en juego el singleton. De hecho, este es un objeto que existe (y puede existir) solo en una sola copia.

 public class GameController : MonoBehaviour { public static GameController Instance; //   ,      public Database database; public GameObject player; public GameObject GUI; public List<Enemy> enemies; public List<Spell> spells; void Awake () { if (Instance == null) { DontDestroyOnLoad (gameObject); Instance = this; } else if (Instance != this) { Destroy (gameObject); } } } 

Lo coloco en un objeto de escena vacío. Llamémoslo GameController.

Por lo tanto, tengo un objeto en la escena que almacena toda la información sobre el juego. Además, puede moverse entre escenas, destruir sus dobles (si ya hay otro GameController en la nueva escena), transferir datos entre escenas y, si lo desea, implementar guardar / cargar el juego.

De todos los scripts ya escritos, puede eliminar el enlace al almacén de datos. Después de todo, ahora no necesito configurarlo manualmente. Todas las referencias a objetos de escena se eliminan de la tienda y se transfieren a nuestro GameController (lo más probable es que las necesitemos para guardar el estado de la escena cuando salgamos del juego). Y luego lo completo con toda la información necesaria de una manera conveniente para mí. Por ejemplo, en Awake () del jugador y enemigos (y objetos importantes de la escena), se prescribe agregar un enlace a ellos mismos en el GameController. Como ahora estoy trabajando con Monobehaviour, las referencias a objetos en la escena encajan muy orgánicamente en ella.

¿Qué obtenemos?

Cualquier objeto puede obtener cualquier información sobre el juego que necesita:

 if (GameController.Instance.database.playerData.isSprinting) ActivateTrap(); 

En este caso, no hay absolutamente ninguna necesidad de configurar enlaces entre objetos, todo se almacena en nuestro GameController.

Ahora no habrá nada complicado en mantener el estado de la escena. Después de todo, ya tenemos toda la información necesaria: enemigos, artículos, posición del jugador, almacén de datos. Es suficiente seleccionar la información sobre la escena que desea guardar y escribirla en un archivo usando FileStream al salir de la escena.

Los peligros


Si lees hasta este punto, debería advertirte sobre los peligros de este enfoque.

Una situación muy mala es cuando muchos scripts hacen referencia a una variable dentro de nuestro ScriptableObject. No hay nada de malo en obtener un valor, pero cuando comienzan a afectar una variable desde diferentes lugares, esta es una amenaza potencial.

Si tenemos una variable playerSpeed ​​guardada, y necesitamos que el jugador se mueva a diferentes velocidades, no deberíamos cambiar playerSpeed ​​en el almacenamiento, deberíamos obtenerla, guardarla en una variable temporal y aplicarle modificadores de velocidad.

El segundo punto, si algún objeto tiene acceso a algo, es un gran poder. Y una gran responsabilidad. Y debes abordarlo con precaución para que algún script de hongos accidentalmente no rompa por completo a todos tus enemigos con IA. La encapsulación configurada adecuadamente reducirá los riesgos, pero no lo salvará de ellos.

Además, no olvide que los singletones son criaturas suaves. No abuses de ellos.

Eso es todo por hoy.

Mucho se ha extraído de los tutoriales oficiales de Unity, algunos de los no oficiales. Tenía que llegar a algo yo mismo. Por lo tanto, los enfoques anteriores pueden tener sus peligros y desventajas, lo que me perdí.

Y por lo tanto, ¡la discusión es bienvenida!

Source: https://habr.com/ru/post/es414361/


All Articles