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 ("player");
bulletTex = Content.Load("bullet");
zombieTex = Content.Load("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.
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.