using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MonoGame.Extended.ViewportAdapters;
namespace MonoGame.Extended
{
///
/// Represents an orthographic (2D) camera that provides view and projection transformations for rendering
/// within a 2D world.
///
public sealed class OrthographicCamera : Camera, IMovable, IRotatable
{
private readonly ViewportAdapter _viewportAdapter;
private float _maximumZoom = float.MaxValue;
private float _minimumZoom;
private float _zoom;
private float _pitch;
private float _maximumPitch = float.MaxValue;
private float _minimumPitch;
private Vector2 _position;
private Rectangle _worldBounds;
private bool _clampZoomToWorldBounds;
///
///
/// When is , the camera position is clamped so that its
/// view remains within the defined .
///
public override Vector2 Position
{
get => _position;
set
{
_position = value;
if (IsClampedToWorldBounds)
{
ClampPositionToWorldBounds();
}
}
}
///
public override float Rotation { get; set; }
///
///
/// When is , the camera zoom is clamped so that its
/// view remains within the defined .
///
public override float Zoom
{
get => _zoom;
set
{
_zoom = value;
bool canClampToWorldBounds = CanClampToWorldBounds();
if (IsZoomClampedToWorldBounds && canClampToWorldBounds)
{
ClampZoomToWorldBounds();
}
_zoom = MathHelper.Clamp(_zoom, _minimumZoom, _maximumZoom);
if (canClampToWorldBounds)
{
ClampPositionToWorldBounds();
}
}
}
///
public override float MinimumZoom
{
get => _minimumZoom;
set
{
ArgumentOutOfRangeException.ThrowIfLessThan(value, 0);
_minimumZoom = value;
bool canClampToWorldBounds = CanClampToWorldBounds();
if (IsZoomClampedToWorldBounds && canClampToWorldBounds)
{
ClampZoomToWorldBounds();
}
_zoom = MathHelper.Clamp(_zoom, _minimumZoom, _maximumZoom);
if (canClampToWorldBounds)
{
ClampPositionToWorldBounds();
}
}
}
///
public override float MaximumZoom
{
get => _maximumZoom;
set
{
ArgumentOutOfRangeException.ThrowIfLessThan(value, 0);
_maximumZoom = value;
bool canClampToWorldBounds = CanClampToWorldBounds();
if (IsZoomClampedToWorldBounds && canClampToWorldBounds)
{
ClampZoomToWorldBounds();
}
_zoom = MathHelper.Clamp(_zoom, _minimumZoom, _maximumZoom);
if (canClampToWorldBounds)
{
ClampPositionToWorldBounds();
}
}
}
///
[Obsolete("Pitch will be removed in the next major version")]
public override float Pitch
{
get => _pitch;
set => _pitch = MathHelper.Clamp(value, _minimumPitch, _maximumPitch);
}
///
[Obsolete("Pitch will be removed in the next major version")]
public override float MinimumPitch
{
get => _minimumPitch;
set
{
ArgumentOutOfRangeException.ThrowIfLessThan(value, 0);
_minimumPitch = value;
_pitch = MathHelper.Clamp(_pitch, _minimumPitch, _maximumPitch);
}
}
///
[Obsolete("Pitch will be removed in the next major version")]
public override float MaximumPitch
{
get => _maximumPitch;
set
{
ArgumentOutOfRangeException.ThrowIfLessThan(value, 0);
_maximumPitch = value;
_pitch = MathHelper.Clamp(_pitch, _minimumPitch, _maximumPitch);
}
}
///
public override RectangleF BoundingRectangle
{
get
{
var frustum = GetBoundingFrustum();
var corners = frustum.GetCorners();
var topLeft = corners[0];
var bottomRight = corners[2];
var width = bottomRight.X - topLeft.X;
var height = bottomRight.Y - topLeft.Y;
return new RectangleF(topLeft.X, topLeft.Y, width, height);
}
}
///
public override Vector2 Origin { get; set; }
///
public override Vector2 Center => Position + Origin;
///
/// Gets the bounding rectangle that defines the limits of the camera's movement.
///
///
/// Use to set world bounds and enable constraints,
/// or to remove constraints.
///
public Rectangle WorldBounds => _worldBounds;
///
/// Gets a value indicating whether the camera is currently constrained within world bounds.
///
///
/// Use to enable world bounds constraints,
/// or to disable them.
///
public bool IsClampedToWorldBounds { get; private set; }
///
/// Gets or sets a value indicating whether the camera zoom should be clamped to world bounds.
///
///
/// When , the camera zoom is constrained so that the view cannot extend
/// beyond the world bounds. When , zoom is only constrained by
/// and .
/// This property only has effect when is .
///
public bool IsZoomClampedToWorldBounds
{
get => _clampZoomToWorldBounds;
set
{
_clampZoomToWorldBounds = value;
if (value)
{
ClampZoomToWorldBounds();
_zoom = MathHelper.Clamp(_zoom, _minimumZoom, _maximumZoom);
ClampPositionToWorldBounds();
}
}
}
///
/// Initializes a new instance of the class.
///
///
/// This constructor uses the .
///
/// The graphics device to associate with this camera.
public OrthographicCamera(GraphicsDevice graphicsDevice)
: this(new DefaultViewportAdapter(graphicsDevice))
{
}
///
/// Initializes a new instance of the class using the specified viewport adapter.
///
///
/// The viewport adapter that defines how world and screen coordinates are transformed.
///
public OrthographicCamera(ViewportAdapter viewportAdapter)
{
_viewportAdapter = viewportAdapter;
Rotation = 0;
Zoom = 1;
Pitch = 1;
Origin = new Vector2(viewportAdapter.VirtualWidth / 2f, viewportAdapter.VirtualHeight / 2f);
Position = Vector2.Zero;
}
///
public override void Move(Vector2 direction)
{
Position += Vector2.Transform(direction, Matrix.CreateRotationZ(-Rotation));
}
///
public override void Rotate(float deltaRadians)
{
Rotation += deltaRadians;
}
///
public override void ZoomIn(float deltaZoom)
{
Zoom += deltaZoom;
}
///
/// Increases the camera's zoom level while maintaining a specified world position as the zoom center.
///
/// The amount to increase the zoom by.
///
/// The world position to use as the zoom center. This point will remain fixed in screen space
/// as the zoom changes.
///
public void ZoomIn(float deltaZoom, Vector2 zoomCenter)
{
float previousZoom = Zoom;
Zoom += deltaZoom;
if (Zoom != previousZoom)
{
Position += (zoomCenter - Origin - Position) * ((Zoom - previousZoom) / Zoom);
}
}
///
public override void ZoomOut(float deltaZoom)
{
Zoom -= deltaZoom;
}
///
/// Decreases the camera's zoom level while maintaining a specified world position as the zoom center.
///
/// The amount to decrease the zoom by.
///
/// The world position to use as the zoom center. This point will remain fixed in screen space
/// as the zoom changes.
///
public void ZoomOut(float deltaZoom, Vector2 zoomCenter)
{
float previousZoom = Zoom;
Zoom -= deltaZoom;
if (Zoom != previousZoom)
{
Position += (zoomCenter - Origin - Position) * ((Zoom - previousZoom) / Zoom);
}
}
///
[Obsolete("Pitch will be removed in the next major version")]
public override void PitchUp(float deltaPitch)
{
Pitch += deltaPitch;
}
///
[Obsolete("Pitch will be removed in the next major version")]
public override void PitchDown(float deltaPitch)
{
Pitch -= deltaPitch;
}
///
///
/// The camera is positioned so that the specified appears at the center of
/// the viewport.
///
public override void LookAt(Vector2 position)
{
Position = position - new Vector2(_viewportAdapter.VirtualWidth / 2f, _viewportAdapter.VirtualHeight / 2f);
}
///
/// Converts a position from world coordinates to screen coordinates.
///
/// The x-position in world coordinates.
/// The y-position in world coordinates.
/// The corresponding position in screen coordinates.
public Vector2 WorldToScreen(float x, float y)
{
return WorldToScreen(new Vector2(x, y));
}
///
public override Vector2 WorldToScreen(Vector2 worldPosition)
{
Vector2 screenPosition = Vector2.Transform(worldPosition, GetViewMatrix());
// For scaling viewport adapters, the viewport offset is part of the coordinate transformation
if (_viewportAdapter is ScalingViewportAdapter)
{
var viewport = _viewportAdapter.Viewport;
screenPosition += new Vector2(viewport.X, viewport.Y);
}
return screenPosition;
}
///
/// Converts a position from screen coordinates to world coordinates.
///
/// The x-position in screen coordinates.
/// The y-position in screen coordinates.
/// The corresponding position in world coordinates.
public Vector2 ScreenToWorld(float x, float y)
{
return ScreenToWorld(new Vector2(x, y));
}
///
public override Vector2 ScreenToWorld(Vector2 screenPosition)
{
// For scaling viewport adapters, the viewport offset is part of the coordinate transformation
if (_viewportAdapter is ScalingViewportAdapter)
{
var viewport = _viewportAdapter.Viewport;
screenPosition -= new Vector2(viewport.X, viewport.Y);
}
return Vector2.Transform(screenPosition, Matrix.Invert(GetViewMatrix()));
}
///
/// Gets the view transformation matrix for the camera, applying a parallax factor.
///
///
/// The parallax factor to apply to the camera position. A value of (1,1) applies no parallax,
/// while values closer to (0,0) create a stronger parallax effect for background layers.
///
///
/// A representing the camera's view transformation with the specified
/// parallax factor applied.
///
public Matrix GetViewMatrix(Vector2 parallaxFactor)
{
return GetVirtualViewMatrix(parallaxFactor) * _viewportAdapter.GetScaleMatrix();
}
private Matrix GetVirtualViewMatrix(Vector2 parallaxFactor)
{
return
Matrix.CreateTranslation(new Vector3(-Position * parallaxFactor, 0.0f)) *
Matrix.CreateTranslation(new Vector3(-Origin, 0.0f)) *
Matrix.CreateRotationZ(Rotation) *
Matrix.CreateScale(Zoom, Zoom * Pitch, 1) *
Matrix.CreateTranslation(new Vector3(Origin, 0.0f));
}
private Matrix GetVirtualViewMatrix()
{
return GetVirtualViewMatrix(Vector2.One);
}
///
public override Matrix GetViewMatrix()
{
return GetViewMatrix(Vector2.One);
}
///
public override Matrix GetInverseViewMatrix()
{
return Matrix.Invert(GetViewMatrix());
}
private Matrix GetProjectionMatrix(Matrix viewMatrix)
{
var projection = Matrix.CreateOrthographicOffCenter(0, _viewportAdapter.VirtualWidth, _viewportAdapter.VirtualHeight, 0, -1, 0);
Matrix.Multiply(ref viewMatrix, ref projection, out projection);
return projection;
}
///
public override BoundingFrustum GetBoundingFrustum()
{
var viewMatrix = GetVirtualViewMatrix();
var projectionMatrix = GetProjectionMatrix(viewMatrix);
return new BoundingFrustum(projectionMatrix);
}
///
/// Determines whether the camera's view contains the specified point.
///
/// The point to test, in world coordinates.
///
/// A indicating whether the point is inside, outside, or
/// intersects the camera's view.
///
public ContainmentType Contains(Point point)
{
return Contains(point.ToVector2());
}
///
public override ContainmentType Contains(Vector2 vector2)
{
return GetBoundingFrustum().Contains(new Vector3(vector2.X, vector2.Y, 0));
}
///
public override ContainmentType Contains(Rectangle rectangle)
{
var max = new Vector3(rectangle.X + rectangle.Width, rectangle.Y + rectangle.Height, 0.5f);
var min = new Vector3(rectangle.X, rectangle.Y, 0.5f);
var boundingBox = new BoundingBox(min, max);
return GetBoundingFrustum().Contains(boundingBox);
}
///
/// Enables world bounds constraint for the camera and sets the bounding rectangle.
///
///
/// The bounding rectangle that defines the limits of the camera's movement and zoom.
///
///
/// When world bounds are enabled, the camera position and zoom are automatically clamped to
/// ensure the visible area does not extend beyond the specified bounds. This only applies
/// when the camera has no rotation and the pitch is 1.0.
///
public void EnableWorldBounds(Rectangle worldBounds)
{
_worldBounds = worldBounds;
IsClampedToWorldBounds = true;
ClampPositionToWorldBounds();
}
///
/// Disables world bounds constraint for the camera.
///
///
/// When world bounds are disabled, the camera can move and zoom freely without any constraints.
/// The world bounds rectangle is reset to .
///
public void DisableWorldBounds()
{
_worldBounds = Rectangle.Empty;
IsClampedToWorldBounds = false;
}
private void ClampZoomToWorldBounds()
{
// Calculate the size of the area the camera can see
Vector2 cameraSize = new Vector2(_viewportAdapter.VirtualWidth, _viewportAdapter.VirtualHeight) / _zoom;
// Only enforce minimum zoom if the camera view is larger than world bounds
if (cameraSize.X > _worldBounds.Width || cameraSize.Y > _worldBounds.Height)
{
float minZoomX = (float)_viewportAdapter.VirtualWidth / _worldBounds.Width;
float minZoomY = (float)_viewportAdapter.VirtualHeight / _worldBounds.Height;
float minZoom = MathHelper.Max(minZoomX, minZoomY);
if (_zoom < minZoom)
{
_zoom = minZoom;
}
}
}
private void ClampPositionToWorldBounds()
{
// Calculate the size of the area the camera can see
Vector2 cameraSize = new Vector2(_viewportAdapter.VirtualWidth, _viewportAdapter.VirtualHeight) / _zoom;
// If the world bounds are smaller than the camera view, then we center the camera in the world bounds.
if (_worldBounds.Width < cameraSize.X || _worldBounds.Height < cameraSize.Y)
{
_position = _worldBounds.Center.ToVector2() - Origin;
return;
}
// Get the camera's top-left corner in world space
Matrix inverseViewMatrix = GetInverseViewMatrix();
Vector2 cameraWorldMin = Vector2.Transform(Vector2.Zero, inverseViewMatrix);
Vector2 worldBoundsMin = new Vector2(_worldBounds.Left, _worldBounds.Top);
Vector2 worldBoundsMax = new Vector2(_worldBounds.Right, _worldBounds.Bottom);
// Calculate difference between position and world-space top-left.
Vector2 positionOffset = _position - cameraWorldMin;
// Clamp the camera's world-space top-left corner, then apply the offset
_position = Vector2.Clamp(cameraWorldMin, worldBoundsMin, worldBoundsMax - cameraSize) + positionOffset;
}
private bool CanClampToWorldBounds()
{
if (!IsClampedToWorldBounds || _worldBounds.Width <= 0 || _worldBounds.Height <= 0)
{
return false;
}
if (MathHelper.Distance(Rotation, 0.0f) >= 0.001f || MathHelper.Distance(Pitch, 1.0f) >= 0.001f)
{
return false;
}
return true;
}
}
}