Como Fazer um Jogo de Plataforma na Unity 2D
Para fazer um jogo de plataforma na Unity, você precisa de cinco peças funcionando juntas: uma fase em Tilemap com colisão limpa, um controller que corre e pula com coyote time e jump buffer, uma câmera que segue sem enjoar, algo que ameaça (inimigo e espinho) e uma saída que leva à próxima fase. Este tutorial monta exatamente isso em C# do Unity 6.3 LTS, como um projeto fechado de uma fase do começo ao fim.
O foco aqui não é cada peça isolada (o blog tem posts para cada uma), e sim a montagem: como as peças se encaixam para formar algo que você consegue jogar, mostrar e expandir.
Escopo: uma fase do começo ao fim
A lista do que entra, e do que fica de fora de propósito:
- Entra: correr com aceleração, pulo com altura variável, coyote time, jump buffer, Tilemap com colisão, câmera Cinemachine com limites, um tipo de inimigo que patrulha, espinhos, gemas coletáveis, porta de fim de fase.
- Fica de fora: ataque, vida com vários pontos, menu, save. Encostou no inimigo ou no espinho, a fase reinicia.
Uma fase de 2 minutos bem feita ensina mais que dez fases pela metade. Crie o projeto pelo Unity Hub com o template Universal 2D, que já traz o Input System ativo e os pacotes 2D, incluindo o Tilemap.
Fase com Tilemap e colisão
- GameObject > 2D Object > Tilemap > Rectangular. Isso cria um
Gridcom umTilemapfilho. Renomeie o filho paraGround. - Abra Window > 2D > Tile Palette, crie uma paleta nova e arraste seu tileset (16x16 pixels por tile, importado com Filter Mode
Pointe Pixels Per Unit 16) para dentro dela. - Pinte o chão e as plataformas com o pincel (atalho B).
- No
Ground, adicione Tilemap Collider 2D e mude Composite Operation paraMerge. - Adicione Composite Collider 2D no mesmo objeto. Ele adiciona um Rigidbody2D: mude o Body Type para
Static. No Composite Collider 2D, use Geometry TypePolygons. - Crie a layer
Grounde coloque o Tilemap nela.
Por que o Composite: sem ele, cada tile é um quadrado separado, e o collider do personagem bate nas quinas entre dois tiles vizinhos no meio do chão liso. É o famoso "personagem enganchando no nada". O Merge junta todos os tiles numa forma única.
Crie um segundo Tilemap Hazards para os espinhos, com Tilemap Collider 2D marcado como Is Trigger. Espinho é trigger porque ele não é chão: encostar mata, não segura.
Controller de plataforma na Unity: correr e pular
No personagem: Rigidbody2D (Gravity Scale 3, Collision Detection Continuous, Interpolate Interpolate, Freeze Rotation Z marcado) e CapsuleCollider2D. Cápsula, porque a base arredondada desliza sobre degraus pequenos em vez de travar. Crie um filho vazio GroundCheck nos pés. Agora PlatformerController.cs:
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(Rigidbody2D))]
public class PlatformerController : MonoBehaviour
{
[Header("Correr")]
[SerializeField] private float maxSpeed = 7f;
[SerializeField] private float acceleration = 60f;
[SerializeField] private float deceleration = 70f;
[SerializeField] private float airControl = 0.6f;
[Header("Pulo")]
[SerializeField] private float jumpVelocity = 13f;
[SerializeField] private float jumpCutMultiplier = 0.5f;
[SerializeField] private float coyoteTime = 0.1f;
[SerializeField] private float jumpBufferTime = 0.12f;
[SerializeField] private float fallGravityMultiplier = 1.6f;
[SerializeField] private float maxFallSpeed = 20f;
[Header("Chão")]
[SerializeField] private Transform groundCheck;
[SerializeField] private Vector2 groundCheckSize = new Vector2(0.6f, 0.08f);
[SerializeField] private LayerMask groundLayers;
private Rigidbody2D rb;
private InputAction moveAction;
private InputAction jumpAction;
private float baseGravity;
private float moveInput;
private float lastGroundedTime = -10f;
private float lastJumpPressedTime = -10f;
private bool isGrounded;
private bool isJumping;
public bool IsGrounded => isGrounded;
private void Awake()
{
rb = GetComponent<Rigidbody2D>();
moveAction = InputSystem.actions.FindAction("Move");
jumpAction = InputSystem.actions.FindAction("Jump");
baseGravity = rb.gravityScale;
}
private void Update()
{
moveInput = moveAction.ReadValue<Vector2>().x;
if (jumpAction.WasPressedThisFrame())
{
lastJumpPressedTime = Time.time;
}
if (jumpAction.WasReleasedThisFrame() && isJumping && rb.linearVelocity.y > 0f)
{
rb.linearVelocity = new Vector2(rb.linearVelocity.x, rb.linearVelocity.y * jumpCutMultiplier);
isJumping = false;
}
if (Mathf.Abs(moveInput) > 0.01f)
{
Vector3 scale = transform.localScale;
scale.x = Mathf.Abs(scale.x) * Mathf.Sign(moveInput);
transform.localScale = scale;
}
}
private void FixedUpdate()
{
isGrounded = Physics2D.OverlapBox(groundCheck.position, groundCheckSize, 0f, groundLayers) != null;
if (isGrounded && rb.linearVelocity.y <= 0.01f)
{
lastGroundedTime = Time.time;
isJumping = false;
}
float targetSpeed = moveInput * maxSpeed;
float rate = Mathf.Abs(targetSpeed) > 0.01f ? acceleration : deceleration;
if (!isGrounded)
{
rate *= airControl;
}
float newX = Mathf.MoveTowards(rb.linearVelocity.x, targetSpeed, rate * Time.fixedDeltaTime);
float newY = rb.linearVelocity.y;
bool inCoyoteWindow = Time.time - lastGroundedTime <= coyoteTime;
bool jumpBuffered = Time.time - lastJumpPressedTime <= jumpBufferTime;
if (jumpBuffered && inCoyoteWindow && !isJumping)
{
newY = jumpVelocity;
isJumping = true;
lastJumpPressedTime = -10f;
lastGroundedTime = -10f;
}
rb.gravityScale = newY < 0f ? baseGravity * fallGravityMultiplier : baseGravity;
newY = Mathf.Max(newY, -maxFallSpeed);
rb.linearVelocity = new Vector2(newX, newY);
}
private void OnDrawGizmosSelected()
{
if (groundCheck == null)
{
return;
}
Gizmos.color = Color.green;
Gizmos.DrawWireCube(groundCheck.position, groundCheckSize);
}
}
O que faz o controle parecer bom, decisão por decisão:
- Aceleração com
Mathf.MoveTowardsem vez de velocidade instantânea. O personagem leva uma fração de segundo para atingir a velocidade máxima e para frear, o que dá peso sem deixar o controle mole. No ar, a taxa cai para 60%: dá para corrigir o pulo, mas não virar de direção no meio dele como se estivesse no chão. - Pulo com altura variável: soltar o botão durante a subida corta a velocidade vertical pela metade. Toque curto, pulo baixo; segurar, pulo alto.
- Gravidade maior na queda (
fallGravityMultiplier): o personagem sobe e desce com tempos diferentes, e a queda mais rápida tira a sensação de "flutuar na lua". Physics2D.OverlapBoxnos pés para o ground check, só contra a layerGround. Uma caixa fina e um pouco mais estreita que o personagem evita contar a parede como chão. O gizmo verde mostra a caixa na Scene view.- Tudo usa
linearVelocity, o nome da velocidade do Rigidbody2D no Unity 6.
Detalhes do ground check e das variações dele estão em pulo 2D e ground check na Unity.
Coyote time e jump buffer
Os dois são guardados como instantes, não como contadores:
lastGroundedTimeé o último momento em que o personagem estava no chão. Se o jogador apertar pulo até 0,1 s depois de sair da borda, o pulo vale. É o coyote time: o personagem "ainda está" na plataforma por um instante, como o coiote do desenho que só cai quando olha para baixo.lastJumpPressedTimeé o último aperto do pulo. Se o personagem tocar o chão até 0,12 s depois do aperto, o pulo sai na hora do pouso. É o jump buffer: o jogo perdoa o aperto um pouco adiantado.
Ao pular, os dois são zerados (-10f), para o mesmo aperto não gerar dois pulos. Sem essas duas janelas, o jogador sente que o jogo "comeu" o pulo, e ele está certo. O post sobre coyote time e sistema de pulo explica a teoria com mais exemplos.
Com fase e controle prontos, você chega ao ponto em que muita gente para: cada sistema funciona sozinho, mas fazer tudo conversar (câmera que segue sem mostrar o vazio, inimigo que mata, gema que conta, porta que só abre com as gemas, reinício que não quebra nada) parece um degrau grande demais. Montar essa integração com calma, num jogo inteiro, é o que um curso de Unity com projetos completos faz você praticar.
Câmera com Cinemachine
Instale o Cinemachine pelo Package Manager (Unity Registry). No Unity 6 a versão instalada é a 3.x, que mudou os nomes da versão 2: o componente é CinemachineCamera (não mais CinemachineVirtualCamera) e o namespace no código é Unity.Cinemachine.
- GameObject > Cinemachine > Cinemachine Camera. A Main Camera ganha um Cinemachine Brain automaticamente.
- Na Cinemachine Camera, arraste o personagem para Tracking Target.
- Em Position Control, escolha Position Composer. Ajuste Damping (0,5 em X e Y), uma Dead Zone pequena e Lookahead Time de 0,2 para a câmera adiantar um pouco na direção do movimento.
- Para a câmera não mostrar o vazio fora da fase, crie um GameObject
CameraBoundscom PolygonCollider2D contornando a fase, adicione a extensão Cinemachine Confiner 2D pela opção Add Extension da Cinemachine Camera e arraste oCameraBoundspara Bounding Shape 2D.
Coloque o CameraBounds numa layer que não colide com nada, para o polígono não virar parede.
Inimigo, perigo e coletável
Um gerenciador simples da fase concentra reinício, contagem de gemas e troca de cena. Crie LevelManager.cs:
using UnityEngine;
using UnityEngine.SceneManagement;
public class LevelManager : MonoBehaviour
{
public static LevelManager Instance { get; private set; }
[SerializeField] private string nextSceneName = "Level02";
public int Gems { get; private set; }
private void Awake()
{
Instance = this;
}
public void AddGem()
{
Gems++;
}
public void RestartLevel()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
public void LoadNextLevel()
{
SceneManager.LoadScene(nextSceneName);
}
}
O inimigo anda até a borda ou até a parede e vira. Crie WalkerEnemy.cs:
using UnityEngine;
[RequireComponent(typeof(Rigidbody2D))]
public class WalkerEnemy : MonoBehaviour
{
[SerializeField] private float speed = 2f;
[SerializeField] private Transform edgeCheck;
[SerializeField] private float wallCheckDistance = 0.6f;
[SerializeField] private LayerMask groundLayers;
private Rigidbody2D rb;
private float direction = 1f;
private void Awake()
{
rb = GetComponent<Rigidbody2D>();
}
private void FixedUpdate()
{
bool groundAhead = Physics2D.Raycast(edgeCheck.position, Vector2.down, 0.5f, groundLayers);
bool wallAhead = Physics2D.Raycast(rb.position, new Vector2(direction, 0f), wallCheckDistance, groundLayers);
if (!groundAhead || wallAhead)
{
direction = -direction;
Vector3 scale = transform.localScale;
scale.x = Mathf.Abs(scale.x) * direction;
transform.localScale = scale;
}
rb.linearVelocity = new Vector2(direction * speed, rb.linearVelocity.y);
}
}
O edgeCheck é um filho posicionado à frente e abaixo do inimigo. Como ele é filho, vira junto quando a escala inverte, e sempre olha para o lado em que o inimigo anda. O RaycastHit2D devolvido pelo Physics2D.Raycast se converte direto em bool: verdadeiro se acertou algo.
Espinhos e inimigos compartilham o mesmo script de perigo, em Hazard.cs:
using UnityEngine;
public class Hazard : MonoBehaviour
{
private void OnTriggerEnter2D(Collider2D other)
{
TryKill(other);
}
private void OnCollisionEnter2D(Collision2D collision)
{
TryKill(collision.collider);
}
private void TryKill(Collider2D other)
{
if (other.CompareTag("Player"))
{
LevelManager.Instance.RestartLevel();
}
}
}
E a gema, em Gem.cs:
using UnityEngine;
public class Gem : MonoBehaviour
{
private bool collected;
private void OnTriggerEnter2D(Collider2D other)
{
if (collected || !other.CompareTag("Player"))
{
return;
}
collected = true;
LevelManager.Instance.AddGem();
gameObject.SetActive(false);
}
}
Fim de fase e próxima cena
A porta exige um número mínimo de gemas. Crie LevelExit.cs:
using UnityEngine;
public class LevelExit : MonoBehaviour
{
[SerializeField] private int gemsRequired = 3;
[SerializeField] private GameObject lockedVisual;
private void Update()
{
if (lockedVisual != null)
{
lockedVisual.SetActive(LevelManager.Instance.Gems < gemsRequired);
}
}
private void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("Player") && LevelManager.Instance.Gems >= gemsRequired)
{
LevelManager.Instance.LoadNextLevel();
}
}
}
Para a troca de cena funcionar, as duas cenas precisam estar em File > Build Profiles > Scene List. Duplique a cena da fase (Ctrl+D no Project), renomeie para Level02 e mude o desenho dos tiles: você tem um jogo com duas fases.
Testar a fase antes de expandir
Antes de adicionar qualquer mecânica nova, jogue a fase inteira cinco vezes seguidas e anote onde você morreu e onde sentiu que o jogo errou, não você. Três sinais de que o problema é a fase, não o jogador:
- Pulo que só passa no limite exato. Se a distância entre duas plataformas exige o pulo máximo com timing perfeito, aumente 1 tile de folga ou abaixe a plataforma de destino. Pulo no limite é para o fim da fase, não para o primeiro minuto.
- Perigo que aparece fora da tela. Inimigo ou espinho que o jogador não tem como ver antes de cair em cima. Ajuste o Lookahead da câmera ou mova o perigo.
- Repetição longa depois de morrer. Se morrer no fim da fase obriga a refazer 90 segundos, divida a fase com um checkpoint ou encurte.
Peça para outra pessoa jogar sem você explicar nada e observe em silêncio. Os lugares onde ela trava mostram o que o tutorial da fase precisa ensinar pelo próprio desenho dos tiles.
Erros comuns e como expandir
Pula duas vezes no mesmo aperto. Faltou zerar lastJumpPressedTime ao pular, ou o ground check está alto demais e conta como chão logo depois do pulo.
Não pula na borda. O ground check está mais largo que o personagem, ou o coyote time é zero.
Engancha no chão. Falta o Composite Collider 2D, ou o personagem usa BoxCollider2D em vez de cápsula.
A câmera treme. O Rigidbody2D do personagem está sem Interpolate.
Inimigo cai da plataforma. O edgeCheck está em cima do chão em vez de à frente da borda.
Para expandir: plataforma móvel, pulo na parede, ataque com espada e vida com vários pontos. Cada um entra no mesmo projeto sem reescrever o controller. Para entender o movimento por trás de tudo isso, o post de movimento de personagem de plataforma 2D aprofunda aceleração, atrito e as curvas de pulo.
Perguntas frequentes
O que é coyote time e jump buffer?
Coyote time é uma janela curta (por volta de 0,1 s) em que o jogador ainda pode pular depois de sair da borda da plataforma. Jump buffer guarda o aperto do pulo feito um pouco antes de tocar o chão e executa o pulo assim que ele pousa. Os dois fazem o controle parecer justo.
Qual Cinemachine usar no Unity 6?
O Cinemachine 3, instalado pelo Package Manager. Nele o componente principal é o CinemachineCamera (no lugar do CinemachineVirtualCamera da versão 2) e o seguimento 2D é feito pelo Position Composer. O código fica no namespace Unity.Cinemachine.
Por que o personagem engancha entre os tiles na Unity?
Cada tile ganha o próprio collider e o personagem bate nas quinas internas entre eles. Configure o Tilemap Collider 2D com Composite Operation em Merge e adicione um Composite Collider 2D, que junta tudo numa forma só, e use um collider de cápsula no personagem.
Unity ou Godot para jogo de plataforma 2D?
As duas fazem plataforma 2D muito bem. A Unity tem Tilemap, Cinemachine e um ecossistema grande de pacotes; o Godot tem um editor 2D leve e CharacterBody2D pronto para plataforma. O que mais pesa é terminar um projeto em qualquer uma delas.



