I wrote something just for this a while ago, but for a 2D camera. I haven't tried adapting it to use a 3D camera, but since your game is always top down, I think it can be considered as a 2D camera too, so the concepts should be similar enough. Hope it's useful:
http://www.david-gouveia.com/limiting-2d-camera-movement-with-zoom/ (Video Here)
It's basically a way to extend your usual Camera class in order to add Rectangle Limits
property that specifies a region that the player can never seen beyond. This is done by validating the camera's Position and Zoom values whenever they change in accordance with the Limits property.
Start by reading the article for the entire solution, download the sample at the end and play with it. See how it could be adapted to your project. It's relatively simple, just two new methods and a few property changes.
As usual, for future reference, I'll leave the most relevant bits here too:
Step 1)
Add a Rectangle? Limits
property to the Camera class:
public interface Camera
{
public Viewport Viewport { get; }
public Vector2 Position { get; set; }
public float Zoom { get; set; }
public Matrix ViewMatrix { get; }
public Rectangle? Limits { set; }
}
Step 2)
Create methods to validate Zoom and Position of the camera. This is where you might need to change something to work with a 3D camera, so I'll also leave a general description:
private void ValidateZoom()
{
if (_limits.HasValue)
{
float minZoomX = (float)_viewport.Width / _limits.Value.Width;
float minZoomY = (float)_viewport.Height / _limits.Value.Height;
_zoom = MathHelper.Max(_zoom, MathHelper.Max(minZoomX, minZoomY));
}
}
Validating Zoom is all about the camera's "size", the extent that it can see. You basically find out what is the smallest amount of zoom the camera can have before it sees too much, and clamp it there.
private void ValidatePosition()
{
if(_limits.HasValue)
{
Vector2 cameraWorldMin = Vector2.Transform(Vector2.Zero, Matrix.Invert(ViewMatrix));
Vector2 cameraSize = new Vector2(_viewport.Width, _viewport.Height) / _zoom;
Vector2 limitWorldMin = new Vector2(_limits.Value.Left, _limits.Value.Top);
Vector2 limitWorldMax = new Vector2(_limits.Value.Right, _limits.Value.Bottom);
Vector2 positionOffset = _position - cameraWorldMin;
_position = Vector2.Clamp(cameraWorldMin, limitWorldMin, limitWorldMax - cameraSize) + positionOffset;
}
}
Validating Position requires basically comparing the camera's position against the corners of our limiting range. Since the limiting range is defined in world space, I get the camera's corner position in world space by multiplying Vector2.Zero by the inverse of the View matrix.
Step 3)
Apply the validations to the setters:
public Vector2 Position
{
get { return _position; }
set
{
_position = value;
ValidatePosition();
}
}
public float Zoom
{
get { return _zoom; }
set
{
_zoom = value;
ValidateZoom();
ValidatePosition();
}
}
public Rectangle? Limits
{
set
{
_limits = value;
ValidateZoom();
ValidatePosition();
}
}