Weird Camera Bug (2D, top-down)

Hey everyone,

I recently got to work on a new project (for which I’ve chosen MonoGame, obviously), and thing is - I don’t really have much experience doing low-level stuff; I’ve made games using Godot, Unity etc, where all the heavy lifting is done for you. I went ahead to get started on my camera class (‘Camera2D.cs’) and experienced some weird issues.

Here’s the code for the camera class, as well as the player class:

‘Camera2D.cs’:

public class Camera2D
{
    #region Fields
    public Matrix Transform;
    public Matrix Offset;

    public Entity Target;

    public bool SmoothingEnabled;

    public float SmoothingValue
    {
        get;
        set;
    }
    #endregion

    #region Constructors
    public Camera2D()
    {

    }

    public Camera2D(Entity target)
    {
        Target = target;
    }
    #endregion

    #region Methods
    public void Follow()
    {
        var Position = Matrix.CreateTranslation(

            // Create a "look-at", wherein the focus is on the center of entity. //

            -Target.Position.X - (Target.Collider.Width / 2),  // X-position of the entity, minus half of its width. //
            -Target.Position.X - (Target.Collider.Height / 2), // Y-position of the entity, minus half of its height. //
             0

            );

        Offset = Matrix.CreateTranslation(

            // The offset value the position should be multiplied by, to ensure that the camera always keeps the entity at the center of the screen. //

            MainGame.ScreenHeight / 2,
            MainGame.ScreenWidth / 2,
            0

            );

        // Set the camera's transform to be the position value multiplied by the offset value. //

        if (SmoothingEnabled == true)
        {
            Transform = (Position * Offset) * SmoothingValue;
        }
        else
        {
            Transform = Position * Offset;
        }
    }
    #endregion
}

‘Player.cs’:

public class Player : Entity
{
    #region Fields
    public Camera2D mainCamera;

    public Input playerInput;
    #endregion

    #region Constructors
    #endregion

    #region Methods
    public override void Initialize()
    {
        base.Initialize();

        MoveSpeed = 2.0f;
        Rotation = 0.0f;
        Scale = 1.0f;
        LayerDepth = 0.0f;

        mainCamera = new Camera2D();
        mainCamera.Target = this;

        playerInput = new Input();
        playerInput.KBMoveForward = Keys.W;
        playerInput.KBMoveBackward = Keys.S;
        playerInput.KBMoveLeft = Keys.A;
        playerInput.KBMoveRight = Keys.D;

    }

    public override void Load()
    {
        base.Load();

        Texture = Globals.Content.Load<Texture2D>("Graphics/space");
    }

    public override void Unload()
    {
        base.Unload();
    }

    public override void Update()
    {
        base.Update();

        var velocity = new Vector2();

        if (Keyboard.GetState().IsKeyDown(playerInput.KBMoveForward))
        {
            velocity.Y = -MoveSpeed;
        }
        if (Keyboard.GetState().IsKeyDown(playerInput.KBMoveBackward))
        {
            velocity.Y = MoveSpeed;
        }
        if (Keyboard.GetState().IsKeyDown(playerInput.KBMoveLeft))
        {
            velocity.X = -MoveSpeed;
        }
        if (Keyboard.GetState().IsKeyDown(playerInput.KBMoveRight))
        {
            velocity.X = MoveSpeed;
        }

        Position += velocity;

        mainCamera.Follow();
    }

    public override void Draw()
    {
        base.Draw();
    }
    #endregion
}

Here’s the ‘Game1.cs’ code:

public MainGame mainGame = new MainGame();
public MainMenu mainMenu = new MainMenu(); 

Globals.spriteBatch.Begin(SpriteSortMode.BackToFront, transformMatrix: mainGame.player.mainCamera.Transform);

And here’s the result. If you need to know anything else, do let me know. Thanks in advance.

UPDATE: Kinda fixed?

In the Camera2D class, in the ‘Position’ variable (in the Follow() method), I changed:

-Target.Position.X - (Target.Collider.Height / 2)

to

-Target.Position.Y - (Target.Collider.Height / 2)

However, I still get a weird offset.

I’ve cut down one of my camera classes ive used.

To use, when starting a new spritebatch pass the camera.TranslationMatrix in.

spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, null, null, null, null, camera.TranslationMatrix(1f, 1f));

The 2 values you pass into translation matrix are an x and a y parralex scrolling offset. If you were drawing background layers with parrelex give them smaller values, say 0.8f;

To move it around, call MoveCamera or CenterOn. Clamping will clamp the camera to the cameraMax bounds.

Use WorldToScreen or ScreenToWorld to get mouse positions etc.

zoom and rotation are pretty self explanatory. If your using a viewport and not the top left of the screen adjust the size of it so ScreenToWorld is accurate.

If you want to start draw culling whats not on the screen you can easily get the top left and bottom right corner of what would be visible using ScreenToWorld and the coordinates of your viewport.

class Camera
{
    private float rotation;
    private Vector2 cameraMax;
    private Viewport viewport;
    private Vector2 cameraPosition;
    private float zoom;

    public Camera()
    {
        viewport = new Viewport(0, 0, 1000, 1000);
        cameraMax = new Vector2(3000, 3000);
        zoom = 1f;
        rotation = 0f;
    }

    public Vector2 GetViewportCenter()
    {
        return new Vector2(viewport.Width * 0.5f, viewport.Height * 0.5f);
    }

    public Matrix TranslationMatrix(float scrollSpeedX, float scrollSpeedY)
    {
        return Matrix.CreateTranslation(-cameraPosition.X * scrollSpeedX, -cameraPosition.Y * scrollSpeedY, 0) *
        Matrix.CreateRotationZ(rotation) *
        Matrix.CreateScale(new Vector3(zoom, zoom, 1)) *
        Matrix.CreateTranslation(new Vector3(GetViewportCenter(), 0));
    }

    public Vector2 WorldToScreen(Vector2 worldPosition)
    {
        return Vector2.Transform(worldPosition, TranslationMatrix(1f, 1f));
    }

    public Vector2 ScreenToWorld(Vector2 screenClick)
    {
        return Vector2.Transform(screenClick - new Vector2(viewport.X, viewport.Y), Matrix.Invert(TranslationMatrix(1f, 1f)));
    }

    public void MoveCamera(Vector2 cameraMovement)
    {
        cameraPosition += cameraMovement;
        ClampCamera();
    }

    public void CenterOn(Vector2 position)
    {
        cameraPosition = position;
        ClampCamera();
    }

    private void ClampCamera()
    {
        Vector2 cameraWorldMin = Vector2.Transform(Vector2.Zero, Matrix.Invert(TranslationMatrix(1f, 1f)));
        Vector2 cameraSize = new Vector2(viewport.Width, viewport.Height) / zoom;
        Vector2 positionOffset = cameraPosition - cameraWorldMin;
        cameraPosition = Vector2.Clamp(cameraWorldMin, Vector2.Zero, cameraMax - cameraSize) + positionOffset;
    }
}
1 Like