Dominando câmeras 2D no Unity: um tutorial para desenvolvedores de jogos

Publicados: 2022-03-11

Para um desenvolvedor, a câmera é um dos pilares do processo de desenvolvimento de jogos. Desde apenas mostrar sua visão de jogo em um aplicativo de xadrez até direcionar com maestria o movimento da câmera em um jogo 3D AAA para obter efeitos cinematográficos, as câmeras são basicamente usadas em qualquer videogame já feito, mesmo antes de serem chamadas de “câmeras”.

Neste artigo, vou explicar como projetar um sistema de câmera para jogos 2D, e também explicar alguns pontos sobre como implementá-lo em um dos mecanismos de jogo mais populares, o Unity.

De 2D a 2.5D: um sistema de câmera extensível

O sistema de câmeras que vamos projetar juntos é modular e extensível. Ele possui um núcleo básico composto por vários componentes que garantirão a funcionalidade básica e, em seguida, vários componentes/efeitos que podem ser usados ​​opcionalmente, dependendo da situação em questão.

O sistema de câmeras que estamos construindo aqui é voltado para jogos de plataforma 2D, mas pode ser facilmente estendido para outros tipos de jogos 2D, jogos 2.5D ou até mesmo jogos 3D.

Dominando a câmera 2D no Unity: um tutorial para desenvolvedores de jogos

Dominando a câmera 2D no Unity: um tutorial para desenvolvedores de jogos
Tweet

Vou dividir a funcionalidade da câmera em dois grupos principais: rastreamento de câmera e efeitos de câmera.

Monitorando

A maior parte do movimento de câmera que faremos aqui será baseado em rastreamento. Essa é a capacidade de um objeto, neste caso a câmera, de rastrear outros objetos enquanto eles se movem na cena do jogo. Os tipos de rastreamento que implementaremos resolverão alguns cenários comuns encontrados em jogos de plataforma 2D, mas eles podem ser estendidos com novos tipos de rastreamento para outros cenários específicos que você possa ter.

Efeitos

Implementaremos alguns efeitos interessantes, como trepidação da câmera, zoom da câmera, desbotamento da câmera e sobreposição de cores.

Começando

Crie um novo projeto 2D no Unity e importe ativos padrão, especialmente o personagem RobotBoy. Em seguida, crie uma caixa de chão e adicione uma instância de personagem. Você deve ser capaz de andar e pular com seu personagem em sua cena atual. Certifique-se de que a câmera esteja configurada para o modo Ortográfico (por padrão, ela está configurada para Perspectiva).

Acompanhando um alvo

O script a seguir adicionará o comportamento básico de rastreamento à nossa câmera principal. O script deve ser anexado como um componente à câmera principal em sua cena e expõe um campo para atribuir um objeto de destino a ser rastreado. Em seguida, o script garante que as coordenadas x e y da câmera sejam as mesmas do objeto que ela rastreia. Todo esse processamento é feito durante a etapa de atualização.

 [SerializeField] protected Transform trackingTarget; // ... void Update() { transform.position = new Vector3(trackingTarget.position.x, trackingTarget.position.y, transform.position.z); }

Arraste o personagem RobotBoy da sua hierarquia de cena sobre o campo “Tracking Target” exposto pelo nosso comportamento a seguir para permitir o rastreamento do personagem principal.

Adicionando Deslocamento

Tudo bem, mas podemos ver uma limitação logo de cara: o personagem está sempre no centro da nossa cena. Podemos ver muito por trás do personagem, que geralmente são coisas que não nos interessam, e estamos vendo muito pouco do que está à frente do nosso personagem, o que pode ser prejudicial à jogabilidade.

Para resolver isso, estamos adicionando alguns novos campos ao script que permitirão o posicionamento da câmera em um deslocamento de seu alvo.

 [SerializeField] float xOffset; [SerializeField] float yOffset; // ... void Update() { transform.position = new Vector3(trackingTarget.position.x + xOffset, trackingTarget.position.y + yOffset, transform.position.z); }

Abaixo você pode ver uma possível configuração para os dois novos campos:

Suavizando as coisas

O movimento da câmera é bastante rígido e também produzirá tontura em alguns jogadores devido ao constante movimento percebido do ambiente. Para corrigir isso, adicionaremos algum atraso no rastreamento da câmera usando interpolação linear e um novo campo para controlar a rapidez com que a câmera se posiciona após o personagem começar a mudar de posição.

 [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); } 

Pare a tontura: travamento do eixo

Como não é agradável para o seu cérebro ver a câmera subindo e descendo o tempo todo junto com o personagem, estamos introduzindo o travamento de eixo. Isso significa que podemos limitar o rastreamento a apenas um eixo. Em seguida, separaremos nosso código de rastreamento em rastreamento independente de eixo e levaremos em consideração os novos sinalizadores de bloqueio.

 [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 pistas

Agora que a câmera apenas rastreia o jogador horizontalmente, estamos limitados à altura de uma tela. Se o personagem subir alguma escada ou pular mais alto que isso, temos que seguir. A maneira como estamos fazendo isso é usando um sistema de pista.

Imagine o seguinte cenário:

O personagem está inicialmente na pista inferior. Enquanto o personagem permanecer dentro dos limites desta pista, a câmera se moverá apenas horizontalmente no deslocamento de altura específico da pista que pudermos definir.

Assim que o personagem entrar em outra pista, a câmera fará a transição para essa pista e continuará a se mover horizontalmente a partir daí até que ocorra a próxima mudança de pista.

Deve-se tomar cuidado no design da pista para evitar trocas rápidas de pista durante ações como saltos, o que pode criar confusão para o jogador. Uma pista deve ser alterada apenas se o personagem do jogador permanecer nela por um tempo.

Os níveis das pistas podem mudar ao longo do nível do jogo com base nas necessidades específicas do designer, ou podem ser interrompidos completamente e outro sistema de rastreamento de câmera pode substituí-lo. Portanto, precisamos de alguns limitadores para especificar zonas de pista.

Implementação

Uma implementação possível é adicionar lanes como objetos simples na cena. Usaremos sua coordenada de posição Y emparelhada com o deslocamento Y no script de rastreamento acima para implementar o sistema. Portanto, seu posicionamento nas coordenadas X e Z não importa.

Adicione a classe LaneSystem à câmera, juntamente com a classe de rastreamento, e atribua os objetos de pista à matriz fornecida. Também atribua o personagem do jogador ao campo Referência. Como a referência está posicionada entre uma pista e outra pista, a mais baixa das duas será utilizada para posicionar a câmera.

E a classe LaneSystem cuida de mover a câmera entre as faixas, com base na posição de referência. O followSpeed ​​é usado aqui novamente para interpolação de posição, para evitar que a mudança de faixa seja muito abrupta:

 [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 implementação não é WYSIWYG, e é deixada como um exercício para o leitor.

Sistema de Nó de Bloqueio

Fazer a câmera se mover nas pistas é ótimo, mas às vezes precisamos que a câmera esteja travada em algo, um ponto de interesse (POI) na cena do jogo.

Isso pode ser feito configurando tal POI na cena e anexando um colisor de gatilho a eles. Sempre que o personagem entra nesse colisor de gatilho, movemos a câmera e ficamos no POI. À medida que o personagem se move e sai do colisor de gatilho do POI, voltamos a outro tipo de rastreamento, geralmente o comportamento padrão de acompanhamento.

A comutação do rastreamento da câmera para um nó de bloqueio e vice-versa pode ser feita por um simples comutador ou por um sistema de pilha, no qual os modos de rastreamento são pressionados e ativados.

Implementação

Para configurar um nó de bloqueio, basta criar um objeto (pode estar vazio ou como na captura de tela abaixo, um sprite) e anexar um grande componente 2D do Circle Collider a ele para marcar a área em que o jogador estará quando a câmera for focar o nó. Você pode escolher qualquer tipo de colisor, estou escolhendo Circle como exemplo aqui. Crie também uma tag que você possa verificar facilmente, como “CameraNode” e atribua-a a este objeto.

Adicione a seguinte propriedade ao script de rastreamento em sua câmera:

 public Transform TrackingTarget { get { return trackingTarget; } set { trackingTarget = value; } }

Em seguida, anexe o seguinte script ao player, que permitirá alternar temporariamente o alvo da câmera para o nó de bloqueio que você definiu. O script também lembrará seu alvo anterior para que possa voltar a ele quando o jogador estiver fora da área de disparo. Você pode ir em frente e transformar isso em uma pilha completa, se precisar, mas para o nosso propósito, já que não sobrepomos vários nós de bloqueio, isso funcionará. Além disso, esteja ciente de que você pode ajustar a posição do Circle Collider 2D, ou novamente adicionar qualquer outro tipo de colisor para acionar o bloqueio da câmera, este é apenas um mero exemplo.

 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 da câmera

O zoom da câmera pode ser executado na entrada do usuário ou como uma animação quando queremos focar em algo como um POI ou uma área mais estreita dentro de um nível.

O zoom da câmera 2D no Unity 3D pode ser obtido manipulando o tamanho ortográfico da câmera. Anexar o próximo script como um componente a uma câmera e usar o método SetZoom para alterar o fator de zoom produzirá o efeito desejado. 1,0 significa sem zoom, 0,5 significa aumentar duas vezes, 2 significa diminuir duas vezes e assim por diante.

 [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; }

Tremor da tela

Sempre que precisamos mostrar um terremoto, alguma explosão ou qualquer outro efeito em nosso jogo, um efeito de trepidação da câmera vem a calhar.

Um exemplo de implementação de como fazer isso está disponível no GitHub: gist.github.com/ftvs/5822103. A implementação é bastante simples. Ao contrário dos outros efeitos que abordamos até agora, ele depende de um pouco de aleatoriedade.

Desvanecimento e sobreposição

Quando o nosso nível começa ou termina, um efeito fade-in ou out é bom. Podemos implementar isso adicionando uma textura de interface do usuário não interativa em um painel que se estende por toda a tela. Inicialmente transparente, podemos preenchê-lo com qualquer cor e opacidade, ou animar isso para obter o efeito que desejamos.

Aqui está um exemplo dessa configuração, observe o objeto UI Panel sendo atribuído ao filho “Camera Overlay” do objeto Main Camera. Camera Overlay expõe um script chamado Overlay que apresenta o seguinte:

 [SerializeField] Image overlay; // ... public void SetOverlayColor(Color color) { overlay.color = color; } 

Para ter um efeito fade-in, altere seu script Overlay adicionando uma interpolação a uma cor de destino que você definiu com SetOverlayColor como no próximo script e defina a cor inicial do nosso Painel para Preto (ou Branco) e a cor de destino para a cor final da sua sobreposição. Você pode alterar o fadeSpeed ​​para o que for mais adequado às suas necessidades, acho que 0,8 é bom para começar. O valor de fadeSpeed ​​funciona como um modificador de tempo. 1.0 significa que acontecerá em vários quadros, mas dentro de um período de 1 segundo. 0,8 significa que levará 1/0,8 = 1,25 segundos para ser concluído.

 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 }

Embrulhar

Neste artigo, tentei demonstrar os componentes básicos necessários para ter um sistema de câmera 2D modular em vigor para o seu jogo, e também qual é a mentalidade necessária para projetá-lo. Naturalmente, todos os jogos têm suas necessidades específicas, mas com o rastreamento básico e os efeitos simples descritos aqui, você pode percorrer um longo caminho e também ter um plano para implementar seus próprios efeitos. Então você pode ir ainda mais longe e empacotar tudo em um pacote reutilizável Unity 3D que você também pode transferir para outros projetos.

Os sistemas de câmeras são muito importantes para transmitir a atmosfera certa para seus jogadores. Uma boa comparação que gosto de usar é quando você pensa na diferença entre teatro clássico e cinema. As câmeras e o próprio filme trouxeram tantas possibilidades para a cena que acabou evoluindo para uma arte própria, então se você não está planejando implementar outro jogo “Pong”, câmeras avançadas devem ser sua ferramenta de escolha em qualquer projeto de jogo que você assumirei a partir de agora.

Relacionado: Unity com MVC: como elevar o nível do seu desenvolvimento de jogos