using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using MLEM.Extensions; using MLEM.Misc; namespace MLEM.Cameras { /// /// Represents a simple, orthographic 2-dimensional camera that can be moved, scaled and that supports automatic viewport sizing. /// To draw with the camera's positioning and scaling applied, use . /// public class Camera { /// /// The top-left corner of the camera's viewport. /// /// public Vector2 Position; /// /// The scale that this camera's should have. /// public float Scale { get => this.scale; set => this.scale = MathHelper.Clamp(value, this.MinScale, this.MaxScale); } /// /// The minimum that the camera can have /// public float MinScale = 0; /// /// The maximum that the camera can have /// public float MaxScale = float.MaxValue; /// /// If this is true, the camera will automatically adapt to changed screen sizes. /// You can use to determine the initial screen size that this camera should base its calculations on. /// public bool AutoScaleWithScreen; /// /// /// public Point AutoScaleReferenceSize; /// /// The scale that this camera currently has, based on and if is true. /// public float ActualScale { get { if (!this.AutoScaleWithScreen) return this.Scale; return Math.Min(this.Viewport.Width / (float) this.AutoScaleReferenceSize.X, this.Viewport.Height / (float) this.AutoScaleReferenceSize.Y) * this.Scale; } } /// /// The matrix that this camera "sees", based on its position and scale. /// Use this in your calls to render based on the camera's viewport. /// public Matrix ViewMatrix { get { var sc = this.ActualScale; var pos = -this.Position * sc; if (this.RoundPosition) pos = pos.FloorCopy(); return Matrix.CreateScale(sc, sc, 1) * Matrix.CreateTranslation(new Vector3(pos, 0)); } } /// /// The bottom-right corner of the camera's viewport /// /// public Vector2 Max { get => this.Position + this.ScaledViewport; set => this.Position = value - this.ScaledViewport; } /// /// The center of the camera's viewport, or the position that the camera is looking at. /// public Vector2 LookingPosition { get => this.Position + this.ScaledViewport / 2; set => this.Position = value - this.ScaledViewport / 2; } /// /// The viewport of this camera, based on the game's and this camera's /// public Vector2 ScaledViewport => new Vector2(this.Viewport.Width, this.Viewport.Height) / this.ActualScale; /// /// Whether the camera's should be rounded to full integers when calculating the . /// If this value is true, the occurence of rendering fragments due to floating point rounding might be reduced. /// public bool RoundPosition; private Rectangle Viewport => this.graphicsDevice.Viewport.Bounds; private readonly GraphicsDevice graphicsDevice; private float scale = 1; /// /// Creates a new camera. /// /// The game's graphics device /// Whether the camera's should be rounded to full integers when calculating the public Camera(GraphicsDevice graphicsDevice, bool roundPosition = true) { this.graphicsDevice = graphicsDevice; this.AutoScaleReferenceSize = this.Viewport.Size; this.RoundPosition = roundPosition; } /// /// Converts a given position in screen space to world space /// /// The position in screen space /// The position in world space public Vector2 ToWorldPos(Vector2 pos) { return Vector2.Transform(pos, Matrix.Invert(this.ViewMatrix)); } /// /// Converts a given position in world space to screen space /// /// The position in world space /// The position in camera space public Vector2 ToCameraPos(Vector2 pos) { return Vector2.Transform(pos, this.ViewMatrix); } /// /// Returns the area that this camera can see, in world space. /// This can be useful for culling of tile and other entity renderers. /// /// A rectangle that represents the camera's visible area in world space public RectangleF GetVisibleRectangle() { var start = this.ToWorldPos(Vector2.Zero); return new RectangleF(start, this.ToWorldPos(new Vector2(this.Viewport.Width, this.Viewport.Height)) - start); } /// /// Forces the camera's bounds into the given min and max positions in world space. /// If the space represented by the given values is smaller than what the camera can see, its position will be forced into the center of the area. /// /// The top left bound, in world space /// The bottom right bound, in world space /// Whether or not the camera's position changed as a result of the constraint public bool ConstrainWorldBounds(Vector2 min, Vector2 max) { var lastPos = this.Position; var visible = this.GetVisibleRectangle(); if (max.X - min.X < visible.Width) { this.LookingPosition = new Vector2((max.X + min.X) / 2, this.LookingPosition.Y); } else { if (this.Position.X < min.X) this.Position.X = min.X; if (this.Max.X > max.X) this.Max = new Vector2(max.X, this.Max.Y); } if (max.Y - min.Y < visible.Height) { this.LookingPosition = new Vector2(this.LookingPosition.X, (max.Y + min.Y) / 2); } else { if (this.Position.Y < min.Y) this.Position.Y = min.Y; if (this.Max.Y > max.Y) this.Max = new Vector2(this.Max.X, max.Y); } return !this.Position.Equals(lastPos, 0.001F); } /// /// Zoom in the camera's view by a given amount, optionally focusing on a given center point. /// /// The amount to zoom in or out by /// The position that should be regarded as the zoom's center, in screen space. The default value is the center. public void Zoom(float delta, Vector2? zoomCenter = null) { var center = (zoomCenter ?? this.Viewport.Size.ToVector2() / 2) / this.ActualScale; var lastScale = this.Scale; this.Scale += delta; this.Position += center * ((this.Scale - lastScale) / this.Scale); } } }