Rolagem de câmera XNA 2d - por que usar a transformação de matriz?

18

Estou fazendo um jogo de teste onde quero que o nível esteja constantemente rolando. Para criar esse efeito, estabeleci uma classe de câmera que simplesmente armazena uma posição vector2 e uma direção enum. Ele também contém um método público para 'mover', que simplesmente altera a posição a uma taxa fixa. Eu então uso essa posição ao percorrer minha matriz de peças ao desenhar. Tudo isso funciona bem.

No entanto, me disseram que eu deveria usar uma matriz Transform para mover a câmera e que eu deveria fornecer isso quando eu iniciar o spritebatch. Estou um pouco confuso a.) Como isso funciona? como se eu estivesse dando apenas quando o spritebatch começa, como ele sabe continuar mudando de posição? b.) Por que, com certeza, ainda preciso da posição da câmera para fazer um loop nos ladrilhos?

No momento, não consigo fazê-lo funcionar, mas isso não é surpresa, pois não entendo completamente como isso deve funcionar. Atualmente, na minha tentativa (código a seguir), as peças desenhadas mudam, o que significa que a posição da câmera está mudando, mas a posição da janela de visualização permanece inalterada (ou seja, na origem da câmera). Eu realmente aprecio alguns conselhos / orientações sobre como deve ser usado?

Câmera:

 class Camera {

    // The position of the camera.
    public Vector2 Position {
        get { return mCameraPosition; }
        set { mCameraPosition = value; }
    }
    Vector2 mCameraPosition;

    public Vector2 Origin { get; set; }

    public float Zoom { get; set; }

    public float Rotation { get; set; }

    public ScrollDirection Direction { get; set; }

    private Vector2 mScrollSpeed = new Vector2(20, 18);

    public Camera() {
        Position = Vector2.Zero;
        Origin = Vector2.Zero;
        Zoom = 1;
        Rotation = 0;
    }


    public Matrix GetTransform() {
        return Matrix.CreateTranslation(new Vector3(mCameraPosition, 0.0f)) *
               Matrix.CreateRotationZ(Rotation) *
               Matrix.CreateScale(Zoom, Zoom, 1.0f) *
               Matrix.CreateTranslation(new Vector3(Origin, 0.0f));
    }




    public void MoveCamera(Level level) {
        if (Direction == ScrollDirection.Up)
        {
            mCameraPosition.Y = MathHelper.Clamp(mCameraPosition.Y - mScrollSpeed.Y, 0, (level.Height * Tile.Height - level.mViewport.Height));
        }
    }

Nível:

 public void Update(GameTime gameTime, TouchCollection touchState) {

            Camera.MoveCamera(this);
 }


 public void Draw(SpriteBatch spriteBatch) {
        //spriteBatch.Begin();
        spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullCounterClockwise, null, mCamera.GetTransform());


        DrawTiles(spriteBatch);

        spriteBatch.End();
    }

Jogo - apenas chama o empate dentro do nível:

  protected override void Draw(GameTime gameTime)  {
        mGraphics.GraphicsDevice.Clear(Color.Black);

        //mSpriteBatch.Begin();

        // Draw the level.
        mLevel.Draw(mSpriteBatch);

        //mSpriteBatch.End();

        base.Draw(gameTime);
    }

==================================================== =============================== EDITAR:

Em primeiro lugar, obrigado craftworkgames por sua ajuda até agora.

Eu brinquei com a sugestão. Quando eu desenhei todas as peças, o fr caiu para cerca de 15 a 30 - provavelmente porque os níveis são bem grandes.

Então, o que fiz foi aplicar a matriz e mover-me na atualização (como sugerido), mas no sorteio eu uso a posição da câmera para percorrer os ladrilhos (ou seja, iniciar o contador à esquerda e terminar no lado direito). Isso tudo funciona bem e eu estou feliz com isso :-)

Minha nova edição está no player. Obviamente, agora que estou movendo a câmera e não o nível, o jogador é deixado para trás pelo camer enquanto sua posição permanece fixa. Eu pensei em duas soluções para esse problema, a primeira é simplesmente considerar a posição das câmeras ao desenhar o player. Ou seja, na função de desenho, basta adicionar a posição da câmera à posição do jogador. O segundo é iniciar um novo lote de sprites para o jogador que não tem transformação. ou seja, finalize o spritebatch depois de desenhar as peças e inicie uma nova ao desenhar o jogador. Eu sei que os dois funcionarão, mas não posso fazer cara de rabo, o que seria melhor em termos de desempenho / boa codificação? Não sei ao certo quais são as implicações de desempenho ao iniciar o lote duas vezes?

Pectus Excavatum
fonte
1
"Minha nova edição está no player. Obviamente, agora que estou movendo a câmera e não o nível, o jogador é deixado para trás pela câmera enquanto sua posição permanece fixa." Basta ler isso e estou tão confuso por que diabos você não moveria o jogador dentro do sistema de coordenadas do nível?
precisa

Respostas:

15

As transformações da matriz da câmera são fáceis

Criar uma câmera básica é fácil. Abaixo, você começará com o básico. Movendo-o, girando e escalando. Mover todos os sprites 2D não é um grande problema, mas se você considerar o dimensionamento ou a rotação, fica muito difícil aplicar individualmente a cada sprite.

class Camera2D 
{
    public float Zoom { get; set; }
    public Vector2 Location { get; set; }
    public float Rotation { get; set;}

    private Rectangle Bounds { get; set; }

    private Matrix TransformMatrix
    { 
        get: {
            return 
                Matrix.CreateTranslation(new Vector3(-Location.X, -Location.Y, 0)) *
                Matrix.CreateRotationZ(Rotation) *
                Matrix.CreateScale(Zoom) *
                Matrix.CreateTranslation(new Vector3(Bounds.Width * 0.5f, Bounds.Height * 0.5f, 0));
        }
    };

    public Camera2D(Viewport viewport) 
    {
        Bounds = viewport.Bounds;
    }
}

Facilita a conversão entre as definições do sistema de coordenadas

Para ir da tela para o espaço mundial simplesmente. Isso é comumente usado para obter a localização do mouse no mundo para a seleção de objetos.

Vector2.Transform(mouseLocation, Matrix.Invert(Camera.TransformMatrix));

Para ir do mundo para o espaço da tela, basta fazer o oposto.

Vector2.Transform(mouseLocation, Camera.TransformMatrix);

Não há nenhuma desvantagem em usar uma matriz que não seja um pouco de aprendizado.

É fácil obter a área visível

public Rectangle VisibleArea {
    get {
        var inverseViewMatrix = Matrix.Invert(View);
        var tl = Vector2.Transform(Vector2.Zero, inverseViewMatrix);
        var tr = Vector2.Transform(new Vector2(_screenSize.X, 0), inverseViewMatrix);
        var bl = Vector2.Transform(new Vector2(0, _screenSize.Y), inverseViewMatrix);
        var br = Vector2.Transform(_screenSize, inverseViewMatrix);
        var min = new Vector2(
            MathHelper.Min(tl.X, MathHelper.Min(tr.X, MathHelper.Min(bl.X, br.X))), 
            MathHelper.Min(tl.Y, MathHelper.Min(tr.Y, MathHelper.Min(bl.Y, br.Y))));
        var max = new Vector2(
            MathHelper.Max(tl.X, MathHelper.Max(tr.X, MathHelper.Max(bl.X, br.X))), 
            MathHelper.Max(tl.Y, MathHelper.Max(tr.Y, MathHelper.Max(bl.Y, br.Y))));
        return new Rectangle((int)min.X, (int)min.Y, (int)(max.X - min.X), (int)(max.Y - min.Y));
    }
}

Você pode simplesmente transformar os cantos das câmeras e obter sua localização no espaço do mundo. Min max os valores x, y e você pode obter um retângulo ao redor do espaço visível. Muito útil para selecionar e otimizar chamadas de sorteio.

ClassicThunder
fonte
1
Obrigado. Achei isso útil ao implementar uma câmera na biblioteca MonoGame.Extended .
craftworkgames
6

A aplicação de uma matriz ao seu SpriteBatch transforma a chamada de desenho inteira de uma só vez. Isso significa que você não precisa usar sua câmera no método DrawTiles.

Poderia se tornar muito mais simples assim:

    // Loop through the number of visible tiles.
    for (int y = 0; y <= tiles.GetUpperBound(1); y++) {
        for (int x = 0; x <= tiles.GetUpperBound(0); x++) {

                // If the tile is not an empty space.
                if (tiles[x, y].Texture != null) {
                     // Get the position of the visible tile within the viewport by multiplying the counters by the tile dimensions
                     // and subtracting the camera offset values incase the position of the camera means only part of a tile is visible.
                     Vector2 tilePosition = new Vector2(x * Tile.Width, y * Tile.Height);
                     // Draw the correct tile
                     spriteBatch.Draw(tiles[x, y].Texture, tilePosition, Color.White);
                }
        }
    }

Portanto, o objetivo de usar uma matriz é para que você não precise pensar sobre isso. Apenas desenhe coisas e mova a câmera de forma independente.

Além disso, seu método MoveCamera parece um pouco estranho. É muito incomum ter uma classe de câmera que assume um nível como dependência. Uma implementação mais típica seria assim:

public void MoveCamera(float deltaX, float deltaY) {
    mCameraPosition.X += deltaX;
    mCameraPosition.Y += deltaY;
}

Então, no seu método Update, você pode fazer algo assim:

    if (Direction == ScrollDirection.Up)
    {
        mCamera.MoveCamera(mScrollSpeed.Y, 0);
    }

No geral, minha sugestão é mantê-lo simples. Faça com que funcione da maneira mais simples e construa sobre ele. Tente não escrever código altamente otimizado até ter o básico funcionando primeiro. Você pode achar que renderizar todos os blocos de cada quadro não é tão ruim.

EDIT: Para a segunda parte da pergunta.

Embora seja verdade que você deseja manter sua contagem de lotes baixa, ter 2 ou 3 não deve ser um problema. Portanto, se você tiver um bom motivo para criar um segundo lote de sprites, faça-o.

Dito isto, provavelmente não há uma boa razão para usar um segundo lote de sprite nesse caso. Provavelmente, você deseja desenhar seu player exatamente da mesma maneira que desenha peças no mesmo lote de sprites com a transformação de câmera aplicada.

É um pouco difícil dizer por que seu jogador está ficando para trás sem olhar para algum código, mas é lógico que se você desenhar seu jogador exatamente na mesma posição que uma peça, ele aparecerá na mesma posição com a mesma lote de sprite.

Por exemplo, se você deseja que o player apareça no bloco 10, 10, faça o seguinte:

var playerPosition = new Vector2(10 * Tile.Width, 10 * Tile.Height);
spriteBatch.Draw(player.Texture, playerPosition, Color.White);

Tente entrar na mentalidade de pensar em desenhar as coisas onde elas estão e a câmera literalmente move toda a "cena" à vista. É isso que sua transformação de matriz está fazendo.

craftworkgames
fonte
Isso é realmente útil e esclareceu tudo! Obrigado pela informação; muito apreciado. Vou tentar implementar seu conselho ainda hoje e informaremos como eu vou. Obrigado novamente.
Pectus Excavatum
Além disso, apenas por interesse, existe uma maneira de usar a transformação de matriz para desenhar apenas blocos na viewport?
Pectus Excavatum
Existe uma maneira de fazer isso, mas eu não sei de nada. Eu suspeito que ele tenha algo a ver com o uso do retângulo da janela de visualização, talvez depois de aplicar a transformação da câmera a ele. Não sei, você terá que experimentar. Use seu depurador.
Craftworkgames
Ok obrigado. Eu brinquei com ele e cheguei a algum lugar, mas me deparei com uma pergunta sobre o que fazer com o jogador - consulte as edições da pergunta.
Pectus Excavatum
Obrigado, segui seu conselho e optei por usar o mesmo lote de sprites e apenas obter a posição da câmera na atualização e aplicá-la à posição dos jogadores ao desenhar. Parece funcionar bem. Obrigado por toda a sua ajuda, ficou preso na câmera por um tempo. Muito mais claro agora :-)
Pectus Excavatum