Dominar cámaras 2D en Unity: un tutorial para desarrolladores de juegos
Publicado: 2022-03-11Para un desarrollador, la cámara es una de las piedras angulares del proceso de desarrollo del juego. Desde simplemente mostrar la vista de tu juego en una aplicación de ajedrez hasta dirigir magistralmente el movimiento de la cámara en un juego 3D AAA para obtener efectos cinematográficos, las cámaras se usan básicamente en cualquier videojuego que se haya creado, incluso antes de llamarse "cámaras".
En este artículo voy a explicar cómo diseñar un sistema de cámara para juegos 2D, y también voy a explicar algunos puntos sobre cómo implementarlo en uno de los motores de juegos más populares que existen, Unity.
De 2D a 2,5D: un sistema de cámara extensible
El sistema de cámaras que vamos a diseñar juntos es modular y extensible. Tiene un núcleo básico que consta de varios componentes que garantizarán la funcionalidad básica, y luego varios componentes/efectos que se pueden usar opcionalmente, dependiendo de la situación en cuestión.
El sistema de cámara que estamos construyendo aquí está dirigido a juegos de plataformas 2D, pero puede extenderse fácilmente a otros tipos de juegos 2D, juegos 2.5D o incluso juegos 3D.
Voy a dividir la funcionalidad de la cámara en dos grupos principales: seguimiento de cámara y efectos de cámara.
Seguimiento
La mayor parte del movimiento de cámara que haremos aquí se basará en el seguimiento. Esa es la capacidad de un objeto, en este caso la cámara, para rastrear otros objetos mientras se mueven en la escena del juego. Los tipos de seguimiento que implementaremos resolverán algunos escenarios comunes que se encuentran en los juegos de plataformas 2D, pero se pueden ampliar con nuevos tipos de seguimiento para otros escenarios particulares que pueda tener.
Efectos
Implementaremos algunos efectos geniales como el movimiento de la cámara, el zoom de la cámara, el desvanecimiento de la cámara y la superposición de colores.
Empezando
Cree un nuevo proyecto 2D en Unity e importe activos estándar, especialmente el personaje RobotBoy. A continuación, cree un cuadro de tierra y agregue una instancia de personaje. Deberías poder caminar y saltar con tu personaje en tu escena actual. Asegúrese de que la cámara esté configurada en modo Ortográfico (está configurada en Perspectiva de forma predeterminada).
Seguimiento de un objetivo
El siguiente script agregará un comportamiento de seguimiento básico a nuestra cámara principal. El guión debe adjuntarse como un componente a la cámara principal en su escena y expone un campo para asignar un objeto de destino para rastrear. Luego, el script se asegura de que las coordenadas x e y de la cámara sean las mismas que las del objeto que rastrea. Todo este procesamiento se realiza durante el paso Actualizar.
[SerializeField] protected Transform trackingTarget; // ... void Update() { transform.position = new Vector3(trackingTarget.position.x, trackingTarget.position.y, transform.position.z); }
Arrastre el personaje de RobotBoy desde la jerarquía de su escena sobre el campo "Objetivo de seguimiento" expuesto por nuestro siguiente comportamiento para habilitar el seguimiento del personaje principal.
Adición de compensación
Todo bien, pero podemos ver una limitación desde el principio: el personaje siempre está en el centro de nuestra escena. Podemos ver mucho detrás del personaje, que generalmente es algo que no nos interesa, y vemos muy poco de lo que está delante de nuestro personaje, lo que podría ser perjudicial para el juego.
Para resolver esto, estamos agregando algunos campos nuevos al script que permitirán el posicionamiento de la cámara en un desplazamiento de su objetivo.
[SerializeField] float xOffset; [SerializeField] float yOffset; // ... void Update() { transform.position = new Vector3(trackingTarget.position.x + xOffset, trackingTarget.position.y + yOffset, transform.position.z); }
A continuación puede ver una posible configuración para los dos nuevos campos:
Suavizar las cosas
El movimiento de la cámara es bastante rígido y también producirá mareos en algunos jugadores por el constante movimiento percibido del entorno. Para solucionar esto, agregaremos un retraso en el seguimiento de la cámara mediante la interpolación lineal y un nuevo campo para controlar qué tan rápido la cámara se coloca en su lugar después de que el personaje comienza a cambiar su posición.
[SerializeField] protected float followSpeed; // ... protected override void Update() { float xTarget = trackingTarget.position.x + xOffset; float yTarget = trackingTarget.position.y + yOffset; float xNew = Mathf.Lerp(transform.position.x, xTarget, Time.deltaTime * followSpeed); float yNew = Mathf.Lerp(transform.position.y, yTarget, Time.deltaTime * followSpeed); transform.position = new Vector3(xNew, yNew, transform.position.z); }
Detener el mareo: Bloqueo del eje
Dado que no es agradable para su cerebro ver la cámara subir y bajar todo el tiempo junto con el personaje, estamos introduciendo el bloqueo del eje. Esto significa que podemos limitar el seguimiento a un solo eje. Luego, separaremos nuestro código de seguimiento en seguimiento independiente del eje y tomaremos en cuenta las nuevas banderas de bloqueo.
[SerializeField] protected bool isXLocked = false; [SerializeField] protected bool isYLocked = false; // ... float xNew = transform.position.x; if (!isXLocked) { xNew = Mathf.Lerp(transform.position.x, xTarget, Time.deltaTime * followSpeed); } float yNew = transform.position.y; if (!isYLocked) { yNew = Mathf.Lerp(transform.position.y, yTarget, Time.deltaTime * followSpeed); }
Sistema de carriles
Ahora que la cámara solo sigue al jugador horizontalmente, estamos limitados a la altura de una pantalla. Si el personaje sube alguna escalera o salta más alto que esta, tenemos que seguirlo. La forma en que estamos haciendo esto es usando un sistema de carriles.
Imagina el siguiente escenario:
El personaje está inicialmente en el carril inferior. Mientras el personaje permanece dentro de los límites de este carril, la cámara se moverá solo horizontalmente en el desplazamiento de altura específico del carril que podemos establecer.
Tan pronto como el personaje ingrese a otro carril, la cámara hará la transición a ese carril y continuará moviéndose horizontalmente desde allí hasta que ocurra el siguiente cambio de carril.
Se debe tener cuidado con el diseño de los carriles para evitar cambios rápidos de carril durante acciones como saltos, que pueden crear confusión para el jugador. Un carril debe cambiarse solo si el personaje del jugador va a permanecer en él por un tiempo.
Los niveles de los carriles pueden cambiar a lo largo del nivel del juego según las necesidades específicas del diseñador, o pueden interrumpirse por completo y otro sistema de seguimiento de cámara puede ocupar su lugar. Por lo tanto, necesitamos algunos limitadores para especificar zonas de carril.
Implementación
Una posible implementación es agregar carriles como objetos simples en la escena. Usaremos su coordenada de posición Y junto con el desplazamiento Y en el script de seguimiento anterior para implementar el sistema. Por lo tanto, su posicionamiento en las coordenadas X y Z no importa.
Agregue la clase LaneSystem a la cámara, junto con la clase de seguimiento, y asigne los objetos de carril a la matriz proporcionada. También asigne el personaje del jugador al campo Referencia. Como la referencia está posicionada entre un carril y otro carril, se utilizará el inferior de los dos para posicionar la cámara.
Y la clase LaneSystem se encarga de mover la cámara entre carriles, según la posición de referencia. El followSpeed se usa aquí nuevamente para la interpolación de posición, para evitar que el cambio de carril sea demasiado abrupto:
[SerializeField] Transform reference; [SerializeField] List<Transform> lanes; [SerializeField] float followSpeed = 5f; // ... void Update() { float targetYCoord = transform.position.y; if (lanes.Count > 1) { int i = 0; for (i = 0; i < lanes.Count - 1; ++i) { if ((reference.position.y > lanes[i].position.y) && (reference.position.y <= lanes[i + 1].position.y)) { targetYCoord = lanes[i].position.y; break; } } if (i == lanes.Count - 1) targetYCoord = lanes[lanes.Count - 1].position.y; } else { targetYCoord = lanes[0].position.y; } float yCoord = Mathf.Lerp(transform.position.y, targetYCoord, Time.deltaTime * followSpeed); transform.position = new Vector3(transform.position.x, yCoord, transform.position.z); }
Esta implementación no es WYSIWYG y se deja como tal como ejercicio para el lector.

Sistema de bloqueo de nodos
Tener la cámara moviéndose en los carriles es genial, pero a veces necesitamos que la cámara se fije en algo, un punto de interés (POI) en la escena del juego.
Esto se puede lograr configurando tales PDI en la escena y agregándoles un disparador colisionador. Cada vez que el personaje entra en ese disparador colisionador, movemos la cámara y nos quedamos en el PDI. A medida que el personaje se mueve y luego deja el colisionador de gatillo del PDI, volvemos a otro tipo de seguimiento, generalmente el comportamiento de seguimiento estándar.
El cambio del seguimiento de la cámara a un nodo de bloqueo y viceversa se puede realizar mediante un simple interruptor o mediante un sistema de pila, en el que los modos de seguimiento se activan y desactivan.
Implementación
Para configurar un nodo de bloqueo, simplemente cree un objeto (puede estar vacío o como en la captura de pantalla a continuación, un sprite) y adjunte un componente 2D grande de Circle Collider para que marque el área en la que estará el jugador cuando la cámara enfocar el nodo. Puede elegir cualquier tipo de colisionador, estoy eligiendo Circle como ejemplo aquí. También cree una etiqueta que pueda verificar fácilmente, como "CameraNode" y asígnela a este objeto.
Agregue la siguiente propiedad al script de seguimiento en su cámara:
public Transform TrackingTarget { get { return trackingTarget; } set { trackingTarget = value; } }
Luego adjunte la siguiente secuencia de comandos al reproductor, que le permitirá cambiar temporalmente el objetivo de la cámara al nodo de bloqueo que ha establecido. El script también recordará su objetivo anterior para que pueda volver a él cuando el jugador esté fuera del área de activación. Puede continuar y transformar esto en una pila completa si lo necesita, pero para nuestro propósito, dado que no superponemos múltiples nodos de bloqueo, esto servirá. También tenga en cuenta que puede modificar la posición del Circle Collider 2D, o agregar cualquier otro tipo de colisionador para activar el bloqueo de la cámara, esto es solo un mero ejemplo.
public class LockBehavior : MonoBehaviour { #region Public Fields [SerializeField] Camera camera; [SerializeField] string tag; #endregion #region Private private Transform previousTarget; private TrackingBehavior trackingBehavior; private bool isLocked = false; #endregion // Use this for initialization void Start() { trackingBehavior = camera.GetComponent<TrackingBehavior>(); } void OnTriggerEnter2D(Collider2D other) { if (other.tag == tag && !isLocked) { isLocked = true; PushTarget(other.transform); } } void OnTriggerExit2D(Collider2D other) { if (other.tag == tag && isLocked) { isLocked = false; PopTarget(); } } private void PushTarget(Transform newTarget) { previousTarget = trackingBehavior.TrackingTarget; trackingBehavior.TrackingTarget = newTarget; } private void PopTarget() { trackingBehavior.TrackingTarget = previousTarget; } }
Zoom de la cámara
El zoom de la cámara se puede ejecutar con la entrada del usuario o como una animación cuando queremos enfocarnos en algo como un PDI o un área más estrecha dentro de un nivel.
El zoom de la cámara 2D en Unity 3D se puede lograr manipulando el tamaño ortográfico de la cámara. Adjuntar el siguiente guión como un componente a una cámara y usar el método SetZoom para cambiar el factor de zoom producirá el efecto deseado. 1,0 significa sin zoom, 0,5 significa acercar dos veces, 2 significa alejar dos veces, y así sucesivamente.
[SerializeField] float zoomFactor = 1.0f; [SerializeField] float zoomSpeed = 5.0f; private float originalSize = 0f; private Camera thisCamera; // Use this for initialization void Start() { thisCamera = GetComponent<Camera>(); originalSize = thisCamera.orthographicSize; } // Update is called once per frame void Update() { float targetSize = originalSize * zoomFactor; if (targetSize != thisCamera.orthographicSize) { thisCamera.orthographicSize = Mathf.Lerp(thisCamera.orthographicSize, targetSize, Time.deltaTime * zoomSpeed); } } void SetZoom(float zoomFactor) { this.zoomFactor = zoomFactor; }
Sacudida de pantalla
Siempre que necesitemos mostrar un terremoto, alguna explosión o cualquier otro efecto en nuestro juego, un efecto de movimiento de cámara viene muy bien.
Un ejemplo de implementación de cómo hacerlo está disponible en GitHub: gist.github.com/ftvs/5822103. La implementación es bastante sencilla. A diferencia de los otros efectos que hemos cubierto hasta ahora, se basa en un poco de aleatoriedad.
Fundido y Superposición
Cuando nuestro nivel comienza o finaliza, un efecto de aparición o desaparición gradual es agradable. Podemos implementar esto agregando una textura de interfaz de usuario no interactuable en un panel que se extiende por toda nuestra pantalla. Inicialmente transparente, podemos rellenarlo con cualquier color y opacidad, o animarlo para lograr el efecto que queremos.
Aquí hay un ejemplo de esa configuración, tenga en cuenta que el objeto Panel de interfaz de usuario se asigna al elemento secundario "Superposición de cámara" del Objeto de cámara principal. Camera Overlay expone un script llamado Overlay que presenta lo siguiente:
[SerializeField] Image overlay; // ... public void SetOverlayColor(Color color) { overlay.color = color; }
Para tener un efecto de aparición gradual, cambie su secuencia de comandos Superposición agregando una interpolación a un color de destino que configuró con SetOverlayColor como en la siguiente secuencia de comandos, y establezca el color inicial de nuestro Panel en Negro (o Blanco) y el color de destino al color final de su superposición. Puede cambiar fadeSpeed a lo que se adapte a sus necesidades, creo que 0.8 es bueno para empezar. El valor de fadeSpeed funciona como un modificador de tiempo. 1.0 significa que sucederá en varios marcos, pero dentro de un marco de tiempo de 1 segundo. 0,8 significa que en realidad tardará 1/0,8 = 1,25 segundos en completarse.
public class Overlay : MonoBehaviour { #region Fields [SerializeField] Image overlay; [SerializeField] float fadeSpeed = 5f; [SerializeField] Color targetColor; #endregion void Update() { if (overlay.color != targetColor) { overlay.color = Color.Lerp(overlay.color, targetColor, Time.deltaTime * fadeSpeed); } } #region Public public void SetOverlayColor(Color color) { targetColor = color; } #endregion }
Envolver
En este artículo, he tratado de demostrar los componentes básicos necesarios para tener un sistema de cámara 2D modular para su juego, y también cuál es la mentalidad necesaria para diseñarlo. Naturalmente, todos los juegos tienen sus necesidades particulares, pero con el seguimiento básico y los efectos simples descritos aquí, puede recorrer un largo camino y también tener un modelo para implementar sus propios efectos. Luego puede ir aún más lejos y empaquetar todo en un paquete Unity 3D reutilizable que también puede transferir a otros proyectos.
Los sistemas de cámara son muy importantes para transmitir la atmósfera adecuada para sus jugadores. Una buena comparación que me gusta usar es cuando piensas en la diferencia entre el teatro clásico y las películas. Las cámaras y la película en sí brindaron tantas posibilidades a la escena que finalmente se convirtió en un arte por sí mismo, por lo que si no planea implementar otro juego de "Pong", las cámaras avanzadas deberían ser su herramienta preferida en cualquier proyecto de juego que desee. emprenderé a partir de ahora.