Agora que já sabemos o básico de como calcular as colisões, podemos deixar esse código um pouco mais elaborado.
Essa segunda parte é parcialmente baseado em um artigo escrito pelo Maddy Thorson (Celeste e TowerFall), do qual uso parte dessa estrutura, mas com algumas alterações, então o objetivo desse artigo é detalhar um pouco mais o código e organizar um pequeno framework.
O que são Actors e Solids?
Actors
Actors são todos os objetos que se movem com bastante frequência como o player, inimigos e projetes como balas e flechas. Quanto aos solids explicarei mais a frente.
Para um objeto da categoria actor temos dois métodos importantes em sua API:
public void MoveX(float amount, Action<string?> onCollideFunction = null);
public void MoveY(float amount, Action<string?> onCollideFunction = null);
Com os métodos acima podemos mover objetos em diferentes direções sempre checando colisões e chamado ações quando houver colisão durante a movimentação.
Antes de tudo precisamos criar uma class para organizar todos os nossos objetos em uma cena. O que basicamente teremos será uma lista de solids e actors como no código abaixo:
// Scene.cs
public class Scene {
//...
public List<Solid> AllSolids = new List<Solid>();
public List<Actor> AllActors = new List<Actor>();
//..
}
Uma das coisas que irá poupá-lo de bastante dor de cabeça é sempre trabalhar com números do tipo int (inteiro), especialmente se seu jogo fizer o uso de pixel art.
Agora vamos detalhar um pouco mais os métodos para movimentação de um actor.
// Actor.cs
//...
float xRemainder = 0;
public void moveX(float amount, Action<string?> onCollideFunction = null)
{
xRemainder += amount;
int move = (int)Math.Round(xRemainder);
if (move != 0)
{
xRemainder -= move;
int sign = Math.Sign(move);
// loop checando a colisão de cada pixel
// o actor é movido até colidir em um solid
while (move != 0)
{
Vector2 _position = new Vector2(this.Position.X+ sign, this.Position.Y);
if (!collideAt(this.Scene.AllSolids, _position))
{
this.Position.X += sign;
move -= sign;
}
else
{
// caso colida em algum solid uma ação pode ser execultada
if(onCollideFunction != null)
onCollideFunction(null);
break;
}
}
}
}
float yRemainder = 0;
public void moveY(float amount, Action <string?> onCollideFunction = null){
yRemainder += amount;
int move = (int)Math.Round(yRemainder);
if (move != 0)
{
yRemainder -= move;
int sign = Math.Sign(move);
while (move != 0)
{
Vector2 _position = new Vector2(this.Position.X, this.Position.Y+ sign);
if (!collideAt(this.Scene.AllSolids, _position))
{
this.Position.Y += sign;
move -= sign;
}
else
{
if(onCollideFunction != null)
onCollideFunction(null);
break;
}
}
}
}
// logica para checar a colisão de um actor em solid na cena
private bool collideAt(List<Solid>solids, Vector2 position){
foreach(Solid solid in solids){
if(solid.check(this.size, position)){
return true;
}
}
return false;
}
//...
Agora precisamos de alguns outros métodos que usaremos durante a checagem de colisão.
// Actor.cs
//...
public Vector2 Position;
public Point Size;
public int Right {
get => (int)(this.Position.X + this.Size.X);
}
public int Left {
get => (int)(this.Position.X);
}
public int Top {
get => (int)(this.Position.Y);
}
public int Bottom {
get => (int)(this.Position.Y + this.Size.Y);
}
//esse é o metodo que uso para checar se o player (actor em geral podendo
// ser um inimigo e etc) está tocando o solo
public virtual bool isRiding(Solid solid){
if(solid.check(this.size, new Vector2(this.Position.X, this.Position.Y + 1)))
return true;
return false;
}
// metodo chamado quando um actor for "espremido" por dois ou mais solids
public virtual void squish(string tag = null){}
//...
Solids
Apesar de na maior parte do tempo os solids serem objetos que não se movem, essa estrutura pode ser usada para plataformas que se movem também, por exemplo.
//Solid.cs
// ...
public void move(float x, float y){
xRemainder += x;
yRemainder += y;
int moveX = (int)Math.Round(xRemainder);
int moveY = (int)Math.Round(yRemainder);
if (moveX != 0 || moveY != 0)
{
this.Collidable = false;
List<Actor> riding = this.GetAllRidingActors();
if (moveX != 0)
{
xRemainder -= moveX;
this.Position = new Vector2(this.Position.X + moveX, this.Position.Y);
if (moveX > 0)
{
int i = 0;
for (i = 0; i < this.Scene.AllActors.Count; i++)
{
if (overlapCheck(this.Scene.AllActors[i]))
{
// Empurra para a direita
this.Scene.AllActors[i].moveX(this.Right - this.Scene.AllActors[i].Left, this.Scene.AllActors[i].squish);
}
else if (riding.Contains(this.Scene.AllActors[i]))
{
// Carrega para a direita
this.Scene.AllActors[i].moveX(moveX, null);
}
}
}else{
int i = 0;
for (i = 0; i < this.Scene.AllActors.Count; i++)
{
if (overlapCheck(this.Scene.AllActors[i]))
{
// Empurra para esquerda
this.Scene.AllActors[i].moveX(this.Left - this.Scene.AllActors[i].Right, this.Scene.AllActors[i].squish);
}
else if (riding.Contains(this.Scene.AllActors[i]))
{
// carrega para a esquerda
this.Scene.AllActors[i].moveX(moveX, null);
}
}
}
}
if(moveY != 0){
yRemainder -= moveY;
this.Position = new Vector2(this.Position.X, this.Position.Y + moveY);
if (moveY > 0)
{
int i = 0;
for (i = 0; i < this.Scene.AllActors.Count; i++)
{
if (overlapCheck(this.Scene.AllActors[i]))
this.Scene.AllActors[i].moveY(this.Bottom - this.Scene.AllActors[i].Top, this.Scene.AllActors[i].squish);
else if (riding.Contains(this.Scene.AllActors[i]))
this.Scene.AllActors[i].moveY(moveY, null);
i++;
}
}
else
{
int i = 0;
for (i = 0; i < this.Scene.AllActors.Count; i++)
{
if (overlapCheck(this.Scene.AllActors[i]))
this.Scene.AllActors[i].moveY(this.Top - this.Scene.AllActors[i].Bottom, this.Scene.AllActors[i].squish);
else if (riding.Contains(this.Scene.AllActors[i]))
this.Scene.AllActors[i].moveY(moveY, null);
}
}
}
this.Collidable = true;
}
}
public bool overlapCheck(Actor actor){
bool AisToTheRightOfB = actor.Left >= this.Right;
bool AisToTheLeftOfB = actor.Right <= this.Left;
bool AisAboveB = actor.Bottom <= this.Top;
bool AisBelowB = actor.Top >= this.Bottom;
return !(AisToTheRightOfB
|| AisToTheLeftOfB
|| AisAboveB
|| AisBelowB);
}
// metodo que lista todos os actors que estão sobre os solids
public List<Actor> GetAllRidingActors(){
List<Actor> rt = new List<Actor>();
int i = 0;
while(i < this.Scene.AllActors.Count){
if(this.Scene.AllActors[i].isRiding(this))
rt.Add(this.Scene.AllActors[i]);
i++;
}
return rt;
}
//...
A lógica usada para saber se um personagem (actor) esta sobre um solid pode ser alterada para, por exemplo, saber se o personagem está escalando, e ainda assim podendo sofrer influência pela física para ser empurrado ou carregado pelos solids.
// Actor.css
// ...
public virtual bool isRiding(Solid solid){
if(solid.check(this.size, new Vector2(this.Position.X + 1, this.Position.Y)))
return true;
return false;
}
// ...
Por enquanto é isso, no próximo artigo vou mostrar como trabalho com grids e como checo colisão de objetos com outras formas que não retangulares. Alguma dúvida ou sugestão? Deixe seu comentário aí!