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; } } }