Przewijanie kamery XNA 2d - po co używać transformacji macierzowej?

18

Robię grę testową, w której chcę, aby poziom ciągle się przewijał. Aby stworzyć ten efekt, stworzyłem klasę kamery, która po prostu przechowuje pozycję vector2 i kierunek wyliczania. Zawiera także publiczną metodę „przemieszczania się”, która po prostu zmienia pozycję według stałej stawki. Następnie używam tej pozycji, gdy przeglądam moją tablicę kafelków. To wszystko działa dobrze.

Jednak powiedziano mi, że powinienem używać matrycy transformacji do poruszania kamerą i że powinienem to zapewnić, kiedy uruchamiam spritebatch. Jestem trochę zdezorientowany.) Jak to działa? tak jakbym dawał to dopiero, gdy spritebatch zaczyna się, skąd wiadomo, że ciągle zmienia pozycję? b.) Dlaczego robię to tak pewnie, że nadal potrzebuję pozycji kamery, gdy przechodzę przez płytki?

W tej chwili nie mogę zmusić go do działania, ale nie jest to zaskoczeniem, ponieważ nie do końca rozumiem, jak to ma działać. Obecnie w mojej próbie (kod do naśladowania) narysowane kafelki zmieniają się, co oznacza, że ​​zmienia się pozycja kamer, ale pozycja rzutni pozostaje niezmieniona (tj. W punkcie początkowym kamery). Byłbym naprawdę wdzięczny za porady / wskazówki, w jaki sposób powinno się go używać?

Aparat fotograficzny:

 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));
        }
    }

Poziom:

 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();
    }

Gra - po prostu sprawdza losowanie na poziomie:

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

        //mSpriteBatch.Begin();

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

        //mSpriteBatch.End();

        base.Draw(gameTime);
    }

================================================== =============================== EDYCJA:

Po pierwsze, dziękuję gry rzemieślnicze za pomoc do tej pory.

Bawiłem się z sugestią. Kiedy narysowałem wszystkie płytki, fr zagrał do około 15 z 30 - prawdopodobnie dlatego, że poziomy są dość duże.

Więc zastosowałem macierz i poruszałem się w aktualizacji (zgodnie z sugestią), ale w losowaniu używam pozycji kamery do zapętlania płytek (tj. Start licznika po lewej i koniec po prawej płytce). To wszystko działa dobrze i jestem z tego zadowolony :-)

Mój nowy problem leży w odtwarzaczu. Oczywiście, ponieważ teraz poruszam kamerą, a nie poziomem, gracz zostaje w tyle za kamerą, gdy jego pozycja pozostaje stała. Pomyślałem o dwóch rozwiązaniach tego problemu. Pierwszym z nich jest po prostu wzięcie pod uwagę położenia kamery podczas rysowania odtwarzacza. Tj. W funkcji losowania po prostu dodaj pozycję kamery do pozycji gracza. Drugim jest rozpoczęcie nowej partii sprite dla gracza, który nie ma transformacji. tzn. zakończ sprite po wylosowaniu kafelków, a następnie rozpocznij nowy podczas losowania gracza. Wiem, że oba będą działać, ale nie mogę zrobić ogonów, które byłyby lepsze pod względem wydajności / dobrego kodowania? Nie jestem pewien, jakie są konsekwencje dla wydajności od dwukrotnego uruchomienia partii?

Pectus Excavatum
źródło
1
„Mój nowy problem leży w odtwarzaczu. Oczywiście, ponieważ teraz poruszam kamerą, a nie poziomem, gracz zostaje w tyle za kamerą, gdy jego pozycja pozostaje stała”. Po prostu przeczytaj to i jestem tak zdezorientowany, dlaczego, do licha, nie poruszyłbyś gracza w obrębie układu współrzędnych poziomu?
ClassicThunder

Odpowiedzi:

15

Transformacje matrycy kamery są łatwe

Stworzenie podstawowego aparatu jest łatwe. Poniżej zapoznasz się z podstawami. Poruszanie, obracanie, skalowanie. Przenoszenie każdego duszka 2d nie stanowi większego problemu, ale jeśli weźmiesz pod uwagę skalowanie lub obrót, bardzo trudno będzie zastosować go indywidualnie do każdego duszka.

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;
    }
}

Ułatwia to konwersję między definicjami układu współrzędnych

Aby przejść z ekranu do przestrzeni świata po prostu. Jest to powszechnie używane do uzyskiwania położenia myszy na świecie do wybierania obiektów.

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

Aby przejść ze świata do miejsca na ekranie, po prostu postępuj odwrotnie.

Vector2.Transform(mouseLocation, Camera.TransformMatrix);

Korzystanie z matrycy innej niż wady nie wymaga wcześniejszej nauki.

Łatwo jest uzyskać widoczny obszar

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));
    }
}

Możesz po prostu przekształcić kąty kamer i uzyskać ich lokalizację w przestrzeni świata. Min. Maks. Wartości x, y, możesz uzyskać prostokąt wokół widocznej przestrzeni. Bardzo przydatny do eliminacji i optymalizacji połączeń losujących.

ClassicThunder
źródło
1
Dzięki. Uznałem to za pomocne przy wdrażaniu kamery w bibliotece MonoGame.Extended .
craftworkgames,
6

Zastosowanie macierzy do SpriteBatch przekształca jednocześnie całe wywołanie losowania. Oznacza to, że w ogóle nie musisz używać aparatu w metodzie DrawTiles.

Może to stać się o wiele prostsze:

    // 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);
                }
        }
    }

Tak więc sens używania macierzy jest taki, że nie musisz o tym myśleć. Po prostu rysuj i przesuwaj aparat niezależnie.

Ponadto metoda MoveCamera wygląda trochę dziwnie. Bardzo rzadkie jest posiadanie klasy kamery, która bierze poziom jako zależność. Bardziej typowa implementacja wyglądałaby następująco:

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

Następnie w metodzie aktualizacji możesz zrobić coś takiego:

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

Ogólnie proponuję, aby było to proste. Spraw, by działał w najprostszy sposób i buduj na nim. Staraj się nie pisać wysoce zoptymalizowanego kodu, dopóki nie zaczniesz od podstaw. Może się okazać, że renderowanie każdej płytki każdej klatki nie jest takie złe.

EDYCJA: Do drugiej części pytania.

Chociaż prawdą jest, że chcesz utrzymać niską liczbę partii, posiadanie 2 lub 3 nie powinno stanowić żadnego problemu. Więc jeśli masz dobry powód, aby utworzyć drugą partię duszka, po prostu zrób to.

To powiedziawszy, prawdopodobnie nie ma dobrego powodu, aby w tym przypadku użyć drugiej partii duszka. Bardziej prawdopodobne jest, że chcesz narysować swojego gracza dokładnie tak samo, jak narysujesz płytki w tej samej partii duszka z zastosowaną transformacją kamery.

Trochę trudno jest powiedzieć, dlaczego twój gracz zostaje w tyle bez patrzenia na jakiś kod, ale jest oczywiste, że jeśli narysujesz swojego gracza dokładnie w tej samej pozycji co kafelek, pojawi się na tej samej pozycji z tą samą pozycją partia duszka.

Na przykład, jeśli chcesz, aby odtwarzacz pojawiał się na polu 10, 10, możesz to zrobić:

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

Postaraj się myśleć o rysowaniu rzeczy tam, gdzie są, a kamera dosłownie przesuwa całą „scenę” na widok. Właśnie to robi twoja transformacja macierzowa.

gry rzemieślnicze
źródło
To naprawdę pomocne i uporządkowane rzeczy! Dzięki za informację; bardzo mile widziane. Postaram się wdrożyć dziś twoją radę i dam ci znać, jak sobie radzę. Dzięki jeszcze raz.
Pectus Excavatum
Poza tym, czy nie jest interesujące, czy istnieje sposób, aby użyć transformacji macierzowej do rysowania tylko kafelków w rzutni?
Pectus Excavatum
Jest na to sposób, ale nie wiem od samego początku. Podejrzewam, że będzie to miało związek z użyciem prostokąta rzutni, być może po zastosowaniu do niego transformacji kamery. Nie wiem, będziesz musiał eksperymentować. Użyj swojego debuggera.
craftworkgames
Ok dzięki. Bawiłem się nim i gdzieś się natknąłem, ale wpadłem na pytanie, co zrobić z odtwarzaczem - zobacz zmiany w pytaniu.
Pectus Excavatum
Dzięki, skorzystałem z twoich rad i wybrałem opcję użycia tej samej partii duszka i po prostu uzyskanie pozycji kamery w aktualizacji i zastosowanie jej do pozycji graczy podczas losowania. Wydaje się działać dobrze. Dzięki za wszelką pomoc, utknąłem na chwilę w aparacie. Znacznie wyraźniejsze teraz :-)
Pectus Excavatum