xnamonogame

(XNA/MonoGame) Implementing the screen dragging camera movement


I'm making a strategy with top-down 2D graphics. I implemented the way to move the view by holding LMB and then "dragging" the screen. It works fine with the exception of the movement being a little too fast, and I have no idea how to calculate the movement properly.

The code for camera movement (and scale) is

class CameraControls : WGComponent
{
    public override void Update(GameTime gameTime)
    {
        if (WGame.userInput.mouse.LeftButton == ButtonState.Pressed) WGame.camera.position -= (WGame.userInput.mouse.Position - WGame.userInput.prevMouse.Position).ToVector2().InvertY() * WGame.camera.scale;

        WGame.camera.scale = Math.Max(WGame.camera.scale - WGame.userInput.frameScroll * 0.1f, 1f);
    }
}

WGame is my Game class, which holds objects I expect to have only one instance as static variables. WGComponent just overrides the constructor to use the WGame instance stored in the static variable.

WGame.userInput.mouse is the MouseState for the current frame, while WGame.userInput.prevMouse is for the previous frame. WGame.userInput.frameScroll is calculated with frameScroll = mouse.ScrollWheelValue - prevMouse.ScrollWheelValue

The camera code is

public class Camera
{
    public Vector2 position = new();
    public float scale = 1;
}

And its position is used in WGame's Update method

protected override void Update(GameTime gameTime)
{
    translationMatrix = Matrix.CreateTranslation(-camera.position.X, camera.position.Y, 0);
    scaleMatrix = Matrix.CreateScale(100 / (camera.scale * camera.scale));
    screenSizeMatrix = Matrix.CreateTranslation(GraphicsDevice.Viewport.Width / 2, GraphicsDevice.Viewport.Height / 2, 0);
    resultMatrix = translationMatrix * scaleMatrix * screenSizeMatrix; //This one is later used in SpriteBatch.Begin()

    base.Update(gameTime);
}

So, am I doing everything right and if I do, is there a way to make the screen be dragged exactly as much as the mouse is moved?

[UPD] The camera scale calculation code is now WGame.camera.scale = Math.Max(WGame.camera.scale - WGame.userInput.frameScroll * MathF.Abs(WGame.userInput.frameScroll) * 0.01f, 1f);

Scale matrix calculation is scaleMatrix = Matrix.CreateScale(100 / camera.scale);


Solution

  • I quickly created a test project to verify the math. As I assumed, you have to 'apply' the same scale as for the scaleMatrix to your mouse movement, in this case dividing by 100. I would suggest to create a variable for that or even better, incorporate it in the scale variable.

    At least for top down 2D sprites, a 'more correct' way to interpret scale would be:

    and so on.

    Anyway, below is the code I tested for your case (I use a slightly different input class but the names should be self-explanatory). Furthermore, there you can see how to change the scale while avoiding the 'scaling becomes slower when bigger' problem (we multiply it by a signed factor and clamp it afterwards):

    // Camera zoom
    CamScale *= 1 - Sign(Input.MouseWheelDelta) * .25f;
    CamScale = Math.Clamp(CamScale, 0.1f, 1f);
    
    // Camera movement
    if (Input.IsMiddleMouseButtonDown) {
        CamPos.X -= Input.MousePositionDelta.X * CamScale / 100;
        CamPos.Y += Input.MousePositionDelta.Y * CamScale / 100;
    }
    
    Matrix resultMatrix;
    var translationMatrix = Matrix.CreateTranslation(-CamPos.X, CamPos.Y, 0);
    var scaleMatrix = Matrix.CreateScale(100 / (CamScale));
    var screenSizeMatrix = Matrix.CreateTranslation(GraphicsDevice.Viewport.Width / 2, GraphicsDevice.Viewport.Height / 2, 0);
    resultMatrix = translationMatrix * scaleMatrix * screenSizeMatrix;
    
    SpriteBatch.Begin(transformMatrix: resultMatrix);
    {
        SpriteBatch.Draw(TestTexture, new Vector2(0, 0), Color.White);
    }
    SpriteBatch.End();