
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ão | Consequência |
|---|---|
| Jogador + moeda | aumenta pontuação |
| Jogador + parede | bloqueia movimento |
| Jogador + inimigo | perde vida |
| Tiro + inimigo | inimigo sofre dano |
| Jogador + plataforma | pode pisar |
| Jogador + porta | muda de fase |
| Bola + parede | rebate |
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:
- move X;
- resolve colisão em X;
- move Y;
- 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
- O jogador anda para esquerda e direita.
- O jogador pula com espaço.
- O jogador cai com gravidade.
- O jogador para sobre plataformas.
- O jogador coleta moeda ao encostar.
- O jogador perde vida ao encostar no inimigo.
- 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
| Conceito | Para que serve |
|---|---|
Vector2 | Representar posição, velocidade e direção |
GameTime | Controlar movimento baseado no tempo |
Rectangle | Criar caixas de colisão |
Intersects | Detectar colisão entre retângulos |
| Velocidade | Definir movimento por segundo |
| Gravidade | Acelerar objeto para baixo |
| Pulo | Aplicar velocidade vertical negativa |
| Plataforma | Bloquear queda do jogador |
| Hitbox | Área real de colisão |
| Invencibilidade | Evitar dano repetido por frame |
| Knockback | Criar impacto após colisão |
| Normalização | Evitar 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