Qué (no) necesitas saber para crear juegos en Unity



Unity es un motor de juego con un umbral de entrada lejos de cero (en comparación con el mismo Game Maker Studio), y en este artículo te diré qué problemas encontré al comenzar a estudiarlo y qué soluciones encontré. Describiré tales momentos con el ejemplo de mi juego de rompecabezas 2D para Android (que espero sea lanzado pronto en Play Market).

No pretendo ser cierto, y no te insto a que repitas después de ti mismo, si conoces la mejor manera, solo te muestro cómo hacerlo tú mismo, y tal vez alguien que está comenzando a conocer a Unity creará su obra maestra independiente de gamedev con menos trabajo.

Soy ingeniero de diseño de plantas de energía, pero siempre me ha interesado la codificación y estoy familiarizado con algunos lenguajes de programación. Por lo tanto, acordamos que para crear juegos en Unity:
  • Necesita saber un poco de C # o JavaScript (al menos la sintaxis en forma de C).

Todo lo que se escribirá a continuación no es un tutorial de Unity, del cual ya se ha criado lo suficiente en la red sin mí. A continuación se recopilarán los momentos difíciles que pueden ocurrir al crear su primer proyecto en Unity.

Vale la pena advertir que los scripts proporcionados omiten la mayor parte de la lógica del juego (que representa "secretos comerciales"), pero su rendimiento como ejemplos ha sido verificado.

Problema uno - Orientación



Bloqueo de orientación
La primera dificultad que surgió en mí fue que no presté la debida atención a la optimización de la interfaz visual para la orientación de la pantalla. La solución es la más simple: si no necesita un cambio en la orientación de la pantalla para el juego, es mejor bloquearlo. No hay necesidad de una flexibilidad excesiva, estás escribiendo un juego independiente, no un proyecto al otro lado de un millón de dólares. ¿Por qué toneladas de transiciones condicionales y cambio de anclajes si el juego se ve mejor en Retrato (por ejemplo)? Puede bloquear la orientación de la pantalla aquí:
Edición> Configuración del proyecto> Reproductor


Diferentes permisos
También es importante probar la interfaz visual con diferentes resoluciones en la orientación seleccionada, y al realizar la prueba, no se olvide de la existencia de dispositivos con proporciones de 4: 3 (bueno, o 3: 4), para que podamos agregar de manera segura 768x1024 (o 1024x768).

Mejor posicionamiento
Para ajustar el posicionamiento y la escala de los objetos del juego, es mejor usar Rect Transform.


Problema dos - COMUNICACIÓN


Tuve un problema similar debido al hecho de que conocí al desarrollador del juego a través de Game Maker Studio, donde el script es una parte completa del objeto del juego, e inmediatamente tiene acceso completo a todos los componentes del objeto. Unity tiene scripts comunes, y solo se agregan instancias de ellos al objeto. Hablando de manera simplista-figurativa, el script no sabe directamente en qué objeto se está ejecutando actualmente. Por lo tanto, al escribir scripts, debe tener en cuenta la inicialización de las interfaces para trabajar con los componentes de un objeto o con los componentes de otros objetos.

Nosotros entrenamos en gatos
En mi juego hay un objeto GameField, en el escenario solo hay una instancia del mismo, también hay un script con el mismo nombre. El objeto es responsable de mostrar la puntuación del juego y de reproducir todo el sonido del juego, por lo que, en mi opinión, es más económico para la memoria (en general, el juego tiene solo tres fuentes de audio: una música de fondo y otros dos efectos de sonido). El script resuelve los problemas de almacenamiento de una cuenta de juego, eligiendo AudioClip para reproducir sonido y para cierta lógica del juego.

Detengámonos en el sonido con más detalle, ya que este ejemplo muestra fácilmente la interacción del guión con los componentes del objeto.

Naturalmente, el objeto debe tener el script GameField.cs en sí y el componente AudioSource, en mi caso dos completos (más adelante quedará claro por qué).

Como se mencionó anteriormente, el script "no sabe" que el objeto tiene un componente AudioSource, por lo tanto, declaramos e inicializamos la interfaz (por ahora, consideramos que solo hay un AudioSource):
private AudioSource Sound; void Start(){ Sound = GetComponent<AudioSource> (); } 

El método GetComponent <component_type> () devolverá el primer componente del tipo especificado del objeto.

Además de AudioSource, necesitará varios AudioClip:
 [Header ("Audio clips")] [SerializeField] private AudioClip OnStart; [SerializeField] private AudioClip OnEfScore; [SerializeField] private AudioClip OnHighScore; [SerializeField] private AudioClip OnMainTimer; [SerializeField] private AudioClip OnBubbMarker; [SerializeField] private AudioClip OnScoreUp; 

En lo sucesivo, los comandos entre corchetes son necesarios para el Inspector`a, más detalles aquí .



Ahora el script en Inspector tiene nuevos campos en los que arrastramos los sonidos necesarios.


A continuación, cree un método SoundPlay en el script que tome AudioClip:
 public void PlaySound(AudioClip Clip = null){ Sound.clip = Clip; Sound.Play (); } 

Para reproducir sonido en el juego, llamamos en el momento adecuado a este método con el clip.

Hay un inconveniente significativo de este enfoque, solo se puede reproducir un sonido a la vez, pero durante el juego puede ser necesario reproducir dos o más sonidos, con la excepción de la música de fondo que se reproduce constantemente.

Para evitar la cacofonía, recomiendo evitar la posibilidad de reproducción simultánea de más de 4-5 sonidos (preferiblemente un máximo de 2-3), me refiero a los sonidos cortos de primer orden del juego (salto, moneda, disparo del jugador ...), para el ruido de fondo es mejor crear su propia fuente sonido en el objeto que hace este ruido (si necesita sonido 2d-3d) o un objeto responsable de todo el ruido de fondo (si no se necesita "volumen").

En mi juego, no hay necesidad de reproducir simultáneamente más de dos AudioClips. Para garantizar la reproducción de ambos sonidos hipotéticos, agregué dos AudioSource al objeto GameField. Para determinar los componentes en el script, usamos el método
 GetComponents<_>() 

que devuelve una matriz de todos los componentes del tipo especificado del objeto.

El código se verá así:
 private AudioSource[] Sound; //    void Start(){ Sound = GetComponents<AudioSource> (); //  GetComponents } 

La mayoría de los cambios afectarán el método PlaySound. Veo dos versiones de este método: "universal" (para cualquier número de AudioSource en un objeto) y "torpe" (para 2-3 AudioSource, no el más elegante pero requiere menos recursos).

La opción "torpe" para dos AudioSource (lo usé)
 private void PlaySound(AudioClip Clip = null){ if (!Sound [0].isPlaying) { Sound [0].clip = Clip; Sound [0].Play (); } else { Sound [1].clip = Clip; Sound [1].Play (); } } 

Puede ampliar hasta tres o más AudioSource, pero la cantidad de condiciones devorará todos los ahorros de rendimiento.

Opción "universal"
 private void PlaySound(AudioClip Clip = null){ foreach (AudioSource _Sound in Sound) { if (!_Sound.isPlaying) { _Sound.clip = Clip; _Sound.Play (); break; } } } 


Acceso a un componente extraño.
En el campo de juego hay varias instancias del prefabricado Fishka, como una ficha de juego. Está construido así:
  • Objeto padre con su SpriteRenderer;
    • El niño se opone con su SpriteRenderer.

Los objetos secundarios son responsables de dibujar el cuerpo del chip, su color, elementos mutables adicionales. El padre dibuja un borde marcador alrededor del chip (el chip activo debe resaltarse en el juego). El script solo está en el objeto padre. Por lo tanto, para administrar sprites secundarios, el script primario necesita especificar estos sprites. Lo organicé así: en el script creé interfaces para acceder a los hijos de SpriteRenderer:
 [Header ("Graphic objects")] public SpriteRenderer Marker; [SerializeField] private SpriteRenderer Base; [Space] [SerializeField] private SpriteRenderer Center_Red; [SerializeField] private SpriteRenderer Center_Green; [SerializeField] private SpriteRenderer Center_Blue; 

Ahora el script en Inspector tiene campos adicionales:


Arrastrar y soltar hijos en los campos correspondientes nos da acceso a ellos en el script.

Ejemplo de uso:
 void OnMouseDown(){ //        Marker.enabled = !Marker.enabled; } 


Llamar el guión de otra persona
Además de manipular componentes extraños, también puede acceder al script de un objeto de terceros, trabajar con sus variables públicas, métodos, subclases.

Daré un ejemplo sobre el objeto GameField ya conocido.

El script GameField tiene un método público FishkiMarkerDisabled (), que es necesario para "eliminar" un marcador de todas las fichas en el campo y se utiliza en el proceso de establecer un marcador al hacer clic en una ficha, ya que solo puede haber uno activo.

En el script Fishka.cs, ​​SpriteRenderer Marker es público, es decir, se puede acceder desde otro script. Para hacer esto, agregue la declaración e inicialización de las interfaces para todas las instancias de la clase Fishka en la secuencia de comandos GameField.cs (cuando se crea una secuencia de comandos, se crea la clase del mismo nombre) de forma similar a como se hace para varios AudioSource:
 private Fishka[] Fishki; void Start(){ Fishki = GameObject.FindObjectsOfType (typeof(Fishka)) as Fishka[]; } public void FishkiMarkerDisabled(){ foreach (Fishka _Fishka in Fishki) { _Fishka .Marker.enabled = false; } } 

En el script Fishka.cs, ​​agregue la declaración e inicialización de la interfaz de la instancia de la clase GameField, y cuando hagamos clic en el objeto, llamaremos al método FishkiMarkerDisabled () de esta clase:
 private GameField gf; void Start(){ gf = GameObject.FindObjectOfType (typeof(GameField)) as GameField; } void OnMouseDown(){ gf.FishkiMarkerDisabled(); Marker.enabled = !Marker.enabled; } 

Por lo tanto, puede interactuar entre scripts (o más bien clases) de diferentes objetos.


Problema Tres - MANTENER


Encargado de la cuenta
Tan pronto como aparece una cuenta en el juego, el problema inmediato es su almacenamiento, tanto durante el juego como fuera de él, también quiero mantener un registro para alentar al jugador a superarlo.

No consideraré las opciones cuando todo el juego (menú, juego, retiro de cuenta) se construya en una escena, porque, en primer lugar, esta no es la mejor manera de construir el primer proyecto y, en mi opinión, la escena de carga inicial debería ser . Por lo tanto, estamos de acuerdo en que hay cuatro escenas en el proyecto:
  1. cargador: una escena en la que se inicializa el objeto de música de fondo (más será más adelante) y carga la configuración desde el guardado;
  2. menú: una escena con un menú;
  3. juego - escena del juego;
  4. puntaje: la escena del puntaje, registro, tabla de clasificación.


Nota: El orden de carga de la escena se establece en Archivo> Configuración de compilación.

Los puntos acumulados durante el juego se almacenan en la variable Puntuación de la clase GameField. Para tener acceso a los datos cuando vaya a la escena de puntajes, cree una clase estática pública ScoreHolder, en la que declaremos una variable para almacenar el valor y una propiedad para obtener y establecer el valor de esta variable (el método fue espiado por apocatastas ):
 using UnityEngine; public static class ScoreHolder{ private static int _Score = 0; public static int Score { get{ return _Score; } set{ _Score = value; } } } 

No es necesario agregar una clase estática pública a ningún objeto, está disponible de inmediato en cualquier escena desde cualquier secuencia de comandos.

Ejemplo de uso en la clase GameField en las puntuaciones del método de transición de escena:
 using UnityEngine.SceneManagement; public class GameField : MonoBehaviour { private int Score = 0; //     ,         Scores void GotoScores(){ ScoreHolder.Score = Score; //   ScoreHolder.Score   SceneManager.LoadScene (“scores”); } } 

Del mismo modo, puede agregar una cuenta de registro al ScoreHolder durante el juego, pero no se guardará al salir.

Guardián de configuraciones
Considere el ejemplo de guardar el valor de la variable booleana SoundEffectsMute, dependiendo del estado en el que el juego tenga o no efectos de sonido.

La variable en sí se almacena en la clase estática pública SettingsHolder:
 using UnityEngine; public static class SettingsHolder{ private static bool _SoundEffectsMute = false; public static bool SoundEffectsMute{ get{ return _SoundEffectsMute; } set{ _SoundEffectsMute = value; } } } 

La clase es similar a ScoreHolder, incluso podría combinarlos en uno, pero en mi opinión, esto es de mala educación.

Como puede ver en el script, por defecto _SoundEffectsMute se declara falso, por lo que cada vez que se inicia el juego, SettingsHolder.SoundEffectsMute devolverá falso independientemente de si el usuario lo ha cambiado antes o no (se cambia usando el botón en la escena del menú).

Guardar variables
Lo más óptimo para una aplicación de Android será utilizar el método PlayerPrefs.SetInt para guardar (para obtener más detalles, consulte la documentación oficial ). Hay dos opciones para mantener el valor de SettingsHolder.SoundEffectsMute en PlayerPrefs, llamémoslas "simple" y "elegante".

La forma "simple" (para mí así) está en el método OnMouseDown () de la clase del botón mencionado anteriormente. El valor almacenado se carga en la misma clase pero en el método Start ():
 using UnityEngine; public class ButtonSoundMute : MonoBehaviour { void Start(){ //    ,  PlayerPrefs    bool switch (PlayerPrefs.GetInt ("SoundEffectsMute")) { case 0: SettingsHolder.SoundEffectsMute = false; break; case 1: SettingsHolder.SoundEffectsMute = true; break; default: //    default SettingsHolder.SoundEffectsMute = true; break; } } void OnMouseDown(){ SettingsHolder.SoundEffectsMute = !SettingsHolder.SoundEffectsMute; //    ,  PlayerPrefs    bool if (SettingsHolder.SoundEffectsMute) PlayerPrefs.SetInt ("SoundEffectsMute", 1); else PlayerPrefs.SetInt ("SoundEffectsMute", 0); } } 


El método "elegante", en mi opinión, no es el más correcto, porque complica el mantenimiento del código, pero hay algo en él, y no puedo evitar compartirlo. Una característica de este método es que se llama al configurador de la propiedad SettingsHolder.SoundEffectsMute en un momento que no requiere un alto rendimiento, y se puede cargar (oh, horror) usando PlayerPrefs (lectura - escritura en un archivo). Cambie la clase estática pública SettingsHolder:

 using UnityEngine; public static class SettingsHolder { private static bool _SoundEffectsMute = false; public static bool SoundEffectsMute{ get{ return _SoundEffectsMute; } set{ _SoundEffectsMute = value; if (_SoundEffectsMute) PlayerPrefs.SetInt ("SoundEffectsMute", 1); else PlayerPrefs.SetInt ("SoundEffectsMute", 0); } } } 

El método OnMouseDown de la clase ButtonSoundMute se simplificará a:
 void OnMouseDown(){ SettingsHolder.SoundEffectsMute = !SettingsHolder.SoundEffectsMute; } 


No vale la pena cargar el captador con la lectura de un archivo, ya que está involucrado en un proceso crítico para el rendimiento, en el método PlaySound () de la clase GameField:
 private void PlaySound(AudioClip Clip = null){ if (!SettingsHolder.SoundEffectsMute) { //      “”  (. ) if (!Sound [0].isPlaying) { Sound [0].clip = Clip; Sound [0].Play (); } else { Sound [1].clip = Clip; Sound [1].Play (); } } } 


De la manera anterior, puede organizar el almacenamiento en el juego de cualquier variable.


Quinto problema: UNO PARA TODOS


Esta musica sera eterna
Tarde o temprano, todos se enfrentan a ese problema, y ​​yo no fui la excepción. Según lo planeado, la música de fondo comienza a reproducirse incluso en la escena del menú, y si no está apagada, reproduce el menú, el juego y los puntajes en las escenas sin interrupción. Pero si el objeto que "reproduce" la música de fondo se instala en la escena del menú, cuando va a la escena del juego, se destruye y el sonido desaparece, y si coloca el mismo objeto en la escena del juego, luego de la transición, la música se reproduce primero. La solución resultó ser el uso del método DontDestroyOnLoad (Object target) ubicado en el método Start () de la clase cuya instancia de script tiene el objeto "music". Para hacer esto, cree el script DontDestroyThis.cs:
 using UnityEngine; public class DontDestroyThis: MonoBehaviour { void Start(){ DontDestroyOnLoad(this.gameObject); } } 

Para que todo funcione, el objeto "musical" debe ser root (en el mismo nivel jerárquico que la cámara principal).

¿Por qué música de fondo en el cargador?
La captura de pantalla muestra que el objeto "musical" no se encuentra en la escena del menú sino en la escena del cargador. Esta es una medida causada por el hecho de que la escena del menú se puede cargar más de una vez (después de la escena de partituras, la transición a la escena del menú), y cada vez que se carga, se creará otro objeto "musical" y el antiguo no se eliminará. Se puede hacer como en el ejemplo de la documentación oficial , pero decidí aprovechar el hecho de que la escena del cargador solo se carga una vez.

En esto, los problemas clave que encontré al desarrollar mi primer juego en Unity, antes de subirlos a Play Market (aún no he registrado una cuenta de desarrollador), terminaron con éxito.

PS
Si la información fue útil, puede apoyar al autor, y finalmente registrará una cuenta de desarrollador de Android.

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


All Articles