Como física de jogos 2D funcionam? Parte 2/3

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í!

Leave a comment