OrthographicCamera.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. using System;
  2. using Microsoft.Xna.Framework;
  3. using Microsoft.Xna.Framework.Graphics;
  4. using MonoGame.Extended.ViewportAdapters;
  5. namespace MonoGame.Extended
  6. {
  7. /// <summary>
  8. /// Represents an orthographic (2D) camera that provides view and projection transformations for rendering
  9. /// within a 2D world.
  10. /// </summary>
  11. public sealed class OrthographicCamera : Camera<Vector2>, IMovable, IRotatable
  12. {
  13. private readonly ViewportAdapter _viewportAdapter;
  14. private float _maximumZoom = float.MaxValue;
  15. private float _minimumZoom;
  16. private float _zoom;
  17. private float _pitch;
  18. private float _maximumPitch = float.MaxValue;
  19. private float _minimumPitch;
  20. private Vector2 _position;
  21. private Rectangle _worldBounds;
  22. private bool _clampZoomToWorldBounds;
  23. /// <inheritdoc/>
  24. /// <remarks>
  25. /// When <see cref="IsClampedToWorldBounds"/> is <see langword="true"/>, the camera position is clamped so that its
  26. /// view remains within the defined <see cref="WorldBounds"/>.
  27. /// </remarks>
  28. public override Vector2 Position
  29. {
  30. get => _position;
  31. set
  32. {
  33. _position = value;
  34. if (IsClampedToWorldBounds)
  35. {
  36. ClampPositionToWorldBounds();
  37. }
  38. }
  39. }
  40. /// <inheritdoc/>
  41. public override float Rotation { get; set; }
  42. /// <inheritdoc/>
  43. /// <remarks>
  44. /// When <see cref="IsClampedToWorldBounds"/> is <see langword="true"/>, the camera zoom is clamped so that its
  45. /// view remains within the defined <see cref="WorldBounds"/>.
  46. /// </remarks>
  47. public override float Zoom
  48. {
  49. get => _zoom;
  50. set
  51. {
  52. _zoom = value;
  53. bool canClampToWorldBounds = CanClampToWorldBounds();
  54. if (IsZoomClampedToWorldBounds && canClampToWorldBounds)
  55. {
  56. ClampZoomToWorldBounds();
  57. }
  58. _zoom = MathHelper.Clamp(_zoom, _minimumZoom, _maximumZoom);
  59. if (canClampToWorldBounds)
  60. {
  61. ClampPositionToWorldBounds();
  62. }
  63. }
  64. }
  65. /// <inheritdoc/>
  66. public override float MinimumZoom
  67. {
  68. get => _minimumZoom;
  69. set
  70. {
  71. ArgumentOutOfRangeException.ThrowIfLessThan(value, 0);
  72. _minimumZoom = value;
  73. bool canClampToWorldBounds = CanClampToWorldBounds();
  74. if (IsZoomClampedToWorldBounds && canClampToWorldBounds)
  75. {
  76. ClampZoomToWorldBounds();
  77. }
  78. _zoom = MathHelper.Clamp(_zoom, _minimumZoom, _maximumZoom);
  79. if (canClampToWorldBounds)
  80. {
  81. ClampPositionToWorldBounds();
  82. }
  83. }
  84. }
  85. /// <inheritdoc/>
  86. public override float MaximumZoom
  87. {
  88. get => _maximumZoom;
  89. set
  90. {
  91. ArgumentOutOfRangeException.ThrowIfLessThan(value, 0);
  92. _maximumZoom = value;
  93. bool canClampToWorldBounds = CanClampToWorldBounds();
  94. if (IsZoomClampedToWorldBounds && canClampToWorldBounds)
  95. {
  96. ClampZoomToWorldBounds();
  97. }
  98. _zoom = MathHelper.Clamp(_zoom, _minimumZoom, _maximumZoom);
  99. if (canClampToWorldBounds)
  100. {
  101. ClampPositionToWorldBounds();
  102. }
  103. }
  104. }
  105. /// <inheritdoc/>
  106. [Obsolete("Pitch will be removed in the next major version")]
  107. public override float Pitch
  108. {
  109. get => _pitch;
  110. set => _pitch = MathHelper.Clamp(value, _minimumPitch, _maximumPitch);
  111. }
  112. /// <inheritdoc/>
  113. [Obsolete("Pitch will be removed in the next major version")]
  114. public override float MinimumPitch
  115. {
  116. get => _minimumPitch;
  117. set
  118. {
  119. ArgumentOutOfRangeException.ThrowIfLessThan(value, 0);
  120. _minimumPitch = value;
  121. _pitch = MathHelper.Clamp(_pitch, _minimumPitch, _maximumPitch);
  122. }
  123. }
  124. /// <inheritdoc/>
  125. [Obsolete("Pitch will be removed in the next major version")]
  126. public override float MaximumPitch
  127. {
  128. get => _maximumPitch;
  129. set
  130. {
  131. ArgumentOutOfRangeException.ThrowIfLessThan(value, 0);
  132. _maximumPitch = value;
  133. _pitch = MathHelper.Clamp(_pitch, _minimumPitch, _maximumPitch);
  134. }
  135. }
  136. /// <inheritdoc/>
  137. public override RectangleF BoundingRectangle
  138. {
  139. get
  140. {
  141. var frustum = GetBoundingFrustum();
  142. var corners = frustum.GetCorners();
  143. var topLeft = corners[0];
  144. var bottomRight = corners[2];
  145. var width = bottomRight.X - topLeft.X;
  146. var height = bottomRight.Y - topLeft.Y;
  147. return new RectangleF(topLeft.X, topLeft.Y, width, height);
  148. }
  149. }
  150. /// <inheritdoc/>
  151. public override Vector2 Origin { get; set; }
  152. /// <inheritdoc/>
  153. public override Vector2 Center => Position + Origin;
  154. /// <summary>
  155. /// Gets the bounding rectangle that defines the limits of the camera's movement.
  156. /// </summary>
  157. /// <remarks>
  158. /// Use <see cref="EnableWorldBounds(Rectangle)"/> to set world bounds and enable constraints,
  159. /// or <see cref="DisableWorldBounds()"/> to remove constraints.
  160. /// </remarks>
  161. public Rectangle WorldBounds => _worldBounds;
  162. /// <summary>
  163. /// Gets a value indicating whether the camera is currently constrained within world bounds.
  164. /// </summary>
  165. /// <remarks>
  166. /// Use <see cref="EnableWorldBounds(Rectangle)"/> to enable world bounds constraints,
  167. /// or <see cref="DisableWorldBounds()"/> to disable them.
  168. /// </remarks>
  169. public bool IsClampedToWorldBounds { get; private set; }
  170. /// <summary>
  171. /// Gets or sets a value indicating whether the camera zoom should be clamped to world bounds.
  172. /// </summary>
  173. /// <remarks>
  174. /// When <see langword="true"/>, the camera zoom is constrained so that the view cannot extend
  175. /// beyond the world bounds. When <see langword="false"/>, zoom is only constrained by
  176. /// <see cref="MinimumZoom"/> and <see cref="MaximumZoom"/>.
  177. /// This property only has effect when <see cref="IsClampedToWorldBounds"/> is <see langword="true"/>.
  178. /// </remarks>
  179. public bool IsZoomClampedToWorldBounds
  180. {
  181. get => _clampZoomToWorldBounds;
  182. set
  183. {
  184. _clampZoomToWorldBounds = value;
  185. if (value)
  186. {
  187. ClampZoomToWorldBounds();
  188. _zoom = MathHelper.Clamp(_zoom, _minimumZoom, _maximumZoom);
  189. ClampPositionToWorldBounds();
  190. }
  191. }
  192. }
  193. /// <summary>
  194. /// Initializes a new instance of the <see cref="OrthographicCamera"/> class.
  195. /// </summary>
  196. /// <remarks>
  197. /// This constructor uses the <see cref="DefaultViewportAdapter"/>.
  198. /// </remarks>
  199. /// <param name="graphicsDevice">The graphics device to associate with this camera.</param>
  200. public OrthographicCamera(GraphicsDevice graphicsDevice)
  201. : this(new DefaultViewportAdapter(graphicsDevice))
  202. {
  203. }
  204. /// <summary>
  205. /// Initializes a new instance of the <see cref="OrthographicCamera"/> class using the specified viewport adapter.
  206. /// </summary>
  207. /// <param name="viewportAdapter">
  208. /// The viewport adapter that defines how world and screen coordinates are transformed.
  209. /// </param>
  210. public OrthographicCamera(ViewportAdapter viewportAdapter)
  211. {
  212. _viewportAdapter = viewportAdapter;
  213. Rotation = 0;
  214. Zoom = 1;
  215. Pitch = 1;
  216. Origin = new Vector2(viewportAdapter.VirtualWidth / 2f, viewportAdapter.VirtualHeight / 2f);
  217. Position = Vector2.Zero;
  218. }
  219. /// <inheritdoc/>
  220. public override void Move(Vector2 direction)
  221. {
  222. Position += Vector2.Transform(direction, Matrix.CreateRotationZ(-Rotation));
  223. }
  224. /// <inheritdoc/>
  225. public override void Rotate(float deltaRadians)
  226. {
  227. Rotation += deltaRadians;
  228. }
  229. /// <inheritdoc/>
  230. public override void ZoomIn(float deltaZoom)
  231. {
  232. Zoom += deltaZoom;
  233. }
  234. /// <summary>
  235. /// Increases the camera's zoom level while maintaining a specified world position as the zoom center.
  236. /// </summary>
  237. /// <param name="deltaZoom">The amount to increase the zoom by.</param>
  238. /// <param name="zoomCenter">
  239. /// The world position to use as the zoom center. This point will remain fixed in screen space
  240. /// as the zoom changes.
  241. /// </param>
  242. public void ZoomIn(float deltaZoom, Vector2 zoomCenter)
  243. {
  244. float previousZoom = Zoom;
  245. Zoom += deltaZoom;
  246. if (Zoom != previousZoom)
  247. {
  248. Position += (zoomCenter - Origin - Position) * ((Zoom - previousZoom) / Zoom);
  249. }
  250. }
  251. /// <inheritdoc/>
  252. public override void ZoomOut(float deltaZoom)
  253. {
  254. Zoom -= deltaZoom;
  255. }
  256. /// <summary>
  257. /// Decreases the camera's zoom level while maintaining a specified world position as the zoom center.
  258. /// </summary>
  259. /// <param name="deltaZoom">The amount to decrease the zoom by.</param>
  260. /// <param name="zoomCenter">
  261. /// The world position to use as the zoom center. This point will remain fixed in screen space
  262. /// as the zoom changes.
  263. /// </param>
  264. public void ZoomOut(float deltaZoom, Vector2 zoomCenter)
  265. {
  266. float previousZoom = Zoom;
  267. Zoom -= deltaZoom;
  268. if (Zoom != previousZoom)
  269. {
  270. Position += (zoomCenter - Origin - Position) * ((Zoom - previousZoom) / Zoom);
  271. }
  272. }
  273. /// <inheritdoc/>
  274. [Obsolete("Pitch will be removed in the next major version")]
  275. public override void PitchUp(float deltaPitch)
  276. {
  277. Pitch += deltaPitch;
  278. }
  279. /// <inheritdoc/>
  280. [Obsolete("Pitch will be removed in the next major version")]
  281. public override void PitchDown(float deltaPitch)
  282. {
  283. Pitch -= deltaPitch;
  284. }
  285. /// <inheritdoc/>
  286. /// <remarks>
  287. /// The camera is positioned so that the specified <paramref name="position"/> appears at the center of
  288. /// the viewport.
  289. /// </remarks>
  290. public override void LookAt(Vector2 position)
  291. {
  292. Position = position - new Vector2(_viewportAdapter.VirtualWidth / 2f, _viewportAdapter.VirtualHeight / 2f);
  293. }
  294. /// <summary>
  295. /// Converts a position from world coordinates to screen coordinates.
  296. /// </summary>
  297. /// <param name="x">The x-position in world coordinates.</param>
  298. /// <param name="y">The y-position in world coordinates.</param>
  299. /// <returns>The corresponding position in screen coordinates.</returns>
  300. public Vector2 WorldToScreen(float x, float y)
  301. {
  302. return WorldToScreen(new Vector2(x, y));
  303. }
  304. /// <inheritdoc/>
  305. public override Vector2 WorldToScreen(Vector2 worldPosition)
  306. {
  307. Vector2 screenPosition = Vector2.Transform(worldPosition, GetViewMatrix());
  308. // For scaling viewport adapters, the viewport offset is part of the coordinate transformation
  309. if (_viewportAdapter is ScalingViewportAdapter)
  310. {
  311. var viewport = _viewportAdapter.Viewport;
  312. screenPosition += new Vector2(viewport.X, viewport.Y);
  313. }
  314. return screenPosition;
  315. }
  316. /// <summary>
  317. /// Converts a position from screen coordinates to world coordinates.
  318. /// </summary>
  319. /// <param name="x">The x-position in screen coordinates.</param>
  320. /// <param name="y">The y-position in screen coordinates.</param>
  321. /// <returns>The corresponding position in world coordinates.</returns>
  322. public Vector2 ScreenToWorld(float x, float y)
  323. {
  324. return ScreenToWorld(new Vector2(x, y));
  325. }
  326. /// <inheritdoc/>
  327. public override Vector2 ScreenToWorld(Vector2 screenPosition)
  328. {
  329. // For scaling viewport adapters, the viewport offset is part of the coordinate transformation
  330. if (_viewportAdapter is ScalingViewportAdapter)
  331. {
  332. var viewport = _viewportAdapter.Viewport;
  333. screenPosition -= new Vector2(viewport.X, viewport.Y);
  334. }
  335. return Vector2.Transform(screenPosition, Matrix.Invert(GetViewMatrix()));
  336. }
  337. /// <summary>
  338. /// Gets the view transformation matrix for the camera, applying a parallax factor.
  339. /// </summary>
  340. /// <param name="parallaxFactor">
  341. /// The parallax factor to apply to the camera position. A value of (1,1) applies no parallax,
  342. /// while values closer to (0,0) create a stronger parallax effect for background layers.
  343. /// </param>
  344. /// <returns>
  345. /// A <see cref="Matrix"/> representing the camera's view transformation with the specified
  346. /// parallax factor applied.
  347. /// </returns>
  348. public Matrix GetViewMatrix(Vector2 parallaxFactor)
  349. {
  350. return GetVirtualViewMatrix(parallaxFactor) * _viewportAdapter.GetScaleMatrix();
  351. }
  352. private Matrix GetVirtualViewMatrix(Vector2 parallaxFactor)
  353. {
  354. return
  355. Matrix.CreateTranslation(new Vector3(-Position * parallaxFactor, 0.0f)) *
  356. Matrix.CreateTranslation(new Vector3(-Origin, 0.0f)) *
  357. Matrix.CreateRotationZ(Rotation) *
  358. Matrix.CreateScale(Zoom, Zoom * Pitch, 1) *
  359. Matrix.CreateTranslation(new Vector3(Origin, 0.0f));
  360. }
  361. private Matrix GetVirtualViewMatrix()
  362. {
  363. return GetVirtualViewMatrix(Vector2.One);
  364. }
  365. /// <inheritdoc/>
  366. public override Matrix GetViewMatrix()
  367. {
  368. return GetViewMatrix(Vector2.One);
  369. }
  370. /// <inheritdoc/>
  371. public override Matrix GetInverseViewMatrix()
  372. {
  373. return Matrix.Invert(GetViewMatrix());
  374. }
  375. private Matrix GetProjectionMatrix(Matrix viewMatrix)
  376. {
  377. var projection = Matrix.CreateOrthographicOffCenter(0, _viewportAdapter.VirtualWidth, _viewportAdapter.VirtualHeight, 0, -1, 0);
  378. Matrix.Multiply(ref viewMatrix, ref projection, out projection);
  379. return projection;
  380. }
  381. /// <inheritdoc/>
  382. public override BoundingFrustum GetBoundingFrustum()
  383. {
  384. var viewMatrix = GetVirtualViewMatrix();
  385. var projectionMatrix = GetProjectionMatrix(viewMatrix);
  386. return new BoundingFrustum(projectionMatrix);
  387. }
  388. /// <summary>
  389. /// Determines whether the camera's view contains the specified point.
  390. /// </summary>
  391. /// <param name="point">The point to test, in world coordinates.</param>
  392. /// <returns>
  393. /// A <see cref="ContainmentType"/> indicating whether the point is inside, outside, or
  394. /// intersects the camera's view.
  395. /// </returns>
  396. public ContainmentType Contains(Point point)
  397. {
  398. return Contains(point.ToVector2());
  399. }
  400. /// <inheritdoc/>
  401. public override ContainmentType Contains(Vector2 vector2)
  402. {
  403. return GetBoundingFrustum().Contains(new Vector3(vector2.X, vector2.Y, 0));
  404. }
  405. /// <inheritdoc/>
  406. public override ContainmentType Contains(Rectangle rectangle)
  407. {
  408. var max = new Vector3(rectangle.X + rectangle.Width, rectangle.Y + rectangle.Height, 0.5f);
  409. var min = new Vector3(rectangle.X, rectangle.Y, 0.5f);
  410. var boundingBox = new BoundingBox(min, max);
  411. return GetBoundingFrustum().Contains(boundingBox);
  412. }
  413. /// <summary>
  414. /// Enables world bounds constraint for the camera and sets the bounding rectangle.
  415. /// </summary>
  416. /// <param name="worldBounds">
  417. /// The bounding rectangle that defines the limits of the camera's movement and zoom.
  418. /// </param>
  419. /// <remarks>
  420. /// When world bounds are enabled, the camera position and zoom are automatically clamped to
  421. /// ensure the visible area does not extend beyond the specified bounds. This only applies
  422. /// when the camera has no rotation and the pitch is 1.0.
  423. /// </remarks>
  424. public void EnableWorldBounds(Rectangle worldBounds)
  425. {
  426. _worldBounds = worldBounds;
  427. IsClampedToWorldBounds = true;
  428. ClampPositionToWorldBounds();
  429. }
  430. /// <summary>
  431. /// Disables world bounds constraint for the camera.
  432. /// </summary>
  433. /// <remarks>
  434. /// When world bounds are disabled, the camera can move and zoom freely without any constraints.
  435. /// The world bounds rectangle is reset to <see cref="Rectangle.Empty"/>.
  436. /// </remarks>
  437. public void DisableWorldBounds()
  438. {
  439. _worldBounds = Rectangle.Empty;
  440. IsClampedToWorldBounds = false;
  441. }
  442. private void ClampZoomToWorldBounds()
  443. {
  444. // Calculate the size of the area the camera can see
  445. Vector2 cameraSize = new Vector2(_viewportAdapter.VirtualWidth, _viewportAdapter.VirtualHeight) / _zoom;
  446. // Only enforce minimum zoom if the camera view is larger than world bounds
  447. if (cameraSize.X > _worldBounds.Width || cameraSize.Y > _worldBounds.Height)
  448. {
  449. float minZoomX = (float)_viewportAdapter.VirtualWidth / _worldBounds.Width;
  450. float minZoomY = (float)_viewportAdapter.VirtualHeight / _worldBounds.Height;
  451. float minZoom = MathHelper.Max(minZoomX, minZoomY);
  452. if (_zoom < minZoom)
  453. {
  454. _zoom = minZoom;
  455. }
  456. }
  457. }
  458. private void ClampPositionToWorldBounds()
  459. {
  460. // Calculate the size of the area the camera can see
  461. Vector2 cameraSize = new Vector2(_viewportAdapter.VirtualWidth, _viewportAdapter.VirtualHeight) / _zoom;
  462. // If the world bounds are smaller than the camera view, then we center the camera in the world bounds.
  463. if (_worldBounds.Width < cameraSize.X || _worldBounds.Height < cameraSize.Y)
  464. {
  465. _position = _worldBounds.Center.ToVector2() - Origin;
  466. return;
  467. }
  468. // Get the camera's top-left corner in world space
  469. Matrix inverseViewMatrix = GetInverseViewMatrix();
  470. Vector2 cameraWorldMin = Vector2.Transform(Vector2.Zero, inverseViewMatrix);
  471. Vector2 worldBoundsMin = new Vector2(_worldBounds.Left, _worldBounds.Top);
  472. Vector2 worldBoundsMax = new Vector2(_worldBounds.Right, _worldBounds.Bottom);
  473. // Calculate difference between position and world-space top-left.
  474. Vector2 positionOffset = _position - cameraWorldMin;
  475. // Clamp the camera's world-space top-left corner, then apply the offset
  476. _position = Vector2.Clamp(cameraWorldMin, worldBoundsMin, worldBoundsMax - cameraSize) + positionOffset;
  477. }
  478. private bool CanClampToWorldBounds()
  479. {
  480. if (!IsClampedToWorldBounds || _worldBounds.Width <= 0 || _worldBounds.Height <= 0)
  481. {
  482. return false;
  483. }
  484. if (MathHelper.Distance(Rotation, 0.0f) >= 0.001f || MathHelper.Distance(Pitch, 1.0f) >= 0.001f)
  485. {
  486. return false;
  487. }
  488. return true;
  489. }
  490. }
  491. }