2019-08-23 19:46:36 +02:00
using System ;
2019-08-07 01:21:32 +02:00
using Microsoft.Xna.Framework ;
using Microsoft.Xna.Framework.Graphics ;
using MLEM.Extensions ;
2019-11-02 14:56:16 +01:00
using MLEM.Misc ;
2019-08-07 01:21:32 +02:00
namespace MLEM.Cameras {
2020-05-20 23:59:40 +02:00
/// <summary>
/// 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 <see cref="ViewMatrix"/>.
/// </summary>
2019-08-07 01:21:32 +02:00
public class Camera {
2020-05-20 23:59:40 +02:00
/// <summary>
/// The top-left corner of the camera's viewport.
/// <seealso cref="LookingPosition"/>
/// </summary>
2019-08-07 01:21:32 +02:00
public Vector2 Position ;
2020-05-20 23:59:40 +02:00
/// <summary>
/// The scale that this camera's <see cref="ViewMatrix"/> should have.
/// </summary>
2019-11-10 22:36:25 +01:00
public float Scale {
get = > this . scale ;
set = > this . scale = MathHelper . Clamp ( value , this . MinScale , this . MaxScale ) ;
}
2020-05-20 23:59:40 +02:00
/// <summary>
/// The minimum <see cref="Scale"/> that the camera can have
/// </summary>
2019-11-10 22:36:25 +01:00
public float MinScale = 0 ;
2020-05-20 23:59:40 +02:00
/// <summary>
/// The maximum <see cref="Scale"/> that the camera can have
/// </summary>
2019-11-10 22:36:25 +01:00
public float MaxScale = float . MaxValue ;
2020-05-20 23:59:40 +02:00
/// <summary>
/// If this is true, the camera will automatically adapt to changed screen sizes.
/// You can use <see cref="AutoScaleReferenceSize"/> to determine the initial screen size that this camera should base its calculations on.
/// </summary>
2019-08-23 19:46:36 +02:00
public bool AutoScaleWithScreen ;
2020-05-20 23:59:40 +02:00
/// <summary>
/// <seealso cref="AutoScaleWithScreen"/>
/// </summary>
2019-08-23 19:46:36 +02:00
public Point AutoScaleReferenceSize ;
2020-05-20 23:59:40 +02:00
/// <summary>
/// The scale that this camera currently has, based on <see cref="Scale"/> and <see cref="AutoScaleReferenceSize"/> if <see cref="AutoScaleWithScreen"/> is true.
/// </summary>
2019-08-23 19:46:36 +02:00
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 ;
}
}
2020-05-20 23:59:40 +02:00
/// <summary>
/// The matrix that this camera "sees", based on its position and scale.
/// Use this in your <see cref="SpriteBatch.Begin"/> calls to render based on the camera's viewport.
/// </summary>
2019-08-07 01:21:32 +02:00
public Matrix ViewMatrix {
get {
2019-08-23 19:46:36 +02:00
var sc = this . ActualScale ;
var pos = - this . Position * sc ;
2019-08-07 01:21:32 +02:00
if ( this . roundPosition )
2020-07-27 00:24:49 +02:00
pos = pos . FloorCopy ( ) ;
2019-08-23 19:46:36 +02:00
return Matrix . CreateScale ( sc , sc , 1 ) * Matrix . CreateTranslation ( new Vector3 ( pos , 0 ) ) ;
2019-08-07 01:21:32 +02:00
}
}
2020-05-20 23:59:40 +02:00
/// <summary>
/// The bottom-right corner of the camera's viewport
/// <seealso cref="LookingPosition"/>
/// </summary>
2019-08-07 16:32:19 +02:00
public Vector2 Max {
get = > this . Position + this . ScaledViewport ;
set = > this . Position = value - this . ScaledViewport ;
}
2020-05-20 23:59:40 +02:00
/// <summary>
/// The center of the camera's viewport, or the position that the camera is looking at.
/// </summary>
2019-08-07 16:32:19 +02:00
public Vector2 LookingPosition {
get = > this . Position + this . ScaledViewport / 2 ;
set = > this . Position = value - this . ScaledViewport / 2 ;
}
2021-03-09 02:29:06 +01:00
/// <summary>
/// The viewport of this camera, based on the game's <see cref="GraphicsDevice.Viewport"/> and this camera's <see cref="ActualScale"/>
/// </summary>
public Vector2 ScaledViewport = > new Vector2 ( this . Viewport . Width , this . Viewport . Height ) / this . ActualScale ;
2019-08-07 01:21:32 +02:00
2021-03-09 02:29:06 +01:00
private Rectangle Viewport = > this . graphicsDevice . Viewport . Bounds ;
2019-08-07 01:21:32 +02:00
private readonly bool roundPosition ;
private readonly GraphicsDevice graphicsDevice ;
2021-03-09 02:29:06 +01:00
private float scale = 1 ;
2019-08-07 01:21:32 +02:00
2020-05-20 23:59:40 +02:00
/// <summary>
/// Creates a new camera.
/// </summary>
/// <param name="graphicsDevice">The game's graphics device</param>
/// <param name="roundPosition">If this is true, the camera's <see cref="Position"/> and related properties will be rounded to full integers.</param>
2019-08-07 01:21:32 +02:00
public Camera ( GraphicsDevice graphicsDevice , bool roundPosition = true ) {
this . graphicsDevice = graphicsDevice ;
2019-08-23 19:46:36 +02:00
this . AutoScaleReferenceSize = this . Viewport . Size ;
2019-08-07 01:21:32 +02:00
this . roundPosition = roundPosition ;
}
2020-05-20 23:59:40 +02:00
/// <summary>
/// Converts a given position in screen space to world space
/// </summary>
/// <param name="pos">The position in screen space</param>
/// <returns>The position in world space</returns>
2019-08-07 01:21:32 +02:00
public Vector2 ToWorldPos ( Vector2 pos ) {
return Vector2 . Transform ( pos , Matrix . Invert ( this . ViewMatrix ) ) ;
}
2020-05-20 23:59:40 +02:00
/// <summary>
/// Converts a given position in world space to screen space
/// </summary>
/// <param name="pos">The position in world space</param>
/// <returns>The position in camera space</returns>
2019-08-07 01:21:32 +02:00
public Vector2 ToCameraPos ( Vector2 pos ) {
return Vector2 . Transform ( pos , this . ViewMatrix ) ;
}
2020-05-20 23:59:40 +02:00
/// <summary>
/// Returns the area that this camera can see, in world space.
/// This can be useful for culling of tile and other entity renderers.
/// </summary>
/// <returns>A rectangle that represents the camera's visible area in world space</returns>
2019-11-02 14:56:16 +01:00
public RectangleF GetVisibleRectangle ( ) {
var start = this . ToWorldPos ( Vector2 . Zero ) ;
return new RectangleF ( start , this . ToWorldPos ( new Vector2 ( this . Viewport . Width , this . Viewport . Height ) ) - start ) ;
}
2020-05-20 23:59:40 +02:00
/// <summary>
/// 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.
/// </summary>
/// <param name="min">The top left bound, in world space</param>
/// <param name="max">The bottom right bound, in world space</param>
/// <returns>Whether or not the camera's position changed as a result of the constraint</returns>
2019-12-26 13:01:32 +01:00
public bool ConstrainWorldBounds ( Vector2 min , Vector2 max ) {
var lastPos = this . Position ;
2019-12-26 22:46:38 +01:00
var visible = this . GetVisibleRectangle ( ) ;
if ( max . X - min . X < visible . Width ) {
2020-06-01 17:00:32 +02:00
this . LookingPosition = new Vector2 ( ( max . X + min . X ) / 2 , this . LookingPosition . Y ) ;
2019-12-06 20:54:30 +01:00
} 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 ) ;
}
2019-08-07 16:32:19 +02:00
2019-12-26 22:46:38 +01:00
if ( max . Y - min . Y < visible . Height ) {
2020-06-01 17:00:32 +02:00
this . LookingPosition = new Vector2 ( this . LookingPosition . X , ( max . Y + min . Y ) / 2 ) ;
2019-12-06 20:54:30 +01:00
} 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 ) ;
}
2019-12-26 13:01:32 +01:00
return ! this . Position . Equals ( lastPos , 0.001F ) ;
2019-08-07 16:32:19 +02:00
}
2020-05-20 23:59:40 +02:00
/// <summary>
/// Zoom in the camera's view by a given amount, optionally focusing on a given center point.
/// </summary>
/// <param name="delta">The amount to zoom in or out by</param>
/// <param name="zoomCenter">The position that should be regarded as the zoom's center, in screen space. The default value is the center.</param>
2019-11-10 22:36:25 +01:00
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 ) ;
}
2019-08-07 01:21:32 +02:00
}
}