Capítulo 6 — Colisão, Física Simples e Movimentação no MonoGame

Quando o jogo deixa de apenas se mover e começa a “sentir” o mundo

Nos capítulos anteriores, você aprendeu a preparar o ambiente, entender o game loop, desenhar sprites, carregar texturas e capturar entrada do jogador com teclado, mouse e controle.

Agora chegamos a um ponto decisivo: fazer o mundo do jogo reagir.

Um personagem andando na tela é interessante. Mas um personagem que bate em paredes, pisa em plataformas, coleta moedas, sofre impacto de inimigos, pula com gravidade e respeita os limites do cenário começa a parecer um jogo de verdade.

É aqui que entram três pilares fundamentais:

🎮 movimentação — como o personagem anda, corre, pula ou cai;
🧱 colisão — como objetos detectam contato entre si;
⚙️ física simples — como velocidade, gravidade, aceleração e forças básicas afetam o jogo.

No MonoGame, você não recebe um sistema completo de física pronto como em algumas engines visuais. Isso pode parecer uma desvantagem no começo, mas também é uma excelente oportunidade para aprender os fundamentos. Você controla a lógica, decide como os objetos se comportam e constrói exatamente o tipo de movimentação que seu jogo precisa.

A documentação oficial do MonoGame descreve Vector2 como uma estrutura para representar e manipular vetores 2D, normalmente usados para direção, magnitude, coordenadas e outros dados bidimensionais. Isso é essencial para movimentação, velocidade, posição e aceleração em jogos 2D.

Em termos simples:

Movimentação dá vida ao personagem. Colisão dá regras ao mundo. Física simples dá sensação ao jogo.


🎮 O que é movimentação em um jogo 2D?

Movimentação é a forma como um objeto muda de posição ao longo do tempo.

No MonoGame, geralmente usamos:

Vector2 position;
Vector2 velocity;

A posição indica onde o objeto está.

A velocidade indica para onde e com que intensidade ele está se movendo.

Exemplo:

Vector2 playerPosition = new Vector2(100, 200);
Vector2 playerVelocity = Vector2.Zero;

Se você altera diretamente a posição:

playerPosition.X += 5;

o personagem se move 5 pixels para a direita.

Mas em jogos mais organizados, o ideal é pensar assim:

playerPosition += playerVelocity * deltaTime;

Essa abordagem permite criar movimentos mais suaves, consistentes e fáceis de controlar.


🧠 O papel do tempo na movimentação

Movimentação sem tempo costuma ser inconsistente.

Se você faz:

playerPosition.X += 5;

o personagem anda 5 pixels por atualização. Mas e se o jogo rodar mais rápido em uma máquina e mais lento em outra? A velocidade percebida pode mudar.

Por isso usamos o tempo decorrido entre frames, normalmente chamado de delta time.

No MonoGame, GameTime fornece informações como TotalGameTime e ElapsedGameTime. A documentação oficial define ElapsedGameTime como o tempo decorrido desde a última chamada de atualização.

Exemplo:

float deltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds;

Depois:

playerPosition += playerVelocity * deltaTime;

Isso transforma a velocidade em algo mais estável, normalmente medido em pixels por segundo.

A própria documentação introdutória do MonoGame recomenda experimentar o efeito de multiplicar a velocidade por gameTime.ElapsedGameTime.TotalSeconds, mostrando como isso muda o comportamento do movimento.


🏃 Movimento básico com teclado

Vamos começar com o movimento mais simples: personagem andando para os lados.

Campos:

private Vector2 _playerPosition;
private Vector2 _playerVelocity;
private float _playerSpeed = 250f;

No Initialize:

protected override void Initialize()
{
_playerPosition = new Vector2(100, 300);
_playerVelocity = Vector2.Zero;

base.Initialize();
}

No Update:

protected override void Update(GameTime gameTime)
{
float deltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds;

KeyboardState keyboard = Keyboard.GetState();

_playerVelocity = Vector2.Zero;

if (keyboard.IsKeyDown(Keys.D) || keyboard.IsKeyDown(Keys.Right))
_playerVelocity.X = _playerSpeed;

if (keyboard.IsKeyDown(Keys.A) || keyboard.IsKeyDown(Keys.Left))
_playerVelocity.X = -_playerSpeed;

_playerPosition += _playerVelocity * deltaTime;

base.Update(gameTime);
}

Esse código faz o personagem andar para direita e esquerda.

A lógica é simples:

  • se apertar direita, velocidade positiva no eixo X;
  • se apertar esquerda, velocidade negativa no eixo X;
  • posição recebe velocidade multiplicada pelo tempo.

🧭 Movimento em quatro direções

Para jogos top-down, como RPGs, shooters 2D ou jogos de exploração, o personagem pode se mover para cima, baixo, esquerda e direita.

protected override void Update(GameTime gameTime)
{
float deltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds;

KeyboardState keyboard = Keyboard.GetState();

Vector2 movement = Vector2.Zero;

if (keyboard.IsKeyDown(Keys.D) || keyboard.IsKeyDown(Keys.Right))
movement.X += 1;

if (keyboard.IsKeyDown(Keys.A) || keyboard.IsKeyDown(Keys.Left))
movement.X -= 1;

if (keyboard.IsKeyDown(Keys.W) || keyboard.IsKeyDown(Keys.Up))
movement.Y -= 1;

if (keyboard.IsKeyDown(Keys.S) || keyboard.IsKeyDown(Keys.Down))
movement.Y += 1;

if (movement != Vector2.Zero)
movement.Normalize();

_playerPosition += movement * _playerSpeed * deltaTime;

base.Update(gameTime);
}

O detalhe importante é:

movement.Normalize();

Sem normalizar, o personagem pode andar mais rápido na diagonal, porque estaria somando movimento horizontal e vertical ao mesmo tempo.


🧱 O que é colisão?

Colisão é a detecção de contato entre dois objetos.

Exemplos:

  • jogador encosta em uma parede;
  • personagem pisa em uma plataforma;
  • tiro atinge inimigo;
  • jogador coleta moeda;
  • inimigo encosta no jogador;
  • bola bate na raquete;
  • personagem chega ao limite da tela.

Sem colisão, os objetos atravessam tudo.

Com colisão, o jogo ganha regras.

Em MonoGame, uma das formas mais simples de detectar colisão 2D é usando Rectangle.

A documentação oficial define Rectangle como uma estrutura que descreve um retângulo 2D, com posição, largura e altura. Ela também oferece métodos como Contains, Intersect e Intersects, sendo Intersects(Rectangle) usado para verificar se outro retângulo intersecta o retângulo atual.


📦 Colisão AABB: a mais simples e útil para começar

A forma mais comum para iniciantes é a colisão AABB.

AABB significa:

Axis-Aligned Bounding Box

Em português: caixa delimitadora alinhada aos eixos.

Na prática, é um retângulo em volta do objeto.

Exemplo:

Rectangle playerBounds = new Rectangle(
(int)_playerPosition.X,
(int)_playerPosition.Y,
_playerTexture.Width,
_playerTexture.Height
);

Para uma moeda:

Rectangle coinBounds = new Rectangle(
(int)_coinPosition.X,
(int)_coinPosition.Y,
_coinTexture.Width,
_coinTexture.Height
);

Para testar colisão:

if (playerBounds.Intersects(coinBounds))
{
// O jogador encostou na moeda
}

Esse é um dos fundamentos mais importantes de jogos 2D.


🎯 Exemplo: coletando uma moeda

Imagine que você tem:

private Vector2 _playerPosition;
private Vector2 _coinPosition;

private Texture2D _playerTexture;
private Texture2D _coinTexture;

private bool _coinCollected = false;
private int _score = 0;

No Update:

Rectangle playerBounds = new Rectangle(
(int)_playerPosition.X,
(int)_playerPosition.Y,
_playerTexture.Width,
_playerTexture.Height
);

Rectangle coinBounds = new Rectangle(
(int)_coinPosition.X,
(int)_coinPosition.Y,
_coinTexture.Width,
_coinTexture.Height
);

if (!_coinCollected && playerBounds.Intersects(coinBounds))
{
_coinCollected = true;
_score += 10;
}

No Draw:

_spriteBatch.Begin();

_spriteBatch.Draw(_playerTexture, _playerPosition, Color.White);

if (!_coinCollected)
_spriteBatch.Draw(_coinTexture, _coinPosition, Color.White);

_spriteBatch.End();

A lógica é clara:

  • se o jogador encostar na moeda;
  • a moeda é marcada como coletada;
  • a pontuação aumenta;
  • a moeda deixa de ser desenhada.

Esse pequeno exemplo já representa uma mecânica real de jogo.


🧠 Colisão não é só encostar: é decidir o que acontece depois

Detectar colisão é apenas metade do trabalho.

A pergunta mais importante é:

O que acontece quando a colisão é detectada?

Exemplos:

ColisãoConsequência
Jogador + moedaaumenta pontuação
Jogador + paredebloqueia movimento
Jogador + inimigoperde vida
Tiro + inimigoinimigo sofre dano
Jogador + plataformapode pisar
Jogador + portamuda de fase
Bola + parederebate

Colisão sem resposta não cria mecânica.

A resposta da colisão é o que transforma contato em regra de jogo.


🧱 Limitando o personagem à tela

Antes de criar paredes e plataformas, você pode começar impedindo o jogador de sair da tela.

Exemplo:

_playerPosition.X = MathHelper.Clamp(
_playerPosition.X,
0,
_graphics.PreferredBackBufferWidth - _playerTexture.Width
);

_playerPosition.Y = MathHelper.Clamp(
_playerPosition.Y,
0,
_graphics.PreferredBackBufferHeight - _playerTexture.Height
);

Isso limita o personagem dentro da janela.

Se ele tentar passar da esquerda, fica em 0.

Se tentar passar da direita, fica no máximo permitido.

Esse tipo de limite é simples, mas extremamente útil.


⚙️ O que é física simples?

Física simples em jogos não significa simular o mundo real com precisão científica.

Significa criar regras básicas para dar sensação de movimento.

Exemplos:

  • gravidade;
  • pulo;
  • queda;
  • aceleração;
  • desaceleração;
  • atrito;
  • impulso;
  • colisão;
  • rebote;
  • limite de velocidade.

Em jogos 2D, especialmente plataformas, a física precisa ser mais divertida do que realista.

Um pulo “perfeito” para gameplay pode não ser fisicamente real. O objetivo é sensação, controle e clareza.


🧲 Gravidade simples

Gravidade é uma aceleração constante para baixo.

Campos:

private Vector2 _playerPosition;
private Vector2 _playerVelocity;

private float _gravity = 900f;

No Update:

float deltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds;

_playerVelocity.Y += _gravity * deltaTime;
_playerPosition += _playerVelocity * deltaTime;

Isso faz o personagem cair.

A velocidade vertical aumenta com o tempo, criando sensação de aceleração.


🦘 Pulo simples

Para pular, você aplica uma velocidade para cima.

Como no eixo Y da tela valores maiores ficam mais para baixo, subir significa velocidade negativa.

Campos:

private bool _isOnGround = false;
private float _jumpForce = -450f;
private float _gravity = 900f;

No Update:

KeyboardState keyboard = Keyboard.GetState();

if (_isOnGround && keyboard.IsKeyDown(Keys.Space))
{
_playerVelocity.Y = _jumpForce;
_isOnGround = false;
}

Depois aplica gravidade:

_playerVelocity.Y += _gravity * deltaTime;
_playerPosition += _playerVelocity * deltaTime;

Mas falta algo: detectar o chão.


🧱 Criando um chão simples

Vamos criar um retângulo representando o chão:

private Rectangle _ground;

No Initialize:

_ground = new Rectangle(0, 500, 1280, 80);

Agora criamos o retângulo do jogador:

Rectangle playerBounds = new Rectangle(
(int)_playerPosition.X,
(int)_playerPosition.Y,
_playerTexture.Width,
_playerTexture.Height
);

Depois da movimentação:

if (playerBounds.Intersects(_ground))
{
_playerPosition.Y = _ground.Top - _playerTexture.Height;
_playerVelocity.Y = 0;
_isOnGround = true;
}

Isso faz o jogador parar sobre o chão.

A lógica é:

  • jogador caiu;
  • retângulo dele encostou no chão;
  • reposicionamos o jogador em cima do chão;
  • zeramos a velocidade vertical;
  • marcamos que ele está no chão.

🧩 Exemplo completo: gravidade + pulo + chão

private Vector2 _playerPosition;
private Vector2 _playerVelocity;

private float _moveSpeed = 250f;
private float _gravity = 900f;
private float _jumpForce = -450f;

private bool _isOnGround = false;

private Rectangle _ground;
private Texture2D _playerTexture;

No Initialize:

protected override void Initialize()
{
_playerPosition = new Vector2(100, 100);
_playerVelocity = Vector2.Zero;

_ground = new Rectangle(0, 500, 1280, 80);

base.Initialize();
}

No Update:

protected override void Update(GameTime gameTime)
{
float deltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
KeyboardState keyboard = Keyboard.GetState();

_playerVelocity.X = 0;

if (keyboard.IsKeyDown(Keys.D) || keyboard.IsKeyDown(Keys.Right))
_playerVelocity.X = _moveSpeed;

if (keyboard.IsKeyDown(Keys.A) || keyboard.IsKeyDown(Keys.Left))
_playerVelocity.X = -_moveSpeed;

if (_isOnGround && keyboard.IsKeyDown(Keys.Space))
{
_playerVelocity.Y = _jumpForce;
_isOnGround = false;
}

_playerVelocity.Y += _gravity * deltaTime;

_playerPosition += _playerVelocity * deltaTime;

Rectangle playerBounds = new Rectangle(
(int)_playerPosition.X,
(int)_playerPosition.Y,
_playerTexture.Width,
_playerTexture.Height
);

if (playerBounds.Intersects(_ground))
{
_playerPosition.Y = _ground.Top - _playerTexture.Height;
_playerVelocity.Y = 0;
_isOnGround = true;
}

base.Update(gameTime);
}

Esse é um dos primeiros grandes marcos no desenvolvimento de um jogo de plataforma.

Agora o personagem anda, cai, pula e para no chão.


⚠️ Problema comum: a tecla espaço segurada gera pulos estranhos

No exemplo acima, usamos:

keyboard.IsKeyDown(Keys.Space)

Isso funciona, mas pode causar comportamento estranho se o jogador segurar a tecla.

O ideal é usar detecção de tecla recém-pressionada, como vimos no capítulo anterior.

Campos:

private KeyboardState _currentKeyboard;
private KeyboardState _previousKeyboard;

No Update:

_previousKeyboard = _currentKeyboard;
_currentKeyboard = Keyboard.GetState();

Método:

private bool IsKeyPressed(Keys key)
{
return _currentKeyboard.IsKeyDown(key) &&
_previousKeyboard.IsKeyUp(key);
}

Uso:

if (_isOnGround && IsKeyPressed(Keys.Space))
{
_playerVelocity.Y = _jumpForce;
_isOnGround = false;
}

Isso deixa o pulo mais controlado.


🧱 Colisão com paredes

Agora imagine uma parede:

private Rectangle _wall = new Rectangle(600, 350, 60, 150);

Se o jogador colidir com ela, precisamos impedir que atravesse.

Uma abordagem simples é separar movimento horizontal e vertical.

Primeiro aplica movimento horizontal:

_playerPosition.X += _playerVelocity.X * deltaTime;

Depois testa colisão horizontal:

Rectangle playerBounds = GetPlayerBounds();

if (playerBounds.Intersects(_wall))
{
if (_playerVelocity.X > 0)
_playerPosition.X = _wall.Left - _playerTexture.Width;
else if (_playerVelocity.X < 0)
_playerPosition.X = _wall.Right;
}

Depois aplica movimento vertical:

_playerPosition.Y += _playerVelocity.Y * deltaTime;

Testa colisão vertical:

playerBounds = GetPlayerBounds();

if (playerBounds.Intersects(_wall))
{
if (_playerVelocity.Y > 0)
{
_playerPosition.Y = _wall.Top - _playerTexture.Height;
_playerVelocity.Y = 0;
_isOnGround = true;
}
else if (_playerVelocity.Y < 0)
{
_playerPosition.Y = _wall.Bottom;
_playerVelocity.Y = 0;
}
}

Essa separação por eixo evita muitos bugs.


🧠 Por que separar colisão horizontal e vertical?

Se você move o personagem em X e Y ao mesmo tempo, fica mais difícil saber de que lado ele bateu.

Ele bateu na parede lateral?

Caiu em cima da plataforma?

Bateu a cabeça no teto?

Entrou diagonalmente no bloco?

Ao separar:

  1. move X;
  2. resolve colisão em X;
  3. move Y;
  4. resolve colisão em Y;

você simplifica muito a lógica.

Esse padrão é extremamente comum em jogos 2D.


🧰 Criando um método GetBounds

Para não repetir código, crie:

private Rectangle GetPlayerBounds()
{
return new Rectangle(
(int)_playerPosition.X,
(int)_playerPosition.Y,
_playerTexture.Width,
_playerTexture.Height
);
}

Agora você pode usar:

Rectangle playerBounds = GetPlayerBounds();

Isso deixa o código mais limpo.

Em uma classe Player, o ideal seria ter uma propriedade:

public Rectangle Bounds
{
get
{
return new Rectangle(
(int)Position.X,
(int)Position.Y,
Texture.Width,
Texture.Height
);
}
}

🎮 Criando uma classe Player com física simples

public class Player
{
public Vector2 Position;
public Vector2 Velocity;

private Texture2D _texture;

private float _moveSpeed = 250f;
private float _gravity = 900f;
private float _jumpForce = -450f;

public bool IsOnGround { get; private set; }

public Rectangle Bounds
{
get
{
return new Rectangle(
(int)Position.X,
(int)Position.Y,
_texture.Width,
_texture.Height
);
}
}

public Player(Texture2D texture, Vector2 startPosition)
{
_texture = texture;
Position = startPosition;
Velocity = Vector2.Zero;
}

public void Update(GameTime gameTime, KeyboardState keyboard)
{
float deltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds;

Velocity.X = 0;

if (keyboard.IsKeyDown(Keys.D) || keyboard.IsKeyDown(Keys.Right))
Velocity.X = _moveSpeed;

if (keyboard.IsKeyDown(Keys.A) || keyboard.IsKeyDown(Keys.Left))
Velocity.X = -_moveSpeed;

if (IsOnGround && keyboard.IsKeyDown(Keys.Space))
{
Velocity.Y = _jumpForce;
IsOnGround = false;
}

Velocity.Y += _gravity * deltaTime;
}

public void MoveX(float deltaTime)
{
Position.X += Velocity.X * deltaTime;
}

public void MoveY(float deltaTime)
{
Position.Y += Velocity.Y * deltaTime;
}

public void LandOn(Rectangle platform)
{
Position.Y = platform.Top - _texture.Height;
Velocity.Y = 0;
IsOnGround = true;
}

public void HitCeiling(Rectangle block)
{
Position.Y = block.Bottom;
Velocity.Y = 0;
}

public void HitWallLeft(Rectangle wall)
{
Position.X = wall.Left - _texture.Width;
}

public void HitWallRight(Rectangle wall)
{
Position.X = wall.Right;
}

public void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(_texture, Position, Color.White);
}
}

Essa classe ainda é simples, mas já organiza bem:

  • posição;
  • velocidade;
  • gravidade;
  • pulo;
  • colisão;
  • desenho.

🧱 Plataformas e blocos sólidos

Você pode criar uma lista de blocos:

private List<Rectangle> _solidBlocks;

No Initialize:

_solidBlocks = new List<Rectangle>
{
new Rectangle(0, 500, 1280, 80),
new Rectangle(300, 420, 200, 30),
new Rectangle(650, 350, 200, 30),
new Rectangle(950, 280, 180, 30)
};

Agora o jogador pode colidir com várias plataformas.

No Update, depois de mover em X:

_player.MoveX(deltaTime);

foreach (Rectangle block in _solidBlocks)
{
if (_player.Bounds.Intersects(block))
{
if (_player.Velocity.X > 0)
_player.HitWallLeft(block);
else if (_player.Velocity.X < 0)
_player.HitWallRight(block);
}
}

Depois de mover em Y:

_player.MoveY(deltaTime);

foreach (Rectangle block in _solidBlocks)
{
if (_player.Bounds.Intersects(block))
{
if (_player.Velocity.Y > 0)
_player.LandOn(block);
else if (_player.Velocity.Y < 0)
_player.HitCeiling(block);
}
}

Esse modelo já permite criar fases simples de plataforma.


🧲 Aceleração e desaceleração

Até agora, o personagem começa e para instantaneamente.

Para alguns jogos, isso é ótimo. Para outros, pode parecer duro.

Você pode adicionar aceleração.

Campos:

private float _acceleration = 1200f;
private float _deceleration = 1600f;
private float _maxSpeed = 280f;

No movimento horizontal:

float input = 0f;

if (keyboard.IsKeyDown(Keys.D) || keyboard.IsKeyDown(Keys.Right))
input = 1f;

if (keyboard.IsKeyDown(Keys.A) || keyboard.IsKeyDown(Keys.Left))
input = -1f;

if (input != 0)
{
Velocity.X += input * _acceleration * deltaTime;
Velocity.X = MathHelper.Clamp(Velocity.X, -_maxSpeed, _maxSpeed);
}
else
{
if (Velocity.X > 0)
{
Velocity.X -= _deceleration * deltaTime;

if (Velocity.X < 0)
Velocity.X = 0;
}
else if (Velocity.X < 0)
{
Velocity.X += _deceleration * deltaTime;

if (Velocity.X > 0)
Velocity.X = 0;
}
}

Esse tipo de lógica cria movimento mais suave.


🎯 Movimento arcade vs movimento físico

Nem todo jogo precisa de aceleração.

Movimento arcade direto

Bom para:

  • jogos de ação rápida;
  • shooters;
  • puzzles;
  • jogos casuais;
  • controles precisos.

Exemplo:

Velocity.X = input * speed;

Movimento com aceleração

Bom para:

  • plataforma com sensação de peso;
  • jogos com gelo;
  • corrida;
  • naves;
  • personagens mais “físicos”.

Exemplo:

Velocity.X += input * acceleration * deltaTime;

A escolha depende do tipo de jogo.

O objetivo não é ser realista. É ser agradável de jogar.


🧪 Colisão com inimigos

Agora vamos criar um inimigo simples:

private Rectangle _enemyBounds = new Rectangle(700, 460, 48, 40);
private int _playerHealth = 3;

No Update:

if (_player.Bounds.Intersects(_enemyBounds))
{
_playerHealth--;
_player.Position = new Vector2(100, 100);
}

Esse exemplo é simples: se encostar no inimigo, perde vida e volta ao início.

Mas em um jogo real, você deve evitar que a vida diminua dezenas de vezes por segundo enquanto o jogador está encostado no inimigo.

Para isso, use invencibilidade temporária.


🛡️ Invencibilidade temporária após dano

Campos:

private float _invincibilityTimer = 0f;
private float _invincibilityDuration = 1.5f;

No Update:

float deltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds;

if (_invincibilityTimer > 0)
_invincibilityTimer -= deltaTime;

if (_player.Bounds.Intersects(_enemyBounds) && _invincibilityTimer <= 0)
{
_playerHealth--;
_invincibilityTimer = _invincibilityDuration;
}

Agora o jogador não perde vida a cada frame.

Esse tipo de detalhe melhora muito a experiência.


💥 Knockback simples

Quando o jogador toma dano, você pode empurrá-lo.

if (_player.Bounds.Intersects(_enemyBounds) && _invincibilityTimer <= 0)
{
_playerHealth--;
_invincibilityTimer = _invincibilityDuration;

if (_player.Position.X < _enemyBounds.X)
_player.Velocity.X = -300f;
else
_player.Velocity.X = 300f;

_player.Velocity.Y = -250f;
}

Isso cria um impacto visual e mecânico.

O jogador sente que foi atingido.


🧲 Física simples de projéteis

Projéteis são ótimos para praticar movimentação e colisão.

Classe simples:

public class Projectile
{
public Vector2 Position;
public Vector2 Velocity;
public bool IsActive = true;

private Texture2D _texture;

public Rectangle Bounds
{
get
{
return new Rectangle(
(int)Position.X,
(int)Position.Y,
_texture.Width,
_texture.Height
);
}
}

public Projectile(Texture2D texture, Vector2 position, Vector2 velocity)
{
_texture = texture;
Position = position;
Velocity = velocity;
}

public void Update(GameTime gameTime)
{
float deltaTime = (float)gameTime.ElapsedGameTime.TotalSeconds;
Position += Velocity * deltaTime;
}

public void Draw(SpriteBatch spriteBatch)
{
if (IsActive)
spriteBatch.Draw(_texture, Position, Color.White);
}
}

Criando um tiro:

Projectile bullet = new Projectile(
bulletTexture,
_player.Position,
new Vector2(500f, 0f)
);

Colisão com inimigo:

if (bullet.Bounds.Intersects(enemy.Bounds))
{
bullet.IsActive = false;
enemy.TakeDamage(1);
}

🧹 Removendo objetos inativos

Se você tem uma lista de projéteis:

private List<Projectile> _projectiles;

Atualiza:

foreach (Projectile projectile in _projectiles)
{
projectile.Update(gameTime);
}

Remove inativos:

_projectiles.RemoveAll(p => !p.IsActive);

Isso evita que a lista cresça indefinidamente.


🧠 Collision Layers: nem tudo precisa colidir com tudo

Em jogos maiores, você não quer testar todos os objetos contra todos.

Exemplo:

  • moeda não precisa colidir com parede;
  • fundo não precisa colidir com jogador;
  • partículas não precisam colidir com plataformas;
  • projéteis do jogador não precisam atingir o próprio jogador.

Uma organização simples:

Player colide com: plataformas, inimigos, itens
Enemy colide com: plataformas, jogador
Projectile colide com: inimigos, paredes
Coin colide com: jogador

Essa mentalidade ajuda a manter o jogo eficiente.


📈 Performance em colisões

Para jogos pequenos, testar retângulos é barato.

Mas se você tiver milhares de objetos, precisa otimizar.

Técnicas futuras:

  • dividir o mapa em células;
  • testar apenas objetos próximos;
  • usar grid espacial;
  • usar quadtrees;
  • separar objetos por tipo;
  • evitar colisões desnecessárias.

No começo, não complique.

Comece com Rectangle.Intersects.

Depois otimize apenas se precisar.


⚠️ Erros comuns com colisão e física simples

1. Mover X e Y juntos sem resolver por eixo

Isso dificulta saber de que lado a colisão aconteceu.

2. Esquecer de zerar velocidade vertical ao cair no chão

Se não zerar, o personagem pode continuar acumulando velocidade para baixo.

3. Usar IsKeyDown para pulo sem controle

Pode causar pulos repetidos ou comportamento estranho.

4. Não reposicionar o jogador após colisão

Detectar colisão não basta. É preciso corrigir a posição.

5. Usar hitbox exatamente igual ao sprite sempre

Às vezes o sprite tem espaços vazios. A hitbox pode precisar ser menor.

6. Não separar física de desenho

A física deve ficar no Update. O desenho no Draw.

7. Criar física realista demais

Jogo precisa ser divertido, não uma simulação perfeita.

8. Não limitar velocidade de queda

Gravidade acumulada pode fazer o personagem cair rápido demais.

9. Permitir perda de vida a cada frame

Use invencibilidade temporária após dano.

10. Não testar em velocidades diferentes

Movimentos muito rápidos podem atravessar colisores finos se o sistema for simples.


🧱 Hitbox menor que o sprite

Muitas vezes, a imagem do personagem é maior que a área real de colisão.

Você pode ajustar:

public Rectangle Bounds
{
get
{
return new Rectangle(
(int)Position.X + 8,
(int)Position.Y + 4,
_texture.Width - 16,
_texture.Height - 4
);
}
}

Isso cria uma colisão mais justa.

Exemplo:

  • sprite tem cabelo, capa ou sombra;
  • hitbox considera apenas o corpo;
  • o jogador não sente colisões injustas.

Esse detalhe é muito importante em jogos de plataforma.


🧪 Exercício prático do capítulo

Crie uma cena com:

✅ jogador;
✅ chão;
✅ três plataformas;
✅ uma moeda;
✅ um inimigo;
✅ pontuação;
✅ vida;
✅ gravidade;
✅ pulo;
✅ colisão AABB.

Regras

  1. O jogador anda para esquerda e direita.
  2. O jogador pula com espaço.
  3. O jogador cai com gravidade.
  4. O jogador para sobre plataformas.
  5. O jogador coleta moeda ao encostar.
  6. O jogador perde vida ao encostar no inimigo.
  7. O jogador fica invencível por 1,5 segundo após sofrer dano.

Esse exercício consolida tudo que importa neste capítulo.


📊 Tabela prática: conceitos do capítulo

ConceitoPara que serve
Vector2Representar posição, velocidade e direção
GameTimeControlar movimento baseado no tempo
RectangleCriar caixas de colisão
IntersectsDetectar colisão entre retângulos
VelocidadeDefinir movimento por segundo
GravidadeAcelerar objeto para baixo
PuloAplicar velocidade vertical negativa
PlataformaBloquear queda do jogador
HitboxÁrea real de colisão
InvencibilidadeEvitar dano repetido por frame
KnockbackCriar impacto após colisão
NormalizaçãoEvitar movimento diagonal mais rápido

✅ Checklist do Capítulo 6

Ao final deste capítulo, você deve entender:

✅ como mover personagem com Vector2;
✅ por que usar deltaTime;
✅ como aplicar velocidade;
✅ como criar colisão AABB;
✅ como usar Rectangle;
✅ como usar Intersects;
✅ como coletar itens;
✅ como limitar personagem à tela;
✅ como aplicar gravidade simples;
✅ como criar pulo;
✅ como detectar chão;
✅ como resolver colisões com plataformas;
✅ como separar colisão horizontal e vertical;
✅ como criar hitboxes melhores;
✅ como aplicar dano;
✅ como criar invencibilidade temporária;
✅ como criar knockback;
✅ quais erros evitar.


🏁 Conclusão: colisão e física transformam movimento em gameplay

Movimentar um personagem é apenas o começo.

O jogo começa a ficar interessante quando esse movimento encontra regras: paredes bloqueiam, plataformas sustentam, moedas são coletadas, inimigos causam dano, pulos obedecem gravidade e colisões geram consequências.

No MonoGame, você constrói esses sistemas com código. Isso exige mais trabalho, mas ensina profundamente como jogos 2D funcionam.

A colisão AABB com Rectangle.Intersects é uma das formas mais simples e eficientes para começar. A física simples com Vector2, velocidade, gravidade e pulo é suficiente para criar protótipos jogáveis, plataformas básicas, jogos top-down, shooters, arcades e muitas outras experiências.

O mais importante é entender a lógica:

posição define onde o objeto está; velocidade define para onde ele vai; colisão define o que acontece quando ele encontra o mundo.

A partir daqui, seu jogo deixa de ser apenas visual e passa a ter comportamento.

No próximo capítulo, essa estrutura começará a crescer de forma mais organizada com cenas, menus e arquitetura do jogo.


❓ FAQ — Colisão, física simples e movimentação no MonoGame

1. O que é colisão no MonoGame?

Colisão é a detecção de contato entre objetos do jogo. Uma forma comum de começar é usando Rectangle e o método Intersects, que verifica se um retângulo intersecta outro.

2. O que é AABB?

AABB é uma colisão baseada em retângulos alinhados aos eixos. É simples, rápida e muito usada em jogos 2D.

3. O que é Vector2?

Vector2 representa dados bidimensionais, como posição, direção e velocidade. A documentação do MonoGame descreve vetores como úteis para direção, magnitude e coordenadas.

4. Por que usar deltaTime?

Porque ele ajusta o movimento ao tempo decorrido entre frames, tornando a movimentação mais estável.

5. Como faço o personagem pular?

Aplicando uma velocidade vertical negativa quando ele está no chão:

_playerVelocity.Y = -450f;

6. Como aplico gravidade?

Somando uma aceleração vertical ao longo do tempo:

_playerVelocity.Y += gravity * deltaTime;

7. Por que separar colisão horizontal e vertical?

Porque isso facilita descobrir se o jogador bateu em uma parede, caiu em uma plataforma ou bateu a cabeça no teto.

8. Preciso de uma engine de física?

Não para começar. Muitos jogos 2D simples podem usar física própria baseada em velocidade, gravidade e retângulos.

9. O que é hitbox?

É a área real usada para colisão. Ela pode ser igual ou menor que o sprite.

10. Qual frase resume este capítulo?

Movimento dá vida. Colisão dá regra. Física dá sensação.

Capítulo 1 — O que é MonoGame e por que usar para criar jogos

Capítulo 2 — Preparando o Ambiente de Desenvolvimento

Capítulo 3 — Fundamentos do Game Loop: Update e Draw

Capítulo 4 — Sprites, Texturas e Content Pipeline

Capítulo 5 — Entrada do Jogador: Teclado, Mouse e Controle

Capítulo 6 — Colisão, Física Simples e Movimentação

Capítulo 7 — Cenas, Menus e Arquitetura do Jogo

Capítulo 8 — Áudio, Partículas, Animações e Polimento

Capítulo 9 — Shaders, Câmera, Mapas e Performance

Capítulo 10 — Publicação, Monetização e Projeto Final no MonoGame

Posts Similares