19
\$\begingroup\$

I am making a test game where I want the level to be constantly scrolling. To create this effect I have established a camera class which simply stores a vector2 position and an enum direction. It also contains a public method to 'move' which simply changes the position at a fixed rate. I then use this position when looping through my array of tiles when drawing. This all works fine.

However, I have been told that I should be using a Transform matrix to move the camera, and that I should provide this when I start the spritebatch. I am a little confused a.) how this works? as if I am only giving it when spritebatch begins how does it know to keep changing position? b.) Why do it as surely I still need the camera position for when I loop through tiles?

AT the moment I can't get it to work, but that's no surprise as I dont completely understand how it is meant to work. Currently in my attempt (code to follow) the tiles being drawn changes which means the cameras position is changing, but the position of the viewport remains unchanged (i.e. at the camera origin). I would really appreciate some advice/guidance on how its supposed to be used?

Camera:

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

Level:

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


   

Game - just calls the draw within level:

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

        //mSpriteBatch.Begin();

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

        //mSpriteBatch.End();

        base.Draw(gameTime);
    }

I played around with the suggestion from craftworkgames. When I did draw all tiles, the fr tanked to around 15 from 30 - probably because the levels are pretty big.

So what I have done is apply the matrix and move in update (as suggested) but in draw I use the cameras position for looping through tiles (i.e. start counter at left and end at right tile). This all works well and I'm happy with it :-)

My new issue lies in the player. Obviously as I am now moving the camera rather than the level, the player gets left behind by the camer as its position stays fixed. I have thought of two solutions to this problem, the first is to simply consider the cameras position when drawing the player. I.e. in the draw funcion simply add the camera position to the player position. The second is to start a new sprite batch for the player which has no transform. i.e. end the spritebatch after drawing the tiles then start a new one when drawing the player. I know both will work, but I cant make heads of tails of which would be better in terms of performance/good coding? I am not sure what the performance implications are from starting the batch twice?

\$\endgroup\$
1
  • 1
    \$\begingroup\$ "My new issue lies in the player. Obviously as I am now moving the camera rather than the level, the player gets left behind by the camer as its position stays fixed." Just read this and I am so confused why on earth would you not move the player within the level's coordiante system? \$\endgroup\$ Jul 19, 2013 at 19:39

2 Answers 2

15
\$\begingroup\$

Camera matrix transformations are easy

Creating a basic camera is easy. Below will get you started with the basics. Moving it around, rotating, and scaling. Moving every 2d sprite isn't much of an issues but if you factor in either scaling or rotation then it gets really hard to apply to every sprite individually.

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

It makes it really easy to convert between coordinate system definitions

To go from screen to world space simply. This is commonly used to get the location of the mouse in the world for object picking.

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

To go from world to screen space simply do the opposite.

Vector2.Transform(mouseLocation, Camera.TransformMatrix);

There is no draw back to using a matrix other than it takes a little learning.

Its easy to get the visible area

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

You can simply transform the cameras corners and get their location in world space. Min and max the x,y values and you can get a rectangle around the viewable space. Very useful for culling and optimizing draw calls.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Thanks. I found this helpful while implementing a camera in the MonoGame.Extended library. \$\endgroup\$ Jul 16, 2015 at 23:50
5
\$\begingroup\$

Applying a matrix to your SpriteBatch transforms the entire draw call at once. This means you don't need to use your camera in your DrawTiles method at all.

It could become a lot simpler like so:

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

So the point of using a matrix is so that you don't have to think about it. Just draw things and move the camera around independently.

Also, your MoveCamera method looks a little strange. It's very unusual to have a camera class that takes a Level as a dependency. A more typical implementation would look like this:

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

Then in your Update method you might do something like this:

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

Overall, my suggestion is to keep it simple. Get it working the simplest way fist and build on it. Try not to write highly optimized code until you've got the basics working first. You may find that rendering every tile every frame is not so bad.

EDIT: For second part of question.

While it's true that you want to keep your batch count low, having 2 or 3 shouldn't be a problem at all. So if you have a good reason to create a second sprite batch just do it.

That said, there's probably not a good reason to use a second sprite batch in this case. More likely, you want to draw your player in the exact same way you draw tiles in the same sprite batch with the camera transform applied.

It's a little hard to tell why your player is getting left behind without looking at some code but it stands to reason that if you draw your player at the exact same position as a tile, he / she will appear at the same position with the same sprite batch.

For example, if you want the player to appear on tile 10, 10 you could do this:

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

Try to get into the mindset of thinking about drawing things where they are and the camera literally moves the entire "scene" into view. That's what your matrix transform is doing.

\$\endgroup\$
6
  • \$\begingroup\$ That's really helpful and cleared things up loads! Thanks for the info; much appreciated. I will try to implement your advise later today and will let you know how I get on. Thanks again. \$\endgroup\$ Jul 18, 2013 at 8:37
  • \$\begingroup\$ Also, just out of interest, is there a way to use the matrix transform to only draw tiles in the viewport? \$\endgroup\$ Jul 18, 2013 at 9:38
  • \$\begingroup\$ There is a way to do that, but I don't know off the top of my head. I suspect it will have something to do with using the viewport rectangle, maybe after applying the camera transform to it. I dunno, you'll have to experiment. Use your debugger. \$\endgroup\$ Jul 18, 2013 at 11:29
  • \$\begingroup\$ Ok, thanks. I have played around with it and got somewhere but have run into an question about what to do with the player - please see edits to question. \$\endgroup\$ Jul 18, 2013 at 13:14
  • \$\begingroup\$ Thanks, I took your advice and went with the option of using the same sprite batch and just getting the camera position in the update and applying it to the players position when drawing. Seems to work well. Thanks for all your help, been stuck on the camera for a while. Much clearer now :-) \$\endgroup\$ Jul 19, 2013 at 15:45

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .