Take the 2-minute tour ×
Game Development Stack Exchange is a question and answer site for professional and independent game developers. It's 100% free, no registration required.

I'm building a top down shooter but I have a little issue with my camera and mouse. When I add the camera that I use to my game (see here) my mouse pointer seems to be in the wrong location. When I call my Mouse.GetState().X or Y the location seems to be about 300 pixels off. I've looked all over the net but I just can't find the solution.

I'm pretty new to coding, so I'll just post my main and my camera class. My objective is to get the green circle on the actual position of my mouse.

Camera class:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

namespace ZombieSurvival
{
    public class Camera2D
    {
            protected float _zoom; // Camera Zoom
            public Matrix _transform; // Matrix Transform
            public Vector2 _pos; // Camera Position
            protected float _rotation; // Camera Rotation

            public Camera2D()
            {
                _zoom = 1.0f;
                _rotation = 0.0f;
                _pos = Vector2.Zero;
            }

            // Sets and gets zoom
            public float Zoom
            {
                get { return _zoom; }
                set { _zoom = value; if (_zoom < 0.1f) _zoom = 0.1f; } // Negative zoom will flip image
            }

            public float Rotation
            {
                get { return _rotation; }
                set { _rotation = value; }
            }

            // Auxiliary function to move the camera
            public void Move(Vector2 amount)
            {
                _pos += amount;
            }
            // Get set position
            public Vector2 Pos
            {
                get { return _pos; }    
                set { _pos = value; }
            }

            public Matrix get_transformation(GraphicsDevice graphicsDevice)
            {
                _transform =       // Thanks to o KB o for this solution
                  Matrix.CreateTranslation(new Vector3(-_pos.X, -_pos.Y, 0)) *
                                             Matrix.CreateRotationZ(Rotation) *
                                             Matrix.CreateScale(new Vector3(Zoom, Zoom, 1)) *
                                             Matrix.CreateTranslation(new Vector3(graphicsDevice.Viewport.Width * 0.5f, graphicsDevice.Viewport.Height * 0.5f, 0));
                return _transform;
            }


    }
}

Game:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
//using Microsoft.Xna.Framework.Graphics.GraphicsDevice.SetCursorProperties;

namespace ZombieSurvival
{
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        int cursorX, cursorY;

        Camera2D camera;

        Player player;
        Texture2D playertex;
        Texture2D bulletTex;
        Texture2D zombieTex;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

        }

        protected override void Initialize()
        {
            this.IsMouseVisible = true;
            graphics.IsFullScreen = true;
            graphics.ApplyChanges();
            Mouse.SetPosition(0, 0);
            cursorX = Window.ClientBounds.X + Mouse.GetState().X;
            cursorY = Window.ClientBounds.Y + Mouse.GetState().Y;

            //graphics.

            base.Initialize();
        }

        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);
            playertex = Content.Load <Texture2D>("player");
            bulletTex = Content.Load<Texture2D>("bullet");
            zombieTex = Content.Load<Texture2D>("zombie");

            player = new Player(new Rectangle(100, 100, 50, 50), playertex, 5, bulletTex);

            // TODO: use this.Content to load your game content here
        }

        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        protected override void Update(GameTime gameTime)
        {
            player.Update();

            /*if (IsActive)
            {
                MouseState mouseState = Mouse.GetState();
                int cx = graphics.GraphicsDevice.Viewport.Width / 2;
                int cy = graphics.GraphicsDevice.Viewport.Height / 2;
                int x = mouseState.X - cx;
                int y = mouseState.Y - cy;
                Mouse.SetPosition(cx, cy);
            }*/

            base.Update(gameTime);
        }

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


            Camera2D cam = new Camera2D();
            cam.Pos = new Vector2(player.playerPosition.X, player.playerPosition.Y);
            // cam.Zoom = 2.0f // Example of Zoom in
            cam.Zoom = 0.5f; // Example of Zoom out


            //// if using XNA 3.1
            spriteBatch.Begin(SpriteBlendMode.AlphaBlend,
                                    SpriteSortMode.Immediate,
                                    SaveStateMode.SaveState,
                                    cam.get_transformation(graphics.GraphicsDevice));

            player.Draw(spriteBatch);
            spriteBatch.Draw(zombieTex, new Rectangle(Mouse.GetState().X, Mouse.GetState().Y, 50, 50), Color.White);
            // Draw Everything
            // You can draw everything in their positions since the cam matrix has already done the maths for you 

            spriteBatch.End();

            //Console.WriteLine(camera.Pos);

            /*if (camera._pos.X - player.PlayerRect.X >= 100)
            {
                camera.Pos = new Vector2(camera.Pos.X - player._PlayerSpeed, camera.Pos.Y);
            }

            if (camera._pos.X - player.PlayerRect.X <= -300)
            {
                camera.Pos = new Vector2(camera.Pos.X + player._PlayerSpeed, camera.Pos.Y);
            }

            if (camera._pos.Y - player.PlayerRect.Y >= 0)
            {
                camera.Pos = new Vector2(camera.Pos.X, camera.Pos.Y - player._PlayerSpeed);
            }

            if (camera._pos.Y - player.PlayerRect.Y <= 250)
            {
                camera.Pos = new Vector2(camera.Pos.X, camera.Pos.Y + player._PlayerSpeed);
            }*/

            /*spriteBatch.Begin();
            player.Draw(spriteBatch);   

            spriteBatch.End();*/

            base.Draw(gameTime);
        }
    }
}

I realy hope someone has a solution for this, because I'm stuck right now. I'd really appreciate the help.

share|improve this question
5  
Fyi, using everything available isn't good programming practice. I can immediately tell if a class of mine handles input at all if I see it imports Microsoft.Xna.Framework.Input. It helps me to understand what my code is doing when I revisit it, and it'll help others when they begin to work with it. If your camera imports Microsoft.Xna.Framework.Audio then doesn't end up doing anything audio-related at all, your namespace references lose all the value they can add to understanding source code. –  Jonathan Hobbs May 18 '11 at 12:34
add comment

1 Answer

up vote 11 down vote accepted

The positions that you store within cursorX and cursorY hold the position of the cursor relative to top left corner of your screen monitor (since you add the ClientBounds to it). I'd guess that you used that exact position when drawing the circle, and since the Draw function takes a position relative to the top left corner of the game window, you'd be off by the position of the ClientBounds.

But I have to ask: why are you holding the "real" value of the mouse position? Is there any particular reason why you're storing it instead of the usual positions return by Mouse.GetState().X and Mouse.GetState().Y?


EDIT

After looking at your code, I think I found where the main problem is.

When defining the transformation matrix for your camera, you apply a translation to the center of the screen, like so:

_transform =       // Thanks to o KB o for this solution
    Matrix.CreateTranslation(new Vector3(-_pos.X, -_pos.Y, 0)) *
    Matrix.CreateRotationZ(Rotation) *
    Matrix.CreateScale(new Vector3(Zoom, Zoom, 0)) *
    Matrix.CreateTranslation(new Vector3(graphicsDevice.Viewport.Height * 0.5f,
                                         graphicsDevice.Viewport.Width * 0.5f, 0));

the relevant part being:

Matrix.CreateTranslation(new Vector3(graphicsDevice.Viewport.Height * 0.5f, graphicsDevice.Viewport.Width * 0.5f, 0));

From the comment I'm guessing that someone proposed this solution and it probably is perfectly fine but here is how I've changed your code to make it work:

Ok, so I changed a couple of things.

First, I removed the translation to the center, making the camera point at the top left corner, like so (removed the last translation):

_transform =       // Thanks to o KB o for this solution
    Matrix.CreateTranslation(new Vector3(-_pos.X, -_pos.Y, 0)) *
    Matrix.CreateRotationZ(Rotation) *
    Matrix.CreateScale(new Vector3(Zoom, Zoom, 0)));

Then, in your Game1.cs file, I put the player.Draw(spriteBatch) call within the spriteBatch.Begin() call affected by the camera's transformations, since the player should be affected by the camera. Like so:

spriteBatch.Begin(SpriteBlendMode.AlphaBlend,
                  SpriteSortMode.Immediate,
                  SaveStateMode.SaveState,
                  cam.get_transformation(GraphicsDevice));

player.Draw(spriteBatch);

spriteBatch.End();

Basically, you just have to think differently from usual. It's all about understanding the difference between screen positions and world positions (well, that's how I call them).

Screen positions are those that you use when you call spriteBatch.Draw(). They represent positions on your screen monitor.

World positions are those within your virtual world. You have to convert these to screen positions before drawing (by using the camera transformation matrix with a spriteBatch.Draw() call).

To illustrate the difference, here is an example: Let's say that you have a map with a size of 2500 x 2500. Your game window has a size of 500 x 500. Here is a quick drawing of that game world:

The green rectangle represents your game world (the one with a 2500 x 2500 size). The red rectangle represents the camera view (the size of the game screen). The blue rectangle represents an object within the world.

Now, the world position of that object would be (600, 600), because the world position is independent of where the camera is.

The screen position, however, is relative to the top left camera position. So its screen position (where it is drawn) would be (100, 100).

I think it's pretty clear with that example that to convert from a screen position to a world position, you must add the camera's position. To convert from world position to screen position, you must subtract the camera's position.

So, anything that is part of your world holds a world position (like a player or bullets). Anything that is part of the user interface holds a screen position (like the amount of ammo left or the aim of the player). All you have to do is put all the objects drawn using world positions within the spriteBatch.Draw() call using the camera. All objects independent of the camera should be drawn on the next spriteBatch.Draw() call.

So, let's go ahead and hide these implementation details within your Camera2D class:

public Vector2 GetScreenPosition(Vector2 worldPosition)
{
    return worldPosition - Pos;
}

public Vector2 GetWorldPosition(Vector2 screenPosition)
{
    return screenPosition + Pos;
}

I guess that you were using a translation to the center of the screen to keep the player centered. So let's place the player to the center within the LoadContent() function:

player = new Player(new Rectangle(
                        (int)(GraphicsDevice.Viewport.Width / 2f), 
                        (int)(GraphicsDevice.Viewport.Height / 2f), 50, 50), 
                    playertex, 5, bulletTex);

Next, let's add camera movement support (I changed the code you were using since I'm not applying the center-screen camera translation anymore):

Vector2 cameraDirection = Vector2.Zero;
const int TO_MOVE_SIDE_DISTANCE = 150; // the distance to the side required to move.

Vector2 playerScreenPosition = cam.GetScreenPosition(new Vector2(
            player.PlayerRect.X, player.PlayerRect.Y));
Rectangle playerScreenRect = new Rectangle((int)playerScreenPosition.X,
            (int)playerScreenPosition.Y, player.PlayerRect.Width, player.PlayerRect.Height);

//We make the camera follow the player if he goes too close to the sides.
if (playerScreenRect.Left < TO_MOVE_SIDE_DISTANCE) // move to the left
{
    --cameraDirection.X;
}
if (playerScreenRect.Right > GraphicsDevice.Viewport.Width - TO_MOVE_SIDE_DISTANCE) // move to the right
{
    ++cameraDirection.X;
}
if (playerScreenRect.Top < TO_MOVE_SIDE_DISTANCE) // move up
{
    --cameraDirection.Y;
}
if (playerScreenRect.Bottom > GraphicsDevice.Viewport.Height - TO_MOVE_SIDE_DISTANCE) // move down
{
    ++cameraDirection.Y;
}

cam.Pos += cameraDirection * player._PlayerSpeed; // move with the player.

So now the camera works. There's a new bug though: bullets start at the player's position, but are not aimed at the right place when the camera has been moved.

This is because we're aiming using the screen position (the cursor position, actually), when really we're aiming within the world. So, we have to convert the cursor's position to a world position to be able to aim properly. The shooting happens inside the Player class:

Vector2 targetWorldPosition = cam.GetWorldPosition(
    new Vector2(Mouse.GetState().X, Mouse.GetState().Y)); // we're aiming at an object within the world.

BulletList.Add(new Bullet(new Vector2(_PlayerRect.X, _PlayerRect.Y),
    targetWorldPosition, 5, bulletTex, 
    new Rectangle(_PlayerRect.X, _PlayerRect.Y, bulletTex.Width,bulletTex.Height), 90));

Notice that we're passing the target world position to the Bullet constructor now.

There you go, you have a working camera system.

I'm sure you could go with the centered transformation that you had at first, but I'm not used to that system. If you ever want to change system, just change the GetWorldPosition function, the GetScreenPosition function and the matrix transformation. Since those are hidden within the Camera2D class, changing the implementation details will be easy.

I hope this helped!


EDIT 2

To remove the problem where the camera transformation is applied twice when drawing the tile map, you must change this part of the code:

spriteBatch.Begin(SpriteBlendMode.AlphaBlend,
                  SpriteSortMode.Immediate,
                  SaveStateMode.SaveState,
                  cam.get_transformation(GraphicsDevice));

#region mapDrawing

Vector2 firstSquare = new Vector2(cam.Pos.X / Tile.TileWidth, cam.Pos.Y / Tile.TileHeight);
int firstX = (int)firstSquare.X;
int firstY = (int)firstSquare.Y;

Vector2 squareOffset = new Vector2(cam.Pos.X % Tile.TileWidth, cam.Pos.Y % Tile.TileHeight);
int offsetX = (int)squareOffset.X;
int offsetY = (int)squareOffset.Y;

if (Mouse.GetState().LeftButton == ButtonState.Pressed)
{
    Console.WriteLine(firstSquare + " " + squareOffset);
}

for (int y = 0; y < squaresDown; y++)
{
    for (int x = 0; x < squaresAcross; x++)
    {
        if(firstY >=0 && firstX >=0)
        foreach (int tileID in myMap.Rows[y + firstY].Columns[x + firstX].BaseTiles)
        {
            spriteBatch.Draw(
                Tile.TileSetTexture,
                new Rectangle(
                    (x * Tile.TileWidth) - offsetX, (y * Tile.TileHeight) - offsetY,
                    Tile.TileWidth, Tile.TileHeight),
                Tile.GetSourceRectangle(tileID),
                Color.White);
        }
    }
}

#endregion

player.Draw(spriteBatch, cam);


spriteBatch.End();

The whole problem is that you tell the spriteBatch to use your camera's transformation with this statement:

spriteBatch.Begin(SpriteBlendMode.AlphaBlend,
                  SpriteSortMode.Immediate,
                  SaveStateMode.SaveState,
                  cam.get_transformation(GraphicsDevice));

but then you use the camera's position to find firstSquare and squareOffset (cam.Pos.X / Tile.TileWidth, cam.Pos.Y % Tile.TileHeight, etc).

To avoid applying the transformation twice, you must use the camera's transformation only once by either moving that Draw call on a different spriteBatch.Begin call that doesn't apply the transformation, or by removing all the calculations involving the camera's position. I chose the latter since it makes simpler code:

spriteBatch.Begin(SpriteBlendMode.AlphaBlend,
                  SpriteSortMode.Immediate,
                  SaveStateMode.SaveState,
                  cam.get_transformation(GraphicsDevice));

#region mapDrawing

for (int y = 0; y < squaresDown; y++)
{
    for (int x = 0; x < squaresAcross; x++)
    {
        foreach (int tileID in myMap.Rows[y].Columns[x].BaseTiles)
        {
            spriteBatch.Draw(
                Tile.TileSetTexture,
                new Rectangle(
                    (x * Tile.TileWidth), (y * Tile.TileHeight),
                    Tile.TileWidth, Tile.TileHeight),
                Tile.GetSourceRectangle(tileID),
                Color.White);
        }
    }
}

#endregion

player.Draw(spriteBatch, cam);


spriteBatch.End();

This should work. If you prefer the other method, just us another spriteBatch.Begin() call without the matrix parameter and use the code you already have.

share|improve this answer
    
i did that at first, but it results in the same problem. and moving the window to the top left corner of the screen didn't work either. do you have any other idea? but still thanks. –  Bert May 18 '11 at 12:51
    
Could you post the code that you used to draw the circle as an edit? –  Jesse Emond May 18 '11 at 20:17
    
@Jesse the circle is just an image i placed on the location of the mouse, spriteBatch.Draw(zombieTex, new Rectangle(Mouse.GetState().X, Mouse.GetState().Y, 50, 50), Color.White); –  Bert May 19 '11 at 7:34
    
Oh, I see. Well you just called the SpriteBatch.Begin by providing a transformation (cam.get_transformation(graphics.GraphicsDevice)) so the "zombieTex" texture will be offset by the camera's transformation when it needs to be drawn directly to the screen without translation. That would be my guess. –  Jesse Emond May 19 '11 at 11:45
    
But how do i add that transormation to my sprites and other coördinates than? becaus i use my mouse location to shoot at too. –  Bert May 19 '11 at 12:10
show 24 more comments

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.