Solution: See reply below. TL;DR: Make sure to understand all 3 spaces – window space, camera space, and world space.
Hey all,
I just recently got into MonoGame (and C#, for the most part), though I have some prior experience with OOP and app development. I have also worked with matrices and vectors in a prior university-level course, but it’s been probably too much time since then – I only seem to have a basic idea of what is going on.
In order to support resolution independence, I discovered after some research that using a RenderTarget2D is the way to go. Here is basically what I have working:
- Camera that supports rotating and scaling.
- Transitioning from screen to world coordinates (and vice versa) using my camera class
- Drawing to a
RenderTarget2D
However, I can’t seem to get everything to work together. I’m not sure if what I’m doing wrong is due to a conceptual misunderstanding or just a misunderstanding based on implementation (e.g. MonoGame specifics).
Here is the short version of what I’m doing (with more complete code snippets posted below):
- Instantiate
Camera
with bounds equivalent to the virtual resolution bounds that I want to target. - Create a
RenderTarget2D
with a virtual resolution that I want to target, e.g. 640x360. - Set
GraphicsDevice.RenderTarget
to myrenderTarget
. - Draw to world also using my camera’s Transform;
worldSpriteBatch.Begin(transformMatrix: camera.Transform);
- End my world’s SpriteBatch (
worldSpriteBatch
) and setGraphicsDevice.RenderTarget(null)
. - Draw the
renderTarget
to mymainSpriteBatch
with a destination rectangle equivalent to the camera’s bounds;mainSpriteBatch.Draw(texture: renderTarget, destinationRectangle: camera.Bounds, color: Color.White);
That’s the gist of it. Now, here’s what happens:
Option A: Details as above; setting ```destinationRectangle` to camera’s bounds
Everything “works” here. Game world is drawn correctly, camera works, proper world coordinates are received even after rotating and zooming.
Drawing with rotation and zoom support
Coordinate system
Option B: Setting destinationRectangle
to screen’s bounds
Now I want to expand the RenderTarget2D to fit the whole game window so I can achieve a pixel art look and feel.This is where things begin to break. The game world draws correctly to the screen. However, as we can see with the rectangle drawn to screen, the camera’s bounds (and therefore ScreenToWorld method) only work properly in that rectangle.
Drawing
Coordinates
Option C - attempted solution: Well, maybe I simply just need to set the camera’s bounds to fit the whole window.
The coordinate system works, but the drawing does not. I suspect this is due to, upon changing the camera’s bounds, also impacting the drawing when we feed the camera’s Transform into the worldSpriteBatch.Begin(transformMatrix: camera.Bounds)
.
Drawing
Coordinates
Possible solution
Upon input-related events (e.g. polling mouse position or mouse clicks) that require ScreenToWorld
, I’m thinking I could create a virtual matrix for the camera using a scale and run this new matrix through my ScreenToWorld
function. Alternatively, I can keep track of both virtual bounds and actual bounds (and corresponding transforms) in the Camera class. These, however, seem hack-y to me.I’m worried about going about the “right” way to do things. I don’t want a code base full of hacks and workarounds.
Anyhow, if you read this far, thanks for taking a look!
Most of the actual code here:
Camera.cs
important snippets
Camera.cs
/// <summary>
/// Instantiate Camera given a viewport, namely the Bounds
/// </summary>
/// <param name="viewport"></param>
public Camera(Viewport viewport)
{
Bounds = viewport.Bounds;
//VirtualBounds = Bounds;
Zoom = 1f;
}
#region Space Transforms
/// <summary>
/// Get screen coordinates from world coordinates
/// </summary>
/// <param name="worldPosition"></param>
/// <returns></returns>
public Vector2 WorldToScreen(Vector2 worldPosition)
{
return Vector2.Transform(worldPosition, Transform);
}
/// <summary>
/// Get world coordinates from screen coordinates
/// </summary>
/// <param name="screenPosition"></param>
/// <returns></returns>
public Vector2 ScreenToWorld(Vector2 screenPosition)
{
return Vector2.Transform(screenPosition, Matrix.Invert(Transform));
}
#endregion Space Transforms
/// <summary>
/// Update camera to a specific position
/// Also updates the camera's Transform
/// </summary>
/// <param name="position"></param>
public void Update(Vector2 position)
{
// TODO: Check for camera input
Position = position;
Transform = Matrix.CreateTranslation(new Vector3(-position.X, -position.Y, 0)) *
Matrix.CreateRotationZ(Rotation) * // Rotation
Matrix.CreateScale(new Vector3(Zoom, Zoom, 1f)) * // Scale
Matrix.CreateTranslation(new Vector3(Bounds.Width * 0.5f, Bounds.Height * 0.5f, 0)); // Centred
}
Game1.cs
important snippets
Game1.cs
private int actualWidth = 1152;
private int actualHeight = 648;
private int virtualWidth = 640;
private int virtualHeight = 360;
RenderTarget2D renderTarget;
GraphicsDeviceManager graphics;
SpriteBatch mainSpriteBatch;
SpriteBatch worldSpriteBatch;
SpriteBatch textSpriteBatch;
Camera camera;
...
public Game1()
{
graphics = new GraphicsDeviceManager(this);
// Set a 16:9 aspect ratio for game *window*
graphics.PreferredBackBufferWidth = actualWidth;
graphics.PreferredBackBufferHeight = actualHeight;
...
}
protected override void LoadContent()
{
...
// Load camera
//camera = new Camera(graphics.GraphicsDevice.Viewport);
camera = new Camera(new Viewport(0, 0, virtualWidth, virtualHeight));
// Prepare RenderTarget2D
renderTarget = new RenderTarget2D(graphics.GraphicsDevice, virtualWidth, virtualHeight); // Resolution to target
....
}
protected override void Draw(GameTime gameTime)
{
// Draw scene to RenderTarget, essentially a texture
// Width and height of our *virtual* resolution
GraphicsDevice.SetRenderTarget(renderTarget);
GraphicsDevice.Clear(Color.CornflowerBlue); // Blue for "inside" the game world
// Can now draw our stuff to RenderTarget
#region worldSpriteBatch
// Use our camera Transform so that we can specify *world* coordinates
worldSpriteBatch.Begin(transformMatrix: camera.Transform);
for (int i = 0; i < 10; i++)
{
worldSpriteBatch.DrawString(arialFont, "W(0, " + 15 * i + ")", new Vector2(0, 15 * i), Color.White);
}
worldSpriteBatch.DrawString(arialFont, "W(250 250)", new Vector2(250, 250), Color.White);
worldSpriteBatch.Draw(gargoyle, new Vector2(-300, 0), Color.White);
worldSpriteBatch.DrawString(arialFont, "W(-300, 0)", new Vector2(-300, 0), Color.White);
//worldSpriteBatch.Draw(frame32, new Vector2(32, 32), Color.White);
worldSpriteBatch.End();
#endregion worldSpriteBatch
// Revert back to the original (the screen -- the backbuffer) render target
GraphicsDevice.SetRenderTarget(null);
GraphicsDevice.Clear(Color.Gray); // Gray for "outside" the world/camera render
// Now draw the mainSpriteBatch
string debugMessage =
"camera Bounds: " + camera.Bounds + "; " +
"renderTarget: " + renderTarget.Bounds + "; " +
"x,y scale: " + xScale + ", " + yScale;
System.Console.WriteLine(debugMessage);
mainSpriteBatch.Begin(samplerState: SamplerState.PointClamp); // Use PointClamp for hard edges
// Option A: Set destination of renderTarget to *camer's* bounds
Rectangle destination = camera.Bounds;
// Option B: Set destination of renderTarget to *window's* bounds
//Rectangle destination = new Rectangle(0, 0, graphics.PreferredBackBufferWidth, graphics.PreferredBackBufferHeight);
// Option C - attempted fix: Initialise camera with bounds to match actual window (see above)
// I suspect this isn't working due to something to do with drawing to worldSpriteBatch using camera's Transform
// Draw to our mainSpriteBatch, responsible for what gets shown to screen in totality
mainSpriteBatch.Draw(
texture: renderTarget,
//position: Vector2.Zero, // Draw RenderTarget to top-left of screen (0, 0)
destinationRectangle: destination,
color: Color.White);
// Debug to draw rectangle's with specified pixel width and color
DrawBorder(mainSpriteBatch, camera.Bounds, 15, Color.Blue); // Where mainSpriteBatch thinks camera Bounds are
DrawBorder(mainSpriteBatch, destination, 10, Color.Red); // Where mainSpriteBatch thinks destination of renderTarget is
mainSpriteBatch.End();
// SpriteBatch for drawing text and UI, independent of game world
#region uiSpriteBatch
textSpriteBatch.Begin();
//DrawBorder(textSpriteBatch, dst, 4, Color.Green);
DrawBorder(textSpriteBatch, camera.Bounds, 6, Color.Black); // Where actual camera bounds are
DrawBorder(textSpriteBatch, destination, 3, Color.Green); // Where actual destination bounds are
// Debug to show screen and world coordinates of mouse. Uses Camera's space transforms (ScreenToWorld and WorldToScreen)
InputManager.Debug(textSpriteBatch, arialFont, camera, new Vector2(0, 0));
//textSpriteBatch.DrawString(arialFont, "Mouse: screen " + mousePositionScreenSpace + ", world " + mousePositionWorldSpace, new Vector2(0, 0), Color.White);
textSpriteBatch.End();
#endregion uiSpriteBatch
base.Draw(gameTime);