diff options
Diffstat (limited to 'Plugins/MonoGame.Extended/source')
423 files changed, 31527 insertions, 0 deletions
diff --git a/Plugins/MonoGame.Extended/source/Directory.Build.props b/Plugins/MonoGame.Extended/source/Directory.Build.props new file mode 100644 index 0000000..ae50af1 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/Directory.Build.props @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project> + + <Import Project="$(MSBuildThisFileDirectory)..\Directory.Build.props" /> + + <PropertyGroup> + <ArtifactsPath>$(SolutionDirectory).artifacts/source</ArtifactsPath> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <IsPackable>true</IsPackable> + <NoWarn>NU1701</NoWarn> + <AllowUnsafeBlocks>true</AllowUnsafeBlocks> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="MonoGame.Framework.DesktopGL" + Version="3.8.1.303" + PrivateAssets="All" /> + </ItemGroup> + +</Project> diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Animations/MonoGame.Extended.Animations.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Animations/MonoGame.Extended.Animations.csproj new file mode 100644 index 0000000..3745741 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Animations/MonoGame.Extended.Animations.csproj @@ -0,0 +1,12 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <Description>Animations to make MonoGame more awesome.</Description> + <PackageTags>monogame animations spritesheet sprite</PackageTags> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" /> + </ItemGroup> + +</Project> diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionComponent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionComponent.cs new file mode 100644 index 0000000..51467d9 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionComponent.cs @@ -0,0 +1,341 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Diagnostics; +using System.Linq; +using Microsoft.Xna.Framework; +using MonoGame.Extended.Collisions.Layers; +using MonoGame.Extended.Collisions.QuadTree; + +namespace MonoGame.Extended.Collisions +{ + /// <summary> + /// Handles basic collision between actors. + /// When two actors collide, their OnCollision method is called. + /// </summary> + public class CollisionComponent : SimpleGameComponent + { + public const string DEFAULT_LAYER_NAME = "default"; + + private Dictionary<string, Layer> _layers = new(); + + /// <summary> + /// List of collision's layers + /// </summary> + public IReadOnlyDictionary<string, Layer> Layers => _layers; + + private HashSet<(Layer, Layer)> _layerCollision = new(); + + /// <summary> + /// Creates component with default layer, which is a collision tree covering the specified area (using <see cref="QuadTree"/>. + /// </summary> + /// <param name="boundary">Boundary of the collision tree.</param> + public CollisionComponent(RectangleF boundary) + { + SetDefaultLayer(new Layer(new QuadTreeSpace(boundary))); + } + + /// <summary> + /// Creates component with specifies default layer. + /// If layer is null, method creates component without default layer. + /// </summary> + /// <param name="layer">Default layer</param> + public CollisionComponent(Layer layer = null) + { + if (layer is not null) + SetDefaultLayer(layer); + } + + /// <summary> + /// The main layer has the name from <see cref="DEFAULT_LAYER_NAME"/>. + /// The main layer collision with itself and all other layers. + /// </summary> + /// <param name="layer">Layer to set default</param> + public void SetDefaultLayer(Layer layer) + { + if (_layers.ContainsKey(DEFAULT_LAYER_NAME)) + Remove(DEFAULT_LAYER_NAME); + Add(DEFAULT_LAYER_NAME, layer); + foreach (var otherLayer in _layers.Values) + AddCollisionBetweenLayer(layer, otherLayer); + } + + /// <summary> + /// Update the collision tree and process collisions. + /// </summary> + /// <remarks> + /// Boundary shapes are updated if they were changed since the last + /// update. + /// </remarks> + /// <param name="gameTime"></param> + public override void Update(GameTime gameTime) + { + foreach (var layer in _layers.Values) + layer.Reset(); + + foreach (var (firstLayer, secondLayer) in _layerCollision) + foreach (var actor in firstLayer.Space) + { + var collisions = secondLayer.Space.Query(actor.Bounds.BoundingRectangle); + foreach (var other in collisions) + if (actor != other && actor.Bounds.Intersects(other.Bounds)) + { + var penetrationVector = CalculatePenetrationVector(actor.Bounds, other.Bounds); + + actor.OnCollision(new CollisionEventArgs + { + Other = other, + PenetrationVector = penetrationVector + }); + other.OnCollision(new CollisionEventArgs + { + Other = actor, + PenetrationVector = -penetrationVector + }); + } + + } + } + + /// <summary> + /// Inserts the target into the collision tree. + /// The target will have its OnCollision called when collisions occur. + /// </summary> + /// <param name="target">Target to insert.</param> + public void Insert(ICollisionActor target) + { + var layerName = target.LayerName ?? DEFAULT_LAYER_NAME; + if (!_layers.TryGetValue(layerName, out var layer)) + { + throw new UndefinedLayerException(layerName); + } + + layer.Space.Insert(target); + } + + /// <summary> + /// Removes the target from the collision tree. + /// </summary> + /// <param name="target">Target to remove.</param> + public void Remove(ICollisionActor target) + { + if (target.LayerName is not null) + _layers[target.LayerName].Space.Remove(target); + else + foreach (var layer in _layers.Values) + if (layer.Space.Remove(target)) + return; + } + + #region Layers + + /// <summary> + /// Add the new layer. The name of layer must be unique. + /// </summary> + /// <param name="name">Name of layer</param> + /// <param name="layer">The new layer</param> + /// <exception cref="ArgumentNullException"><paramref name="name"/> is null</exception> + public void Add(string name, Layer layer) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentNullException(nameof(name)); + + if (!_layers.TryAdd(name, layer)) + throw new DuplicateNameException(name); + + if (name != DEFAULT_LAYER_NAME) + AddCollisionBetweenLayer(_layers[DEFAULT_LAYER_NAME], layer); + } + + /// <summary> + /// Remove the layer and all layer's collisions. + /// </summary> + /// <param name="name">The name of the layer to delete</param> + /// <param name="layer">The layer to delete</param> + public void Remove(string name = null, Layer layer = null) + { + name ??= _layers.First(x => x.Value == layer).Key; + _layers.Remove(name, out layer); + _layerCollision.RemoveWhere(tuple => tuple.Item1 == layer || tuple.Item2 == layer); + } + + public void AddCollisionBetweenLayer(Layer a, Layer b) + { + _layerCollision.Add((a, b)); + } + + public void AddCollisionBetweenLayer(string nameA, string nameB) + { + _layerCollision.Add((_layers[nameA], _layers[nameB])); + } + + #endregion + + #region Penetration Vectors + + /// <summary> + /// Calculate a's penetration into b + /// </summary> + /// <param name="a">The penetrating shape.</param> + /// <param name="b">The shape being penetrated.</param> + /// <returns>The distance vector from the edge of b to a's Position</returns> + private static Vector2 CalculatePenetrationVector(IShapeF a, IShapeF b) + { + return a switch + { + CircleF circleA when b is CircleF circleB => PenetrationVector(circleA, circleB), + CircleF circleA when b is RectangleF rectangleB => PenetrationVector(circleA, rectangleB), + CircleF circleA when b is OrientedRectangle orientedRectangleB => PenetrationVector(circleA, orientedRectangleB), + + RectangleF rectangleA when b is CircleF circleB => PenetrationVector(rectangleA, circleB), + RectangleF rectangleA when b is RectangleF rectangleB => PenetrationVector(rectangleA, rectangleB), + RectangleF rectangleA when b is OrientedRectangle orientedRectangleB => PenetrationVector(rectangleA, orientedRectangleB), + + OrientedRectangle orientedRectangleA when b is CircleF circleB => PenetrationVector(orientedRectangleA, circleB), + OrientedRectangle orientedRectangleA when b is RectangleF rectangleB => PenetrationVector(orientedRectangleA, rectangleB), + OrientedRectangle orientedRectangleA when b is OrientedRectangle orientedRectangleB => PenetrationVector(orientedRectangleA, orientedRectangleB), + + _ => throw new ArgumentOutOfRangeException(nameof(a)) + }; + } + + private static Vector2 PenetrationVector(CircleF circ1, CircleF circ2) + { + if (!circ1.Intersects(circ2)) + { + return Vector2.Zero; + } + + var displacement = Point2.Displacement(circ1.Center, circ2.Center); + + Vector2 desiredDisplacement; + if (displacement != Vector2.Zero) + { + desiredDisplacement = displacement.NormalizedCopy() * (circ1.Radius + circ2.Radius); + } + else + { + desiredDisplacement = -Vector2.UnitY * (circ1.Radius + circ2.Radius); + } + + + var penetration = displacement - desiredDisplacement; + return penetration; + } + + private static Vector2 PenetrationVector(CircleF circ, RectangleF rect) + { + var collisionPoint = rect.ClosestPointTo(circ.Center); + var cToCollPoint = collisionPoint - circ.Center; + + if (rect.Contains(circ.Center) || cToCollPoint.Equals(Vector2.Zero)) + { + var displacement = Point2.Displacement(circ.Center, rect.Center); + + Vector2 desiredDisplacement; + if (displacement != Vector2.Zero) + { + // Calculate penetration as only in X or Y direction. + // Whichever is lower. + var dispx = new Vector2(displacement.X, 0); + var dispy = new Vector2(0, displacement.Y); + dispx.Normalize(); + dispy.Normalize(); + + dispx *= (circ.Radius + rect.Width / 2); + dispy *= (circ.Radius + rect.Height / 2); + + if (dispx.LengthSquared() < dispy.LengthSquared()) + { + desiredDisplacement = dispx; + displacement.Y = 0; + } + else + { + desiredDisplacement = dispy; + displacement.X = 0; + } + } + else + { + desiredDisplacement = -Vector2.UnitY * (circ.Radius + rect.Height / 2); + } + + var penetration = displacement - desiredDisplacement; + return penetration; + } + else + { + var penetration = circ.Radius * cToCollPoint.NormalizedCopy() - cToCollPoint; + return penetration; + } + } + + private static Vector2 PenetrationVector(CircleF circleA, OrientedRectangle orientedRectangleB) + { + var rotation = Matrix2.CreateRotationZ(orientedRectangleB.Orientation.Rotation); + var circleCenterInRectangleSpace = rotation.Transform(circleA.Center - orientedRectangleB.Center); + var circleInRectangleSpace = new CircleF(circleCenterInRectangleSpace, circleA.Radius); + var boundingRectangle = new BoundingRectangle(new Point2(), orientedRectangleB.Radii); + + var penetrationVector = PenetrationVector(circleInRectangleSpace, boundingRectangle); + var inverseRotation = Matrix2.CreateRotationZ(-orientedRectangleB.Orientation.Rotation); + var transformedPenetration = inverseRotation.Transform(penetrationVector); + + return transformedPenetration; + } + + private static Vector2 PenetrationVector(RectangleF rect, CircleF circ) + { + return -PenetrationVector(circ, rect); + } + + private static Vector2 PenetrationVector(RectangleF rect1, RectangleF rect2) + { + var intersectingRectangle = RectangleF.Intersection(rect1, rect2); + Debug.Assert(!intersectingRectangle.IsEmpty, + "Violation of: !intersect.IsEmpty; Rectangles must intersect to calculate a penetration vector."); + + Vector2 penetration; + if (intersectingRectangle.Width < intersectingRectangle.Height) + { + var d = rect1.Center.X < rect2.Center.X + ? intersectingRectangle.Width + : -intersectingRectangle.Width; + penetration = new Vector2(d, 0); + } + else + { + var d = rect1.Center.Y < rect2.Center.Y + ? intersectingRectangle.Height + : -intersectingRectangle.Height; + penetration = new Vector2(0, d); + } + + return penetration; + } + + private static Vector2 PenetrationVector(RectangleF rectangleA, OrientedRectangle orientedRectangleB) + { + return PenetrationVector((OrientedRectangle)rectangleA, orientedRectangleB); + } + + private static Vector2 PenetrationVector(OrientedRectangle orientedRectangleA, CircleF circleB) + { + return -PenetrationVector(circleB, orientedRectangleA); + } + + private static Vector2 PenetrationVector(OrientedRectangle orientedRectangleA, RectangleF rectangleB) + { + return -PenetrationVector(rectangleB, orientedRectangleA); + } + + private static Vector2 PenetrationVector(OrientedRectangle orientedRectangleA, OrientedRectangle orientedRectangleB) + { + return OrientedRectangle.Intersects(orientedRectangleA, orientedRectangleB) + .MinimumTranslationVector; + } + + #endregion + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionEventArgs.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionEventArgs.cs new file mode 100644 index 0000000..ca401df --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/CollisionEventArgs.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Collisions +{ + /// <summary> + /// This class holds data on a collision. It is passed as a parameter to + /// OnCollision methods. + /// </summary> + public class CollisionEventArgs : EventArgs + { + /// <summary> + /// Gets the object being collided with. + /// </summary> + public ICollisionActor Other { get; internal set; } + + /// <summary> + /// Gets a vector representing the overlap between the two objects. + /// </summary> + /// <remarks> + /// This vector starts at the edge of <see cref="Other"/> and ends at + /// the Actor's location. + /// </remarks> + public Vector2 PenetrationVector { get; internal set; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/ICollisionActor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/ICollisionActor.cs new file mode 100644 index 0000000..6a05592 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/ICollisionActor.cs @@ -0,0 +1,27 @@ +using System; + +namespace MonoGame.Extended.Collisions +{ + /// <summary> + /// An actor that can be collided with. + /// </summary> + public interface ICollisionActor + { + /// <summary> + /// A name of layer, which will contains this actor. + /// If it equals null, an actor will insert into a default layer + /// </summary> + string LayerName { get => null; } + + /// <summary> + /// A bounds of an actor. It is using for collision calculating + /// </summary> + IShapeF Bounds { get; } + + /// <summary> + /// It will called, when collision with an another actor fires + /// </summary> + /// <param name="collisionInfo">Data about collision</param> + void OnCollision(CollisionEventArgs collisionInfo); + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/ISpaceAlgorithm.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/ISpaceAlgorithm.cs new file mode 100644 index 0000000..a95f737 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/ISpaceAlgorithm.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; + +namespace MonoGame.Extended.Collisions; + +/// <summary> +/// Interface, which split space for optimization of collisions. +/// </summary> +public interface ISpaceAlgorithm +{ + /// <summary> + /// Inserts the actor into the space. + /// The actor will have its OnCollision called when collisions occur. + /// </summary> + /// <param name="actor">Actor to insert.</param> + void Insert(ICollisionActor actor); + + /// <summary> + /// Removes the actor into the space. + /// </summary> + /// <param name="actor">Actor to remove.</param> + bool Remove(ICollisionActor actor); + + /// <summary> + /// Removes the actor into the space. + /// The actor will have its OnCollision called when collisions occur. + /// </summary> + /// <param name="actor">Actor to remove.</param> + IEnumerable<ICollisionActor> Query(RectangleF boundsBoundingRectangle); + + /// <summary> + /// for foreach + /// </summary> + /// <returns></returns> + List<ICollisionActor>.Enumerator GetEnumerator(); + + /// <summary> + /// Restructure the space with new positions. + /// </summary> + void Reset(); +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/Layers/Layer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/Layers/Layer.cs new file mode 100644 index 0000000..6e97ac8 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/Layers/Layer.cs @@ -0,0 +1,39 @@ +using System; + +namespace MonoGame.Extended.Collisions.Layers; + +/// <summary> +/// Layer is a group of collision's actors. +/// </summary> +public class Layer +{ + /// <summary> + /// If this property equals true, layer always will reset collision space. + /// </summary> + public bool IsDynamic { get; set; } = true; + + + /// <summary> + /// The space, which contain actors. + /// </summary> + public readonly ISpaceAlgorithm Space; + + /// <summary> + /// Constructor for layer + /// </summary> + /// <param name="spaceAlgorithm">A space algorithm for actors</param> + /// <exception cref="ArgumentNullException"><paramref name="spaceAlgorithm"/> is null</exception> + public Layer(ISpaceAlgorithm spaceAlgorithm) + { + Space = spaceAlgorithm ?? throw new ArgumentNullException(nameof(spaceAlgorithm)); + } + + /// <summary> + /// Restructure a inner collection, if layer is dynamic, because actors can change own position + /// </summary> + public virtual void Reset() + { + if (IsDynamic) + Space.Reset(); + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/Layers/UndefinedLayerException.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/Layers/UndefinedLayerException.cs new file mode 100644 index 0000000..a27b5b6 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/Layers/UndefinedLayerException.cs @@ -0,0 +1,18 @@ +namespace MonoGame.Extended.Collisions.Layers; + +using System; + +/// <summary> +/// Thrown when the collision system has no layer defined with the specified name +/// </summary> +public class UndefinedLayerException : Exception +{ + /// <summary> + /// Thrown when the collision system has no layer defined with the specified name + /// </summary> + /// <param name="layerName">The undefined layer name</param> + public UndefinedLayerException(string layerName) + : base($"Layer with name '{layerName}' is undefined") + { + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/MonoGame.Extended.Collisions.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/MonoGame.Extended.Collisions.csproj new file mode 100644 index 0000000..bd0729c --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/MonoGame.Extended.Collisions.csproj @@ -0,0 +1,12 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <Description>Collisions to make MonoGame more awesome.</Description> + <PackageTags>monogame collisions</PackageTags> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" /> + </ItemGroup> + +</Project> diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTree.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTree.cs new file mode 100644 index 0000000..a46699a --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTree.cs @@ -0,0 +1,310 @@ +using System; +using System.Collections.Generic; + +namespace MonoGame.Extended.Collisions.QuadTree +{ + /// <summary> + /// Class for doing collision handling with a quad tree. + /// </summary> + public class QuadTree + { + /// <summary> + /// The default maximum depth. + /// </summary> + public const int DefaultMaxDepth = 7; + + /// <summary> + /// The default maximum objects per node. + /// </summary> + public const int DefaultMaxObjectsPerNode = 25; + + /// <summary> + /// Contains the children of this node. + /// </summary> + protected List<QuadTree> Children = new List<QuadTree>(); + + /// <summary> + /// Contains the data for this node in the quadtree. + /// </summary> + protected HashSet<QuadtreeData> Contents = new HashSet<QuadtreeData>(); + + /// <summary> + /// Creates a quad tree with the given bounds. + /// </summary> + /// <param name="bounds">The bounds of the new quad tree.</param> + public QuadTree(RectangleF bounds) + { + CurrentDepth = 0; + NodeBounds = bounds; + } + + /// <summary> + /// Gets or sets the current depth for this node in the quadtree. + /// </summary> + protected int CurrentDepth { get; set; } + /// <summary> + /// Gets or sets the maximum depth of the quadtree. + /// </summary> + protected int MaxDepth { get; set; } = DefaultMaxDepth; + /// <summary> + /// Gets or sets the maximum objects per node in this quadtree. + /// </summary> + protected int MaxObjectsPerNode { get; set; } = DefaultMaxObjectsPerNode; + + /// <summary> + /// Gets the bounds of the area contained in this quad tree. + /// </summary> + public RectangleF NodeBounds { get; protected set; } + + /// <summary> + /// Gets whether the current node is a leaf node. + /// </summary> + public bool IsLeaf => Children.Count == 0; + + /// <summary> + /// Counts the number of unique targets in the current Quadtree. + /// </summary> + /// <returns>Returns the targets of objects found.</returns> + public int NumTargets() + { + List<QuadtreeData> dirtyItems = new List<QuadtreeData>(); + var objectCount = 0; + + // Do BFS on nodes to count children. + var process = new Queue<QuadTree>(); + process.Enqueue(this); + while (process.Count > 0) + { + var processing = process.Dequeue(); + if (!processing.IsLeaf) + { + foreach (var child in processing.Children) + { + process.Enqueue(child); + } + } + else + { + foreach (var data in processing.Contents) + { + if (data.Dirty == false) + { + objectCount++; + data.MarkDirty(); + dirtyItems.Add(data); + } + } + } + } + foreach (var quadtreeData in dirtyItems) + { + quadtreeData.MarkClean(); + } + return objectCount; + } + + /// <summary> + /// Inserts the data into the tree. + /// </summary> + /// <param name="data">Data being inserted.</param> + public void Insert(QuadtreeData data) + { + var actorBounds = data.Bounds; + + // Object doesn't fit into this node. + if (!NodeBounds.Intersects(actorBounds)) + { + return; + } + + if (IsLeaf && Contents.Count >= MaxObjectsPerNode) + { + Split(); + } + + if (IsLeaf) + { + AddToLeaf(data); + } + else + { + foreach (var child in Children) + { + child.Insert(data); + } + } + } + + /// <summary> + /// Removes data from the Quadtree + /// </summary> + /// <param name="data">The data to be removed.</param> + public void Remove(QuadtreeData data) + { + if (IsLeaf) + { + data.RemoveParent(this); + Contents.Remove(data); + } + else + { + throw new InvalidOperationException($"Cannot remove from a non leaf {nameof(QuadTree)}"); + } + } + + /// <summary> + /// Removes unnecessary leaf nodes and simplifies the quad tree. + /// </summary> + public void Shake() + { + if (IsLeaf) + { + return; + } + + List<QuadtreeData> dirtyItems = new List<QuadtreeData>(); + + var numObjects = NumTargets(); + if (numObjects == 0) + { + Children.Clear(); + } + else if (numObjects < MaxObjectsPerNode) + { + var process = new Queue<QuadTree>(); + process.Enqueue(this); + while (process.Count > 0) + { + var processing = process.Dequeue(); + if (!processing.IsLeaf) + { + foreach (var subTree in processing.Children) + { + process.Enqueue(subTree); + } + } + else + { + foreach (var data in processing.Contents) + { + if (data.Dirty == false) + { + AddToLeaf(data); + data.MarkDirty(); + dirtyItems.Add(data); + } + } + } + } + Children.Clear(); + } + + foreach (var quadtreeData in dirtyItems) + { + quadtreeData.MarkClean(); + } + } + + private void AddToLeaf(QuadtreeData data) + { + data.AddParent(this); + Contents.Add(data); + } + + /// <summary> + /// Splits a quadtree into quadrants. + /// </summary> + public void Split() + { + if (CurrentDepth + 1 >= MaxDepth) return; + + var min = NodeBounds.TopLeft; + var max = NodeBounds.BottomRight; + var center = NodeBounds.Center; + + RectangleF[] childAreas = + { + RectangleF.CreateFrom(min, center), + RectangleF.CreateFrom(new Point2(center.X, min.Y), new Point2(max.X, center.Y)), + RectangleF.CreateFrom(center, max), + RectangleF.CreateFrom(new Point2(min.X, center.Y), new Point2(center.X, max.Y)) + }; + + for (var i = 0; i < childAreas.Length; ++i) + { + var node = new QuadTree(childAreas[i]); + Children.Add(node); + Children[i].CurrentDepth = CurrentDepth + 1; + } + + foreach (QuadtreeData contentQuadtree in Contents) + { + foreach (QuadTree childQuadtree in Children) + { + childQuadtree.Insert(contentQuadtree); + } + } + Clear(); + } + + /// <summary> + /// Clear current node and all children + /// </summary> + public void ClearAll() + { + foreach (QuadTree childQuadtree in Children) + childQuadtree.ClearAll(); + Clear(); + } + + private void Clear() + { + foreach (QuadtreeData quadtreeData in Contents) + { + quadtreeData.RemoveParent(this); + } + Contents.Clear(); + } + + /// <summary> + /// Queries the quadtree for targets that intersect with the given area. + /// </summary> + /// <param name="area">The area to query for overlapping targets</param> + /// <returns>A unique list of targets intersected by area.</returns> + public List<QuadtreeData> Query(ref RectangleF area) + { + var recursiveResult = new List<QuadtreeData>(); + QueryWithoutReset(ref area, recursiveResult); + foreach (var quadtreeData in recursiveResult) + { + quadtreeData.MarkClean(); + } + return recursiveResult; + } + + private void QueryWithoutReset(ref RectangleF area, List<QuadtreeData> recursiveResult) + { + if (!NodeBounds.Intersects(area)) + return; + + if (IsLeaf) + { + foreach (QuadtreeData quadtreeData in Contents) + { + if (quadtreeData.Dirty == false && quadtreeData.Bounds.Intersects(area)) + { + recursiveResult.Add(quadtreeData); + quadtreeData.MarkDirty(); + } + } + } + else + { + for (int i = 0, size = Children.Count; i < size; i++) + { + Children[i].QueryWithoutReset(ref area, recursiveResult); + } + } + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTreeData.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTreeData.cs new file mode 100644 index 0000000..db6da3e --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTreeData.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; +using System.Linq; + +namespace MonoGame.Extended.Collisions.QuadTree; + +/// <summary> +/// Data structure for the quad tree. +/// Holds the entity and collision data for it. +/// </summary> +public class QuadtreeData +{ + private readonly ICollisionActor _target; + private readonly HashSet<QuadTree> _parents = new(); + + /// <summary> + /// Initialize a new instance of QuadTreeData. + /// </summary> + /// <param name="target"></param> + public QuadtreeData(ICollisionActor target) + { + _target = target; + Bounds = _target.Bounds.BoundingRectangle; + } + + /// <summary> + /// Remove a parent node. + /// </summary> + /// <param name="parent"></param> + public void RemoveParent(QuadTree parent) + { + _parents.Remove(parent); + } + + /// <summary> + /// Add a parent node. + /// </summary> + /// <param name="parent"></param> + public void AddParent(QuadTree parent) + { + _parents.Add(parent); + Bounds = _target.Bounds.BoundingRectangle; + } + + /// <summary> + /// Remove all parent nodes from this node. + /// </summary> + public void RemoveFromAllParents() + { + foreach (var parent in _parents.ToList()) + { + parent.Remove(this); + } + + _parents.Clear(); + } + + /// <summary> + /// Gets the bounding box for collision detection. + /// </summary> + public RectangleF Bounds { get; set; } + + /// <summary> + /// Gets the collision actor target. + /// </summary> + public ICollisionActor Target => _target; + + /// <summary> + /// Gets or sets whether Target has had its collision handled this + /// iteration. + /// </summary> + public bool Dirty { get; private set; } + + /// <summary> + /// Mark node as dirty. + /// </summary> + public void MarkDirty() + { + Dirty = true; + } + + /// <summary> + /// Mark node as clean, i.e. not dirty. + /// </summary> + public void MarkClean() + { + Dirty = false; + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTreeSpace.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTreeSpace.cs new file mode 100644 index 0000000..3e9625f --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/QuadTree/QuadTreeSpace.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Linq; + +namespace MonoGame.Extended.Collisions.QuadTree; + +public class QuadTreeSpace: ISpaceAlgorithm +{ + private readonly QuadTree _collisionTree; + private readonly List<ICollisionActor> _actors = new(); + private readonly Dictionary<ICollisionActor, QuadtreeData> _targetDataDictionary = new(); + + public QuadTreeSpace(RectangleF boundary) + { + _collisionTree = new QuadTree(boundary); + } + + /// <summary> + /// Inserts the target into the collision tree. + /// The target will have its OnCollision called when collisions occur. + /// </summary> + /// <param name="target">Target to insert.</param> + public void Insert(ICollisionActor target) + { + if (!_targetDataDictionary.ContainsKey(target)) + { + var data = new QuadtreeData(target); + _targetDataDictionary.Add(target, data); + _collisionTree.Insert(data); + _actors.Add(target); + } + } + + /// <summary> + /// Removes the target from the collision tree. + /// </summary> + /// <param name="target">Target to remove.</param> + public bool Remove(ICollisionActor target) + { + if (_targetDataDictionary.ContainsKey(target)) + { + var data = _targetDataDictionary[target]; + data.RemoveFromAllParents(); + _targetDataDictionary.Remove(target); + _collisionTree.Shake(); + _actors.Remove(target); + return true; + } + + return false; + } + + /// <summary> + /// Restructure a inner collection, if layer is dynamic, because actors can change own position + /// </summary> + public void Reset() + { + _collisionTree.ClearAll(); + foreach (var value in _targetDataDictionary.Values) + { + _collisionTree.Insert(value); + } + _collisionTree.Shake(); + } + + /// <summary> + /// foreach support + /// </summary> + /// <returns></returns> + public List<ICollisionActor>.Enumerator GetEnumerator() => _actors.GetEnumerator(); + + /// <inheritdoc cref="QuadTree.Query"/> + public IEnumerable<ICollisionActor> Query(RectangleF boundsBoundingRectangle) + { + return _collisionTree.Query(ref boundsBoundingRectangle).Select(x => x.Target); + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/SpatialHash.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/SpatialHash.cs new file mode 100644 index 0000000..2b0920e --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/SpatialHash.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace MonoGame.Extended.Collisions; + +public class SpatialHash: ISpaceAlgorithm +{ + private readonly Dictionary<int, List<ICollisionActor>> _dictionary = new(); + private readonly List<ICollisionActor> _actors = new(); + + private readonly Size2 _size; + + public SpatialHash(Size2 size) + { + _size = size; + } + + public void Insert(ICollisionActor actor) + { + InsertToHash(actor); + _actors.Add(actor); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void InsertToHash(ICollisionActor actor) + { + var rect = actor.Bounds.BoundingRectangle; + for (var x = rect.Left; x < rect.Right; x+=_size.Width) + for (var y = rect.Top; y < rect.Bottom; y+=_size.Height) + AddToCell(x, y, actor); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddToCell(float x, float y, ICollisionActor actor) + { + var index = GetIndex(x, y); + if (_dictionary.TryGetValue(index, out var actors)) + actors.Add(actor); + else + _dictionary[index] = new() { actor }; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetIndex(float x, float y) + { + return (int)(x / _size.Width) << 16 + (int)(y / _size.Height); + } + + public bool Remove(ICollisionActor actor) + { + foreach (var actors in _dictionary.Values) + actors.Remove(actor); + return _actors.Remove(actor); + } + + public IEnumerable<ICollisionActor> Query(RectangleF boundsBoundingRectangle) + { + var results = new HashSet<ICollisionActor>(); + var bounds = boundsBoundingRectangle.BoundingRectangle; + + for (var x = boundsBoundingRectangle.Left; x < boundsBoundingRectangle.Right; x+=_size.Width) + for (var y = boundsBoundingRectangle.Top; y < boundsBoundingRectangle.Bottom; y+=_size.Height) + if (_dictionary.TryGetValue(GetIndex(x, y), out var actors)) + foreach (var actor in actors) + if (bounds.Intersects(actor.Bounds)) + results.Add(actor); + return results; + } + + public List<ICollisionActor>.Enumerator GetEnumerator() => _actors.GetEnumerator(); + + public void Reset() + { + _dictionary.Clear(); + foreach (var actor in _actors) + InsertToHash(actor); + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/packages.config b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/packages.config new file mode 100644 index 0000000..28c0144 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Collisions/packages.config @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<packages> + <package id="MonoGame.Framework.Portable" version="3.6.0.1625" targetFramework="portable45-net45+win8+wpa81" /> +</packages>
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorAnimation.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorAnimation.cs new file mode 100644 index 0000000..14638d4 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorAnimation.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace MonoGame.Extended.Content.Pipeline.Animations +{ + public class AstridAnimatorAnimation + { + public string Name { get; set; } + public int FramesPerSecond { get; set; } + public List<string> Frames { get; set; } + public bool IsLooping { get; set; } + public bool IsReversed { get; set; } + public bool IsPingPong { get; set; } + + public AstridAnimatorAnimation(string name, int framesPerSecond) + { + Name = name; + FramesPerSecond = framesPerSecond; + Frames = new List<string>(); + IsLooping = true; + IsReversed = false; + IsPingPong = false; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorFile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorFile.cs new file mode 100644 index 0000000..765f2f2 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorFile.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace MonoGame.Extended.Content.Pipeline.Animations +{ + public class AstridAnimatorFile + { + public string TextureAtlas { get; set; } + public List<AstridAnimatorAnimation> Animations { get; set; } + + public AstridAnimatorFile() + { + Animations = new List<AstridAnimatorAnimation>(); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorImporter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorImporter.cs new file mode 100644 index 0000000..855a0d3 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorImporter.cs @@ -0,0 +1,18 @@ +using System.IO; +using System.Text.Json; +using Microsoft.Xna.Framework.Content.Pipeline; + +namespace MonoGame.Extended.Content.Pipeline.Animations +{ + [ContentImporter(".aa", DefaultProcessor = "AstridAnimatorProcessor", + DisplayName = "Astrid Animator Importer - MonoGame.Extended")] + public class AstridAnimatorImporter : ContentImporter<ContentImporterResult<AstridAnimatorFile>> + { + public override ContentImporterResult<AstridAnimatorFile> Import(string filename, ContentImporterContext context) + { + var json = File.ReadAllText(filename); + var data = JsonSerializer.Deserialize<AstridAnimatorFile>(json); + return new ContentImporterResult<AstridAnimatorFile>(filename, data); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorProcessor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorProcessor.cs new file mode 100644 index 0000000..e22d65b --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorProcessor.cs @@ -0,0 +1,24 @@ +using System.IO; +using System.Linq; +using Microsoft.Xna.Framework.Content.Pipeline; + +namespace MonoGame.Extended.Content.Pipeline.Animations +{ + [ContentProcessor(DisplayName = "Astrid Animator Processor - MonoGame.Extended")] + public class AstridAnimatorProcessor : + ContentProcessor<ContentImporterResult<AstridAnimatorFile>, AstridAnimatorProcessorResult> + { + public override AstridAnimatorProcessorResult Process(ContentImporterResult<AstridAnimatorFile> input, + ContentProcessorContext context) + { + var data = input.Data; + var directory = Path.GetDirectoryName(input.FilePath); + var frames = data.Animations + .SelectMany(i => i.Frames) + .OrderBy(f => f) + .Distinct(); + + return new AstridAnimatorProcessorResult(directory, data, frames); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorProcessorResult.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorProcessorResult.cs new file mode 100644 index 0000000..de92ec3 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorProcessorResult.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.IO; + +namespace MonoGame.Extended.Content.Pipeline.Animations +{ + public class AstridAnimatorProcessorResult + { + public string TextureAtlasAssetName { get; private set; } + public string Directory { get; private set; } + public AstridAnimatorFile Data { get; private set; } + public List<string> Frames { get; private set; } + + public AstridAnimatorProcessorResult(string directory, AstridAnimatorFile data, IEnumerable<string> frames) + { + Directory = directory; + Data = data; + Frames = new List<string>(frames); + TextureAtlasAssetName = Path.GetFileNameWithoutExtension(data.TextureAtlas); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorWriter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorWriter.cs new file mode 100644 index 0000000..6f56f3f --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Animations/AstridAnimatorWriter.cs @@ -0,0 +1,40 @@ +using Microsoft.Xna.Framework.Content.Pipeline; +using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler; + +namespace MonoGame.Extended.Content.Pipeline.Animations +{ + [ContentTypeWriter] + public class AstridAnimatorWriter : ContentTypeWriter<AstridAnimatorProcessorResult> + { + public override string GetRuntimeReader(TargetPlatform targetPlatform) + { + return "MonoGame.Extended.Animations.SpriteSheets.SpriteSheetAnimationFactoryReader, MonoGame.Extended.Animations"; + } + + protected override void Write(ContentWriter writer, AstridAnimatorProcessorResult input) + { + var data = input.Data; + + writer.Write(input.TextureAtlasAssetName); + writer.Write(input.Frames.Count); + + foreach (var frame in input.Frames) + writer.Write(frame); + + writer.Write(data.Animations.Count); + + foreach (var animation in data.Animations) + { + writer.Write(animation.Name); + writer.Write(animation.FramesPerSecond); + writer.Write(animation.IsLooping); + writer.Write(animation.IsReversed); + writer.Write(animation.IsPingPong); + writer.Write(animation.Frames.Count); + + foreach (var frame in animation.Frames) + writer.Write(input.Frames.IndexOf(frame)); + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontChar.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontChar.cs new file mode 100644 index 0000000..c012da4 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontChar.cs @@ -0,0 +1,41 @@ +using System.Xml.Serialization; + +namespace MonoGame.Extended.Content.Pipeline.BitmapFonts +{ + // ---- AngelCode BmFont XML serializer ---------------------- + // ---- By DeadlyDan @ deadlydan@gmail.com ------------------- + // ---- There's no license restrictions, use as you will. ---- + // ---- Credits to http://www.angelcode.com/ ----------------- + public class BitmapFontChar + { + [XmlAttribute("id")] + public int Id { get; set; } + + [XmlAttribute("x")] + public int X { get; set; } + + [XmlAttribute("y")] + public int Y { get; set; } + + [XmlAttribute("width")] + public int Width { get; set; } + + [XmlAttribute("height")] + public int Height { get; set; } + + [XmlAttribute("xoffset")] + public int XOffset { get; set; } + + [XmlAttribute("yoffset")] + public int YOffset { get; set; } + + [XmlAttribute("xadvance")] + public int XAdvance { get; set; } + + [XmlAttribute("page")] + public int Page { get; set; } + + [XmlAttribute("chnl")] + public int Channel { get; set; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontCommon.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontCommon.cs new file mode 100644 index 0000000..6247fcf --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontCommon.cs @@ -0,0 +1,41 @@ +using System.Xml.Serialization; + +namespace MonoGame.Extended.Content.Pipeline.BitmapFonts +{ + // ---- AngelCode BmFont XML serializer ---------------------- + // ---- By DeadlyDan @ deadlydan@gmail.com ------------------- + // ---- There's no license restrictions, use as you will. ---- + // ---- Credits to http://www.angelcode.com/ ----------------- + public class BitmapFontCommon + { + [XmlAttribute("lineHeight")] + public int LineHeight { get; set; } + + [XmlAttribute("base")] + public int Base { get; set; } + + [XmlAttribute("scaleW")] + public int ScaleW { get; set; } + + [XmlAttribute("scaleH")] + public int ScaleH { get; set; } + + [XmlAttribute("pages")] + public int Pages { get; set; } + + [XmlAttribute("packed")] + public int Packed { get; set; } + + [XmlAttribute("alphaChnl")] + public int AlphaChannel { get; set; } + + [XmlAttribute("redChnl")] + public int RedChannel { get; set; } + + [XmlAttribute("greenChnl")] + public int GreenChannel { get; set; } + + [XmlAttribute("blueChnl")] + public int BlueChannel { get; set; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontFile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontFile.cs new file mode 100644 index 0000000..db77270 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontFile.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace MonoGame.Extended.Content.Pipeline.BitmapFonts +{ + // ---- AngelCode BmFont XML serializer ---------------------- + // ---- By DeadlyDan @ deadlydan@gmail.com ------------------- + // ---- There's no license restrictions, use as you will. ---- + // ---- Credits to http://www.angelcode.com/ ----------------- + [XmlRoot("font")] + public class BitmapFontFile + { + [XmlElement("info")] + public BitmapFontInfo Info { get; set; } + + [XmlElement("common")] + public BitmapFontCommon Common { get; set; } + + [XmlArray("pages")] + [XmlArrayItem("page")] + public List<BitmapFontPage> Pages { get; set; } + + [XmlArray("chars")] + [XmlArrayItem("char")] + public List<BitmapFontChar> Chars { get; set; } + + [XmlArray("kernings")] + [XmlArrayItem("kerning")] + public List<BitmapFontKerning> Kernings { get; set; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontImporter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontImporter.cs new file mode 100644 index 0000000..ea4c528 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontImporter.cs @@ -0,0 +1,22 @@ +using System.IO; +using System.Xml.Serialization; +using Microsoft.Xna.Framework.Content.Pipeline; + +namespace MonoGame.Extended.Content.Pipeline.BitmapFonts +{ + [ContentImporter(".fnt", DefaultProcessor = "BitmapFontProcessor", + DisplayName = "BMFont Importer - MonoGame.Extended")] + public class BitmapFontImporter : ContentImporter<BitmapFontFile> + { + public override BitmapFontFile Import(string filename, ContentImporterContext context) + { + context.Logger.LogMessage("Importing XML file: {0}", filename); + + using (var streamReader = new StreamReader(filename)) + { + var deserializer = new XmlSerializer(typeof(BitmapFontFile)); + return (BitmapFontFile)deserializer.Deserialize(streamReader); + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontInfo.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontInfo.cs new file mode 100644 index 0000000..1f50cf8 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontInfo.cs @@ -0,0 +1,47 @@ +using System.Xml.Serialization; + +namespace MonoGame.Extended.Content.Pipeline.BitmapFonts +{ + // ---- AngelCode BmFont XML serializer ---------------------- + // ---- By DeadlyDan @ deadlydan@gmail.com ------------------- + // ---- There's no license restrictions, use as you will. ---- + // ---- Credits to http://www.angelcode.com/ ----------------- + public class BitmapFontInfo + { + [XmlAttribute("face")] + public string Face { get; set; } + + [XmlAttribute("size")] + public int Size { get; set; } + + [XmlAttribute("bold")] + public int Bold { get; set; } + + [XmlAttribute("italic")] + public int Italic { get; set; } + + [XmlAttribute("charset")] + public string CharSet { get; set; } + + [XmlAttribute("unicode")] + public int Unicode { get; set; } + + [XmlAttribute("stretchH")] + public int StretchHeight { get; set; } + + [XmlAttribute("smooth")] + public int Smooth { get; set; } + + [XmlAttribute("aa")] + public int SuperSampling { get; set; } + + [XmlAttribute("padding")] + public string Padding { get; set; } + + [XmlAttribute("spacing")] + public string Spacing { get; set; } + + [XmlAttribute("outline")] + public int OutLine { get; set; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontKerning.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontKerning.cs new file mode 100644 index 0000000..77caf13 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontKerning.cs @@ -0,0 +1,20 @@ +using System.Xml.Serialization; + +namespace MonoGame.Extended.Content.Pipeline.BitmapFonts +{ + // ---- AngelCode BmFont XML serializer ---------------------- + // ---- By DeadlyDan @ deadlydan@gmail.com ------------------- + // ---- There's no license restrictions, use as you will. ---- + // ---- Credits to http://www.angelcode.com/ ----------------- + public class BitmapFontKerning + { + [XmlAttribute("first")] + public int First { get; set; } + + [XmlAttribute("second")] + public int Second { get; set; } + + [XmlAttribute("amount")] + public int Amount { get; set; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontPage.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontPage.cs new file mode 100644 index 0000000..3841ff5 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontPage.cs @@ -0,0 +1,17 @@ +using System.Xml.Serialization; + +namespace MonoGame.Extended.Content.Pipeline.BitmapFonts +{ + // ---- AngelCode BmFont XML serializer ---------------------- + // ---- By DeadlyDan @ deadlydan@gmail.com ------------------- + // ---- There's no license restrictions, use as you will. ---- + // ---- Credits to http://www.angelcode.com/ ----------------- + public class BitmapFontPage + { + [XmlAttribute("id")] + public int Id { get; set; } + + [XmlAttribute("file")] + public string File { get; set; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessor.cs new file mode 100644 index 0000000..a859eb5 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessor.cs @@ -0,0 +1,33 @@ +using System; +using System.IO; +using Microsoft.Xna.Framework.Content.Pipeline; + +namespace MonoGame.Extended.Content.Pipeline.BitmapFonts +{ + [ContentProcessor(DisplayName = "BMFont Processor - MonoGame.Extended")] + public class BitmapFontProcessor : ContentProcessor<BitmapFontFile, BitmapFontProcessorResult> + { + public override BitmapFontProcessorResult Process(BitmapFontFile bitmapFontFile, ContentProcessorContext context) + { + try + { + context.Logger.LogMessage("Processing BMFont"); + var result = new BitmapFontProcessorResult(bitmapFontFile); + + foreach (var fontPage in bitmapFontFile.Pages) + { + var assetName = Path.GetFileNameWithoutExtension(fontPage.File); + context.Logger.LogMessage("Expected texture asset: {0}", assetName); + result.TextureAssets.Add(assetName); + } + + return result; + } + catch (Exception ex) + { + context.Logger.LogMessage("Error {0}", ex); + throw; + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessorResult.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessorResult.cs new file mode 100644 index 0000000..5841acc --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontProcessorResult.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace MonoGame.Extended.Content.Pipeline.BitmapFonts +{ + public class BitmapFontProcessorResult + { + public List<string> TextureAssets { get; private set; } + public BitmapFontFile FontFile { get; private set; } + + public BitmapFontProcessorResult(BitmapFontFile fontFile) + { + FontFile = fontFile; + TextureAssets = new List<string>(); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontWriter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontWriter.cs new file mode 100644 index 0000000..343c40c --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/BitmapFonts/BitmapFontWriter.cs @@ -0,0 +1,52 @@ +using Microsoft.Xna.Framework.Content.Pipeline; +using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler; + +namespace MonoGame.Extended.Content.Pipeline.BitmapFonts +{ + [ContentTypeWriter] + public class BitmapFontWriter : ContentTypeWriter<BitmapFontProcessorResult> + { + protected override void Write(ContentWriter writer, BitmapFontProcessorResult result) + { + writer.Write(result.TextureAssets.Count); + + foreach (var textureAsset in result.TextureAssets) + writer.Write(textureAsset); + + var fontFile = result.FontFile; + writer.Write(fontFile.Common.LineHeight); + writer.Write(fontFile.Chars.Count); + + foreach (var c in fontFile.Chars) + { + writer.Write(c.Id); + writer.Write(c.Page); + writer.Write(c.X); + writer.Write(c.Y); + writer.Write(c.Width); + writer.Write(c.Height); + writer.Write(c.XOffset); + writer.Write(c.YOffset); + writer.Write(c.XAdvance); + } + + writer.Write(fontFile.Kernings.Count); + foreach(var k in fontFile.Kernings) + { + writer.Write(k.First); + writer.Write(k.Second); + writer.Write(k.Amount); + } + } + + public override string GetRuntimeType(TargetPlatform targetPlatform) + { + return "MonoGame.Extended.BitmapFonts.BitmapFont, MonoGame.Extended"; + } + + public override string GetRuntimeReader(TargetPlatform targetPlatform) + { + return "MonoGame.Extended.BitmapFonts.BitmapFontReader, MonoGame.Extended"; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentImporterContextExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentImporterContextExtensions.cs new file mode 100644 index 0000000..7f63815 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentImporterContextExtensions.cs @@ -0,0 +1,15 @@ +using System.IO; +using Microsoft.Xna.Framework.Content.Pipeline; + +namespace MonoGame.Extended.Content.Pipeline; + +public static class ContentImporterContextExtensions +{ + public static string AddDependencyWithLogging(this ContentImporterContext context, string filePath, string source) + { + source = Path.Combine(Path.GetDirectoryName(filePath), source); + ContentLogger.Log($"Adding dependency '{source}'"); + context.AddDependency(source); + return source; + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentImporterResult.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentImporterResult.cs new file mode 100644 index 0000000..e302ca7 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentImporterResult.cs @@ -0,0 +1,14 @@ +namespace MonoGame.Extended.Content.Pipeline +{ + public class ContentImporterResult<T> + { + public ContentImporterResult(string filePath, T data) + { + FilePath = filePath; + Data = data; + } + + public string FilePath { get; } + public T Data { get; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentItem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentItem.cs new file mode 100644 index 0000000..e69d48e --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentItem.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework.Content.Pipeline; + +namespace MonoGame.Extended.Content.Pipeline +{ + public interface IExternalReferenceRepository + { + ExternalReference<TInput> GetExternalReference<TInput>(string source); + } + + public class ContentItem<T> : ContentItem, IExternalReferenceRepository + { + public ContentItem(T data) + { + Data = data; + } + + public T Data { get; } + + private readonly Dictionary<string, ContentItem> _externalReferences = new Dictionary<string, ContentItem>(); + + public void BuildExternalReference<TInput>(ContentProcessorContext context, string source, OpaqueDataDictionary parameters = null) + { + var sourceAsset = new ExternalReference<TInput>(source); + var externalReference = context.BuildAsset<TInput, TInput>(sourceAsset, "", parameters, "", ""); + _externalReferences.Add(source, externalReference); + } + + public ExternalReference<TInput> GetExternalReference<TInput>(string source) + { + if (source is not null && _externalReferences.TryGetValue(source, out var contentItem)) + return contentItem as ExternalReference<TInput>; + + return null; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentLogger.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentLogger.cs new file mode 100644 index 0000000..83848a1 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentLogger.cs @@ -0,0 +1,14 @@ +using Microsoft.Xna.Framework.Content.Pipeline; + +namespace MonoGame.Extended.Content.Pipeline +{ + public class ContentLogger + { + public static ContentBuildLogger Logger { get; set; } + + public static void Log(string message) + { + Logger?.LogMessage(message); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentWriterExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentWriterExtensions.cs new file mode 100644 index 0000000..b8cf9d2 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/ContentWriterExtensions.cs @@ -0,0 +1,79 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler; + +namespace MonoGame.Extended.Content.Pipeline +{ + public static class ContentWriterExtensions + { + public static void Write(this ContentWriter contentWriter, Color value) + { + contentWriter.Write(value.R); + contentWriter.Write(value.G); + contentWriter.Write(value.B); + contentWriter.Write(value.A); + } + + public static void Write(this ContentWriter contentWriter, Matrix value) + { + contentWriter.Write(value.M11); + contentWriter.Write(value.M12); + contentWriter.Write(value.M13); + contentWriter.Write(value.M14); + contentWriter.Write(value.M21); + contentWriter.Write(value.M22); + contentWriter.Write(value.M23); + contentWriter.Write(value.M24); + contentWriter.Write(value.M31); + contentWriter.Write(value.M32); + contentWriter.Write(value.M33); + contentWriter.Write(value.M34); + contentWriter.Write(value.M41); + contentWriter.Write(value.M42); + contentWriter.Write(value.M43); + contentWriter.Write(value.M44); + } + + public static void Write(this ContentWriter contentWriter, Quaternion value) + { + contentWriter.Write(value.X); + contentWriter.Write(value.Y); + contentWriter.Write(value.Z); + contentWriter.Write(value.W); + } + + public static void Write(this ContentWriter contentWriter, Vector2 value) + { + contentWriter.Write(value.X); + contentWriter.Write(value.Y); + } + + public static void Write(this ContentWriter contentWriter, Vector3 value) + { + contentWriter.Write(value.X); + contentWriter.Write(value.Y); + contentWriter.Write(value.Z); + } + + public static void Write(this ContentWriter contentWriter, Vector4 value) + { + contentWriter.Write(value.X); + contentWriter.Write(value.Y); + contentWriter.Write(value.Z); + contentWriter.Write(value.W); + } + + public static void Write(this ContentWriter contentWriter, BoundingSphere value) + { + contentWriter.Write(value.Center); + contentWriter.Write(value.Radius); + } + + public static void Write(this ContentWriter contentWriter, Rectangle value) + { + contentWriter.Write(value.X); + contentWriter.Write(value.Y); + contentWriter.Write(value.Width); + contentWriter.Write(value.Height); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentImporter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentImporter.cs new file mode 100644 index 0000000..437d7eb --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentImporter.cs @@ -0,0 +1,15 @@ +using System.IO; +using Microsoft.Xna.Framework.Content.Pipeline; + +namespace MonoGame.Extended.Content.Pipeline.Json +{ + [ContentImporter(".json", DefaultProcessor = nameof(JsonContentProcessor), DisplayName = "JSON Importer - MonoGame.Extended")] + public class JsonContentImporter : ContentImporter<ContentImporterResult<string>> + { + public override ContentImporterResult<string> Import(string filename, ContentImporterContext context) + { + var json = File.ReadAllText(filename); + return new ContentImporterResult<string>(filename, json); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentProcessor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentProcessor.cs new file mode 100644 index 0000000..6be4ac3 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentProcessor.cs @@ -0,0 +1,31 @@ +using System; +using System.ComponentModel; +using Microsoft.Xna.Framework.Content.Pipeline; + +namespace MonoGame.Extended.Content.Pipeline.Json +{ + [ContentProcessor(DisplayName = "JSON Processor - MonoGame.Extended")] + public class JsonContentProcessor : ContentProcessor<ContentImporterResult<string>, JsonContentProcessorResult> + { + [DefaultValue(typeof(Type), "System.Object")] + public string ContentType { get; set; } + + public override JsonContentProcessorResult Process(ContentImporterResult<string> input, ContentProcessorContext context) + { + try + { + var output = new JsonContentProcessorResult + { + ContentType = ContentType, + Json = input.Data + }; + return output; + } + catch (Exception ex) + { + context.Logger.LogMessage("Error {0}", ex); + throw; + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentProcessorResult.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentProcessorResult.cs new file mode 100644 index 0000000..eaef99e --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentProcessorResult.cs @@ -0,0 +1,8 @@ +namespace MonoGame.Extended.Content.Pipeline.Json +{ + public class JsonContentProcessorResult + { + public string ContentType { get; set; } + public string Json { get; set; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentTypeWriter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentTypeWriter.cs new file mode 100644 index 0000000..6efa696 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Json/JsonContentTypeWriter.cs @@ -0,0 +1,27 @@ +using Microsoft.Xna.Framework.Content.Pipeline; +using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler; + +namespace MonoGame.Extended.Content.Pipeline.Json +{ + [ContentTypeWriter] + public class JsonContentTypeWriter : ContentTypeWriter<JsonContentProcessorResult> + { + private string _runtimeType; + + protected override void Write(ContentWriter writer, JsonContentProcessorResult result) + { + _runtimeType = result.ContentType; + writer.Write(result.Json); + } + + public override string GetRuntimeReader(TargetPlatform targetPlatform) + { + return _runtimeType;// "MonoGame.Extended.Serialization.SpriteFactoryContentTypeReader, MonoGame.Extended"; + } + + public override string GetRuntimeType(TargetPlatform targetPlatform) + { + return _runtimeType;// "MonoGame.Extended.Serialization.SpriteFactoryContentTypeReader, MonoGame.Extended"; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/MonoGame.Extended.Content.Pipeline.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/MonoGame.Extended.Content.Pipeline.csproj new file mode 100644 index 0000000..70b81e8 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/MonoGame.Extended.Content.Pipeline.csproj @@ -0,0 +1,25 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <ItemGroup> + <Content Include="$(ArtifactsPath)/bin/MonoGame.Extended.Content.Pipeline/release/*.dll" Pack="True" PackagePath="tools" /> + </ItemGroup> + + <PropertyGroup> + <Description>Content Pipeline importers and processors to make MonoGame more awesome.</Description> + <PackageTags>monogame content importer processor reader tiled texturepacker bmfont animations</PackageTags> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Autofac" Version="5.2.0" /> + + <PackageReference Include="MonoGame.Framework.Content.Pipeline" + Version="3.8.1.303" + PrivateAssets="All" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\MonoGame.Extended.Tiled\MonoGame.Extended.Tiled.csproj" /> + <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" /> + </ItemGroup> + +</Project> diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/PathExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/PathExtensions.cs new file mode 100644 index 0000000..b85afb5 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/PathExtensions.cs @@ -0,0 +1,14 @@ +using System; +using System.IO; + +namespace MonoGame.Extended.Content.Pipeline +{ + public static class PathExtensions + { + public static string GetApplicationFullPath(params string[] pathParts) + { + var path = Path.Combine(pathParts); + return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, path); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/SpriteFactory/SpriteFactoryContentImporter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/SpriteFactory/SpriteFactoryContentImporter.cs new file mode 100644 index 0000000..3c4607d --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/SpriteFactory/SpriteFactoryContentImporter.cs @@ -0,0 +1,10 @@ +using Microsoft.Xna.Framework.Content.Pipeline; +using MonoGame.Extended.Content.Pipeline.Json; + +namespace MonoGame.Extended.Content.Pipeline.SpriteFactory +{ + [ContentImporter(".sf", DefaultProcessor = nameof(SpriteFactoryContentProcessor), DisplayName = "Sprite Factory Importer - MonoGame.Extended")] + public class SpriteFactoryContentImporter : JsonContentImporter + { + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/SpriteFactory/SpriteFactoryContentProcessor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/SpriteFactory/SpriteFactoryContentProcessor.cs new file mode 100644 index 0000000..4920f33 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/SpriteFactory/SpriteFactoryContentProcessor.cs @@ -0,0 +1,15 @@ +using Microsoft.Xna.Framework.Content.Pipeline; +using MonoGame.Extended.Content.Pipeline.Json; + +namespace MonoGame.Extended.Content.Pipeline.SpriteFactory +{ + [ContentProcessor(DisplayName = "Sprite Factory Processor - MonoGame.Extended")] + public class SpriteFactoryContentProcessor : JsonContentProcessor + { + public SpriteFactoryContentProcessor() + { + ContentType = "MonoGame.Extended MonoGame.Extended.Animations.SpriteFactory.SpriteFactoryFileReader, MonoGame.Extended.Animations"; + } + + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerJsonImporter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerJsonImporter.cs new file mode 100644 index 0000000..a80e654 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerJsonImporter.cs @@ -0,0 +1,18 @@ +using System.IO; +using System.Text.Json; +using Microsoft.Xna.Framework.Content.Pipeline; +using MonoGame.Extended.TextureAtlases; + + +namespace MonoGame.Extended.Content.Pipeline.TextureAtlases +{ + [ContentImporter(".json", DefaultProcessor = "TexturePackerProcessor", DisplayName = "TexturePacker JSON Importer - MonoGame.Extended")] + public class TexturePackerJsonImporter : ContentImporter<TexturePackerFile> + { + public override TexturePackerFile Import(string filename, ContentImporterContext context) + { + var json = File.ReadAllText(filename); + return JsonSerializer.Deserialize<TexturePackerFile>(json); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerProcessor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerProcessor.cs new file mode 100644 index 0000000..1f14ee7 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerProcessor.cs @@ -0,0 +1,24 @@ +using System; +using Microsoft.Xna.Framework.Content.Pipeline; +using MonoGame.Extended.TextureAtlases; + +namespace MonoGame.Extended.Content.Pipeline.TextureAtlases +{ + [ContentProcessor(DisplayName = "TexturePacker Processor - MonoGame.Extended")] + public class TexturePackerProcessor : ContentProcessor<TexturePackerFile, TexturePackerProcessorResult> + { + public override TexturePackerProcessorResult Process(TexturePackerFile input, ContentProcessorContext context) + { + try + { + var output = new TexturePackerProcessorResult {Data = input}; + return output; + } + catch (Exception ex) + { + context.Logger.LogMessage("Error {0}", ex); + throw; + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerProcessorResult.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerProcessorResult.cs new file mode 100644 index 0000000..a996259 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerProcessorResult.cs @@ -0,0 +1,9 @@ +using MonoGame.Extended.TextureAtlases; + +namespace MonoGame.Extended.Content.Pipeline.TextureAtlases +{ + public class TexturePackerProcessorResult + { + public TexturePackerFile Data { get; set; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerWriter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerWriter.cs new file mode 100644 index 0000000..3fd15ff --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/TextureAtlases/TexturePackerWriter.cs @@ -0,0 +1,45 @@ +using System.Diagnostics; +using System.IO; +using Microsoft.Xna.Framework.Content.Pipeline; +using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler; + +namespace MonoGame.Extended.Content.Pipeline.TextureAtlases +{ + [ContentTypeWriter] + public class TexturePackerWriter : ContentTypeWriter<TexturePackerProcessorResult> + { + protected override void Write(ContentWriter writer, TexturePackerProcessorResult result) + { + var data = result.Data; + var metadata = data.Metadata; + + var assetName = Path.GetFileNameWithoutExtension(metadata.Image); + Debug.Assert(assetName != null, "assetName != null"); + + writer.Write(assetName); + writer.Write(data.Regions.Count); + + foreach (var region in data.Regions) + { + var regionName = Path.ChangeExtension(region.Filename, null); + Debug.Assert(regionName != null, "regionName != null"); + + writer.Write(regionName); + writer.Write(region.Frame.X); + writer.Write(region.Frame.Y); + writer.Write(region.Frame.Width); + writer.Write(region.Frame.Height); + } + } + + public override string GetRuntimeType(TargetPlatform targetPlatform) + { + return "MonoGame.Extended.TextureAtlases.TextureAtlas, MonoGame.Extended"; + } + + public override string GetRuntimeReader(TargetPlatform targetPlatform) + { + return "MonoGame.Extended.TextureAtlases.TextureAtlasReader, MonoGame.Extended"; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/ContentWriterExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/ContentWriterExtensions.cs new file mode 100644 index 0000000..28ded17 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/ContentWriterExtensions.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler; +using MonoGame.Extended.Tiled.Serialization; + +namespace MonoGame.Extended.Content.Pipeline.Tiled +{ + public static class ContentWriterExtensions + { + // ReSharper disable once SuggestBaseTypeForParameter + public static void WriteTiledMapProperties(this ContentWriter writer, IReadOnlyCollection<TiledMapPropertyContent> value) + { + if (value == null) + { + writer.Write(0); + return; + } + writer.Write(value.Count); + foreach (var property in value) + { + writer.Write(property.Name); + writer.Write(property.Value ?? string.Empty); + WriteTiledMapProperties(writer, property.Properties); + } + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledContentItem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledContentItem.cs new file mode 100644 index 0000000..b376252 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledContentItem.cs @@ -0,0 +1,22 @@ +using Microsoft.Xna.Framework.Content.Pipeline; +using Microsoft.Xna.Framework.Content.Pipeline.Graphics; +using MonoGame.Extended.Tiled.Serialization; + +namespace MonoGame.Extended.Content.Pipeline.Tiled; + +public class TiledContentItem<T>: ContentItem<T> +{ + public TiledContentItem(T data) : base(data) + { + } + + public void BuildExternalReference<T>(ContentProcessorContext context, TiledMapImageContent image) + { + var parameters = new OpaqueDataDictionary + { + { "ColorKeyColor", image.TransparentColor }, + { "ColorKeyEnabled", true } + }; + BuildExternalReference<Texture2DContent>(context, image.Source, parameters); + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapContentItem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapContentItem.cs new file mode 100644 index 0000000..b0b7b5d --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapContentItem.cs @@ -0,0 +1,12 @@ +using MonoGame.Extended.Tiled.Serialization; + +namespace MonoGame.Extended.Content.Pipeline.Tiled +{ + public class TiledMapContentItem : TiledContentItem<TiledMapContent> + { + public TiledMapContentItem(TiledMapContent data) + : base(data) + { + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapImporter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapImporter.cs new file mode 100644 index 0000000..70e2bee --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapImporter.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Xml.Serialization; +using Microsoft.Xna.Framework.Content.Pipeline; +using MonoGame.Extended.Tiled.Serialization; + +namespace MonoGame.Extended.Content.Pipeline.Tiled +{ + [ContentImporter(".tmx", DefaultProcessor = "TiledMapProcessor", DisplayName = "Tiled Map Importer - MonoGame.Extended")] + public class TiledMapImporter : ContentImporter<TiledMapContentItem> + { + public override TiledMapContentItem Import(string filePath, ContentImporterContext context) + { + try + { + if (filePath == null) + throw new ArgumentNullException(nameof(filePath)); + + ContentLogger.Logger = context.Logger; + ContentLogger.Log($"Importing '{filePath}'"); + + var map = DeserializeTiledMapContent(filePath, context); + + if (map.Width > ushort.MaxValue || map.Height > ushort.MaxValue) + throw new InvalidContentException($"The map '{filePath} is much too large. The maximum supported width and height for a Tiled map is {ushort.MaxValue}."); + + ContentLogger.Log($"Imported '{filePath}'"); + + return new TiledMapContentItem(map); + + } + catch (Exception e) + { + context.Logger.LogImportantMessage(e.StackTrace); + throw; + } + } + + private static TiledMapContent DeserializeTiledMapContent(string mapFilePath, ContentImporterContext context) + { + using (var reader = new StreamReader(mapFilePath)) + { + var mapSerializer = new XmlSerializer(typeof(TiledMapContent)); + var map = (TiledMapContent)mapSerializer.Deserialize(reader); + + map.FilePath = mapFilePath; + + for (var i = 0; i < map.Tilesets.Count; i++) + { + var tileset = map.Tilesets[i]; + + string getTilesetSource(string source) + => Path.GetFullPath(Path.Combine(Path.GetDirectoryName(mapFilePath), source)); + + if (!string.IsNullOrWhiteSpace(tileset.Source)) + { + tileset.Source = getTilesetSource(tileset.Source); + ContentLogger.Log($"Adding dependency for {tileset.Source}"); + // We depend on the tileset. If the tileset changes, the map also needs to rebuild. + context.AddDependency(tileset.Source); + } + else + { + tileset.Image.Source = getTilesetSource(tileset.Image.Source); + ContentLogger.Log($"Adding dependency for {tileset.Image.Source}"); + context.AddDependency(tileset.Image.Source); + } + } + + ImportLayers(context, map.Layers, Path.GetDirectoryName(mapFilePath)); + + map.Name = mapFilePath; + return map; + } + } + + private static void ImportLayers(ContentImporterContext context, List<TiledMapLayerContent> layers, string path) + { + for (var i = 0; i < layers.Count; i++) + { + if (layers[i] is TiledMapImageLayerContent imageLayer) + { + imageLayer.Image.Source = Path.Combine(path, imageLayer.Image.Source); + ContentLogger.Log($"Adding dependency for '{imageLayer.Image.Source}'"); + + // Tell the pipeline that we depend on this image and need to rebuild the map if the image changes. + // (Maybe the image is a different size) + context.AddDependency(imageLayer.Image.Source); + } + if (layers[i] is TiledMapObjectLayerContent objectLayer) + foreach (var obj in objectLayer.Objects) + if (!String.IsNullOrWhiteSpace(obj.TemplateSource)) + { + obj.TemplateSource = Path.Combine(path, obj.TemplateSource); + ContentLogger.Log($"Adding dependency for '{obj.TemplateSource}'"); + // Tell the pipeline that we depend on this template and need to rebuild the map if the template changes. + // (Templates are loaded into objects on process, so all objects which depend on the template file + // need the change to the template) + context.AddDependency(obj.TemplateSource); + } + if (layers[i] is TiledMapGroupLayerContent groupLayer) + // Yay recursion! + ImportLayers(context, groupLayer.Layers, path); + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapObjectTemplateImporter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapObjectTemplateImporter.cs new file mode 100644 index 0000000..eeddcaa --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapObjectTemplateImporter.cs @@ -0,0 +1,54 @@ +using Microsoft.Xna.Framework.Content.Pipeline; +using System; +using System.IO; +using System.Xml.Serialization; +using MonoGame.Extended.Tiled.Serialization; + +namespace MonoGame.Extended.Content.Pipeline.Tiled +{ + [ContentImporter(".tx", DefaultProcessor = "TiledMapObjectTemplateProcessor", DisplayName = "Tiled Map Object Template Importer - MonoGame.Extended")] + public class TiledMapObjectTemplateImporter : ContentImporter<TiledMapObjectTemplateContent> + { + public override TiledMapObjectTemplateContent Import(string filePath, ContentImporterContext context) + { + try + { + if (filePath == null) + throw new ArgumentNullException(nameof(filePath)); + + ContentLogger.Logger = context.Logger; + ContentLogger.Log($"Importing '{filePath}'"); + + var template = DeserializeTileMapObjectTemplateContent(filePath, context); + + ContentLogger.Log($"Imported '{filePath}'"); + + return template; + } + catch (Exception e) + { + context.Logger.LogImportantMessage(e.StackTrace); + return null; + } + } + + private static TiledMapObjectTemplateContent DeserializeTileMapObjectTemplateContent(string filePath, ContentImporterContext context) + { + using (var reader = new StreamReader(filePath)) + { + var templateSerializer = new XmlSerializer(typeof(TiledMapObjectTemplateContent)); + var template = (TiledMapObjectTemplateContent)templateSerializer.Deserialize(reader); + + if (!string.IsNullOrWhiteSpace(template.Tileset?.Source)) + { + template.Tileset.Source = Path.Combine(Path.GetDirectoryName(filePath), template.Tileset.Source); + ContentLogger.Log($"Adding dependency '{template.Tileset.Source}'"); + // We depend on this tileset. + context.AddDependency(template.Tileset.Source); + } + + return template; + } + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapProcessor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapProcessor.cs new file mode 100644 index 0000000..9668a72 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapProcessor.cs @@ -0,0 +1,324 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Xna.Framework.Content.Pipeline; +using Microsoft.Xna.Framework.Content.Pipeline.Graphics; +using MonoGame.Extended.Tiled; +using MonoGame.Extended.Tiled.Serialization; +using MonoGame.Framework.Utilities.Deflate; +using CompressionMode = System.IO.Compression.CompressionMode; +using GZipStream = System.IO.Compression.GZipStream; + +namespace MonoGame.Extended.Content.Pipeline.Tiled +{ + public static class TiledMapContentHelper + { + public static void Process(TiledMapObjectContent obj, ContentProcessorContext context) + { + if (!string.IsNullOrWhiteSpace(obj.TemplateSource)) + { + var externalReference = new ExternalReference<TiledMapObjectLayerContent>(obj.TemplateSource); + var template = context.BuildAndLoadAsset<TiledMapObjectLayerContent, TiledMapObjectTemplateContent>(externalReference, ""); + + // Nothing says a template can't reference another template. + // Yay recusion! + Process(template.Object, context); + + if (!obj._globalIdentifier.HasValue && template.Object._globalIdentifier.HasValue) + obj.GlobalIdentifier = template.Object.GlobalIdentifier; + + if (!obj._height.HasValue && template.Object._height.HasValue) + obj.Height = template.Object.Height; + + if (!obj._identifier.HasValue && template.Object._identifier.HasValue) + obj.Identifier = template.Object.Identifier; + + if (!obj._rotation.HasValue && template.Object._rotation.HasValue) + obj.Rotation = template.Object.Rotation; + + if (!obj._visible.HasValue && template.Object._visible.HasValue) + obj.Visible = template.Object.Visible; + + if (!obj._width.HasValue && template.Object._width.HasValue) + obj.Width = template.Object.Width; + + if (!obj._x.HasValue && template.Object._x.HasValue) + obj.X = template.Object.X; + + if (!obj._y.HasValue && template.Object._y.HasValue) + obj.Y = template.Object.Y; + + if (obj.Ellipse == null && template.Object.Ellipse != null) + obj.Ellipse = template.Object.Ellipse; + + if (string.IsNullOrWhiteSpace(obj.Name) && !string.IsNullOrWhiteSpace(template.Object.Name)) + obj.Name = template.Object.Name; + + if (obj.Polygon == null && template.Object.Polygon != null) + obj.Polygon = template.Object.Polygon; + + if (obj.Polyline == null && template.Object.Polyline != null) + obj.Polyline = template.Object.Polyline; + + foreach (var tProperty in template.Object.Properties) + { + if (!obj.Properties.Exists(p => p.Name == tProperty.Name)) + obj.Properties.Add(tProperty); + } + + if (string.IsNullOrWhiteSpace(obj.Type) && !string.IsNullOrWhiteSpace(template.Object.Type)) + obj.Type = template.Object.Type; + + if (string.IsNullOrWhiteSpace(obj.Class) && !string.IsNullOrWhiteSpace(template.Object.Class)) + obj.Class = template.Object.Class; + } + } + } + + + [ContentProcessor(DisplayName = "Tiled Map Processor - MonoGame.Extended")] + public class TiledMapProcessor : ContentProcessor<TiledMapContentItem, TiledMapContentItem> + { + public override TiledMapContentItem Process(TiledMapContentItem contentItem, ContentProcessorContext context) + { + try + { + ContentLogger.Logger = context.Logger; + var map = contentItem.Data; + + if (map.Orientation == TiledMapOrientationContent.Hexagonal || map.Orientation == TiledMapOrientationContent.Staggered) + throw new NotSupportedException($"{map.Orientation} Tiled Maps are currently not implemented!"); + + foreach (var tileset in map.Tilesets) + { + if (string.IsNullOrWhiteSpace(tileset.Source)) + { + // Load the Texture2DContent for the tileset as it will be saved into the map content file. + contentItem.BuildExternalReference<Texture2DContent>(context, tileset.Image); + } + else + { + // Link to the tileset for the content loader to load at runtime. + //var externalReference = new ExternalReference<TiledMapTilesetContent>(tileset.Source); + //tileset.Content = context.BuildAsset<TiledMapTilesetContent, TiledMapTilesetContent>(externalReference, ""); + contentItem.BuildExternalReference<TiledMapTilesetContent>(context, tileset.Source); + } + } + + ProcessLayers(contentItem, map, context, map.Layers); + + return contentItem; + } + catch (Exception ex) + { + context.Logger.LogImportantMessage(ex.Message); + throw; + } + } + + private static void ProcessLayers(TiledMapContentItem contentItem, TiledMapContent map, ContentProcessorContext context, List<TiledMapLayerContent> layers) + { + foreach (var layer in layers) + { + switch (layer) + { + case TiledMapImageLayerContent imageLayer: + ContentLogger.Log($"Processing image layer '{imageLayer.Name}'"); + contentItem.BuildExternalReference<Texture2DContent>(context, imageLayer.Image); + ContentLogger.Log($"Processed image layer '{imageLayer.Name}'"); + break; + + case TiledMapTileLayerContent tileLayer when tileLayer.Data.Chunks.Count > 0: + throw new NotSupportedException($"{map.FilePath} contains data chunks. These are currently not supported."); + + case TiledMapTileLayerContent tileLayer: + var data = tileLayer.Data; + var encodingType = data.Encoding ?? "xml"; + var compressionType = data.Compression ?? "xml"; + + ContentLogger.Log($"Processing tile layer '{tileLayer.Name}': Encoding: '{encodingType}', Compression: '{compressionType}'"); + var tileData = DecodeTileLayerData(encodingType, tileLayer); + var tiles = CreateTiles(map.RenderOrder, map.Width, map.Height, tileData); + tileLayer.Tiles = tiles; + ContentLogger.Log($"Processed tile layer '{tileLayer}': {tiles.Length} tiles"); + break; + + case TiledMapObjectLayerContent objectLayer: + ContentLogger.Log($"Processing object layer '{objectLayer.Name}'"); + + foreach (var obj in objectLayer.Objects) + TiledMapContentHelper.Process(obj, context); + + ContentLogger.Log($"Processed object layer '{objectLayer.Name}'"); + break; + + case TiledMapGroupLayerContent groupLayer: + ProcessLayers(contentItem, map, context, groupLayer.Layers); + break; + } + } + } + + private static List<TiledMapTileContent> DecodeTileLayerData(string encodingType, TiledMapTileLayerContent tileLayer) + { + List<TiledMapTileContent> tiles; + + switch (encodingType) + { + case "xml": + tiles = tileLayer.Data.Tiles; + break; + case "csv": + tiles = DecodeCommaSeperatedValuesData(tileLayer.Data); + break; + case "base64": + tiles = DecodeBase64Data(tileLayer.Data, tileLayer.Width, tileLayer.Height); + break; + default: + throw new NotSupportedException($"The tile layer encoding '{encodingType}' is not supported."); + } + + return tiles; + } + + private static TiledMapTile[] CreateTiles(TiledMapTileDrawOrderContent renderOrder, int mapWidth, int mapHeight, List<TiledMapTileContent> tileData) + { + TiledMapTile[] tiles; + + switch (renderOrder) + { + case TiledMapTileDrawOrderContent.LeftDown: + tiles = CreateTilesInLeftDownOrder(tileData, mapWidth, mapHeight).ToArray(); + break; + case TiledMapTileDrawOrderContent.LeftUp: + tiles = CreateTilesInLeftUpOrder(tileData, mapWidth, mapHeight).ToArray(); + break; + case TiledMapTileDrawOrderContent.RightDown: + tiles = CreateTilesInRightDownOrder(tileData, mapWidth, mapHeight).ToArray(); + break; + case TiledMapTileDrawOrderContent.RightUp: + tiles = CreateTilesInRightUpOrder(tileData, mapWidth, mapHeight).ToArray(); + break; + default: + throw new NotSupportedException($"{renderOrder} is not supported."); + } + + return tiles.ToArray(); + } + + private static IEnumerable<TiledMapTile> CreateTilesInLeftDownOrder(List<TiledMapTileContent> tileLayerData, int mapWidth, int mapHeight) + { + for (var y = 0; y < mapHeight; y++) + { + for (var x = mapWidth - 1; x >= 0; x--) + { + var dataIndex = x + y * mapWidth; + var globalIdentifier = tileLayerData[dataIndex].GlobalIdentifier; + if (globalIdentifier == 0) + continue; + var tile = new TiledMapTile(globalIdentifier, (ushort)x, (ushort)y); + yield return tile; + } + } + } + + private static IEnumerable<TiledMapTile> CreateTilesInLeftUpOrder(List<TiledMapTileContent> tileLayerData, int mapWidth, int mapHeight) + { + for (var y = mapHeight - 1; y >= 0; y--) + { + for (var x = mapWidth - 1; x >= 0; x--) + { + var dataIndex = x + y * mapWidth; + var globalIdentifier = tileLayerData[dataIndex].GlobalIdentifier; + if (globalIdentifier == 0) + continue; + var tile = new TiledMapTile(globalIdentifier, (ushort)x, (ushort)y); + yield return tile; + } + } + } + + private static IEnumerable<TiledMapTile> CreateTilesInRightDownOrder(List<TiledMapTileContent> tileLayerData, int mapWidth, int mapHeight) + { + for (var y = 0; y < mapHeight; y++) + { + for (var x = 0; x < mapWidth; x++) + { + var dataIndex = x + y * mapWidth; + var globalIdentifier = tileLayerData[dataIndex].GlobalIdentifier; + if (globalIdentifier == 0) + continue; + var tile = new TiledMapTile(globalIdentifier, (ushort)x, (ushort)y); + yield return tile; + } + } + } + + private static IEnumerable<TiledMapTile> CreateTilesInRightUpOrder(List<TiledMapTileContent> tileLayerData, int mapWidth, int mapHeight) + { + for (var y = mapHeight - 1; y >= 0; y--) + { + for (var x = mapWidth - 1; x >= 0; x--) + { + var dataIndex = x + y * mapWidth; + var globalIdentifier = tileLayerData[dataIndex].GlobalIdentifier; + if (globalIdentifier == 0) + continue; + var tile = new TiledMapTile(globalIdentifier, (ushort)x, (ushort)y); + yield return tile; + } + } + } + + private static List<TiledMapTileContent> DecodeBase64Data(TiledMapTileLayerDataContent data, int width, int height) + { + var tileList = new List<TiledMapTileContent>(); + var encodedData = data.Value.Trim(); + var decodedData = Convert.FromBase64String(encodedData); + + using (var stream = OpenStream(decodedData, data.Compression)) + { + using (var reader = new BinaryReader(stream)) + { + data.Tiles = new List<TiledMapTileContent>(); + + for (var y = 0; y < width; y++) + { + for (var x = 0; x < height; x++) + { + var gid = reader.ReadUInt32(); + tileList.Add(new TiledMapTileContent + { + GlobalIdentifier = gid + }); + } + } + } + } + + return tileList; + } + + private static List<TiledMapTileContent> DecodeCommaSeperatedValuesData(TiledMapTileLayerDataContent data) + { + return data.Value + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(uint.Parse) + .Select(x => new TiledMapTileContent { GlobalIdentifier = x }) + .ToList(); + } + + private static Stream OpenStream(byte[] decodedData, string compressionMode) + { + var memoryStream = new MemoryStream(decodedData, false); + + return compressionMode switch + { + "gzip" => new GZipStream(memoryStream, CompressionMode.Decompress), + "zlib" => new ZlibStream(memoryStream, Framework.Utilities.Deflate.CompressionMode.Decompress), + _ => memoryStream + }; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetContentItem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetContentItem.cs new file mode 100644 index 0000000..d4b2221 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetContentItem.cs @@ -0,0 +1,14 @@ +using Microsoft.Xna.Framework.Content.Pipeline; +using Microsoft.Xna.Framework.Content.Pipeline.Graphics; +using MonoGame.Extended.Tiled.Serialization; + +namespace MonoGame.Extended.Content.Pipeline.Tiled +{ + public class TiledMapTilesetContentItem : TiledContentItem<TiledMapTilesetContent> + { + public TiledMapTilesetContentItem(TiledMapTilesetContent data) + : base(data) + { + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetImporter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetImporter.cs new file mode 100644 index 0000000..c848bde --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetImporter.cs @@ -0,0 +1,60 @@ +using Microsoft.Xna.Framework.Content.Pipeline; +using System; +using System.IO; +using System.Xml.Serialization; +using MonoGame.Extended.Tiled.Serialization; + +namespace MonoGame.Extended.Content.Pipeline.Tiled +{ + [ContentImporter(".tsx", DefaultProcessor = "TiledMapTilesetProcessor", DisplayName = "Tiled Map Tileset Importer - MonoGame.Extended")] + public class TiledMapTilesetImporter : ContentImporter<TiledMapTilesetContentItem> + { + public override TiledMapTilesetContentItem Import(string filePath, ContentImporterContext context) + { + try + { + if (filePath == null) + throw new ArgumentNullException(nameof(filePath)); + + ContentLogger.Logger = context.Logger; + ContentLogger.Log($"Importing '{filePath}'"); + + var tileset = DeserializeTiledMapTilesetContent(filePath, context); + + ContentLogger.Log($"Imported '{filePath}'"); + + return new TiledMapTilesetContentItem(tileset); + } + catch (Exception e) + { + context.Logger.LogImportantMessage(e.StackTrace); + throw; + } + } + + private TiledMapTilesetContent DeserializeTiledMapTilesetContent(string filePath, ContentImporterContext context) + { + using (var reader = new StreamReader(filePath)) + { + var tilesetSerializer = new XmlSerializer(typeof(TiledMapTilesetContent)); + var tileset = (TiledMapTilesetContent)tilesetSerializer.Deserialize(reader); + + if (tileset.Image is not null) + tileset.Image.Source = context.AddDependencyWithLogging(filePath, tileset.Image.Source); + + foreach (var tile in tileset.Tiles) + { + foreach (var obj in tile.Objects) + { + if (!string.IsNullOrWhiteSpace(obj.TemplateSource)) + obj.TemplateSource = context.AddDependencyWithLogging(filePath, obj.TemplateSource); + } + if (tile.Image is not null) + tile.Image.Source = context.AddDependencyWithLogging(filePath, tile.Image.Source); + } + + return tileset; + } + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetProcessor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetProcessor.cs new file mode 100644 index 0000000..5682602 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetProcessor.cs @@ -0,0 +1,44 @@ +using Microsoft.Xna.Framework.Content.Pipeline; +using Microsoft.Xna.Framework.Content.Pipeline.Graphics; +using System; + +namespace MonoGame.Extended.Content.Pipeline.Tiled +{ + [ContentProcessor(DisplayName = "Tiled Map Tileset Processor - MonoGame.Extended")] + public class TiledMapTilesetProcessor : ContentProcessor<TiledMapTilesetContentItem, TiledMapTilesetContentItem> + { + public override TiledMapTilesetContentItem Process(TiledMapTilesetContentItem contentItem, ContentProcessorContext context) + { + try + { + var tileset = contentItem.Data; + + ContentLogger.Logger = context.Logger; + ContentLogger.Log($"Processing tileset '{tileset.Name}'"); + + // Build the Texture2D asset and load it as it will be saved as part of this tileset file. + if (tileset.Image is not null) + contentItem.BuildExternalReference<Texture2DContent>(context, tileset.Image); + + foreach (var tile in tileset.Tiles) + { + foreach (var obj in tile.Objects) + { + TiledMapContentHelper.Process(obj, context); + } + if (tile.Image is not null) + contentItem.BuildExternalReference<Texture2DContent>(context, tile.Image); + } + + ContentLogger.Log($"Processed tileset '{tileset.Name}'"); + + return contentItem; + } + catch (Exception ex) + { + context.Logger.LogImportantMessage(ex.Message); + throw ex; + } + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetWriter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetWriter.cs new file mode 100644 index 0000000..48690ad --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapTilesetWriter.cs @@ -0,0 +1,145 @@ +using Microsoft.Xna.Framework.Content.Pipeline; +using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler; +using MonoGame.Extended.Tiled; +using System; +using System.Globalization; +using Microsoft.Xna.Framework.Content.Pipeline.Graphics; +using MonoGame.Extended.Tiled.Serialization; + +namespace MonoGame.Extended.Content.Pipeline.Tiled +{ + [ContentTypeWriter] + public class TiledMapTilesetWriter : ContentTypeWriter<TiledMapTilesetContentItem> + { + public override string GetRuntimeReader(TargetPlatform targetPlatform) => "MonoGame.Extended.Tiled.TiledMapTilesetReader, MonoGame.Extended.Tiled"; + + public override string GetRuntimeType(TargetPlatform targetPlatform) => "MonoGame.Extended.Tiled.TiledMapTileset, MonoGame.Extended.Tiled"; + + protected override void Write(ContentWriter writer, TiledMapTilesetContentItem contentItem) + { + try + { + WriteTileset(writer, contentItem.Data, contentItem); + } + catch (Exception ex) + { + ContentLogger.Logger.LogImportantMessage(ex.StackTrace); + throw; + } + } + + public static void WriteTileset(ContentWriter writer, TiledMapTilesetContent tileset, IExternalReferenceRepository externalReferenceRepository) + { + var externalReference = externalReferenceRepository.GetExternalReference<Texture2DContent>(tileset.Image?.Source); + writer.WriteExternalReference(externalReference); + writer.Write(tileset.Class ?? tileset.Type ?? string.Empty); + writer.Write(tileset.TileWidth); + writer.Write(tileset.TileHeight); + writer.Write(tileset.TileCount); + writer.Write(tileset.Spacing); + writer.Write(tileset.Margin); + writer.Write(tileset.Columns); + writer.Write(tileset.Tiles.Count); + + foreach (var tilesetTile in tileset.Tiles) + WriteTilesetTile(writer, tilesetTile, externalReferenceRepository); + + writer.WriteTiledMapProperties(tileset.Properties); + } + + private static void WriteTilesetTile(ContentWriter writer, TiledMapTilesetTileContent tilesetTile, + IExternalReferenceRepository externalReferenceRepository) + { + var externalReference = externalReferenceRepository.GetExternalReference<Texture2DContent>(tilesetTile.Image?.Source); + writer.WriteExternalReference(externalReference); + + writer.Write(tilesetTile.LocalIdentifier); + writer.Write(tilesetTile.Type); + writer.Write(tilesetTile.Frames.Count); + writer.Write(tilesetTile.Objects.Count); + + foreach (var @object in tilesetTile.Objects) + WriteObject(writer, @object); + + foreach (var frame in tilesetTile.Frames) + { + writer.Write(frame.TileIdentifier); + writer.Write(frame.Duration); + } + + writer.WriteTiledMapProperties(tilesetTile.Properties); + } + + private static void WriteObject(ContentWriter writer, TiledMapObjectContent @object) + { + var type = GetObjectType(@object); + + writer.Write((byte)type); + + writer.Write(@object.Identifier); + writer.Write(@object.Name ?? string.Empty); + writer.Write(@object.Class ?? @object.Type ?? string.Empty); + writer.Write(@object.X); + writer.Write(@object.Y); + writer.Write(@object.Width); + writer.Write(@object.Height); + writer.Write(@object.Rotation); + writer.Write(@object.Visible); + + writer.WriteTiledMapProperties(@object.Properties); + + switch (type) + { + case TiledMapObjectType.Rectangle: + case TiledMapObjectType.Ellipse: + break; + case TiledMapObjectType.Tile: + writer.Write(@object.GlobalIdentifier); + break; + case TiledMapObjectType.Polygon: + WritePolyPoints(writer, @object.Polygon.Points); + break; + case TiledMapObjectType.Polyline: + WritePolyPoints(writer, @object.Polyline.Points); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + // ReSharper disable once SuggestBaseTypeForParameter + private static void WritePolyPoints(ContentWriter writer, string @string) + { + var stringPoints = @string.Split(' '); + + writer.Write(stringPoints.Length); + + foreach (var stringPoint in stringPoints) + { + var xy = stringPoint.Split(','); + var x = float.Parse(xy[0], CultureInfo.InvariantCulture.NumberFormat); + writer.Write(x); + var y = float.Parse(xy[1], CultureInfo.InvariantCulture.NumberFormat); + writer.Write(y); + } + } + + public static TiledMapObjectType GetObjectType(TiledMapObjectContent content) + { + if (content.GlobalIdentifier > 0) + return TiledMapObjectType.Tile; + + if (content.Ellipse != null) + return TiledMapObjectType.Ellipse; + + if (content.Polygon != null) + return TiledMapObjectType.Polygon; + + // ReSharper disable once ConvertIfStatementToReturnStatement + if (content.Polyline != null) + return TiledMapObjectType.Polyline; + + return TiledMapObjectType.Rectangle; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapWriter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapWriter.cs new file mode 100644 index 0000000..126debb --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/Tiled/TiledMapWriter.cs @@ -0,0 +1,227 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content.Pipeline; +using Microsoft.Xna.Framework.Content.Pipeline.Graphics; +using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler; +using MonoGame.Extended.Tiled; +using MonoGame.Extended.Tiled.Serialization; + +namespace MonoGame.Extended.Content.Pipeline.Tiled +{ + [ContentTypeWriter] + public class TiledMapWriter : ContentTypeWriter<TiledMapContentItem> + { + private TiledMapContentItem _contentItem; + + protected override void Write(ContentWriter writer, TiledMapContentItem contentItem) + { + _contentItem = contentItem; + + var map = contentItem.Data; + + try + { + WriteMetaData(writer, map); + WriteTilesets(writer, map.Tilesets); + WriteLayers(writer, map.Layers); + } + catch (Exception ex) + { + ContentLogger.Logger.LogImportantMessage(ex.StackTrace); + throw; + } + } + + private static void WriteMetaData(ContentWriter writer, TiledMapContent map) + { + writer.Write(map.Class ?? map.Type ?? string.Empty); + writer.Write(map.Width); + writer.Write(map.Height); + writer.Write(map.TileWidth); + writer.Write(map.TileHeight); + writer.Write(ColorHelper.FromHex(map.BackgroundColor)); + writer.Write((byte)map.RenderOrder); + writer.Write((byte)map.Orientation); + writer.WriteTiledMapProperties(map.Properties); + } + + private void WriteTilesets(ContentWriter writer, IReadOnlyCollection<TiledMapTilesetContent> tilesets) + { + writer.Write(tilesets.Count); + + foreach (var tileset in tilesets) + WriteTileset(writer, tileset); + } + + private void WriteTileset(ContentWriter writer, TiledMapTilesetContent tileset) + { + writer.Write(tileset.FirstGlobalIdentifier); + + if (!string.IsNullOrWhiteSpace(tileset.Source)) + { + writer.Write(true); + writer.WriteExternalReference(_contentItem.GetExternalReference<TiledMapTilesetContent>(tileset.Source)); + } + else + { + writer.Write(false); + TiledMapTilesetWriter.WriteTileset(writer, tileset, _contentItem); + } + } + + private void WriteLayers(ContentWriter writer, IReadOnlyCollection<TiledMapLayerContent> layers) + { + writer.Write(layers.Count); + + foreach (var layer in layers) + WriteLayer(writer, layer); + } + + private void WriteLayer(ContentWriter writer, TiledMapLayerContent layer) + { + writer.Write((byte)layer.LayerType); + + writer.Write(layer.Name ?? string.Empty); + writer.Write(layer.Class ?? layer.Type ?? string.Empty); + writer.Write(layer.Visible); + writer.Write(layer.Opacity); + writer.Write(layer.OffsetX); + writer.Write(layer.OffsetY); + writer.Write(layer.ParallaxX); + writer.Write(layer.ParallaxY); + + writer.WriteTiledMapProperties(layer.Properties); + + switch (layer.LayerType) + { + case TiledMapLayerType.ImageLayer: + WriteImageLayer(writer, (TiledMapImageLayerContent)layer); + break; + case TiledMapLayerType.TileLayer: + WriteTileLayer(writer, (TiledMapTileLayerContent)layer); + break; + case TiledMapLayerType.ObjectLayer: + WriteObjectLayer(writer, (TiledMapObjectLayerContent)layer); + break; + case TiledMapLayerType.GroupLayer: + WriteLayers(writer, ((TiledMapGroupLayerContent)layer).Layers); + break; + default: + throw new ArgumentOutOfRangeException(nameof(layer.LayerType)); + } + } + + private void WriteImageLayer(ContentWriter writer, TiledMapImageLayerContent imageLayer) + { + var externalReference = _contentItem.GetExternalReference<Texture2DContent>(imageLayer.Image.Source); + writer.WriteExternalReference(externalReference); + writer.Write(new Vector2(imageLayer.X, imageLayer.Y)); + } + + // ReSharper disable once SuggestBaseTypeForParameter + private static void WriteTileLayer(ContentWriter writer, TiledMapTileLayerContent tileLayer) + { + writer.Write(tileLayer.Width); + writer.Write(tileLayer.Height); + + writer.Write(tileLayer.Tiles.Length); + + foreach (var tile in tileLayer.Tiles) + { + writer.Write(tile.GlobalTileIdentifierWithFlags); + writer.Write(tile.X); + writer.Write(tile.Y); + } + } + + private static void WriteObjectLayer(ContentWriter writer, TiledMapObjectLayerContent layer) + { + writer.Write(ColorHelper.FromHex(layer.Color)); + writer.Write((byte)layer.DrawOrder); + + writer.Write(layer.Objects.Count); + + foreach (var @object in layer.Objects) + WriteObject(writer, @object); + } + + + private static void WriteObject(ContentWriter writer, TiledMapObjectContent @object) + { + var type = GetObjectType(@object); + + writer.Write((byte)type); + + writer.Write(@object.Identifier); + writer.Write(@object.Name ?? string.Empty); + writer.Write(@object.Class ?? @object.Type ?? string.Empty); + writer.Write(@object.X); + writer.Write(@object.Y); + writer.Write(@object.Width); + writer.Write(@object.Height); + writer.Write(@object.Rotation); + writer.Write(@object.Visible); + + writer.WriteTiledMapProperties(@object.Properties); + + switch (type) + { + case TiledMapObjectType.Rectangle: + case TiledMapObjectType.Ellipse: + break; + case TiledMapObjectType.Tile: + writer.Write(@object.GlobalIdentifier); + break; + case TiledMapObjectType.Polygon: + WritePolyPoints(writer, @object.Polygon.Points); + break; + case TiledMapObjectType.Polyline: + WritePolyPoints(writer, @object.Polyline.Points); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + // ReSharper disable once SuggestBaseTypeForParameter + private static void WritePolyPoints(ContentWriter writer, string @string) + { + var stringPoints = @string.Split(' '); + + writer.Write(stringPoints.Length); + + foreach (var stringPoint in stringPoints) + { + var xy = stringPoint.Split(','); + var x = float.Parse(xy[0], CultureInfo.InvariantCulture.NumberFormat); + writer.Write(x); + var y = float.Parse(xy[1], CultureInfo.InvariantCulture.NumberFormat); + writer.Write(y); + } + } + + public static TiledMapObjectType GetObjectType(TiledMapObjectContent content) + { + if (content.GlobalIdentifier > 0) + return TiledMapObjectType.Tile; + + if (content.Ellipse != null) + return TiledMapObjectType.Ellipse; + + if (content.Polygon != null) + return TiledMapObjectType.Polygon; + + // ReSharper disable once ConvertIfStatementToReturnStatement + if (content.Polyline != null) + return TiledMapObjectType.Polyline; + + return TiledMapObjectType.Rectangle; + } + + public override string GetRuntimeType(TargetPlatform targetPlatform) => "MonoGame.Extended.Tiled.TiledMap, MonoGame.Extended.Tiled"; + + public override string GetRuntimeReader(TargetPlatform targetPlatform) => "MonoGame.Extended.Tiled.TiledMapReader, MonoGame.Extended.Tiled"; + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/readme.txt b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/readme.txt new file mode 100644 index 0000000..54f1cda --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Content.Pipeline/readme.txt @@ -0,0 +1,15 @@ +# MonoGame.Extended.Content.Pipeline + +This NuGet package is intended to be used with the MonoGame Content Pipeline tool. You'll need to +make some changes to your Content Pipline file (usually Content.mgcb) for everything to work +properly. + +Add a reference to the `MonoGame.Extended.Content.Pipeline.dll` now installed in your `packages` folder +to the [MonoGame Content Pipeline tool](http://www.monogame.net/documentation/?page=Pipeline). + +You can do this either with the Reference Editor in the GUI or by manually adding a reference line to +your `Content.mgcb` file using a text editor. + +**Remember**: the versions need to match exactly for everything to work. + +For more information, take a look at the [installation guide](http://craftworkgames.github.io/MonoGame.Extended/installation/). diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Aspect.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Aspect.cs new file mode 100644 index 0000000..e6fb606 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Aspect.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Specialized; + +namespace MonoGame.Extended.Entities +{ + public class Aspect + { + internal Aspect() + { + AllSet = new BitVector32(); + ExclusionSet = new BitVector32(); + OneSet = new BitVector32(); + } + + public BitVector32 AllSet; + public BitVector32 ExclusionSet; + public BitVector32 OneSet; + + public static AspectBuilder All(params Type[] types) + { + return new AspectBuilder().All(types); + } + + public static AspectBuilder One(params Type[] types) + { + return new AspectBuilder().One(types); + } + + public static AspectBuilder Exclude(params Type[] types) + { + return new AspectBuilder().Exclude(types); + } + + public bool IsInterested(BitVector32 componentBits) + { + if (AllSet.Data != 0 && (componentBits.Data & AllSet.Data) != AllSet.Data) + return false; + + if (ExclusionSet.Data != 0 && (componentBits.Data & ExclusionSet.Data) != 0) + return false; + + if (OneSet.Data != 0 && (componentBits.Data & OneSet.Data) == 0) + return false; + + return true; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/AspectBuilder.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/AspectBuilder.cs new file mode 100644 index 0000000..d749795 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/AspectBuilder.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Specialized; +using MonoGame.Extended.Collections; + +namespace MonoGame.Extended.Entities +{ + public class AspectBuilder + { + public AspectBuilder() + { + AllTypes = new Bag<Type>(); + ExclusionTypes = new Bag<Type>(); + OneTypes = new Bag<Type>(); + } + + public Bag<Type> AllTypes { get; } + public Bag<Type> ExclusionTypes { get; } + public Bag<Type> OneTypes { get; } + + public AspectBuilder All(params Type[] types) + { + foreach (var type in types) + AllTypes.Add(type); + + return this; + } + + public AspectBuilder One(params Type[] types) + { + foreach (var type in types) + OneTypes.Add(type); + + return this; + } + + public AspectBuilder Exclude(params Type[] types) + { + foreach (var type in types) + ExclusionTypes.Add(type); + + return this; + } + + public Aspect Build(ComponentManager componentManager) + { + var aspect = new Aspect(); + Associate(componentManager, AllTypes, ref aspect.AllSet); + Associate(componentManager, OneTypes, ref aspect.OneSet); + Associate(componentManager, ExclusionTypes, ref aspect.ExclusionSet); + return aspect; + } + + // ReSharper disable once ParameterTypeCanBeEnumerable.Local + private static void Associate(ComponentManager componentManager, Bag<Type> types, ref BitVector32 bits) + { + foreach (var type in types) + { + var id = componentManager.GetComponentTypeId(type); + bits[1 << id] = true; + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/BitArrayExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/BitArrayExtensions.cs new file mode 100644 index 0000000..2ca0737 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/BitArrayExtensions.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections; + +namespace MonoGame.Extended.Entities +{ + public static class BitArrayExtensions + { + public static bool IsEmpty(this BitArray bitArray) + { + for (var i = 0; i < bitArray.Length; i++) + { + if (bitArray[i]) + return false; + } + + return true; + } + + public static bool ContainsAll(this BitArray bitArray, BitArray other) + { + var otherBitsLength = other.Length; + var bitsLength = bitArray.Length; + + for (var i = bitsLength; i < otherBitsLength; i++) + { + if (other[i]) + return false; + } + + var s = Math.Min(bitsLength, otherBitsLength); + + for (var i = 0; s > i; i++) + { + if ((bitArray[i] & other[i]) != other[i]) + return false; + } + + return true; + } + + public static bool Intersects(this BitArray bitArray, BitArray other) + { + var s = Math.Min(bitArray.Length, other.Length); + + for (var i = 0; s > i; i++) + { + if (bitArray[i] & other[i]) + return true; + } + + return false; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentManager.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentManager.cs new file mode 100644 index 0000000..a59c87c --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentManager.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using Microsoft.Xna.Framework; +using MonoGame.Extended.Collections; +using MonoGame.Extended.Entities.Systems; + +namespace MonoGame.Extended.Entities +{ + public interface IComponentMapperService + { + ComponentMapper<T> GetMapper<T>() where T : class; + } + + public class ComponentManager : UpdateSystem, IComponentMapperService + { + public ComponentManager() + { + _componentMappers = new Bag<ComponentMapper>(); + _componentTypes = new Dictionary<Type, int>(); + } + + private readonly Bag<ComponentMapper> _componentMappers; + private readonly Dictionary<Type, int> _componentTypes; + + public Action<int> ComponentsChanged; + + private ComponentMapper<T> CreateMapperForType<T>(int componentTypeId) + where T : class + { + // TODO: We can probably do better than this without a huge performance penalty by creating our own bit vector that grows after the first 32 bits. + if (componentTypeId >= 32) + throw new InvalidOperationException("Component type limit exceeded. We currently only allow 32 component types for performance reasons."); + + var mapper = new ComponentMapper<T>(componentTypeId, ComponentsChanged); + _componentMappers[componentTypeId] = mapper; + return mapper; + } + + public ComponentMapper GetMapper(int componentTypeId) + { + return _componentMappers[componentTypeId]; + } + + public ComponentMapper<T> GetMapper<T>() + where T : class + { + var componentTypeId = GetComponentTypeId(typeof(T)); + + if (_componentMappers[componentTypeId] != null) + return _componentMappers[componentTypeId] as ComponentMapper<T>; + + return CreateMapperForType<T>(componentTypeId); + } + + public int GetComponentTypeId(Type type) + { + if (_componentTypes.TryGetValue(type, out var id)) + return id; + + id = _componentTypes.Count; + _componentTypes.Add(type, id); + return id; + } + + public BitVector32 CreateComponentBits(int entityId) + { + var componentBits = new BitVector32(); + var mask = BitVector32.CreateMask(); + + for (var componentId = 0; componentId < _componentMappers.Count; componentId++) + { + componentBits[mask] = _componentMappers[componentId]?.Has(entityId) ?? false; + mask = BitVector32.CreateMask(mask); + } + + return componentBits; + } + + public void Destroy(int entityId) + { + foreach (var componentMapper in _componentMappers) + componentMapper?.Delete(entityId); + } + + public override void Update(GameTime gameTime) + { + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentMapper.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentMapper.cs new file mode 100644 index 0000000..35b196c --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentMapper.cs @@ -0,0 +1,69 @@ +using System; +using MonoGame.Extended.Collections; + +namespace MonoGame.Extended.Entities +{ + public abstract class ComponentMapper + { + protected ComponentMapper(int id, Type componentType) + { + Id = id; + ComponentType = componentType; + } + + public int Id { get; } + public Type ComponentType { get; } + public abstract bool Has(int entityId); + public abstract void Delete(int entityId); + } + + public class ComponentMapper<T> : ComponentMapper + where T : class + { + public event Action<int> OnPut; + public event Action<int> OnDelete; + + private readonly Action<int> _onCompositionChanged; + + public ComponentMapper(int id, Action<int> onCompositionChanged) + : base(id, typeof(T)) + { + _onCompositionChanged = onCompositionChanged; + Components = new Bag<T>(); + } + + public Bag<T> Components { get; } + + public void Put(int entityId, T component) + { + Components[entityId] = component; + _onCompositionChanged(entityId); + OnPut?.Invoke(entityId); + } + + public T Get(Entity entity) + { + return Get(entity.Id); + } + + public T Get(int entityId) + { + return Components[entityId]; + } + + public override bool Has(int entityId) + { + if (entityId >= Components.Count) + return false; + + return Components[entityId] != null; + } + + public override void Delete(int entityId) + { + Components[entityId] = null; + _onCompositionChanged(entityId); + OnDelete?.Invoke(entityId); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentType.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentType.cs new file mode 100644 index 0000000..ebef996 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/ComponentType.cs @@ -0,0 +1,46 @@ +using System; + +namespace MonoGame.Extended.Entities +{ + //public class ComponentType : IEquatable<ComponentType> + //{ + // public ComponentType(Type type, int id) + // { + // Type = type; + // Id = id; + // } + + // public Type Type { get; } + // public int Id { get; } + + // public bool Equals(ComponentType other) + // { + // if (ReferenceEquals(null, other)) return false; + // if (ReferenceEquals(this, other)) return true; + // return Id == other.Id; + // } + + // public override bool Equals(object obj) + // { + // if (ReferenceEquals(null, obj)) return false; + // if (ReferenceEquals(this, obj)) return true; + // if (obj.GetType() != GetType()) return false; + // return Equals((ComponentType) obj); + // } + + // public override int GetHashCode() + // { + // return Id; + // } + + // public static bool operator ==(ComponentType left, ComponentType right) + // { + // return Equals(left, right); + // } + + // public static bool operator !=(ComponentType left, ComponentType right) + // { + // return !Equals(left, right); + // } + //} +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Entity.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Entity.cs new file mode 100644 index 0000000..a56ccff --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Entity.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Specialized; + +namespace MonoGame.Extended.Entities +{ + public class Entity : IEquatable<Entity> + { + private readonly EntityManager _entityManager; + private readonly ComponentManager _componentManager; + + internal Entity(int id, EntityManager entityManager, ComponentManager componentManager) + { + Id = id; + + _entityManager = entityManager; + _componentManager = componentManager; + } + + public int Id { get; } + + public BitVector32 ComponentBits => _entityManager.GetComponentBits(Id); + + public void Attach<T>(T component) + where T : class + { + var mapper = _componentManager.GetMapper<T>(); + mapper.Put(Id, component); + } + + public void Detach<T>() + where T : class + { + var mapper = _componentManager.GetMapper<T>(); + mapper.Delete(Id); + } + + public T Get<T>() + where T : class + { + var mapper = _componentManager.GetMapper<T>(); + return mapper.Get(Id); + } + + + public bool Has<T>() + where T : class + { + return _componentManager.GetMapper<T>().Has(Id); + } + + public void Destroy() + { + _entityManager.Destroy(Id); + } + + public bool Equals(Entity other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Id == other.Id; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((Entity) obj); + } + + public override int GetHashCode() + { + // ReSharper disable once NonReadonlyMemberInGetHashCode + return Id; + } + + public static bool operator ==(Entity left, Entity right) + { + return Equals(left, right); + } + + public static bool operator !=(Entity left, Entity right) + { + return !Equals(left, right); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/EntityManager.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/EntityManager.cs new file mode 100644 index 0000000..5e1d28a --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/EntityManager.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Linq; +using Microsoft.Xna.Framework; +using MonoGame.Extended.Collections; +using MonoGame.Extended.Entities.Systems; + +namespace MonoGame.Extended.Entities +{ + public class EntityManager : UpdateSystem + { + private const int _defaultBagSize = 128; + + public EntityManager(ComponentManager componentManager) + { + _componentManager = componentManager; + _addedEntities = new Bag<int>(_defaultBagSize); + _removedEntities = new Bag<int>(_defaultBagSize); + _changedEntities = new Bag<int>(_defaultBagSize); + _entityToComponentBits = new Bag<BitVector32>(_defaultBagSize); + _componentManager.ComponentsChanged += OnComponentsChanged; + + _entityBag = new Bag<Entity>(_defaultBagSize); + _entityPool = new Pool<Entity>(() => new Entity(_nextId++, this, _componentManager), _defaultBagSize); + } + + private readonly ComponentManager _componentManager; + private int _nextId; + + public int Capacity => _entityBag.Capacity; + public IEnumerable<int> Entities => _entityBag.Where(e => e != null).Select(e => e.Id); + public int ActiveCount { get; private set; } + + private readonly Bag<Entity> _entityBag; + private readonly Pool<Entity> _entityPool; + private readonly Bag<int> _addedEntities; + private readonly Bag<int> _removedEntities; + private readonly Bag<int> _changedEntities; + private readonly Bag<BitVector32> _entityToComponentBits; + + public event Action<int> EntityAdded; + public event Action<int> EntityRemoved; + public event Action<int> EntityChanged; + + public Entity Create() + { + var entity = _entityPool.Obtain(); + var id = entity.Id; + Debug.Assert(_entityBag[id] == null); + _entityBag[id] = entity; + _addedEntities.Add(id); + _entityToComponentBits[id] = new BitVector32(0); + return entity; + } + + public void Destroy(int entityId) + { + if (!_removedEntities.Contains(entityId)) + _removedEntities.Add(entityId); + } + + public void Destroy(Entity entity) + { + Destroy(entity.Id); + } + + public Entity Get(int entityId) + { + return _entityBag[entityId]; + } + + public BitVector32 GetComponentBits(int entityId) + { + return _entityToComponentBits[entityId]; + } + + private void OnComponentsChanged(int entityId) + { + _changedEntities.Add(entityId); + _entityToComponentBits[entityId] = _componentManager.CreateComponentBits(entityId); + EntityChanged?.Invoke(entityId); + } + + public override void Update(GameTime gameTime) + { + foreach (var entityId in _addedEntities) + { + _entityToComponentBits[entityId] = _componentManager.CreateComponentBits(entityId); + ActiveCount++; + EntityAdded?.Invoke(entityId); + } + + foreach (var entityId in _changedEntities) + { + _entityToComponentBits[entityId] = _componentManager.CreateComponentBits(entityId); + EntityChanged?.Invoke(entityId); + } + + foreach (var entityId in _removedEntities) + { + // we must notify subscribers before removing it from the pool + // otherwise an entity system could still be using the entity when the same id is obtained. + EntityRemoved?.Invoke(entityId); + + var entity = _entityBag[entityId]; + _entityBag[entityId] = null; + _componentManager.Destroy(entityId); + _entityToComponentBits[entityId] = default(BitVector32); + ActiveCount--; + _entityPool.Free(entity); + } + + _addedEntities.Clear(); + _removedEntities.Clear(); + _changedEntities.Clear(); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/EntitySubscription.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/EntitySubscription.cs new file mode 100644 index 0000000..6766b0f --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/EntitySubscription.cs @@ -0,0 +1,61 @@ +using System; +using MonoGame.Extended.Collections; + +namespace MonoGame.Extended.Entities +{ + internal class EntitySubscription : IDisposable + { + private readonly Bag<int> _activeEntities; + private readonly EntityManager _entityManager; + private readonly Aspect _aspect; + private bool _rebuildActives; + + internal EntitySubscription(EntityManager entityManager, Aspect aspect) + { + _entityManager = entityManager; + _aspect = aspect; + _activeEntities = new Bag<int>(entityManager.Capacity); + _rebuildActives = true; + + _entityManager.EntityAdded += OnEntityAdded; + _entityManager.EntityRemoved += OnEntityRemoved; + _entityManager.EntityChanged += OnEntityChanged; + } + + private void OnEntityAdded(int entityId) + { + if (_aspect.IsInterested(_entityManager.GetComponentBits(entityId))) + _activeEntities.Add(entityId); + } + + private void OnEntityRemoved(int entityId) => _rebuildActives = true; + private void OnEntityChanged(int entityId) => _rebuildActives = true; + + public void Dispose() + { + _entityManager.EntityAdded -= OnEntityAdded; + _entityManager.EntityRemoved -= OnEntityRemoved; + } + + public Bag<int> ActiveEntities + { + get + { + if (_rebuildActives) + RebuildActives(); + + return _activeEntities; + } + } + + private void RebuildActives() + { + _activeEntities.Clear(); + + foreach (var entity in _entityManager.Entities) + OnEntityAdded(entity); + + _rebuildActives = false; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/MonoGame.Extended.Entities.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/MonoGame.Extended.Entities.csproj new file mode 100644 index 0000000..21ca693 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/MonoGame.Extended.Entities.csproj @@ -0,0 +1,12 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <Description>An Entity Component System to make MonoGame more awesome.</Description> + <PackageTags>monogame ecs entity component system</PackageTags> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" /> + </ItemGroup> + +</Project> diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/DrawSystem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/DrawSystem.cs new file mode 100644 index 0000000..b9e63eb --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/DrawSystem.cs @@ -0,0 +1,16 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Entities.Systems +{ + public interface IDrawSystem : ISystem + { + void Draw(GameTime gameTime); + } + + public abstract class DrawSystem : IDrawSystem + { + public virtual void Dispose() { } + public virtual void Initialize(World world) { } + public abstract void Draw(GameTime gameTime); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityDrawSystem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityDrawSystem.cs new file mode 100644 index 0000000..5a0d6b9 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityDrawSystem.cs @@ -0,0 +1,14 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Entities.Systems +{ + public abstract class EntityDrawSystem : EntitySystem, IDrawSystem + { + protected EntityDrawSystem(AspectBuilder aspect) + : base(aspect) + { + } + + public abstract void Draw(GameTime gameTime); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityProcessingSystem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityProcessingSystem.cs new file mode 100644 index 0000000..c4d9339 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityProcessingSystem.cs @@ -0,0 +1,26 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Entities.Systems +{ + public abstract class EntityProcessingSystem : EntityUpdateSystem + { + protected EntityProcessingSystem(AspectBuilder aspectBuilder) + : base(aspectBuilder) + { + } + + public override void Update(GameTime gameTime) + { + Begin(); + + foreach (var entityId in ActiveEntities) + Process(gameTime, entityId); + + End(); + } + + public virtual void Begin() { } + public abstract void Process(GameTime gameTime, int entityId); + public virtual void End() { } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntitySystem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntitySystem.cs new file mode 100644 index 0000000..2c95107 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntitySystem.cs @@ -0,0 +1,51 @@ +using MonoGame.Extended.Collections; + +namespace MonoGame.Extended.Entities.Systems +{ + public abstract class EntitySystem : ISystem + { + protected EntitySystem(AspectBuilder aspectBuilder) + { + _aspectBuilder = aspectBuilder; + } + + public void Dispose() + { + if (_world != null) + { + _world.EntityManager.EntityAdded -= OnEntityAdded; + _world.EntityManager.EntityRemoved -= OnEntityRemoved; + } + } + + private readonly AspectBuilder _aspectBuilder; + private EntitySubscription _subscription; + + private World _world; + + protected virtual void OnEntityChanged(int entityId) { } + protected virtual void OnEntityAdded(int entityId) { } + protected virtual void OnEntityRemoved(int entityId) { } + + public Bag<int> ActiveEntities => _subscription.ActiveEntities; + + public virtual void Initialize(World world) + { + _world = world; + + var aspect = _aspectBuilder.Build(_world.ComponentManager); + _subscription = new EntitySubscription(_world.EntityManager, aspect); + _world.EntityManager.EntityAdded += OnEntityAdded; + _world.EntityManager.EntityRemoved += OnEntityRemoved; + _world.EntityManager.EntityChanged += OnEntityChanged; + + Initialize(world.ComponentManager); + } + + public abstract void Initialize(IComponentMapperService mapperService); + + protected void DestroyEntity(int entityId) => _world.DestroyEntity(entityId); + protected Entity CreateEntity() => _world.CreateEntity(); + protected Entity GetEntity(int entityId) => _world.GetEntity(entityId); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityUpdateSystem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityUpdateSystem.cs new file mode 100644 index 0000000..97a61d5 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/EntityUpdateSystem.cs @@ -0,0 +1,14 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Entities.Systems +{ + public abstract class EntityUpdateSystem : EntitySystem, IUpdateSystem + { + protected EntityUpdateSystem(AspectBuilder aspectBuilder) + : base(aspectBuilder) + { + } + + public abstract void Update(GameTime gameTime); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/ISystem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/ISystem.cs new file mode 100644 index 0000000..d4511fc --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/ISystem.cs @@ -0,0 +1,9 @@ +using System; + +namespace MonoGame.Extended.Entities.Systems +{ + public interface ISystem : IDisposable + { + void Initialize(World world); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/UpdateSystem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/UpdateSystem.cs new file mode 100644 index 0000000..e85c964 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/Systems/UpdateSystem.cs @@ -0,0 +1,16 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Entities.Systems +{ + public interface IUpdateSystem : ISystem + { + void Update(GameTime gameTime); + } + + public abstract class UpdateSystem : IUpdateSystem + { + public virtual void Dispose() { } + public virtual void Initialize(World world) { } + public abstract void Update(GameTime gameTime); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/World.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/World.cs new file mode 100644 index 0000000..022d4b8 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/World.cs @@ -0,0 +1,84 @@ +using Microsoft.Xna.Framework; +using MonoGame.Extended.Collections; +using MonoGame.Extended.Entities.Systems; + +namespace MonoGame.Extended.Entities +{ + public class World : SimpleDrawableGameComponent + { + private readonly Bag<IUpdateSystem> _updateSystems; + private readonly Bag<IDrawSystem> _drawSystems; + + internal World() + { + _updateSystems = new Bag<IUpdateSystem>(); + _drawSystems = new Bag<IDrawSystem>(); + + RegisterSystem(ComponentManager = new ComponentManager()); + RegisterSystem(EntityManager = new EntityManager(ComponentManager)); + } + + public override void Dispose() + { + foreach (var updateSystem in _updateSystems) + updateSystem.Dispose(); + + foreach (var drawSystem in _drawSystems) + drawSystem.Dispose(); + + _updateSystems.Clear(); + _drawSystems.Clear(); + + base.Dispose(); + } + + internal EntityManager EntityManager { get; } + internal ComponentManager ComponentManager { get; } + + public int EntityCount => EntityManager.ActiveCount; + + internal void RegisterSystem(ISystem system) + { + // ReSharper disable once ConvertIfStatementToSwitchStatement + if (system is IUpdateSystem updateSystem) + _updateSystems.Add(updateSystem); + + if (system is IDrawSystem drawSystem) + _drawSystems.Add(drawSystem); + + system.Initialize(this); + } + + public Entity GetEntity(int entityId) + { + return EntityManager.Get(entityId); + } + + public Entity CreateEntity() + { + return EntityManager.Create(); + } + + public void DestroyEntity(int entityId) + { + EntityManager.Destroy(entityId); + } + + public void DestroyEntity(Entity entity) + { + EntityManager.Destroy(entity); + } + + public override void Update(GameTime gameTime) + { + foreach (var system in _updateSystems) + system.Update(gameTime); + } + + public override void Draw(GameTime gameTime) + { + foreach (var system in _drawSystems) + system.Draw(gameTime); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/WorldBuilder.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/WorldBuilder.cs new file mode 100644 index 0000000..7f03323 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Entities/WorldBuilder.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using MonoGame.Extended.Entities.Systems; + +namespace MonoGame.Extended.Entities +{ + public class WorldBuilder + { + private readonly List<ISystem> _systems = new List<ISystem>(); + + public WorldBuilder AddSystem(ISystem system) + { + _systems.Add(system); + return this; + } + + public World Build() + { + var world = new World(); + + foreach (var system in _systems) + world.RegisterSystem(system); + + return world; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Batcher.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Batcher.cs new file mode 100644 index 0000000..3cb5704 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Batcher.cs @@ -0,0 +1,387 @@ +using System; +using System.Runtime.CompilerServices; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Graphics +{ + /// <summary> + /// Minimizes draw calls to a <see cref="GraphicsDevice" /> by sorting them and attempting to merge them together + /// before submitting them. + /// </summary> + /// <typeparam name="TDrawCallInfo">The type of the information for a draw call.</typeparam> + /// <seealso cref="IDisposable" /> + public abstract class Batcher<TDrawCallInfo> : IDisposable + where TDrawCallInfo : struct, IBatchDrawCallInfo<TDrawCallInfo>, IComparable<TDrawCallInfo> + { + internal const int DefaultBatchMaximumDrawCallsCount = 2048; + private BlendState _blendState; + private SamplerState _samplerState; + private DepthStencilState _depthStencilState; + private RasterizerState _rasterizerState; + private readonly Effect _defaultEffect; + private Effect _currentEffect; + private Matrix? _viewMatrix; + private Matrix? _projectionMatrix; + + /// <summary> + /// The array of <see cref="TDrawCallInfo" /> structs currently enqueued. + /// </summary> + protected TDrawCallInfo[] DrawCalls; + + /// <summary> + /// The number of <see cref="TDrawCallInfo" /> structs currently enqueued. + /// </summary> + protected int EnqueuedDrawCallCount; + + /// <summary> + /// Gets the <see cref="GraphicsDevice" /> associated with this <see cref="Batcher{TDrawCallInfo}" />. + /// </summary> + /// <value> + /// The <see cref="GraphicsDevice" /> associated with this <see cref="Batcher{TDrawCallInfo}" />. + /// </value> + public GraphicsDevice GraphicsDevice { get; } + + /// <summary> + /// Gets a value indicating whether batching is currently in progress by being within a <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and + /// <see cref="End" /> pair block of code. + /// </summary> + /// <value> + /// <c>true</c> if batching has begun; otherwise, <c>false</c>. + /// </value> + public bool HasBegun { get; internal set; } + + /// <summary> + /// Initializes a new instance of the <see cref="Batcher{TDrawCallInfo}" /> class. + /// </summary> + /// <param name="graphicsDevice">The graphics device.</param> + /// <param name="defaultEffect">The default effect.</param> + /// <param name="maximumDrawCallsCount"> + /// The maximum number of <see cref="TDrawCallInfo" /> structs that can be enqueued before a + /// <see cref="Batcher{TDrawCallInfo}.Flush" /> + /// is required. The default value is <code>2048</code>. + /// </param> + /// <exception cref="ArgumentNullException"> + /// <paramref name="graphicsDevice" /> is + /// null. + /// </exception> + /// <exception cref="ArgumentOutOfRangeException"> + /// <paramref name="maximumDrawCallsCount" /> is less than or equal + /// <code>0</code>. + /// </exception> + protected Batcher(GraphicsDevice graphicsDevice, Effect defaultEffect, + int maximumDrawCallsCount = DefaultBatchMaximumDrawCallsCount) + { + if (graphicsDevice == null) + throw new ArgumentNullException(nameof(graphicsDevice)); + + if (maximumDrawCallsCount <= 0) + throw new ArgumentOutOfRangeException(nameof(maximumDrawCallsCount)); + + GraphicsDevice = graphicsDevice; + _defaultEffect = defaultEffect; + DrawCalls = new TDrawCallInfo[maximumDrawCallsCount]; + } + + /// <summary> + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// </summary> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Releases unmanaged and - optionally - managed resources. + /// </summary> + /// <param name="diposing"> + /// <c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only + /// unmanaged resources. + /// </param> + protected virtual void Dispose(bool diposing) + { + if (!diposing) + return; + _defaultEffect.Dispose(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void EnsureHasBegun([CallerMemberName] string callerMemberName = null) + { + if (!HasBegun) + throw new InvalidOperationException( + $"The {nameof(Begin)} method must be called before the {callerMemberName} method can be called."); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected void EnsureHasNotBegun([CallerMemberName] string callerMemberName = null) + { + if (HasBegun) + throw new InvalidOperationException( + $"The {nameof(End)} method must be called before the {callerMemberName} method can be called."); + } + + /// <summary> + /// Begins the batch operation using an optional <see cref="BlendState" />, <see cref="SamplerState" />, + /// <see cref="DepthStencilState" />, <see cref="RasterizerState" />, <see cref="Effect" />, world-to-view + /// <see cref="Matrix" />, or view-to-projection <see cref="Matrix" />. + /// </summary> + /// <remarks> + /// <para> + /// The default objects for <paramref name="blendState" />, <paramref name="samplerState" />, + /// <paramref name="depthStencilState" />, and <paramref name="rasterizerState" /> are + /// <see cref="BlendState.AlphaBlend" />, <see cref="SamplerState.LinearClamp" />, + /// <see cref="DepthStencilState.None" /> and <see cref="RasterizerState.CullCounterClockwise" /> respectively. + /// Passing + /// <code>null</code> for any of the previously mentioned parameters result in using their default object. + /// </para> + /// </remarks> + /// <param name="blendState">The <see cref="BlendState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" />, <see cref="End" /> pair.</param> + /// <param name="samplerState"> + /// The texture <see cref="SamplerState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and + /// <see cref="End" /> pair. + /// </param> + /// <param name="depthStencilState"> + /// The <see cref="DepthStencilState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and + /// <see cref="End" /> pair. + /// </param> + /// <param name="rasterizerState"> + /// The <see cref="RasterizerState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and + /// <see cref="End" /> pair. + /// </param> + /// <param name="effect">The <see cref="Effect" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and <see cref="End" /> pair.</param> + /// <param name="viewMatrix"> + /// The world-to-view transformation matrix to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and + /// <see cref="End" /> pair. + /// </param> + /// <param name="projectionMatrix"> + /// The view-to-projection transformation matrix to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and + /// <see cref="End" /> pair. + /// </param> + /// <exception cref="InvalidOperationException"> + /// <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> cannot be invoked again until <see cref="End" /> has been invoked. + /// </exception> + /// <remarks> + /// <para> + /// This method must be called before any enqueuing of draw calls. When all the geometry have been enqueued for + /// drawing, call <see cref="End" />. + /// </para> + /// </remarks> + public virtual void Begin(Matrix? viewMatrix = null, Matrix? projectionMatrix = null, BlendState blendState = null, SamplerState samplerState = null, + DepthStencilState depthStencilState = null, RasterizerState rasterizerState = null, Effect effect = null) + { + var viewMatrix1 = viewMatrix ?? Matrix.Identity; + var projectionMatrix1 = projectionMatrix ?? Matrix.CreateOrthographicOffCenter(0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height, 0, 0, -1); + Begin(ref viewMatrix1, ref projectionMatrix1, blendState, samplerState, depthStencilState, rasterizerState, effect); + } + + /// <summary> + /// Begins the batch operation using an optional <see cref="BlendState" />, <see cref="SamplerState" />, + /// <see cref="DepthStencilState" />, <see cref="RasterizerState" />, <see cref="Effect" />, world-to-view + /// <see cref="Matrix" />, or view-to-projection <see cref="Matrix" />. + /// </summary> + /// <remarks> + /// <para> + /// The default objects for <paramref name="blendState" />, <paramref name="samplerState" />, + /// <paramref name="depthStencilState" />, and <paramref name="rasterizerState" /> are + /// <see cref="BlendState.AlphaBlend" />, <see cref="SamplerState.LinearClamp" />, + /// <see cref="DepthStencilState.None" /> and <see cref="RasterizerState.CullCounterClockwise" /> respectively. + /// Passing + /// <code>null</code> for any of the previously mentioned parameters result in using their default object. + /// </para> + /// </remarks> + /// <param name="blendState">The <see cref="BlendState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" />, <see cref="End" /> pair.</param> + /// <param name="samplerState"> + /// The texture <see cref="SamplerState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and + /// <see cref="End" /> pair. + /// </param> + /// <param name="depthStencilState"> + /// The <see cref="DepthStencilState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and + /// <see cref="End" /> pair. + /// </param> + /// <param name="rasterizerState"> + /// The <see cref="RasterizerState" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and + /// <see cref="End" /> pair. + /// </param> + /// <param name="effect">The <see cref="Effect" /> to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and <see cref="End" /> pair.</param> + /// <param name="viewMatrix"> + /// The world-to-view transformation matrix to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and + /// <see cref="End" /> pair. + /// </param> + /// <param name="projectionMatrix"> + /// The view-to-projection transformation matrix to use for the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and + /// <see cref="End" /> pair. + /// </param> + /// <exception cref="InvalidOperationException"> + /// <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> cannot be invoked again until <see cref="End" /> has been invoked. + /// </exception> + /// <remarks> + /// <para> + /// This method must be called before any enqueuing of draw calls. When all the geometry have been enqueued for + /// drawing, call <see cref="End" />. + /// </para> + /// </remarks> + public virtual void Begin(ref Matrix viewMatrix, ref Matrix projectionMatrix, BlendState blendState = null, SamplerState samplerState = null, + DepthStencilState depthStencilState = null, RasterizerState rasterizerState = null, Effect effect = null) + { + EnsureHasNotBegun(); + HasBegun = true; + + // Store the states to be applied on End() + // This ensures that two or more batchers will not affect each other + _blendState = blendState ?? BlendState.AlphaBlend; + _samplerState = samplerState ?? SamplerState.PointClamp; + _depthStencilState = depthStencilState ?? DepthStencilState.None; + _rasterizerState = rasterizerState ?? RasterizerState.CullCounterClockwise; + _currentEffect = effect ?? _defaultEffect; + _projectionMatrix = projectionMatrix; + _viewMatrix = viewMatrix; + } + + /// <summary> + /// Flushes the batched geometry to the <see cref="GraphicsDevice" /> and restores it's state to how it was before + /// <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> was called. + /// </summary> + /// <exception cref="InvalidOperationException"> + /// <see cref="End" /> cannot be invoked until <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> has been invoked. + /// </exception> + /// <remarks> + /// <para> + /// This method must be called after all enqueuing of draw calls. + /// </para> + /// </remarks> + public void End() + { + EnsureHasBegun(); + Flush(); + HasBegun = false; + } + + /// <summary> + /// Sorts then submits the (sorted) enqueued draw calls to the <see cref="GraphicsDevice" /> for + /// rendering without ending the <see cref="Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> and <see cref="End" /> pair. + /// </summary> + protected void Flush() + { + if (EnqueuedDrawCallCount == 0) + return; + SortDrawCallsAndBindBuffers(); + ApplyStates(); + SubmitDrawCalls(); + RestoreStates(); + } + + /// <summary> + /// Sorts the enqueued draw calls and binds any used <see cref="VertexBuffer" /> or <see cref="IndexBuffer" /> to the <see cref="GraphicsDevice" />. + /// </summary> + protected abstract void SortDrawCallsAndBindBuffers(); + + private void ApplyStates() + { + var oldBlendState = GraphicsDevice.BlendState; + var oldSamplerState = GraphicsDevice.SamplerStates[0]; + var oldDepthStencilState = GraphicsDevice.DepthStencilState; + var oldRasterizerState = GraphicsDevice.RasterizerState; + + GraphicsDevice.BlendState = _blendState; + + GraphicsDevice.SamplerStates[0] = _samplerState; + GraphicsDevice.DepthStencilState = _depthStencilState; + GraphicsDevice.RasterizerState = _rasterizerState; + + _blendState = oldBlendState; + _samplerState = oldSamplerState; + _depthStencilState = oldDepthStencilState; + _rasterizerState = oldRasterizerState; + + var viewMatrix = _viewMatrix ?? Matrix.Identity; + var projectionMatrix = _projectionMatrix ?? + Matrix.CreateOrthographicOffCenter(0, GraphicsDevice.Viewport.Width, + GraphicsDevice.Viewport.Height, 0, 0f, -1f); + + var matrixChainEffect = _currentEffect as IMatrixChainEffect; + if (matrixChainEffect != null) + { + matrixChainEffect.World = Matrix.Identity; + matrixChainEffect.SetView(ref viewMatrix); + matrixChainEffect.SetProjection(ref projectionMatrix); + } + else + { + var effectMatrices = _currentEffect as IEffectMatrices; + if (effectMatrices == null) + return; + effectMatrices.World = Matrix.Identity; + effectMatrices.View = viewMatrix; + effectMatrices.Projection = projectionMatrix; + } + } + + private void RestoreStates() + { + GraphicsDevice.BlendState = _blendState; + GraphicsDevice.SamplerStates[0] = _samplerState; + GraphicsDevice.DepthStencilState = _depthStencilState; + GraphicsDevice.RasterizerState = _rasterizerState; + } + + /// <summary> + /// Enqueues draw call information. + /// </summary> + /// <param name="drawCall">The draw call information.</param> + /// <remarks> + /// <para> + /// If possible, the <paramref name="drawCall" /> is merged with the last enqueued draw call information instead of + /// being + /// enqueued. + /// </para> + /// <para> + /// If the enqueue buffer is full, a <see cref="Flush" /> is invoked and then afterwards + /// <paramref name="drawCall" /> is enqueued. + /// </para> + /// </remarks> + protected void Enqueue(ref TDrawCallInfo drawCall) + { + if (EnqueuedDrawCallCount > 0 && drawCall.TryMerge(ref DrawCalls[EnqueuedDrawCallCount - 1])) + return; + if (EnqueuedDrawCallCount >= DrawCalls.Length) + Flush(); + DrawCalls[EnqueuedDrawCallCount++] = drawCall; + } + + /* It might be better to have derived classes just implement the for loop instead of having this virtual method call... + * However, if the derived class is only going to override this method once and the code is short, which should both be + * true, then maybe we can get away with this virtual method call by having it inlined. So tell the JIT or AOT compiler + * we would like it be so. This does NOT guarantee the compiler will respect our wishes. + */ + + /// <summary> + /// Submits a draw operation to the <see cref="GraphicsDevice" /> using the specified <see cref="TDrawCallInfo"/>. + /// </summary> + /// <param name="drawCall">The draw call information.</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected abstract void InvokeDrawCall(ref TDrawCallInfo drawCall); + + private void SubmitDrawCalls() + { + if (EnqueuedDrawCallCount == 0) + return; + + for (var i = 0; i < EnqueuedDrawCallCount; i++) + { + DrawCalls[i].SetState(_currentEffect); + + foreach (var pass in _currentEffect.CurrentTechnique.Passes) + { + pass.Apply(); + InvokeDrawCall(ref DrawCalls[i]); + } + } + + Array.Clear(DrawCalls, 0, EnqueuedDrawCallCount); + EnqueuedDrawCallCount = 0; + } + } +} +
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Batcher2D.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Batcher2D.cs new file mode 100644 index 0000000..e7ec533 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Batcher2D.cs @@ -0,0 +1,450 @@ +using System; +using System.Text; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using MonoGame.Extended.BitmapFonts; +using MonoGame.Extended.Graphics.Effects; +using MonoGame.Extended.Graphics.Geometry; + +namespace MonoGame.Extended.Graphics +{ + /// <summary> + /// A general purpose <see cref="Batcher{TDrawCallInfo}" /> for two-dimensional geometry that change + /// frequently between frames such as sprites and shapes. + /// </summary> + /// <seealso cref="IDisposable" /> + /// <remarks> + /// <para>For drawing user interfaces, consider using <see cref="UIBatcher(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> instead because it supports scissor rectangles.</para> + /// </remarks> + public sealed class Batcher2D : Batcher<Batcher2D.DrawCallInfo> + { + + internal const int DefaultMaximumVerticesCount = 8192; + internal const int DefaultMaximumIndicesCount = 12288; + + private readonly VertexBuffer _vertexBuffer; + private readonly IndexBuffer _indexBuffer; + private readonly VertexPositionColorTexture[] _vertices; + private int _vertexCount; + private readonly ushort[] _indices; + private int _indexCount; + private readonly ushort[] _sortedIndices; + private readonly GeometryBuilder2D _geometryBuilder; + + /// <summary> + /// Initializes a new instance of the <see cref="Batcher2D" /> class. + /// </summary> + /// <param name="graphicsDevice">The graphics device.</param> + /// <param name="maximumVerticesCount"> + /// The maximum number of vertices that can be enqueued before a + /// <see cref="Batcher{TDrawCallInfo}.Flush" /> is required. The default value is <code>8192</code>. + /// </param> + /// <param name="maximumIndicesCount"> + /// The maximum number of indices that can be enqueued before a + /// <see cref="Batcher{TDrawCallInfo}.Flush" /> is required. The default value is <code>12288</code>. + /// </param> + /// <param name="maximumDrawCallsCount"> + /// The maximum number of <see cref="DrawCallInfo" /> structs that can be enqueued before a + /// <see cref="Batcher{TDrawCallInfo}.Flush" /> is required. The default value is <code>2048</code>. + /// </param> + /// <exception cref="ArgumentNullException"><paramref name="graphicsDevice" />.</exception> + /// <exception cref="ArgumentOutOfRangeException"> + /// <paramref name="maximumDrawCallsCount" /> is less than or equal + /// <code>0</code>, or <paramref name="maximumVerticesCount" /> is less than or equal to <code>0</code>, or, + /// <paramref name="maximumVerticesCount" /> is less than or equal to <code>0</code>. + /// </exception> + public Batcher2D(GraphicsDevice graphicsDevice, + ushort maximumVerticesCount = DefaultMaximumVerticesCount, + ushort maximumIndicesCount = DefaultMaximumIndicesCount, + int maximumDrawCallsCount = DefaultBatchMaximumDrawCallsCount) + : base( + graphicsDevice, + new DefaultEffect(graphicsDevice) + { + TextureEnabled = true, + VertexColorEnabled = true + }, maximumDrawCallsCount) + + { + _vertices = new VertexPositionColorTexture[maximumVerticesCount]; + _vertexBuffer = new DynamicVertexBuffer(graphicsDevice, VertexPositionColorTexture.VertexDeclaration, maximumVerticesCount, BufferUsage.WriteOnly); + + _indices = new ushort[maximumIndicesCount]; + _sortedIndices = new ushort[maximumIndicesCount]; + _indexBuffer = new DynamicIndexBuffer(graphicsDevice, IndexElementSize.SixteenBits, maximumIndicesCount, BufferUsage.WriteOnly); + _geometryBuilder = new GeometryBuilder2D(4, 6); + } + + protected override void SortDrawCallsAndBindBuffers() + { + // Upload the vertices to the GPU and then select that vertex stream for drawing + _vertexBuffer.SetData(_vertices, 0, _vertexCount); + GraphicsDevice.SetVertexBuffer(_vertexBuffer); + + Array.Sort(DrawCalls, 0, EnqueuedDrawCallCount); + BuildSortedIndices(); + + // Upload the indices to the GPU and then select that index stream for drawing + _indexBuffer.SetData(_sortedIndices, 0, _indexCount); + GraphicsDevice.Indices = _indexBuffer; + + _indexCount = 0; + _vertexCount = 0; + } + + private void BuildSortedIndices() + { + var newDrawCallsCount = 0; + DrawCalls[0].StartIndex = 0; + var currentDrawCall = DrawCalls[0]; + DrawCalls[newDrawCallsCount++] = DrawCalls[0]; + + var drawCallIndexCount = currentDrawCall.PrimitiveCount * 3; + Array.Copy(_indices, currentDrawCall.StartIndex, _sortedIndices, 0, drawCallIndexCount); + var sortedIndexCount = drawCallIndexCount; + + // iterate through sorted draw calls checking if any can now be merged to reduce expensive draw calls to the graphics API + // this might need to be changed for next-gen graphics API (Vulkan, Metal, DirectX 12) where the draw calls are not so expensive + for (var i = 1; i < EnqueuedDrawCallCount; i++) + { + currentDrawCall = DrawCalls[i]; + drawCallIndexCount = currentDrawCall.PrimitiveCount * 3; + Array.Copy(_indices, currentDrawCall.StartIndex, _sortedIndices, sortedIndexCount, drawCallIndexCount); + sortedIndexCount += drawCallIndexCount; + + if (currentDrawCall.TryMerge(ref DrawCalls[newDrawCallsCount - 1])) + continue; + + currentDrawCall.StartIndex = sortedIndexCount; + DrawCalls[newDrawCallsCount++] = currentDrawCall; + } + + EnqueuedDrawCallCount = newDrawCallsCount; + } + + /// <summary> + /// Submits a draw operation to the <see cref="GraphicsDevice" /> using the specified <see cref="DrawCallInfo"/>. + /// </summary> + /// <param name="drawCall">The draw call information.</param> + protected override void InvokeDrawCall(ref DrawCallInfo drawCall) + { + GraphicsDevice.DrawIndexedPrimitives(drawCall.PrimitiveType, 0, drawCall.StartIndex, drawCall.PrimitiveCount); + } + + /// <summary> + /// Draws a sprite using a specified <see cref="Texture" />, transform <see cref="Matrix2" />, source + /// <see cref="Rectangle" />, and an optional + /// <see cref="Color" />, origin <see cref="Vector2" />, <see cref="FlipFlags" />, and depth <see cref="float" />. + /// </summary> + /// <param name="texture">The <see cref="Texture" />.</param> + /// <param name="transformMatrix">The transform <see cref="Matrix2" />.</param> + /// <param name="sourceRectangle"> + /// The texture region <see cref="Rectangle" /> of the <paramref name="texture" />. Use + /// <code>null</code> to use the entire <see cref="Texture2D" />. + /// </param> + /// <param name="color">The <see cref="Color" />. Use <code>null</code> to use the default <see cref="Color.White" />.</param> + /// <param name="flags">The <see cref="FlipFlags" />. The default value is <see cref="FlipFlags.None" />.</param> + /// <param name="depth">The depth <see cref="float" />. The default value is <code>0</code>.</param> + /// <exception cref="InvalidOperationException">The <see cref="Batcher{TDrawCallInfo}.Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> method has not been called.</exception> + /// <exception cref="ArgumentNullException"><paramref name="texture" /> is null.</exception> + public void DrawSprite(Texture2D texture, ref Matrix2 transformMatrix, ref Rectangle sourceRectangle, + Color? color = null, FlipFlags flags = FlipFlags.None, float depth = 0) + { + _geometryBuilder.BuildSprite(_vertexCount, ref transformMatrix, texture, ref sourceRectangle, color, flags, depth); + EnqueueBuiltGeometry(texture, depth); + } + + /// <summary> + /// Draws a <see cref="Texture" /> using the specified transform <see cref="Matrix2" /> and an optional + /// <see cref="Color" />, origin <see cref="Vector2" />, <see cref="FlipFlags" />, and depth <see cref="float" />. + /// </summary> + /// <param name="texture">The <see cref="Texture" />.</param> + /// <param name="transformMatrix">The transform <see cref="Matrix2" />.</param> + /// <param name="color">The <see cref="Color" />. Use <code>null</code> to use the default <see cref="Color.White" />.</param> + /// <param name="flags">The <see cref="FlipFlags" />. The default value is <see cref="FlipFlags.None" />.</param> + /// <param name="depth">The depth <see cref="float" />. The default value is <code>0</code>.</param> + /// <exception cref="InvalidOperationException">The <see cref="Batcher{TDrawCallInfo}.Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> method has not been called.</exception> + /// <exception cref="ArgumentNullException"><paramref name="texture" /> is null.</exception> + public void DrawTexture(Texture2D texture, ref Matrix2 transformMatrix, Color? color = null, + FlipFlags flags = FlipFlags.None, float depth = 0) + { + var rectangle = default(Rectangle); + _geometryBuilder.BuildSprite(_vertexCount, ref transformMatrix, texture, ref rectangle, color, flags, depth); + EnqueueBuiltGeometry(texture, depth); + } + + private void EnqueueBuiltGeometry(Texture2D texture, float depth) + { + if ((_vertexCount + _geometryBuilder.VertexCount > _vertices.Length) || + (_indexCount + _geometryBuilder.IndexCount > _indices.Length)) + Flush(); + var drawCall = new DrawCallInfo(texture, _geometryBuilder.PrimitiveType, _indexCount, + _geometryBuilder.PrimitivesCount, depth); + Array.Copy(_geometryBuilder.Vertices, 0, _vertices, _vertexCount, _geometryBuilder.VertexCount); + _vertexCount += _geometryBuilder.VertexCount; + Array.Copy(_geometryBuilder.Indices, 0, _indices, _indexCount, _geometryBuilder.IndexCount); + _indexCount += _geometryBuilder.IndexCount; + Enqueue(ref drawCall); + } + + /// <summary> + /// Draws unicode (UTF-16) characters as sprites using the specified <see cref="BitmapFont" />, text + /// <see cref="StringBuilder" />, transform <see cref="Matrix2" /> and optional <see cref="Color" />, origin + /// <see cref="Vector2" />, <see cref="FlipFlags" />, and depth <see cref="float" />. + /// </summary> + /// <param name="bitmapFont">The <see cref="BitmapFont" />.</param> + /// <param name="text">The text <see cref="StringBuilder" />.</param> + /// <param name="transformMatrix">The transform <see cref="Matrix2" />.</param> + /// <param name="color"> + /// The <see cref="Color" />. Use <code>null</code> to use the default + /// <see cref="Color.White" />. + /// </param> + /// <param name="flags">The <see cref="FlipFlags" />. The default value is <see cref="FlipFlags.None" />.</param> + /// <param name="depth">The depth <see cref="float" />. The default value is <code>0f</code>.</param> + /// <exception cref="InvalidOperationException">The <see cref="Batcher{TDrawCallInfo}.Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> method has not been called.</exception> + /// <exception cref="ArgumentNullException"><paramref name="bitmapFont" /> is null or <paramref name="text" /> is null.</exception> + public void DrawString(BitmapFont bitmapFont, StringBuilder text, ref Matrix2 transformMatrix, + Color? color = null, FlipFlags flags = FlipFlags.None, float depth = 0f) + { + EnsureHasBegun(); + + if (bitmapFont == null) + throw new ArgumentNullException(nameof(bitmapFont)); + + if (text == null) + throw new ArgumentNullException(nameof(text)); + + var lineSpacing = bitmapFont.LineHeight; + var offset = new Vector2(0, 0); + + BitmapFontRegion lastGlyph = null; + for (var i = 0; i < text.Length;) + { + int character; + if (char.IsLowSurrogate(text[i])) + { + character = char.ConvertToUtf32(text[i - 1], text[i]); + i += 2; + } + else if (char.IsHighSurrogate(text[i])) + { + character = char.ConvertToUtf32(text[i], text[i - 1]); + i += 2; + } + else + { + character = text[i]; + i += 1; + } + + // ReSharper disable once SwitchStatementMissingSomeCases + switch (character) + { + case '\r': + continue; + case '\n': + offset.X = 0; + offset.Y += lineSpacing; + lastGlyph = null; + continue; + } + + var fontRegion = bitmapFont.GetCharacterRegion(character); + if (fontRegion == null) + continue; + + var transform1Matrix = transformMatrix; + transform1Matrix.M31 += offset.X + fontRegion.XOffset; + transform1Matrix.M32 += offset.Y + fontRegion.YOffset; + + var textureRegion = fontRegion.TextureRegion; + var bounds = textureRegion.Bounds; + DrawSprite(textureRegion.Texture, ref transform1Matrix, ref bounds, color, flags, depth); + + var advance = fontRegion.XAdvance + bitmapFont.LetterSpacing; + if (BitmapFont.UseKernings && lastGlyph != null) + { + int amount; + if (lastGlyph.Kernings.TryGetValue(character, out amount)) + { + advance += amount; + } + } + + offset.X += i != text.Length - 1 + ? advance + : fontRegion.XOffset + fontRegion.Width; + + lastGlyph = fontRegion; + } + } + + /// <summary> + /// Draws unicode (UTF-16) characters as sprites using the specified <see cref="BitmapFont" />, text + /// <see cref="StringBuilder" />, position <see cref="Vector2" /> and optional <see cref="Color" />, rotation + /// <see cref="float" />, origin <see cref="Vector2" />, scale <see cref="Vector2" /> <see cref="FlipFlags" />, and + /// depth <see cref="float" />. + /// </summary> + /// <param name="bitmapFont">The <see cref="BitmapFont" />.</param> + /// <param name="text">The text <see cref="string" />.</param> + /// <param name="position">The position <see cref="Vector2" />.</param> + /// <param name="color"> + /// The <see cref="Color" />. Use <code>null</code> to use the default + /// <see cref="Color.White" />. + /// </param> + /// <param name="rotation"> + /// The angle <see cref="float" /> (in radians) to rotate each sprite about its <paramref name="origin" />. The default + /// value is <code>0f</code>. + /// </param> + /// <param name="origin"> + /// The origin <see cref="Vector2" />. Use <code>null</code> to use the default + /// <see cref="Vector2.Zero" />. + /// </param> + /// <param name="scale"> + /// The scale <see cref="Vector2" />. Use <code>null</code> to use the default + /// <see cref="Vector2.One" />. + /// </param> + /// <param name="flags">The <see cref="FlipFlags" />. The default value is <see cref="FlipFlags.None" />.</param> + /// <param name="depth">The depth <see cref="float" />. The default value is <code>0f</code></param> + /// <exception cref="InvalidOperationException">The <see cref="Batcher{TDrawCallInfo}.Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> method has not been called.</exception> + /// <exception cref="ArgumentNullException"><paramref name="bitmapFont" /> is null or <paramref name="text" /> is null.</exception> + public void DrawString(BitmapFont bitmapFont, StringBuilder text, Vector2 position, Color? color = null, + float rotation = 0f, Vector2? origin = null, Vector2? scale = null, + FlipFlags flags = FlipFlags.None, float depth = 0f) + { + Matrix2 transformMatrix; + Matrix2.CreateFrom(position, rotation, scale, origin, out transformMatrix); + DrawString(bitmapFont, text, ref transformMatrix, color, flags, depth); + } + + /// <summary> + /// Draws unicode (UTF-16) characters as sprites using the specified <see cref="BitmapFont" />, text + /// <see cref="string" />, transform <see cref="Matrix2" /> and optional <see cref="Color" />, origin + /// <see cref="Vector2" />, <see cref="FlipFlags" />, and depth <see cref="float" />. + /// </summary> + /// <param name="bitmapFont">The <see cref="BitmapFont" />.</param> + /// <param name="text">The text <see cref="string" />.</param> + /// <param name="transformMatrix">The transform <see cref="Matrix2" />.</param> + /// <param name="color"> + /// The <see cref="Color" />. Use <code>null</code> to use the default + /// <see cref="Color.White" />. + /// </param> + /// <param name="flags">The <see cref="FlipFlags" />. The default value is <see cref="FlipFlags.None" />.</param> + /// <param name="depth">The depth <see cref="float" />. The default value is <code>0f</code></param> + /// <exception cref="InvalidOperationException">The <see cref="Batcher{TDrawCallInfo}.Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> method has not been called.</exception> + /// <exception cref="ArgumentNullException"><paramref name="bitmapFont" /> is null or <paramref name="text" /> is null.</exception> + public void DrawString(BitmapFont bitmapFont, string text, ref Matrix2 transformMatrix, Color? color = null, + FlipFlags flags = FlipFlags.None, float depth = 0f) + { + EnsureHasBegun(); + + if (bitmapFont == null) + throw new ArgumentNullException(nameof(bitmapFont)); + + if (text == null) + throw new ArgumentNullException(nameof(text)); + + var glyphs = bitmapFont.GetGlyphs(text); + foreach (var glyph in glyphs) + { + var transform1Matrix = transformMatrix; + transform1Matrix.M31 += glyph.Position.X; + transform1Matrix.M32 += glyph.Position.Y; + + var texture = glyph.FontRegion.TextureRegion.Texture; + var bounds = texture.Bounds; + DrawSprite(texture, ref transform1Matrix, ref bounds, color, flags, depth); + } + } + + /// <summary> + /// Draws unicode (UTF-16) characters as sprites using the specified <see cref="BitmapFont" />, text + /// <see cref="string" />, position <see cref="Vector2" /> and optional <see cref="Color" />, rotation + /// <see cref="float" />, origin <see cref="Vector2" />, scale <see cref="Vector2" /> <see cref="FlipFlags" />, and + /// depth <see cref="float" />. + /// </summary> + /// <param name="bitmapFont">The <see cref="BitmapFont" />.</param> + /// <param name="text">The text <see cref="string" />.</param> + /// <param name="position">The position <see cref="Vector2" />.</param> + /// <param name="color"> + /// The <see cref="Color" />. Use <code>null</code> to use the default + /// <see cref="Color.White" />. + /// </param> + /// <param name="rotation"> + /// The angle <see cref="float" /> (in radians) to rotate each sprite about its <paramref name="origin" />. The default + /// value is <code>0f</code>. + /// </param> + /// <param name="origin"> + /// The origin <see cref="Vector2" />. Use <code>null</code> to use the default + /// <see cref="Vector2.Zero" />. + /// </param> + /// <param name="scale"> + /// The scale <see cref="Vector2" />. Use <code>null</code> to use the default + /// <see cref="Vector2.One" />. + /// </param> + /// <param name="flags">The <see cref="FlipFlags" />. The default value is <see cref="FlipFlags.None" />.</param> + /// <param name="depth">The depth <see cref="float" />. The default value is <code>0f</code></param> + /// <exception cref="InvalidOperationException">The <see cref="Batcher{TDrawCallInfo}.Begin(ref Matrix, ref Matrix, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect)" /> method has not been called.</exception> + /// <exception cref="ArgumentNullException"><paramref name="bitmapFont" /> is null or <paramref name="text" /> is null.</exception> + public void DrawString(BitmapFont bitmapFont, string text, Vector2 position, Color? color = null, + float rotation = 0f, Vector2? origin = null, Vector2? scale = null, + FlipFlags flags = FlipFlags.None, float depth = 0f) + { + Matrix2 matrix; + Matrix2.CreateFrom(position, rotation, scale, origin, out matrix); + DrawString(bitmapFont, text, ref matrix, color, flags, depth); + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + [EditorBrowsable(EditorBrowsableState.Never)] + public struct DrawCallInfo : IBatchDrawCallInfo<DrawCallInfo>, IComparable<DrawCallInfo> + { + internal readonly PrimitiveType PrimitiveType; + internal int StartIndex; + internal int PrimitiveCount; + internal readonly Texture2D Texture; + internal readonly uint TextureKey; + internal readonly uint DepthKey; + + internal unsafe DrawCallInfo(Texture2D texture, PrimitiveType primitiveType, int startIndex, int primitiveCount, float depth) + { + PrimitiveType = primitiveType; + StartIndex = startIndex; + PrimitiveCount = primitiveCount; + Texture = texture; + TextureKey = (uint)RuntimeHelpers.GetHashCode(texture); + DepthKey = *(uint*)&depth; + } + + public void SetState(Effect effect) + { + var textureEffect = effect as ITextureEffect; + if (textureEffect != null) + textureEffect.Texture = Texture; + } + + public bool TryMerge(ref DrawCallInfo drawCall) + { + if (PrimitiveType != drawCall.PrimitiveType || TextureKey != drawCall.TextureKey || + DepthKey != drawCall.DepthKey) + return false; + drawCall.PrimitiveCount += PrimitiveCount; + return true; + } + + [SuppressMessage("ReSharper", "ImpureMethodCallOnReadonlyValueField")] + public int CompareTo(DrawCallInfo other) + { + var result = TextureKey.CompareTo(other.TextureKey);; + if (result != 0) + return result; + result = DepthKey.CompareTo(other.DepthKey); + return result != 0 ? result : ((byte)PrimitiveType).CompareTo((byte)other.PrimitiveType); + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/DefaultEffect.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/DefaultEffect.cs new file mode 100644 index 0000000..df71e47 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/DefaultEffect.cs @@ -0,0 +1,203 @@ +using System.Collections.Specialized; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Graphics.Effects +{ + /// <summary> + /// An <see cref="Effect" /> that allows objects, within a 3D context, to be represented on a 2D monitor. + /// </summary> + /// <seealso cref="MatrixChainEffect" /> + /// <seealso cref="ITextureEffect" /> + public class DefaultEffect : MatrixChainEffect, ITextureEffect + { + /// <summary> + /// The bitmask for use with <see cref="MatrixChainEffect.Flags" /> indicating wether <see cref="Texture" /> has + /// changed in the last frame. + /// </summary> + protected static int DirtyTextureBitMask = BitVector32.CreateMask(UseDefaultProjectionBitMask); + + /// <summary> + /// The bitmask for use with <see cref="MatrixChainEffect.Flags" /> indicating wether the underlying vertex shader and + /// fragment (pixel) shaders have changed to one of the pre-defined shaders in the last frame. + /// </summary> + protected static int DirtyShaderIndexBitMask = BitVector32.CreateMask(DirtyTextureBitMask); + + /// <summary> + /// The bitmask for use with <see cref="MatrixChainEffect.Flags" /> indicating wether the material color has changed in + /// the last frame. + /// </summary> + public static int DirtyMaterialColorBitMask = BitVector32.CreateMask(DirtyShaderIndexBitMask); + + private Texture2D _texture; + private EffectParameter _textureParameter; + + private float _alpha = 1; + private Color _diffuseColor = Color.White; + private EffectParameter _diffuseColorParameter; + + private bool _textureEnabled; + private bool _vertexColorEnabled; + + /// <summary> + /// Gets or sets the material <see cref="Texture2D" />. + /// </summary> + /// <value> + /// The material <see cref="Texture2D" />. + /// </value> + public Texture2D Texture + { + get { return _texture; } + set + { + _texture = value; + Flags[DirtyTextureBitMask] = true; + } + } + + /// <summary> + /// Gets or sets the material color alpha. + /// </summary> + /// <remarks> + /// <para> + /// The alpha channel uses the premultiplied (associated) representation. This means that the RGB components of a + /// color represent + /// the color of the object of pixel, adjusted for its opacity by multiplication of <see cref="Alpha" />. + /// </para> + /// </remarks> + public float Alpha + { + get { return _alpha; } + + set + { + _alpha = value; + Flags[DirtyMaterialColorBitMask] = true; + } + } + + /// <summary> + /// Gets or sets whether texturing is enabled. + /// </summary> + public bool TextureEnabled + { + get { return _textureEnabled; } + + set + { + if (_textureEnabled == value) + return; + _textureEnabled = value; + Flags[DirtyShaderIndexBitMask] = true; + } + } + + /// <summary> + /// Gets or sets whether vertex color is enabled. + /// </summary> + public bool VertexColorEnabled + { + get { return _vertexColorEnabled; } + + set + { + if (_vertexColorEnabled == value) + return; + _vertexColorEnabled = value; + Flags[DirtyShaderIndexBitMask] = true; + } + } + + /// <summary> + /// Initializes a new instance of the <see cref="DefaultEffect" /> class. + /// </summary> + /// <param name="graphicsDevice">The graphics device.</param> + public DefaultEffect(GraphicsDevice graphicsDevice) + : base(graphicsDevice, EffectResource.DefaultEffect.Bytecode) + { + Initialize(); + } + + /// <summary> + /// Initializes a new instance of the <see cref="DefaultEffect" /> class. + /// </summary> + /// <param name="graphicsDevice">The graphics device.</param> + /// <param name="byteCode">The byte code of the shader program.</param> + public DefaultEffect(GraphicsDevice graphicsDevice, byte[] byteCode) + : base(graphicsDevice, byteCode) + { + Initialize(); + } + + /// <summary> + /// Initializes a new instance of the <see cref="DefaultEffect" /> class. + /// </summary> + /// <param name="cloneSource">The clone source.</param> + public DefaultEffect(Effect cloneSource) + : base(cloneSource) + { + Initialize(); + } + + private void Initialize() + { + Flags[DirtyMaterialColorBitMask] = true; + _textureParameter = Parameters["Texture"]; + _diffuseColorParameter = Parameters["DiffuseColor"]; + } + + /// <summary> + /// Computes derived parameter values immediately before applying the effect. + /// </summary> + protected override void OnApply() + { + base.OnApply(); + + if (Flags[DirtyTextureBitMask]) + { + _textureParameter.SetValue(_texture); + Flags[DirtyTextureBitMask] = false; + } + + // ReSharper disable once InvertIf + if (Flags[DirtyMaterialColorBitMask]) + { + UpdateMaterialColor(); + Flags[DirtyMaterialColorBitMask] = false; + } + + // ReSharper disable once InvertIf + if (Flags[DirtyShaderIndexBitMask]) + { + var shaderIndex = 0; + + if (_textureEnabled) + shaderIndex += 1; + + if (_vertexColorEnabled) + shaderIndex += 2; + + Flags[DirtyShaderIndexBitMask] = false; + CurrentTechnique = Techniques[shaderIndex]; + } + } + + /// <summary> + /// Updates the material color parameters associated with this <see cref="DefaultEffect" />. + /// </summary> + protected virtual void UpdateMaterialColor() + { + var diffuseColorVector3 = _diffuseColor.ToVector3(); + + var diffuseColorVector4 = new Vector4() + { + X = diffuseColorVector3.X * Alpha, + Y = diffuseColorVector3.Y * Alpha, + Z = diffuseColorVector3.Z * Alpha, + W = Alpha, + }; + + _diffuseColorParameter.SetValue(diffuseColorVector4); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/EffectResource.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/EffectResource.cs new file mode 100644 index 0000000..43bd535 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/EffectResource.cs @@ -0,0 +1,119 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Graphics.Effects +{ + /// <summary> + /// Reperesents the bytecode of an <see cref="Effect" /> that is encapsulated inside a compiled assembly. + /// </summary> + /// <remarks> + /// <para> + /// Files that are encapsulated inside a compiled assembly are commonly known as Manifiest or embedded resources. + /// Since embedded resources are added to the assembly at compiled time, they can not be accidentally deleted or + /// misplaced. However, if the file needs to be changed, the assembly will need to be re-compiled with the changed + /// file. + /// </para> + /// <para> + /// To add an embedded resource file to an assembly, first add it to the project and then change the Build Action + /// in the Properties of the file to <code>Embedded Resource</code>. The next time the project is compiled, the + /// compiler will add the file to the assembly as an embedded resource. The compiler adds namespace(s) to the + /// embedded resource so it matches with the path of where the file was added to the project. + /// </para> + /// </remarks> + public class EffectResource + { + private static EffectResource _defaultEffect; + private static string _shaderExtension; + + /// <summary> + /// Gets the <see cref="Effects.DefaultEffect" /> embedded into the MonoGame.Extended.Graphics library. + /// </summary> + public static EffectResource DefaultEffect => _defaultEffect ?? (_defaultEffect = new EffectResource($"MonoGame.Extended.Graphics.Effects.Resources.DefaultEffect.{_shaderExtension}.mgfxo")); + + static EffectResource() + { + DetermineShaderExtension(); + } + + private static void DetermineShaderExtension() + { + // use reflection to figure out if Shader.Profile is OpenGL (0) or DirectX (1), + // may need to be changed / fixed for future shader profiles + + var assembly = typeof(Game).GetTypeInfo().Assembly; + Debug.Assert(assembly != null); + + var shaderType = assembly.GetType("Microsoft.Xna.Framework.Graphics.Shader"); + Debug.Assert(shaderType != null); + var shaderTypeInfo = shaderType.GetTypeInfo(); + Debug.Assert(shaderTypeInfo != null); + + // https://github.com/MonoGame/MonoGame/blob/develop/MonoGame.Framework/Graphics/Shader/Shader.cs#L47 + var profileProperty = shaderTypeInfo.GetDeclaredProperty("Profile"); + var value = (int)profileProperty.GetValue(null); + + switch (value) + { + case 0: + // OpenGL + _shaderExtension = "ogl"; + break; + case 1: + // DirectX + _shaderExtension = "dx11"; + break; + default: + throw new InvalidOperationException("Unknown shader profile."); + } + } + + private readonly string _resourceName; + private volatile byte[] _bytecode; + private readonly Assembly _assembly; + + /// <summary> + /// Gets the bytecode of the <see cref="Effect" /> file. + /// </summary> + /// <value> + /// The bytecode of the <see cref="Effect" /> file. + /// </value> + public byte[] Bytecode + { + get + { + if (_bytecode != null) + return _bytecode; + + lock (this) + { + if (_bytecode != null) + return _bytecode; + + var stream = _assembly.GetManifestResourceStream(_resourceName); + using (var memoryStream = new MemoryStream()) + { + stream.CopyTo(memoryStream); + _bytecode = memoryStream.ToArray(); + } + } + + return _bytecode; + } + } + + /// <summary> + /// Initializes a new instance of the <see cref="EffectResource" /> class. + /// </summary> + /// <param name="resourceName">The name of the embedded resource. This must include the namespace(s).</param> + /// <param name="assembly">The assembly which the embedded resource is apart of.</param> + public EffectResource(string resourceName, Assembly assembly = null) + { + _resourceName = resourceName; + _assembly = assembly ?? typeof(EffectResource).GetTypeInfo().Assembly; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/ITextureEffect.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/ITextureEffect.cs new file mode 100644 index 0000000..ea2ef0b --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/ITextureEffect.cs @@ -0,0 +1,18 @@ +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Graphics.Effects +{ + /// <summary> + /// Defines an <see cref="Effect" /> that uses a <see cref="Texture2D" />. + /// </summary> + public interface ITextureEffect + { + /// <summary> + /// Gets or sets the <see cref="Texture2D" />. + /// </summary> + /// <value> + /// The <see cref="Texture2D" />. + /// </value> + Texture2D Texture { get; set; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/MatrixChainEffect.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/MatrixChainEffect.cs new file mode 100644 index 0000000..046479f --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/MatrixChainEffect.cs @@ -0,0 +1,155 @@ +using System.Collections.Specialized; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Graphics.Effects +{ + /// <summary> + /// An <see cref="Effect" /> that uses the standard chain of matrix transformations to represent a 3D object on a 2D + /// monitor. + /// </summary> + /// <seealso cref="Effect" /> + /// <seealso cref="IEffectMatrices" /> + public abstract class MatrixChainEffect : Effect, IMatrixChainEffect + { + /// <summary> + /// The bitmask for use with <see cref="Flags"/> indicating wether <see cref="World"/>, <see cref="View"/>, or <see cref="Projection"/> has changed in the last frame. + /// </summary> + protected static int DirtyWorldViewProjectionBitMask = BitVector32.CreateMask(); + + /// <summary> + /// The bitmask for use with <see cref="Flags"/> indicating wether to use a default projection matrix or a custom projection matrix. + /// </summary> + protected static int UseDefaultProjectionBitMask = BitVector32.CreateMask(DirtyWorldViewProjectionBitMask); + + /// <summary> + /// The dirty flags associated with this <see cref="MatrixChainEffect"/>. + /// </summary> + protected BitVector32 Flags; + + private Matrix _projection = Matrix.Identity; + private Matrix _view = Matrix.Identity; + private Matrix _world = Matrix.Identity; + private EffectParameter _matrixParameter; + + /// <summary> + /// Gets or sets the model-to-world <see cref="Matrix" />. + /// </summary> + /// <value> + /// The model-to-world <see cref="Matrix" />. + /// </value> + public Matrix World + { + get { return _world; } + set { SetWorld(ref value); } + } + + /// <summary> + /// Gets or sets the world-to-view <see cref="Matrix" />. + /// </summary> + /// <value> + /// The world-to-view <see cref="Matrix" />. + /// </value> + public Matrix View + { + get { return _view; } + set { SetView(ref value); } + } + + /// <summary> + /// Gets or sets the view-to-projection <see cref="Matrix" />. + /// </summary> + /// <value> + /// The view-to-projection <see cref="Matrix" />. + /// </value> + public Matrix Projection + { + get { return _projection; } + set { SetProjection(ref value); } + } + + /// <summary> + /// Initializes a new instance of the <see cref="MatrixChainEffect" /> class. + /// </summary> + /// <param name="graphicsDevice">The graphics device.</param> + /// <param name="byteCode">The effect code.</param> + protected MatrixChainEffect(GraphicsDevice graphicsDevice, byte[] byteCode) + : base(graphicsDevice, byteCode) + { + Initialize(); + } + + /// <summary> + /// Initializes a new instance of the <see cref="MatrixChainEffect" /> class. + /// </summary> + /// <param name="cloneSource">The clone source.</param> + protected MatrixChainEffect(Effect cloneSource) + : base(cloneSource) + { + Initialize(); + } + + private void Initialize() + { + Flags[UseDefaultProjectionBitMask] = true; + + _matrixParameter = Parameters["WorldViewProjection"]; + } + + /// <summary> + /// Sets the model-to-world <see cref="Matrix" />. + /// </summary> + /// <param name="world">The model-to-world <see cref="Matrix" />.</param> + public void SetWorld(ref Matrix world) + { + _world = world; + Flags[DirtyWorldViewProjectionBitMask] = true; + } + + /// <summary> + /// Sets the world-to-view <see cref="Matrix" />. + /// </summary> + /// <param name="view">The world-to-view <see cref="Matrix" />.</param> + public void SetView(ref Matrix view) + { + _view = view; + Flags[DirtyWorldViewProjectionBitMask] = true; + } + + /// <summary> + /// Sets the view-to-projection <see cref="Matrix" />. + /// </summary> + /// <param name="projection">The view-to-projection <see cref="Matrix" />.</param> + public void SetProjection(ref Matrix projection) + { + _projection = projection; + Flags[DirtyWorldViewProjectionBitMask] = true; + Flags[UseDefaultProjectionBitMask] = false; + } + + /// <summary> + /// Computes derived parameter values immediately before applying the effect. + /// </summary> + protected override void OnApply() + { + base.OnApply(); + + // ReSharper disable once InvertIf + if (Flags[DirtyWorldViewProjectionBitMask] || Flags[UseDefaultProjectionBitMask]) + { + if (Flags[UseDefaultProjectionBitMask]) + { + var viewport = GraphicsDevice.Viewport; + _projection = Matrix.CreateOrthographicOffCenter(0, viewport.Width, viewport.Height, 0, 0, -1); + } + + Matrix worldViewProjection; + Matrix.Multiply(ref _world, ref _view, out worldViewProjection); + Matrix.Multiply(ref worldViewProjection, ref _projection, out worldViewProjection); + _matrixParameter.SetValue(worldViewProjection); + + Flags[DirtyWorldViewProjectionBitMask] = false; + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.dx11.mgfxo b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.dx11.mgfxo Binary files differnew file mode 100644 index 0000000..b83b6ed --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.dx11.mgfxo diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.fx b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.fx new file mode 100644 index 0000000..30a772d --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.fx @@ -0,0 +1,72 @@ +#include "Macros.fxh" +#include "Structures.fxh" + +DECLARE_TEXTURE(Texture, 0); + +BEGIN_CONSTANTS + +float4 DiffuseColor = float4(1, 1, 1, 1); + +MATRIX_CONSTANTS + +float4x4 WorldViewProjection _vs(c0) _cb(c0); + +END_CONSTANTS + +VertexShaderOutputPosition VertexShaderFunctionPosition(VertexShaderInputPosition input) +{ + VertexShaderOutputPosition output; + output.Position = mul(input.Position, WorldViewProjection); + return output; +} + +float4 PixelShaderFunctionPosition(VertexShaderOutputPosition input) : SV_Target0 +{ + return DiffuseColor; +} + +VertexShaderOutputPositionTexture VertexShaderFunctionPositionTexture(VertexShaderInputPositionTexture input) +{ + VertexShaderOutputPositionTexture output; + output.Position = mul(input.Position, WorldViewProjection); + output.TextureCoordinate = input.TextureCoordinate; + return output; +} + +float4 PixelShaderFunctionPositionTexture(VertexShaderOutputPositionTexture input) : SV_Target0 +{ + return SAMPLE_TEXTURE(Texture, input.TextureCoordinate) * DiffuseColor; +} + +VertexShaderOutputPositionColor VertexShaderFunctionPositionColor(VertexShaderInputPositionColor input) +{ + VertexShaderOutputPositionColor output; + output.Position = mul(input.Position, WorldViewProjection); + output.Color = input.Color; + return output; +} + +float4 PixelShaderFunctionPositionColor(VertexShaderOutputPositionColor input) : SV_Target0 +{ + return input.Color * DiffuseColor; +} + +VertexShaderOutputPositionColorTexture VertexShaderFunctionPositionColorTexture(VertexShaderInputPositionColorTexture input) +{ + VertexShaderOutputPositionColorTexture output; + output.Position = mul(input.Position, WorldViewProjection); + output.Color = input.Color; + output.TextureCoordinate = input.TextureCoordinate; + return output; +} + +float4 PixelShaderFunctionPositionColorTexture(VertexShaderOutputPositionColorTexture input) : SV_Target0 +{ + float4 textureColor = SAMPLE_TEXTURE(Texture, input.TextureCoordinate); + return textureColor * input.Color * DiffuseColor; +} + +TECHNIQUE(Position, VertexShaderFunctionPosition, PixelShaderFunctionPosition); +TECHNIQUE(PositionTexture, VertexShaderFunctionPositionTexture, PixelShaderFunctionPositionTexture); +TECHNIQUE(PositionColor, VertexShaderFunctionPositionColor, PixelShaderFunctionPositionColor); +TECHNIQUE(PositionColorTexture, VertexShaderFunctionPositionColorTexture, PixelShaderFunctionPositionColorTexture);
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.ogl.mgfxo b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.ogl.mgfxo Binary files differnew file mode 100644 index 0000000..4f51d2a --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/DefaultEffect.ogl.mgfxo diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/Macros.fxh b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/Macros.fxh new file mode 100644 index 0000000..c253efe --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/Macros.fxh @@ -0,0 +1,60 @@ +//----------------------------------------------------------------------------- +// Macros.fxh +// +// Microsoft XNA Community Game Platform +// Copyright (C) Microsoft Corporation. All rights reserved. +//----------------------------------------------------------------------------- + +#if SM4 + +// Macros for targetting shader model 4.0 (DX11) + +#define TECHNIQUE(name, vsname, psname ) \ + technique name { pass { VertexShader = compile vs_4_0_level_9_1 vsname (); PixelShader = compile ps_4_0_level_9_1 psname(); } } + +#define BEGIN_CONSTANTS cbuffer Parameters : register(b0) { +#define MATRIX_CONSTANTS +#define END_CONSTANTS }; + +#define _vs(r) +#define _ps(r) +#define _cb(r) + +#define DECLARE_TEXTURE(Name, index) \ + Texture2D<float4> Name : register(t##index); \ + sampler Name##Sampler : register(s##index) + +#define DECLARE_CUBEMAP(Name, index) \ + TextureCube<float4> Name : register(t##index); \ + sampler Name##Sampler : register(s##index) + +#define SAMPLE_TEXTURE(Name, texCoord) Name.Sample(Name##Sampler, texCoord) +#define SAMPLE_CUBEMAP(Name, texCoord) Name.Sample(Name##Sampler, texCoord) + + +#else + +// Macros for targetting shader model 2.0 (DX9) + +#define TECHNIQUE(name, vsname, psname ) \ + technique name { pass { VertexShader = compile vs_2_0 vsname (); PixelShader = compile ps_2_0 psname(); } } + +#define BEGIN_CONSTANTS +#define MATRIX_CONSTANTS +#define END_CONSTANTS + +#define _vs(r) : register(vs, r) +#define _ps(r) : register(ps, r) +#define _cb(r) + +#define DECLARE_TEXTURE(Name, index) \ + sampler2D Name : register(s##index); + +#define DECLARE_CUBEMAP(Name, index) \ + samplerCUBE Name : register(s##index); + +#define SAMPLE_TEXTURE(Name, texCoord) tex2D(Name, texCoord) +#define SAMPLE_CUBEMAP(Name, texCoord) texCUBE(Name, texCoord) + + +#endif
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/RebuildEffects.bat b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/RebuildEffects.bat new file mode 100644 index 0000000..4fc7f21 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/RebuildEffects.bat @@ -0,0 +1,15 @@ +setlocal + +SET TWOMGFX="mgfxc" + +@for /f %%f IN ('dir /b *.fx') do ( + + call %TWOMGFX% %%~nf.fx %%~nf.ogl.mgfxo /Profile:OpenGL + + call %TWOMGFX% %%~nf.fx %%~nf.dx11.mgfxo /Profile:DirectX_11 + +) + +endlocal + +pause
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/Structures.fxh b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/Structures.fxh new file mode 100644 index 0000000..d3af6a0 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Effects/Resources/Structures.fxh @@ -0,0 +1,51 @@ +// Vertex shader input structures. + +struct VertexShaderInputPosition +{ + float4 Position : SV_Position; +}; + +struct VertexShaderInputPositionColor +{ + float4 Position : SV_Position; + float4 Color : COLOR; +}; + +struct VertexShaderInputPositionTexture +{ + float4 Position : SV_Position; + float2 TextureCoordinate : TEXCOORD0; +}; + +struct VertexShaderInputPositionColorTexture +{ + float4 Position : SV_Position; + float4 Color : COLOR; + float2 TextureCoordinate : TEXCOORD0; +}; + +// Vertex shader output structures. + +struct VertexShaderOutputPosition +{ + float4 Position : SV_Position; +}; + +struct VertexShaderOutputPositionColor +{ + float4 Position : SV_Position; + float4 Color : COLOR0; +}; + +struct VertexShaderOutputPositionTexture +{ + float4 Position : SV_Position; + float2 TextureCoordinate : TEXCOORD0; +}; + +struct VertexShaderOutputPositionColorTexture +{ + float4 Position : SV_Position; + float4 Color : COLOR0; + float2 TextureCoordinate : TEXCOORD0; +}; diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/FlipFlags.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/FlipFlags.cs new file mode 100644 index 0000000..6c19887 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/FlipFlags.cs @@ -0,0 +1,13 @@ +using System; + +namespace MonoGame.Extended.Graphics +{ + [Flags] + public enum FlipFlags : byte + { + None = 0, + FlipDiagonally = 1 << 0, + FlipVertically = 1 << 1, + FlipHorizontally = 1 << 2 + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Geometry/GeometryBuilder.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Geometry/GeometryBuilder.cs new file mode 100644 index 0000000..dad4f15 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Geometry/GeometryBuilder.cs @@ -0,0 +1,24 @@ +using System; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Graphics.Geometry +{ + public abstract class GeometryBuilder<TVertexType, TIndexType> + where TVertexType : struct, IVertexType + where TIndexType : struct + { + public PrimitiveType PrimitiveType { get; protected set; } + public int VertexCount { get; protected set; } + public int IndexCount { get; protected set; } + public int PrimitivesCount { get; protected set; } + + public TVertexType[] Vertices { get; } + public TIndexType[] Indices { get; } + + protected GeometryBuilder(int maximumVerticesCount, int maximumIndicesCount) + { + Vertices = new TVertexType[maximumVerticesCount]; + Indices = new TIndexType[maximumIndicesCount]; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Geometry/GeometryBuilder2D.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Geometry/GeometryBuilder2D.cs new file mode 100644 index 0000000..c113fbe --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/Geometry/GeometryBuilder2D.cs @@ -0,0 +1,119 @@ +using System; +using System.Runtime.CompilerServices; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Graphics.Geometry +{ + public class GeometryBuilder2D : GeometryBuilder<VertexPositionColorTexture, ushort> + { + public GeometryBuilder2D(int maximumVerticesCount, int maximumIndicesCount) + : base(maximumVerticesCount, maximumIndicesCount) + { + } + + public void BuildSprite(int indexOffset, ref Matrix2 transformMatrix, Texture2D texture, + ref Rectangle sourceRectangle, + Color? color = null, FlipFlags flags = FlipFlags.None, float depth = 0) + { + if (texture == null) + throw new ArgumentNullException(nameof(texture)); + + var texelLeft = 0f; + var texelTop = 0f; + var texelRight = 1f; + var texelBottom = 1f; + + if (sourceRectangle.Width > 0) + { + texelLeft = (float)sourceRectangle.X / texture.Width; + texelTop = (float)sourceRectangle.Y / texture.Height; + texelRight = (sourceRectangle.X + sourceRectangle.Width) / (float)texture.Width; + texelBottom = (sourceRectangle.Y + sourceRectangle.Height) / (float)texture.Height; + } + else + { + sourceRectangle.Width = texture.Width; + sourceRectangle.Height = texture.Height; + } + + var color1 = color ?? Color.White; + + var vertices = Vertices; + + transformMatrix.Transform(0, 0, ref vertices[0].Position); + vertices[0].Position.Z = depth; + vertices[0].Color = color1; + vertices[0].TextureCoordinate.X = texelLeft; + vertices[0].TextureCoordinate.Y = texelTop; + + transformMatrix.Transform(sourceRectangle.Width, 0, ref vertices[1].Position); + vertices[1].Position.Z = depth; + vertices[1].Color = color1; + vertices[1].TextureCoordinate.X = texelRight; + vertices[1].TextureCoordinate.Y = texelTop; + + transformMatrix.Transform(0, sourceRectangle.Height, ref vertices[2].Position); + vertices[2].Position.Z = depth; + vertices[2].Color = color1; + vertices[2].TextureCoordinate.X = texelLeft; + vertices[2].TextureCoordinate.Y = texelBottom; + + transformMatrix.Transform(sourceRectangle.Width, sourceRectangle.Height, ref vertices[3].Position); + vertices[3].Position.Z = depth; + vertices[3].Color = color1; + vertices[3].TextureCoordinate.X = texelRight; + vertices[3].TextureCoordinate.Y = texelBottom; + + var flipDiagonally = (flags & FlipFlags.FlipDiagonally) != 0; + var flipHorizontally = (flags & FlipFlags.FlipHorizontally) != 0; + var flipVertically = (flags & FlipFlags.FlipVertically) != 0; + + if (flipDiagonally) + { + FloatHelper.Swap(ref vertices[1].TextureCoordinate.X, ref vertices[2].TextureCoordinate.X); + FloatHelper.Swap(ref vertices[1].TextureCoordinate.Y, ref vertices[2].TextureCoordinate.Y); + } + + if (flipHorizontally) + if (flipDiagonally) + { + FloatHelper.Swap(ref vertices[0].TextureCoordinate.Y, ref vertices[1].TextureCoordinate.Y); + FloatHelper.Swap(ref vertices[2].TextureCoordinate.Y, ref vertices[3].TextureCoordinate.Y); + } + else + { + FloatHelper.Swap(ref vertices[0].TextureCoordinate.X, ref vertices[1].TextureCoordinate.X); + FloatHelper.Swap(ref vertices[2].TextureCoordinate.X, ref vertices[3].TextureCoordinate.X); + } + + if (flipVertically) + if (flipDiagonally) + { + FloatHelper.Swap(ref vertices[0].TextureCoordinate.X, ref vertices[2].TextureCoordinate.X); + FloatHelper.Swap(ref vertices[1].TextureCoordinate.X, ref vertices[3].TextureCoordinate.X); + } + else + { + FloatHelper.Swap(ref vertices[0].TextureCoordinate.Y, ref vertices[2].TextureCoordinate.Y); + FloatHelper.Swap(ref vertices[1].TextureCoordinate.Y, ref vertices[3].TextureCoordinate.Y); + } + + VertexCount = 4; + AddQuadrilateralIndices(indexOffset); + IndexCount = 6; + PrimitivesCount = 2; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddQuadrilateralIndices(int indexOffset) + { + Indices[0] = (ushort)(0 + indexOffset); + Indices[1] = (ushort)(1 + indexOffset); + Indices[2] = (ushort)(2 + indexOffset); + Indices[3] = (ushort)(1 + indexOffset); + Indices[4] = (ushort)(3 + indexOffset); + Indices[5] = (ushort)(2 + indexOffset); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/IBatchDrawCallInfo.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/IBatchDrawCallInfo.cs new file mode 100644 index 0000000..67c4755 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/IBatchDrawCallInfo.cs @@ -0,0 +1,19 @@ +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Graphics +{ + /// <summary> + /// Defines the for a deferred draw call when batching. + /// </summary> + public interface IBatchDrawCallInfo<TDrawCallInfo> where TDrawCallInfo : IBatchDrawCallInfo<TDrawCallInfo> + { + /// <summary> + /// Applies any state from the <see cref="IBatchDrawCallInfo{TDrawCallInfo}" /> to the + /// <see cref="Effect" /> or <see cref="Effect.GraphicsDevice"/>. + /// </summary> + /// <param name="effect">The effect.</param> + void SetState(Effect effect); + + bool TryMerge(ref TDrawCallInfo drawCall); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/IMatrixChainEffect.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/IMatrixChainEffect.cs new file mode 100644 index 0000000..da8ccb9 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/IMatrixChainEffect.cs @@ -0,0 +1,30 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Graphics +{ + /// <summary> + /// Defines an <see cref="Effect" /> that uses the standard chain of matrix transformations to represent a 3D object on + /// a 2D monitor. + /// </summary> + public interface IMatrixChainEffect : IEffectMatrices + { + /// <summary> + /// Sets the model-to-world <see cref="Matrix" />. + /// </summary> + /// <param name="world">The model-to-world <see cref="Matrix" />.</param> + void SetWorld(ref Matrix world); + + /// <summary> + /// Sets the world-to-view <see cref="Matrix" />. + /// </summary> + /// <param name="view">The world-to-view <see cref="Matrix" />.</param> + void SetView(ref Matrix view); + + /// <summary> + /// Sets the view-to-projection <see cref="Matrix" />. + /// </summary> + /// <param name="projection">The view-to-projection <see cref="Matrix" />.</param> + void SetProjection(ref Matrix projection); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/MonoGame.Extended.Graphics.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/MonoGame.Extended.Graphics.csproj new file mode 100644 index 0000000..fd25850 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/MonoGame.Extended.Graphics.csproj @@ -0,0 +1,28 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <Description>Graphics makes MonoGame more awesome.</Description> + <PackageTags>monogame graphics batcher effects</PackageTags> + </PropertyGroup> + + <ItemGroup> + <None Remove="Effects\Resources\DefaultEffect.dx11.mgfxo" /> + <None Remove="Effects\Resources\DefaultEffect.fx" /> + <None Remove="Effects\Resources\DefaultEffect.ogl.mgfxo" /> + <None Remove="Effects\Resources\Macros.fxh" /> + <None Remove="Effects\Resources\Structures.fxh" /> + </ItemGroup> + + <ItemGroup> + <EmbeddedResource Include="Effects\Resources\DefaultEffect.dx11.mgfxo" /> + <EmbeddedResource Include="Effects\Resources\DefaultEffect.fx" /> + <EmbeddedResource Include="Effects\Resources\DefaultEffect.ogl.mgfxo" /> + <EmbeddedResource Include="Effects\Resources\Macros.fxh" /> + <EmbeddedResource Include="Effects\Resources\Structures.fxh" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" /> + </ItemGroup> + +</Project> diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/PrimitiveTypeExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/PrimitiveTypeExtensions.cs new file mode 100644 index 0000000..68a39ef --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/PrimitiveTypeExtensions.cs @@ -0,0 +1,42 @@ +using System; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Graphics +{ + public static class PrimitiveTypeExtensions + { + internal static int GetPrimitivesCount(this PrimitiveType primitiveType, int verticesCount) + { + switch (primitiveType) + { + case PrimitiveType.LineStrip: + return verticesCount - 1; + case PrimitiveType.LineList: + return verticesCount/2; + case PrimitiveType.TriangleStrip: + return verticesCount - 2; + case PrimitiveType.TriangleList: + return verticesCount/3; + default: + throw new ArgumentException("Invalid primitive type."); + } + } + + internal static int GetVerticesCount(this PrimitiveType primitiveType, int primitivesCount) + { + switch (primitiveType) + { + case PrimitiveType.LineStrip: + return primitivesCount + 1; + case PrimitiveType.LineList: + return primitivesCount*2; + case PrimitiveType.TriangleStrip: + return primitivesCount + 2; + case PrimitiveType.TriangleList: + return primitivesCount*3; + default: + throw new ArgumentException("Invalid primitive type."); + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/RenderTarget2DExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/RenderTarget2DExtensions.cs new file mode 100644 index 0000000..f090603 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Graphics/RenderTarget2DExtensions.cs @@ -0,0 +1,41 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Graphics +{ + public static class RenderTarget2DExtensions + { + public static IDisposable BeginDraw(this RenderTarget2D renderTarget, GraphicsDevice graphicsDevice, + Color backgroundColor) + { + return new RenderTargetOperation(renderTarget, graphicsDevice, backgroundColor); + } + + private class RenderTargetOperation : IDisposable + { + private readonly GraphicsDevice _graphicsDevice; + private readonly RenderTargetUsage _previousRenderTargetUsage; + private readonly Viewport _viewport; + + public RenderTargetOperation(RenderTarget2D renderTarget, GraphicsDevice graphicsDevice, + Color backgroundColor) + { + _graphicsDevice = graphicsDevice; + _viewport = _graphicsDevice.Viewport; + _previousRenderTargetUsage = _graphicsDevice.PresentationParameters.RenderTargetUsage; + + _graphicsDevice.PresentationParameters.RenderTargetUsage = RenderTargetUsage.PreserveContents; + _graphicsDevice.SetRenderTarget(renderTarget); + _graphicsDevice.Clear(backgroundColor); + } + + public void Dispose() + { + _graphicsDevice.SetRenderTarget(null); + _graphicsDevice.PresentationParameters.RenderTargetUsage = _previousRenderTargetUsage; + _graphicsDevice.Viewport = _viewport; + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ControlStyle.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ControlStyle.cs new file mode 100644 index 0000000..1593da3 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ControlStyle.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using MonoGame.Extended.Gui.Controls; + +namespace MonoGame.Extended.Gui +{ + public class ControlStyle : IDictionary<string, object> + { + private readonly Dictionary<Guid, Dictionary<string, object>> _previousStates = new Dictionary<Guid, Dictionary<string, object>>(); + + public ControlStyle() + : this(typeof(Element)) + { + } + + public ControlStyle(Type targetType) + : this(targetType.FullName, targetType) + { + } + + public ControlStyle(string name, Type targetType) + { + Name = name; + TargetType = targetType; + _setters = new Dictionary<string, object>(); + } + + public string Name { get; } + public Type TargetType { get; set; } + + private readonly Dictionary<string, object> _setters; + + public void ApplyIf(Control control, bool predicate) + { + if (predicate) + Apply(control); + else + Revert(control); + } + + public void Apply(Control control) + { + _previousStates[control.Id] = _setters + .ToDictionary(i => i.Key, i => TargetType.GetRuntimeProperty(i.Key)?.GetValue(control)); + + ChangePropertyValues(control, _setters); + } + + public void Revert(Control control) + { + if (_previousStates.ContainsKey(control.Id) && _previousStates[control.Id] != null) + ChangePropertyValues(control, _previousStates[control.Id]); + + _previousStates[control.Id] = null; + } + + private static void ChangePropertyValues(Control control, Dictionary<string, object> setters) + { + var targetType = control.GetType(); + + foreach (var propertyName in setters.Keys) + { + var propertyInfo = targetType.GetRuntimeProperty(propertyName); + var value = setters[propertyName]; + + if (propertyInfo != null) + { + if(propertyInfo.CanWrite) + propertyInfo.SetValue(control, value); + + // special case when we have a list of items as objects (like on a list box) + if (propertyInfo.PropertyType == typeof(List<object>)) + { + var items = (List<object>)value; + var addMethod = propertyInfo.PropertyType.GetRuntimeMethod("Add", new[] { typeof(object) }); + + foreach (var item in items) + addMethod.Invoke(propertyInfo.GetValue(control), new[] {item}); + } + } + } + } + + public object this[string key] + { + get { return _setters[key]; } + set { _setters[key] = value; } + } + + public ICollection<string> Keys => _setters.Keys; + public ICollection<object> Values => _setters.Values; + public int Count => _setters.Count; + public bool IsReadOnly => false; + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public IEnumerator<KeyValuePair<string, object>> GetEnumerator() => _setters.GetEnumerator(); + public void Add(string key, object value) => _setters.Add(key, value); + public void Add(KeyValuePair<string, object> item) => _setters.Add(item.Key, item.Value); + public bool Remove(string key) => _setters.Remove(key); + public bool Remove(KeyValuePair<string, object> item) => _setters.Remove(item.Key); + public void Clear() => _setters.Clear(); + public bool Contains(KeyValuePair<string, object> item) => _setters.Contains(item); + public bool ContainsKey(string key) => _setters.ContainsKey(key); + public bool TryGetValue(string key, out object value) => _setters.TryGetValue(key, out value); + + public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex) + { + throw new NotSupportedException(); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Box.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Box.cs new file mode 100644 index 0000000..1e5c06e --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Box.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Gui.Controls +{ + public class Box : Control + { + public override IEnumerable<Control> Children { get; } = Enumerable.Empty<Control>(); + + public override Size GetContentSize(IGuiContext context) + { + return new Size(Width, Height); + } + + public Color FillColor { get; set; } = Color.White; + + public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds) + { + base.Draw(context, renderer, deltaSeconds); + renderer.FillRectangle(ContentRectangle, FillColor); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Button.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Button.cs new file mode 100644 index 0000000..310dc00 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Button.cs @@ -0,0 +1,92 @@ +using System; + +namespace MonoGame.Extended.Gui.Controls +{ + public class Button : ContentControl + { + public Button() + { + } + + public event EventHandler Clicked; + public event EventHandler PressedStateChanged; + + private bool _isPressed; + public bool IsPressed + { + get => _isPressed; + set + { + if (_isPressed != value) + { + _isPressed = value; + PressedStyle?.ApplyIf(this, _isPressed); + PressedStateChanged?.Invoke(this, EventArgs.Empty); + } + } + } + + private ControlStyle _pressedStyle; + public ControlStyle PressedStyle + { + get => _pressedStyle; + set + { + if (_pressedStyle != value) + { + _pressedStyle = value; + PressedStyle?.ApplyIf(this, _isPressed); + } + } + } + + private bool _isPointerDown; + + public override bool OnPointerDown(IGuiContext context, PointerEventArgs args) + { + if (IsEnabled) + { + _isPointerDown = true; + IsPressed = true; + } + + return base.OnPointerDown(context, args); + } + + public override bool OnPointerUp(IGuiContext context, PointerEventArgs args) + { + _isPointerDown = false; + + if (IsPressed) + { + IsPressed = false; + + if (BoundingRectangle.Contains(args.Position) && IsEnabled) + Click(); + } + + return base.OnPointerUp(context, args); + } + + public override bool OnPointerEnter(IGuiContext context, PointerEventArgs args) + { + if (IsEnabled && _isPointerDown) + IsPressed = true; + + return base.OnPointerEnter(context, args); + } + + public override bool OnPointerLeave(IGuiContext context, PointerEventArgs args) + { + if (IsEnabled) + IsPressed = false; + + return base.OnPointerLeave(context, args); + } + + public void Click() + { + Clicked?.Invoke(this, EventArgs.Empty); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Canvas.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Canvas.cs new file mode 100644 index 0000000..d667a73 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Canvas.cs @@ -0,0 +1,25 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Gui.Controls +{ + public class Canvas : LayoutControl + { + public Canvas() + { + } + + protected override void Layout(IGuiContext context, Rectangle rectangle) + { + foreach (var control in Items) + { + var actualSize = control.CalculateActualSize(context); + PlaceControl(context, control, control.Position.X, control.Position.Y, actualSize.Width, actualSize.Height); + } + } + + public override Size GetContentSize(IGuiContext context) + { + return new Size(); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CheckBox.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CheckBox.cs new file mode 100644 index 0000000..c6c375f --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CheckBox.cs @@ -0,0 +1,65 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Gui.Controls +{ + public class CheckBox : CompositeControl + { + public CheckBox() + { + _contentLabel = new Label(); + _checkLabel = new Box {Width = 20, Height = 20}; + + _toggleButton = new ToggleButton + { + Margin = 0, + Padding = 0, + BackgroundColor = Color.Transparent, + BorderThickness = 0, + HoverStyle = null, + CheckedStyle = null, + PressedStyle = null, + Content = new StackPanel + { + Margin = 0, + Orientation = Orientation.Horizontal, + Items = + { + _checkLabel, + _contentLabel + } + } + }; + + _toggleButton.CheckedStateChanged += (sender, args) => OnIsCheckedChanged(); + Template = _toggleButton; + OnIsCheckedChanged(); + } + + private readonly Label _contentLabel; + private readonly ToggleButton _toggleButton; + private readonly Box _checkLabel; + + protected override Control Template { get; } + + public override object Content + { + get => _contentLabel.Content; + set => _contentLabel.Content = value; + } + + public bool IsChecked + { + get => _toggleButton.IsChecked; + set + { + _toggleButton.IsChecked = value; + OnIsCheckedChanged(); + } + } + + private void OnIsCheckedChanged() + { + _checkLabel.FillColor = IsChecked ? Color.White : Color.Transparent; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ComboBox.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ComboBox.cs new file mode 100644 index 0000000..f8867d2 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ComboBox.cs @@ -0,0 +1,102 @@ +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; +using MonoGame.Extended.Input.InputListeners; +using MonoGame.Extended.TextureAtlases; + +namespace MonoGame.Extended.Gui.Controls +{ + public class ComboBox : SelectorControl + { + public ComboBox() + { + } + + public bool IsOpen { get; set; } + public TextureRegion2D DropDownRegion { get; set; } + public Color DropDownColor { get; set; } = Color.White; + + public override bool OnKeyPressed(IGuiContext context, KeyboardEventArgs args) + { + if (args.Key == Keys.Enter) + IsOpen = false; + + return base.OnKeyPressed(context, args); + } + + public override bool OnPointerUp(IGuiContext context, PointerEventArgs args) + { + IsOpen = !IsOpen; + return base.OnPointerUp(context, args); + } + + protected override Rectangle GetListAreaRectangle(IGuiContext context) + { + return GetDropDownRectangle(context); + } + + public override bool Contains(IGuiContext context, Point point) + { + return base.Contains(context, point) || IsOpen && GetListAreaRectangle(context).Contains(point); + } + + public override Size GetContentSize(IGuiContext context) + { + var width = 0; + var height = 0; + + foreach (var item in Items) + { + var itemSize = GetItemSize(context, item); + + if (itemSize.Width > width) + width = itemSize.Width; + + if (itemSize.Height > height) + height = itemSize.Height; + } + + return new Size(width + ClipPadding.Width, height + ClipPadding.Height); + } + + public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds) + { + base.Draw(context, renderer, deltaSeconds); + + if (IsOpen) + { + var dropDownRectangle = GetListAreaRectangle(context); + + if (DropDownRegion != null) + { + renderer.DrawRegion(DropDownRegion, dropDownRectangle, DropDownColor); + } + else + { + renderer.FillRectangle(dropDownRectangle, DropDownColor); + renderer.DrawRectangle(dropDownRectangle, BorderColor); + } + + DrawItemList(context, renderer); + } + + var selectedTextInfo = GetItemTextInfo(context, ContentRectangle, SelectedItem); + + if (!string.IsNullOrWhiteSpace(selectedTextInfo.Text)) + renderer.DrawText(selectedTextInfo.Font, selectedTextInfo.Text, selectedTextInfo.Position + TextOffset, selectedTextInfo.Color, selectedTextInfo.ClippingRectangle); + } + + private Rectangle GetDropDownRectangle(IGuiContext context) + { + var dropDownRectangle = BoundingRectangle; + + dropDownRectangle.Y = dropDownRectangle.Y + dropDownRectangle.Height; + dropDownRectangle.Height = (int) Items + .Select(item => GetItemSize(context, item)) + .Select(itemSize => itemSize.Height) + .Sum(); + + return dropDownRectangle; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CompositeControl.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CompositeControl.cs new file mode 100644 index 0000000..8217c69 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/CompositeControl.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Gui.Controls +{ + public abstract class CompositeControl : Control + { + protected CompositeControl() + { + } + + protected bool IsDirty { get; set; } = true; + + public abstract object Content { get; set; } + + protected abstract Control Template { get; } + + public override IEnumerable<Control> Children + { + get + { + var control = Template; + + if (control != null) + yield return control; + } + } + + public override void InvalidateMeasure() + { + base.InvalidateMeasure(); + IsDirty = true; + } + + public override Size GetContentSize(IGuiContext context) + { + var control = Template; + + if (control != null) + return Template.CalculateActualSize(context); + + return Size.Empty; + } + + public override void Update(IGuiContext context, float deltaSeconds) + { + var control = Template; + + if (control != null) + { + if (IsDirty) + { + control.Parent = this; + control.ActualSize = ContentRectangle.Size; + control.Position = new Point(Padding.Left, Padding.Top); + control.InvalidateMeasure(); + IsDirty = false; + } + } + } + + public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds) + { + base.Draw(context, renderer, deltaSeconds); + + var control = Template; + control?.Draw(context, renderer, deltaSeconds); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ContentControl.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ContentControl.cs new file mode 100644 index 0000000..932f8e4 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ContentControl.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Gui.Controls +{ + public class ContentControl : Control + { + private bool _contentChanged = true; + + private object _content; + public object Content + { + get => _content; + set + { + if (_content != value) + { + _content = value; + _contentChanged = true; + } + } + } + + public override IEnumerable<Control> Children + { + get + { + if (Content is Control control) + yield return control; + } + } + + public bool HasContent => Content == null; + + public override void InvalidateMeasure() + { + base.InvalidateMeasure(); + _contentChanged = true; + } + + public override void Update(IGuiContext context, float deltaSeconds) + { + if (_content is Control control && _contentChanged) + { + control.Parent = this; + control.ActualSize = ContentRectangle.Size; + control.Position = new Point(Padding.Left, Padding.Top); + control.InvalidateMeasure(); + _contentChanged = false; + } + } + + public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds) + { + base.Draw(context, renderer, deltaSeconds); + + if (Content is Control control) + { + control.Draw(context, renderer, deltaSeconds); + } + else + { + var text = Content?.ToString(); + var textInfo = GetTextInfo(context, text, ContentRectangle, HorizontalTextAlignment, VerticalTextAlignment); + + if (!string.IsNullOrWhiteSpace(textInfo.Text)) + renderer.DrawText(textInfo.Font, textInfo.Text, textInfo.Position + TextOffset, textInfo.Color, textInfo.ClippingRectangle); + } + } + + public override Size GetContentSize(IGuiContext context) + { + if (Content is Control control) + return control.CalculateActualSize(context); + + var text = Content?.ToString(); + var font = Font ?? context.DefaultFont; + return (Size)font.MeasureString(text ?? string.Empty); + } + } + +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Control.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Control.cs new file mode 100644 index 0000000..5440fdf --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Control.cs @@ -0,0 +1,271 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Text.Json.Serialization; +using Microsoft.Xna.Framework; +using MonoGame.Extended.BitmapFonts; +using MonoGame.Extended.Input.InputListeners; + +namespace MonoGame.Extended.Gui.Controls +{ + public abstract class Control : Element<Control>, IRectangular + { + protected Control() + { + BackgroundColor = Color.White; + TextColor = Color.White; + IsEnabled = true; + IsVisible = true; + Origin = Point.Zero; + Skin = Skin.Default; + } + + private Skin _skin; + + [EditorBrowsable(EditorBrowsableState.Never)] + public Skin Skin + { + get => _skin; + set + { + if (_skin != value) + { + _skin = value; + _skin?.Apply(this); + } + } + } + + public abstract IEnumerable<Control> Children { get; } + + [EditorBrowsable(EditorBrowsableState.Never)] + public Thickness Margin { get; set; } + + [EditorBrowsable(EditorBrowsableState.Never)] + public Thickness Padding { get; set; } + + [EditorBrowsable(EditorBrowsableState.Never)] + public Thickness ClipPadding { get; set; } + + public bool IsLayoutRequired { get; set; } + + public Rectangle ClippingRectangle + { + get + { + var r = BoundingRectangle; + return new Rectangle(r.Left + ClipPadding.Left, r.Top + ClipPadding.Top, r.Width - ClipPadding.Width, r.Height - ClipPadding.Height); + } + } + + public Rectangle ContentRectangle + { + get + { + var r = BoundingRectangle; + return new Rectangle(r.Left + Padding.Left, r.Top + Padding.Top, r.Width - Padding.Width, r.Height - Padding.Height); + } + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsFocused { get; set; } + + private bool _isHovered; + + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsHovered + { + get => _isHovered; + private set + { + if (_isHovered != value) + { + _isHovered = value; + HoverStyle?.ApplyIf(this, _isHovered); + } + } + } + + [JsonIgnore] + [EditorBrowsable(EditorBrowsableState.Never)] + public Guid Id { get; set; } = Guid.NewGuid(); + + public Vector2 Offset { get; set; } + public BitmapFont Font { get; set; } + public Color TextColor { get; set; } + public Vector2 TextOffset { get; set; } + public HorizontalAlignment HorizontalAlignment { get; set; } = HorizontalAlignment.Stretch; + public VerticalAlignment VerticalAlignment { get; set; } = VerticalAlignment.Stretch; + public HorizontalAlignment HorizontalTextAlignment { get; set; } = HorizontalAlignment.Centre; + public VerticalAlignment VerticalTextAlignment { get; set; } = VerticalAlignment.Centre; + + public abstract Size GetContentSize(IGuiContext context); + + public virtual Size CalculateActualSize(IGuiContext context) + { + var fixedSize = Size; + var desiredSize = GetContentSize(context) + Margin.Size + Padding.Size; + + if (desiredSize.Width < MinWidth) + desiredSize.Width = MinWidth; + + if (desiredSize.Height < MinHeight) + desiredSize.Height = MinHeight; + + if (desiredSize.Width > MaxWidth) + desiredSize.Width = MaxWidth; + + if (desiredSize.Height > MaxWidth) + desiredSize.Height = MaxHeight; + + var width = fixedSize.Width == 0 ? desiredSize.Width : fixedSize.Width; + var height = fixedSize.Height == 0 ? desiredSize.Height : fixedSize.Height; + return new Size(width, height); + } + + public virtual void InvalidateMeasure() { } + + private bool _isEnabled; + public bool IsEnabled + { + get => _isEnabled; + set + { + if (_isEnabled != value) + { + _isEnabled = value; + DisabledStyle?.ApplyIf(this, !_isEnabled); + } + } + } + + public bool IsVisible { get; set; } + + private ControlStyle _hoverStyle; + public ControlStyle HoverStyle + { + get => _hoverStyle; + set + { + if (_hoverStyle != value) + { + _hoverStyle = value; + HoverStyle?.ApplyIf(this, _isHovered); + } + } + } + + private ControlStyle _disabledStyle; + public ControlStyle DisabledStyle + { + get => _disabledStyle; + set + { + _disabledStyle = value; + DisabledStyle?.ApplyIf(this, !_isEnabled); + } + } + + public virtual void OnScrolled(int delta) { } + + public virtual bool OnKeyTyped(IGuiContext context, KeyboardEventArgs args) { return true; } + public virtual bool OnKeyPressed(IGuiContext context, KeyboardEventArgs args) { return true; } + + public virtual bool OnFocus(IGuiContext context) { return true; } + public virtual bool OnUnfocus(IGuiContext context) { return true; } + + public virtual bool OnPointerDown(IGuiContext context, PointerEventArgs args) { return true; } + public virtual bool OnPointerMove(IGuiContext context, PointerEventArgs args) { return true; } + public virtual bool OnPointerUp(IGuiContext context, PointerEventArgs args) { return true; } + + public virtual bool OnPointerEnter(IGuiContext context, PointerEventArgs args) + { + if (IsEnabled && !IsHovered) + IsHovered = true; + + return true; + } + + public virtual bool OnPointerLeave(IGuiContext context, PointerEventArgs args) + { + if (IsEnabled && IsHovered) + IsHovered = false; + + return true; + } + + public virtual bool Contains(IGuiContext context, Point point) + { + return BoundingRectangle.Contains(point); + } + + public virtual void Update(IGuiContext context, float deltaSeconds) { } + + public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds) + { + if (BackgroundRegion != null) + renderer.DrawRegion(BackgroundRegion, BoundingRectangle, BackgroundColor); + else if (BackgroundColor != Color.Transparent) + renderer.FillRectangle(BoundingRectangle, BackgroundColor); + + if (BorderThickness != 0) + renderer.DrawRectangle(BoundingRectangle, BorderColor, BorderThickness); + + // handy debug rectangles + //renderer.DrawRectangle(ContentRectangle, Color.Magenta); + //renderer.DrawRectangle(BoundingRectangle, Color.Lime); + } + + public bool HasParent(Control control) + { + return Parent != null && (Parent == control || Parent.HasParent(control)); + } + + protected TextInfo GetTextInfo(IGuiContext context, string text, Rectangle targetRectangle, HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment) + { + var font = Font ?? context.DefaultFont; + var textSize = (Size)font.GetStringRectangle(text ?? string.Empty, Vector2.Zero).Size; + var destinationRectangle = LayoutHelper.AlignRectangle(horizontalAlignment, verticalAlignment, textSize, targetRectangle); + var textPosition = destinationRectangle.Location.ToVector2(); + var textInfo = new TextInfo(text, font, textPosition, textSize, TextColor, targetRectangle); + return textInfo; + } + + public struct TextInfo + { + public TextInfo(string text, BitmapFont font, Vector2 position, Size size, Color color, Rectangle? clippingRectangle) + { + Text = text ?? string.Empty; + Font = font; + Size = size; + Color = color; + ClippingRectangle = clippingRectangle; + Position = position; + } + + public string Text; + public BitmapFont Font; + public Size Size; + public Color Color; + public Rectangle? ClippingRectangle; + public Vector2 Position; + } + + public Dictionary<string, object> AttachedProperties { get; } = new Dictionary<string, object>(); + + public object GetAttachedProperty(string name) + { + return AttachedProperties.TryGetValue(name, out var value) ? value : null; + } + + public void SetAttachedProperty(string name, object value) + { + AttachedProperties[name] = value; + } + + public virtual Type GetAttachedPropertyType(string propertyName) + { + return null; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ControlCollection.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ControlCollection.cs new file mode 100644 index 0000000..6135b4c --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ControlCollection.cs @@ -0,0 +1,15 @@ +namespace MonoGame.Extended.Gui.Controls +{ + public class ControlCollection : ElementCollection<Control, Control> + { + public ControlCollection() + : base(null) + { + } + + public ControlCollection(Control parent) + : base(parent) + { + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Dialog.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Dialog.cs new file mode 100644 index 0000000..26f0fca --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Dialog.cs @@ -0,0 +1,41 @@ +using System.Linq; + +namespace MonoGame.Extended.Gui.Controls +{ + //public class Dialog : LayoutControl + //{ + // public Dialog() + // { + // HorizontalAlignment = HorizontalAlignment.Centre; + // VerticalAlignment = VerticalAlignment.Centre; + // } + + // public Thickness Padding { get; set; } + // public Screen Owner { get; private set; } + + // public void Show(Screen owner) + // { + // Owner = owner; + // Owner.Controls.Add(this); + // } + + // public void Hide() + // { + // Owner.Controls.Remove(this); + // } + + // protected override Size2 CalculateDesiredSize(IGuiContext context, Size2 availableSize) + // { + // var sizes = Items.Select(control => LayoutHelper.GetSizeWithMargins(control, context, availableSize)).ToArray(); + // var width = sizes.Max(s => s.Width); + // var height = sizes.Max(s => s.Height); + // return new Size2(width, height) + Padding.Size; + // } + + // public override void Layout(IGuiContext context, RectangleF rectangle) + // { + // foreach (var control in Items) + // PlaceControl(context, control, Padding.Left, Padding.Top, Width - Padding.Size.Width, Height - Padding.Size.Height); + // } + //} +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/DockPanel.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/DockPanel.cs new file mode 100644 index 0000000..7dfe1e6 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/DockPanel.cs @@ -0,0 +1,104 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Gui.Controls +{ + public enum Dock + { + Left, Right, Top, Bottom + } + + public class DockPanel : LayoutControl + { + public override Size GetContentSize(IGuiContext context) + { + var size = new Size(); + + for (var i = 0; i < Items.Count; i++) + { + var control = Items[i]; + var actualSize = control.CalculateActualSize(context); + + if (LastChildFill && i == Items.Count - 1) + { + size.Width += actualSize.Width; + size.Height += actualSize.Height; + } + else + { + var dock = control.GetAttachedProperty(DockProperty) as Dock? ?? Dock.Left; + + switch (dock) + { + case Dock.Left: + case Dock.Right: + size.Width += actualSize.Width; + break; + case Dock.Top: + case Dock.Bottom: + size.Height += actualSize.Height; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + return size; + } + + protected override void Layout(IGuiContext context, Rectangle rectangle) + { + for (var i = 0; i < Items.Count; i++) + { + var control = Items[i]; + + if (LastChildFill && i == Items.Count - 1) + { + PlaceControl(context, control, rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height); + } + else + { + var actualSize = control.CalculateActualSize(context); + var dock = control.GetAttachedProperty(DockProperty) as Dock? ?? Dock.Left; + + switch (dock) + { + case Dock.Left: + PlaceControl(context, control, rectangle.Left, rectangle.Top, actualSize.Width, rectangle.Height); + rectangle.X += actualSize.Width; + rectangle.Width -= actualSize.Width; + break; + case Dock.Right: + PlaceControl(context, control, rectangle.Right - actualSize.Width, rectangle.Top, actualSize.Width, rectangle.Height); + rectangle.Width -= actualSize.Width; + break; + case Dock.Top: + PlaceControl(context, control, rectangle.Left, rectangle.Top, rectangle.Width, actualSize.Height); + rectangle.Y += actualSize.Height; + rectangle.Height -= actualSize.Height; + break; + case Dock.Bottom: + PlaceControl(context, control, rectangle.Left, rectangle.Bottom - actualSize.Height, rectangle.Width, actualSize.Height); + rectangle.Height -= actualSize.Height; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + } + + public const string DockProperty = "Dock"; + + public override Type GetAttachedPropertyType(string propertyName) + { + if (string.Equals(DockProperty, propertyName, StringComparison.OrdinalIgnoreCase)) + return typeof(Dock); + + return base.GetAttachedPropertyType(propertyName); + } + + public bool LastChildFill { get; set; } = true; + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Form.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Form.cs new file mode 100644 index 0000000..97b984d --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Form.cs @@ -0,0 +1,42 @@ +using System.Linq; +using MonoGame.Extended.Input.InputListeners; +using Microsoft.Xna.Framework.Input; + +namespace MonoGame.Extended.Gui.Controls +{ + //public class Form : StackPanel + //{ + // public Form() + // { + // } + + // public override bool OnKeyPressed(IGuiContext context, KeyboardEventArgs args) + // { + // if (args.Key == Keys.Tab) + // { + // var controls = FindControls<Control>(); + // var index = controls.IndexOf(context.FocusedControl); + // if (index > -1) + // { + // index++; + // if (index >= controls.Count) index = 0; + // context.SetFocus(controls[index]); + // return true; + // } + // } + + // if (args.Key == Keys.Enter) + // { + // var controls = FindControls<Submit>(); + // if (controls.Count > 0) + // { + // var submit = controls.FirstOrDefault(); + // submit.TriggerClicked(); + // return true; + // } + // } + + // return base.OnKeyPressed(context, args); + // } + //} +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ItemsControl.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ItemsControl.cs new file mode 100644 index 0000000..f0babaf --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ItemsControl.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; + +namespace MonoGame.Extended.Gui.Controls +{ + public abstract class ItemsControl : Control + { + protected ItemsControl() + { + Items = new ControlCollection(this); + //{ + // ItemAdded = x => UpdateRootIsLayoutRequired(), + // ItemRemoved = x => UpdateRootIsLayoutRequired() + //}; + } + + public override IEnumerable<Control> Children => Items; + + public ControlCollection Items { get; } + + ///// <summary> + ///// Recursive Method to find the root element and update the IsLayoutRequired property. So that the screen knows that something in the controls + ///// have had a change to their layout. Also, it will reset the size of the element so that it can get a clean build so that the background patches + ///// can be rendered with the updates. + ///// </summary> + //private void UpdateRootIsLayoutRequired() + //{ + // var parent = Parent as ItemsControl; + + // if (parent == null) + // IsLayoutRequired = true; + // else + // parent.UpdateRootIsLayoutRequired(); + + // Size = Size2.Empty; + //} + + //protected List<T> FindControls<T>() + // where T : Control + //{ + // return FindControls<T>(Items); + //} + + //protected List<T> FindControls<T>(ControlCollection controls) + // where T : Control + //{ + // var results = new List<T>(); + // foreach (var control in controls) + // { + // if (control is T) + // results.Add(control as T); + + // var itemsControl = control as ItemsControl; + + // if (itemsControl != null && itemsControl.Items.Any()) + // results = results.Concat(FindControls<T>(itemsControl.Items)).ToList(); + // } + // return results; + //} + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Label.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Label.cs new file mode 100644 index 0000000..b949148 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/Label.cs @@ -0,0 +1,14 @@ +namespace MonoGame.Extended.Gui.Controls +{ + public class Label : ContentControl + { + public Label() + { + } + + public Label(string text = null) + { + Content = text ?? string.Empty; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/LayoutControl.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/LayoutControl.cs new file mode 100644 index 0000000..3935baa --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/LayoutControl.cs @@ -0,0 +1,40 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Gui.Controls +{ + public abstract class LayoutControl : ItemsControl + { + protected LayoutControl() + { + HorizontalAlignment = HorizontalAlignment.Stretch; + VerticalAlignment = VerticalAlignment.Stretch; + BackgroundColor = Color.Transparent; + } + + private bool _isLayoutValid; + + public override void InvalidateMeasure() + { + base.InvalidateMeasure(); + _isLayoutValid = false; + } + + public override void Update(IGuiContext context, float deltaSeconds) + { + base.Update(context, deltaSeconds); + + if (!_isLayoutValid) + { + Layout(context, new Rectangle(Padding.Left, Padding.Top, ContentRectangle.Width, ContentRectangle.Height)); + _isLayoutValid = true; + } + } + + protected abstract void Layout(IGuiContext context, Rectangle rectangle); + + protected static void PlaceControl(IGuiContext context, Control control, float x, float y, float width, float height) + { + LayoutHelper.PlaceControl(context, control, x, y, width, height); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ListBox.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ListBox.cs new file mode 100644 index 0000000..d456e07 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ListBox.cs @@ -0,0 +1,42 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Gui.Controls +{ + public class ListBox : SelectorControl + { + public ListBox() + { + } + + public override Size GetContentSize(IGuiContext context) + { + var width = 0; + var height = 0; + + foreach (var item in Items) + { + var itemSize = GetItemSize(context, item); + + if (itemSize.Width > width) + width = itemSize.Width; + + height += itemSize.Height; + } + + return new Size(width + ClipPadding.Width, height + ClipPadding.Height); + } + + public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds) + { + base.Draw(context, renderer, deltaSeconds); + + ScrollIntoView(context); + DrawItemList(context, renderer); + } + + protected override Rectangle GetListAreaRectangle(IGuiContext context) + { + return ContentRectangle; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ProgressBar.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ProgressBar.cs new file mode 100644 index 0000000..6983d09 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ProgressBar.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using MonoGame.Extended.TextureAtlases; + +namespace MonoGame.Extended.Gui.Controls +{ + public class ProgressBar : Control + { + public ProgressBar() + { + } + + private float _progress = 1.0f; + public float Progress + { + get { return _progress; } + set + { + // ReSharper disable once CompareOfFloatsByEqualityOperator + if(_progress != value) + { + _progress = value; + ProgressChanged?.Invoke(this, EventArgs.Empty); + } + } + } + + public TextureRegion2D BarRegion { get; set; } + public Color BarColor { get; set; } = Color.White; + + public event EventHandler ProgressChanged; + + public override IEnumerable<Control> Children { get; } = Enumerable.Empty<Control>(); + + public override Size GetContentSize(IGuiContext context) + { + return new Size(5, 5); + } + + public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds) + { + base.Draw(context, renderer, deltaSeconds); + + var boundingRectangle = ContentRectangle; + var clippingRectangle = new Rectangle(boundingRectangle.X, boundingRectangle.Y, (int)(boundingRectangle.Width * Progress), boundingRectangle.Height); + + if (BarRegion != null) + renderer.DrawRegion(BarRegion, BoundingRectangle, BarColor, clippingRectangle); + else + renderer.FillRectangle(BoundingRectangle, BarColor, clippingRectangle); + } + + //protected override void DrawBackground(IGuiContext context, IGuiRenderer renderer, float deltaSeconds) + //{ + // base.DrawBackground(context, renderer, deltaSeconds); + + + //} + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/SelectorControl.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/SelectorControl.cs new file mode 100644 index 0000000..49b39ba --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/SelectorControl.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; +using MonoGame.Extended.Input.InputListeners; + +namespace MonoGame.Extended.Gui.Controls +{ + public abstract class SelectorControl : Control + { + protected SelectorControl() + { + } + + private int _selectedIndex = -1; + public virtual int SelectedIndex + { + get { return _selectedIndex; } + set + { + if (_selectedIndex != value) + { + _selectedIndex = value; + SelectedIndexChanged?.Invoke(this, EventArgs.Empty); + } + } + } + + public override IEnumerable<Control> Children => Items.OfType<Control>(); + + public virtual List<object> Items { get; } = new List<object>(); + public virtual Color SelectedTextColor { get; set; } = Color.White; + public virtual Color SelectedItemColor { get; set; } = Color.CornflowerBlue; + public virtual Thickness ItemPadding { get; set; } = new Thickness(4, 2); + public virtual string NameProperty { get; set; } + + public event EventHandler SelectedIndexChanged; + + protected int FirstIndex; + + public object SelectedItem + { + get { return SelectedIndex >= 0 && SelectedIndex <= Items.Count - 1 ? Items[SelectedIndex] : null; } + set { SelectedIndex = Items.IndexOf(value); } + } + + public override bool OnKeyPressed(IGuiContext context, KeyboardEventArgs args) + { + if (args.Key == Keys.Down) ScrollDown(); + if (args.Key == Keys.Up) ScrollUp(); + + return base.OnKeyPressed(context, args); + } + + public override void OnScrolled(int delta) + { + base.OnScrolled(delta); + + if (delta < 0) ScrollDown(); + if (delta > 0) ScrollUp(); + } + + private void ScrollDown() + { + if (SelectedIndex < Items.Count - 1) + SelectedIndex++; + } + + private void ScrollUp() + { + if (SelectedIndex > 0) + SelectedIndex--; + } + + public override bool OnPointerDown(IGuiContext context, PointerEventArgs args) + { + var contentRectangle = GetListAreaRectangle(context); + + for (var i = FirstIndex; i < Items.Count; i++) + { + var itemRectangle = GetItemRectangle(context, i - FirstIndex, contentRectangle); + + if (itemRectangle.Contains(args.Position)) + { + SelectedIndex = i; + OnItemClicked(context, args); + break; + } + } + + return base.OnPointerDown(context, args); + } + + protected virtual void OnItemClicked(IGuiContext context, PointerEventArgs args) { } + + protected TextInfo GetItemTextInfo(IGuiContext context, Rectangle itemRectangle, object item) + { + var textRectangle = new Rectangle(itemRectangle.X + ItemPadding.Left, itemRectangle.Y + ItemPadding.Top, + itemRectangle.Width - ItemPadding.Right, itemRectangle.Height - ItemPadding.Bottom); + var itemTextInfo = GetTextInfo(context, GetItemName(item), textRectangle, HorizontalTextAlignment, VerticalTextAlignment); + return itemTextInfo; + } + + private string GetItemName(object item) + { + if (item != null) + { + if (NameProperty != null) + { + return item.GetType() + .GetRuntimeProperty(NameProperty) + .GetValue(item) + ?.ToString() ?? string.Empty; + } + + return item.ToString(); + } + + return string.Empty; + } + + protected Rectangle GetItemRectangle(IGuiContext context, int index, Rectangle contentRectangle) + { + var font = Font ?? context.DefaultFont; + var itemHeight = font.LineHeight + ItemPadding.Top + ItemPadding.Bottom; + return new Rectangle(contentRectangle.X, contentRectangle.Y + itemHeight * index, contentRectangle.Width, itemHeight); + } + + protected void ScrollIntoView(IGuiContext context) + { + var contentRectangle = GetListAreaRectangle(context); + var selectedItemRectangle = GetItemRectangle(context, SelectedIndex - FirstIndex, contentRectangle); + + if (selectedItemRectangle.Bottom > ClippingRectangle.Bottom) + FirstIndex++; + + if (selectedItemRectangle.Top < ClippingRectangle.Top && FirstIndex > 0) + FirstIndex--; + } + + protected Size GetItemSize(IGuiContext context, object item) + { + var text = GetItemName(item); + var font = Font ?? context.DefaultFont; + var textSize = (Size)font.MeasureString(text ?? string.Empty); + var itemWidth = textSize.Width + ItemPadding.Width; + var itemHeight = textSize.Height + ItemPadding.Height; + return new Size(itemWidth, itemHeight); + } + + protected abstract Rectangle GetListAreaRectangle(IGuiContext context); + + protected void DrawItemList(IGuiContext context, IGuiRenderer renderer) + { + var listRectangle = GetListAreaRectangle(context); + + for (var i = FirstIndex; i < Items.Count; i++) + { + var item = Items[i]; + var itemRectangle = GetItemRectangle(context, i - FirstIndex, listRectangle); + var itemTextInfo = GetItemTextInfo(context, itemRectangle, item); + var textColor = i == SelectedIndex ? SelectedTextColor : itemTextInfo.Color; + + if (SelectedIndex == i) + renderer.FillRectangle(itemRectangle, SelectedItemColor, listRectangle); + + renderer.DrawText(itemTextInfo.Font, itemTextInfo.Text, itemTextInfo.Position + TextOffset, textColor, itemTextInfo.ClippingRectangle); + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/StackPanel.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/StackPanel.cs new file mode 100644 index 0000000..5d8926d --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/StackPanel.cs @@ -0,0 +1,69 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Gui.Controls +{ + public class StackPanel : LayoutControl + { + public StackPanel() + { + } + + public Orientation Orientation { get; set; } = Orientation.Vertical; + public int Spacing { get; set; } + + public override Size GetContentSize(IGuiContext context) + { + var width = 0; + var height = 0; + + foreach (var control in Items) + { + var actualSize = control.CalculateActualSize(context); + + switch (Orientation) + { + case Orientation.Horizontal: + width += actualSize.Width; + height = actualSize.Height > height ? actualSize.Height : height; + break; + case Orientation.Vertical: + width = actualSize.Width > width ? actualSize.Width : width; + height += actualSize.Height; + break; + default: + throw new InvalidOperationException($"Unexpected orientation {Orientation}"); + } + } + + width += Orientation == Orientation.Horizontal ? (Items.Count - 1) * Spacing : 0; + height += Orientation == Orientation.Vertical ? (Items.Count - 1) * Spacing : 0; + + return new Size(width, height); + } + + protected override void Layout(IGuiContext context, Rectangle rectangle) + { + foreach (var control in Items) + { + var actualSize = control.CalculateActualSize(context); + + switch (Orientation) + { + case Orientation.Vertical: + PlaceControl(context, control, rectangle.X, rectangle.Y, rectangle.Width, actualSize.Height); + rectangle.Y += actualSize.Height + Spacing; + rectangle.Height -= actualSize.Height; + break; + case Orientation.Horizontal: + PlaceControl(context, control, rectangle.X, rectangle.Y, actualSize.Width, rectangle.Height); + rectangle.X += actualSize.Width + Spacing; + rectangle.Width -= actualSize.Width; + break; + default: + throw new InvalidOperationException($"Unexpected orientation {Orientation}"); + } + } + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox.cs new file mode 100644 index 0000000..75b6940 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox.cs @@ -0,0 +1,331 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; +using MonoGame.Extended.Input.InputListeners; + +namespace MonoGame.Extended.Gui.Controls +{ + public sealed class TextBox : Control + { + public TextBox(string text = null) + { + Text = text ?? string.Empty; + HorizontalTextAlignment = HorizontalAlignment.Left; + } + + public TextBox() + : this(null) + { + } + + public int SelectionStart { get; set; } + public char? PasswordCharacter { get; set; } + + private string _text; + + public string Text + { + get { return _text; } + set + { + if (_text != value) + { + _text = value; + OnTextChanged(); + } + } + } + + private void OnTextChanged() + { + if (!string.IsNullOrEmpty(Text) && SelectionStart > Text.Length) + SelectionStart = Text.Length; + + TextChanged?.Invoke(this, EventArgs.Empty); + } + + public event EventHandler TextChanged; + + public override IEnumerable<Control> Children { get; } = Enumerable.Empty<Control>(); + + public override Size GetContentSize(IGuiContext context) + { + var font = Font ?? context.DefaultFont; + var stringSize = (Size) font.MeasureString(Text ?? string.Empty); + return new Size(stringSize.Width, + stringSize.Height < font.LineHeight ? font.LineHeight : stringSize.Height); + } + + //protected override Size2 CalculateDesiredSize(IGuiContext context, Size2 availableSize) + //{ + // var font = Font ?? context.DefaultFont; + // return new Size2(Width + Padding.Left + Padding.Right, (Height <= 0.0f ? font.LineHeight + 2 : Height) + Padding.Top + Padding.Bottom); + //} + + public override bool OnPointerDown(IGuiContext context, PointerEventArgs args) + { + SelectionStart = FindNearestGlyphIndex(context, args.Position); + _isCaretVisible = true; + + //_selectionIndexes.Clear(); + //_selectionIndexes.Push(SelectionStart); + //_startSelectionBox = Text.Length > 0; + + return base.OnPointerDown(context, args); + } + + //public override bool OnPointerMove(IGuiContext context, PointerEventArgs args) + //{ + // if (_startSelectionBox) + // { + // var selection = FindNearestGlyphIndex(context, args.Position); + // if (selection != _selectionIndexes.Peek()) + // { + // if (_selectionIndexes.Count == 1) + // { + // _selectionIndexes.Push(selection); + // } + // else if (_selectionIndexes.Last() < _selectionIndexes.Peek()) + // { + // if (selection > _selectionIndexes.Peek()) _selectionIndexes.Pop(); + // else _selectionIndexes.Push(selection); + // } + // else + // { + // if (selection < _selectionIndexes.Peek()) _selectionIndexes.Pop(); + // else _selectionIndexes.Push(selection); + // } + // SelectionStart = selection; + // } + // } + + // return base.OnPointerMove(context, args); + //} + + //public override bool OnPointerLeave(IGuiContext context, PointerEventArgs args) + //{ + // _startSelectionBox = false; + + // return base.OnPointerLeave(context, args); + //} + + //public override bool OnPointerUp(IGuiContext context, PointerEventArgs args) + //{ + // _startSelectionBox = false; + + // return base.OnPointerUp(context, args); + //} + + private int FindNearestGlyphIndex(IGuiContext context, Point position) + { + var font = Font ?? context.DefaultFont; + var textInfo = GetTextInfo(context, Text, ContentRectangle, HorizontalTextAlignment, VerticalTextAlignment); + var i = 0; + + foreach (var glyph in font.GetGlyphs(textInfo.Text, textInfo.Position)) + { + var fontRegionWidth = glyph.FontRegion?.Width ?? 0; + var glyphMiddle = (int) (glyph.Position.X + fontRegionWidth * 0.5f); + + if (position.X >= glyphMiddle) + { + i++; + continue; + } + + return i; + } + + return i; + } + + public override bool OnKeyPressed(IGuiContext context, KeyboardEventArgs args) + { + switch (args.Key) + { + case Keys.Tab: + case Keys.Enter: + return true; + case Keys.Back: + if (Text.Length > 0) + { + if (SelectionStart > 0) // && _selectionIndexes.Count <= 1) + { + SelectionStart--; + Text = Text.Remove(SelectionStart, 1); + } + //else + //{ + // var start = MathHelper.Min(_selectionIndexes.Last(), _selectionIndexes.Peek()); + // var end = MathHelper.Max(_selectionIndexes.Last(), _selectionIndexes.Peek()); + // Text = Text.Remove(start, end - start); + + // _selectionIndexes.Clear(); + //} + } + + break; + case Keys.Delete: + if (SelectionStart < Text.Length) // && _selectionIndexes.Count <= 1) + { + Text = Text.Remove(SelectionStart, 1); + } + //else if (_selectionIndexes.Count > 1) + //{ + // var start = MathHelper.Min(_selectionIndexes.Last(), _selectionIndexes.Peek()); + // var end = MathHelper.Max(_selectionIndexes.Last(), _selectionIndexes.Peek()); + // Text = Text.Remove(start, end - start); + // SelectionStart = 0; // yeah, nah. + + // _selectionIndexes.Clear(); + //} + break; + case Keys.Left: + if (SelectionStart > 0) + { + //if (_selectionIndexes.Count > 1) + //{ + // if (_selectionIndexes.Last() < SelectionStart) SelectionStart = _selectionIndexes.Last(); + // _selectionIndexes.Clear(); + //} + //else + { + SelectionStart--; + } + } + + break; + case Keys.Right: + if (SelectionStart < Text.Length) + { + //if (_selectionIndexes.Count > 1) + //{ + // if (_selectionIndexes.Last() > SelectionStart) SelectionStart = _selectionIndexes.Last(); + // _selectionIndexes.Clear(); + //} + //else + { + SelectionStart++; + } + } + + break; + case Keys.Home: + SelectionStart = 0; + //_selectionIndexes.Clear(); + break; + case Keys.End: + SelectionStart = Text.Length; + //_selectionIndexes.Clear(); + break; + default: + if (args.Character != null) + { + //if (_selectionIndexes.Count > 1) + //{ + // var start = MathHelper.Min(_selectionIndexes.Last(), _selectionIndexes.Peek()); + // var end = MathHelper.Max(_selectionIndexes.Last(), _selectionIndexes.Peek()); + // Text = Text.Remove(start, end - start); + + // _selectionIndexes.Clear(); + //} + + Text = Text.Insert(SelectionStart, args.Character.ToString()); + SelectionStart++; + } + + break; + } + + _isCaretVisible = true; + return base.OnKeyPressed(context, args); + } + + private const float _caretBlinkRate = 0.53f; + private float _nextCaretBlink = _caretBlinkRate; + private bool _isCaretVisible = true; + + //private bool _startSelectionBox = false; + //private Stack<int> _selectionIndexes = new Stack<int>(); + + public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds) + { + base.Draw(context, renderer, deltaSeconds); + + var text = PasswordCharacter.HasValue ? new string(PasswordCharacter.Value, Text.Length) : Text; + var textInfo = GetTextInfo(context, text, ContentRectangle, HorizontalTextAlignment, VerticalTextAlignment); + + if (!string.IsNullOrWhiteSpace(textInfo.Text)) + renderer.DrawText(textInfo.Font, textInfo.Text, textInfo.Position + TextOffset, textInfo.Color, + textInfo.ClippingRectangle); + + if (IsFocused) + { + var caretRectangle = GetCaretRectangle(textInfo, SelectionStart); + + if (_isCaretVisible) + renderer.DrawRectangle(caretRectangle, TextColor); + + _nextCaretBlink -= deltaSeconds; + + if (_nextCaretBlink <= 0) + { + _isCaretVisible = !_isCaretVisible; + _nextCaretBlink = _caretBlinkRate; + } + + //if (_selectionIndexes.Count > 1) + //{ + // var start = 0; + // var end = 0; + // var point = Point2.Zero; + // if (_selectionIndexes.Last() > _selectionIndexes.Peek()) + // { + // start = _selectionIndexes.Peek(); + // end = _selectionIndexes.Last(); + // point = caretRectangle.Position; + // } + // else + // { + // start = _selectionIndexes.Last(); + // end = _selectionIndexes.Peek(); + // point = GetCaretRectangle(textInfo, start).Position; + // } + // var selectionRectangle = textInfo.Font.GetStringRectangle(textInfo.Text.Substring(start, end - start), point); + + // renderer.FillRectangle((Rectangle)selectionRectangle, Color.Black * 0.25f); + //} + } + } + + + //protected override string CreateBoxText(string text, BitmapFont font, float width) + //{ + // return text; + //} + + private Rectangle GetCaretRectangle(TextInfo textInfo, int index) + { + var caretRectangle = textInfo.Font.GetStringRectangle(textInfo.Text.Substring(0, index), textInfo.Position); + + // TODO: Finish the caret position stuff when it's outside the clipping rectangle + if (caretRectangle.Right > ClippingRectangle.Right) + textInfo.Position.X -= caretRectangle.Right - ClippingRectangle.Right; + + caretRectangle.X = caretRectangle.Right < ClippingRectangle.Right + ? caretRectangle.Right + : ClippingRectangle.Right; + caretRectangle.Width = 1; + + if (caretRectangle.Left < ClippingRectangle.Left) + { + textInfo.Position.X += ClippingRectangle.Left - caretRectangle.Left; + caretRectangle.X = ClippingRectangle.Left; + } + + return (Rectangle) caretRectangle; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox2.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox2.cs new file mode 100644 index 0000000..e20b621 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/TextBox2.cs @@ -0,0 +1,328 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; +using MonoGame.Extended.Input.InputListeners; + +namespace MonoGame.Extended.Gui.Controls +{ + public class TextBox2 : Control + { + public TextBox2() + : this(null) + { + } + + public TextBox2(string text) + { + Text = text ?? string.Empty; + HorizontalTextAlignment = HorizontalAlignment.Left; + VerticalTextAlignment = VerticalAlignment.Top; + } + + + private const float _caretBlinkRate = 0.53f; + private float _nextCaretBlink = _caretBlinkRate; + private bool _isCaretVisible = true; + + private readonly List<StringBuilder> _lines = new List<StringBuilder>(); + public string Text + { + get => string.Concat(_lines.SelectMany(s => $"{s}\n")); + set + { + _lines.Clear(); + + var line = new StringBuilder(); + + for (var i = 0; i < value.Length; i++) + { + var c = value[i]; + + if (c == '\n') + { + _lines.Add(line); + line = new StringBuilder(); + } + else if(c != '\r') + { + line.Append(c); + } + } + + _lines.Add(line); + } + } + + public int CaretIndex => ColumnIndex * LineIndex + ColumnIndex; + public int LineIndex { get; set; } + public int ColumnIndex { get; set; } + public int TabStops { get; set; } = 4; + + public override IEnumerable<Control> Children => Enumerable.Empty<Control>(); + + public string GetLineText(int lineIndex) => _lines[lineIndex].ToString(); + public int GetLineLength(int lineIndex) => _lines[lineIndex].Length; + + public override Size GetContentSize(IGuiContext context) + { + var font = Font ?? context.DefaultFont; + var stringSize = (Size)font.MeasureString(Text ?? string.Empty); + return new Size(stringSize.Width, stringSize.Height < font.LineHeight ? font.LineHeight : stringSize.Height); + } + + public override bool OnKeyPressed(IGuiContext context, KeyboardEventArgs args) + { + switch (args.Key) + { + case Keys.Tab: + Tab(); + break; + case Keys.Back: + Backspace(); + break; + case Keys.Delete: + Delete(); + break; + case Keys.Left: + Left(); + break; + case Keys.Right: + Right(); + break; + case Keys.Up: + Up(); + break; + case Keys.Down: + Down(); + break; + case Keys.Home: + Home(); + break; + case Keys.End: + End(); + break; + case Keys.Enter: + Type('\n'); + return true; + default: + if (args.Character.HasValue) + Type(args.Character.Value); + + break; + } + + _isCaretVisible = true; + return base.OnKeyPressed(context, args); + } + + public void Type(char c) + { + switch (c) + { + case '\n': + var lineText = GetLineText(LineIndex); + var left = lineText.Substring(0, ColumnIndex); + var right = lineText.Substring(ColumnIndex); + _lines.Insert(LineIndex + 1, new StringBuilder(right)); + _lines[LineIndex] = new StringBuilder(left); + LineIndex++; + Home(); + break; + case '\t': + Tab(); + break; + default: + _lines[LineIndex].Insert(ColumnIndex, c); + ColumnIndex++; + break; + } + } + + public void Backspace() + { + if (ColumnIndex == 0 && LineIndex > 0) + { + var topLineLength = GetLineLength(LineIndex - 1); + + if (RemoveLineBreak(LineIndex - 1)) + { + LineIndex--; + ColumnIndex = topLineLength; + } + } + else if (Left()) + { + RemoveCharacter(LineIndex, ColumnIndex); + } + } + + public void Delete() + { + var lineLength = GetLineLength(LineIndex); + + if (ColumnIndex == lineLength) + RemoveLineBreak(LineIndex); + else + RemoveCharacter(LineIndex, ColumnIndex); + } + + public void RemoveCharacter(int lineIndex, int columnIndex) + { + _lines[lineIndex].Remove(columnIndex, 1); + } + + public bool RemoveLineBreak(int lineIndex) + { + if (lineIndex < _lines.Count - 1) + { + var topLine = _lines[lineIndex]; + var bottomLine = _lines[lineIndex + 1]; + _lines.RemoveAt(lineIndex + 1); + topLine.Append(bottomLine); + return true; + } + + return false; + } + + public bool Home() + { + ColumnIndex = 0; + return true; + } + + public bool End() + { + ColumnIndex = GetLineLength(LineIndex); + return true; + } + + public bool Up() + { + if (LineIndex > 0) + { + LineIndex--; + + if (ColumnIndex > GetLineLength(LineIndex)) + ColumnIndex = GetLineLength(LineIndex); + + return true; + } + + return false; + } + + public bool Down() + { + if (LineIndex < _lines.Count - 1) + { + LineIndex++; + + if (ColumnIndex > GetLineLength(LineIndex)) + ColumnIndex = GetLineLength(LineIndex); + + return true; + } + + return false; + } + + public bool Left() + { + if (ColumnIndex == 0) + { + if (LineIndex == 0) + return false; + + LineIndex--; + ColumnIndex = GetLineLength(LineIndex); + } + else + { + ColumnIndex--; + } + + return true; + } + + public bool Right() + { + if (ColumnIndex == _lines[LineIndex].Length) + { + if (LineIndex == _lines.Count - 1) + return false; + + LineIndex++; + ColumnIndex = 0; + } + else + { + ColumnIndex++; + } + + return true; + } + + public bool Tab() + { + var spaces = TabStops - ColumnIndex % TabStops; + + for (var s = 0; s < spaces; s++) + Type(' '); + + return spaces > 0; + } + + public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds) + { + base.Draw(context, renderer, deltaSeconds); + + var text = Text; + var textInfo = GetTextInfo(context, text, ContentRectangle, HorizontalTextAlignment, VerticalTextAlignment); + + if (!string.IsNullOrWhiteSpace(textInfo.Text)) + renderer.DrawText(textInfo.Font, textInfo.Text, textInfo.Position + TextOffset, textInfo.Color, textInfo.ClippingRectangle); + + if (IsFocused) + { + var caretRectangle = GetCaretRectangle(textInfo); + + if (_isCaretVisible) + renderer.DrawRectangle(caretRectangle, TextColor); + + _nextCaretBlink -= deltaSeconds; + + if (_nextCaretBlink <= 0) + { + _isCaretVisible = !_isCaretVisible; + _nextCaretBlink = _caretBlinkRate; + } + } + } + + private Rectangle GetCaretRectangle(TextInfo textInfo) + { + var font = textInfo.Font; + var text = GetLineText(LineIndex); + var offset = new Vector2(0, font.LineHeight * LineIndex); + var caretRectangle = font.GetStringRectangle(text.Substring(0, ColumnIndex), textInfo.Position + offset); + + // TODO: Finish the caret position stuff when it's outside the clipping rectangle + if (caretRectangle.Right > ClippingRectangle.Right) + textInfo.Position.X -= caretRectangle.Right - ClippingRectangle.Right; + + caretRectangle.X = caretRectangle.Right < ClippingRectangle.Right ? caretRectangle.Right : ClippingRectangle.Right; + caretRectangle.Width = 1; + + if (caretRectangle.Left < ClippingRectangle.Left) + { + textInfo.Position.X += ClippingRectangle.Left - caretRectangle.Left; + caretRectangle.X = ClippingRectangle.Left; + } + + return (Rectangle)caretRectangle; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ToggleButton.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ToggleButton.cs new file mode 100644 index 0000000..5858894 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/ToggleButton.cs @@ -0,0 +1,98 @@ +using System; + +namespace MonoGame.Extended.Gui.Controls +{ + public class ToggleButton : Button + { + public ToggleButton() + { + } + + public event EventHandler CheckedStateChanged; + + private bool _isChecked; + public bool IsChecked + { + get { return _isChecked; } + set + { + if (_isChecked != value) + { + _isChecked = value; + CheckedStyle?.ApplyIf(this, _isChecked); + CheckedStateChanged?.Invoke(this, EventArgs.Empty); + } + } + } + + private ControlStyle _checkedStyle; + public ControlStyle CheckedStyle + { + get { return _checkedStyle; } + set + { + if (_checkedStyle != value) + { + _checkedStyle = value; + CheckedStyle?.ApplyIf(this, _isChecked); + } + } + } + + private ControlStyle _checkedHoverStyle; + public ControlStyle CheckedHoverStyle + { + get { return _checkedHoverStyle; } + set + { + if (_checkedHoverStyle != value) + { + _checkedHoverStyle = value; + CheckedHoverStyle?.ApplyIf(this, _isChecked && IsHovered); + } + } + } + + public override bool OnPointerUp(IGuiContext context, PointerEventArgs args) + { + base.OnPointerUp(context, args); + + if (BoundingRectangle.Contains(args.Position)) + { + HoverStyle?.Revert(this); + CheckedHoverStyle?.Revert(this); + + IsChecked = !IsChecked; + + if (IsChecked) + CheckedHoverStyle?.Apply(this); + else + HoverStyle?.Apply(this); + } + + return true; + } + + public override bool OnPointerEnter(IGuiContext context, PointerEventArgs args) + { + if (IsChecked) + { + CheckedHoverStyle?.Apply(this); + return true; + } + + return base.OnPointerEnter(context, args); + } + + public override bool OnPointerLeave(IGuiContext context, PointerEventArgs args) + { + if (IsChecked) + { + CheckedHoverStyle?.Revert(this); + return true; + } + + return base.OnPointerLeave(context, args); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/UniformGrid.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/UniformGrid.cs new file mode 100644 index 0000000..829969d --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Controls/UniformGrid.cs @@ -0,0 +1,86 @@ +using System; +using System.Linq; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Gui.Controls +{ + public class UniformGrid : LayoutControl + { + public UniformGrid() + { + } + + public int Columns { get; set; } + public int Rows { get; set; } + + public override Size GetContentSize(IGuiContext context) + { + var columns = Columns == 0 ? (int)Math.Ceiling(Math.Sqrt(Items.Count)) : Columns; + var rows = Rows == 0 ? (int)Math.Ceiling((float)Items.Count / columns) : Rows; + var sizes = Items + .Select(control => control.CalculateActualSize(context)) + .ToArray(); + var minCellWidth = sizes.Max(s => s.Width); + var minCellHeight = sizes.Max(s => s.Height); + return new Size(minCellWidth * columns, minCellHeight * rows); + } + + protected override void Layout(IGuiContext context, Rectangle rectangle) + { + var gridInfo = CalculateGridInfo(context, rectangle.Size); + var columnIndex = 0; + var rowIndex = 0; + var cellWidth = HorizontalAlignment == HorizontalAlignment.Stretch ? gridInfo.MaxCellWidth : gridInfo.MinCellWidth; + var cellHeight = VerticalAlignment == VerticalAlignment.Stretch ? gridInfo.MaxCellHeight : gridInfo.MinCellHeight; + + foreach (var control in Items) + { + var x = columnIndex * cellWidth + rectangle.X; + var y = rowIndex * cellHeight + rectangle.Y; + + PlaceControl(context, control, x, y, cellWidth, cellHeight); + columnIndex++; + + if (columnIndex > gridInfo.Columns - 1) + { + columnIndex = 0; + rowIndex++; + } + } + } + + private struct GridInfo + { + public float MinCellWidth; + public float MinCellHeight; + public float MaxCellWidth; + public float MaxCellHeight; + public float Columns; + public float Rows; + public Size2 MinCellSize => new Size2(MinCellWidth * Columns, MinCellHeight * Rows); + } + + private GridInfo CalculateGridInfo(IGuiContext context, Size2 availableSize) + { + var columns = Columns == 0 ? (int)Math.Ceiling(Math.Sqrt(Items.Count)) : Columns; + var rows = Rows == 0 ? (int)Math.Ceiling((float)Items.Count / columns) : Rows; + var maxCellWidth = availableSize.Width / columns; + var maxCellHeight = availableSize.Height / rows; + var sizes = Items + .Select(control => control.CalculateActualSize(context)) // LayoutHelper.GetSizeWithMargins(control, context, new Size2(maxCellWidth, maxCellHeight))) + .ToArray(); + var maxControlWidth = sizes.Length == 0 ? 0 : sizes.Max(s => s.Width); + var maxControlHeight = sizes.Length == 0 ? 0 : sizes.Max(s => s.Height); + + return new GridInfo + { + Columns = columns, + Rows = rows, + MinCellWidth = Math.Min(maxControlWidth, maxCellWidth), + MinCellHeight = Math.Min(maxControlHeight, maxCellHeight), + MaxCellWidth = maxCellWidth, + MaxCellHeight = maxCellHeight + }; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Cursor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Cursor.cs new file mode 100644 index 0000000..638baec --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Cursor.cs @@ -0,0 +1,11 @@ +using Microsoft.Xna.Framework; +using MonoGame.Extended.TextureAtlases; + +namespace MonoGame.Extended.Gui +{ + public class Cursor + { + public TextureRegion2D TextureRegion { get; set; } + public Color Color { get; set; } = Color.White; + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Element.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Element.cs new file mode 100644 index 0000000..0c364a9 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Element.cs @@ -0,0 +1,125 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Reflection; +using System.Text.Json.Serialization; +using Microsoft.Xna.Framework; +using MonoGame.Extended.TextureAtlases; + +namespace MonoGame.Extended.Gui +{ + public class Binding + { + public Binding(object viewModel, string viewModelProperty, string viewProperty) + { + ViewModel = viewModel; + ViewModelProperty = viewModelProperty; + ViewProperty = viewProperty; + } + + public object ViewModel { get; } + public string ViewModelProperty { get; } + public string ViewProperty { get; } + } + + public abstract class Element + { + public string Name { get; set; } + public Point Position { get; set; } + public Point Origin { get; set; } + public Color BackgroundColor { get; set; } + public Color BorderColor { get; set; } = Color.White; + public int BorderThickness { get; set; } = 0; + + private TextureRegion2D _backgroundRegion; + public TextureRegion2D BackgroundRegion + { + get => _backgroundRegion; + set + { + _backgroundRegion = value; + + if (_backgroundRegion != null && BackgroundColor == Color.Transparent) + BackgroundColor = Color.White; + } + } + + public List<Binding> Bindings { get; } = new List<Binding>(); + + protected void OnPropertyChanged(string propertyName) + { + foreach (var binding in Bindings) + { + if (binding.ViewProperty == propertyName) + { + var value = GetType() + .GetTypeInfo() + .GetDeclaredProperty(binding.ViewProperty) + .GetValue(this); + + binding.ViewModel + .GetType() + .GetTypeInfo() + .GetDeclaredProperty(binding.ViewModelProperty) + .SetValue(binding.ViewModel, value); + } + } + } + + private Size _size; + public Size Size + { + get => _size; + set + { + _size = value; + OnSizeChanged(); + } + } + + protected virtual void OnSizeChanged() { } + + public int MinWidth { get; set; } + public int MinHeight { get; set; } + public int MaxWidth { get; set; } = int.MaxValue; + public int MaxHeight { get; set; } = int.MaxValue; + + public int Width + { + get => Size.Width; + set => Size = new Size(value, Size.Height); + } + + public int Height + { + get => Size.Height; + set => Size = new Size(Size.Width, value); + } + + public Size ActualSize { get; internal set; } + + public abstract void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds); + } + + public abstract class Element<TParent> : Element, IRectangular + where TParent : IRectangular + { + [EditorBrowsable(EditorBrowsableState.Never)] + [JsonIgnore] + public TParent Parent { get; internal set; } + + [EditorBrowsable(EditorBrowsableState.Never)] + [JsonIgnore] + public Rectangle BoundingRectangle + { + get + { + var offset = Point.Zero; + + if (Parent != null) + offset = Parent.BoundingRectangle.Location; + + return new Rectangle(offset + Position - ActualSize * Origin, ActualSize); + } + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ElementCollection.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ElementCollection.cs new file mode 100644 index 0000000..009161d --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ElementCollection.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using MonoGame.Extended.Gui.Controls; + +namespace MonoGame.Extended.Gui +{ + public abstract class ElementCollection<TChild, TParent> : IList<TChild> + where TParent : class, IRectangular + where TChild : Element<TParent> + { + private readonly TParent _parent; + private readonly List<TChild> _list = new List<TChild>(); + + public Action<TChild> ItemAdded { get; set; } + public Action<TChild> ItemRemoved { get; set; } + + protected ElementCollection(TParent parent) + { + _parent = parent; + } + + public IEnumerator<TChild> GetEnumerator() + { + return _list.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_list).GetEnumerator(); + } + + public void Add(TChild item) + { + item.Parent = _parent; + _list.Add(item); + ItemAdded?.Invoke(item); + } + + public void Clear() + { + foreach (var child in _list) + { + child.Parent = null; + ItemRemoved?.Invoke(child); + } + + _list.Clear(); + } + + public bool Contains(TChild item) + { + return _list.Contains(item); + } + + public void CopyTo(TChild[] array, int arrayIndex) + { + _list.CopyTo(array, arrayIndex); + } + + public bool Remove(TChild item) + { + item.Parent = null; + ItemRemoved?.Invoke(item); + return _list.Remove(item); + } + + public int Count => _list.Count; + + public bool IsReadOnly => ((ICollection<Control>)_list).IsReadOnly; + + public int IndexOf(TChild item) + { + return _list.IndexOf(item); + } + + public void Insert(int index, TChild item) + { + item.Parent = _parent; + _list.Insert(index, item); + ItemAdded?.Invoke(item); + } + + public void RemoveAt(int index) + { + var child = _list[index]; + child.Parent = null; + _list.RemoveAt(index); + ItemRemoved?.Invoke(child); + } + + public TChild this[int index] + { + get { return _list[index]; } + set { _list[index] = value; } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSpriteBatchRenderer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSpriteBatchRenderer.cs new file mode 100644 index 0000000..6d1e8ce --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSpriteBatchRenderer.cs @@ -0,0 +1,81 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using MonoGame.Extended.BitmapFonts; +using MonoGame.Extended.TextureAtlases; + +namespace MonoGame.Extended.Gui +{ + public interface IGuiRenderer + { + void Begin(); + void DrawRegion(TextureRegion2D textureRegion, Rectangle rectangle, Color color, Rectangle? clippingRectangle = null); + void DrawRegion(TextureRegion2D textureRegion, Vector2 position, Color color, Rectangle? clippingRectangle = null); + void DrawText(BitmapFont font, string text, Vector2 position, Color color, Rectangle? clippingRectangle = null); + void DrawRectangle(Rectangle rectangle, Color color, float thickness = 1f, Rectangle? clippingRectangle = null); + void FillRectangle(Rectangle rectangle, Color color, Rectangle? clippingRectangle = null); + void End(); + } + + public class GuiSpriteBatchRenderer : IGuiRenderer + { + private readonly Func<Matrix> _getTransformMatrix; + private readonly SpriteBatch _spriteBatch; + + public GuiSpriteBatchRenderer(GraphicsDevice graphicsDevice, Func<Matrix> getTransformMatrix) + { + _getTransformMatrix = getTransformMatrix; + _spriteBatch = new SpriteBatch(graphicsDevice); + } + + public SpriteSortMode SortMode { get; set; } + public BlendState BlendState { get; set; } = BlendState.AlphaBlend; + public SamplerState SamplerState { get; set; } = SamplerState.PointClamp; + public DepthStencilState DepthStencilState { get; set; } = DepthStencilState.Default; + public RasterizerState RasterizerState { get; set; } = RasterizerState.CullNone; + public Effect Effect { get; set; } + + public void Begin() + { + _spriteBatch.Begin(SortMode, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect, _getTransformMatrix()); + } + + public void End() + { + _spriteBatch.End(); + } + + public void DrawRegion(TextureRegion2D textureRegion, Rectangle rectangle, Color color, Rectangle? clippingRectangle = null) + { + if (textureRegion != null) + _spriteBatch.Draw(textureRegion, rectangle, color, clippingRectangle); + } + + public void DrawRegion(TextureRegion2D textureRegion, Vector2 position, Color color, Rectangle? clippingRectangle = null) + { + if (textureRegion != null) + _spriteBatch.Draw(textureRegion, position, color, clippingRectangle); + } + + public void DrawText(BitmapFont font, string text, Vector2 position, Color color, Rectangle? clippingRectangle = null) + { + _spriteBatch.DrawString(font, text, position, color, clippingRectangle); + } + + public void DrawRectangle(Rectangle rectangle, Color color, float thickness = 1f, Rectangle? clippingRectangle = null) + { + if (clippingRectangle.HasValue) + rectangle = rectangle.Clip(clippingRectangle.Value); + + _spriteBatch.DrawRectangle(rectangle, color, thickness); + } + + public void FillRectangle(Rectangle rectangle, Color color, Rectangle? clippingRectangle = null) + { + if (clippingRectangle.HasValue) + rectangle = rectangle.Clip(clippingRectangle.Value); + + _spriteBatch.FillRectangle(rectangle, color); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSystem.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSystem.cs new file mode 100644 index 0000000..8b522fb --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/GuiSystem.cs @@ -0,0 +1,265 @@ +using Microsoft.Xna.Framework; +using MonoGame.Extended.BitmapFonts; +using MonoGame.Extended.Gui.Controls; +using MonoGame.Extended.Input.InputListeners; +using MonoGame.Extended.ViewportAdapters; +using System; +using System.Linq; + +namespace MonoGame.Extended.Gui +{ + public interface IGuiContext + { + BitmapFont DefaultFont { get; } + Vector2 CursorPosition { get; } + Control FocusedControl { get; } + + void SetFocus(Control focusedControl); + } + + public class GuiSystem : IGuiContext, IRectangular + { + private readonly ViewportAdapter _viewportAdapter; + private readonly IGuiRenderer _renderer; + private readonly MouseListener _mouseListener; + private readonly TouchListener _touchListener; + private readonly KeyboardListener _keyboardListener; + + private Control _preFocusedControl; + + public GuiSystem(ViewportAdapter viewportAdapter, IGuiRenderer renderer) + { + _viewportAdapter = viewportAdapter; + _renderer = renderer; + + _mouseListener = new MouseListener(viewportAdapter); + _mouseListener.MouseDown += (s, e) => OnPointerDown(PointerEventArgs.FromMouseArgs(e)); + _mouseListener.MouseMoved += (s, e) => OnPointerMoved(PointerEventArgs.FromMouseArgs(e)); + _mouseListener.MouseUp += (s, e) => OnPointerUp(PointerEventArgs.FromMouseArgs(e)); + _mouseListener.MouseWheelMoved += (s, e) => FocusedControl?.OnScrolled(e.ScrollWheelDelta); + + _touchListener = new TouchListener(viewportAdapter); + _touchListener.TouchStarted += (s, e) => OnPointerDown(PointerEventArgs.FromTouchArgs(e)); + _touchListener.TouchMoved += (s, e) => OnPointerMoved(PointerEventArgs.FromTouchArgs(e)); + _touchListener.TouchEnded += (s, e) => OnPointerUp(PointerEventArgs.FromTouchArgs(e)); + + _keyboardListener = new KeyboardListener(); + _keyboardListener.KeyTyped += (sender, args) => PropagateDown(FocusedControl, x => x.OnKeyTyped(this, args)); + _keyboardListener.KeyPressed += (sender, args) => PropagateDown(FocusedControl, x => x.OnKeyPressed(this, args)); + } + + public Control FocusedControl { get; private set; } + public Control HoveredControl { get; private set; } + + private Screen _activeScreen; + public Screen ActiveScreen + { + get => _activeScreen; + set + { + if (_activeScreen != value) + { + _activeScreen = value; + + if(_activeScreen != null) + InitializeScreen(_activeScreen); + } + } + } + + public Rectangle BoundingRectangle => _viewportAdapter.BoundingRectangle; + + public Vector2 CursorPosition { get; set; } + + public BitmapFont DefaultFont => Skin.Default?.DefaultFont; + + private void InitializeScreen(Screen screen) + { + screen.Layout(this, BoundingRectangle); + } + + public void ClientSizeChanged() + { + //ActiveScreen?.Content?.InvalidateMeasure(); + ActiveScreen?.Layout(this, BoundingRectangle); + } + + public void Update(GameTime gameTime) + { + if(ActiveScreen == null) + return; + + _touchListener.Update(gameTime); + _mouseListener.Update(gameTime); + _keyboardListener.Update(gameTime); + + var deltaSeconds = gameTime.GetElapsedSeconds(); + + if (ActiveScreen != null && ActiveScreen.IsVisible) + UpdateControl(ActiveScreen.Content, deltaSeconds); + + //if (ActiveScreen.IsLayoutRequired) + // ActiveScreen.Layout(this, BoundingRectangle); + + ActiveScreen.Update(gameTime); + } + + public void Draw(GameTime gameTime) + { + var deltaSeconds = gameTime.GetElapsedSeconds(); + + _renderer.Begin(); + + if (ActiveScreen != null && ActiveScreen.IsVisible) + { + DrawControl(ActiveScreen.Content, deltaSeconds); + //DrawWindows(ActiveScreen.Windows, deltaSeconds); + } + + var cursor = Skin.Default?.Cursor; + + if (cursor != null) + _renderer.DrawRegion(cursor.TextureRegion, CursorPosition, cursor.Color); + + _renderer.End(); + } + + //private void DrawWindows(WindowCollection windows, float deltaSeconds) + //{ + // foreach (var window in windows) + // { + // window.Draw(this, _renderer, deltaSeconds); + // DrawChildren(window.Controls, deltaSeconds); + // } + //} + + public void UpdateControl(Control control, float deltaSeconds) + { + if (control.IsVisible) + { + control.Update(this, deltaSeconds); + + foreach (var childControl in control.Children) + UpdateControl(childControl, deltaSeconds); + } + } + + private void DrawControl(Control control, float deltaSeconds) + { + if (control.IsVisible) + { + control.Draw(this, _renderer, deltaSeconds); + + foreach (var childControl in control.Children) + DrawControl(childControl, deltaSeconds); + } + } + + private void OnPointerDown(PointerEventArgs args) + { + if (ActiveScreen == null || !ActiveScreen.IsVisible) + return; + + _preFocusedControl = FindControlAtPoint(args.Position); + PropagateDown(HoveredControl, x => x.OnPointerDown(this, args)); + } + + private void OnPointerUp(PointerEventArgs args) + { + if (ActiveScreen == null || !ActiveScreen.IsVisible) + return; + + var postFocusedControl = FindControlAtPoint(args.Position); + + if (_preFocusedControl == postFocusedControl) + { + SetFocus(postFocusedControl); + } + + _preFocusedControl = null; + PropagateDown(HoveredControl, x => x.OnPointerUp(this, args)); + } + + private void OnPointerMoved(PointerEventArgs args) + { + CursorPosition = args.Position.ToVector2(); + + if (ActiveScreen == null || !ActiveScreen.IsVisible) + return; + + var hoveredControl = FindControlAtPoint(args.Position); + + if (HoveredControl != hoveredControl) + { + if (HoveredControl != null && (hoveredControl == null || !hoveredControl.HasParent(HoveredControl))) + PropagateDown(HoveredControl, x => x.OnPointerLeave(this, args)); + + HoveredControl = hoveredControl; + PropagateDown(HoveredControl, x => x.OnPointerEnter(this, args)); + } + else + { + PropagateDown(HoveredControl, x => x.OnPointerMove(this, args)); + } + } + + public void SetFocus(Control focusedControl) + { + if (FocusedControl != focusedControl) + { + if (FocusedControl != null) + { + FocusedControl.IsFocused = false; + PropagateDown(FocusedControl, x => x.OnUnfocus(this)); + } + + FocusedControl = focusedControl; + + if (FocusedControl != null) + { + FocusedControl.IsFocused = true; + PropagateDown(FocusedControl, x => x.OnFocus(this)); + } + } + } + + /// <summary> + /// Method is meant to loop down the parents control to find a suitable event control. If the predicate returns false + /// it will continue down the control tree. + /// </summary> + /// <param name="control">The control we want to check against</param> + /// <param name="predicate">A function to check if the propagation should resume, if returns false it will continue down the tree.</param> + private static void PropagateDown(Control control, Func<Control, bool> predicate) + { + while(control != null && predicate(control)) + { + control = control.Parent; + } + } + + private Control FindControlAtPoint(Point point) + { + if (ActiveScreen == null || !ActiveScreen.IsVisible) + return null; + + return FindControlAtPoint(ActiveScreen.Content, point); + } + + private Control FindControlAtPoint(Control control, Point point) + { + foreach (var controlChild in control.Children.Reverse()) + { + var c = FindControlAtPoint(controlChild, point); + + if (c != null) + return c; + } + + + if (control.IsVisible && control.Contains(this, point)) + return control; + + return null; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/LayoutHelper.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/LayoutHelper.cs new file mode 100644 index 0000000..1268bff --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/LayoutHelper.cs @@ -0,0 +1,65 @@ +using System; +using Microsoft.Xna.Framework; +using MonoGame.Extended.Gui.Controls; + +namespace MonoGame.Extended.Gui +{ + public enum HorizontalAlignment { Left, Right, Centre, Stretch } + public enum VerticalAlignment { Top, Bottom, Centre, Stretch } + + public static class LayoutHelper + { + public static void PlaceControl(IGuiContext context, Control control, float x, float y, float width, float height) + { + var rectangle = new Rectangle((int)x, (int)y, (int)width, (int)height); + var desiredSize = control.CalculateActualSize(context); + var alignedRectangle = AlignRectangle(control.HorizontalAlignment, control.VerticalAlignment, desiredSize, rectangle); + + control.Position = new Point(control.Margin.Left + alignedRectangle.X, control.Margin.Top + alignedRectangle.Y); + control.ActualSize = (Size)alignedRectangle.Size - control.Margin.Size; + control.InvalidateMeasure(); + } + + public static Rectangle AlignRectangle(HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment, Size size, Rectangle targetRectangle) + { + var x = GetHorizontalPosition(horizontalAlignment, size, targetRectangle); + var y = GetVerticalPosition(verticalAlignment, size, targetRectangle); + var width = horizontalAlignment == HorizontalAlignment.Stretch ? targetRectangle.Width : size.Width; + var height = verticalAlignment == VerticalAlignment.Stretch ? targetRectangle.Height : size.Height; + + return new Rectangle(x, y, width, height); + } + + public static int GetHorizontalPosition(HorizontalAlignment horizontalAlignment, Size size, Rectangle targetRectangle) + { + switch (horizontalAlignment) + { + case HorizontalAlignment.Stretch: + case HorizontalAlignment.Left: + return targetRectangle.X; + case HorizontalAlignment.Right: + return targetRectangle.Right - size.Width; + case HorizontalAlignment.Centre: + return targetRectangle.X + targetRectangle.Width / 2 - size.Width / 2; + default: + throw new ArgumentOutOfRangeException(nameof(horizontalAlignment), horizontalAlignment, $"{horizontalAlignment} is not supported"); + } + } + + public static int GetVerticalPosition(VerticalAlignment verticalAlignment, Size size, Rectangle targetRectangle) + { + switch (verticalAlignment) + { + case VerticalAlignment.Stretch: + case VerticalAlignment.Top: + return targetRectangle.Y; + case VerticalAlignment.Bottom: + return targetRectangle.Bottom - size.Height; + case VerticalAlignment.Centre: + return targetRectangle.Y + targetRectangle.Height / 2 - size.Height / 2; + default: + throw new ArgumentOutOfRangeException(nameof(verticalAlignment), verticalAlignment, $"{verticalAlignment} is not supported"); + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Markup/MarkupParser.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Markup/MarkupParser.cs new file mode 100644 index 0000000..c2e8776 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Markup/MarkupParser.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Xml; +using Microsoft.Xna.Framework; +using MonoGame.Extended.Gui.Controls; + +namespace MonoGame.Extended.Gui.Markup +{ + public class MarkupParser + { + public MarkupParser() + { + } + + private static readonly Dictionary<string, Type> _controlTypes = + typeof(Control).Assembly + .ExportedTypes + .Where(t => t.IsSubclassOf(typeof(Control))) + .ToDictionary(t => t.Name, StringComparer.OrdinalIgnoreCase); + + private static readonly Dictionary<Type, Func<string, object>> _converters = + new Dictionary<Type, Func<string, object>> + { + {typeof(object), s => s}, + {typeof(string), s => s}, + {typeof(bool), s => bool.Parse(s)}, + {typeof(int), s => int.Parse(s)}, + {typeof(Color), s => s.StartsWith("#") ? ColorHelper.FromHex(s) : ColorHelper.FromName(s)} + }; + + private static object ConvertValue(Type propertyType, string input, object dataContext) + { + var value = ParseBinding(input, dataContext); + + if (_converters.TryGetValue(propertyType, out var converter)) + return converter(value); //property.SetValue(control, converter(value)); + + if (propertyType.IsEnum) + return + Enum.Parse(propertyType, value, + true); // property.SetValue(control, Enum.Parse(propertyType, value, true)); + + throw new InvalidOperationException($"Converter not found for {propertyType}"); + } + + private static object ParseChildNode(XmlNode node, Control parent, object dataContext) + { + if (node is XmlText) + return node.InnerText.Trim(); + + if (_controlTypes.TryGetValue(node.Name, out var type)) + { + var typeInfo = type.GetTypeInfo(); + var control = (Control) Activator.CreateInstance(type); + + // ReSharper disable once AssignNullToNotNullAttribute + foreach (var attribute in node.Attributes.Cast<XmlAttribute>()) + { + var property = typeInfo.GetProperty(attribute.Name); + + if (property != null) + { + var value = ConvertValue(property.PropertyType, attribute.Value, dataContext); + property.SetValue(control, value); + } + else + { + var parts = attribute.Name.Split('.'); + var parentType = parts[0]; + var propertyName = parts[1]; + var propertyType = parent.GetAttachedPropertyType(propertyName); + var propertyValue = ConvertValue(propertyType, attribute.Value, dataContext); + + if (!string.Equals(parent.GetType().Name, parentType, StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException( + $"Attached properties are only supported on the immediate parent type {parentType}"); + + control.SetAttachedProperty(propertyName, propertyValue); + } + } + + + if (node.HasChildNodes) + { + switch (control) + { + case ContentControl contentControl: + if (node.ChildNodes.Count > 1) + throw new InvalidOperationException("A content control can only have one child"); + + contentControl.Content = ParseChildNode(node.ChildNodes[0], control, dataContext); + break; + case LayoutControl layoutControl: + foreach (var childControl in ParseChildNodes(node.ChildNodes, control, dataContext)) + layoutControl.Items.Add(childControl as Control); + break; + } + } + + return control; + } + + throw new InvalidOperationException($"Unknown control type {node.Name}"); + } + + private static string ParseBinding(string expression, object dataContext) + { + if (dataContext != null && expression.StartsWith("{{") && expression.EndsWith("}}")) + { + var binding = expression.Substring(2, expression.Length - 4); + var bindingValue = dataContext + .GetType() + .GetProperty(binding) + ?.GetValue(dataContext); + + return $"{bindingValue}"; + } + + return expression; + } + + private static IEnumerable<object> ParseChildNodes(XmlNodeList nodes, Control parent, object dataContext) + { + foreach (var node in nodes.Cast<XmlNode>()) + { + if (node.Name == "xml") + { + // TODO: Validate header + } + else + { + yield return ParseChildNode(node, parent, dataContext); + } + } + } + + public Control Parse(string filePath, object dataContext) + { + var d = new XmlDocument(); + d.Load(filePath); + return ParseChildNodes(d.ChildNodes, null, dataContext) + .LastOrDefault() as Control; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/MonoGame.Extended.Gui.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/MonoGame.Extended.Gui.csproj new file mode 100644 index 0000000..d319cb4 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/MonoGame.Extended.Gui.csproj @@ -0,0 +1,13 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <Description>A GUI system for MonoGame written from the ground up to make MonoGame more awesome.</Description> + <PackageTags>monogame gui</PackageTags> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\MonoGame.Extended.Input\MonoGame.Extended.Input.csproj" /> + <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" /> + </ItemGroup> + +</Project> diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Orientation.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Orientation.cs new file mode 100644 index 0000000..a12599f --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Orientation.cs @@ -0,0 +1,4 @@ +namespace MonoGame.Extended.Gui +{ + public enum Orientation { Horizontal, Vertical } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/PointerEventArgs.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/PointerEventArgs.cs new file mode 100644 index 0000000..18cffac --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/PointerEventArgs.cs @@ -0,0 +1,42 @@ +using System; +using Microsoft.Xna.Framework; +using MonoGame.Extended.Input; +using MonoGame.Extended.Input.InputListeners; + +namespace MonoGame.Extended.Gui +{ + public class PointerEventArgs : EventArgs + { + private PointerEventArgs() + { + } + + public Point Position { get; private set; } + public MouseButton Button { get; private set; } + public int ScrollWheelDelta { get; private set; } + public int ScrollWheelValue { get; private set; } + public TimeSpan Time { get; private set; } + + public static PointerEventArgs FromMouseArgs(MouseEventArgs args) + { + return new PointerEventArgs + { + Position = args.Position, + Button = args.Button, + ScrollWheelDelta = args.ScrollWheelDelta, + ScrollWheelValue = args.ScrollWheelValue, + Time = args.Time + }; + } + + public static PointerEventArgs FromTouchArgs(TouchEventArgs args) + { + return new PointerEventArgs + { + Position = args.Position, + Button = MouseButton.Left, + Time = args.Time + }; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Screen.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Screen.cs new file mode 100644 index 0000000..821a1e2 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Screen.cs @@ -0,0 +1,140 @@ +using System; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using MonoGame.Extended.Gui.Controls; +using MonoGame.Extended.Gui.Serialization; + +namespace MonoGame.Extended.Gui +{ + public class Screen //: Element<GuiSystem>, IDisposable + { + public Screen() + { + //Windows = new WindowCollection(this) { ItemAdded = w => _isLayoutRequired = true }; + } + + public virtual void Dispose() + { + } + + private Control _content; + [JsonPropertyOrder(1)] + public Control Content + { + get { return _content; } + set + { + if (_content != value) + { + _content = value; + _isLayoutRequired = true; + } + } + } + + //[JsonIgnore] + //public WindowCollection Windows { get; } + + public float Width { get; private set; } + public float Height { get; private set; } + public Size2 Size => new Size2(Width, Height); + public bool IsVisible { get; set; } = true; + + private bool _isLayoutRequired; + [JsonIgnore] + public bool IsLayoutRequired => _isLayoutRequired || Content.IsLayoutRequired; + + public virtual void Update(GameTime gameTime) + { + + } + + public void Show() + { + IsVisible = true; + } + + public void Hide() + { + IsVisible = false; + } + + public T FindControl<T>(string name) + where T : Control + { + return FindControl<T>(Content, name); + } + + private static T FindControl<T>(Control rootControl, string name) + where T : Control + { + if (rootControl.Name == name) + return rootControl as T; + + foreach (var childControl in rootControl.Children) + { + var control = FindControl<T>(childControl, name); + + if (control != null) + return control; + } + + return null; + } + + public void Layout(IGuiContext context, Rectangle rectangle) + { + Width = rectangle.Width; + Height = rectangle.Height; + + LayoutHelper.PlaceControl(context, Content, rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height); + + //foreach (var window in Windows) + // LayoutHelper.PlaceWindow(context, window, rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height); + + _isLayoutRequired = false; + Content.IsLayoutRequired = false; + } + + //public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds) + //{ + // renderer.DrawRectangle(BoundingRectangle, Color.Green); + //} + + public static Screen FromStream(ContentManager contentManager, Stream stream, params Type[] customControlTypes) + { + return FromStream<Screen>(contentManager, stream, customControlTypes); + } + + public static TScreen FromStream<TScreen>(ContentManager contentManager, Stream stream, params Type[] customControlTypes) + where TScreen : Screen + { + var skinService = new SkinService(); + var options = GuiJsonSerializerOptionsProvider.GetOptions(contentManager, customControlTypes); + options.Converters.Add(new SkinJsonConverter(contentManager, skinService, customControlTypes)); + options.Converters.Add(new ControlJsonConverter(skinService, customControlTypes)); + return JsonSerializer.Deserialize<TScreen>(stream, options); + } + + public static Screen FromFile(ContentManager contentManager, string path, params Type[] customControlTypes) + { + using (var stream = TitleContainer.OpenStream(path)) + { + return FromStream<Screen>(contentManager, stream, customControlTypes); + } + } + + public static TScreen FromFile<TScreen>(ContentManager contentManager, string path, params Type[] customControlTypes) + where TScreen : Screen + { + using (var stream = TitleContainer.OpenStream(path)) + { + return FromStream<TScreen>(contentManager, stream, customControlTypes); + } + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ScreenCollection.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ScreenCollection.cs new file mode 100644 index 0000000..0572728 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/ScreenCollection.cs @@ -0,0 +1,10 @@ +namespace MonoGame.Extended.Gui +{ + //public class ScreenCollection : ElementCollection<Screen, GuiSystem> + //{ + // public ScreenCollection(GuiSystem parent) + // : base(parent) + // { + // } + //} +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlJsonConverter.cs new file mode 100644 index 0000000..a9c71f8 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlJsonConverter.cs @@ -0,0 +1,67 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using MonoGame.Extended.Gui.Controls; + +namespace MonoGame.Extended.Gui.Serialization +{ + public class ControlJsonConverter : JsonConverter<Control> + { + private readonly IGuiSkinService _guiSkinService; + private readonly ControlStyleJsonConverter _styleConverter; + private const string _styleProperty = "Style"; + + public ControlJsonConverter(IGuiSkinService guiSkinService, params Type[] customControlTypes) + { + _guiSkinService = guiSkinService; + _styleConverter = new ControlStyleJsonConverter(customControlTypes); + } + + /// <inheritdoc /> + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Control); + + /// <inheritdoc /> + public override Control Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var style = _styleConverter.Read(ref reader, typeToConvert, options); + var template = GetControlTemplate(style); + var skin = _guiSkinService.Skin; + var control = skin.Create(style.TargetType, template); + + var itemsControl = control as ItemsControl; + if (itemsControl != null) + { + object childControls; + + if (style.TryGetValue(nameof(ItemsControl.Items), out childControls)) + { + var controlCollection = childControls as ControlCollection; + + if (controlCollection != null) + { + foreach (var child in controlCollection) + itemsControl.Items.Add(child); + } + } + } + + style.Apply(control); + return control; + } + + /// <inheritdoc /> + public override void Write(Utf8JsonWriter writer, Control value, JsonSerializerOptions options) { } + + + + private static string GetControlTemplate(ControlStyle style) + { + object template; + + if (style.TryGetValue(_styleProperty, out template)) + return template as string; + + return null; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlStyleJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlStyleJsonConverter.cs new file mode 100644 index 0000000..c31549b --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/ControlStyleJsonConverter.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using MonoGame.Extended.Collections; +using MonoGame.Extended.Gui.Controls; + +namespace MonoGame.Extended.Gui.Serialization +{ + public class ControlStyleJsonConverter : JsonConverter<ControlStyle> + { + private readonly Dictionary<string, Type> _controlTypes; + private const string _typeProperty = "Type"; + private const string _nameProperty = "Name"; + + public ControlStyleJsonConverter(params Type[] customControlTypes) + { + _controlTypes = typeof(Control) + .GetTypeInfo() + .Assembly + .ExportedTypes + .Concat(customControlTypes) + .Where(t => t.GetTypeInfo().IsSubclassOf(typeof(Control))) + .ToDictionary(t => t.Name); + } + + /// <inheritdoc /> + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(ControlStyle); + + /// <inheritdoc /> + public override ControlStyle Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var dictionary = JsonSerializer.Deserialize<Dictionary<string, object>>(ref reader, options); + var name = dictionary.GetValueOrDefault(_nameProperty) as string; + var typeName = dictionary.GetValueOrDefault(_typeProperty) as string; + + if (!_controlTypes.TryGetValue(typeName, out Type controlType)) + throw new FormatException("invalid control type: " + typeName); + + var targetType = typeName != null ? controlType : typeof(Control); + var properties = targetType + .GetRuntimeProperties() + .ToDictionary(p => p.Name); + var style = new ControlStyle(name, targetType); + + foreach (var keyValuePair in dictionary.Where(i => i.Key != _typeProperty)) + { + var propertyName = keyValuePair.Key; + var rawValue = keyValuePair.Value; + + PropertyInfo propertyInfo; + var value = properties.TryGetValue(propertyName, out propertyInfo) + ? DeserializeValueAs(rawValue, propertyInfo.PropertyType) + : DeserializeValueAs(rawValue, typeof(object)); + + style.Add(propertyName, value); + } + + return style; + } + + private static object DeserializeValueAs(object value, Type type) + { + var json = JsonSerializer.Serialize(value, type); + return JsonSerializer.Deserialize(json, type); + } + + /// <inheritdoc /> + public override void Write(Utf8JsonWriter writer, ControlStyle value, JsonSerializerOptions options) + { + var style = (ControlStyle)value; + var dictionary = new Dictionary<string, object> { [_typeProperty] = style.TargetType.Name }; + + foreach (var keyValuePair in style) + dictionary.Add(keyValuePair.Key, keyValuePair.Value); + + JsonSerializer.Serialize(writer, dictionary); + } + + + + + + + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiJsonSerializerOptionsProvider.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiJsonSerializerOptionsProvider.cs new file mode 100644 index 0000000..5ebc880 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiJsonSerializerOptionsProvider.cs @@ -0,0 +1,38 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Xna.Framework.Content; +using MonoGame.Extended.BitmapFonts; +using MonoGame.Extended.Serialization; + +namespace MonoGame.Extended.Gui.Serialization; + +public static class GuiJsonSerializerOptionsProvider +{ + public static JsonSerializerOptions GetOptions(ContentManager contentManager, params Type[] customControlTypes) + { + var options = new JsonSerializerOptions + { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + var textureRegionService = new GuiTextureRegionService(); + + options.Converters.Add(new Vector2JsonConverter()); + options.Converters.Add(new SizeJsonConverter()); + options.Converters.Add(new Size2JsonConverter()); + options.Converters.Add(new ColorJsonConverter()); + options.Converters.Add(new ThicknessJsonConverter()); + options.Converters.Add(new ContentManagerJsonConverter<BitmapFont>(contentManager, font => font.Name)); + options.Converters.Add(new ControlStyleJsonConverter(customControlTypes)); + options.Converters.Add(new GuiTextureAtlasJsonConverter(contentManager, textureRegionService)); + options.Converters.Add(new GuiNinePatchRegion2DJsonConverter(textureRegionService)); + options.Converters.Add(new TextureRegion2DJsonConverter(textureRegionService)); + options.Converters.Add(new VerticalAlignmentConverter()); + options.Converters.Add(new HorizontalAlignmentConverter()); + + return options; + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiNinePatchRegion2DJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiNinePatchRegion2DJsonConverter.cs new file mode 100644 index 0000000..9be58e5 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiNinePatchRegion2DJsonConverter.cs @@ -0,0 +1,15 @@ +using MonoGame.Extended.Serialization; + +namespace MonoGame.Extended.Gui.Serialization +{ + public class GuiNinePatchRegion2DJsonConverter : NinePatchRegion2DJsonConverter + { + private readonly IGuiTextureRegionService _textureRegionService; + + public GuiNinePatchRegion2DJsonConverter(IGuiTextureRegionService textureRegionService) + : base(textureRegionService) + { + _textureRegionService = textureRegionService; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureAtlasJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureAtlasJsonConverter.cs new file mode 100644 index 0000000..a0617cf --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureAtlasJsonConverter.cs @@ -0,0 +1,31 @@ +using System; +using System.Text.Json; +using Microsoft.Xna.Framework.Content; +using MonoGame.Extended.Serialization; +using MonoGame.Extended.TextureAtlases; + +namespace MonoGame.Extended.Gui.Serialization +{ + public class GuiTextureAtlasJsonConverter : ContentManagerJsonConverter<TextureAtlas> + { + private readonly IGuiTextureRegionService _textureRegionService; + + public GuiTextureAtlasJsonConverter(ContentManager contentManager, IGuiTextureRegionService textureRegionService) + : base(contentManager, atlas => atlas.Name) + { + _textureRegionService = textureRegionService; + } + + /// <inheritdoc /> + public override TextureAtlas Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var textureAtlas = base.Read(ref reader, typeToConvert, options); + if (textureAtlas is not null) + { + _textureRegionService.TextureAtlases.Add(textureAtlas); + } + + return textureAtlas; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureRegionService.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureRegionService.cs new file mode 100644 index 0000000..cf9ab9e --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/GuiTextureRegionService.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using MonoGame.Extended.Serialization; +using MonoGame.Extended.TextureAtlases; + +namespace MonoGame.Extended.Gui.Serialization +{ + public interface IGuiTextureRegionService : ITextureRegionService + { + IList<TextureAtlas> TextureAtlases { get; } + IList<NinePatchRegion2D> NinePatches { get; } + } + + public class GuiTextureRegionService : TextureRegionService, IGuiTextureRegionService + { + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/HorizontalAlignmentConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/HorizontalAlignmentConverter.cs new file mode 100644 index 0000000..a696528 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/HorizontalAlignmentConverter.cs @@ -0,0 +1,32 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MonoGame.Extended.Gui.Serialization; + +public class HorizontalAlignmentConverter : JsonConverter<HorizontalAlignment> +{ + /// <inheritdoc /> + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(HorizontalAlignment); + + /// <inheritdoc /> + public override HorizontalAlignment Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + + if (value.Equals("Center", StringComparison.OrdinalIgnoreCase) || value.Equals("Centre", StringComparison.OrdinalIgnoreCase)) + { + return HorizontalAlignment.Centre; + } + + if (Enum.TryParse<HorizontalAlignment>(value, true, out var alignment)) + { + return alignment; + } + + throw new InvalidOperationException($"Invalid value for '{nameof(HorizontalAlignment)}'"); + } + + /// <inheritdoc /> + public override void Write(Utf8JsonWriter writer, HorizontalAlignment value, JsonSerializerOptions options) { } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/SkinJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/SkinJsonConverter.cs new file mode 100644 index 0000000..016ea6d --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/SkinJsonConverter.cs @@ -0,0 +1,57 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; + +namespace MonoGame.Extended.Gui.Serialization; + +public interface IGuiSkinService +{ + Skin Skin { get; set; } +} + +public class SkinService : IGuiSkinService +{ + public Skin Skin { get; set; } +} + +public class SkinJsonConverter : JsonConverter<Skin> +{ + private readonly ContentManager _contentManager; + private readonly IGuiSkinService _skinService; + private readonly Type[] _customControlTypes; + + public SkinJsonConverter(ContentManager contentManager, IGuiSkinService skinService, params Type[] customControlTypes) + { + _contentManager = contentManager; + _skinService = skinService; + _customControlTypes = customControlTypes; + } + + /// <inheritdoc /> + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Skin); + + /// <inheritdoc /> + public override Skin Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + var assetName = reader.GetString(); + + // TODO: Load this using the ContentManager instead. + using (var stream = TitleContainer.OpenStream(assetName)) + { + var skin = Skin.FromStream(_contentManager, stream, _customControlTypes); + _skinService.Skin = skin; + return skin; + } + + } + + throw new InvalidOperationException($"{nameof(SkinJsonConverter)} can only convert from a string"); + } + + /// <inheritdoc /> + public override void Write(Utf8JsonWriter writer, Skin value, JsonSerializerOptions options) { } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/VerticalAlignmentConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/VerticalAlignmentConverter.cs new file mode 100644 index 0000000..bc55cda --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Serialization/VerticalAlignmentConverter.cs @@ -0,0 +1,32 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MonoGame.Extended.Gui.Serialization; + +public class VerticalAlignmentConverter : JsonConverter<VerticalAlignment> +{ + /// <inheritdoc /> + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(VerticalAlignment); + + /// <inheritdoc /> + public override VerticalAlignment Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + + if (value.Equals("Center", StringComparison.OrdinalIgnoreCase) || value.Equals("Centre", StringComparison.OrdinalIgnoreCase)) + { + return VerticalAlignment.Centre; + } + + if (Enum.TryParse<VerticalAlignment>(value, true, out var alignment)) + { + return alignment; + } + + throw new InvalidOperationException($"Invalid value for '{nameof(VerticalAlignment)}'"); + } + + /// <inheritdoc /> + public override void Write(Utf8JsonWriter writer, VerticalAlignment value, JsonSerializerOptions options) { } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Skin.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Skin.cs new file mode 100644 index 0000000..77476be --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Skin.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.IO; +using MonoGame.Extended.BitmapFonts; +using System.Linq; +using System.Reflection; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using MonoGame.Extended.Collections; +using MonoGame.Extended.Gui.Controls; +using MonoGame.Extended.Gui.Serialization; +using MonoGame.Extended.TextureAtlases; +using System.Text.Json.Serialization; +using System.Text.Json; + +namespace MonoGame.Extended.Gui +{ + public class Skin + { + public Skin() + { + TextureAtlases = new List<TextureAtlas>(); + Fonts = new List<BitmapFont>(); + NinePatches = new List<NinePatchRegion2D>(); + Styles = new KeyedCollection<string, ControlStyle>(s => s.Name ?? s.TargetType.Name); + } + + [JsonPropertyOrder(0)] + public string Name { get; set; } + + [JsonPropertyOrder(1)] + public IList<TextureAtlas> TextureAtlases { get; set; } + + [JsonPropertyOrder(2)] + public IList<BitmapFont> Fonts { get; set; } + + [JsonPropertyOrder(3)] + public IList<NinePatchRegion2D> NinePatches { get; set; } + + [JsonPropertyOrder(4)] + public BitmapFont DefaultFont => Fonts.FirstOrDefault(); + + [JsonPropertyOrder(5)] + public Cursor Cursor { get; set; } + + [JsonPropertyOrder(6)] + public KeyedCollection<string, ControlStyle> Styles { get; private set; } + + public ControlStyle GetStyle(string name) + { + if (Styles.TryGetValue(name, out var controlStyle)) + return controlStyle; + + return null; + } + + public ControlStyle GetStyle(Type controlType) + { + return GetStyle(controlType.FullName); + } + + public void Apply(Control control) + { + // TODO: This allocates memory on each apply because it needs to apply styles in reverse + var types = new List<Type>(); + var controlType = control.GetType(); + + while (controlType != null) + { + types.Add(controlType); + controlType = controlType.GetTypeInfo().BaseType; + } + + for (var i = types.Count - 1; i >= 0; i--) + { + var style = GetStyle(types[i]); + style?.Apply(control); + } + } + + public static Skin FromFile(ContentManager contentManager, string path, params Type[] customControlTypes) + { + using (var stream = TitleContainer.OpenStream(path)) + { + return FromStream(contentManager, stream, customControlTypes); + } + } + + public static Skin FromStream(ContentManager contentManager, Stream stream, params Type[] customControlTypes) + { + var options = GuiJsonSerializerOptionsProvider.GetOptions(contentManager, customControlTypes); + return JsonSerializer.Deserialize<Skin>(stream, options); + } + + + public T Create<T>(string template, Action<T> onCreate) + where T : Control, new() + { + var control = new T(); + GetStyle(template).Apply(control); + onCreate(control); + return control; + } + + public Control Create(Type type, string template) + { + var control = (Control)Activator.CreateInstance(type); + + if (template != null) + { + var style = GetStyle(template); + if (style != null) + style.Apply(control); + else + throw new FormatException($"invalid style {template} for control {type.Name}"); + } + + return control; + } + + public static Skin Default { get; set; } + + public static Skin CreateDefault(BitmapFont font) + { + Default = new Skin + { + Fonts = { font }, + Styles = + { + new ControlStyle(typeof(Control)) { + {nameof(Control.BackgroundColor), new Color(51, 51, 55)}, + {nameof(Control.BorderColor), new Color(67, 67, 70)}, + {nameof(Control.BorderThickness), 1}, + {nameof(Control.TextColor), new Color(241, 241, 241)}, + {nameof(Control.Padding), new Thickness(5)}, + {nameof(Control.DisabledStyle), new ControlStyle(typeof(Control)) { + { nameof(Control.TextColor), new Color(78,78,80) } + } + } + }, + new ControlStyle(typeof(LayoutControl)) { + {nameof(Control.BackgroundColor), Color.Transparent}, + {nameof(Control.BorderColor), Color.Transparent }, + {nameof(Control.BorderThickness), 0}, + {nameof(Control.Padding), new Thickness(0)}, + {nameof(Control.Margin), new Thickness(0)}, + }, + new ControlStyle(typeof(ComboBox)) { + {nameof(ComboBox.DropDownColor), new Color(71, 71, 75)}, + {nameof(ComboBox.SelectedItemColor), new Color(0, 122, 204)}, + {nameof(ComboBox.HorizontalTextAlignment), HorizontalAlignment.Left } + }, + new ControlStyle(typeof(CheckBox)) + { + {nameof(CheckBox.HorizontalTextAlignment), HorizontalAlignment.Left }, + {nameof(CheckBox.BorderThickness), 0}, + {nameof(CheckBox.BackgroundColor), Color.Transparent}, + }, + new ControlStyle(typeof(ListBox)) + { + {nameof(ListBox.SelectedItemColor), new Color(0, 122, 204)}, + {nameof(ListBox.HorizontalTextAlignment), HorizontalAlignment.Left } + }, + new ControlStyle(typeof(Label)) { + {nameof(Label.BackgroundColor), Color.Transparent}, + {nameof(Label.TextColor), Color.White}, + {nameof(Label.BorderColor), Color.Transparent}, + {nameof(Label.BorderThickness), 0}, + {nameof(Label.HorizontalTextAlignment), HorizontalAlignment.Left}, + {nameof(Label.VerticalTextAlignment), VerticalAlignment.Bottom}, + {nameof(Control.Margin), new Thickness(5,0)}, + {nameof(Control.Padding), new Thickness(0)}, + }, + new ControlStyle(typeof(TextBox)) { + {nameof(Control.BackgroundColor), Color.DarkGray}, + {nameof(Control.TextColor), Color.Black}, + {nameof(Control.BorderColor), new Color(67, 67, 70)}, + {nameof(Control.BorderThickness), 2}, + }, + new ControlStyle(typeof(TextBox2)) { + {nameof(Control.BackgroundColor), Color.DarkGray}, + {nameof(Control.TextColor), Color.Black}, + {nameof(Control.BorderColor), new Color(67, 67, 70)}, + {nameof(Control.BorderThickness), 2}, + }, + new ControlStyle(typeof(Button)) { + { + nameof(Button.HoverStyle), new ControlStyle { + {nameof(Button.BackgroundColor), new Color(62, 62, 64)}, + {nameof(Button.BorderColor), Color.WhiteSmoke } + } + }, + { + nameof(Button.PressedStyle), new ControlStyle { + {nameof(Button.BackgroundColor), new Color(0, 122, 204)} + } + } + }, + new ControlStyle(typeof(ToggleButton)) { + { + nameof(ToggleButton.CheckedStyle), new ControlStyle { + {nameof(Button.BackgroundColor), new Color(0, 122, 204)} + } + }, + { + nameof(ToggleButton.CheckedHoverStyle), new ControlStyle { + {nameof(Button.BorderColor), Color.WhiteSmoke} + } + } + }, + new ControlStyle(typeof(ProgressBar)) { + {nameof(ProgressBar.BarColor), new Color(0, 122, 204) }, + {nameof(ProgressBar.Height), 32 }, + {nameof(ProgressBar.Padding), new Thickness(5, 4)}, + } + } + }; + return Default; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Window.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Window.cs new file mode 100644 index 0000000..12f4dcf --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/Window.cs @@ -0,0 +1,42 @@ +using System; +using Microsoft.Xna.Framework; +using MonoGame.Extended.Gui.Controls; + +namespace MonoGame.Extended.Gui +{ + //public class Window : Element<Screen> + //{ + // public Window(Screen parent) + // { + // Parent = parent; + // } + + // public ControlCollection Controls { get; } = new ControlCollection(); + + // public void Show() + // { + // Parent.Windows.Add(this); + // } + + // public void Hide() + // { + // Parent.Windows.Remove(this); + // } + + // public override void Draw(IGuiContext context, IGuiRenderer renderer, float deltaSeconds) + // { + // renderer.FillRectangle(BoundingRectangle, Color.Magenta); + // } + + // public Size2 GetDesiredSize(IGuiContext context, Size2 availableSize) + // { + // return new Size2(Width, Height); + // } + + // public void Layout(IGuiContext context, RectangleF rectangle) + // { + // foreach (var control in Controls) + // LayoutHelper.PlaceControl(context, control, rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height); + // } + //} +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/WindowCollection.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/WindowCollection.cs new file mode 100644 index 0000000..d22f274 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Gui/WindowCollection.cs @@ -0,0 +1,10 @@ +namespace MonoGame.Extended.Gui +{ + //public class WindowCollection : ElementCollection<Window, Screen> + //{ + // public WindowCollection(Screen parent) + // : base(parent) + // { + // } + //} +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/ExtendedPlayerIndex.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/ExtendedPlayerIndex.cs new file mode 100644 index 0000000..a153f02 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/ExtendedPlayerIndex.cs @@ -0,0 +1,32 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Input +{ + /// <summary>Player index enumeration with slots for 8 players</summary> + public enum ExtendedPlayerIndex + { + /// <summary>First player</summary> + One = PlayerIndex.One, + + /// <summary>Second player</summary> + Two = PlayerIndex.Two, + + /// <summary>Third player</summary> + Three = PlayerIndex.Three, + + /// <summary>Fourth player</summary> + Four = PlayerIndex.Four, + + /// <summary>Fifth player</summary> + Five, + + /// <summary>Sixth player</summary> + Six, + + /// <summary>Seventh player</summary> + Seven, + + /// <summary>Eigth player</summary> + Eight + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadEventArgs.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadEventArgs.cs new file mode 100644 index 0000000..9b113c6 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadEventArgs.cs @@ -0,0 +1,62 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +namespace MonoGame.Extended.Input.InputListeners +{ + /// <summary> + /// This class contains all information resulting from events fired by + /// <see cref="GamePadListener" />. + /// </summary> + public class GamePadEventArgs : EventArgs + { + public GamePadEventArgs(GamePadState previousState, GamePadState currentState, + TimeSpan elapsedTime, PlayerIndex playerIndex, Buttons? button = null, + float triggerState = 0, Vector2? thumbStickState = null) + { + PlayerIndex = playerIndex; + PreviousState = previousState; + CurrentState = currentState; + ElapsedTime = elapsedTime; + if (button != null) + Button = button.Value; + TriggerState = triggerState; + ThumbStickState = thumbStickState ?? Vector2.Zero; + } + + /// <summary> + /// The index of the controller. + /// </summary> + public PlayerIndex PlayerIndex { get; private set; } + + /// <summary> + /// The state of the controller in the previous update. + /// </summary> + public GamePadState PreviousState { get; private set; } + + /// <summary> + /// The state of the controller in this update. + /// </summary> + public GamePadState CurrentState { get; private set; } + + /// <summary> + /// The button that triggered this event, if appliable. + /// </summary> + public Buttons Button { get; private set; } + + /// <summary> + /// The time elapsed since last event. + /// </summary> + public TimeSpan ElapsedTime { get; private set; } + + /// <summary> + /// If a TriggerMoved event, displays the responsible trigger's position. + /// </summary> + public float TriggerState { get; private set; } + + /// <summary> + /// If a ThumbStickMoved event, displays the responsible stick's position. + /// </summary> + public Vector2 ThumbStickState { get; private set; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadListener.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadListener.cs new file mode 100644 index 0000000..b7ea79b --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadListener.cs @@ -0,0 +1,529 @@ +using System; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +namespace MonoGame.Extended.Input.InputListeners +{ + /// <summary> + /// This is a listener that exposes several events for easier handling of gamepads. + /// </summary> + public class GamePadListener : InputListener + { + private static readonly bool[] _gamePadConnections = new bool[4]; + + // These buttons are not to be evaluated normally, but with the debounce filter + // in their respective methods. + private readonly Buttons[] _excludedButtons = + { + Buttons.LeftTrigger, Buttons.RightTrigger, + Buttons.LeftThumbstickDown, Buttons.LeftThumbstickUp, Buttons.LeftThumbstickRight, + Buttons.LeftThumbstickLeft, + Buttons.RightThumbstickLeft, Buttons.RightThumbstickRight, Buttons.RightThumbstickUp, + Buttons.RightThumbstickDown + }; + + private GamePadState _currentState; + //private int _lastPacketNumber; + // Implementation doesn't work, see explanation in CheckAllButtons(). + private GameTime _gameTime; + private Buttons _lastButton; + private Buttons _lastLeftStickDirection; + private Buttons _lastRightStickDirection; + private GamePadState _lastThumbStickState; + + private GamePadState _lastTriggerState; + + private float _leftCurVibrationStrength; + private bool _leftStickDown; + private bool _leftTriggerDown; + private bool _leftVibrating; + private GameTime _previousGameTime; + private GamePadState _previousState; + private int _repeatedButtonTimer; + private float _rightCurVibrationStrength; + private bool _rightStickDown; + private bool _rightTriggerDown; + private bool _rightVibrating; + private TimeSpan _vibrationDurationLeft; + private TimeSpan _vibrationDurationRight; + private TimeSpan _vibrationStart; + + private float _vibrationStrengthLeft; + private float _vibrationStrengthRight; + + public GamePadListener() + : this(new GamePadListenerSettings()) + { + } + + public GamePadListener(GamePadListenerSettings settings) + { + PlayerIndex = settings.PlayerIndex; + VibrationEnabled = settings.VibrationEnabled; + VibrationStrengthLeft = settings.VibrationStrengthLeft; + VibrationStrengthRight = settings.VibrationStrengthRight; + ThumbStickDeltaTreshold = settings.ThumbStickDeltaTreshold; + ThumbstickDownTreshold = settings.ThumbstickDownTreshold; + TriggerDeltaTreshold = settings.TriggerDeltaTreshold; + TriggerDownTreshold = settings.TriggerDownTreshold; + RepeatInitialDelay = settings.RepeatInitialDelay; + RepeatDelay = settings.RepeatDelay; + + _previousGameTime = new GameTime(); + _previousState = GamePadState.Default; + } + + /// <summary> + /// If set to true, the static event <see cref="ControllerConnectionChanged" /> + /// will fire when any controller changes in connectivity status. + /// <para> + /// This functionality requires that you have one actively updating + /// <see cref="InputListenerManager" />. + /// </para> + /// </summary> + public static bool CheckControllerConnections { get; set; } + + /// <summary> + /// The index of the controller. + /// </summary> + public PlayerIndex PlayerIndex { get; } + + /// <summary> + /// When a button is held down, the interval in which + /// ButtonRepeated fires. Value in milliseconds. + /// </summary> + public int RepeatDelay { get; } + + /// <summary> + /// The amount of time a button has to be held down + /// in order to fire ButtonRepeated the first time. + /// Value in milliseconds. + /// </summary> + public int RepeatInitialDelay { get; } + + /// <summary> + /// Whether vibration is enabled for this controller. + /// </summary> + public bool VibrationEnabled { get; set; } + + /// <summary> + /// General setting for the strength of the left motor. + /// This motor has a slow, deep, powerful rumble. + /// <para> + /// This setting will modify all future vibrations + /// through this listener. + /// </para> + /// </summary> + public float VibrationStrengthLeft + { + get { return _vibrationStrengthLeft; } + // Clamp the value, just to be sure. + set { _vibrationStrengthLeft = MathHelper.Clamp(value, 0, 1); } + } + + /// <summary> + /// General setting for the strength of the right motor. + /// This motor has a snappy, quick, high-pitched rumble. + /// <para> + /// This setting will modify all future vibrations + /// through this listener. + /// </para> + /// </summary> + public float VibrationStrengthRight + { + get { return _vibrationStrengthRight; } + // Clamp the value, just to be sure. + set { _vibrationStrengthRight = MathHelper.Clamp(value, 0, 1); } + } + + /// <summary> + /// The treshold of movement that has to be met in order + /// for the listener to fire an event with the trigger's + /// updated position. + /// <para> + /// In essence this defines the event's + /// resolution. + /// </para> + /// At a value of 0 this will fire every time + /// the trigger's position is not 0f. + /// </summary> + public float TriggerDeltaTreshold { get; } + + /// <summary> + /// The treshold of movement that has to be met in order + /// for the listener to fire an event with the thumbstick's + /// updated position. + /// <para> + /// In essence this defines the event's + /// resolution. + /// </para> + /// At a value of 0 this will fire every time + /// the thumbstick's position is not {x:0, y:0}. + /// </summary> + public float ThumbStickDeltaTreshold { get; } + + /// <summary> + /// How deep the triggers have to be depressed in order to + /// register as a ButtonDown event. + /// </summary> + public float TriggerDownTreshold { get; } + + /// <summary> + /// How deep the triggers have to be depressed in order to + /// register as a ButtonDown event. + /// </summary> + public float ThumbstickDownTreshold { get; } + + /// <summary> + /// This event fires whenever a controller connects or disconnects. + /// <para> + /// In order + /// for it to work, the <see cref="CheckControllerConnections" /> property must + /// be set to true. + /// </para> + /// </summary> + public static event EventHandler<GamePadEventArgs> ControllerConnectionChanged; + + /// <summary> + /// This event fires whenever a button changes from the Up + /// to the Down state. + /// </summary> + public event EventHandler<GamePadEventArgs> ButtonDown; + + /// <summary> + /// This event fires whenever a button changes from the Down + /// to the Up state. + /// </summary> + public event EventHandler<GamePadEventArgs> ButtonUp; + + /// <summary> + /// This event fires repeatedly whenever a button is held sufficiently + /// long. Use this for things like menu navigation. + /// </summary> + public event EventHandler<GamePadEventArgs> ButtonRepeated; + + /// <summary> + /// This event fires whenever a thumbstick changes position. + /// <para> + /// The parameter governing the sensitivity of this functionality + /// is <see cref="GamePadListenerSettings.ThumbStickDeltaTreshold" />. + /// </para> + /// </summary> + public event EventHandler<GamePadEventArgs> ThumbStickMoved; + + /// <summary> + /// This event fires whenever a trigger changes position. + /// <para> + /// The parameter governing the sensitivity of this functionality + /// is <see cref="GamePadListenerSettings.TriggerDeltaTreshold" />. + /// </para> + /// </summary> + public event EventHandler<GamePadEventArgs> TriggerMoved; + + + /// <summary> + /// Send a vibration command to the controller. + /// Returns true if the operation succeeded. + /// <para> + /// Motor values that are unset preserve + /// their current vibration strength and duration. + /// </para> + /// Note: Vibration currently only works on select platforms, + /// like Monogame.Windows. + /// </summary> + /// <param name="durationMs">Duration of the vibration in milliseconds.</param> + /// <param name="leftStrength"> + /// The strength of the left motor. + /// This motor has a slow, deep, powerful rumble. + /// </param> + /// <param name="rightStrength"> + /// The strength of the right motor. + /// This motor has a snappy, quick, high-pitched rumble. + /// </param> + /// <returns>Returns true if the operation succeeded.</returns> + public bool Vibrate(int durationMs, float leftStrength = float.NegativeInfinity, + float rightStrength = float.NegativeInfinity) + { + if (!VibrationEnabled) + return false; + + var lstrength = MathHelper.Clamp(leftStrength, 0, 1); + var rstrength = MathHelper.Clamp(rightStrength, 0, 1); + + if (float.IsNegativeInfinity(leftStrength)) + lstrength = _leftCurVibrationStrength; + if (float.IsNegativeInfinity(rightStrength)) + rstrength = _rightCurVibrationStrength; + + var success = GamePad.SetVibration(PlayerIndex, lstrength*VibrationStrengthLeft, + rstrength*VibrationStrengthRight); + if (success) + { + _leftVibrating = true; + _rightVibrating = true; + + if (leftStrength > 0) + _vibrationDurationLeft = new TimeSpan(0, 0, 0, 0, durationMs); + else + { + if (lstrength > 0) + _vibrationDurationLeft -= _gameTime.TotalGameTime - _vibrationStart; + else + _leftVibrating = false; + } + + if (rightStrength > 0) + _vibrationDurationRight = new TimeSpan(0, 0, 0, 0, durationMs); + else + { + if (rstrength > 0) + _vibrationDurationRight -= _gameTime.TotalGameTime - _vibrationStart; + else + _rightVibrating = false; + } + + _vibrationStart = _gameTime.TotalGameTime; + + _leftCurVibrationStrength = lstrength; + _rightCurVibrationStrength = rstrength; + } + return success; + } + + private void CheckAllButtons() + { + // PacketNumber only and always changes if there is a difference between GamePadStates. + // ...At least, that's the theory. It doesn't seem to be implemented. Disabled for now. + //if (_lastPacketNumber == _currentState.PacketNumber) + // return; + foreach (Buttons button in Enum.GetValues(typeof(Buttons))) + { + if (_excludedButtons.Contains(button)) + break; + if (_currentState.IsButtonDown(button) && _previousState.IsButtonUp(button)) + RaiseButtonDown(button); + if (_currentState.IsButtonUp(button) && _previousState.IsButtonDown(button)) + RaiseButtonUp(button); + } + + // Checks triggers as buttons and floats + CheckTriggers(s => s.Triggers.Left, Buttons.LeftTrigger); + CheckTriggers(s => s.Triggers.Right, Buttons.RightTrigger); + + // Checks thumbsticks as vector2s + CheckThumbSticks(s => s.ThumbSticks.Right, Buttons.RightStick); + CheckThumbSticks(s => s.ThumbSticks.Left, Buttons.LeftStick); + } + + private void CheckTriggers(Func<GamePadState, float> getButtonState, Buttons button) + { + var debounce = 0.05f; // Value used to qualify a trigger as coming Up from a Down state + var curstate = getButtonState(_currentState); + var curdown = curstate > TriggerDownTreshold; + var prevdown = button == Buttons.RightTrigger ? _rightTriggerDown : _leftTriggerDown; + + if (!prevdown && curdown) + { + RaiseButtonDown(button); + if (button == Buttons.RightTrigger) + _rightTriggerDown = true; + else + _leftTriggerDown = true; + } + else + { + if (prevdown && (curstate < debounce)) + { + RaiseButtonUp(button); + if (button == Buttons.RightTrigger) + _rightTriggerDown = false; + else + _leftTriggerDown = false; + } + } + + var prevstate = getButtonState(_lastTriggerState); + if (curstate > TriggerDeltaTreshold) + { + if (Math.Abs(prevstate - curstate) >= TriggerDeltaTreshold) + { + TriggerMoved?.Invoke(this, MakeArgs(button, curstate)); + _lastTriggerState = _currentState; + } + } + else + { + if (prevstate > TriggerDeltaTreshold) + { + TriggerMoved?.Invoke(this, MakeArgs(button, curstate)); + _lastTriggerState = _currentState; + } + } + } + + private void CheckThumbSticks(Func<GamePadState, Vector2> getButtonState, Buttons button) + { + const float debounce = 0.15f; + var curVector = getButtonState(_currentState); + var curdown = curVector.Length() > ThumbstickDownTreshold; + var right = button == Buttons.RightStick; + var prevdown = right ? _rightStickDown : _leftStickDown; + + var prevdir = button == Buttons.RightStick ? _lastRightStickDirection : _lastLeftStickDirection; + Buttons curdir; + if (curVector.Y > curVector.X) + { + if (curVector.Y > -curVector.X) + curdir = right ? Buttons.RightThumbstickUp : Buttons.LeftThumbstickUp; + else + curdir = right ? Buttons.RightThumbstickLeft : Buttons.LeftThumbstickLeft; + } + else + { + if (curVector.Y < -curVector.X) + curdir = right ? Buttons.RightThumbstickDown : Buttons.LeftThumbstickDown; + else + curdir = right ? Buttons.RightThumbstickRight : Buttons.LeftThumbstickRight; + } + + if (!prevdown && curdown) + { + if (right) + _lastRightStickDirection = curdir; + else + _lastLeftStickDirection = curdir; + + RaiseButtonDown(curdir); + if (button == Buttons.RightStick) + _rightStickDown = true; + else + _leftStickDown = true; + } + else + { + if (prevdown && (curVector.Length() < debounce)) + { + RaiseButtonUp(prevdir); + if (button == Buttons.RightStick) + _rightStickDown = false; + else + _leftStickDown = false; + } + else + { + if (prevdown && curdown && (curdir != prevdir)) + { + RaiseButtonUp(prevdir); + if (right) + _lastRightStickDirection = curdir; + else + _lastLeftStickDirection = curdir; + RaiseButtonDown(curdir); + } + } + } + + var prevVector = getButtonState(_lastThumbStickState); + if (curVector.Length() > ThumbStickDeltaTreshold) + { + if (Vector2.Distance(curVector, prevVector) >= ThumbStickDeltaTreshold) + { + ThumbStickMoved?.Invoke(this, MakeArgs(button, thumbStickState: curVector)); + _lastThumbStickState = _currentState; + } + } + else + { + if (prevVector.Length() > ThumbStickDeltaTreshold) + { + ThumbStickMoved?.Invoke(this, MakeArgs(button, thumbStickState: curVector)); + _lastThumbStickState = _currentState; + } + } + } + + internal static void CheckConnections() + { + if (!CheckControllerConnections) + return; + + foreach (PlayerIndex index in Enum.GetValues(typeof(PlayerIndex))) + { + if (GamePad.GetState(index).IsConnected ^ _gamePadConnections[(int) index]) + // We need more XORs in this world + { + _gamePadConnections[(int) index] = !_gamePadConnections[(int) index]; + ControllerConnectionChanged?.Invoke(null, + new GamePadEventArgs(GamePadState.Default, GamePad.GetState(index), TimeSpan.Zero, index)); + } + } + } + + private void CheckVibrate() + { + if (_leftVibrating && (_vibrationStart + _vibrationDurationLeft < _gameTime.TotalGameTime)) + Vibrate(0, 0); + if (_rightVibrating && (_vibrationStart + _vibrationDurationRight < _gameTime.TotalGameTime)) + Vibrate(0, rightStrength: 0); + } + + public override void Update(GameTime gameTime) + { + _gameTime = gameTime; + _currentState = GamePad.GetState(PlayerIndex); + CheckVibrate(); + if (!_currentState.IsConnected) + return; + CheckAllButtons(); + CheckRepeatButton(); + //_lastPacketNumber = _currentState.PacketNumber; + _previousGameTime = gameTime; + _previousState = _currentState; + } + + private GamePadEventArgs MakeArgs(Buttons? button, + float triggerstate = 0, Vector2? thumbStickState = null) + { + var elapsedTime = _gameTime.TotalGameTime - _previousGameTime.TotalGameTime; + return new GamePadEventArgs(_previousState, _currentState, + elapsedTime, PlayerIndex, button, triggerstate, thumbStickState); + } + + private void RaiseButtonDown(Buttons button) + { + ButtonDown?.Invoke(this, MakeArgs(button)); + ButtonRepeated?.Invoke(this, MakeArgs(button)); + _lastButton = button; + _repeatedButtonTimer = 0; + } + + private void RaiseButtonUp(Buttons button) + { + ButtonUp?.Invoke(this, MakeArgs(button)); + _lastButton = 0; + } + + private void CheckRepeatButton() + { + _repeatedButtonTimer += _gameTime.ElapsedGameTime.Milliseconds; + + if ((_repeatedButtonTimer < RepeatInitialDelay) || (_lastButton == 0)) + return; + + if (_repeatedButtonTimer < RepeatInitialDelay + RepeatDelay) + { + ButtonRepeated?.Invoke(this, MakeArgs(_lastButton)); + _repeatedButtonTimer = RepeatDelay + RepeatInitialDelay; + } + else + { + if (_repeatedButtonTimer > RepeatInitialDelay + RepeatDelay*2) + { + ButtonRepeated?.Invoke(this, MakeArgs(_lastButton)); + _repeatedButtonTimer = RepeatDelay + RepeatInitialDelay; + } + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadListenerSettings.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadListenerSettings.cs new file mode 100644 index 0000000..8c36e4c --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/GamePadListenerSettings.cs @@ -0,0 +1,134 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Input.InputListeners +{ + /// <summary> + /// This is a class that contains settings to be used to initialise a <see cref="GamePadListener" />. + /// </summary> + /// <seealso cref="InputListenerManager" /> + public class GamePadListenerSettings : InputListenerSettings<GamePadListener> + { + public GamePadListenerSettings() + : this(PlayerIndex.One) + { + } + + /// <summary> + /// This is a class that contains settings to be used to initialise a <see cref="GamePadListener" />. + /// <para>Note: There are a number of extra settings that are settable properties.</para> + /// </summary> + /// <param name="playerIndex">The index of the controller the listener will be tied to.</param> + /// <param name="vibrationEnabled">Whether vibration is enabled on the controller.</param> + /// <param name="vibrationStrengthLeft"> + /// General setting for the strength of the left motor. + /// This motor has a slow, deep, powerful rumble. + /// This setting will modify all future vibrations + /// through this listener. + /// </param> + /// <param name="vibrationStrengthRight"> + /// General setting for the strength of the right motor. + /// This motor has a snappy, quick, high-pitched rumble. + /// This setting will modify all future vibrations + /// through this listener. + /// </param> + public GamePadListenerSettings(PlayerIndex playerIndex, bool vibrationEnabled = true, + float vibrationStrengthLeft = 1.0f, float vibrationStrengthRight = 1.0f) + { + PlayerIndex = playerIndex; + VibrationEnabled = vibrationEnabled; + VibrationStrengthLeft = vibrationStrengthLeft; + VibrationStrengthRight = vibrationStrengthRight; + TriggerDownTreshold = 0.15f; + ThumbstickDownTreshold = 0.5f; + RepeatInitialDelay = 500; + RepeatDelay = 50; + } + + /// <summary> + /// The index of the controller. + /// </summary> + public PlayerIndex PlayerIndex { get; set; } + + /// <summary> + /// When a button is held down, the interval in which + /// ButtonRepeated fires. Value in milliseconds. + /// </summary> + public int RepeatDelay { get; set; } + + /// <summary> + /// The amount of time a button has to be held down + /// in order to fire ButtonRepeated the first time. + /// Value in milliseconds. + /// </summary> + public int RepeatInitialDelay { get; set; } + + + /// <summary> + /// Whether vibration is enabled for this controller. + /// </summary> + public bool VibrationEnabled { get; set; } + + /// <summary> + /// General setting for the strength of the left motor. + /// This motor has a slow, deep, powerful rumble. + /// <para> + /// This setting will modify all future vibrations + /// through this listener. + /// </para> + /// </summary> + public float VibrationStrengthLeft { get; set; } + + /// <summary> + /// General setting for the strength of the right motor. + /// This motor has a snappy, quick, high-pitched rumble. + /// <para> + /// This setting will modify all future vibrations + /// through this listener. + /// </para> + /// </summary> + public float VibrationStrengthRight { get; set; } + + /// <summary> + /// The treshold of movement that has to be met in order + /// for the listener to fire an event with the trigger's + /// updated position. + /// <para> + /// In essence this defines the event's + /// resolution. + /// </para> + /// At a value of 0 this will fire every time + /// the trigger's position is not 0f. + /// </summary> + public float TriggerDeltaTreshold { get; set; } + + /// <summary> + /// The treshold of movement that has to be met in order + /// for the listener to fire an event with the thumbstick's + /// updated position. + /// <para> + /// In essence this defines the event's + /// resolution. + /// </para> + /// At a value of 0 this will fire every time + /// the thumbstick's position is not {x:0, y:0}. + /// </summary> + public float ThumbStickDeltaTreshold { get; set; } + + /// <summary> + /// How deep the triggers have to be depressed in order to + /// register as a ButtonDown event. + /// </summary> + public float TriggerDownTreshold { get; set; } + + /// <summary> + /// How deep the triggers have to be depressed in order to + /// register as a ButtonDown event. + /// </summary> + public float ThumbstickDownTreshold { get; private set; } + + public override GamePadListener CreateListener() + { + return new GamePadListener(this); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/IInputService.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/IInputService.cs new file mode 100644 index 0000000..46198ec --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/IInputService.cs @@ -0,0 +1,13 @@ +namespace MonoGame.Extended.Input.InputListeners +{ + public interface IInputService + { + KeyboardListener GuiKeyboardListener { get; } + + MouseListener GuiMouseListener { get; } + + GamePadListener GuiGamePadListener { get; } + + TouchListener GuiTouchListener { get; } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListener.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListener.cs new file mode 100644 index 0000000..6323295 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListener.cs @@ -0,0 +1,13 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Input.InputListeners +{ + public abstract class InputListener + { + protected InputListener() + { + } + + public abstract void Update(GameTime gameTime); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListenerComponent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListenerComponent.cs new file mode 100644 index 0000000..302f6b5 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListenerComponent.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Input.InputListeners +{ + public class InputListenerComponent : GameComponent, IUpdate + { + private readonly List<InputListener> _listeners; + + public InputListenerComponent(Game game) + : base(game) + { + _listeners = new List<InputListener>(); + } + + public InputListenerComponent(Game game, params InputListener[] listeners) + : base(game) + { + _listeners = new List<InputListener>(listeners); + } + + public IList<InputListener> Listeners => _listeners; + + public override void Update(GameTime gameTime) + { + base.Update(gameTime); + + if (Game.IsActive) + { + foreach (var listener in _listeners) + listener.Update(gameTime); + } + + GamePadListener.CheckConnections(); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListenerSettings.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListenerSettings.cs new file mode 100644 index 0000000..468a30b --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/InputListenerSettings.cs @@ -0,0 +1,8 @@ +namespace MonoGame.Extended.Input.InputListeners +{ + public abstract class InputListenerSettings<T> + where T : InputListener + { + public abstract T CreateListener(); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardEventArgs.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardEventArgs.cs new file mode 100644 index 0000000..d6d01ab --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardEventArgs.cs @@ -0,0 +1,119 @@ +using System; +using Microsoft.Xna.Framework.Input; + +namespace MonoGame.Extended.Input.InputListeners +{ + public class KeyboardEventArgs : EventArgs + { + public KeyboardEventArgs(Keys key, KeyboardState keyboardState) + { + Key = key; + + Modifiers = KeyboardModifiers.None; + + if (keyboardState.IsKeyDown(Keys.LeftControl) || keyboardState.IsKeyDown(Keys.RightControl)) + Modifiers |= KeyboardModifiers.Control; + + if (keyboardState.IsKeyDown(Keys.LeftShift) || keyboardState.IsKeyDown(Keys.RightShift)) + Modifiers |= KeyboardModifiers.Shift; + + if (keyboardState.IsKeyDown(Keys.LeftAlt) || keyboardState.IsKeyDown(Keys.RightAlt)) + Modifiers |= KeyboardModifiers.Alt; + } + + public Keys Key { get; } + public KeyboardModifiers Modifiers { get; } + + public char? Character => ToChar(Key, Modifiers); + + private static char? ToChar(Keys key, KeyboardModifiers modifiers = KeyboardModifiers.None) + { + var isShiftDown = (modifiers & KeyboardModifiers.Shift) == KeyboardModifiers.Shift; + + if (key == Keys.A) return isShiftDown ? 'A' : 'a'; + if (key == Keys.B) return isShiftDown ? 'B' : 'b'; + if (key == Keys.C) return isShiftDown ? 'C' : 'c'; + if (key == Keys.D) return isShiftDown ? 'D' : 'd'; + if (key == Keys.E) return isShiftDown ? 'E' : 'e'; + if (key == Keys.F) return isShiftDown ? 'F' : 'f'; + if (key == Keys.G) return isShiftDown ? 'G' : 'g'; + if (key == Keys.H) return isShiftDown ? 'H' : 'h'; + if (key == Keys.I) return isShiftDown ? 'I' : 'i'; + if (key == Keys.J) return isShiftDown ? 'J' : 'j'; + if (key == Keys.K) return isShiftDown ? 'K' : 'k'; + if (key == Keys.L) return isShiftDown ? 'L' : 'l'; + if (key == Keys.M) return isShiftDown ? 'M' : 'm'; + if (key == Keys.N) return isShiftDown ? 'N' : 'n'; + if (key == Keys.O) return isShiftDown ? 'O' : 'o'; + if (key == Keys.P) return isShiftDown ? 'P' : 'p'; + if (key == Keys.Q) return isShiftDown ? 'Q' : 'q'; + if (key == Keys.R) return isShiftDown ? 'R' : 'r'; + if (key == Keys.S) return isShiftDown ? 'S' : 's'; + if (key == Keys.T) return isShiftDown ? 'T' : 't'; + if (key == Keys.U) return isShiftDown ? 'U' : 'u'; + if (key == Keys.V) return isShiftDown ? 'V' : 'v'; + if (key == Keys.W) return isShiftDown ? 'W' : 'w'; + if (key == Keys.X) return isShiftDown ? 'X' : 'x'; + if (key == Keys.Y) return isShiftDown ? 'Y' : 'y'; + if (key == Keys.Z) return isShiftDown ? 'Z' : 'z'; + + if (((key == Keys.D0) && !isShiftDown) || (key == Keys.NumPad0)) return '0'; + if (((key == Keys.D1) && !isShiftDown) || (key == Keys.NumPad1)) return '1'; + if (((key == Keys.D2) && !isShiftDown) || (key == Keys.NumPad2)) return '2'; + if (((key == Keys.D3) && !isShiftDown) || (key == Keys.NumPad3)) return '3'; + if (((key == Keys.D4) && !isShiftDown) || (key == Keys.NumPad4)) return '4'; + if (((key == Keys.D5) && !isShiftDown) || (key == Keys.NumPad5)) return '5'; + if (((key == Keys.D6) && !isShiftDown) || (key == Keys.NumPad6)) return '6'; + if (((key == Keys.D7) && !isShiftDown) || (key == Keys.NumPad7)) return '7'; + if (((key == Keys.D8) && !isShiftDown) || (key == Keys.NumPad8)) return '8'; + if (((key == Keys.D9) && !isShiftDown) || (key == Keys.NumPad9)) return '9'; + + if ((key == Keys.D0) && isShiftDown) return ')'; + if ((key == Keys.D1) && isShiftDown) return '!'; + if ((key == Keys.D2) && isShiftDown) return '@'; + if ((key == Keys.D3) && isShiftDown) return '#'; + if ((key == Keys.D4) && isShiftDown) return '$'; + if ((key == Keys.D5) && isShiftDown) return '%'; + if ((key == Keys.D6) && isShiftDown) return '^'; + if ((key == Keys.D7) && isShiftDown) return '&'; + if ((key == Keys.D8) && isShiftDown) return '*'; + if ((key == Keys.D9) && isShiftDown) return '('; + + if (key == Keys.Space) return ' '; + if (key == Keys.Tab) return '\t'; + if (key == Keys.Enter) return (char) 13; + if (key == Keys.Back) return (char) 8; + + if (key == Keys.Add) return '+'; + if (key == Keys.Decimal) return '.'; + if (key == Keys.Divide) return '/'; + if (key == Keys.Multiply) return '*'; + if (key == Keys.OemBackslash) return '\\'; + if ((key == Keys.OemComma) && !isShiftDown) return ','; + if ((key == Keys.OemComma) && isShiftDown) return '<'; + if ((key == Keys.OemOpenBrackets) && !isShiftDown) return '['; + if ((key == Keys.OemOpenBrackets) && isShiftDown) return '{'; + if ((key == Keys.OemCloseBrackets) && !isShiftDown) return ']'; + if ((key == Keys.OemCloseBrackets) && isShiftDown) return '}'; + if ((key == Keys.OemPeriod) && !isShiftDown) return '.'; + if ((key == Keys.OemPeriod) && isShiftDown) return '>'; + if ((key == Keys.OemPipe) && !isShiftDown) return '\\'; + if ((key == Keys.OemPipe) && isShiftDown) return '|'; + if ((key == Keys.OemPlus) && !isShiftDown) return '='; + if ((key == Keys.OemPlus) && isShiftDown) return '+'; + if ((key == Keys.OemMinus) && !isShiftDown) return '-'; + if ((key == Keys.OemMinus) && isShiftDown) return '_'; + if ((key == Keys.OemQuestion) && !isShiftDown) return '/'; + if ((key == Keys.OemQuestion) && isShiftDown) return '?'; + if ((key == Keys.OemQuotes) && !isShiftDown) return '\''; + if ((key == Keys.OemQuotes) && isShiftDown) return '"'; + if ((key == Keys.OemSemicolon) && !isShiftDown) return ';'; + if ((key == Keys.OemSemicolon) && isShiftDown) return ':'; + if ((key == Keys.OemTilde) && !isShiftDown) return '`'; + if ((key == Keys.OemTilde) && isShiftDown) return '~'; + if (key == Keys.Subtract) return '-'; + + return null; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardListener.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardListener.cs new file mode 100644 index 0000000..9fc85a0 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardListener.cs @@ -0,0 +1,104 @@ +using System; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +namespace MonoGame.Extended.Input.InputListeners +{ + public class KeyboardListener : InputListener + { + private Array _keysValues = Enum.GetValues(typeof(Keys)); + + private bool _isInitial; + private TimeSpan _lastPressTime; + + private Keys _previousKey; + private KeyboardState _previousState; + + public KeyboardListener() + : this(new KeyboardListenerSettings()) + { + } + + public KeyboardListener(KeyboardListenerSettings settings) + { + RepeatPress = settings.RepeatPress; + InitialDelay = settings.InitialDelayMilliseconds; + RepeatDelay = settings.RepeatDelayMilliseconds; + } + + public bool RepeatPress { get; } + public int InitialDelay { get; } + public int RepeatDelay { get; } + + public event EventHandler<KeyboardEventArgs> KeyTyped; + public event EventHandler<KeyboardEventArgs> KeyPressed; + public event EventHandler<KeyboardEventArgs> KeyReleased; + + public override void Update(GameTime gameTime) + { + var currentState = Keyboard.GetState(); + + RaisePressedEvents(gameTime, currentState); + RaiseReleasedEvents(currentState); + + if (RepeatPress) + RaiseRepeatEvents(gameTime, currentState); + + _previousState = currentState; + } + + private void RaisePressedEvents(GameTime gameTime, KeyboardState currentState) + { + if (!currentState.IsKeyDown(Keys.LeftAlt) && !currentState.IsKeyDown(Keys.RightAlt)) + { + var pressedKeys = _keysValues + .Cast<Keys>() + .Where(key => currentState.IsKeyDown(key) && _previousState.IsKeyUp(key)); + + foreach (var key in pressedKeys) + { + var args = new KeyboardEventArgs(key, currentState); + + KeyPressed?.Invoke(this, args); + + if (args.Character.HasValue) + KeyTyped?.Invoke(this, args); + + _previousKey = key; + _lastPressTime = gameTime.TotalGameTime; + _isInitial = true; + } + } + } + + private void RaiseReleasedEvents(KeyboardState currentState) + { + var releasedKeys = _keysValues + .Cast<Keys>() + .Where(key => currentState.IsKeyUp(key) && _previousState.IsKeyDown(key)); + + foreach (var key in releasedKeys) + KeyReleased?.Invoke(this, new KeyboardEventArgs(key, currentState)); + } + + private void RaiseRepeatEvents(GameTime gameTime, KeyboardState currentState) + { + var elapsedTime = (gameTime.TotalGameTime - _lastPressTime).TotalMilliseconds; + + if (currentState.IsKeyDown(_previousKey) && + (_isInitial && elapsedTime > InitialDelay || !_isInitial && elapsedTime > RepeatDelay)) + { + var args = new KeyboardEventArgs(_previousKey, currentState); + + KeyPressed?.Invoke(this, args); + + if (args.Character.HasValue) + KeyTyped?.Invoke(this, args); + + _lastPressTime = gameTime.TotalGameTime; + _isInitial = false; + } + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardListenerSettings.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardListenerSettings.cs new file mode 100644 index 0000000..86481ed --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardListenerSettings.cs @@ -0,0 +1,21 @@ +namespace MonoGame.Extended.Input.InputListeners +{ + public class KeyboardListenerSettings : InputListenerSettings<KeyboardListener> + { + public KeyboardListenerSettings() + { + RepeatPress = true; + InitialDelayMilliseconds = 800; + RepeatDelayMilliseconds = 50; + } + + public bool RepeatPress { get; set; } + public int InitialDelayMilliseconds { get; set; } + public int RepeatDelayMilliseconds { get; set; } + + public override KeyboardListener CreateListener() + { + return new KeyboardListener(this); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardModifiers.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardModifiers.cs new file mode 100644 index 0000000..de59905 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/KeyboardModifiers.cs @@ -0,0 +1,13 @@ +using System; + +namespace MonoGame.Extended.Input.InputListeners +{ + [Flags] + public enum KeyboardModifiers + { + Control = 1, + Shift = 2, + Alt = 4, + None = 0 + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseEventArgs.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseEventArgs.cs new file mode 100644 index 0000000..2717325 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseEventArgs.cs @@ -0,0 +1,35 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; +using MonoGame.Extended.ViewportAdapters; + +namespace MonoGame.Extended.Input.InputListeners +{ + public class MouseEventArgs : EventArgs + { + public MouseEventArgs(ViewportAdapter viewportAdapter, TimeSpan time, MouseState previousState, + MouseState currentState, + MouseButton button = MouseButton.None) + { + PreviousState = previousState; + CurrentState = currentState; + Position = viewportAdapter?.PointToScreen(currentState.X, currentState.Y) + ?? new Point(currentState.X, currentState.Y); + Button = button; + ScrollWheelValue = currentState.ScrollWheelValue; + ScrollWheelDelta = currentState.ScrollWheelValue - previousState.ScrollWheelValue; + Time = time; + } + + public TimeSpan Time { get; } + + public MouseState PreviousState { get; } + public MouseState CurrentState { get; } + public Point Position { get; } + public MouseButton Button { get; } + public int ScrollWheelValue { get; } + public int ScrollWheelDelta { get; } + + public Vector2 DistanceMoved => CurrentState.Position.ToVector2() - PreviousState.Position.ToVector2(); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseListener.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseListener.cs new file mode 100644 index 0000000..e71a67f --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseListener.cs @@ -0,0 +1,193 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; +using MonoGame.Extended.ViewportAdapters; + +namespace MonoGame.Extended.Input.InputListeners +{ + /// <summary> + /// Handles mouse input. + /// </summary> + /// <remarks> + /// Due to nature of the listener, even when game is not in focus, listener will continue to be updated. + /// To avoid that, manual pause of Update() method is required whenever game loses focus. + /// To avoid having to do it manually, register listener to <see cref="InputListenerComponent" /> + /// </remarks> + public class MouseListener : InputListener + { + private MouseState _currentState; + private bool _dragging; + private GameTime _gameTime; + private bool _hasDoubleClicked; + private MouseEventArgs _mouseDownArgs; + private MouseEventArgs _previousClickArgs; + private MouseState _previousState; + + public MouseListener() + : this(new MouseListenerSettings()) + { + } + + public MouseListener(ViewportAdapter viewportAdapter) + : this(new MouseListenerSettings()) + { + ViewportAdapter = viewportAdapter; + } + + public MouseListener(MouseListenerSettings settings) + { + ViewportAdapter = settings.ViewportAdapter; + DoubleClickMilliseconds = settings.DoubleClickMilliseconds; + DragThreshold = settings.DragThreshold; + } + + public ViewportAdapter ViewportAdapter { get; } + + public int DoubleClickMilliseconds { get; } + public int DragThreshold { get; } + + /// <summary> + /// Returns true if the mouse has moved between the current and previous frames. + /// </summary> + /// <value><c>true</c> if the mouse has moved; otherwise, <c>false</c>.</value> + public bool HasMouseMoved => (_previousState.X != _currentState.X) || (_previousState.Y != _currentState.Y); + + public event EventHandler<MouseEventArgs> MouseDown; + public event EventHandler<MouseEventArgs> MouseUp; + public event EventHandler<MouseEventArgs> MouseClicked; + public event EventHandler<MouseEventArgs> MouseDoubleClicked; + public event EventHandler<MouseEventArgs> MouseMoved; + public event EventHandler<MouseEventArgs> MouseWheelMoved; + public event EventHandler<MouseEventArgs> MouseDragStart; + public event EventHandler<MouseEventArgs> MouseDrag; + public event EventHandler<MouseEventArgs> MouseDragEnd; + + private void CheckButtonPressed(Func<MouseState, ButtonState> getButtonState, MouseButton button) + { + if ((getButtonState(_currentState) == ButtonState.Pressed) && + (getButtonState(_previousState) == ButtonState.Released)) + { + var args = new MouseEventArgs(ViewportAdapter, _gameTime.TotalGameTime, _previousState, _currentState, button); + + MouseDown?.Invoke(this, args); + _mouseDownArgs = args; + + if (_previousClickArgs != null) + { + // If the last click was recent + var clickMilliseconds = (args.Time - _previousClickArgs.Time).TotalMilliseconds; + + if (clickMilliseconds <= DoubleClickMilliseconds) + { + MouseDoubleClicked?.Invoke(this, args); + _hasDoubleClicked = true; + } + + _previousClickArgs = null; + } + } + } + + private void CheckButtonReleased(Func<MouseState, ButtonState> getButtonState, MouseButton button) + { + if ((getButtonState(_currentState) == ButtonState.Released) && + (getButtonState(_previousState) == ButtonState.Pressed)) + { + var args = new MouseEventArgs(ViewportAdapter, _gameTime.TotalGameTime, _previousState, _currentState, button); + + if (_mouseDownArgs.Button == args.Button) + { + var clickMovement = DistanceBetween(args.Position, _mouseDownArgs.Position); + + // If the mouse hasn't moved much between mouse down and mouse up + if (clickMovement < DragThreshold) + { + if (!_hasDoubleClicked) + MouseClicked?.Invoke(this, args); + } + else // If the mouse has moved between mouse down and mouse up + { + MouseDragEnd?.Invoke(this, args); + _dragging = false; + } + } + + MouseUp?.Invoke(this, args); + + _hasDoubleClicked = false; + _previousClickArgs = args; + } + } + + private void CheckMouseDragged(Func<MouseState, ButtonState> getButtonState, MouseButton button) + { + if ((getButtonState(_currentState) == ButtonState.Pressed) && + (getButtonState(_previousState) == ButtonState.Pressed)) + { + var args = new MouseEventArgs(ViewportAdapter, _gameTime.TotalGameTime, _previousState, _currentState, button); + + if (_mouseDownArgs.Button == args.Button) + { + if (_dragging) + MouseDrag?.Invoke(this, args); + else + { + // Only start to drag based on DragThreshold + var clickMovement = DistanceBetween(args.Position, _mouseDownArgs.Position); + + if (clickMovement > DragThreshold) + { + _dragging = true; + MouseDragStart?.Invoke(this, args); + } + } + } + } + } + + public override void Update(GameTime gameTime) + { + _gameTime = gameTime; + _currentState = Mouse.GetState(); + + CheckButtonPressed(s => s.LeftButton, MouseButton.Left); + CheckButtonPressed(s => s.MiddleButton, MouseButton.Middle); + CheckButtonPressed(s => s.RightButton, MouseButton.Right); + CheckButtonPressed(s => s.XButton1, MouseButton.XButton1); + CheckButtonPressed(s => s.XButton2, MouseButton.XButton2); + + CheckButtonReleased(s => s.LeftButton, MouseButton.Left); + CheckButtonReleased(s => s.MiddleButton, MouseButton.Middle); + CheckButtonReleased(s => s.RightButton, MouseButton.Right); + CheckButtonReleased(s => s.XButton1, MouseButton.XButton1); + CheckButtonReleased(s => s.XButton2, MouseButton.XButton2); + + // Check for any sort of mouse movement. + if (HasMouseMoved) + { + MouseMoved?.Invoke(this, + new MouseEventArgs(ViewportAdapter, gameTime.TotalGameTime, _previousState, _currentState)); + + CheckMouseDragged(s => s.LeftButton, MouseButton.Left); + CheckMouseDragged(s => s.MiddleButton, MouseButton.Middle); + CheckMouseDragged(s => s.RightButton, MouseButton.Right); + CheckMouseDragged(s => s.XButton1, MouseButton.XButton1); + CheckMouseDragged(s => s.XButton2, MouseButton.XButton2); + } + + // Handle mouse wheel events. + if (_previousState.ScrollWheelValue != _currentState.ScrollWheelValue) + { + MouseWheelMoved?.Invoke(this, + new MouseEventArgs(ViewportAdapter, gameTime.TotalGameTime, _previousState, _currentState)); + } + + _previousState = _currentState; + } + + private static int DistanceBetween(Point a, Point b) + { + return Math.Abs(a.X - b.X) + Math.Abs(a.Y - b.Y); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseListenerSettings.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseListenerSettings.cs new file mode 100644 index 0000000..1c0ca3d --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/MouseListenerSettings.cs @@ -0,0 +1,23 @@ +using MonoGame.Extended.ViewportAdapters; + +namespace MonoGame.Extended.Input.InputListeners +{ + public class MouseListenerSettings : InputListenerSettings<MouseListener> + { + public MouseListenerSettings() + { + // initial values are windows defaults + DoubleClickMilliseconds = 500; + DragThreshold = 2; + } + + public int DragThreshold { get; set; } + public int DoubleClickMilliseconds { get; set; } + public ViewportAdapter ViewportAdapter { get; set; } + + public override MouseListener CreateListener() + { + return new MouseListener(this); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchEventArgs.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchEventArgs.cs new file mode 100644 index 0000000..8172885 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchEventArgs.cs @@ -0,0 +1,39 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input.Touch; +using MonoGame.Extended.ViewportAdapters; + +namespace MonoGame.Extended.Input.InputListeners +{ + public class TouchEventArgs : EventArgs + { + public TouchEventArgs(ViewportAdapter viewportAdapter, TimeSpan time, TouchLocation location) + { + ViewportAdapter = viewportAdapter; + RawTouchLocation = location; + Time = time; + Position = viewportAdapter?.PointToScreen((int)location.Position.X, (int)location.Position.Y) + ?? location.Position.ToPoint(); + } + + public ViewportAdapter ViewportAdapter { get; } + public TouchLocation RawTouchLocation { get; } + public TimeSpan Time { get; } + public Point Position { get; } + + public override bool Equals(object other) + { + var args = other as TouchEventArgs; + + if (args == null) + return false; + + return ReferenceEquals(this, args) || RawTouchLocation.Id.Equals(args.RawTouchLocation.Id); + } + + public override int GetHashCode() + { + return RawTouchLocation.Id.GetHashCode(); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchListener.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchListener.cs new file mode 100644 index 0000000..2a89cc9 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchListener.cs @@ -0,0 +1,57 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input.Touch; +using MonoGame.Extended.ViewportAdapters; + +namespace MonoGame.Extended.Input.InputListeners +{ + public class TouchListener : InputListener + { + public TouchListener() + : this(new TouchListenerSettings()) + { + } + + public TouchListener(ViewportAdapter viewportAdapter) + : this(new TouchListenerSettings()) + { + ViewportAdapter = viewportAdapter; + } + + public TouchListener(TouchListenerSettings settings) + { + ViewportAdapter = settings.ViewportAdapter; + } + + public ViewportAdapter ViewportAdapter { get; set; } + + public event EventHandler<TouchEventArgs> TouchStarted; + public event EventHandler<TouchEventArgs> TouchEnded; + public event EventHandler<TouchEventArgs> TouchMoved; + public event EventHandler<TouchEventArgs> TouchCancelled; + + public override void Update(GameTime gameTime) + { + var touchCollection = TouchPanel.GetState(); + + foreach (var touchLocation in touchCollection) + { + switch (touchLocation.State) + { + case TouchLocationState.Pressed: + TouchStarted?.Invoke(this, new TouchEventArgs(ViewportAdapter, gameTime.TotalGameTime, touchLocation)); + break; + case TouchLocationState.Moved: + TouchMoved?.Invoke(this, new TouchEventArgs(ViewportAdapter, gameTime.TotalGameTime, touchLocation)); + break; + case TouchLocationState.Released: + TouchEnded?.Invoke(this, new TouchEventArgs(ViewportAdapter, gameTime.TotalGameTime, touchLocation)); + break; + case TouchLocationState.Invalid: + TouchCancelled?.Invoke(this, new TouchEventArgs(ViewportAdapter, gameTime.TotalGameTime, touchLocation)); + break; + } + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchListenerSettings.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchListenerSettings.cs new file mode 100644 index 0000000..6d42b42 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/InputListeners/TouchListenerSettings.cs @@ -0,0 +1,18 @@ +using MonoGame.Extended.ViewportAdapters; + +namespace MonoGame.Extended.Input.InputListeners +{ + public class TouchListenerSettings : InputListenerSettings<TouchListener> + { + public TouchListenerSettings() + { + } + + public ViewportAdapter ViewportAdapter { get; set; } + + public override TouchListener CreateListener() + { + return new TouchListener(this); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/KeyboardExtended.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/KeyboardExtended.cs new file mode 100644 index 0000000..0ed7c76 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/KeyboardExtended.cs @@ -0,0 +1,22 @@ +using Microsoft.Xna.Framework.Input; + +namespace MonoGame.Extended.Input +{ + public static class KeyboardExtended + { + // TODO: This global static state was a horrible idea. + private static KeyboardState _currentKeyboardState; + private static KeyboardState _previousKeyboardState; + + public static KeyboardStateExtended GetState() + { + return new KeyboardStateExtended(_currentKeyboardState, _previousKeyboardState); + } + + public static void Refresh() + { + _previousKeyboardState = _currentKeyboardState; + _currentKeyboardState = Keyboard.GetState(); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/KeyboardStateExtended.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/KeyboardStateExtended.cs new file mode 100644 index 0000000..ee18677 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/KeyboardStateExtended.cs @@ -0,0 +1,60 @@ +using System; +using System.Linq; +using Microsoft.Xna.Framework.Input; + +namespace MonoGame.Extended.Input +{ + public struct KeyboardStateExtended + { + private KeyboardState _currentKeyboardState; + private KeyboardState _previousKeyboardState; + + public KeyboardStateExtended(KeyboardState currentKeyboardState, KeyboardState previousKeyboardState) + { + _currentKeyboardState = currentKeyboardState; + _previousKeyboardState = previousKeyboardState; + } + + public bool CapsLock => _currentKeyboardState.CapsLock; + public bool NumLock => _currentKeyboardState.NumLock; + public bool IsShiftDown() => _currentKeyboardState.IsKeyDown(Keys.LeftShift) || _currentKeyboardState.IsKeyDown(Keys.RightShift); + public bool IsControlDown() => _currentKeyboardState.IsKeyDown(Keys.LeftControl) || _currentKeyboardState.IsKeyDown(Keys.RightControl); + public bool IsAltDown() => _currentKeyboardState.IsKeyDown(Keys.LeftAlt) || _currentKeyboardState.IsKeyDown(Keys.RightAlt); + public bool IsKeyDown(Keys key) => _currentKeyboardState.IsKeyDown(key); + public bool IsKeyUp(Keys key) => _currentKeyboardState.IsKeyUp(key); + public Keys[] GetPressedKeys() => _currentKeyboardState.GetPressedKeys(); + public void GetPressedKeys(Keys[] keys) => _currentKeyboardState.GetPressedKeys(keys); + + /// <summary> + /// Gets whether the given key was down on the previous state, but is now up. + /// </summary> + /// <param name="key">The key to check.</param> + /// <returns>true if the key was released this state-change, otherwise false.</returns> + [Obsolete($"Deprecated in favor of {nameof(IsKeyReleased)}")] + public bool WasKeyJustDown(Keys key) => _previousKeyboardState.IsKeyDown(key) && _currentKeyboardState.IsKeyUp(key); + + /// <summary> + /// Gets whether the given key was up on the previous state, but is now down. + /// </summary> + /// <param name="key">The key to check.</param> + /// <returns>true if the key was pressed this state-change, otherwise false.</returns> + [Obsolete($"Deprecated in favor of {nameof(IsKeyPressed)}")] + public bool WasKeyJustUp(Keys key) => _previousKeyboardState.IsKeyUp(key) && _currentKeyboardState.IsKeyDown(key); + + /// <summary> + /// Gets whether the given key was down on the previous state, but is now up. + /// </summary> + /// <param name="key">The key to check.</param> + /// <returns>true if the key was released this state-change, otherwise false.</returns> + public readonly bool IsKeyReleased(Keys key) => _previousKeyboardState.IsKeyDown(key) && _currentKeyboardState.IsKeyUp(key); + + /// <summary> + /// Gets whether the given key was up on the previous state, but is now down. + /// </summary> + /// <param name="key">The key to check.</param> + /// <returns>true if the key was pressed this state-change, otherwise false.</returns> + public readonly bool IsKeyPressed(Keys key) => _previousKeyboardState.IsKeyUp(key) && _currentKeyboardState.IsKeyDown(key); + + public bool WasAnyKeyJustDown() => _previousKeyboardState.GetPressedKeyCount() > 0; + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MonoGame.Extended.Input.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MonoGame.Extended.Input.csproj new file mode 100644 index 0000000..4b3bd82 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MonoGame.Extended.Input.csproj @@ -0,0 +1,12 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <Description>An event based input system to MonoGame more awesome.</Description> + <PackageTags>monogame input event based listeners</PackageTags> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" /> + </ItemGroup> + +</Project> diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseButton.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseButton.cs new file mode 100644 index 0000000..e4a00f8 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseButton.cs @@ -0,0 +1,15 @@ +using System; + +namespace MonoGame.Extended.Input +{ + [Flags] + public enum MouseButton + { + None = 0, + Left = 1, + Middle = 2, + Right = 4, + XButton1 = 8, + XButton2 = 16 + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseExtended.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseExtended.cs new file mode 100644 index 0000000..61d6d18 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseExtended.cs @@ -0,0 +1,34 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +namespace MonoGame.Extended.Input +{ + public static class MouseExtended + { + // TODO: This global static state was a horrible idea. + private static MouseState _currentMouseState; + private static MouseState _previousMouseState; + + public static MouseStateExtended GetState() + { + return new MouseStateExtended(_currentMouseState, _previousMouseState); + } + + public static void Refresh() + { + _previousMouseState = _currentMouseState; + _currentMouseState = Mouse.GetState(); + } + + public static void SetPosition(int x, int y) => Mouse.SetPosition(x, y); + public static void SetPosition(Point point) => Mouse.SetPosition(point.X, point.Y); + public static void SetCursor(MouseCursor cursor) => Mouse.SetCursor(cursor); + + public static IntPtr WindowHandle + { + get => Mouse.WindowHandle; + set => Mouse.WindowHandle = value; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseStateExtended.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseStateExtended.cs new file mode 100644 index 0000000..0ec6943 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Input/MouseStateExtended.cs @@ -0,0 +1,149 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +namespace MonoGame.Extended.Input +{ + public struct MouseStateExtended + { + private readonly MouseState _currentMouseState; + private readonly MouseState _previousMouseState; + + public MouseStateExtended(MouseState currentMouseState, MouseState previousMouseState) + { + _currentMouseState = currentMouseState; + _previousMouseState = previousMouseState; + } + + public int X => _currentMouseState.X; + public int Y => _currentMouseState.Y; + public Point Position => _currentMouseState.Position; + public bool PositionChanged => _currentMouseState.Position != _previousMouseState.Position; + + public int DeltaX => _previousMouseState.X - _currentMouseState.X; + public int DeltaY => _previousMouseState.Y - _currentMouseState.Y; + public Point DeltaPosition => new Point(DeltaX, DeltaY); + + public int ScrollWheelValue => _currentMouseState.ScrollWheelValue; + public int DeltaScrollWheelValue => _previousMouseState.ScrollWheelValue - _currentMouseState.ScrollWheelValue; + + public ButtonState LeftButton => _currentMouseState.LeftButton; + public ButtonState MiddleButton => _currentMouseState.MiddleButton; + public ButtonState RightButton => _currentMouseState.RightButton; + public ButtonState XButton1 => _currentMouseState.XButton1; + public ButtonState XButton2 => _currentMouseState.XButton2; + + public bool IsButtonDown(MouseButton button) + { + // ReSharper disable once SwitchStatementMissingSomeCases + switch (button) + { + case MouseButton.Left: return IsPressed(m => m.LeftButton); + case MouseButton.Middle: return IsPressed(m => m.MiddleButton); + case MouseButton.Right: return IsPressed(m => m.RightButton); + case MouseButton.XButton1: return IsPressed(m => m.XButton1); + case MouseButton.XButton2: return IsPressed(m => m.XButton2); + } + + return false; + } + + public bool IsButtonUp(MouseButton button) + { + // ReSharper disable once SwitchStatementMissingSomeCases + switch (button) + { + case MouseButton.Left: return IsReleased(m => m.LeftButton); + case MouseButton.Middle: return IsReleased(m => m.MiddleButton); + case MouseButton.Right: return IsReleased(m => m.RightButton); + case MouseButton.XButton1: return IsReleased(m => m.XButton1); + case MouseButton.XButton2: return IsReleased(m => m.XButton2); + } + + return false; + } + + /// <summary> + /// Get the just-down state for the mouse on this state-change: true if the mouse button has just been pressed. + /// </summary> + /// <param name="button"></param> + /// <remarks>Deprecated because of inconsistency with <see cref="KeyboardStateExtended"/></remarks> + /// <returns>The just-down state for the mouse on this state-change.</returns> + [Obsolete($"Deprecated in favor of {nameof(IsButtonPressed)}")] + public bool WasButtonJustDown(MouseButton button) + { + // ReSharper disable once SwitchStatementMissingSomeCases + switch (button) + { + case MouseButton.Left: return WasJustPressed(m => m.LeftButton); + case MouseButton.Middle: return WasJustPressed(m => m.MiddleButton); + case MouseButton.Right: return WasJustPressed(m => m.RightButton); + case MouseButton.XButton1: return WasJustPressed(m => m.XButton1); + case MouseButton.XButton2: return WasJustPressed(m => m.XButton2); + } + + return false; + } + + /// <summary> + /// Get the just-up state for the mouse on this state-change: true if the mouse button has just been released. + /// </summary> + /// <param name="button"></param> + /// <remarks>Deprecated because of inconsistency with <see cref="KeyboardStateExtended"/></remarks> + /// <returns>The just-up state for the mouse on this state-change.</returns> + [Obsolete($"Deprecated in favor of {nameof(IsButtonReleased)}")] + public bool WasButtonJustUp(MouseButton button) + { + // ReSharper disable once SwitchStatementMissingSomeCases + switch (button) + { + case MouseButton.Left: return WasJustReleased(m => m.LeftButton); + case MouseButton.Middle: return WasJustReleased(m => m.MiddleButton); + case MouseButton.Right: return WasJustReleased(m => m.RightButton); + case MouseButton.XButton1: return WasJustReleased(m => m.XButton1); + case MouseButton.XButton2: return WasJustReleased(m => m.XButton2); + } + + return false; + } + + /// <summary> + /// Get the pressed state of a mouse button, for this state-change. + /// </summary> + /// <param name="button">The button to check.</param> + /// <returns>true if the given mouse button was pressed this state-change, otherwise false</returns> + public readonly bool IsButtonPressed(MouseButton button) => button switch + { + MouseButton.Left => WasJustPressed(m => m.LeftButton), + MouseButton.Middle => WasJustPressed(m => m.MiddleButton), + MouseButton.Right => WasJustPressed(m => m.RightButton), + MouseButton.XButton1 => WasJustPressed(m => m.XButton1), + MouseButton.XButton2 => WasJustPressed(m => m.XButton2), + _ => false, + }; + + /// <summary> + /// Get the released state of a mouse button, for this state-change. + /// </summary> + /// <param name="button">The button to check.</param> + /// <returns>true if the given mouse button was released this state-change, otherwise false</returns> + public readonly bool IsButtonReleased(MouseButton button) => button switch + { + MouseButton.Left => WasJustReleased(m => m.LeftButton), + MouseButton.Middle => WasJustReleased(m => m.MiddleButton), + MouseButton.Right => WasJustReleased(m => m.RightButton), + MouseButton.XButton1 => WasJustReleased(m => m.XButton1), + MouseButton.XButton2 => WasJustReleased(m => m.XButton2), + _ => false, + }; + + private readonly bool IsPressed(Func<MouseState, ButtonState> button) + => button(_currentMouseState) == ButtonState.Pressed; + private readonly bool IsReleased(Func<MouseState, ButtonState> button) + => button(_currentMouseState) == ButtonState.Released; + private readonly bool WasJustPressed(Func<MouseState, ButtonState> button) + => button(_previousMouseState) == ButtonState.Released && button(_currentMouseState) == ButtonState.Pressed; + private readonly bool WasJustReleased(Func<MouseState, ButtonState> button) + => button(_previousMouseState) == ButtonState.Pressed && button(_currentMouseState) == ButtonState.Released; + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/FastRandomExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/FastRandomExtensions.cs new file mode 100644 index 0000000..66c99e7 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/FastRandomExtensions.cs @@ -0,0 +1,17 @@ +using MonoGame.Extended; + +namespace MonoGame.Extended.Particles +{ + public static class FastRandomExtensions + { + public static void NextColor(this FastRandom random, out HslColor color, Range<HslColor> range) + { + var maxH = range.Max.H >= range.Min.H + ? range.Max.H + : range.Max.H + 360; + color = new HslColor(random.NextSingle(range.Min.H, maxH), + random.NextSingle(range.Min.S, range.Max.S), + random.NextSingle(range.Min.L, range.Max.L)); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/LineSegment.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/LineSegment.cs new file mode 100644 index 0000000..a3c1422 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/LineSegment.cs @@ -0,0 +1,77 @@ +using System; +using System.Runtime.InteropServices; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Particles +{ + /// <summary> + /// Defines a part of a line that is bounded by two distinct end points. + /// </summary> + [StructLayout(LayoutKind.Sequential)] + public struct LineSegment : IEquatable<LineSegment> + { + internal readonly Vector2 _point1; + internal readonly Vector2 _point2; + + /// <summary> + /// Initializes a new instance of the <see cref="LineSegment" /> structure. + /// </summary> + /// <param name="point1"></param> + /// <param name="point2"></param> + public LineSegment(Vector2 point1, Vector2 point2) + { + _point1 = point1; + _point2 = point2; + } + + public static LineSegment FromPoints(Vector2 p1, Vector2 p2) => new LineSegment(p1, p2); + public static LineSegment FromOrigin(Vector2 origin, Vector2 vector) => new LineSegment(origin, origin + vector); + + public Vector2 Origin => _point1; + + public Vector2 Direction + { + get + { + var coord = _point2 - _point1; + return new Vector2(coord.X, coord.Y); + } + } + + public LineSegment Translate(Vector2 t) + { + return new LineSegment(_point1 + t, _point2 + t); + } + + public Vector2 ToVector() + { + var t = _point2 - _point1; + return new Vector2(t.X, t.Y); + } + + public bool Equals(LineSegment other) + { + // ReSharper disable ImpureMethodCallOnReadonlyValueField + return _point1.Equals(other._point1) && _point2.Equals(other._point2); + // ReSharper restore ImpureMethodCallOnReadonlyValueField + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + return false; + + return obj is LineSegment & Equals((LineSegment) obj); + } + + public override int GetHashCode() + { + return (_point1.GetHashCode()*397) ^ _point2.GetHashCode(); + } + + public override string ToString() + { + return $"({_point1.X}:{_point1.Y} {_point2.X}:{_point2.Y})"; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/AgeModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/AgeModifier.cs new file mode 100644 index 0000000..fac85f0 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/AgeModifier.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.ComponentModel; +using MonoGame.Extended.Particles.Modifiers.Interpolators; + +namespace MonoGame.Extended.Particles.Modifiers +{ + public class AgeModifier : Modifier + { + [EditorBrowsable(EditorBrowsableState.Always)] + public List<Interpolator> Interpolators { get; set; } = new List<Interpolator>(); + + public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator) + { + var n = Interpolators.Count; + while (iterator.HasNext) + { + var particle = iterator.Next(); + for (var i = 0; i < n; i++) + { + var interpolator = Interpolators[i]; + interpolator.Update(particle->Age, particle); + } + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/CircleContainerModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/CircleContainerModifier.cs new file mode 100644 index 0000000..30d2802 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/CircleContainerModifier.cs @@ -0,0 +1,52 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Particles.Modifiers.Containers +{ + public class CircleContainerModifier : Modifier + { + public float Radius { get; set; } + public bool Inside { get; set; } = true; + public float RestitutionCoefficient { get; set; } = 1; + + public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator) + { + var radiusSq = Radius*Radius; + while (iterator.HasNext) + { + var particle = iterator.Next(); + var localPos = particle->Position - particle->TriggerPos; + + var distSq = localPos.LengthSquared(); + var normal = localPos; + normal.Normalize(); + + if (Inside) + { + if (distSq < radiusSq) continue; + + SetReflected(distSq, particle, normal); + } + else + { + if (distSq > radiusSq) continue; + + SetReflected(distSq, particle, -normal); + } + } + } + + private unsafe void SetReflected(float distSq, Particle* particle, Vector2 normal) + { + var dist = (float) Math.Sqrt(distSq); + var d = dist - Radius; // how far outside the circle is the particle + + var twoRestDot = 2*RestitutionCoefficient* + Vector2.Dot(particle->Velocity, normal); + particle->Velocity -= twoRestDot*normal; + + // exact computation requires sqrt or goniometrics + particle->Position -= normal*d; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleContainerModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleContainerModifier.cs new file mode 100644 index 0000000..a56e072 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleContainerModifier.cs @@ -0,0 +1,59 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Particles.Modifiers.Containers +{ + public sealed class RectangleContainerModifier : Modifier + { + public int Width { get; set; } + public int Height { get; set; } + public float RestitutionCoefficient { get; set; } = 1; + + public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator) + { + while (iterator.HasNext) + { + var particle = iterator.Next(); + + var left = particle->TriggerPos.X + Width*-0.5f; + var right = particle->TriggerPos.X + Width*0.5f; + var top = particle->TriggerPos.Y + Height*-0.5f; + var bottom = particle->TriggerPos.Y + Height*0.5f; + + var xPos = particle->Position.X; + var xVel = particle->Velocity.X; + var yPos = particle->Position.Y; + var yVel = particle->Velocity.Y; + + if ((int) particle->Position.X < left) + { + xPos = left + (left - xPos); + xVel = -xVel*RestitutionCoefficient; + } + else + { + if (particle->Position.X > right) + { + xPos = right - (xPos - right); + xVel = -xVel*RestitutionCoefficient; + } + } + + if (particle->Position.Y < top) + { + yPos = top + (top - yPos); + yVel = -yVel*RestitutionCoefficient; + } + else + { + if ((int) particle->Position.Y > bottom) + { + yPos = bottom - (yPos - bottom); + yVel = -yVel*RestitutionCoefficient; + } + } + particle->Position = new Vector2(xPos, yPos); + particle->Velocity = new Vector2(xVel, yVel); + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleLoopContainerModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleLoopContainerModifier.cs new file mode 100644 index 0000000..4da6efa --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Containers/RectangleLoopContainerModifier.cs @@ -0,0 +1,42 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Particles.Modifiers.Containers +{ + public class RectangleLoopContainerModifier : Modifier + { + public int Width { get; set; } + public int Height { get; set; } + + public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator) + { + while (iterator.HasNext) + { + var particle = iterator.Next(); + var left = particle->TriggerPos.X + Width*-0.5f; + var right = particle->TriggerPos.X + Width*0.5f; + var top = particle->TriggerPos.Y + Height*-0.5f; + var bottom = particle->TriggerPos.Y + Height*0.5f; + + var xPos = particle->Position.X; + var yPos = particle->Position.Y; + + if ((int) particle->Position.X < left) + xPos = particle->Position.X + Width; + else + { + if ((int) particle->Position.X > right) + xPos = particle->Position.X - Width; + } + + if ((int) particle->Position.Y < top) + yPos = particle->Position.Y + Height; + else + { + if ((int) particle->Position.Y > bottom) + yPos = particle->Position.Y - Height; + } + particle->Position = new Vector2(xPos, yPos); + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/DragModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/DragModifier.cs new file mode 100644 index 0000000..f2d6d09 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/DragModifier.cs @@ -0,0 +1,23 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Particles.Modifiers +{ + public class DragModifier : Modifier + { + public float DragCoefficient { get; set; } = 0.47f; + public float Density { get; set; } = .5f; + + public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator) + { + while (iterator.HasNext) + { + var particle = iterator.Next(); + var drag = -DragCoefficient*Density*particle->Mass*elapsedSeconds; + + particle->Velocity = new Vector2( + particle->Velocity.X + particle->Velocity.X*drag, + particle->Velocity.Y + particle->Velocity.Y*drag); + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ColorInterpolator.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ColorInterpolator.cs new file mode 100644 index 0000000..f94c689 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ColorInterpolator.cs @@ -0,0 +1,13 @@ +namespace MonoGame.Extended.Particles.Modifiers.Interpolators +{ + /// <summary> + /// Defines a modifier which interpolates the color of a particle over the course of its lifetime. + /// </summary> + public sealed class ColorInterpolator : Interpolator<HslColor> + { + public override unsafe void Update(float amount, Particle* particle) + { + particle->Color = HslColor.Lerp(StartValue, EndValue, amount); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/HueInterpolator.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/HueInterpolator.cs new file mode 100644 index 0000000..8b58a8e --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/HueInterpolator.cs @@ -0,0 +1,12 @@ +using MonoGame.Extended; + +namespace MonoGame.Extended.Particles.Modifiers.Interpolators +{ + public class HueInterpolator : Interpolator<float> + { + public override unsafe void Update(float amount, Particle* particle) + { + particle->Color = new HslColor((EndValue - StartValue) * amount + StartValue, particle->Color.S, particle->Color.L); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/Interpolator.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/Interpolator.cs new file mode 100644 index 0000000..0ec9866 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/Interpolator.cs @@ -0,0 +1,26 @@ +namespace MonoGame.Extended.Particles.Modifiers.Interpolators +{ + public abstract class Interpolator + { + protected Interpolator() + { + Name = GetType().Name; + } + + public string Name { get; set; } + public abstract unsafe void Update(float amount, Particle* particle); + } + + public abstract class Interpolator<T> : Interpolator + { + /// <summary> + /// Gets or sets the intial value when the particles are created. + /// </summary> + public virtual T StartValue { get; set; } + + /// <summary> + /// Gets or sets the final value when the particles are retired. + /// </summary> + public virtual T EndValue { get; set; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/OpacityInterpolator.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/OpacityInterpolator.cs new file mode 100644 index 0000000..0a26fcd --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/OpacityInterpolator.cs @@ -0,0 +1,10 @@ +namespace MonoGame.Extended.Particles.Modifiers.Interpolators +{ + public class OpacityInterpolator : Interpolator<float> + { + public override unsafe void Update(float amount, Particle* particle) + { + particle->Opacity = (EndValue - StartValue) * amount + StartValue; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/RotationInterpolator.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/RotationInterpolator.cs new file mode 100644 index 0000000..44a72de --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/RotationInterpolator.cs @@ -0,0 +1,10 @@ +namespace MonoGame.Extended.Particles.Modifiers.Interpolators +{ + public class RotationInterpolator : Interpolator<float> + { + public override unsafe void Update(float amount, Particle* particle) + { + particle->Rotation = (EndValue - StartValue) * amount + StartValue; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ScaleInterpolator.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ScaleInterpolator.cs new file mode 100644 index 0000000..aff9244 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/ScaleInterpolator.cs @@ -0,0 +1,12 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Particles.Modifiers.Interpolators +{ + public class ScaleInterpolator : Interpolator<Vector2> + { + public override unsafe void Update(float amount, Particle* particle) + { + particle->Scale = (EndValue - StartValue) * amount + StartValue; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/VelocityInterpolator b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/VelocityInterpolator new file mode 100644 index 0000000..bad04b1 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Interpolators/VelocityInterpolator @@ -0,0 +1,12 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Particles.Modifiers.Interpolators +{ + public class VelocityInterpolator : Interpolator<Vector2> + { + public override unsafe void Update(float amount, Particle* particle) + { + particle->Velocity = (EndValue - StartValue) * amount + StartValue; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/LinearGravityModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/LinearGravityModifier.cs new file mode 100644 index 0000000..e9ed216 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/LinearGravityModifier.cs @@ -0,0 +1,23 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Particles.Modifiers +{ + public class LinearGravityModifier : Modifier + { + public Vector2 Direction { get; set; } + public float Strength { get; set; } + + public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator) + { + var vector = Direction*(Strength*elapsedSeconds); + + while (iterator.HasNext) + { + var particle = iterator.Next(); + particle->Velocity = new Vector2( + particle->Velocity.X + vector.X*particle->Mass, + particle->Velocity.Y + vector.Y*particle->Mass); + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Modifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Modifier.cs new file mode 100644 index 0000000..bd62d25 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/Modifier.cs @@ -0,0 +1,18 @@ +namespace MonoGame.Extended.Particles.Modifiers +{ + public abstract class Modifier + { + protected Modifier() + { + Name = GetType().Name; + } + + public string Name { get; set; } + public abstract void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator); + + public override string ToString() + { + return $"{Name} [{GetType().Name}]"; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/OpacityFastFadeModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/OpacityFastFadeModifier.cs new file mode 100644 index 0000000..9cc44d8 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/OpacityFastFadeModifier.cs @@ -0,0 +1,14 @@ +namespace MonoGame.Extended.Particles.Modifiers +{ + public sealed class OpacityFastFadeModifier : Modifier + { + public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator) + { + while (iterator.HasNext) + { + var particle = iterator.Next(); + particle->Opacity = 1.0f - particle->Age; + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/RotationModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/RotationModifier.cs new file mode 100644 index 0000000..2b17058 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/RotationModifier.cs @@ -0,0 +1,18 @@ +namespace MonoGame.Extended.Particles.Modifiers +{ + public class RotationModifier : Modifier + { + public float RotationRate { get; set; } + + public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator) + { + var rotationRateDelta = RotationRate*elapsedSeconds; + + while (iterator.HasNext) + { + var particle = iterator.Next(); + particle->Rotation += rotationRateDelta; + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityColorModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityColorModifier.cs new file mode 100644 index 0000000..ae9dc7b --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityColorModifier.cs @@ -0,0 +1,36 @@ +using System; + +namespace MonoGame.Extended.Particles.Modifiers +{ + public class VelocityColorModifier : Modifier + { + public HslColor StationaryColor { get; set; } + public HslColor VelocityColor { get; set; } + public float VelocityThreshold { get; set; } + + public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator) + { + var velocityThreshold2 = VelocityThreshold*VelocityThreshold; + + while (iterator.HasNext) + { + var particle = iterator.Next(); + var velocity2 = particle->Velocity.X*particle->Velocity.X + + particle->Velocity.Y*particle->Velocity.Y; + var deltaColor = VelocityColor - StationaryColor; + + if (velocity2 >= velocityThreshold2) + VelocityColor.CopyTo(out particle->Color); + else + { + var t = (float) Math.Sqrt(velocity2)/VelocityThreshold; + + particle->Color = new HslColor( + deltaColor.H*t + StationaryColor.H, + deltaColor.S*t + StationaryColor.S, + deltaColor.L*t + StationaryColor.L); + } + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityModifier.cs new file mode 100644 index 0000000..f089abc --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VelocityModifier.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using MonoGame.Extended.Particles.Modifiers.Interpolators; + +namespace MonoGame.Extended.Particles.Modifiers +{ + public class VelocityModifier : Modifier + { + public List<Interpolator> Interpolators { get; set; } = new List<Interpolator>(); + + public float VelocityThreshold { get; set; } + + public override unsafe void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator) + { + var velocityThreshold2 = VelocityThreshold*VelocityThreshold; + var n = Interpolators.Count; + + while (iterator.HasNext) + { + var particle = iterator.Next(); + var velocity2 = particle->Velocity.LengthSquared(); + + if (velocity2 >= velocityThreshold2) + { + for (var i = 0; i < n; i++) + { + var interpolator = Interpolators[i]; + interpolator.Update(1, particle); + } + } + else + { + var t = (float) Math.Sqrt(velocity2)/VelocityThreshold; + for (var i = 0; i < n; i++) + { + var interpolator = Interpolators[i]; + interpolator.Update(t, particle); + } + } + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VortexModifier.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VortexModifier.cs new file mode 100644 index 0000000..03a9999 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Modifiers/VortexModifier.cs @@ -0,0 +1,30 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Particles.Modifiers +{ + public unsafe class VortexModifier : Modifier + { + // Note: not the real-life one + private const float _gravConst = 100000f; + + public Vector2 Position { get; set; } + public float Mass { get; set; } + public float MaxSpeed { get; set; } + + public override void Update(float elapsedSeconds, ParticleBuffer.ParticleIterator iterator) + { + while (iterator.HasNext) + { + var particle = iterator.Next(); + var diff = Position + particle->TriggerPos - particle->Position; + + var distance2 = diff.LengthSquared(); + + var speedGain = _gravConst*Mass/distance2*elapsedSeconds; + // normalize distances and multiply by speedGain + diff.Normalize(); + particle->Velocity += diff*speedGain; + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/MonoGame.Extended.Particles.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/MonoGame.Extended.Particles.csproj new file mode 100644 index 0000000..d82f26e --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/MonoGame.Extended.Particles.csproj @@ -0,0 +1,12 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <Description>A high performance particle system to make MonoGame more awesome.</Description> + <PackageTags>monogame particle system</PackageTags> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" /> + </ItemGroup> + +</Project> diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Particle.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Particle.cs new file mode 100644 index 0000000..042d8ce --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Particle.cs @@ -0,0 +1,24 @@ +using System.Runtime.InteropServices; +using Microsoft.Xna.Framework; +using MonoGame.Extended; + +namespace MonoGame.Extended.Particles +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct Particle + { + public float Inception; + public float Age; + public Vector2 Position; + public Vector2 TriggerPos; + public Vector2 Velocity; + public HslColor Color; + public float Opacity; + public Vector2 Scale; + public float Rotation; + public float Mass; + public float LayerDepth; + + public static readonly int SizeInBytes = Marshal.SizeOf(typeof(Particle)); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleBuffer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleBuffer.cs new file mode 100644 index 0000000..2e410fa --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleBuffer.cs @@ -0,0 +1,152 @@ +using System; +using System.Runtime.InteropServices; + +namespace MonoGame.Extended.Particles +{ + public class ParticleBuffer : IDisposable + { + private readonly ParticleIterator _iterator; + private readonly IntPtr _nativePointer; + + // points to the first memory pos after the buffer + protected readonly unsafe Particle* BufferEnd; + + private bool _disposed; + // points to the particle after the last active particle. + protected unsafe Particle* Tail; + + public unsafe ParticleBuffer(int size) + { + Size = size; + _nativePointer = Marshal.AllocHGlobal(SizeInBytes); + + BufferEnd = (Particle*) (_nativePointer + SizeInBytes); + Head = (Particle*) _nativePointer; + Tail = (Particle*) _nativePointer; + + _iterator = new ParticleIterator(this); + + GC.AddMemoryPressure(SizeInBytes); + } + + public int Size { get; } + + public ParticleIterator Iterator => _iterator.Reset(); + // pointer to the first particle + public unsafe Particle* Head { get; private set; } + + // Number of available particle spots in the buffer + public int Available => Size - Count; + // current number of particles + public int Count { get; private set; } + // total size of the buffer (add one extra spot in memory for margin between head and tail so the iterator can see that it's at the end) + public int SizeInBytes => Particle.SizeInBytes*(Size + 1); + // total size of active particles + public int ActiveSizeInBytes => Particle.SizeInBytes*Count; + + public void Dispose() + { + if (!_disposed) + { + Marshal.FreeHGlobal(_nativePointer); + _disposed = true; + GC.RemoveMemoryPressure(SizeInBytes); + } + + GC.SuppressFinalize(this); + } + + /// <summary> + /// Release the given number of particles or the most available. + /// Returns a started iterator to iterate over the new particles. + /// </summary> + public unsafe ParticleIterator Release(int releaseQuantity) + { + var numToRelease = Math.Min(releaseQuantity, Available); + + var prevCount = Count; + Count += numToRelease; + + Tail += numToRelease; + if (Tail >= BufferEnd) Tail -= Size + 1; + + return Iterator.Reset(prevCount); + } + + public unsafe void Reclaim(int number) + { + Count -= number; + + Head += number; + if (Head >= BufferEnd) + Head -= Size + 1; + } + + //public void CopyTo(IntPtr destination) + //{ + // memcpy(destination, _nativePointer, ActiveSizeInBytes); + //} + + //public void CopyToReverse(IntPtr destination) + //{ + // var offset = 0; + // for (var i = ActiveSizeInBytes - Particle.SizeInBytes; i >= 0; i -= Particle.SizeInBytes) + // { + // memcpy(IntPtr.Add(destination, offset), IntPtr.Add(_nativePointer, i), Particle.SizeInBytes); + // offset += Particle.SizeInBytes; + // } + //} + + //[DllImport("msvcrt.dll", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl, SetLastError = false)] + //public static extern void memcpy(IntPtr dest, IntPtr src, int count); + + ~ParticleBuffer() + { + Dispose(); + } + + public class ParticleIterator + { + private readonly ParticleBuffer _buffer; + + private unsafe Particle* _current; + + public int Total; + + public ParticleIterator(ParticleBuffer buffer) + { + _buffer = buffer; + } + + public unsafe bool HasNext => _current != _buffer.Tail; + + public unsafe ParticleIterator Reset() + { + _current = _buffer.Head; + Total = _buffer.Count; + return this; + } + + internal unsafe ParticleIterator Reset(int offset) + { + Total = _buffer.Count; + + _current = _buffer.Head + offset; + if (_current >= _buffer.BufferEnd) + _current -= _buffer.Size + 1; + + return this; + } + + public unsafe Particle* Next() + { + var p = _current; + _current++; + if (_current == _buffer.BufferEnd) + _current = (Particle*) _buffer._nativePointer; + + return p; + } + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEffect.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEffect.cs new file mode 100644 index 0000000..ce6c782 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEffect.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using Microsoft.Xna.Framework; +using MonoGame.Extended.Particles.Serialization; +using MonoGame.Extended.Serialization; + +namespace MonoGame.Extended.Particles +{ + public class ParticleEffect : Transform2, IDisposable + { + public ParticleEffect(string name = null) + { + Name = name; + Emitters = new List<ParticleEmitter>(); + } + + public void Dispose() + { + foreach (var emitter in Emitters) + emitter.Dispose(); + } + + public string Name { get; set; } + public List<ParticleEmitter> Emitters { get; set; } + public int ActiveParticles => Emitters.Sum(t => t.ActiveParticles); + + public void FastForward(Vector2 position, float seconds, float triggerPeriod) + { + var time = 0f; + while (time < seconds) + { + Update(triggerPeriod); + Trigger(position); + time += triggerPeriod; + } + } + + public static ParticleEffect FromFile(ITextureRegionService textureRegionService, string path) + { + using (var stream = TitleContainer.OpenStream(path)) + { + return FromStream(textureRegionService, stream); + } + } + + public static ParticleEffect FromStream(ITextureRegionService textureRegionService, Stream stream) + { + var options = ParticleJsonSerializerOptionsProvider.GetOptions(textureRegionService); + return JsonSerializer.Deserialize<ParticleEffect>(stream, options); + } + + public void Update(float elapsedSeconds) + { + for (var i = 0; i < Emitters.Count; i++) + Emitters[i].Update(elapsedSeconds, Position); + } + + public void Trigger() + { + Trigger(Position); + } + + public void Trigger(Vector2 position, float layerDepth = 0) + { + for (var i = 0; i < Emitters.Count; i++) + Emitters[i].Trigger(position, layerDepth); + } + + public void Trigger(LineSegment line, float layerDepth = 0) + { + for (var i = 0; i < Emitters.Count; i++) + Emitters[i].Trigger(line, layerDepth); + } + + public override string ToString() + { + return Name; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEmitter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEmitter.cs new file mode 100644 index 0000000..1a6273b --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleEmitter.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Text.Json.Serialization; +using Microsoft.Xna.Framework; +using MonoGame.Extended.Particles.Modifiers; +using MonoGame.Extended.Particles.Profiles; +using MonoGame.Extended.TextureAtlases; + +namespace MonoGame.Extended.Particles +{ + public unsafe class ParticleEmitter : IDisposable + { + private readonly FastRandom _random = new FastRandom(Math.Abs(Guid.NewGuid().GetHashCode())); + private float _totalSeconds; + + [JsonConstructor] + public ParticleEmitter(string name, TextureRegion2D textureRegion, int capacity, TimeSpan lifeSpan, Profile profile) + { + if (profile == null) + throw new ArgumentNullException(nameof(profile)); + + _lifeSpanSeconds = (float)lifeSpan.TotalSeconds; + + Name = name; + TextureRegion = textureRegion; + Buffer = new ParticleBuffer(capacity); + Offset = Vector2.Zero; + Profile = profile; + Modifiers = new List<Modifier>(); + ModifierExecutionStrategy = ParticleModifierExecutionStrategy.Serial; + Parameters = new ParticleReleaseParameters(); + } + + public ParticleEmitter(TextureRegion2D textureRegion, int capacity, TimeSpan lifeSpan, Profile profile) + : this(null, textureRegion, capacity, lifeSpan, profile) + { + } + + public void Dispose() + { + Buffer.Dispose(); + GC.SuppressFinalize(this); + } + + ~ParticleEmitter() + { + Dispose(); + } + + public string Name { get; set; } + public int ActiveParticles => Buffer.Count; + public Vector2 Offset { get; set; } + public List<Modifier> Modifiers { get; } + public Profile Profile { get; set; } + public float LayerDepth { get; set; } + public ParticleReleaseParameters Parameters { get; set; } + public TextureRegion2D TextureRegion { get; set; } + + [EditorBrowsable(EditorBrowsableState.Never)] + public ParticleModifierExecutionStrategy ModifierExecutionStrategy { get; set; } + + internal ParticleBuffer Buffer; + + public int Capacity + { + get { return Buffer.Size; } + set + { + var oldBuffer = Buffer; + oldBuffer.Dispose(); + Buffer = new ParticleBuffer(value); + } + } + + private float _lifeSpanSeconds; + public TimeSpan LifeSpan + { + get { return TimeSpan.FromSeconds(_lifeSpanSeconds); } + set { _lifeSpanSeconds = (float) value.TotalSeconds; } + } + + private float _nextAutoTrigger; + + private bool _autoTrigger = true; + public bool AutoTrigger + { + get { return _autoTrigger; } + set + { + _autoTrigger = value; + _nextAutoTrigger = 0; + } + } + + private float _autoTriggerFrequency; + public float AutoTriggerFrequency + { + get { return _autoTriggerFrequency; } + set + { + _autoTriggerFrequency = value; + _nextAutoTrigger = 0; + } + } + + private void ReclaimExpiredParticles() + { + var iterator = Buffer.Iterator; + var expired = 0; + + while (iterator.HasNext) + { + var particle = iterator.Next(); + + if (_totalSeconds - particle->Inception < _lifeSpanSeconds) + break; + + expired++; + } + + if (expired != 0) + Buffer.Reclaim(expired); + } + + public bool Update(float elapsedSeconds, Vector2 position = default(Vector2)) + { + _totalSeconds += elapsedSeconds; + + if (_autoTrigger) + { + _nextAutoTrigger -= elapsedSeconds; + + if (_nextAutoTrigger <= 0) + { + Trigger(position, this.LayerDepth); + _nextAutoTrigger = _autoTriggerFrequency; + } + } + + if (Buffer.Count == 0) + return false; + + ReclaimExpiredParticles(); + + var iterator = Buffer.Iterator; + + while (iterator.HasNext) + { + var particle = iterator.Next(); + particle->Age = (_totalSeconds - particle->Inception) / _lifeSpanSeconds; + particle->Position = particle->Position + particle->Velocity * elapsedSeconds; + } + + ModifierExecutionStrategy.ExecuteModifiers(Modifiers, elapsedSeconds, iterator); + return true; + } + + public void Trigger(Vector2 position, float layerDepth = 0) + { + var numToRelease = _random.Next(Parameters.Quantity); + Release(position + Offset, numToRelease, layerDepth); + } + + public void Trigger(LineSegment line, float layerDepth = 0) + { + var numToRelease = _random.Next(Parameters.Quantity); + var lineVector = line.ToVector(); + + for (var i = 0; i < numToRelease; i++) + { + var offset = lineVector * _random.NextSingle(); + Release(line.Origin + offset, 1, layerDepth); + } + } + + private void Release(Vector2 position, int numToRelease, float layerDepth) + { + var iterator = Buffer.Release(numToRelease); + + while (iterator.HasNext) + { + var particle = iterator.Next(); + + Vector2 heading; + Profile.GetOffsetAndHeading(out particle->Position, out heading); + + particle->Age = 0f; + particle->Inception = _totalSeconds; + particle->Position += position; + particle->TriggerPos = position; + + var speed = _random.NextSingle(Parameters.Speed); + + particle->Velocity = heading * speed; + + _random.NextColor(out particle->Color, Parameters.Color); + + particle->Opacity = _random.NextSingle(Parameters.Opacity); + + if(Parameters.MaintainAspectRatioOnScale) + { + var scale = _random.NextSingle(Parameters.Scale); + particle->Scale = new Vector2(scale, scale); + } + else + { + particle->Scale = new Vector2(_random.NextSingle(Parameters.ScaleX), _random.NextSingle(Parameters.ScaleY)); + } + + particle->Rotation = _random.NextSingle(Parameters.Rotation); + particle->Mass = _random.NextSingle(Parameters.Mass); + particle->LayerDepth = layerDepth; + } + } + + public override string ToString() + { + return Name; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleExtensions.cs new file mode 100644 index 0000000..7566e8d --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleExtensions.cs @@ -0,0 +1,49 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using MonoGame.Extended.TextureAtlases; + +namespace MonoGame.Extended.Particles +{ + public static class ParticleExtensions + { + public static void Draw(this SpriteBatch spriteBatch, ParticleEffect effect) + { + for (var i = 0; i < effect.Emitters.Count; i++) + UnsafeDraw(spriteBatch, effect.Emitters[i]); + } + + public static void Draw(this SpriteBatch spriteBatch, ParticleEmitter emitter) + { + UnsafeDraw(spriteBatch, emitter); + } + + private static unsafe void UnsafeDraw(SpriteBatch spriteBatch, ParticleEmitter emitter) + { + if(emitter.TextureRegion == null) + return; + + var textureRegion = emitter.TextureRegion; + var origin = new Vector2(textureRegion.Width/2f, textureRegion.Height/2f); + var iterator = emitter.Buffer.Iterator; + + while (iterator.HasNext) + { + var particle = iterator.Next(); + var color = particle->Color.ToRgb(); + + if (spriteBatch.GraphicsDevice.BlendState == BlendState.AlphaBlend) + color *= particle->Opacity; + else + color.A = (byte) (particle->Opacity*255); + + var position = new Vector2(particle->Position.X, particle->Position.Y); + var scale = particle->Scale; + var particleColor = new Color(color, particle->Opacity); + var rotation = particle->Rotation; + var layerDepth = particle->LayerDepth; + + spriteBatch.Draw(textureRegion, position, particleColor, rotation, origin, scale, SpriteEffects.None, layerDepth); + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleModifierExecutionStrategy.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleModifierExecutionStrategy.cs new file mode 100644 index 0000000..20a4d51 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleModifierExecutionStrategy.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using MonoGame.Extended.Particles.Modifiers; + +namespace MonoGame.Extended.Particles +{ + using TPL = System.Threading.Tasks; + + public abstract class ParticleModifierExecutionStrategy + { + public static readonly ParticleModifierExecutionStrategy Serial = new SerialModifierExecutionStrategy(); + public static readonly ParticleModifierExecutionStrategy Parallel = new ParallelModifierExecutionStrategy(); + + internal abstract void ExecuteModifiers(List<Modifier> modifiers, float elapsedSeconds, ParticleBuffer.ParticleIterator iterator); + + internal class SerialModifierExecutionStrategy : ParticleModifierExecutionStrategy + { + internal override void ExecuteModifiers(List<Modifier> modifiers, float elapsedSeconds, ParticleBuffer.ParticleIterator iterator) + { + for (var i = 0; i < modifiers.Count; i++) + modifiers[i].Update(elapsedSeconds, iterator.Reset()); + } + + public override string ToString() + { + return nameof(Serial); + } + } + + internal class ParallelModifierExecutionStrategy : ParticleModifierExecutionStrategy + { + internal override void ExecuteModifiers(List<Modifier> modifiers, float elapsedSeconds, ParticleBuffer.ParticleIterator iterator) + { + TPL.Parallel.ForEach(modifiers, modifier => modifier.Update(elapsedSeconds, iterator.Reset())); + } + + public override string ToString() + { + return nameof(Parallel); + } + } + + public static ParticleModifierExecutionStrategy Parse(string value) + { + if (string.Equals(nameof(Parallel), value, StringComparison.OrdinalIgnoreCase)) + return Parallel; + + if (string.Equals(nameof(Serial), value, StringComparison.OrdinalIgnoreCase)) + return Serial; + + throw new InvalidOperationException($"Unknown particle modifier execution strategy '{value}'"); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleReleaseParameters.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleReleaseParameters.cs new file mode 100644 index 0000000..e19fbc2 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/ParticleReleaseParameters.cs @@ -0,0 +1,34 @@ +using Microsoft.Xna.Framework; +using MonoGame.Extended; + +namespace MonoGame.Extended.Particles +{ + public class ParticleReleaseParameters + { + public ParticleReleaseParameters() + { + Quantity = 1; + Speed = new Range<float>(-1f, 1f); + Color = new Range<HslColor>(Microsoft.Xna.Framework.Color.White.ToHsl(), Microsoft.Xna.Framework.Color.White.ToHsl()); + Opacity = new Range<float>(0f, 1f); + Scale = new Range<float>(1f, 1f); + Rotation = new Range<float>(-MathHelper.Pi, MathHelper.Pi); + Mass = 1f; + MaintainAspectRatioOnScale = true; + ScaleX = new Range<float>(1f, 1f); + ScaleY = new Range<float>(1f, 1f); + } + + public Range<int> Quantity { get; set; } + public Range<float> Speed { get; set; } + public Range<HslColor> Color { get; set; } + public Range<float> Opacity { get; set; } + public Range<float> Scale { get; set; } + public Range<float> Rotation { get; set; } + public Range<float> Mass { get; set; } + public bool MaintainAspectRatioOnScale { get; set; } + public Range<float> ScaleX { get; set; } + public Range<float> ScaleY { get; set; } + + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxFillProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxFillProfile.cs new file mode 100644 index 0000000..e363713 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxFillProfile.cs @@ -0,0 +1,18 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Particles.Profiles +{ + public class BoxFillProfile : Profile + { + public float Width { get; set; } + public float Height { get; set; } + + public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading) + { + offset = new Vector2(Random.NextSingle(Width*-0.5f, Width*0.5f), + Random.NextSingle(Height*-0.5f, Height*0.5f)); + + Random.NextUnitVector(out heading); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxProfile.cs new file mode 100644 index 0000000..7d31984 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxProfile.cs @@ -0,0 +1,31 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Particles.Profiles +{ + public class BoxProfile : Profile + { + public float Width { get; set; } + public float Height { get; set; } + + public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading) + { + switch (Random.Next(3)) + { + case 0: // Left + offset = new Vector2(Width*-0.5f, Random.NextSingle(Height*-0.5f, Height*0.5f)); + break; + case 1: // Top + offset = new Vector2(Random.NextSingle(Width*-0.5f, Width*0.5f), Height*-0.5f); + break; + case 2: // Right + offset = new Vector2(Width*0.5f, Random.NextSingle(Height*-0.5f, Height*0.5f)); + break; + default: // Bottom + offset = new Vector2(Random.NextSingle(Width*-0.5f, Width*0.5f), Height*0.5f); + break; + } + + Random.NextUnitVector(out heading); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxUniformProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxUniformProfile.cs new file mode 100644 index 0000000..9f766dc --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/BoxUniformProfile.cs @@ -0,0 +1,32 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Particles.Profiles +{ + public class BoxUniformProfile : Profile + { + public float Width { get; set; } + public float Height { get; set; } + + public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading) + { + var value = Random.Next((int) (2*Width + 2*Height)); + + if (value < Width) // Top + offset = new Vector2(Random.NextSingle(Width*-0.5f, Width*0.5f), Height*-0.5f); + else + { + if (value < 2*Width) // Bottom + offset = new Vector2(Random.NextSingle(Width*-0.5f, Width*0.5f), Height*0.5f); + else + { + if (value < 2*Width + Height) // Left + offset = new Vector2(Width*-0.5f, Random.NextSingle(Height*-0.5f, Height*0.5f)); + else // Right + offset = new Vector2(Width*0.5f, Random.NextSingle(Height*-0.5f, Height*0.5f)); + } + } + + Random.NextUnitVector(out heading); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/CircleProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/CircleProfile.cs new file mode 100644 index 0000000..ef7f11e --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/CircleProfile.cs @@ -0,0 +1,24 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Particles.Profiles +{ + public class CircleProfile : Profile + { + public float Radius { get; set; } + public CircleRadiation Radiate { get; set; } + + public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading) + { + var dist = Random.NextSingle(0f, Radius); + + Random.NextUnitVector(out heading); + + offset = Radiate == CircleRadiation.In + ? new Vector2(-heading.X*dist, -heading.Y*dist) + : new Vector2(heading.X*dist, heading.Y*dist); + + if (Radiate == CircleRadiation.None) + Random.NextUnitVector(out heading); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/LineProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/LineProfile.cs new file mode 100644 index 0000000..bea584e --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/LineProfile.cs @@ -0,0 +1,17 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Particles.Profiles +{ + public class LineProfile : Profile + { + public Vector2 Axis { get; set; } + public float Length { get; set; } + + public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading) + { + var vect = Axis*Random.NextSingle(Length*-0.5f, Length*0.5f); + offset = new Vector2(vect.X, vect.Y); + Random.NextUnitVector(out heading); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/PointProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/PointProfile.cs new file mode 100644 index 0000000..04456d3 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/PointProfile.cs @@ -0,0 +1,14 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Particles.Profiles +{ + public class PointProfile : Profile + { + public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading) + { + offset = Vector2.Zero; + + Random.NextUnitVector(out heading); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/Profile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/Profile.cs new file mode 100644 index 0000000..bfde371 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/Profile.cs @@ -0,0 +1,68 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Particles.Profiles +{ + public abstract class Profile + { + public enum CircleRadiation + { + None, + In, + Out + } + + protected FastRandom Random { get; } = new FastRandom(); + + public abstract void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading); + + public object Clone() + { + return MemberwiseClone(); + } + + public static Profile Point() + { + return new PointProfile(); + } + + public static Profile Line(Vector2 axis, float length) + { + return new LineProfile {Axis = axis, Length = length}; + } + + public static Profile Ring(float radius, CircleRadiation radiate) + { + return new RingProfile {Radius = radius, Radiate = radiate}; + } + + public static Profile Box(float width, float height) + { + return new BoxProfile {Width = width, Height = height}; + } + + public static Profile BoxFill(float width, float height) + { + return new BoxFillProfile {Width = width, Height = height}; + } + + public static Profile BoxUniform(float width, float height) + { + return new BoxUniformProfile {Width = width, Height = height}; + } + + public static Profile Circle(float radius, CircleRadiation radiate) + { + return new CircleProfile {Radius = radius, Radiate = radiate}; + } + + public static Profile Spray(Vector2 direction, float spread) + { + return new SprayProfile {Direction = direction, Spread = spread}; + } + + public override string ToString() + { + return GetType().ToString(); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/RingProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/RingProfile.cs new file mode 100644 index 0000000..d5ac2f8 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/RingProfile.cs @@ -0,0 +1,32 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Particles.Profiles +{ + public class RingProfile : Profile + { + public float Radius { get; set; } + public CircleRadiation Radiate { get; set; } + + public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading) + { + Random.NextUnitVector(out heading); + + switch (Radiate) + { + case CircleRadiation.In: + offset = new Vector2(-heading.X*Radius, -heading.Y*Radius); + break; + case CircleRadiation.Out: + offset = new Vector2(heading.X*Radius, heading.Y*Radius); + break; + case CircleRadiation.None: + offset = new Vector2(heading.X*Radius, heading.Y*Radius); + Random.NextUnitVector(out heading); + break; + default: + throw new ArgumentOutOfRangeException($"{Radiate} is not supported"); + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/SprayProfile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/SprayProfile.cs new file mode 100644 index 0000000..6259210 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Profiles/SprayProfile.cs @@ -0,0 +1,20 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Particles.Profiles +{ + public class SprayProfile : Profile + { + public Vector2 Direction { get; set; } + public float Spread { get; set; } + + public override void GetOffsetAndHeading(out Vector2 offset, out Vector2 heading) + { + var angle = (float) Math.Atan2(Direction.Y, Direction.X); + + angle = Random.NextSingle(angle - Spread/2f, angle + Spread/2f); + offset = Vector2.Zero; + heading = new Vector2((float) Math.Cos(angle), (float) Math.Sin(angle)); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/InterpolatorJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/InterpolatorJsonConverter.cs new file mode 100644 index 0000000..21bd7fc --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/InterpolatorJsonConverter.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using MonoGame.Extended.Particles.Modifiers.Interpolators; +using MonoGame.Extended.Serialization; + +namespace MonoGame.Extended.Particles.Serialization +{ + public class InterpolatorJsonConverter : BaseTypeJsonConverter<Interpolator> + { + public InterpolatorJsonConverter() + : base(GetSupportedTypes(), "Interpolator") + { + } + + private static IEnumerable<TypeInfo> GetSupportedTypes() + { + return typeof(Interpolator) + .GetTypeInfo() + .Assembly + .DefinedTypes + .Where(type => typeof(Interpolator).GetTypeInfo().IsAssignableFrom(type) && !type.IsAbstract); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierExecutionStrategyJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierExecutionStrategyJsonConverter.cs new file mode 100644 index 0000000..8fc894b --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierExecutionStrategyJsonConverter.cs @@ -0,0 +1,34 @@ +using System; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MonoGame.Extended.Particles.Serialization; + +/// <summary> +/// Converts a <see cref="ParticleModifierExecutionStrategy"/> value to or from JSON. +/// </summary> +public class ModifierExecutionStrategyJsonConverter : JsonConverter<ParticleModifierExecutionStrategy> +{ + /// <inheritdoc /> + public override bool CanConvert(Type typeToConvert) => + typeToConvert == typeof(ParticleModifierExecutionStrategy) || + typeToConvert.GetTypeInfo().BaseType == typeof(ParticleModifierExecutionStrategy); + + /// <inheritdoc /> + public override ParticleModifierExecutionStrategy Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = JsonSerializer.Deserialize<string>(ref reader, options); + return ParticleModifierExecutionStrategy.Parse(value); + } + + /// <inheritdoc /> + /// <exception cref="ArgumentNullException"> + /// Throw if <paramref name="writer"/> is <see langword="null"/>. + /// </exception> + public override void Write(Utf8JsonWriter writer, ParticleModifierExecutionStrategy value, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(writer); + writer.WriteStringValue(value.ToString()); + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierJsonConverter.cs new file mode 100644 index 0000000..5985e40 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ModifierJsonConverter.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using MonoGame.Extended.Particles.Modifiers; +using MonoGame.Extended.Serialization; + +namespace MonoGame.Extended.Particles.Serialization +{ + public class ModifierJsonConverter : BaseTypeJsonConverter<Modifier> + { + public ModifierJsonConverter() + : base(GetSupportedTypes(), "Modifier") + { + } + + private static IEnumerable<TypeInfo> GetSupportedTypes() + { + return typeof(Modifier) + .GetTypeInfo() + .Assembly + .DefinedTypes + .Where(type => typeof(Modifier).GetTypeInfo().IsAssignableFrom(type) && !type.IsAbstract); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ParticleJsonSerializerOptionsProvider.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ParticleJsonSerializerOptionsProvider.cs new file mode 100644 index 0000000..489b3ee --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ParticleJsonSerializerOptionsProvider.cs @@ -0,0 +1,34 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using MonoGame.Extended.Serialization; + +namespace MonoGame.Extended.Particles.Serialization; + +public static class ParticleJsonSerializerOptionsProvider +{ + public static JsonSerializerOptions GetOptions(ITextureRegionService textureRegionService) + { + var options = new JsonSerializerOptions + { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + options.Converters.Add(new Vector2JsonConverter()); + options.Converters.Add(new Size2JsonConverter()); + options.Converters.Add(new ColorJsonConverter()); + options.Converters.Add(new TextureRegion2DJsonConverter(textureRegionService)); + options.Converters.Add(new ProfileJsonConverter()); + options.Converters.Add(new ModifierJsonConverter()); + options.Converters.Add(new InterpolatorJsonConverter()); + options.Converters.Add(new TimeSpanJsonConverter()); + options.Converters.Add(new RangeJsonConverter<int>()); + options.Converters.Add(new RangeJsonConverter<float>()); + options.Converters.Add(new RangeJsonConverter<HslColor>()); + options.Converters.Add(new HslColorJsonConverter()); + options.Converters.Add(new ModifierExecutionStrategyJsonConverter()); + + return options; + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ProfileJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ProfileJsonConverter.cs new file mode 100644 index 0000000..8d98079 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/ProfileJsonConverter.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using MonoGame.Extended.Particles.Profiles; +using MonoGame.Extended.Serialization; + +namespace MonoGame.Extended.Particles.Serialization +{ + public class ProfileJsonConverter : BaseTypeJsonConverter<Profile> + { + public ProfileJsonConverter() + : base(GetSupportedTypes(), nameof(Profile)) + { + } + + private static IEnumerable<TypeInfo> GetSupportedTypes() + { + return typeof(Profile) + .GetTypeInfo() + .Assembly + .DefinedTypes + .Where(type => type.IsSubclassOf(typeof(Profile)) && !type.IsAbstract); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/TimeSpanJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/TimeSpanJsonConverter.cs new file mode 100644 index 0000000..0155b08 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Particles/Serialization/TimeSpanJsonConverter.cs @@ -0,0 +1,33 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MonoGame.Extended.Particles.Serialization; + +public class TimeSpanJsonConverter : JsonConverter<TimeSpan> +{ + /// <inheritdoc /> + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(TimeSpan); + + /// <inheritdoc /> + public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Number) + { + double seconds = reader.GetDouble(); + return TimeSpan.FromSeconds(seconds); + } + + return TimeSpan.Zero; + } + + /// <inheritdoc /> + /// <exception cref="ArgumentNullException"> + /// Throw if <paramref name="writer"/> is <see langword="null"/>. + /// </exception> + public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(writer); + writer.WriteNumberValue(value.TotalSeconds); + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/ContentReaderExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/ContentReaderExtensions.cs new file mode 100644 index 0000000..86f1b6b --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/ContentReaderExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.Xna.Framework.Content; + +namespace MonoGame.Extended.Tiled +{ + public static class ContentReaderExtensions + { + public static void ReadTiledMapProperties(this ContentReader reader, TiledMapProperties properties) + { + var count = reader.ReadInt32(); + + for (var i = 0; i < count; i++) + { + var key = reader.ReadString(); + var value = new TiledMapPropertyValue(reader.ReadString()); + ReadTiledMapProperties(reader, value.Properties); + properties[key] = value; + } + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/MonoGame.Extended.Tiled.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/MonoGame.Extended.Tiled.csproj new file mode 100644 index 0000000..a52cc65 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/MonoGame.Extended.Tiled.csproj @@ -0,0 +1,13 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <Description>Support for Tiled maps to make MonoGame more awesome. See http://www.mapeditor.org</Description> + <PackageTags>monogame tiled maps orthographic isometric</PackageTags> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\MonoGame.Extended.Graphics\MonoGame.Extended.Graphics.csproj" /> + <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" /> + </ItemGroup> + +</Project> diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapAnimatedLayerModel.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapAnimatedLayerModel.cs new file mode 100644 index 0000000..791819a --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapAnimatedLayerModel.cs @@ -0,0 +1,32 @@ +using Microsoft.Xna.Framework.Graphics; +using System; + +namespace MonoGame.Extended.Tiled.Renderers +{ + public sealed class TiledMapAnimatedLayerModel : TiledMapLayerModel + { + public TiledMapAnimatedLayerModel(GraphicsDevice graphicsDevice, Texture2D texture, VertexPositionTexture[] vertices, ushort[] indices, TiledMapTilesetAnimatedTile[] animatedTilesetTiles, TiledMapTileFlipFlags[] animatedTilesetTileFlipFlags) + : base(graphicsDevice, texture, vertices, indices) + { + Vertices = vertices; + AnimatedTilesetTiles = animatedTilesetTiles; + _animatedTilesetFlipFlags = animatedTilesetTileFlipFlags; + } + + public VertexPositionTexture[] Vertices { get; } + public TiledMapTilesetAnimatedTile[] AnimatedTilesetTiles { get; } + private readonly TiledMapTileFlipFlags[] _animatedTilesetFlipFlags; + + public ReadOnlySpan<TiledMapTileFlipFlags> AnimatedTilesetFlipFlags => _animatedTilesetFlipFlags; + + protected override VertexBuffer CreateVertexBuffer(GraphicsDevice graphicsDevice, int vertexCount) + { + return new DynamicVertexBuffer(graphicsDevice, VertexPositionTexture.VertexDeclaration, vertexCount, BufferUsage.WriteOnly); + } + + protected override IndexBuffer CreateIndexBuffer(GraphicsDevice graphicsDevice, int indexCount) + { + return new DynamicIndexBuffer(graphicsDevice, IndexElementSize.SixteenBits, indexCount, BufferUsage.WriteOnly); ; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapAnimatedLayerModelBuilder.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapAnimatedLayerModelBuilder.cs new file mode 100644 index 0000000..7afcc21 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapAnimatedLayerModelBuilder.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Tiled.Renderers +{ + public class TiledMapAnimatedLayerModelBuilder : TiledMapLayerModelBuilder<TiledMapAnimatedLayerModel> + { + public TiledMapAnimatedLayerModelBuilder() + { + AnimatedTilesetTiles = new List<TiledMapTilesetAnimatedTile>(); + AnimatedTilesetFlipFlags = new List<TiledMapTileFlipFlags>(); + } + + public List<TiledMapTilesetAnimatedTile> AnimatedTilesetTiles { get; } + public List<TiledMapTileFlipFlags> AnimatedTilesetFlipFlags { get; } + + protected override void ClearBuffers() + { + AnimatedTilesetTiles.Clear(); + AnimatedTilesetFlipFlags.Clear(); + } + + protected override TiledMapAnimatedLayerModel CreateModel(GraphicsDevice graphicsDevice, Texture2D texture) + { + return new TiledMapAnimatedLayerModel(graphicsDevice, texture, Vertices.ToArray(), Indices.ToArray(), AnimatedTilesetTiles.ToArray(), AnimatedTilesetFlipFlags.ToArray()); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapEffect.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapEffect.cs new file mode 100644 index 0000000..b24e2e9 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapEffect.cs @@ -0,0 +1,37 @@ +using Microsoft.Xna.Framework.Graphics; +using MonoGame.Extended.Graphics.Effects; + +namespace MonoGame.Extended.Tiled.Renderers +{ + public interface ITiledMapEffect : IEffectMatrices, ITextureEffect + { + float Alpha { get; set; } + } + + public class TiledMapEffect : DefaultEffect, ITiledMapEffect + { + public TiledMapEffect(GraphicsDevice graphicsDevice) + : base(graphicsDevice) + { + Initialize(); + } + + public TiledMapEffect(GraphicsDevice graphicsDevice, byte[] byteCode) + : base(graphicsDevice, byteCode) + { + Initialize(); + } + + public TiledMapEffect(Effect cloneSource) + : base(cloneSource) + { + Initialize(); + } + + private void Initialize() + { + VertexColorEnabled = false; + TextureEnabled = true; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapLayerModel.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapLayerModel.cs new file mode 100644 index 0000000..f837e1a --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapLayerModel.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Tiled.Renderers +{ + public abstract class TiledMapLayerModel : IDisposable + { + protected TiledMapLayerModel(GraphicsDevice graphicsDevice, Texture2D texture, VertexPositionTexture[] vertices, ushort[] indices) + { + Texture = texture; + + // ReSharper disable once VirtualMemberCallInConstructor + VertexBuffer = CreateVertexBuffer(graphicsDevice, vertices.Length); + VertexBuffer.SetData(vertices, 0, vertices.Length); + + // ReSharper disable once VirtualMemberCallInConstructor + IndexBuffer = CreateIndexBuffer(graphicsDevice, indices.Length); + IndexBuffer.SetData(indices, 0, indices.Length); + + TriangleCount = indices.Length / 3; + } + + public void Dispose() + { + IndexBuffer.Dispose(); + VertexBuffer.Dispose(); + } + + public Texture2D Texture { get; } + public VertexBuffer VertexBuffer { get; } + public IndexBuffer IndexBuffer { get; } + public int TriangleCount { get; } + + protected abstract VertexBuffer CreateVertexBuffer(GraphicsDevice graphicsDevice, int vertexCount); + protected abstract IndexBuffer CreateIndexBuffer(GraphicsDevice graphicsDevice, int indexCount); + + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapLayerModelBuilder.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapLayerModelBuilder.cs new file mode 100644 index 0000000..fbc99d6 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapLayerModelBuilder.cs @@ -0,0 +1,125 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Tiled.Renderers +{ + public abstract class TiledMapLayerModelBuilder<T> + { + protected TiledMapLayerModelBuilder() + { + Indices = new List<ushort>(); + Vertices = new List<VertexPositionTexture>(); + } + + public List<ushort> Indices { get; } + public List<VertexPositionTexture> Vertices { get; } + public bool IsFull => Vertices.Count + TiledMapHelper.VerticesPerTile >= TiledMapHelper.MaximumVerticesPerModel; + public bool IsBuildable => Vertices.Any(); + + protected abstract void ClearBuffers(); + protected abstract T CreateModel(GraphicsDevice graphicsDevice, Texture2D texture); + + public T Build(GraphicsDevice graphicsDevice, Texture2D texture) + { + var model = CreateModel(graphicsDevice, texture); + Vertices.Clear(); + Indices.Clear(); + ClearBuffers(); + return model; + } + + public void AddSprite(Texture2D texture, Point2 position, Rectangle sourceRectangle, TiledMapTileFlipFlags flipFlags) + { + Indices.AddRange(CreateTileIndices(Vertices.Count)); + Debug.Assert(Indices.Count <= TiledMapHelper.MaximumIndicesPerModel); + + Vertices.AddRange(CreateVertices(texture, position, sourceRectangle, flipFlags)); + Debug.Assert(Vertices.Count <= TiledMapHelper.MaximumVerticesPerModel); + } + + private static IEnumerable<VertexPositionTexture> CreateVertices(Texture2D texture, Vector2 position, Rectangle sourceRectangle, TiledMapTileFlipFlags flags = TiledMapTileFlipFlags.None) + { + var reciprocalWidth = 1f / texture.Width; + var reciprocalHeight = 1f / texture.Height; + var texelLeft = sourceRectangle.X * reciprocalWidth; + var texelTop = sourceRectangle.Y * reciprocalHeight; + var texelRight = (sourceRectangle.X + sourceRectangle.Width) * reciprocalWidth; + var texelBottom = (sourceRectangle.Y + sourceRectangle.Height) * reciprocalHeight; + + VertexPositionTexture vertexTopLeft, vertexTopRight, vertexBottomLeft, vertexBottomRight; + + vertexTopLeft.Position = new Vector3(position, 0); + vertexTopRight.Position = new Vector3(position + new Vector2(sourceRectangle.Width, 0), 0); + vertexBottomLeft.Position = new Vector3(position + new Vector2(0, sourceRectangle.Height), 0); + vertexBottomRight.Position = new Vector3(position + new Vector2(sourceRectangle.Width, sourceRectangle.Height), 0); + + vertexTopLeft.TextureCoordinate.Y = texelTop; + vertexTopLeft.TextureCoordinate.X = texelLeft; + + vertexTopRight.TextureCoordinate.Y = texelTop; + vertexTopRight.TextureCoordinate.X = texelRight; + + vertexBottomLeft.TextureCoordinate.Y = texelBottom; + vertexBottomLeft.TextureCoordinate.X = texelLeft; + + vertexBottomRight.TextureCoordinate.Y = texelBottom; + vertexBottomRight.TextureCoordinate.X = texelRight; + + var flipDiagonally = (flags & TiledMapTileFlipFlags.FlipDiagonally) != 0; + var flipHorizontally = (flags & TiledMapTileFlipFlags.FlipHorizontally) != 0; + var flipVertically = (flags & TiledMapTileFlipFlags.FlipVertically) != 0; + + if (flipDiagonally) + { + FloatHelper.Swap(ref vertexTopRight.TextureCoordinate.X, ref vertexBottomLeft.TextureCoordinate.X); + FloatHelper.Swap(ref vertexTopRight.TextureCoordinate.Y, ref vertexBottomLeft.TextureCoordinate.Y); + } + + if (flipHorizontally) + { + if (flipDiagonally) + { + FloatHelper.Swap(ref vertexTopLeft.TextureCoordinate.Y, ref vertexTopRight.TextureCoordinate.Y); + FloatHelper.Swap(ref vertexBottomLeft.TextureCoordinate.Y, ref vertexBottomRight.TextureCoordinate.Y); + } + else + { + FloatHelper.Swap(ref vertexTopLeft.TextureCoordinate.X, ref vertexTopRight.TextureCoordinate.X); + FloatHelper.Swap(ref vertexBottomLeft.TextureCoordinate.X, ref vertexBottomRight.TextureCoordinate.X); + } + } + + if (flipVertically) + { + if (flipDiagonally) + { + FloatHelper.Swap(ref vertexTopLeft.TextureCoordinate.X, ref vertexBottomLeft.TextureCoordinate.X); + FloatHelper.Swap(ref vertexTopRight.TextureCoordinate.X, ref vertexBottomRight.TextureCoordinate.X); + } + else + { + FloatHelper.Swap(ref vertexTopLeft.TextureCoordinate.Y, ref vertexBottomLeft.TextureCoordinate.Y); + FloatHelper.Swap(ref vertexTopRight.TextureCoordinate.Y, ref vertexBottomRight.TextureCoordinate.Y); + } + } + + yield return vertexTopLeft; + yield return vertexTopRight; + yield return vertexBottomLeft; + yield return vertexBottomRight; + } + + private static IEnumerable<ushort> CreateTileIndices(int indexOffset) + { + yield return (ushort)(0 + indexOffset); + yield return (ushort)(1 + indexOffset); + yield return (ushort)(2 + indexOffset); + yield return (ushort)(1 + indexOffset); + yield return (ushort)(3 + indexOffset); + yield return (ushort)(2 + indexOffset); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapModel.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapModel.cs new file mode 100644 index 0000000..52e56bc --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapModel.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace MonoGame.Extended.Tiled.Renderers +{ + public class TiledMapModel : IDisposable + { + private readonly TiledMap _map; + private readonly Dictionary<TiledMapTileset, List<TiledMapTilesetAnimatedTile>> _animatedTilesByTileset; + + public TiledMapModel(TiledMap map, Dictionary<TiledMapLayer, TiledMapLayerModel[]> layersOfLayerModels) + { + _map = map; + LayersOfLayerModels = layersOfLayerModels; + _animatedTilesByTileset = _map.Tilesets + .ToDictionary(i => i, i => i.Tiles.OfType<TiledMapTilesetAnimatedTile>() + .ToList()); + } + + public void Dispose() + { + foreach (var layerModel in LayersOfLayerModels) + foreach (var model in layerModel.Value) + model.Dispose(); + } + + public ReadOnlyCollection<TiledMapTileset> Tilesets => _map.Tilesets; + public ReadOnlyCollection<TiledMapLayer> Layers => _map.Layers; + + // each layer has many models + public Dictionary<TiledMapLayer, TiledMapLayerModel[]> LayersOfLayerModels { get; } + + public IEnumerable<TiledMapTilesetAnimatedTile> GetAnimatedTiles(int tilesetIndex) + { + var tileset = _map.Tilesets[tilesetIndex]; + return _animatedTilesByTileset[tileset]; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapModelBuilder.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapModelBuilder.cs new file mode 100644 index 0000000..b96925e --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapModelBuilder.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Tiled.Renderers +{ + public class TiledMapModelBuilder + { + private readonly GraphicsDevice _graphicsDevice; + + public TiledMapModelBuilder(GraphicsDevice graphicsDevice) + { + _graphicsDevice = graphicsDevice; + } + + private IEnumerable<TiledMapLayerModel> CreateLayerModels(TiledMap map, TiledMapLayer layer) + { + switch(layer) + { + case TiledMapTileLayer tileLayer: + return CreateTileLayerModels(map, tileLayer); + case TiledMapImageLayer imageLayer: + return CreateImageLayerModels(imageLayer); + default: + return new List<TiledMapLayerModel>(); + } + + } + + private IEnumerable<TiledMapLayerModel> CreateImageLayerModels(TiledMapImageLayer imageLayer) + { + var modelBuilder = new TiledMapStaticLayerModelBuilder(); + modelBuilder.AddSprite(imageLayer.Image, imageLayer.Position, imageLayer.Image.Bounds, TiledMapTileFlipFlags.None); + yield return modelBuilder.Build(_graphicsDevice, imageLayer.Image); + } + + private IEnumerable<TiledMapLayerModel> CreateTileLayerModels(TiledMap map, TiledMapTileLayer tileLayer) + { + var layerModels = new List<TiledMapLayerModel>(); + var staticLayerBuilder = new TiledMapStaticLayerModelBuilder(); + var animatedLayerBuilder = new TiledMapAnimatedLayerModelBuilder(); + + foreach (var tileset in map.Tilesets) + { + var firstGlobalIdentifier = map.GetTilesetFirstGlobalIdentifier(tileset); + var lastGlobalIdentifier = tileset.TileCount + firstGlobalIdentifier - 1; + var texture = tileset.Texture; + + foreach (var tile in tileLayer.Tiles.Where(t => firstGlobalIdentifier <= t.GlobalIdentifier && t.GlobalIdentifier <= lastGlobalIdentifier)) + { + var tileGid = tile.GlobalIdentifier; + var localTileIdentifier = tileGid - firstGlobalIdentifier; + var position = GetTilePosition(map, tile); + var sourceRectangle = tileset.GetTileRegion(localTileIdentifier); + var flipFlags = tile.Flags; + + // animated tiles + var tilesetTile = tileset.Tiles.FirstOrDefault(x => x.LocalTileIdentifier == localTileIdentifier); + if (tilesetTile?.Texture is not null) + { + position.Y += map.TileHeight - sourceRectangle.Height; + texture = tilesetTile.Texture; + } + + if (tilesetTile is TiledMapTilesetAnimatedTile animatedTilesetTile) + { + animatedLayerBuilder.AddSprite(texture, position, sourceRectangle, flipFlags); + animatedTilesetTile.CreateTextureRotations(tileset, flipFlags); + animatedLayerBuilder.AnimatedTilesetTiles.Add(animatedTilesetTile); + animatedLayerBuilder.AnimatedTilesetFlipFlags.Add(flipFlags); + + if (animatedLayerBuilder.IsFull) + layerModels.Add(animatedLayerBuilder.Build(_graphicsDevice, texture)); + } + else + { + staticLayerBuilder.AddSprite(texture, position, sourceRectangle, flipFlags); + + if (staticLayerBuilder.IsFull) + layerModels.Add(staticLayerBuilder.Build(_graphicsDevice, texture)); + } + } + + if (staticLayerBuilder.IsBuildable) + layerModels.Add(staticLayerBuilder.Build(_graphicsDevice, texture)); + + if (animatedLayerBuilder.IsBuildable) + layerModels.Add(animatedLayerBuilder.Build(_graphicsDevice, texture)); + } + + return layerModels; + } + + public TiledMapModel Build(TiledMap map) + { + var dictionary = new Dictionary<TiledMapLayer, TiledMapLayerModel[]>(); + foreach (var layer in map.Layers) + BuildLayer(map, layer, dictionary); + + return new TiledMapModel(map, dictionary); + } + + private void BuildLayer(TiledMap map, TiledMapLayer layer, Dictionary<TiledMapLayer, TiledMapLayerModel[]> dictionary) + { + if (layer is TiledMapGroupLayer groupLayer) + foreach (var subLayer in groupLayer.Layers) + BuildLayer(map, subLayer, dictionary); + else + dictionary.Add(layer, CreateLayerModels(map, layer).ToArray()); + } + + private static Point2 GetTilePosition(TiledMap map, TiledMapTile mapTile) + { + switch (map.Orientation) + { + case TiledMapOrientation.Orthogonal: + return TiledMapHelper.GetOrthogonalPosition(mapTile.X, mapTile.Y, map.TileWidth, map.TileHeight); + case TiledMapOrientation.Isometric: + return TiledMapHelper.GetIsometricPosition(mapTile.X, mapTile.Y, map.TileWidth, map.TileHeight); + default: + throw new NotSupportedException($"{map.Orientation} Tiled Maps are not yet implemented."); + } + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapRenderer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapRenderer.cs new file mode 100644 index 0000000..66babad --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapRenderer.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Tiled.Renderers +{ + public class TiledMapRenderer : IDisposable + { + private readonly TiledMapModelBuilder _mapModelBuilder; + private readonly TiledMapEffect _defaultEffect; + private readonly GraphicsDevice _graphicsDevice; + private TiledMapModel _mapModel; + private Matrix _worldMatrix = Matrix.Identity; + + public TiledMapRenderer(GraphicsDevice graphicsDevice, TiledMap map = null) + { + if (graphicsDevice == null) throw new ArgumentNullException(nameof(graphicsDevice)); + + _graphicsDevice = graphicsDevice; + _defaultEffect = new TiledMapEffect(graphicsDevice); + _mapModelBuilder = new TiledMapModelBuilder(graphicsDevice); + + if(map != null) + LoadMap(map); + } + + public void Dispose() + { + _mapModel?.Dispose(); + _defaultEffect.Dispose(); + } + + public void LoadMap(TiledMap map) + { + _mapModel?.Dispose(); + _mapModel = map != null ? _mapModelBuilder.Build(map) : null; + } + + public void Update(GameTime gameTime) + { + if(_mapModel == null) + return; + + for (var tilesetIndex = 0; tilesetIndex < _mapModel.Tilesets.Count; tilesetIndex++) + { + foreach (var animatedTilesetTile in _mapModel.GetAnimatedTiles(tilesetIndex)) + animatedTilesetTile.Update(gameTime); + } + + foreach(var layer in _mapModel.LayersOfLayerModels) + UpdateAnimatedLayerModels(layer.Value.OfType<TiledMapAnimatedLayerModel>()); + } + + private static unsafe void UpdateAnimatedLayerModels(IEnumerable<TiledMapAnimatedLayerModel> animatedLayerModels) + { + foreach (var animatedModel in animatedLayerModels) + { + // update the texture coordinates for each animated tile + fixed (VertexPositionTexture* fixedVerticesPointer = animatedModel.Vertices) + { + var verticesPointer = fixedVerticesPointer; + for (int i = 0; i < animatedModel.AnimatedTilesetTiles.Length; i++) + { + var currentFrameTextureCoordinates = animatedModel.AnimatedTilesetTiles[i].CurrentAnimationFrame.GetTextureCoordinates(animatedModel.AnimatedTilesetFlipFlags[i]); + + // ReSharper disable ArrangeRedundantParentheses + (*verticesPointer++).TextureCoordinate = currentFrameTextureCoordinates[0]; + (*verticesPointer++).TextureCoordinate = currentFrameTextureCoordinates[1]; + (*verticesPointer++).TextureCoordinate = currentFrameTextureCoordinates[2]; + (*verticesPointer++).TextureCoordinate = currentFrameTextureCoordinates[3]; + // ReSharper restore ArrangeRedundantParentheses + } + } + + // copy (upload) the updated vertices to the GPU's memory + animatedModel.VertexBuffer.SetData(animatedModel.Vertices, 0, animatedModel.Vertices.Length); + } + } + + public void Draw(Matrix? viewMatrix = null, Matrix? projectionMatrix = null, Effect effect = null, float depth = 0.0f) + { + var viewMatrix1 = viewMatrix ?? Matrix.Identity; + var projectionMatrix1 = projectionMatrix ?? Matrix.CreateOrthographicOffCenter(0, _graphicsDevice.Viewport.Width, _graphicsDevice.Viewport.Height, 0, 0, -1); + + Draw(ref viewMatrix1, ref projectionMatrix1, effect, depth); + } + + public void Draw(ref Matrix viewMatrix, ref Matrix projectionMatrix, Effect effect = null, float depth = 0.0f) + { + if (_mapModel == null) + return; + + for (var index = 0; index < _mapModel.Layers.Count; index++) + Draw(index, ref viewMatrix, ref projectionMatrix, effect, depth); + } + + public void Draw(TiledMapLayer layer, Matrix? viewMatrix = null, Matrix? projectionMatrix = null, Effect effect = null, float depth = 0.0f) + { + var viewMatrix1 = viewMatrix ?? Matrix.Identity; + var projectionMatrix1 = projectionMatrix ?? Matrix.CreateOrthographicOffCenter(0, _graphicsDevice.Viewport.Width, _graphicsDevice.Viewport.Height, 0, 0, -1); + + Draw(layer, ref viewMatrix1, ref projectionMatrix1, effect, depth); + } + + public void Draw(int layerIndex, Matrix? viewMatrix = null, Matrix? projectionMatrix = null, Effect effect = null, float depth = 0.0f) + { + var viewMatrix1 = viewMatrix ?? Matrix.Identity; + var projectionMatrix1 = projectionMatrix ?? Matrix.CreateOrthographicOffCenter(0, _graphicsDevice.Viewport.Width, _graphicsDevice.Viewport.Height, 0, 0, -1); + + Draw(layerIndex, ref viewMatrix1, ref projectionMatrix1, effect, depth); + } + + public void Draw(int layerIndex, ref Matrix viewMatrix, ref Matrix projectionMatrix, Effect effect = null, float depth = 0.0f) + { + var layer = _mapModel.Layers[layerIndex]; + + Draw(layer, ref viewMatrix, ref projectionMatrix, effect, depth); + } + + public void Draw(TiledMapLayer layer, ref Matrix viewMatrix, ref Matrix projectionMatrix, Effect effect = null, float depth = 0.0f) + { + if (_mapModel == null) + return; + + if (!layer.IsVisible) + return; + + if (layer is TiledMapObjectLayer) + return; + + Draw(layer, Vector2.Zero, Vector2.One, ref viewMatrix, ref projectionMatrix, effect, depth); + } + + private void Draw(TiledMapLayer layer, Vector2 parentOffset, Vector2 parentParallaxFactor, ref Matrix viewMatrix, ref Matrix projectionMatrix, Effect effect, float depth) + { + var offset = parentOffset + layer.Offset; + var parallaxFactor = parentParallaxFactor * layer.ParallaxFactor; + + if (layer is TiledMapGroupLayer groupLayer) + { + foreach (var subLayer in groupLayer.Layers) + Draw(subLayer, offset, parallaxFactor, ref viewMatrix, ref projectionMatrix, effect, depth); + } + else + { + _worldMatrix.Translation = new Vector3(offset, depth); + + var effect1 = effect ?? _defaultEffect; + var tiledMapEffect = effect1 as ITiledMapEffect; + if (tiledMapEffect == null) + return; + + // model-to-world transform + tiledMapEffect.World = _worldMatrix; + tiledMapEffect.View = parallaxFactor == Vector2.One ? viewMatrix : IncludeParallax(viewMatrix, parallaxFactor); + tiledMapEffect.Projection = projectionMatrix; + + foreach (var layerModel in _mapModel.LayersOfLayerModels[layer]) + { + // desired alpha + tiledMapEffect.Alpha = layer.Opacity; + + // desired texture + tiledMapEffect.Texture = layerModel.Texture; + + // bind the vertex and index buffer + _graphicsDevice.SetVertexBuffer(layerModel.VertexBuffer); + _graphicsDevice.Indices = layerModel.IndexBuffer; + + // for each pass in our effect + foreach (var pass in effect1.CurrentTechnique.Passes) + { + // apply the pass, effectively choosing which vertex shader and fragment (pixel) shader to use + pass.Apply(); + + // draw the geometry from the vertex buffer / index buffer + _graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, layerModel.TriangleCount); + } + } + } + } + + private Matrix IncludeParallax(Matrix viewMatrix, Vector2 parallaxFactor) + { + viewMatrix.Translation *=new Vector3(parallaxFactor, 1f); + return viewMatrix; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapStaticLayerModel.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapStaticLayerModel.cs new file mode 100644 index 0000000..34bf683 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapStaticLayerModel.cs @@ -0,0 +1,22 @@ +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Tiled.Renderers +{ + public sealed class TiledMapStaticLayerModel : TiledMapLayerModel + { + public TiledMapStaticLayerModel(GraphicsDevice graphicsDevice, Texture2D texture, VertexPositionTexture[] vertices, ushort[] indices) + : base(graphicsDevice, texture, vertices, indices) + { + } + + protected override VertexBuffer CreateVertexBuffer(GraphicsDevice graphicsDevice, int vertexCount) + { + return new VertexBuffer(graphicsDevice, VertexPositionTexture.VertexDeclaration, vertexCount, BufferUsage.WriteOnly); + } + + protected override IndexBuffer CreateIndexBuffer(GraphicsDevice graphicsDevice, int indexCount) + { + return new IndexBuffer(graphicsDevice, IndexElementSize.SixteenBits, indexCount, BufferUsage.WriteOnly); ; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapStaticLayerModelBuilder.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapStaticLayerModelBuilder.cs new file mode 100644 index 0000000..8c7d49b --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Renderers/TiledMapStaticLayerModelBuilder.cs @@ -0,0 +1,16 @@ +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Tiled.Renderers +{ + public class TiledMapStaticLayerModelBuilder : TiledMapLayerModelBuilder<TiledMapStaticLayerModel> + { + protected override void ClearBuffers() + { + } + + protected override TiledMapStaticLayerModel CreateModel(GraphicsDevice graphicsDevice, Texture2D texture) + { + return new TiledMapStaticLayerModel(graphicsDevice, texture, Vertices.ToArray(), Indices.ToArray()); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapContent.cs new file mode 100644 index 0000000..30ecbfd --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapContent.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace MonoGame.Extended.Tiled.Serialization +{ + [XmlRoot(ElementName = "map")] + public class TiledMapContent + { + public TiledMapContent() + { + Properties = new List<TiledMapPropertyContent>(); + Tilesets = new List<TiledMapTilesetContent>(); + Layers = new List<TiledMapLayerContent>(); + } + + [XmlIgnore] + public string Name { get; set; } + + [XmlIgnore] + public string FilePath { get; set; } + + // Deprecated as of Tiled 1.9.0 (replaced by "class" attribute) + [XmlAttribute(DataType = "string", AttributeName = "type")] + public string Type { get; set; } + + [XmlAttribute(DataType = "string", AttributeName = "class")] + public string Class { get; set; } + + [XmlAttribute(AttributeName = "version")] + public string Version { get; set; } + + [XmlAttribute(AttributeName = "orientation")] + public TiledMapOrientationContent Orientation { get; set; } + + [XmlAttribute(AttributeName = "renderorder")] + public TiledMapTileDrawOrderContent RenderOrder { get; set; } + + [XmlAttribute(AttributeName = "backgroundcolor")] + public string BackgroundColor { get; set; } + + [XmlAttribute(AttributeName = "width")] + public int Width { get; set; } + + [XmlAttribute(AttributeName = "height")] + public int Height { get; set; } + + [XmlAttribute(AttributeName = "tilewidth")] + public int TileWidth { get; set; } + + [XmlAttribute(AttributeName = "tileheight")] + public int TileHeight { get; set; } + + [XmlAttribute(AttributeName = "hexsidelength")] + public int HexSideLength { get; set; } + + [XmlAttribute(AttributeName = "staggeraxis")] + public TiledMapStaggerAxisContent StaggerAxis { get; set; } + + [XmlAttribute(AttributeName = "staggerindex")] + public TiledMapStaggerIndexContent StaggerIndex { get; set; } + + [XmlElement(ElementName = "tileset")] + public List<TiledMapTilesetContent> Tilesets { get; set; } + + [XmlElement(ElementName = "layer", Type = typeof(TiledMapTileLayerContent))] + [XmlElement(ElementName = "imagelayer", Type = typeof(TiledMapImageLayerContent))] + [XmlElement(ElementName = "objectgroup", Type = typeof(TiledMapObjectLayerContent))] + [XmlElement(ElementName = "group", Type = typeof(TiledMapGroupLayerContent))] + public List<TiledMapLayerContent> Layers { get; set; } + + [XmlArray("properties")] + [XmlArrayItem("property")] + public List<TiledMapPropertyContent> Properties { get; set; } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapEllipseContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapEllipseContent.cs new file mode 100644 index 0000000..78f7bdd --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapEllipseContent.cs @@ -0,0 +1,6 @@ +namespace MonoGame.Extended.Tiled.Serialization +{ + public class TiledMapEllipseContent + { + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapGroupLayerContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapGroupLayerContent.cs new file mode 100644 index 0000000..a72b276 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapGroupLayerContent.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace MonoGame.Extended.Tiled.Serialization +{ + public class TiledMapGroupLayerContent : TiledMapLayerContent + { + protected TiledMapGroupLayerContent() + : base(TiledMapLayerType.GroupLayer) + { + } + + [XmlElement(ElementName = "layer", Type = typeof(TiledMapTileLayerContent))] + [XmlElement(ElementName = "imagelayer", Type = typeof(TiledMapImageLayerContent))] + [XmlElement(ElementName = "objectgroup", Type = typeof(TiledMapObjectLayerContent))] + [XmlElement(ElementName = "group", Type = typeof(TiledMapGroupLayerContent))] + public List<TiledMapLayerContent> Layers { get; set; } + + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapImageContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapImageContent.cs new file mode 100644 index 0000000..714da37 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapImageContent.cs @@ -0,0 +1,44 @@ +using System.Xml.Serialization; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Tiled.Serialization +{ + public class TiledMapImageContent + { + //[XmlIgnore] + //public Texture2DContent Content { get; set; } + + //[XmlIgnore] + //public ExternalReference<Texture2DContent> ContentRef { get; set; } + + [XmlAttribute(AttributeName = "source")] + public string Source { get; set; } + + [XmlAttribute(AttributeName = "width")] + public int Width { get; set; } + + [XmlAttribute(AttributeName = "height")] + public int Height { get; set; } + + [XmlAttribute(AttributeName = "format")] + public string Format { get; set; } + + [XmlAttribute(AttributeName = "trans")] + public string RawTransparentColor { get; set; } = string.Empty; + + [XmlIgnore] + public Color TransparentColor + { + get => RawTransparentColor == string.Empty ? Color.Transparent : ColorHelper.FromHex(RawTransparentColor); + set => RawTransparentColor = ColorHelper.ToHex(value); + } + + [XmlElement(ElementName = "data")] + public TiledMapTileLayerDataContent Data { get; set; } + + public override string ToString() + { + return Source; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapImageLayerContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapImageLayerContent.cs new file mode 100644 index 0000000..f347d1e --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapImageLayerContent.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace MonoGame.Extended.Tiled.Serialization +{ + public class TiledMapImageLayerContent : TiledMapLayerContent + { + [XmlAttribute(AttributeName = "x")] + public int X { get; set; } + + [XmlAttribute(AttributeName = "y")] + public int Y { get; set; } + + [XmlElement(ElementName = "image")] + public TiledMapImageContent Image { get; set; } + + public TiledMapImageLayerContent() + : base(TiledMapLayerType.ImageLayer) + { + Opacity = 1.0f; + Visible = true; + Properties = new List<TiledMapPropertyContent>(); + } + + public override string ToString() + { + return $"{Name}: {Image}"; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapLayerContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapLayerContent.cs new file mode 100644 index 0000000..01d8a9d --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapLayerContent.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace MonoGame.Extended.Tiled.Serialization +{ + [XmlInclude(typeof(TiledMapTileLayerContent))] + [XmlInclude(typeof(TiledMapImageLayerContent))] + [XmlInclude(typeof(TiledMapObjectLayerContent))] + public abstract class TiledMapLayerContent + { + protected TiledMapLayerContent(TiledMapLayerType layerType) + { + LayerType = layerType; + Opacity = 1.0f; + ParallaxX = 1.0f; + ParallaxY = 1.0f; + Visible = true; + Properties = new List<TiledMapPropertyContent>(); + } + + [XmlAttribute(AttributeName = "name")] + public string Name { get; set; } + + // Deprecated as of Tiled 1.9.0 (replaced by "class" attribute) + [XmlAttribute(DataType = "string", AttributeName = "type")] + public string Type { get; set; } + + [XmlAttribute(DataType = "string", AttributeName = "class")] + public string Class { get; set; } + + [XmlAttribute(AttributeName = "opacity")] + public float Opacity { get; set; } + + [XmlAttribute(AttributeName = "visible")] + public bool Visible { get; set; } + + [XmlAttribute(AttributeName = "offsetx")] + public float OffsetX { get; set; } + + [XmlAttribute(AttributeName = "offsety")] + public float OffsetY { get; set; } + + [XmlAttribute(AttributeName = "parallaxx")] + public float ParallaxX { get; set; } + + [XmlAttribute(AttributeName = "parallaxy")] + public float ParallaxY { get; set; } + + [XmlArray("properties")] + [XmlArrayItem("property")] + public List<TiledMapPropertyContent> Properties { get; set; } + + [XmlIgnore] + public TiledMapLayerType LayerType { get; } + + public override string ToString() + { + return Name; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapLayerModelContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapLayerModelContent.cs new file mode 100644 index 0000000..4c2a761 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapLayerModelContent.cs @@ -0,0 +1,139 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Tiled.Serialization +{ + public class TiledMapLayerModelContent + { + private readonly List<VertexPositionTexture> _vertices; + private readonly List<ushort> _indices; + + public string LayerName { get; } + public ReadOnlyCollection<VertexPositionTexture> Vertices { get; } + public ReadOnlyCollection<ushort> Indices { get; } + public Size2 ImageSize { get; } + public string TextureAssetName { get; } + + public TiledMapLayerModelContent(string layerName, TiledMapImageContent image) + { + LayerName = layerName; + _vertices = new List<VertexPositionTexture>(); + Vertices = new ReadOnlyCollection<VertexPositionTexture>(_vertices); + _indices = new List<ushort>(); + Indices = new ReadOnlyCollection<ushort>(_indices); + ImageSize = new Size2(image.Width, image.Height); + TextureAssetName = Path.ChangeExtension(image.Source, null); + } + + public TiledMapLayerModelContent(string layerName, TiledMapTilesetContent tileset) + : this(layerName, tileset.Image) + { + } + + public void AddTileVertices(Point2 position, Rectangle? sourceRectangle = null, TiledMapTileFlipFlags flags = TiledMapTileFlipFlags.None) + { + float texelLeft, texelTop, texelRight, texelBottom; + var sourceRectangle1 = sourceRectangle ?? new Rectangle(0, 0, (int)ImageSize.Width, (int)ImageSize.Height); + + if (sourceRectangle.HasValue) + { + var reciprocalWidth = 1f / ImageSize.Width; + var reciprocalHeight = 1f / ImageSize.Height; + texelLeft = sourceRectangle1.X * reciprocalWidth; + texelTop = sourceRectangle1.Y * reciprocalHeight; + texelRight = (sourceRectangle1.X + sourceRectangle1.Width) * reciprocalWidth; + texelBottom = (sourceRectangle1.Y + sourceRectangle1.Height) * reciprocalHeight; + } + else + { + texelLeft = 0; + texelTop = 0; + texelBottom = 1; + texelRight = 1; + } + + VertexPositionTexture vertexTopLeft, vertexTopRight, vertexBottomLeft, vertexBottomRight; + + vertexTopLeft.Position = new Vector3(position, 0); + vertexTopRight.Position = new Vector3(position + new Vector2(sourceRectangle1.Width, 0), 0); + vertexBottomLeft.Position = new Vector3(position + new Vector2(0, sourceRectangle1.Height), 0); + vertexBottomRight.Position = + new Vector3(position + new Vector2(sourceRectangle1.Width, sourceRectangle1.Height), 0); + + vertexTopLeft.TextureCoordinate.Y = texelTop; + vertexTopLeft.TextureCoordinate.X = texelLeft; + + vertexTopRight.TextureCoordinate.Y = texelTop; + vertexTopRight.TextureCoordinate.X = texelRight; + + vertexBottomLeft.TextureCoordinate.Y = texelBottom; + vertexBottomLeft.TextureCoordinate.X = texelLeft; + + vertexBottomRight.TextureCoordinate.Y = texelBottom; + vertexBottomRight.TextureCoordinate.X = texelRight; + + var flipDiagonally = (flags & TiledMapTileFlipFlags.FlipDiagonally) != 0; + var flipHorizontally = (flags & TiledMapTileFlipFlags.FlipHorizontally) != 0; + var flipVertically = (flags & TiledMapTileFlipFlags.FlipVertically) != 0; + + if (flipDiagonally) + { + FloatHelper.Swap(ref vertexTopRight.TextureCoordinate.X, ref vertexBottomLeft.TextureCoordinate.X); + FloatHelper.Swap(ref vertexTopRight.TextureCoordinate.Y, ref vertexBottomLeft.TextureCoordinate.Y); + } + + if (flipHorizontally) + { + if (flipDiagonally) + { + FloatHelper.Swap(ref vertexTopLeft.TextureCoordinate.Y, ref vertexTopRight.TextureCoordinate.Y); + FloatHelper.Swap(ref vertexBottomLeft.TextureCoordinate.Y, ref vertexBottomRight.TextureCoordinate.Y); + } + else + { + FloatHelper.Swap(ref vertexTopLeft.TextureCoordinate.X, ref vertexTopRight.TextureCoordinate.X); + FloatHelper.Swap(ref vertexBottomLeft.TextureCoordinate.X, ref vertexBottomRight.TextureCoordinate.X); + } + } + + if (flipVertically) + if (flipDiagonally) + { + FloatHelper.Swap(ref vertexTopLeft.TextureCoordinate.X, ref vertexBottomLeft.TextureCoordinate.X); + FloatHelper.Swap(ref vertexTopRight.TextureCoordinate.X, ref vertexBottomRight.TextureCoordinate.X); + } + else + { + FloatHelper.Swap(ref vertexTopLeft.TextureCoordinate.Y, ref vertexBottomLeft.TextureCoordinate.Y); + FloatHelper.Swap(ref vertexTopRight.TextureCoordinate.Y, ref vertexBottomRight.TextureCoordinate.Y); + } + + _vertices.Add(vertexTopLeft); + _vertices.Add(vertexTopRight); + _vertices.Add(vertexBottomLeft); + _vertices.Add(vertexBottomRight); + + Debug.Assert(Vertices.Count <= TiledMapHelper.MaximumVerticesPerModel); + } + + public void AddTileIndices() + { + var indexOffset = Vertices.Count; + + Debug.Assert(3 + indexOffset <= TiledMapHelper.MaximumVerticesPerModel); + + _indices.Add((ushort)(0 + indexOffset)); + _indices.Add((ushort)(1 + indexOffset)); + _indices.Add((ushort)(2 + indexOffset)); + _indices.Add((ushort)(1 + indexOffset)); + _indices.Add((ushort)(3 + indexOffset)); + _indices.Add((ushort)(2 + indexOffset)); + + Debug.Assert(Indices.Count <= TiledMapHelper.MaximumIndicesPerModel); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectContent.cs new file mode 100644 index 0000000..76e2ba2 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectContent.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace MonoGame.Extended.Tiled.Serialization +{ + // This content class is going to be a lot more complex than the others we use. + // Objects can reference a template file which has starting values for the + // object. The value in the object file overrides any value specified in the + // template. All values have to be able to store a null value so we know if the + // XML parser actually found a value for the property and not just a default + // value. Default values are used when the object and any templates don't + // specify a value. + public class TiledMapObjectContent + { + // TODO: HACK These shouldn't be public + public uint? _globalIdentifier; + public int? _identifier; + public float? _height; + public float? _rotation; + public bool? _visible; + public float? _width; + public float? _x; + public float? _y; + + [XmlAttribute(DataType = "int", AttributeName = "id")] + public int Identifier { get => _identifier ?? 0; set => _identifier = value; } + + [XmlAttribute(DataType = "string", AttributeName = "name")] + public string Name { get; set; } + + // Deprecated as of Tiled 1.9.0 (replaced by "class" attribute) + [XmlAttribute(DataType = "string", AttributeName = "type")] + public string Type { get; set; } + + [XmlAttribute(DataType = "string", AttributeName = "class")] + public string Class { get; set; } + + [XmlAttribute(DataType = "float", AttributeName = "x")] + public float X { get => _x ?? 0; set => _x = value; } + + [XmlAttribute(DataType = "float", AttributeName = "y")] + public float Y { get => _y ?? 0; set => _y = value; } + + [XmlAttribute(DataType = "float", AttributeName = "width")] + public float Width { get => _width ?? 0; set => _width = value; } + + [XmlAttribute(DataType = "float", AttributeName = "height")] + public float Height { get => _height ?? 0; set => _height = value; } + + [XmlAttribute(DataType = "float", AttributeName = "rotation")] + public float Rotation { get => _rotation ?? 0; set => _rotation = value; } + + [XmlAttribute(DataType = "boolean", AttributeName = "visible")] + public bool Visible { get => _visible ?? true; set => _visible = value; } + + [XmlArray("properties")] + [XmlArrayItem("property")] + public List<TiledMapPropertyContent> Properties { get; set; } + + [XmlAttribute(DataType = "unsignedInt", AttributeName = "gid")] + public uint GlobalIdentifier { get => _globalIdentifier??0; set => _globalIdentifier = value; } + + [XmlElement(ElementName = "ellipse")] + public TiledMapEllipseContent Ellipse { get; set; } + + [XmlElement(ElementName = "polygon")] + public TiledMapPolygonContent Polygon { get; set; } + + [XmlElement(ElementName = "polyline")] + public TiledMapPolylineContent Polyline { get; set; } + + [XmlAttribute(DataType = "string", AttributeName = "template")] + public string TemplateSource { get; set; } + + + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectDrawOrderContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectDrawOrderContent.cs new file mode 100644 index 0000000..f441375 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectDrawOrderContent.cs @@ -0,0 +1,10 @@ +using System.Xml.Serialization; + +namespace MonoGame.Extended.Tiled.Serialization +{ + public enum TiledMapObjectDrawOrderContent : byte + { + [XmlEnum(Name = "topdown")] TopDown, + [XmlEnum(Name = "index")] Manual + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectLayerContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectLayerContent.cs new file mode 100644 index 0000000..30669b5 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectLayerContent.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace MonoGame.Extended.Tiled.Serialization +{ + public class TiledMapObjectLayerContent : TiledMapLayerContent + { + public TiledMapObjectLayerContent() + : base(TiledMapLayerType.ObjectLayer) + { + Objects = new List<TiledMapObjectContent>(); + } + + [XmlAttribute(AttributeName = "color")] + public string Color { get; set; } + + [XmlElement(ElementName = "object")] + public List<TiledMapObjectContent> Objects { get; set; } + + [XmlAttribute(AttributeName = "draworder")] + public TiledMapObjectDrawOrderContent DrawOrder { get; set; } + + public override string ToString() + { + return Name; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectTemplateContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectTemplateContent.cs new file mode 100644 index 0000000..096f528 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapObjectTemplateContent.cs @@ -0,0 +1,17 @@ +using System.Xml.Serialization; + +namespace MonoGame.Extended.Tiled.Serialization +{ + [XmlRoot(ElementName = "template")] + public class TiledMapObjectTemplateContent + { + [XmlElement(ElementName = "tileset")] + public TiledMapTilesetContent Tileset { get; set; } + + //[XmlIgnore] + //public ExternalReference<TiledMapTilesetContent> TilesetReference { get; set; } + + [XmlElement(ElementName = "object")] + public TiledMapObjectContent Object { get; set; } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapOrientationContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapOrientationContent.cs new file mode 100644 index 0000000..af83824 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapOrientationContent.cs @@ -0,0 +1,12 @@ +using System.Xml.Serialization; + +namespace MonoGame.Extended.Tiled.Serialization +{ + public enum TiledMapOrientationContent : byte + { + [XmlEnum(Name = "orthogonal")] Orthogonal, + [XmlEnum(Name = "isometric")] Isometric, + [XmlEnum(Name = "staggered")] Staggered, + [XmlEnum(Name = "hexagonal")] Hexagonal + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPolygonContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPolygonContent.cs new file mode 100644 index 0000000..e4a3d5e --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPolygonContent.cs @@ -0,0 +1,10 @@ +using System.Xml.Serialization; + +namespace MonoGame.Extended.Tiled.Serialization +{ + public class TiledMapPolygonContent + { + [XmlAttribute(AttributeName = "points")] + public string Points { get; set; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPolylineContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPolylineContent.cs new file mode 100644 index 0000000..ba15f59 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPolylineContent.cs @@ -0,0 +1,10 @@ +using System.Xml.Serialization; + +namespace MonoGame.Extended.Tiled.Serialization +{ + public class TiledMapPolylineContent + { + [XmlAttribute(AttributeName = "points")] + public string Points { get; set; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPropertyContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPropertyContent.cs new file mode 100644 index 0000000..10cdfc1 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapPropertyContent.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace MonoGame.Extended.Tiled.Serialization +{ + public class TiledMapPropertyContent + { + [XmlAttribute(AttributeName = "name")] + public string Name { get; set; } + + [XmlAttribute(AttributeName = "value")] + public string ValueAttribute { get; set; } + + [XmlText] + public string ValueBody { get; set; } + + [XmlArray("properties")] + [XmlArrayItem("property")] + public List<TiledMapPropertyContent> Properties { get; set; } + + public string Value => ValueAttribute ?? ValueBody; + + public override string ToString() + { + return $"{Name}: {Value}"; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapStaggerAxisContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapStaggerAxisContent.cs new file mode 100644 index 0000000..7a073f5 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapStaggerAxisContent.cs @@ -0,0 +1,10 @@ +using System.Xml.Serialization; + +namespace MonoGame.Extended.Tiled.Serialization +{ + public enum TiledMapStaggerAxisContent : byte + { + [XmlEnum("x")]X, + [XmlEnum("y")]Y + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapStaggerIndexContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapStaggerIndexContent.cs new file mode 100644 index 0000000..834c3a6 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapStaggerIndexContent.cs @@ -0,0 +1,10 @@ +using System.Xml.Serialization; + +namespace MonoGame.Extended.Tiled.Serialization +{ + public enum TiledMapStaggerIndexContent : byte + { + [XmlEnum("even")]Even, + [XmlEnum("odd")]Odd + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileContent.cs new file mode 100644 index 0000000..fcd7a69 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileContent.cs @@ -0,0 +1,9 @@ +using System.Xml.Serialization; + +namespace MonoGame.Extended.Tiled.Serialization +{ + public struct TiledMapTileContent + { + [XmlAttribute(AttributeName = "gid")] public uint GlobalIdentifier; + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileDrawOrderContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileDrawOrderContent.cs new file mode 100644 index 0000000..4762afd --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileDrawOrderContent.cs @@ -0,0 +1,12 @@ +using System.Xml.Serialization; + +namespace MonoGame.Extended.Tiled.Serialization +{ + public enum TiledMapTileDrawOrderContent : byte + { + [XmlEnum(Name = "right-down")] RightDown, + [XmlEnum(Name = "right-up")] RightUp, + [XmlEnum(Name = "left-down")] LeftDown, + [XmlEnum(Name = "left-up")] LeftUp + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerContent.cs new file mode 100644 index 0000000..49f5ccf --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerContent.cs @@ -0,0 +1,30 @@ +using System.Xml.Serialization; + +namespace MonoGame.Extended.Tiled.Serialization +{ + public class TiledMapTileLayerContent : TiledMapLayerContent + { + public TiledMapTileLayerContent() + : base(TiledMapLayerType.TileLayer) + { + } + + [XmlAttribute(AttributeName = "x")] + public int X { get; set; } + + [XmlAttribute(AttributeName = "y")] + public int Y { get; set; } + + [XmlAttribute(AttributeName = "width")] + public int Width { get; set; } + + [XmlAttribute(AttributeName = "height")] + public int Height { get; set; } + + [XmlElement(ElementName = "data")] + public TiledMapTileLayerDataContent Data { get; set; } + + [XmlIgnore] + public TiledMapTile[] Tiles { get; set; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerDataChunkContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerDataChunkContent.cs new file mode 100644 index 0000000..0ec10d9 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerDataChunkContent.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace MonoGame.Extended.Tiled.Serialization +{ + public class TiledMapTileLayerDataChunkContent + { + [XmlAttribute(AttributeName = "x")] + public int X { get; set; } + + [XmlAttribute(AttributeName = "y")] + public int Y { get; set; } + + [XmlAttribute(AttributeName = "width")] + public int Width { get; set; } + + [XmlAttribute(AttributeName = "height")] + public int Height { get; set; } + + [XmlElement(ElementName = "tile")] + public List<TiledMapTileContent> Tiles { get; set; } + + [XmlText] + public string Value { get; set; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerDataContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerDataContent.cs new file mode 100644 index 0000000..c19a02e --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileLayerDataContent.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace MonoGame.Extended.Tiled.Serialization +{ + public class TiledMapTileLayerDataContent + { + public TiledMapTileLayerDataContent() + { + Tiles = new List<TiledMapTileContent>(); + } + + [XmlAttribute(AttributeName = "encoding")] + public string Encoding { get; set; } + + [XmlAttribute(AttributeName = "compression")] + public string Compression { get; set; } + + [XmlElement(ElementName = "tile")] + public List<TiledMapTileContent> Tiles { get; set; } + + [XmlElement(ElementName = "chunk")] + public List<TiledMapTileLayerDataChunkContent> Chunks { get; set; } + + [XmlText] + public string Value { get; set; } + + public override string ToString() + { + return $"{Encoding} {Compression}"; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileOffsetContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileOffsetContent.cs new file mode 100644 index 0000000..7d2af87 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTileOffsetContent.cs @@ -0,0 +1,17 @@ +using System.Xml.Serialization; + +namespace MonoGame.Extended.Tiled.Serialization +{ + [XmlRoot(ElementName = "tileoffset")] + public class TiledMapTileOffsetContent + { + [XmlAttribute(AttributeName = "x")] public int X; + + [XmlAttribute(AttributeName = "y")] public int Y; + + public override string ToString() + { + return $"{X}, {Y}"; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetContent.cs new file mode 100644 index 0000000..fa04c31 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetContent.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace MonoGame.Extended.Tiled.Serialization +{ + [XmlRoot(ElementName = "tileset")] + public class TiledMapTilesetContent + { + public TiledMapTilesetContent() + { + TileOffset = new TiledMapTileOffsetContent(); + Tiles = new List<TiledMapTilesetTileContent>(); + Properties = new List<TiledMapPropertyContent>(); + } + + [XmlAttribute(AttributeName = "firstgid")] + public int FirstGlobalIdentifier { get; set; } + + [XmlAttribute(AttributeName = "source")] + public string Source { get; set; } + + [XmlAttribute(AttributeName = "name")] + public string Name { get; set; } + + // Deprecated as of Tiled 1.9.0 (replaced by "class" attribute) + [XmlAttribute(DataType = "string", AttributeName = "type")] + public string Type { get; set; } + + [XmlAttribute(DataType = "string", AttributeName = "class")] + public string Class { get; set; } + + [XmlAttribute(AttributeName = "tilewidth")] + public int TileWidth { get; set; } + + [XmlAttribute(AttributeName = "tileheight")] + public int TileHeight { get; set; } + + [XmlAttribute(AttributeName = "spacing")] + public int Spacing { get; set; } + + [XmlAttribute(AttributeName = "margin")] + public int Margin { get; set; } + + [XmlAttribute(AttributeName = "columns")] + public int Columns { get; set; } + + [XmlAttribute(AttributeName = "tilecount")] + public int TileCount { get; set; } + + [XmlElement(ElementName = "tileoffset")] + public TiledMapTileOffsetContent TileOffset { get; set; } + + [XmlElement(ElementName = "grid")] + public TiledMapTilesetGridContent Grid { get; set; } + + [XmlElement(ElementName = "tile")] + public List<TiledMapTilesetTileContent> Tiles { get; set; } + + [XmlArray("properties")] + [XmlArrayItem("property")] + public List<TiledMapPropertyContent> Properties { get; set; } + + [XmlElement(ElementName = "image")] + public TiledMapImageContent Image { get; set; } + + public bool ContainsGlobalIdentifier(int globalIdentifier) + { + return globalIdentifier >= FirstGlobalIdentifier && globalIdentifier < FirstGlobalIdentifier + TileCount; + } + + public override string ToString() + { + return $"{Name}: {Image}"; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetGridContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetGridContent.cs new file mode 100644 index 0000000..a9071b8 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetGridContent.cs @@ -0,0 +1,16 @@ +using System.Xml.Serialization; + +namespace MonoGame.Extended.Tiled.Serialization +{ + public class TiledMapTilesetGridContent + { + [XmlAttribute(AttributeName = "orientation")] + public TiledMapOrientationContent Orientation { get; set; } + + [XmlAttribute(AttributeName = "width")] + public int Width { get; set; } + + [XmlAttribute(AttributeName = "height")] + public int Height { get; set; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetTileAnimationFrameContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetTileAnimationFrameContent.cs new file mode 100644 index 0000000..5a17137 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetTileAnimationFrameContent.cs @@ -0,0 +1,18 @@ +using System.Xml.Serialization; + +namespace MonoGame.Extended.Tiled.Serialization +{ + public class TiledMapTilesetTileAnimationFrameContent + { + [XmlAttribute(AttributeName = "tileid")] + public int TileIdentifier { get; set; } + + [XmlAttribute(AttributeName = "duration")] + public int Duration { get; set; } + + public override string ToString() + { + return $"TileID: {TileIdentifier}, Duration: {Duration}"; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetTileContent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetTileContent.cs new file mode 100644 index 0000000..2a82197 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/Serialization/TiledMapTilesetTileContent.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace MonoGame.Extended.Tiled.Serialization +{ + public class TiledMapTilesetTileContent + { + public TiledMapTilesetTileContent() + { + Properties = new List<TiledMapPropertyContent>(); + Type = string.Empty; + } + + [XmlAttribute(AttributeName = "id")] + public int LocalIdentifier { get; set; } + + [XmlAttribute(AttributeName = "type")] + public string Type { get; set; } + + [XmlElement(ElementName = "image")] + public TiledMapImageContent Image { get; set; } + + [XmlArray("objectgroup")] + [XmlArrayItem("object")] + public List<TiledMapObjectContent> Objects { get; set; } + + [XmlArray("animation")] + [XmlArrayItem("frame")] + public List<TiledMapTilesetTileAnimationFrameContent> Frames { get; set; } + + [XmlArray("properties")] + [XmlArrayItem("property")] + public List<TiledMapPropertyContent> Properties { get; set; } + + public override string ToString() + { + return LocalIdentifier.ToString(); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMap.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMap.cs new file mode 100644 index 0000000..b3486bd --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMap.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Tiled +{ + public sealed class TiledMap + { + private readonly List<TiledMapImageLayer> _imageLayers = new List<TiledMapImageLayer>(); + private readonly List<TiledMapLayer> _layers = new List<TiledMapLayer>(); + private readonly Dictionary<string, TiledMapLayer> _layersByName = new Dictionary<string, TiledMapLayer>(); + private readonly List<TiledMapObjectLayer> _objectLayers = new List<TiledMapObjectLayer>(); + private readonly List<TiledMapTileLayer> _tileLayers = new List<TiledMapTileLayer>(); + private readonly List<TiledMapTileset> _tilesets = new List<TiledMapTileset>(); + private readonly List<Tuple<TiledMapTileset, int>> _firstGlobalIdentifiers = new List<Tuple<TiledMapTileset, int>>(); + + public string Name { get; } + public string Type { get; } + public int Width { get; } + public int Height { get; } + public int TileWidth { get; } + public int TileHeight { get; } + public TiledMapTileDrawOrder RenderOrder { get; } + public TiledMapOrientation Orientation { get; } + public TiledMapProperties Properties { get; } + public ReadOnlyCollection<TiledMapTileset> Tilesets { get; } + public ReadOnlyCollection<TiledMapLayer> Layers { get; } + public ReadOnlyCollection<TiledMapImageLayer> ImageLayers { get; } + public ReadOnlyCollection<TiledMapTileLayer> TileLayers { get; } + public ReadOnlyCollection<TiledMapObjectLayer> ObjectLayers { get; } + + public Color? BackgroundColor { get; set; } + public int WidthInPixels => Width * TileWidth; + public int HeightInPixels => Height * TileHeight; + + private TiledMap() + { + Layers = new ReadOnlyCollection<TiledMapLayer>(_layers); + ImageLayers = new ReadOnlyCollection<TiledMapImageLayer>(_imageLayers); + TileLayers = new ReadOnlyCollection<TiledMapTileLayer>(_tileLayers); + ObjectLayers = new ReadOnlyCollection<TiledMapObjectLayer>(_objectLayers); + Tilesets = new ReadOnlyCollection<TiledMapTileset>(_tilesets); + Properties = new TiledMapProperties(); + } + + public TiledMap(string name, string type, int width, int height, int tileWidth, int tileHeight, TiledMapTileDrawOrder renderOrder, TiledMapOrientation orientation, Color? backgroundColor = null) + : this() + { + Name = name; + Type = type; + Width = width; + Height = height; + TileWidth = tileWidth; + TileHeight = tileHeight; + RenderOrder = renderOrder; + Orientation = orientation; + BackgroundColor = backgroundColor; + } + + public void AddTileset(TiledMapTileset tileset, int firstGlobalIdentifier) + { + _tilesets.Add(tileset); + _firstGlobalIdentifiers.Add(new Tuple<TiledMapTileset, int>(tileset, firstGlobalIdentifier)); + } + + public void AddLayer(TiledMapLayer layer) + => AddLayer(layer, true); + + private void AddLayer(TiledMapLayer layer, bool root) + { + if (root) _layers.Add(layer); + + if (_layersByName.ContainsKey(layer.Name)) + throw new ArgumentException($"The TiledMap '{Name}' contains two or more layers named '{layer.Name}'. Please ensure all layers have unique names."); + + _layersByName.Add(layer.Name, layer); + + switch(layer) + { + case TiledMapImageLayer imageLayer: + _imageLayers.Add(imageLayer); + break; + case TiledMapTileLayer tileLayer: + _tileLayers.Add(tileLayer); + break; + case TiledMapObjectLayer objectLayer: + _objectLayers.Add(objectLayer); + break; + case TiledMapGroupLayer groupLayer: + foreach (var subLayer in groupLayer.Layers) + AddLayer(subLayer, false); + break; + } + } + + public TiledMapLayer GetLayer(string layerName) + { + TiledMapLayer layer; + _layersByName.TryGetValue(layerName, out layer); + return layer; + } + + public T GetLayer<T>(string layerName) + where T : TiledMapLayer + { + return GetLayer(layerName) as T; + } + + public TiledMapTileset GetTilesetByTileGlobalIdentifier(int tileIdentifier) + { + foreach (var tileset in _firstGlobalIdentifiers) + { + if (tileIdentifier >= tileset.Item2 && tileIdentifier < tileset.Item2 + tileset.Item1.TileCount) + return tileset.Item1; + } + + return null; + } + + public int GetTilesetFirstGlobalIdentifier(TiledMapTileset tileset) + { + return _firstGlobalIdentifiers.FirstOrDefault(t => t.Item1 == tileset).Item2; + } + + private static int CountLayers(TiledMapLayer layer) + { + var value = 0; + if (layer is TiledMapGroupLayer groupLayer) + foreach (var subLayer in groupLayer.Layers) + value += CountLayers(subLayer); + else + value = 1; + + return value; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapEllipseObject.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapEllipseObject.cs new file mode 100644 index 0000000..9140d62 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapEllipseObject.cs @@ -0,0 +1,17 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Tiled +{ + public sealed class TiledMapEllipseObject : TiledMapObject + { + public TiledMapEllipseObject(int identifier, string name, Size2 size, Vector2 position, float rotation = 0, float opacity = 1, bool isVisible = true, string type = null) + : base(identifier, name, size, position, rotation, opacity, isVisible, type) + { + Radius = new Vector2(size.Width / 2.0f, size.Height / 2.0f); + Center = new Vector2(position.X + Radius.X, position.Y); + } + + public Vector2 Center { get; } + public Vector2 Radius { get; } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapGroupLayer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapGroupLayer.cs new file mode 100644 index 0000000..48d43d1 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapGroupLayer.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Tiled +{ + public class TiledMapGroupLayer : TiledMapLayer + { + public List<TiledMapLayer> Layers { get; } + public TiledMapGroupLayer(string name, string type, List<TiledMapLayer> layers, Vector2? offset = null, Vector2? parallaxFactor = null, float opacity = 1, bool isVisible = true) + : base(name, type, offset, parallaxFactor, opacity, isVisible) + { + Layers = layers; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapHelper.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapHelper.cs new file mode 100644 index 0000000..43edf67 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapHelper.cs @@ -0,0 +1,51 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Tiled +{ + public static class TiledMapHelper + { + // 4 vertices per tile + public const int VerticesPerTile = 4; + // 2 triangles per tile (mesh), with each triangle indexing 3 out of 4 vertices, so 6 vertices + public const int IndicesPerTile = 6; + // by using ushort type for indices we are limited to indexing vertices from 0 to 65535 + // this limits us on how many vertices can fit inside a single vertex buffer (65536 vertices) + public const int MaximumVerticesPerModel = ushort.MaxValue + 1; + // and thus, we know how many tiles we can fit inside a vertex or index buffer (16384 tiles) + public const int MaximumTilesPerGeometryContent = MaximumVerticesPerModel / VerticesPerTile; + // and thus, we also know the maximum number of indices we can fit inside a single index buffer (98304 indices) + public const int MaximumIndicesPerModel = MaximumTilesPerGeometryContent * IndicesPerTile; + // these optimal maximum numbers of course are not considering texture bindings which would practically lower the actual number of tiles per vertex / index buffer + // thus, the reason why it is a good to have ONE giant tileset (at least per layer) + + internal static Rectangle GetTileSourceRectangle(int localTileIdentifier, int tileWidth, int tileHeight, int columns, int margin, int spacing) + { + var x = margin + localTileIdentifier % columns * (tileWidth + spacing); + var y = margin + localTileIdentifier / columns * (tileHeight + spacing); + return new Rectangle(x, y, tileWidth, tileHeight); + } + + internal static Point2 GetOrthogonalPosition(int tileX, int tileY, int tileWidth, int tileHeight) + { + var x = tileX * tileWidth; + var y = tileY * tileHeight; + return new Vector2(x, y); + } + + internal static Point2 GetIsometricPosition(int tileX, int tileY, int tileWidth, int tileHeight) + { + // You can think of an isometric Tiled map as a regular orthogonal map that is rotated -45 degrees + // i.e.: the origin (0, 0) is the top tile of the diamond grid; + // (mapWidth, 0) is the far right tile of the diamond grid + // (0, mapHeight) is the far left tile of the diamond grid + // (mapWidth, mapHeight) is the bottom tile of the diamond grid + + var halfTileWidth = tileWidth * 0.5f; + var halfTileHeight = tileHeight * 0.5f; + // -1 because we want the top the tile-diamond (top-center) to be the origin in tile space + var x = (tileX - tileY - 1) * halfTileWidth; + var y = (tileX + tileY) * halfTileHeight; + return new Vector2(x, y); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapImageLayer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapImageLayer.cs new file mode 100644 index 0000000..33bb700 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapImageLayer.cs @@ -0,0 +1,18 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Tiled +{ + public class TiledMapImageLayer : TiledMapLayer, IMovable + { + public TiledMapImageLayer(string name, string type, Texture2D image, Vector2? position = null, Vector2? offset = null, Vector2? parallaxFactor = null, float opacity = 1.0f, bool isVisible = true) + : base(name, type, offset, parallaxFactor, opacity, isVisible) + { + Image = image; + Position = position ?? Vector2.Zero; + } + + public Texture2D Image { get; } + public Vector2 Position { get; set; } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapLayer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapLayer.cs new file mode 100644 index 0000000..6226dd1 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapLayer.cs @@ -0,0 +1,26 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Tiled +{ + public abstract class TiledMapLayer + { + public string Name { get; } + public string Type { get; } + public bool IsVisible { get; set; } + public float Opacity { get; set; } + public Vector2 Offset { get; set; } + public Vector2 ParallaxFactor { get; set; } + public TiledMapProperties Properties { get; } + + protected TiledMapLayer(string name, string type, Vector2? offset = null, Vector2? parallaxFactor = null, float opacity = 1.0f, bool isVisible = true) + { + Name = name; + Type = type; + Offset = offset ?? Vector2.Zero; + ParallaxFactor = parallaxFactor ?? Vector2.One; + Opacity = opacity; + IsVisible = isVisible; + Properties = new TiledMapProperties(); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapLayerType.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapLayerType.cs new file mode 100644 index 0000000..8af5a2e --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapLayerType.cs @@ -0,0 +1,10 @@ +namespace MonoGame.Extended.Tiled +{ + public enum TiledMapLayerType : byte + { + ImageLayer = 0, + TileLayer = 1, + ObjectLayer = 2, + GroupLayer = 3 + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObject.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObject.cs new file mode 100644 index 0000000..1e25084 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObject.cs @@ -0,0 +1,35 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Tiled +{ + public abstract class TiledMapObject + { + protected TiledMapObject(int identifier, string name, Size2 size, Vector2 position, float rotation = 0, float opacity = 1, bool isVisible = true, string type = null) + { + Identifier = identifier; + Name = name; + IsVisible = isVisible; + Rotation = rotation; + Position = position; + Size = size; + Opacity = opacity; + Type = type; + Properties = new TiledMapProperties(); + } + + public int Identifier { get; } + public string Name { get; set; } + public string Type { get; set; } + public bool IsVisible { get; set; } + public float Opacity { get; set; } + public float Rotation { get; set; } + public Vector2 Position { get; } + public Size2 Size { get; set; } + public TiledMapProperties Properties { get; } + + public override string ToString() + { + return $"{Identifier}"; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectDrawOrder.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectDrawOrder.cs new file mode 100644 index 0000000..1668f66 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectDrawOrder.cs @@ -0,0 +1,8 @@ +namespace MonoGame.Extended.Tiled +{ + public enum TiledMapObjectDrawOrder : byte + { + TopDown, + Index, + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectLayer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectLayer.cs new file mode 100644 index 0000000..b39b8d7 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectLayer.cs @@ -0,0 +1,20 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Tiled +{ + public class TiledMapObjectLayer : TiledMapLayer + { + public TiledMapObjectLayer(string name, string type, TiledMapObject[] objects, Color? color = null, TiledMapObjectDrawOrder drawOrder = TiledMapObjectDrawOrder.TopDown, + Vector2? offset = null, Vector2? parallaxFactor = null, float opacity = 1.0f, bool isVisible = true) + : base(name, type, offset, parallaxFactor, opacity, isVisible) + { + Color = color; + DrawOrder = drawOrder; + Objects = objects; + } + + public Color? Color { get; } + public TiledMapObjectDrawOrder DrawOrder { get; } + public TiledMapObject[] Objects { get; } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectType.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectType.cs new file mode 100644 index 0000000..305500a --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapObjectType.cs @@ -0,0 +1,11 @@ +namespace MonoGame.Extended.Tiled +{ + public enum TiledMapObjectType : byte + { + Rectangle, + Ellipse, + Polygon, + Polyline, + Tile + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapOrientation.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapOrientation.cs new file mode 100644 index 0000000..798c1c8 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapOrientation.cs @@ -0,0 +1,9 @@ +namespace MonoGame.Extended.Tiled +{ + public enum TiledMapOrientation + { + Orthogonal, + Isometric, + Staggered + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPolygonObject.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPolygonObject.cs new file mode 100644 index 0000000..2df343f --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPolygonObject.cs @@ -0,0 +1,15 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Tiled +{ + public sealed class TiledMapPolygonObject : TiledMapObject + { + public TiledMapPolygonObject(int identifier, string name, Point2[] points, Size2 size, Vector2 position, float rotation = 0, float opacity = 1, bool isVisible = true, string type = null) + : base(identifier, name, size, position, rotation, opacity, isVisible, type) + { + Points = points; + } + + public Point2[] Points { get; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPolylineObject.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPolylineObject.cs new file mode 100644 index 0000000..de50dbc --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPolylineObject.cs @@ -0,0 +1,15 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Tiled +{ + public sealed class TiledMapPolylineObject : TiledMapObject + { + public TiledMapPolylineObject(int identifier, string name, Point2[] points, Size2 size, Vector2 position, float rotation = 0, float opacity = 1, bool isVisible = true, string type = null) + : base(identifier, name, size, position, rotation, opacity, isVisible, type) + { + Points = points; + } + + public Point2[] Points { get; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapProperties.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapProperties.cs new file mode 100644 index 0000000..651ae5d --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapProperties.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace MonoGame.Extended.Tiled +{ + public class TiledMapProperties : Dictionary<string, TiledMapPropertyValue> + { + public bool TryGetValue(string key, out string value) + { + bool result = TryGetValue(key, out TiledMapPropertyValue tmpVal); + value = result ? null : tmpVal.Value; + return result; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPropertyValue.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPropertyValue.cs new file mode 100644 index 0000000..d7a0893 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapPropertyValue.cs @@ -0,0 +1,32 @@ +namespace MonoGame.Extended.Tiled; + +public class TiledMapPropertyValue +{ + public string Value { get; } + + public TiledMapProperties Properties; + + public TiledMapPropertyValue() + { + Value = string.Empty; + Properties = new(); + } + + public TiledMapPropertyValue(string value) + { + Value = value; + Properties = new(); + } + + public TiledMapPropertyValue(TiledMapProperties properties) + { + Value = string.Empty; + Properties = properties; + } + + public override string ToString() => Value; + + //public static implicit operator TiledMapPropertyValue(string value) => new(value); + public static implicit operator string(TiledMapPropertyValue value) => value.Value; + public static implicit operator TiledMapProperties(TiledMapPropertyValue value) => value.Properties; +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapReader.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapReader.cs new file mode 100644 index 0000000..bd49655 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapReader.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using MonoGame.Extended.Content; + +namespace MonoGame.Extended.Tiled +{ + public class TiledMapReader : ContentTypeReader<TiledMap> + { + protected override TiledMap Read(ContentReader reader, TiledMap existingInstance) + { + if (existingInstance != null) + return existingInstance; + + var map = ReadTiledMap(reader); + reader.ReadTiledMapProperties(map.Properties); + ReadTilesets(reader, map); + ReadLayers(reader, map); + return map; + } + + private static TiledMap ReadTiledMap(ContentReader reader) + { + var name = reader.AssetName; + var type = reader.ReadString(); + var width = reader.ReadInt32(); + var height = reader.ReadInt32(); + var tileWidth = reader.ReadInt32(); + var tileHeight = reader.ReadInt32(); + var backgroundColor = reader.ReadColor(); + var renderOrder = (TiledMapTileDrawOrder)reader.ReadByte(); + var orientation = (TiledMapOrientation)reader.ReadByte(); + + return new TiledMap(name, type, width, height, tileWidth, tileHeight, renderOrder, orientation, backgroundColor); + } + + private static void ReadTilesets(ContentReader reader, TiledMap map) + { + var tilesetCount = reader.ReadInt32(); + + for (var i = 0; i < tilesetCount; i++) + { + var firstGlobalIdentifier = reader.ReadInt32(); + var tileset = ReadTileset(reader, map); + map.AddTileset(tileset, firstGlobalIdentifier); + } + } + + private static TiledMapTileset ReadTileset(ContentReader reader, TiledMap map) + { + var external = reader.ReadBoolean(); + var tileset = external ? reader.ReadExternalReference<TiledMapTileset>() : TiledMapTilesetReader.ReadTileset(reader); + + return tileset; + } + + private static void ReadLayers(ContentReader reader, TiledMap map) + { + foreach (var layer in ReadGroup(reader, map)) + map.AddLayer(layer); + } + private static List<TiledMapLayer> ReadGroup(ContentReader reader, TiledMap map) + { + var layerCount = reader.ReadInt32(); + var value = new List<TiledMapLayer>(layerCount); + + for (var i = 0; i < layerCount; i++) + value.Add(ReadLayer(reader, map)); + + return value; + } + + private static TiledMapLayer ReadLayer(ContentReader reader, TiledMap map) + { + var layerType = (TiledMapLayerType)reader.ReadByte(); + var name = reader.ReadString(); + var type = reader.ReadString(); + var isVisible = reader.ReadBoolean(); + var opacity = reader.ReadSingle(); + var offsetX = reader.ReadSingle(); + var offsetY = reader.ReadSingle(); + var offset = new Vector2(offsetX, offsetY); + var parallaxX = reader.ReadSingle(); + var parallaxY = reader.ReadSingle(); + var parallaxFactor = new Vector2(parallaxX, parallaxY); + var properties = new TiledMapProperties(); + + reader.ReadTiledMapProperties(properties); + + TiledMapLayer layer; + + switch (layerType) + { + case TiledMapLayerType.ImageLayer: + layer = ReadImageLayer(reader, name, type, offset, parallaxFactor, opacity, isVisible); + break; + case TiledMapLayerType.TileLayer: + layer = ReadTileLayer(reader, name, type, offset, parallaxFactor, opacity, isVisible, map); + break; + case TiledMapLayerType.ObjectLayer: + layer = ReadObjectLayer(reader, name, type, offset, parallaxFactor, opacity, isVisible, map); + break; + case TiledMapLayerType.GroupLayer: + layer = new TiledMapGroupLayer(name, type, ReadGroup(reader, map), offset, parallaxFactor, opacity, isVisible); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + foreach (var property in properties) + layer.Properties.Add(property.Key, property.Value); + + return layer; + } + + private static TiledMapLayer ReadObjectLayer(ContentReader reader, string name, string type, Vector2 offset, Vector2 parallaxFactor, float opacity, bool isVisible, TiledMap map) + { + var color = reader.ReadColor(); + var drawOrder = (TiledMapObjectDrawOrder)reader.ReadByte(); + var objectCount = reader.ReadInt32(); + var objects = new TiledMapObject[objectCount]; + + for (var i = 0; i < objectCount; i++) + objects[i] = ReadTiledMapObject(reader, map); + + return new TiledMapObjectLayer(name, type, objects, color, drawOrder, offset, parallaxFactor, opacity, isVisible); + } + + private static TiledMapObject ReadTiledMapObject(ContentReader reader, TiledMap map) + { + var objectType = (TiledMapObjectType)reader.ReadByte(); + var identifier = reader.ReadInt32(); + var name = reader.ReadString(); + var type = reader.ReadString(); + var position = new Vector2(reader.ReadSingle(), reader.ReadSingle()); + var width = reader.ReadSingle(); + var height = reader.ReadSingle(); + var size = new Size2(width, height); + var rotation = reader.ReadSingle(); + var isVisible = reader.ReadBoolean(); + var properties = new TiledMapProperties(); + const float opacity = 1.0f; + + reader.ReadTiledMapProperties(properties); + + TiledMapObject mapObject; + + switch (objectType) + { + case TiledMapObjectType.Rectangle: + mapObject = new TiledMapRectangleObject(identifier, name, size, position, rotation, opacity, isVisible, type); + break; + case TiledMapObjectType.Tile: + var globalTileIdentifierWithFlags = reader.ReadUInt32(); + var tile = new TiledMapTile(globalTileIdentifierWithFlags, (ushort)position.X, (ushort)position.Y); + var tileset = map.GetTilesetByTileGlobalIdentifier(tile.GlobalIdentifier); + var localTileIdentifier = tile.GlobalIdentifier - map.GetTilesetFirstGlobalIdentifier(tileset); + var tilesetTile = tileset.Tiles.FirstOrDefault(x => x.LocalTileIdentifier == localTileIdentifier); + mapObject = new TiledMapTileObject(identifier, name, tileset, tilesetTile, size, position, rotation, opacity, isVisible, type); + break; + case TiledMapObjectType.Ellipse: + mapObject = new TiledMapEllipseObject(identifier, name, size, position, rotation, opacity, isVisible); + break; + case TiledMapObjectType.Polygon: + mapObject = new TiledMapPolygonObject(identifier, name, ReadPoints(reader), size, position, rotation, opacity, isVisible, type); + break; + case TiledMapObjectType.Polyline: + mapObject = new TiledMapPolylineObject(identifier, name, ReadPoints(reader), size, position, rotation, opacity, isVisible, type); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + foreach (var property in properties) + mapObject.Properties.Add(property.Key, property.Value); + + return mapObject; + } + + private static Point2[] ReadPoints(ContentReader reader) + { + var pointCount = reader.ReadInt32(); + var points = new Point2[pointCount]; + + for (var i = 0; i < pointCount; i++) + { + var x = reader.ReadSingle(); + var y = reader.ReadSingle(); + points[i] = new Point2(x, y); + } + + return points; + } + + private static TiledMapImageLayer ReadImageLayer(ContentReader reader, string name, string type, Vector2 offset, Vector2 parallaxFactor, float opacity, bool isVisible) + { + var texture = reader.ReadExternalReference<Texture2D>(); + var x = reader.ReadSingle(); + var y = reader.ReadSingle(); + var position = new Vector2(x, y); + return new TiledMapImageLayer(name, type, texture, position, offset, parallaxFactor, opacity, isVisible); + } + + private static TiledMapTileLayer ReadTileLayer(ContentReader reader, string name, string type, Vector2 offset, Vector2 parallaxFactor, float opacity, bool isVisible, TiledMap map) + { + var width = reader.ReadInt32(); + var height = reader.ReadInt32(); + var tileWidth = map.TileWidth; + var tileHeight = map.TileHeight; + + var tileCount = reader.ReadInt32(); + var layer = new TiledMapTileLayer(name, type, width, height, tileWidth, tileHeight, offset, parallaxFactor, opacity, isVisible); + + for (var i = 0; i < tileCount; i++) + { + var globalTileIdentifierWithFlags = reader.ReadUInt32(); + var x = reader.ReadUInt16(); + var y = reader.ReadUInt16(); + + layer.Tiles[x + y * width] = new TiledMapTile(globalTileIdentifierWithFlags, x, y); + } + + return layer; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapRectangleObject.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapRectangleObject.cs new file mode 100644 index 0000000..de4c7ec --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapRectangleObject.cs @@ -0,0 +1,12 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Tiled +{ + public sealed class TiledMapRectangleObject : TiledMapObject + { + public TiledMapRectangleObject(int identifier, string name, Size2 size, Vector2 position, float rotation = 0, float opacity = 1, bool isVisible = true, string type = null) + : base(identifier, name, size, position, rotation, opacity, isVisible, type) + { + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTile.cs new file mode 100644 index 0000000..d3147a4 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTile.cs @@ -0,0 +1,28 @@ +namespace MonoGame.Extended.Tiled +{ + public struct TiledMapTile + { + public readonly ushort X; + public readonly ushort Y; + public readonly uint GlobalTileIdentifierWithFlags; + + public int GlobalIdentifier => (int)(GlobalTileIdentifierWithFlags & ~(uint)TiledMapTileFlipFlags.All); + public bool IsFlippedHorizontally => (GlobalTileIdentifierWithFlags & (uint)TiledMapTileFlipFlags.FlipHorizontally) != 0; + public bool IsFlippedVertically => (GlobalTileIdentifierWithFlags & (uint)TiledMapTileFlipFlags.FlipVertically) != 0; + public bool IsFlippedDiagonally => (GlobalTileIdentifierWithFlags & (uint)TiledMapTileFlipFlags.FlipDiagonally) != 0; + public bool IsBlank => GlobalIdentifier == 0; + public TiledMapTileFlipFlags Flags => (TiledMapTileFlipFlags)(GlobalTileIdentifierWithFlags & (uint)TiledMapTileFlipFlags.All); + + public TiledMapTile(uint globalTileIdentifierWithFlags, ushort x, ushort y) + { + GlobalTileIdentifierWithFlags = globalTileIdentifierWithFlags; + X = x; + Y = y; + } + + public override string ToString() + { + return $"GlobalIdentifier: {GlobalIdentifier}, Flags: {Flags}"; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileDrawOrder.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileDrawOrder.cs new file mode 100644 index 0000000..a40b596 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileDrawOrder.cs @@ -0,0 +1,10 @@ +namespace MonoGame.Extended.Tiled +{ + public enum TiledMapTileDrawOrder : byte + { + RightDown, + RightUp, + LeftDown, + LeftUp + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileFlipFlags.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileFlipFlags.cs new file mode 100644 index 0000000..f883bad --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileFlipFlags.cs @@ -0,0 +1,14 @@ +using System; + +namespace MonoGame.Extended.Tiled +{ + [Flags] + public enum TiledMapTileFlipFlags : uint + { + None = 0, + FlipDiagonally = 0x20000000, + FlipVertically = 0x40000000, + FlipHorizontally = 0x80000000, + All = FlipDiagonally | FlipVertically | FlipHorizontally + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileLayer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileLayer.cs new file mode 100644 index 0000000..5d07f06 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileLayer.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Tiled +{ + public class TiledMapTileLayer : TiledMapLayer + { + public TiledMapTileLayer(string name, string type, int width, int height, int tileWidth, int tileHeight, Vector2? offset = null, + Vector2? parallaxFactor = null, float opacity = 1, bool isVisible = true) + : base(name, type, offset, parallaxFactor, opacity, isVisible) + { + Width = width; + Height = height; + TileWidth = tileWidth; + TileHeight = tileHeight; + Tiles = new TiledMapTile[Width * Height]; + } + + public int Width { get; } + public int Height { get; } + public int TileWidth { get; } + public int TileHeight { get; } + public TiledMapTile[] Tiles { get; } + + public int GetTileIndex(ushort x, ushort y) + { + return x + y * Width; + } + + public bool TryGetTile(ushort x, ushort y, out TiledMapTile? tile) + { + if (x >= Width) + { + tile = null; + return false; + } + var index = GetTileIndex(x, y); + + if (index < 0 || index >= Tiles.Length) + { + tile = null; + return false; + } + + tile = Tiles[index]; + return true; + } + + public TiledMapTile GetTile(ushort x, ushort y) + { + var index = GetTileIndex(x, y); + return Tiles[index]; + } + + public void SetTile(ushort x, ushort y, uint globalIdentifier) + { + var index = GetTileIndex(x, y); + Tiles[index] = new TiledMapTile(globalIdentifier, x, y); + } + + public void RemoveTile(ushort x, ushort y) + { + SetTile(x, y, 0); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileObject.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileObject.cs new file mode 100644 index 0000000..b4e8679 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileObject.cs @@ -0,0 +1,18 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Tiled +{ + public sealed class TiledMapTileObject : TiledMapObject + { + public TiledMapTileObject(int identifier, string name, TiledMapTileset tileset, TiledMapTilesetTile tile, + Size2 size, Vector2 position, float rotation = 0, float opacity = 1, bool isVisible = true, string type = null) + : base(identifier, name, size, position, rotation, opacity, isVisible, type) + { + Tileset = tileset; + Tile = tile; + } + + public TiledMapTilesetTile Tile { get; } + public TiledMapTileset Tileset { get; } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileset.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileset.cs new file mode 100644 index 0000000..ed14e15 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTileset.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using MonoGame.Extended.TextureAtlases; + +namespace MonoGame.Extended.Tiled +{ + public interface ITileset + { + int ActualWidth { get; } + int Columns { get; } + int ActualHeight { get; } + int Rows { get; } + int TileWidth { get; } + int TileHeight { get; } + Texture2D Texture { get; } + TextureRegion2D GetRegion(int column, int row); + } + + public class TiledMapTileset : ITileset + { + public TiledMapTileset(Texture2D texture, string type, int tileWidth, int tileHeight, int tileCount, int spacing, int margin, int columns) + { + Texture = texture; + Type = type; + TileWidth = tileWidth; + TileHeight = tileHeight; + TileCount = tileCount; + Spacing = spacing; + Margin = margin; + Columns = columns; + Properties = new TiledMapProperties(); + Tiles = new List<TiledMapTilesetTile>(); + } + + public string Name => Texture.Name; + public Texture2D Texture { get; } + + public TextureRegion2D GetRegion(int column, int row) + { + var x = Margin + column * (TileWidth + Spacing); + var y = Margin + row * (TileHeight + Spacing); + return new TextureRegion2D(Texture, x, y, TileWidth, TileHeight); + } + + public string Type { get; } + public int TileWidth { get; } + public int TileHeight { get; } + public int Spacing { get; } + public int Margin { get; } + public int TileCount { get; } + public int Columns { get; } + public List<TiledMapTilesetTile> Tiles { get; } + public TiledMapProperties Properties { get; } + + public int Rows => (int)Math.Ceiling((double) TileCount / Columns); + public int ActualWidth => TileWidth * Columns; + public int ActualHeight => TileHeight * Rows; + + public Rectangle GetTileRegion(int localTileIdentifier) + { + return Texture is not null + ? TiledMapHelper.GetTileSourceRectangle(localTileIdentifier, TileWidth, TileHeight, Columns, Margin, + Spacing) + : Tiles.FirstOrDefault(x => x.LocalTileIdentifier == localTileIdentifier).Texture.Bounds; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetAnimatedTile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetAnimatedTile.cs new file mode 100644 index 0000000..1bbb770 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetAnimatedTile.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.ObjectModel; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Tiled +{ + public class TiledMapTilesetAnimatedTile : TiledMapTilesetTile + { + private TimeSpan _timer = TimeSpan.Zero; + private int _frameIndex; + + public ReadOnlyCollection<TiledMapTilesetTileAnimationFrame> AnimationFrames { get; } + public TiledMapTilesetTileAnimationFrame CurrentAnimationFrame { get; private set; } + + public TiledMapTilesetAnimatedTile(int localTileIdentifier, + TiledMapTilesetTileAnimationFrame[] frames, string type = null, TiledMapObject[] objects = null, Texture2D texture = null) + : base(localTileIdentifier, type, objects, texture) + { + if (frames.Length == 0) throw new InvalidOperationException("There must be at least one tileset animation frame"); + + AnimationFrames = new ReadOnlyCollection<TiledMapTilesetTileAnimationFrame>(frames); + CurrentAnimationFrame = AnimationFrames[0]; + } + + public void CreateTextureRotations(TiledMapTileset tileset, TiledMapTileFlipFlags flipFlags) + { + for (int i = 0; i < AnimationFrames.Count; i++) + { + AnimationFrames[i].CreateTextureRotations(tileset, flipFlags); + } + } + + public void Update(GameTime gameTime) + { + _timer += gameTime.ElapsedGameTime; + + if (_timer <= CurrentAnimationFrame.Duration) + return; + + _timer -= CurrentAnimationFrame.Duration; + _frameIndex = (_frameIndex + 1) % AnimationFrames.Count; + CurrentAnimationFrame = AnimationFrames[_frameIndex]; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetReader.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetReader.cs new file mode 100644 index 0000000..646f835 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetReader.cs @@ -0,0 +1,150 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using System; + +namespace MonoGame.Extended.Tiled +{ + public class TiledMapTilesetReader : ContentTypeReader<TiledMapTileset> + { + protected override TiledMapTileset Read(ContentReader reader, TiledMapTileset existingInstance) + { + if (existingInstance != null) + return existingInstance; + + return ReadTileset(reader); + } + + public static TiledMapTileset ReadTileset(ContentReader reader) + { + var texture = reader.ReadExternalReference<Texture2D>(); + var @class = reader.ReadString(); + var tileWidth = reader.ReadInt32(); + var tileHeight = reader.ReadInt32(); + var tileCount = reader.ReadInt32(); + var spacing = reader.ReadInt32(); + var margin = reader.ReadInt32(); + var columns = reader.ReadInt32(); + var explicitTileCount = reader.ReadInt32(); + + var tileset = new TiledMapTileset(texture, @class, tileWidth, tileHeight, tileCount, spacing, margin, columns); + + for (var tileIndex = 0; tileIndex < explicitTileCount; tileIndex++) + ReadTile(reader, tileset); + + reader.ReadTiledMapProperties(tileset.Properties); + return tileset; + } + + private static void ReadTile(ContentReader reader, TiledMapTileset tileset) + { + var texture = reader.ReadExternalReference<Texture2D>(); + + var localTileIdentifier = reader.ReadInt32(); + var type = reader.ReadString(); + var animationFramesCount = reader.ReadInt32(); + var objectCount = reader.ReadInt32(); + var objects = new TiledMapObject[objectCount]; + + for (var i = 0; i < objectCount; i++) + objects[i] = ReadTiledMapObject(reader, tileset); + + var tilesetTile = animationFramesCount <= 0 + ? new TiledMapTilesetTile(localTileIdentifier, type, objects, texture) + : new TiledMapTilesetAnimatedTile(localTileIdentifier, + ReadTiledMapTilesetAnimationFrames(reader, tileset, animationFramesCount), type, objects, texture); + + reader.ReadTiledMapProperties(tilesetTile.Properties); + tileset.Tiles.Add(tilesetTile); + } + + private static TiledMapTilesetTileAnimationFrame[] ReadTiledMapTilesetAnimationFrames(ContentReader reader, TiledMapTileset tileset, int animationFramesCount) + { + var animationFrames = new TiledMapTilesetTileAnimationFrame[animationFramesCount]; + + for (var i = 0; i < animationFramesCount; i++) + { + var localTileIdentifierForFrame = reader.ReadInt32(); + var frameDurationInMilliseconds = reader.ReadInt32(); + var tileSetTileFrame = new TiledMapTilesetTileAnimationFrame(tileset, localTileIdentifierForFrame, frameDurationInMilliseconds); + animationFrames[i] = tileSetTileFrame; + } + + return animationFrames; + } + + private static TiledMapTilesetTile ReadTiledMapTilesetTile(ContentReader reader, TiledMapTileset tileset, Func<TiledMapObject[], TiledMapTilesetTile> createTile) + { + var texture = reader.ReadExternalReference<Texture2D>(); + var objectCount = reader.ReadInt32(); + var objects = new TiledMapObject[objectCount]; + + for (var i = 0; i < objectCount; i++) + objects[i] = ReadTiledMapObject(reader, tileset); + + return createTile(objects); + } + + private static TiledMapObject ReadTiledMapObject(ContentReader reader, TiledMapTileset tileset) + { + var objectType = (TiledMapObjectType)reader.ReadByte(); + var identifier = reader.ReadInt32(); + var name = reader.ReadString(); + var type = reader.ReadString(); + var position = new Vector2(reader.ReadSingle(), reader.ReadSingle()); + var width = reader.ReadSingle(); + var height = reader.ReadSingle(); + var size = new Size2(width, height); + var rotation = reader.ReadSingle(); + var isVisible = reader.ReadBoolean(); + var properties = new TiledMapProperties(); + const float opacity = 1.0f; + + reader.ReadTiledMapProperties(properties); + + TiledMapObject mapObject; + + switch (objectType) + { + case TiledMapObjectType.Rectangle: + mapObject = new TiledMapRectangleObject(identifier, name, size, position, rotation, opacity, isVisible, type); + break; + case TiledMapObjectType.Tile: + reader.ReadUInt32(); // Tile objects within TiledMapTilesetTiles currently ignore the gid and behave like rectangle objects. + mapObject = new TiledMapRectangleObject(identifier, name, size, position, rotation, opacity, isVisible, type); + break; + case TiledMapObjectType.Ellipse: + mapObject = new TiledMapEllipseObject(identifier, name, size, position, rotation, opacity, isVisible); + break; + case TiledMapObjectType.Polygon: + mapObject = new TiledMapPolygonObject(identifier, name, ReadPoints(reader), size, position, rotation, opacity, isVisible, type); + break; + case TiledMapObjectType.Polyline: + mapObject = new TiledMapPolylineObject(identifier, name, ReadPoints(reader), size, position, rotation, opacity, isVisible, type); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + foreach (var property in properties) + mapObject.Properties.Add(property.Key, property.Value); + + return mapObject; + } + + private static Point2[] ReadPoints(ContentReader reader) + { + var pointCount = reader.ReadInt32(); + var points = new Point2[pointCount]; + + for (var i = 0; i < pointCount; i++) + { + var x = reader.ReadSingle(); + var y = reader.ReadSingle(); + points[i] = new Point2(x, y); + } + + return points; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetTile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetTile.cs new file mode 100644 index 0000000..e378aa3 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetTile.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Tiled +{ + [DebuggerDisplay("{LocalTileIdentifier}: Type: {Type}, Properties: {Properties.Count}, Objects: {Objects.Count}")] + public class TiledMapTilesetTile + { + // For remove libraries + public TiledMapTilesetTile(int localTileIdentifier, string type = null, + TiledMapObject[] objects = null) + { + LocalTileIdentifier = localTileIdentifier; + Type = type; + Objects = objects != null ? new List<TiledMapObject>(objects) : new List<TiledMapObject>(); + Properties = new TiledMapProperties(); + } + + public TiledMapTilesetTile(int localTileIdentifier, string type = null, + TiledMapObject[] objects = null, Texture2D texture = null) + { + Texture = texture; + LocalTileIdentifier = localTileIdentifier; + Type = type; + Objects = objects != null ? new List<TiledMapObject>(objects) : new List<TiledMapObject>(); + Properties = new TiledMapProperties(); + } + + public int LocalTileIdentifier { get; } + public string Type { get; } + public TiledMapProperties Properties { get; } + public List<TiledMapObject> Objects { get; } + public Texture2D Texture { get; } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetTileAnimationFrame.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetTileAnimationFrame.cs new file mode 100644 index 0000000..9dbca80 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tiled/TiledMapTilesetTileAnimationFrame.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Tiled +{ + public struct TiledMapTilesetTileAnimationFrame + { + public readonly int LocalTileIdentifier; + public readonly TimeSpan Duration; + public readonly Vector2[] TextureCoordinates; + private readonly Dictionary<TiledMapTileFlipFlags, Vector2[]> _flipDictionary = new Dictionary<TiledMapTileFlipFlags, Vector2[]>(); + + internal TiledMapTilesetTileAnimationFrame(TiledMapTileset tileset, int localTileIdentifier, int durationInMilliseconds) + { + LocalTileIdentifier = localTileIdentifier; + Duration = new TimeSpan(0, 0, 0, 0, durationInMilliseconds); + TextureCoordinates = new Vector2[4]; + CreateTextureCoordinates(tileset); + } + + public Vector2[] GetTextureCoordinates(TiledMapTileFlipFlags flipFlags) + { + if (!_flipDictionary.TryGetValue(flipFlags, out Vector2[] flippedTextureCoordiantes)) + { + return TextureCoordinates; + } + else + { + return flippedTextureCoordiantes; + } + } + + public void CreateTextureRotations(TiledMapTileset tileset, TiledMapTileFlipFlags flipFlags) + { + if (!_flipDictionary.ContainsKey(flipFlags)) + { + if (flipFlags == TiledMapTileFlipFlags.None) + { + _flipDictionary.Add(flipFlags, TextureCoordinates); + } + else + { + _flipDictionary.Add(flipFlags, TransformTextureCoordinates(tileset, flipFlags)); + } + } + } + + public Vector2[] TransformTextureCoordinates(TiledMapTileset tileset, TiledMapTileFlipFlags flipFlags) + { + var sourceRectangle = tileset.GetTileRegion(LocalTileIdentifier); + var texture = tileset.Texture; + var texelLeft = (float)sourceRectangle.X / texture.Width; + var texelTop = (float)sourceRectangle.Y / texture.Height; + var texelRight = (sourceRectangle.X + sourceRectangle.Width) / (float)texture.Width; + var texelBottom = (sourceRectangle.Y + sourceRectangle.Height) / (float)texture.Height; + + var flipDiagonally = (flipFlags & TiledMapTileFlipFlags.FlipDiagonally) != 0; + var flipHorizontally = (flipFlags & TiledMapTileFlipFlags.FlipHorizontally) != 0; + var flipVertically = (flipFlags & TiledMapTileFlipFlags.FlipVertically) != 0; + var transform = new Vector2[4]; + + transform[0].X = texelLeft; + transform[0].Y = texelTop; + + transform[1].X = texelRight; + transform[1].Y = texelTop; + + transform[2].X = texelLeft; + transform[2].Y = texelBottom; + + transform[3].X = texelRight; + transform[3].Y = texelBottom; + + if (flipDiagonally) + { + FloatHelper.Swap(ref transform[1].X, ref transform[2].X); + FloatHelper.Swap(ref transform[1].Y, ref transform[2].Y); + } + + if (flipHorizontally) + { + if (flipDiagonally) + { + FloatHelper.Swap(ref transform[0].Y, ref transform[1].Y); + FloatHelper.Swap(ref transform[2].Y, ref transform[3].Y); + } + else + { + FloatHelper.Swap(ref transform[0].X, ref transform[1].X); + FloatHelper.Swap(ref transform[2].X, ref transform[3].X); + } + } + + if (flipVertically) + { + if (flipDiagonally) + { + FloatHelper.Swap(ref transform[0].X, ref transform[2].X); + FloatHelper.Swap(ref transform[1].X, ref transform[3].X); + } + else + { + FloatHelper.Swap(ref transform[0].Y, ref transform[2].Y); + FloatHelper.Swap(ref transform[1].Y, ref transform[3].Y); + } + } + + transform[0] = transform[0]; + transform[1] = transform[1]; + transform[2] = transform[2]; + transform[3] = transform[3]; + + return transform; + } + + private void CreateTextureCoordinates(TiledMapTileset tileset) + { + var sourceRectangle = tileset.GetTileRegion(LocalTileIdentifier); + var texture = tileset.Texture; + var texelLeft = (float)sourceRectangle.X / texture.Width; + var texelTop = (float)sourceRectangle.Y / texture.Height; + var texelRight = (sourceRectangle.X + sourceRectangle.Width) / (float)texture.Width; + var texelBottom = (sourceRectangle.Y + sourceRectangle.Height) / (float)texture.Height; + + TextureCoordinates[0].X = texelLeft; + TextureCoordinates[0].Y = texelTop; + + TextureCoordinates[1].X = texelRight; + TextureCoordinates[1].Y = texelTop; + + TextureCoordinates[2].X = texelLeft; + TextureCoordinates[2].Y = texelBottom; + + TextureCoordinates[3].X = texelRight; + TextureCoordinates[3].Y = texelBottom; + } + + public override string ToString() + { + return $"{LocalTileIdentifier}:{Duration}"; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/ColorTween.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/ColorTween.cs new file mode 100644 index 0000000..2e98e5d --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/ColorTween.cs @@ -0,0 +1,15 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Tweening; + +public class ColorTween: Tween<Color> +{ + internal ColorTween(object target, float duration, float delay, TweenMember<Color> member, Color endValue) : base(target, duration, delay, member, endValue) + { + } + + protected override void Interpolate(float n) + { + Member.Value = Color.Lerp(_startValue, _endValue, n); + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/EasingFunctions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/EasingFunctions.cs new file mode 100644 index 0000000..f93a5ff --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/EasingFunctions.cs @@ -0,0 +1,120 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Tweening +{ + public static class EasingFunctions + { + public static float Linear(float value) => value; + + public static float CubicIn(float value) => Power.In(value, 3); + public static float CubicOut(float value) => Power.Out(value, 3); + public static float CubicInOut(float value) => Power.InOut(value, 3); + + public static float QuadraticIn(float value) => Power.In(value, 2); + public static float QuadraticOut(float value) => Power.Out(value, 2); + public static float QuadraticInOut(float value) => Power.InOut(value, 2); + + public static float QuarticIn(float value) => Power.In(value, 4); + public static float QuarticOut(float value) => Power.Out(value, 4); + public static float QuarticInOut(float value) => Power.InOut(value, 4); + + public static float QuinticIn(float value) => Power.In(value, 5); + public static float QuinticOut(float value) => Power.Out(value, 5); + public static float QuinticInOut(float value) => Power.InOut(value, 5); + + public static float SineIn(float value) => (float) Math.Sin(value*MathHelper.PiOver2 - MathHelper.PiOver2) + 1; + public static float SineOut(float value) => (float) Math.Sin(value*MathHelper.PiOver2); + public static float SineInOut(float value) => (float) (Math.Sin(value*MathHelper.Pi - MathHelper.PiOver2) + 1)/2; + + public static float ExponentialIn(float value) => (float) Math.Pow(2, 10*(value - 1)); + public static float ExponentialOut(float value) => Out(value, ExponentialIn); + public static float ExponentialInOut(float value) => InOut(value, ExponentialIn); + + public static float CircleIn(float value) => (float) -(Math.Sqrt(1 - value * value) - 1); + public static float CircleOut(float value) => (float) Math.Sqrt(1 - (value - 1) * (value - 1)); + public static float CircleInOut(float value) => (float) (value <= .5 ? (Math.Sqrt(1 - value * value * 4) - 1) / -2 : (Math.Sqrt(1 - (value * 2 - 2) * (value * 2 - 2)) + 1) / 2); + + public static float ElasticIn(float value) + { + const int oscillations = 1; + const float springiness = 3f; + var e = (Math.Exp(springiness*value) - 1)/(Math.Exp(springiness) - 1); + return (float) (e*Math.Sin((MathHelper.PiOver2 + MathHelper.TwoPi*oscillations)*value)); + } + + public static float ElasticOut(float value) => Out(value, ElasticIn); + public static float ElasticInOut(float value) => InOut(value, ElasticIn); + + public static float BackIn(float value) + { + const float amplitude = 1f; + return (float) (Math.Pow(value, 3) - value*amplitude*Math.Sin(value*MathHelper.Pi)); + } + + public static float BackOut(float value) => Out(value, BackIn); + public static float BackInOut(float value) => InOut(value, BackIn); + + public static float BounceOut(float value) => Out(value, BounceIn); + public static float BounceInOut(float value) => InOut(value, BounceIn); + + public static float BounceIn(float value) + { + const float bounceConst1 = 2.75f; + var bounceConst2 = (float) Math.Pow(bounceConst1, 2); + + value = 1 - value; //flip x-axis + + if (value < 1/bounceConst1) // big bounce + return 1f - bounceConst2*value*value; + + if (value < 2/bounceConst1) + return 1 - (float) (bounceConst2*Math.Pow(value - 1.5f/bounceConst1, 2) + .75); + + if (value < 2.5/bounceConst1) + return 1 - (float) (bounceConst2*Math.Pow(value - 2.25f/bounceConst1, 2) + .9375); + + //small bounce + return 1f - (float) (bounceConst2*Math.Pow(value - 2.625f/bounceConst1, 2) + .984375); + } + + + private static float Out(float value, Func<float, float> function) + { + return 1 - function(1 - value); + } + + private static float InOut(float value, Func<float, float> function) + { + if (value < 0.5f) + return 0.5f*function(value*2); + + return 1f - 0.5f*function(2 - value*2); + } + + private static class Power + { + public static float In(double value, int power) + { + return (float) Math.Pow(value, power); + } + + public static float Out(double value, int power) + { + var sign = power%2 == 0 ? -1 : 1; + return (float) (sign*(Math.Pow(value - 1, power) + sign)); + } + + public static float InOut(double s, int power) + { + s *= 2; + + if (s < 1) + return In(s, power)/2; + + var sign = power%2 == 0 ? -1 : 1; + return (float) (sign/2.0*(Math.Pow(s - 2, power) + sign*2)); + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/LinearOperations.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/LinearOperations.cs new file mode 100644 index 0000000..12a03a8 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/LinearOperations.cs @@ -0,0 +1,21 @@ +using System; +using System.Linq.Expressions; + +namespace MonoGame.Extended.Tweening; + +public class LinearOperations<T> +{ + static LinearOperations() + { + var a = Expression.Parameter(typeof(T)); + var b = Expression.Parameter(typeof(T)); + var c = Expression.Parameter(typeof(float)); + Add = Expression.Lambda<Func<T, T, T>>(Expression.Add(a, b), a, b).Compile(); + Subtract = Expression.Lambda<Func<T, T, T>>(Expression.Subtract(a, b), a, b).Compile(); + Multiply = Expression.Lambda<Func<T, float, T>>(Expression.Multiply(a, c), a, c).Compile(); + } + + public static Func<T, T, T> Add { get; } + public static Func<T, T, T> Subtract { get; } + public static Func<T, float, T> Multiply { get; } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/LinearTween.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/LinearTween.cs new file mode 100644 index 0000000..8469a13 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/LinearTween.cs @@ -0,0 +1,23 @@ +namespace MonoGame.Extended.Tweening; + +public class LinearTween<T>: Tween<T> + where T: struct +{ + private T _range; + + internal LinearTween(object target, float duration, float delay, TweenMember<T> member, T endValue) : base(target, duration, delay, member, endValue) + { + } + + protected override void Initialize() + { + base.Initialize(); + _range = LinearOperations<T>.Subtract(_endValue, _startValue); + } + + protected override void Interpolate(float n) + { + var value = LinearOperations<T>.Add(_startValue, LinearOperations<T>.Multiply(_range, n)); + Member.Value = value; + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/MonoGame.Extended.Tweening.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/MonoGame.Extended.Tweening.csproj new file mode 100644 index 0000000..8395e42 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/MonoGame.Extended.Tweening.csproj @@ -0,0 +1,12 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <Description>A tweening system to make MonoGame more awesome.</Description> + <PackageTags>monogame animations tweening</PackageTags> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\MonoGame.Extended\MonoGame.Extended.csproj" /> + </ItemGroup> + +</Project> diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/Tween.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/Tween.cs new file mode 100644 index 0000000..ad29251 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/Tween.cs @@ -0,0 +1,184 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Tweening +{ + public abstract class Tween<T> : Tween + where T : struct + { + internal Tween(object target, float duration, float delay, TweenMember<T> member, T endValue) + : base(target, duration, delay) + { + Member = member; + _endValue = endValue; + } + + public TweenMember<T> Member { get; } + public override string MemberName => Member.Name; + + protected T _startValue; + protected T _endValue; + + protected override void Initialize() + { + _startValue = Member.Value; + } + + protected override void Swap() + { + _endValue = _startValue; + Initialize(); + } + } + + public abstract class Tween + { + internal Tween(object target, float duration, float delay) + { + Target = target; + Duration = duration; + Delay = delay; + IsAlive = true; + + _remainingDelay = delay; + } + + public object Target { get; } + public abstract string MemberName { get; } + public float Duration { get; } + public float Delay { get; } + public bool IsPaused { get; set; } + public bool IsRepeating => _remainingRepeats != 0; + public bool IsRepeatingForever => _remainingRepeats < 0; + public bool IsAutoReverse { get; private set; } + public bool IsAlive { get; private set; } + public bool IsComplete { get; private set; } + public float TimeRemaining => Duration - _elapsedDuration; + public float Completion => MathHelper.Clamp(_completion, 0, 1); + + private Func<float, float> _easingFunction; + private bool _isInitialized; + private float _completion; + private float _elapsedDuration; + private float _remainingDelay; + private float _repeatDelay; + private int _remainingRepeats; + private Action<Tween> _onBegin; + private Action<Tween> _onEnd; + + public Tween Easing(Func<float, float> easingFunction) { _easingFunction = easingFunction; return this; } + public Tween OnBegin(Action<Tween> action) { _onBegin = action; return this; } + public Tween OnEnd(Action<Tween> action) { _onEnd = action; return this; } + public Tween Pause() { IsPaused = true; return this; } + public Tween Resume() { IsPaused = false; return this; } + + public Tween Repeat(int count, float repeatDelay = 0f) + { + _remainingRepeats = count; + _repeatDelay = repeatDelay; + return this; + } + + public Tween RepeatForever(float repeatDelay = 0f) + { + _remainingRepeats = -1; + _repeatDelay = repeatDelay; + return this; + } + + public Tween AutoReverse() + { + if (_remainingRepeats == 0) + _remainingRepeats = 1; + + IsAutoReverse = true; + return this; + } + + protected abstract void Initialize(); + protected abstract void Interpolate(float n); + protected abstract void Swap(); + + public void Cancel() + { + _remainingRepeats = 0; + IsAlive = false; + } + + public void CancelAndComplete() + { + if (IsAlive) + { + _completion = 1; + + Interpolate(1); + IsComplete = true; + _onEnd?.Invoke(this); + } + + Cancel(); + } + + public void Update(float elapsedSeconds) + { + if(IsPaused || !IsAlive) + return; + + if (_remainingDelay > 0) + { + _remainingDelay -= elapsedSeconds; + + if (_remainingDelay > 0) + return; + } + + if (!_isInitialized) + { + _isInitialized = true; + Initialize(); + _onBegin?.Invoke(this); + } + + if (IsComplete) + { + IsComplete = false; + _elapsedDuration = 0; + _onBegin?.Invoke(this); + + if (IsAutoReverse) + Swap(); + } + + _elapsedDuration += elapsedSeconds; + + var n = _completion = _elapsedDuration / Duration; + + if (_easingFunction != null) + n = _easingFunction(n); + + if (_elapsedDuration >= Duration) + { + if (_remainingRepeats != 0) + { + if(_remainingRepeats > 0) + _remainingRepeats--; + + _remainingDelay = _repeatDelay; + } + else if (_remainingRepeats == 0) + { + IsAlive = false; + } + + n = _completion = 1; + IsComplete = true; + } + + Interpolate(n); + + if (IsComplete) + _onEnd?.Invoke(this); + } + + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenFieldMember.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenFieldMember.cs new file mode 100644 index 0000000..da2f75d --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenFieldMember.cs @@ -0,0 +1,44 @@ +using System; +using System.Diagnostics; +using System.Linq.Expressions; +using System.Reflection; + +namespace MonoGame.Extended.Tweening +{ + public sealed class TweenFieldMember<T> : TweenMember<T> + where T : struct + { + private readonly FieldInfo _fieldInfo; + + public TweenFieldMember(object target, FieldInfo fieldInfo) + : base(target, CompileGetMethod(fieldInfo), CompileSetMethod(fieldInfo)) + { + _fieldInfo = fieldInfo; + } + + private static Func<object, object> CompileGetMethod(FieldInfo fieldInfo) + { + var self = Expression.Parameter(typeof(object)); + var instance = Expression.Convert(self, fieldInfo.DeclaringType); + var field = Expression.Field(instance, fieldInfo); + var convert = Expression.TypeAs(field, typeof(object)); + + return Expression.Lambda<Func<object, object>>(convert, self).Compile(); + } + + private static Action<object, object> CompileSetMethod(FieldInfo fieldInfo) + { + Debug.Assert(fieldInfo.DeclaringType != null); + + var self = Expression.Parameter(typeof(object)); + var value = Expression.Parameter(typeof(object)); + var fieldExp = Expression.Field(Expression.Convert(self, fieldInfo.DeclaringType), fieldInfo); + var assignExp = Expression.Assign(fieldExp, Expression.Convert(value, fieldInfo.FieldType)); + + return Expression.Lambda<Action<object, object>>(assignExp, self, value).Compile(); + } + + public override Type Type => _fieldInfo.FieldType; + public override string Name => _fieldInfo.Name; + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenMember.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenMember.cs new file mode 100644 index 0000000..adcee59 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenMember.cs @@ -0,0 +1,37 @@ +using System; +using System.Linq.Expressions; + +namespace MonoGame.Extended.Tweening +{ + public abstract class TweenMember + { + protected TweenMember(object target) + { + Target = target; + } + + public object Target { get; } + public abstract Type Type { get; } + public abstract string Name { get; } + } + + public abstract class TweenMember<T> : TweenMember + where T : struct + { + protected TweenMember(object target, Func<object, object> getMethod, Action<object, object> setMethod) + : base(target) + { + _getMethod = getMethod; + _setMethod = setMethod; + } + + private readonly Func<object, object> _getMethod; + private readonly Action<object, object> _setMethod; + + public T Value + { + get { return (T) _getMethod(Target); } + set { _setMethod(Target, value); } + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenPropertyMember.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenPropertyMember.cs new file mode 100644 index 0000000..7b1db71 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/TweenPropertyMember.cs @@ -0,0 +1,43 @@ +using System; +using System.Diagnostics; +using System.Linq.Expressions; +using System.Reflection; + +namespace MonoGame.Extended.Tweening +{ + public sealed class TweenPropertyMember<T> : TweenMember<T> + where T : struct + { + private readonly PropertyInfo _propertyInfo; + + public TweenPropertyMember(object target, PropertyInfo propertyInfo) + : base(target, CompileGetMethod(propertyInfo), CompileSetMethod(propertyInfo)) + { + _propertyInfo = propertyInfo; + } + + public override Type Type => _propertyInfo.PropertyType; + public override string Name => _propertyInfo.Name; + + private static Func<object, object> CompileGetMethod(PropertyInfo propertyInfo) + { + var param = Expression.Parameter(typeof(object)); + var instance = Expression.Convert(param, propertyInfo.DeclaringType); + var convert = Expression.TypeAs(Expression.Property(instance, propertyInfo), typeof(object)); + return Expression.Lambda<Func<object, object>>(convert, param).Compile(); + } + + private static Action<object, object> CompileSetMethod(PropertyInfo propertyInfo) + { + Debug.Assert(propertyInfo.DeclaringType != null); + + var param = Expression.Parameter(typeof(object)); + var argument = Expression.Parameter(typeof(object)); + var expression = Expression.Convert(param, propertyInfo.DeclaringType); + var methodInfo = propertyInfo.SetMethod; + var arguments = Expression.Convert(argument, propertyInfo.PropertyType); + var setterCall = Expression.Call(expression, methodInfo, arguments); + return Expression.Lambda<Action<object, object>>(setterCall, param, argument).Compile(); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/Tweener.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/Tweener.cs new file mode 100644 index 0000000..61b55f9 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended.Tweening/Tweener.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Tweening +{ + public class Tweener : IDisposable + { + public Tweener() + { + } + + public void Dispose() + { + CancelAll(); + _activeTweens.Clear(); + _memberCache.Clear(); + } + + public long AllocationCount { get; private set; } + + private readonly List<Tween> _activeTweens = new List<Tween>(); + + public Tween<TMember> TweenTo<TTarget, TMember>(TTarget target, Expression<Func<TTarget, TMember>> expression, TMember toValue, float duration, float delay = 0f) + where TTarget : class + where TMember : struct + { + switch (toValue) + { + case Color toValueColor: + return (Tween<TMember>)(object)TweenTo<TTarget, Color, ColorTween>(target, expression as Expression<Func<TTarget, Color>>, toValueColor, duration, delay); + default: + return TweenTo<TTarget, TMember, LinearTween<TMember>>(target, expression, toValue, duration, delay); + } + + } + + public Tween<TMember> TweenTo<TTarget, TMember, TTween>(TTarget target, Expression<Func<TTarget, TMember>> expression, TMember toValue, float duration, float delay = 0f) + where TTarget : class + where TMember : struct + where TTween : Tween<TMember> + { + var memberExpression = (MemberExpression)expression.Body; + var memberInfo = memberExpression.Member; + var member = GetMember<TMember>(target, memberInfo.Name); + var activeTween = FindTween(target, member.Name); + + activeTween?.Cancel(); + + AllocationCount++; + var tween = (TTween)Activator.CreateInstance(typeof(TTween), + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, + new object[]{target, duration, delay, member, toValue}, null); + _activeTweens.Add(tween); + return tween; + } + + public void Update(float elapsedSeconds) + { + for (var i = _activeTweens.Count - 1; i >= 0; i--) + { + var tween = _activeTweens[i]; + + tween.Update(elapsedSeconds); + + if (!tween.IsAlive) + _activeTweens.RemoveAt(i); + } + } + + public Tween FindTween(object target, string memberName) + { + return _activeTweens.FirstOrDefault(t => t.Target == target && t.MemberName == memberName); + } + + public void CancelAll() + { + foreach (var tween in _activeTweens) + tween.Cancel(); + } + + public void CancelAndCompleteAll() + { + foreach (var tween in _activeTweens) + tween.CancelAndComplete(); + } + + private struct TweenMemberKey + { +#pragma warning disable 414 + public object Target; + public string MemberName; +#pragma warning restore 414 + } + + private readonly Dictionary<TweenMemberKey, TweenMember> _memberCache = new Dictionary<TweenMemberKey, TweenMember>(); + + private TweenMember<T> GetMember<T>(object target, string memberName) + where T : struct + { + var key = new TweenMemberKey { Target = target, MemberName = memberName }; + + if (_memberCache.TryGetValue(key, out var member)) + return (TweenMember<T>) member; + + member = CreateMember<T>(target, memberName); + _memberCache.Add(key, member); + return (TweenMember<T>) member; + } + + private TweenMember<T> CreateMember<T>(object target, string memberName) + where T : struct + { + AllocationCount++; + + var type = target.GetType(); + var property = type.GetTypeInfo().GetProperty(memberName); + + if (property != null) + return new TweenPropertyMember<T>(target, property); + + var field = type.GetTypeInfo().GetField(memberName); + + if (field != null) + return new TweenFieldMember<T>(target, field); + + throw new InvalidOperationException($"'{memberName}' is not a property or field of the target"); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/AnimationComponent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/AnimationComponent.cs new file mode 100644 index 0000000..28a0e57 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/AnimationComponent.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using MonoGame.Extended.Sprites; + +namespace MonoGame.Extended.Animations +{ + public class AnimationComponent : GameComponent + { + public AnimationComponent(Game game) + : base(game) + { + Animations = new List<Animation>(); + } + + public List<Animation> Animations { get; } + + public override void Update(GameTime gameTime) + { + base.Update(gameTime); + + for (var i = Animations.Count - 1; i >= 0; i--) + { + var animation = Animations[i]; + animation.Update(gameTime); + } + + Animations.RemoveAll(a => a.IsDisposed); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFont.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFont.cs new file mode 100644 index 0000000..7aaca08 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFont.cs @@ -0,0 +1,354 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.BitmapFonts +{ + public class BitmapFont + { + private readonly Dictionary<int, BitmapFontRegion> _characterMap = new Dictionary<int, BitmapFontRegion>(); + + public BitmapFont(string name, IEnumerable<BitmapFontRegion> regions, int lineHeight) + { + foreach (var region in regions) + _characterMap.Add(region.Character, region); + + Name = name; + LineHeight = lineHeight; + } + + public string Name { get; } + public int LineHeight { get; } + public int LetterSpacing { get; set; } + public static bool UseKernings { get; set; } = true; + + public BitmapFontRegion GetCharacterRegion(int character) + { + return _characterMap.TryGetValue(character, out var region) ? region : null; + } + + public Size2 MeasureString(string text) + { + if (string.IsNullOrEmpty(text)) + return Size2.Empty; + + var stringRectangle = GetStringRectangle(text); + return new Size2(stringRectangle.Width, stringRectangle.Height); + } + + public Size2 MeasureString(StringBuilder text) + { + if (text == null || text.Length == 0) + return Size2.Empty; + + var stringRectangle = GetStringRectangle(text); + return new Size2(stringRectangle.Width, stringRectangle.Height); + } + + public RectangleF GetStringRectangle(string text) + { + return GetStringRectangle(text, Point2.Zero); + } + + public RectangleF GetStringRectangle(string text, Point2 position) + { + var glyphs = GetGlyphs(text, position); + var rectangle = new RectangleF(position.X, position.Y, 0, LineHeight); + + foreach (var glyph in glyphs) + { + if (glyph.FontRegion != null) + { + var right = glyph.Position.X + glyph.FontRegion.Width; + + if (right > rectangle.Right) + rectangle.Width = (int)(right - rectangle.Left); + } + + if (glyph.Character == '\n') + rectangle.Height += LineHeight; + } + + return rectangle; + } + + public RectangleF GetStringRectangle(StringBuilder text, Point2? position = null) + { + var position1 = position ?? new Point2(); + var glyphs = GetGlyphs(text, position1); + var rectangle = new RectangleF(position1.X, position1.Y, 0, LineHeight); + + foreach (var glyph in glyphs) + { + if (glyph.FontRegion != null) + { + var right = glyph.Position.X + glyph.FontRegion.Width; + + if (right > rectangle.Right) + rectangle.Width = (int)(right - rectangle.Left); + } + + if (glyph.Character == '\n') + rectangle.Height += LineHeight; + } + + return rectangle; + } + + public StringGlyphEnumerable GetGlyphs(string text, Point2? position = null) + { + return new StringGlyphEnumerable(this, text, position); + } + + public StringBuilderGlyphEnumerable GetGlyphs(StringBuilder text, Point2? position) + { + return new StringBuilderGlyphEnumerable(this, text, position); + } + + public override string ToString() + { + return $"{Name}"; + } + + public struct StringGlyphEnumerable : IEnumerable<BitmapFontGlyph> + { + private readonly StringGlyphEnumerator _enumerator; + + public StringGlyphEnumerable(BitmapFont font, string text, Point2? position) + { + _enumerator = new StringGlyphEnumerator(font, text, position); + } + + public StringGlyphEnumerator GetEnumerator() + { + return _enumerator; + } + + IEnumerator<BitmapFontGlyph> IEnumerable<BitmapFontGlyph>.GetEnumerator() + { + return _enumerator; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _enumerator; + } + } + + public struct StringGlyphEnumerator : IEnumerator<BitmapFontGlyph> + { + private readonly BitmapFont _font; + private readonly string _text; + private int _index; + private readonly Point2 _position; + private Vector2 _positionDelta; + private BitmapFontGlyph _currentGlyph; + private BitmapFontGlyph? _previousGlyph; + + object IEnumerator.Current + { + get + { + // casting a struct to object will box it, behaviour we want to avoid... + throw new InvalidOperationException(); + } + } + + public BitmapFontGlyph Current => _currentGlyph; + + public StringGlyphEnumerator(BitmapFont font, string text, Point2? position) + { + _font = font; + _text = text; + _index = -1; + _position = position ?? new Point2(); + _positionDelta = new Vector2(); + _currentGlyph = new BitmapFontGlyph(); + _previousGlyph = null; + } + + public bool MoveNext() + { + if (++_index >= _text.Length) + return false; + + var character = GetUnicodeCodePoint(_text, ref _index); + _currentGlyph.Character = character; + _currentGlyph.FontRegion = _font.GetCharacterRegion(character); + _currentGlyph.Position = _position + _positionDelta; + + if (_currentGlyph.FontRegion != null) + { + _currentGlyph.Position.X += _currentGlyph.FontRegion.XOffset; + _currentGlyph.Position.Y += _currentGlyph.FontRegion.YOffset; + _positionDelta.X += _currentGlyph.FontRegion.XAdvance + _font.LetterSpacing; + } + + if (UseKernings && _previousGlyph?.FontRegion != null) + { + if (_previousGlyph.Value.FontRegion.Kernings.TryGetValue(character, out var amount)) + { + _positionDelta.X += amount; + _currentGlyph.Position.X += amount; + } + } + + _previousGlyph = _currentGlyph; + + if (character != '\n') + return true; + + _positionDelta.Y += _font.LineHeight; + _positionDelta.X = 0; + _previousGlyph = null; + + return true; + } + + private static int GetUnicodeCodePoint(string text, ref int index) + { + return char.IsHighSurrogate(text[index]) && ++index < text.Length + ? char.ConvertToUtf32(text[index - 1], text[index]) + : text[index]; + } + + public void Dispose() + { + } + + public void Reset() + { + _positionDelta = new Point2(); + _index = -1; + _previousGlyph = null; + } + } + + public struct StringBuilderGlyphEnumerable : IEnumerable<BitmapFontGlyph> + { + private readonly StringBuilderGlyphEnumerator _enumerator; + + public StringBuilderGlyphEnumerable(BitmapFont font, StringBuilder text, Point2? position) + { + _enumerator = new StringBuilderGlyphEnumerator(font, text, position); + } + + public StringBuilderGlyphEnumerator GetEnumerator() + { + return _enumerator; + } + + IEnumerator<BitmapFontGlyph> IEnumerable<BitmapFontGlyph>.GetEnumerator() + { + return _enumerator; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _enumerator; + } + } + + public struct StringBuilderGlyphEnumerator : IEnumerator<BitmapFontGlyph> + { + private readonly BitmapFont _font; + private readonly StringBuilder _text; + private int _index; + private readonly Point2 _position; + private Vector2 _positionDelta; + private BitmapFontGlyph _currentGlyph; + private BitmapFontGlyph? _previousGlyph; + + object IEnumerator.Current + { + get + { + // casting a struct to object will box it, behaviour we want to avoid... + throw new InvalidOperationException(); + } + } + + public BitmapFontGlyph Current => _currentGlyph; + + public StringBuilderGlyphEnumerator(BitmapFont font, StringBuilder text, Point2? position) + { + _font = font; + _text = text; + _index = -1; + _position = position ?? new Point2(); + _positionDelta = new Vector2(); + _currentGlyph = new BitmapFontGlyph(); + _previousGlyph = null; + } + + public bool MoveNext() + { + if (++_index >= _text.Length) + return false; + + var character = GetUnicodeCodePoint(_text, ref _index); + _currentGlyph = new BitmapFontGlyph + { + Character = character, + FontRegion = _font.GetCharacterRegion(character), + Position = _position + _positionDelta + }; + + if (_currentGlyph.FontRegion != null) + { + _currentGlyph.Position.X += _currentGlyph.FontRegion.XOffset; + _currentGlyph.Position.Y += _currentGlyph.FontRegion.YOffset; + _positionDelta.X += _currentGlyph.FontRegion.XAdvance + _font.LetterSpacing; + } + + if (UseKernings && _previousGlyph.HasValue && _previousGlyph.Value.FontRegion != null) + { + int amount; + if (_previousGlyph.Value.FontRegion.Kernings.TryGetValue(character, out amount)) + { + _positionDelta.X += amount; + _currentGlyph.Position.X += amount; + } + } + + _previousGlyph = _currentGlyph; + + if (character != '\n') + return true; + + _positionDelta.Y += _font.LineHeight; + _positionDelta.X = _position.X; + _previousGlyph = null; + + return true; + } + + private static int GetUnicodeCodePoint(StringBuilder text, ref int index) + { + return char.IsHighSurrogate(text[index]) && ++index < text.Length + ? char.ConvertToUtf32(text[index - 1], text[index]) + : text[index]; + } + + public void Dispose() + { + } + + public void Reset() + { + _positionDelta = new Point2(); + _index = -1; + _previousGlyph = null; + } + } + } + + public struct BitmapFontGlyph + { + public int Character; + public Vector2 Position; + public BitmapFontRegion FontRegion; + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontExtensions.cs new file mode 100644 index 0000000..da072c5 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontExtensions.cs @@ -0,0 +1,156 @@ +using System; +using System.Text; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using MonoGame.Extended.TextureAtlases; + +namespace MonoGame.Extended.BitmapFonts +{ + public static class BitmapFontExtensions + { + /// <summary> + /// Adds a string to a batch of sprites for rendering using the specified font, text, position, color, rotation, + /// origin, scale, effects and layer. + /// </summary> + /// <param name="spriteBatch"></param> + /// <param name="bitmapFont">A font for displaying text.</param> + /// <param name="text">The text message to display.</param> + /// <param name="position">The location (in screen coordinates) to draw the text.</param> + /// <param name="color"> + /// The <see cref="Color" /> to tint a sprite. Use <see cref="Color.White" /> for full color with no + /// tinting. + /// </param> + /// <param name="rotation">Specifies the angle (in radians) to rotate the text about its origin.</param> + /// <param name="origin">The origin for each letter; the default is (0,0) which represents the upper-left corner.</param> + /// <param name="scale">Scale factor.</param> + /// <param name="effect">Effects to apply.</param> + /// <param name="layerDepth"> + /// The depth of a layer. By default, 0 represents the front layer and 1 represents a back layer. + /// Use SpriteSortMode if you want sprites to be sorted during drawing. + /// </param> + /// <param name="clippingRectangle">Clips the boundaries of the text so that it's not drawn outside the clipping rectangle</param> + public static void DrawString(this SpriteBatch spriteBatch, BitmapFont bitmapFont, string text, Vector2 position, + Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effect, float layerDepth, Rectangle? clippingRectangle = null) + { + if (text == null) + throw new ArgumentNullException(nameof(text)); + if (effect != SpriteEffects.None) + throw new NotSupportedException($"{effect} is not currently supported for {nameof(BitmapFont)}"); + + var glyphs = bitmapFont.GetGlyphs(text, position); + foreach (var glyph in glyphs) + { + if (glyph.FontRegion == null) + continue; + var characterOrigin = position - glyph.Position + origin; + spriteBatch.Draw(glyph.FontRegion.TextureRegion, position, color, rotation, characterOrigin, scale, effect, layerDepth, clippingRectangle); + } + } + + public static void DrawString(this SpriteBatch spriteBatch, BitmapFont bitmapFont, StringBuilder text, Vector2 position, + Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effect, float layerDepth, Rectangle? clippingRectangle = null) + { + if (text == null) + throw new ArgumentNullException(nameof(text)); + if (effect != SpriteEffects.None) + throw new NotSupportedException($"{effect} is not currently supported for {nameof(BitmapFont)}"); + + var glyphs = bitmapFont.GetGlyphs(text, position); + foreach (var glyph in glyphs) + { + if (glyph.FontRegion == null) + continue; + var characterOrigin = position - glyph.Position + origin; + spriteBatch.Draw(glyph.FontRegion.TextureRegion, position, color, rotation, characterOrigin, scale, effect, layerDepth, clippingRectangle); + } + } + + /// <summary> + /// Adds a string to a batch of sprites for rendering using the specified font, text, position, color, rotation, + /// origin, scale, effects and layer. + /// </summary> + /// <param name="spriteBatch"></param> + /// <param name="font">A font for displaying text.</param> + /// <param name="text">The text message to display.</param> + /// <param name="position">The location (in screen coordinates) to draw the text.</param> + /// <param name="color"> + /// The <see cref="Color" /> to tint a sprite. Use <see cref="Color.White" /> for full color with no + /// tinting. + /// </param> + /// <param name="rotation">Specifies the angle (in radians) to rotate the text about its origin.</param> + /// <param name="origin">The origin for each letter; the default is (0,0) which represents the upper-left corner.</param> + /// <param name="scale">Scale factor.</param> + /// <param name="effect">Effects to apply.</param> + /// <param name="layerDepth"> + /// The depth of a layer. By default, 0 represents the front layer and 1 represents a back layer. + /// Use SpriteSortMode if you want sprites to be sorted during drawing. + /// </param> + /// <param name="clippingRectangle">Clips the boundaries of the text so that it's not drawn outside the clipping rectangle</param> + public static void DrawString(this SpriteBatch spriteBatch, BitmapFont font, string text, Vector2 position, + Color color, float rotation, Vector2 origin, float scale, SpriteEffects effect, float layerDepth, Rectangle? clippingRectangle = null) + { + DrawString(spriteBatch, font, text, position, color, rotation, origin, new Vector2(scale, scale), effect, layerDepth, clippingRectangle); + } + + public static void DrawString(this SpriteBatch spriteBatch, BitmapFont font, StringBuilder text, Vector2 position, + Color color, float rotation, Vector2 origin, float scale, SpriteEffects effect, float layerDepth, Rectangle? clippingRectangle = null) + { + DrawString(spriteBatch, font, text, position, color, rotation, origin, new Vector2(scale, scale), effect, layerDepth, clippingRectangle); + } + + /// <summary> + /// Adds a string to a batch of sprites for rendering using the specified font, text, position, color, layer, + /// and width (in pixels) where to wrap the text at. + /// </summary> + /// <remarks> + /// <see cref="BitmapFont" /> objects are loaded from the Content Manager. See the <see cref="BitmapFont" /> class for + /// more information. + /// Before any calls to this method you must call <see cref="SpriteBatch.Begin" />. Once all calls + /// are complete, call <see cref="SpriteBatch.End" />. + /// Use a newline character (\n) to draw more than one line of text. + /// </remarks> + /// <param name="spriteBatch"></param> + /// <param name="font">A font for displaying text.</param> + /// <param name="text">The text message to display.</param> + /// <param name="position">The location (in screen coordinates) to draw the text.</param> + /// <param name="color"> + /// The <see cref="Color" /> to tint a sprite. Use <see cref="Color.White" /> for full color with no + /// tinting. + /// </param> + /// <param name="layerDepth"> + /// The depth of a layer. By default, 0 represents the front layer and 1 represents a back layer. + /// Use SpriteSortMode if you want sprites to be sorted during drawing. + /// </param> + /// <param name="clippingRectangle">Clips the boundaries of the text so that it's not drawn outside the clipping rectangle</param> + public static void DrawString(this SpriteBatch spriteBatch, BitmapFont font, string text, Vector2 position, Color color, float layerDepth, Rectangle? clippingRectangle = null) + { + DrawString(spriteBatch, font, text, position, color, rotation: 0, origin: Vector2.Zero, scale: Vector2.One, effect: SpriteEffects.None, + layerDepth: layerDepth, clippingRectangle: clippingRectangle); + } + + /// <summary> + /// Adds a string to a batch of sprites for rendering using the specified font, text, position, color, + /// and width (in pixels) where to wrap the text at. The text is drawn on layer 0f. + /// </summary> + /// <param name="spriteBatch"></param> + /// <param name="font">A font for displaying text.</param> + /// <param name="text">The text message to display.</param> + /// <param name="position">The location (in screen coordinates) to draw the text.</param> + /// <param name="color"> + /// The <see cref="Color" /> to tint a sprite. Use <see cref="Color.White" /> for full color with no + /// tinting. + /// </param> + /// <param name="clippingRectangle">Clips the boundaries of the text so that it's not drawn outside the clipping rectangle</param> + public static void DrawString(this SpriteBatch spriteBatch, BitmapFont font, string text, Vector2 position, Color color, Rectangle? clippingRectangle = null) + { + DrawString(spriteBatch, font, text, position, color, rotation: 0, origin: Vector2.Zero, scale: Vector2.One, effect: SpriteEffects.None, + layerDepth: 0, clippingRectangle: clippingRectangle); + } + + public static void DrawString(this SpriteBatch spriteBatch, BitmapFont font, StringBuilder text, Vector2 position, Color color, Rectangle? clippingRectangle = null) + { + DrawString(spriteBatch, font, text, position, color, rotation: 0, origin: Vector2.Zero, scale: Vector2.One, effect: SpriteEffects.None, + layerDepth: 0, clippingRectangle: clippingRectangle); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontReader.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontReader.cs new file mode 100644 index 0000000..c6d69fc --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontReader.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using MonoGame.Extended.Content; +using MonoGame.Extended.TextureAtlases; + +namespace MonoGame.Extended.BitmapFonts +{ + public class BitmapFontReader : ContentTypeReader<BitmapFont> + { + protected override BitmapFont Read(ContentReader reader, BitmapFont existingInstance) + { + var textureAssetCount = reader.ReadInt32(); + var assets = new List<string>(); + + for (var i = 0; i < textureAssetCount; i++) + { + var assetName = reader.ReadString(); + assets.Add(assetName); + } + + var textures = assets + .Select(textureName => reader.ContentManager.Load<Texture2D>(reader.GetRelativeAssetName(textureName))) + .ToArray(); + + var lineHeight = reader.ReadInt32(); + var regionCount = reader.ReadInt32(); + var regions = new BitmapFontRegion[regionCount]; + + for (var r = 0; r < regionCount; r++) + { + var character = reader.ReadInt32(); + var textureIndex = reader.ReadInt32(); + var x = reader.ReadInt32(); + var y = reader.ReadInt32(); + var width = reader.ReadInt32(); + var height = reader.ReadInt32(); + var xOffset = reader.ReadInt32(); + var yOffset = reader.ReadInt32(); + var xAdvance = reader.ReadInt32(); + var textureRegion = new TextureRegion2D(textures[textureIndex], x, y, width, height); + regions[r] = new BitmapFontRegion(textureRegion, character, xOffset, yOffset, xAdvance); + } + + var characterMap = regions.ToDictionary(r => r.Character); + var kerningsCount = reader.ReadInt32(); + + for (var k = 0; k < kerningsCount; k++) + { + var first = reader.ReadInt32(); + var second = reader.ReadInt32(); + var amount = reader.ReadInt32(); + + // Find region + if (!characterMap.TryGetValue(first, out var region)) + continue; + + region.Kernings[second] = amount; + } + + return new BitmapFont(reader.AssetName, regions, lineHeight); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontRegion.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontRegion.cs new file mode 100644 index 0000000..5254f48 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/BitmapFonts/BitmapFontRegion.cs @@ -0,0 +1,33 @@ +using System; +using MonoGame.Extended.TextureAtlases; +using System.Collections.Generic; + +namespace MonoGame.Extended.BitmapFonts +{ + public class BitmapFontRegion + { + public BitmapFontRegion(TextureRegion2D textureRegion, int character, int xOffset, int yOffset, int xAdvance) + { + TextureRegion = textureRegion; + Character = character; + XOffset = xOffset; + YOffset = yOffset; + XAdvance = xAdvance; + Kernings = new Dictionary<int, int>(); + } + + public int Character { get; } + public TextureRegion2D TextureRegion { get; } + public int XOffset { get; } + public int YOffset { get; } + public int XAdvance { get; } + public int Width => TextureRegion.Width; + public int Height => TextureRegion.Height; + public Dictionary<int, int> Kernings { get; } + + public override string ToString() + { + return $"{Convert.ToChar(Character)} {TextureRegion}"; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Camera.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Camera.cs new file mode 100644 index 0000000..617cb69 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Camera.cs @@ -0,0 +1,32 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + public abstract class Camera<T> where T : struct + { + public abstract T Position { get; set; } + public abstract float Rotation { get; set; } + public abstract float Zoom { get; set; } + public abstract float MinimumZoom { get; set; } + public abstract float MaximumZoom { get; set; } + public abstract RectangleF BoundingRectangle { get; } + public abstract T Origin { get; set; } + public abstract T Center { get; } + + public abstract void Move(T direction); + public abstract void Rotate(float deltaRadians); + public abstract void ZoomIn(float deltaZoom); + public abstract void ZoomOut(float deltaZoom); + public abstract void LookAt(T position); + + public abstract T WorldToScreen(T worldPosition); + public abstract T ScreenToWorld(T screenPosition); + + public abstract Matrix GetViewMatrix(); + public abstract Matrix GetInverseViewMatrix(); + + public abstract BoundingFrustum GetBoundingFrustum(); + public abstract ContainmentType Contains(Vector2 vector2); + public abstract ContainmentType Contains(Rectangle rectangle); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Bag.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Bag.cs new file mode 100644 index 0000000..868b6d7 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Bag.cs @@ -0,0 +1,217 @@ +// Original code dervied from: +// https://github.com/thelinuxlich/artemis_CSharp/blob/master/Artemis_XNA_INDEPENDENT/Utils/Bag.cs + +// -------------------------------------------------------------------------------------------------------------------- +// <copyright file="Bag.cs" company="GAMADU.COM"> +// Copyright © 2013 GAMADU.COM. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other materials +// provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY GAMADU.COM 'AS IS' AND ANY EXPRESS OR IMPLIED +// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GAMADU.COM OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +// ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// The views and conclusions contained in the software and documentation are those of the +// authors and should not be interpreted as representing official policies, either expressed +// or implied, of GAMADU.COM. +// </copyright> +// <summary> +// Class Bag. +// </summary> +// -------------------------------------------------------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace MonoGame.Extended.Collections +{ + public class Bag<T> : IEnumerable<T> + { + private T[] _items; + private readonly bool _isPrimitive; + + public int Capacity => _items.Length; + public bool IsEmpty => Count == 0; + public int Count { get; private set; } + + public Bag(int capacity = 16) + { + _isPrimitive = typeof(T).IsPrimitive; + _items = new T[capacity]; + } + + public T this[int index] + { + get => index >= _items.Length ? default(T) : _items[index]; + set + { + EnsureCapacity(index + 1); + if (index >= Count) + Count = index + 1; + _items[index] = value; + } + } + + public void Add(T element) + { + EnsureCapacity(Count + 1); + _items[Count] = element; + ++Count; + } + + public void AddRange(Bag<T> range) + { + for (int index = 0, j = range.Count; j > index; ++index) + Add(range[index]); + } + + public void Clear() + { + if(Count == 0) + return; + + // non-primitive types are cleared so the garbage collector can release them + if (!_isPrimitive) + Array.Clear(_items, 0, Count); + + Count = 0; + } + + public bool Contains(T element) + { + for (var index = Count - 1; index >= 0; --index) + { + if (element.Equals(_items[index])) + return true; + } + + return false; + } + + public T RemoveAt(int index) + { + var result = _items[index]; + --Count; + _items[index] = _items[Count]; + _items[Count] = default(T); + return result; + } + + public bool Remove(T element) + { + for (var index = Count - 1; index >= 0; --index) + { + if (element.Equals(_items[index])) + { + --Count; + _items[index] = _items[Count]; + _items[Count] = default(T); + + return true; + } + } + + return false; + } + + public bool RemoveAll(Bag<T> bag) + { + var isResult = false; + + for (var index = bag.Count - 1; index >= 0; --index) + { + if (Remove(bag[index])) + isResult = true; + } + + return isResult; + } + + private void EnsureCapacity(int capacity) + { + if (capacity < _items.Length) + return; + + var newCapacity = Math.Max((int)(_items.Length * 1.5), capacity); + var oldElements = _items; + _items = new T[newCapacity]; + Array.Copy(oldElements, 0, _items, 0, oldElements.Length); + } + + IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// <summary> + /// Get the <see cref="BagEnumerator"/> for this <see cref="Bag{T}"/>. + /// </summary> + /// <returns></returns> + /// <remarks> + /// Use this method preferentially over <see cref="IEnumerable.GetEnumerator"/> while enumerating via foreach + /// to avoid boxing the enumerator on every iteration, which can be expensive in high-performance environments. + /// </remarks> + public BagEnumerator GetEnumerator() + { + return new BagEnumerator(this); + } + + /// <summary> + /// Enumerates a Bag. + /// </summary> + public struct BagEnumerator : IEnumerator<T> + { + private readonly Bag<T> _bag; + private volatile int _index; + + /// <summary> + /// Creates a new <see cref="BagEnumerator"/> for this <see cref="Bag{T}"/>. + /// </summary> + /// <param name="bag"></param> + public BagEnumerator(Bag<T> bag) + { + _bag = bag; + _index = -1; + } + + readonly T IEnumerator<T>.Current => _bag[_index]; + + readonly object IEnumerator.Current => _bag[_index]; + + /// <summary> + /// Gets the element in the <see cref="Bag{T}"/> at the current position of the enumerator. + /// </summary> + public readonly T Current => _bag[_index]; + + /// <inheritdoc/> + public bool MoveNext() + { + return ++_index < _bag.Count; + } + + /// <inheritdoc/> + public readonly void Dispose() + { + } + + /// <inheritdoc/> + public readonly void Reset() + { + } + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Deque.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Deque.cs new file mode 100644 index 0000000..fd59066 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Deque.cs @@ -0,0 +1,837 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace MonoGame.Extended.Collections +{ + internal static class Deque + { + internal static readonly Func<int, int> DefaultResizeFunction = x => x * 2; + } + + /// <summary> + /// Represents a collection of objects which elements can added to or removed either from the front or back; a + /// <a href="https://en.wikipedia.org/wiki/Double-ended_queue">double ended queue</a> (deque). + /// </summary> + /// <remarks> + /// <a href="https://en.wikipedia.org/wiki/Circular_buffer">circular array</a> is used as the internal data + /// structure for the <see cref="Deque{T}" />. + /// </remarks> + /// <typeparam name="T">The type of the elements in the deque.</typeparam> + public class Deque<T> : IList<T> + { + private const int _defaultCapacity = 4; + private static readonly T[] _emptyArray = new T[0]; + private int _frontArrayIndex; + private T[] _items; + private Func<int, int> _resizeFunction = Deque.DefaultResizeFunction; + + /// <summary> + /// Initializes a new instance of the <see cref="Deque{T}" /> class that is empty and has the default initial capacity. + /// </summary> + /// <remarks> + /// <para> + /// The capacity of a <see cref="Deque{T}" /> is the number of elements that the <see cref="Deque{T}" /> can + /// hold. As elements are added to a <see cref="Deque{T}" />, <see cref="Capacity" /> is automatically increased by + /// <see cref="ResizeFunction" /> as required by reallocating the internal array. + /// </para> + /// <para> + /// If the size of the collection can be estimated, using the <see cref="Deque{T}(int)" /> constructor and + /// specifying the initial capacity eliminates the need to perform a number of resizing operations while adding + /// elements to the <see cref="Deque{T}" />. + /// </para> + /// <para> + /// The capacity can be decreased by calling the <see cref="TrimExcess" /> method or by setting the + /// <see cref="Capacity" /> property explicitly. Decreasing, or increasing, the capacity reallocates memory and + /// copies all the + /// elements in the <see cref="Deque{T}" />. + /// </para> + /// <para>This constructor is an O(1) operation.</para> + /// </remarks> + public Deque() + { + _items = _emptyArray; + } + + /// <summary> + /// Initializes a new instance of the <see cref="Deque{T}" /> class that contains elements copied from the specified + /// collection and has sufficient capacity to accommodate the number of elements copied. + /// </summary> + /// <param name="collection">The collection whose elements are copied to the new deque.</param> + /// <exception cref="ArgumentNullException"><paramref name="collection" /> is null.</exception> + /// <remarks> + /// <para> + /// The elements are copied onto the <see cref="Deque{T}" /> in the same order they are read by the enumerator of + /// <paramref name="collection" />. + /// </para> + /// <para>This constructor is an O(n) operation, where n is the number of elements in <paramref name="collection" />.</para> + /// </remarks> + public Deque(IEnumerable<T> collection) + { + if (collection == null) + throw new ArgumentNullException(nameof(collection)); + + var array = collection as T[] ?? collection.ToArray(); + var count = array.Length; + + if (count == 0) + _items = _emptyArray; + else + { + _items = new T[count]; + array.CopyTo(_items, 0); + Count = count; + } + } + + /// <summary> + /// Initializes a new instance of the <see cref="Deque{T}" /> class that is empty and has the specified initial + /// capacity. + /// </summary> + /// <param name="capacity">The number of elements that the new <see cref="Deque{T}" /> can initially store.</param> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="capacity" /> is less than 0.</exception> + /// <remarks> + /// <para> + /// The capacity of a <see cref="Deque{T}" /> is the number of elements that the <see cref="Deque{T}" /> can + /// hold. As elements are added to a <see cref="Deque{T}" />, <see cref="Capacity" /> is automatically increased by + /// <see cref="ResizeFunction" /> as required by reallocating the internal array. + /// </para> + /// <para> + /// If the size of the collection can be estimated, specifying the initial capacity eliminates the need to + /// perform a number of resizing operations while adding elements to the <see cref="Deque{T}" />. + /// </para> + /// <para> + /// The capacity can be decreased by calling the <see cref="TrimExcess" /> method or by setting the + /// <see cref="Capacity" /> property explicitly. Decreasing, or increasing, the capacity reallocates memory and + /// copies all the elements in the <see cref="Deque{T}" />. + /// </para> + /// <para>This constructor is an O(n) operation, where n is <paramref name="capacity" />.</para> + /// </remarks> + public Deque(int capacity) + { + if (capacity < 0) + throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity was less than zero."); + + _items = capacity == 0 ? _emptyArray : new T[capacity]; + } + + /// <summary> + /// Gets or sets the resize function used to calculate and set <see cref="Capacity" /> when a greater capacity is + /// required. + /// </summary> + /// <returns> + /// The <see cref="Func{T, TResult}" /> used to calculate and set <see cref="Capacity" /> when a greater capacity + /// is required. + /// </returns> + /// <remarks> + /// The default resize function is twice the <see cref="Capacity" />. Setting + /// <see cref="ResizeFunction" /> to <c>null</c> will set it back to the default. + /// </remarks> + public Func<int, int> ResizeFunction + { + get => _resizeFunction; + set => _resizeFunction = value ?? Deque.DefaultResizeFunction; + } + + /// <summary> + /// Gets or sets the total number of elements the internal data structure can hold without resizing. + /// </summary> + /// <returns>The number of elements that the <see cref="Deque{T}" /> can contain before resizing is required.</returns> + /// <exception cref="ArgumentOutOfRangeException"> + /// <see cref="Capacity" /> cannot be set to a value less than <see cref="Count" />. + /// </exception> + /// <remarks> + /// Changing <see cref="Capacity" /> reallocates memory and copies all the + /// elements in the <see cref="Deque{T}" />. + /// </remarks> + public int Capacity + { + get => _items.Length; + set + { + if (value < Count) + throw new ArgumentOutOfRangeException(nameof(value), "capacity was less than the current size."); + + if (value == Capacity) + return; + + if (value == 0) + { + _items = _emptyArray; + return; + } + + var newItems = new T[value]; + CopyTo(newItems); + + _frontArrayIndex = 0; + _items = null; + _items = newItems; + } + } + + /// <summary> + /// Gets or sets the element at the specified index. + /// </summary> + /// <param name="index">The zero-based index of the element to get or set.</param> + /// <returns>The element at the specified index.</returns> + /// <exception cref="ArgumentOutOfRangeException"> + /// Index was out of range. Must be non-negative and less than <see cref="Count" />. + /// </exception> + /// <remarks> + /// <para></para> + /// <para> + /// Use <c>0</c> for the <paramref name="index" /> to get or set the element at the beginning of the + /// <see cref="Deque{T}" />, and use <c><see cref="Count" /> - 1</c> for the <paramref name="index" /> to get the + /// element at the end of the <see cref="Deque{T}" />. + /// </para> + /// </remarks> + public T this[int index] + { + get + { + var arrayIndex = GetArrayIndex(index); + if (arrayIndex == -1) + { + throw new ArgumentOutOfRangeException(nameof(index), + "Index was out of range. Must be non-negative and less than the size of the collection."); + } + return _items[arrayIndex]; + } + set + { + var arrayIndex = GetArrayIndex(index); + if (arrayIndex == -1) + { + throw new ArgumentOutOfRangeException(nameof(index), + "Index was out of range. Must be non-negative and less than the size of the collection."); + } + _items[arrayIndex] = value; + } + } + + /// <summary> + /// Gets the number of elements contained in the <see cref="Deque{T}" />. + /// </summary> + /// <returns>The number of elements contained in the <see cref="Deque{T}" />.</returns> + public int Count { get; private set; } + + bool ICollection<T>.IsReadOnly => false; + + /// <summary> + /// Returns an enumerator that iterates through the <see cref="Deque{T}" />. + /// </summary> + /// <returns>An <see cref="IEnumerator{T}" /> that can be used to iterate through the <see cref="Deque{T}" />.</returns> + public IEnumerator<T> GetEnumerator() + { + if (Count == 0) + yield break; + + if (Count <= _items.Length - _frontArrayIndex) + { + for (var i = _frontArrayIndex; i < _frontArrayIndex + Count; i++) + yield return _items[i]; + } + else + { + for (var i = _frontArrayIndex; i < Capacity; i++) + yield return _items[i]; + for (var i = 0; i < (_frontArrayIndex + Count) % Capacity; i++) + yield return _items[i]; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + void ICollection<T>.Add(T item) + { + AddToBack(item); + } + + /// <summary> + /// Searches for the specified element and returns the zero-based index of the first occurrence within the entire + /// <see cref="Deque{T}" />. + /// </summary> + /// <param name="item"> + /// The element to locate in the <see cref="Deque{T}" />. The value can be <c>null</c> for reference + /// types. + /// </param> + /// <returns> + /// The zero-based index of the first occurrence of <paramref name="item" /> within the entire + /// <see cref="Deque{T}" />, if found; otherwise, <c>-1</c>. + /// </returns> + /// <remarks> + /// <para> + /// This method is an O(1) operation if <paramref name="item" /> is at the front or back of the + /// <see cref="Deque{T}" />; otherwise, this method is an O(n) operation where n is <see cref="Count" />. + /// </para> + /// </remarks> + public int IndexOf(T item) + { + var comparer = EqualityComparer<T>.Default; + + if (Get(0, out var checkFrontBackItem) && comparer.Equals(checkFrontBackItem, item)) + return 0; + + var backIndex = Count - 1; + + if (Get(backIndex, out checkFrontBackItem) && comparer.Equals(checkFrontBackItem, item)) + return backIndex; + + int index; + + if (Count <= _items.Length - _frontArrayIndex) + index = Array.IndexOf(_items, item, _frontArrayIndex, Count); + else + { + index = Array.IndexOf(_items, item, _frontArrayIndex, _items.Length - _frontArrayIndex); + if (index < 0) + index = Array.IndexOf(_items, item, 0, _frontArrayIndex + Count - _items.Length); + } + + var circularIndex = (index - _frontArrayIndex + _items.Length) % _items.Length; + return circularIndex; + } + + void IList<T>.Insert(int index, T item) + { + throw new NotImplementedException(); + } + + /// <summary> + /// Removes the first occurrence of a specific element from the <see cref="Deque{T}" />. + /// </summary> + /// <param name="item"> + /// The element to remove from the <see cref="Deque{T}" />. The value can be <c>null</c> for reference + /// types. + /// </param> + /// <returns> + /// <c>true</c> if <paramref name="item" /> was successfully removed; otherwise, false. This method also returns false + /// if <paramref name="item" /> is not found in the <see cref="Deque{T}" />. + /// </returns> + /// <remarks> + /// <para> + /// This method is an O(1) operation if <paramref name="item" /> is at the front or back of the + /// <see cref="Deque{T}" />; otherwise, this method is an O(n) operation where n is <see cref="Count" />. + /// </para> + /// </remarks> + public bool Remove(T item) + { + var index = IndexOf(item); + if (index == -1) + return false; + + RemoveAt(index); + return true; + } + + /// <summary> + /// Removes the element at the specified index of the <see cref="Deque{T}" />. + /// </summary> + /// <param name="index">The zero-based index of the element to remove.</param> + /// <exception cref="ArgumentOutOfRangeException"> + /// <para><paramref name="index" /> is less than 0.</para> + /// <para>-or-</para> + /// <para><paramref name="index" /> is equal to or greater than <see cref="Count" />.</para> + /// </exception> + public void RemoveAt(int index) + { + if (index < 0) + throw new ArgumentOutOfRangeException(nameof(index), index, "Index was less than zero."); + + if (index >= Count) + throw new ArgumentOutOfRangeException(nameof(index), index, "Index was equal or greater than TotalCount."); + + if (index == 0) + { + RemoveFromFront(); + } + else + { + if (index == Count - 1) + { + RemoveFromBack(); + } + else + { + if (index < Count / 2) + { + var arrayIndex = GetArrayIndex(index); + // shift the array from 0 to before the index to remove by 1 to the right + // the element to remove is replaced by the copy + Array.Copy(_items, 0, _items, 1, arrayIndex); + // the first element in the arrya is now either a duplicate or it's default value + // to be safe set it to it's default value regardless of circumstance + _items[0] = default(T); + // if we shifted the front element, adjust the front index + if (_frontArrayIndex < arrayIndex) + _frontArrayIndex = (_frontArrayIndex + 1) % _items.Length; + // decrement the count so the back index is calculated correctly + Count--; + } + else + { + var arrayIndex = GetArrayIndex(index); + // shift the array from the center of the array to before the index to remove by 1 to the right + // the element to remove is replaced by the copy + var arrayCenterIndex = _items.Length / 2; + Array.Copy(_items, arrayCenterIndex, _items, arrayCenterIndex + 1, _items.Length - 1 - arrayIndex); + // the last element in the array is now either a duplicate or it's default value + // to be safe set it to it's default value regardless of circumstance + _items[_items.Length - 1] = default(T); + // if we shifted the front element, adjust the front index + if (_frontArrayIndex < arrayIndex) + _frontArrayIndex = (_frontArrayIndex + 1) % _items.Length; + // decrement the count so the back index is calculated correctly + Count--; + } + } + } + } + + /// <summary> + /// Removes all elements from the <see cref="Deque{T}" />. + /// </summary> + /// <remarks> + /// <para> + /// <see cref="Count" /> is set to <c>0</c>, and references to other objects from elements of the collection are + /// also released. + /// </para> + /// <para> + /// <see cref="Capacity" /> remains unchanged. To reset the capacity of the <see cref="Deque{T}" />, call the + /// <see cref="TrimExcess" /> method or set the <see cref="Capacity" /> property explictly. Decreasing, or + /// increasing, the capacity reallocates memory and copies all the elements in the <see cref="Deque{T}" />. + /// Trimming an empty <see cref="Deque{T}" /> sets <see cref="Capacity" /> to the default capacity. + /// </para> + /// <para>This method is an O(n) operation, where n is <see cref="Count" />.</para> + /// </remarks> + public void Clear() + { + // allow the garbage collector to reclaim the references + + if (Count == 0) + return; + + if (Count > _items.Length - _frontArrayIndex) + { + Array.Clear(_items, _frontArrayIndex, _items.Length - _frontArrayIndex); + Array.Clear(_items, 0, _frontArrayIndex + Count - _items.Length); + } + else + Array.Clear(_items, _frontArrayIndex, Count); + Count = 0; + _frontArrayIndex = 0; + } + + /// <summary> + /// Determines whether an element is in the <see cref="Deque{T}" />. + /// </summary> + /// <param name="item"> + /// The element to locate in the <see cref="Deque{T}" />. The value can be <c>null</c> for reference + /// types. + /// </param> + /// <returns><c>true</c> if <paramref name="item" /> is found in the <see cref="Deque{T}" />; otherwise, false.</returns> + /// <remarks> + /// <para> + /// This method determines equality by using the default equality comparer, as defined by the object's + /// implementation + /// of the <see cref="IEquatable{T}.Equals(T)" /> method for the type of values in the list. + /// </para> + /// <para> + /// This method performs a linear search; therefore, this method is an O(n) operation, where n is + /// <see cref="Count" />. + /// </para> + /// </remarks> + public bool Contains(T item) + { + return this.Contains(item, EqualityComparer<T>.Default); + } + + /// <summary> + /// Copies the entire <see cref="Deque{T}" /> to a compatible one-dimensional array, starting at the specified index of + /// the target array. + /// </summary> + /// <param name="array"> + /// The one-dimensional <see cref="Array" /> that is the destination of the elements copied from + /// <see cref="Deque{T}" />. The <see cref="Array" /> must have zero-based indexing. + /// </param> + /// <param name="arrayIndex">The zero-based index in <paramref name="array" /> at which copying begins.</param> + /// <exception cref="ArgumentNullException"><paramref name="array" /> is null.</exception> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="arrayIndex" /> is less than 0.</exception> + /// <exception cref="ArgumentException"> + /// The number of elements in the source <see cref="Deque{T}" /> is greater than the + /// available space from <paramref name="arrayIndex" /> to the end of the destination <paramref name="array" />. + /// </exception> + /// <remarks> + /// This method uses <see cref="Array.Copy(Array, int, Array, int, int)" /> to copy the elements. The elements are + /// copied to the <see cref="Array" /> in the same order in which the enumerator iterates + /// through the <see cref="Deque{T}" />. This method is an O(n) operation, where n is <see cref="Count" />. + /// </remarks> + public void CopyTo(T[] array, int arrayIndex = 0) + { + if (array == null) + throw new ArgumentNullException(nameof(array)); + + if (array.Rank != 1) + throw new ArgumentException("Only single dimensional arrays are supported for the requested action."); + + if (arrayIndex < 0) + throw new ArgumentOutOfRangeException(nameof(arrayIndex), "Index was less than the array's lower bound."); + + if (arrayIndex >= array.Length) + { + throw new ArgumentOutOfRangeException(nameof(arrayIndex), + "Index was greater than the array's upper bound."); + } + + if (array.Length - arrayIndex < Count) + throw new ArgumentException("Destination array was not long enough."); + + if (Count == 0) + return; + + + try + { + var loopsAround = Count > _items.Length - _frontArrayIndex; + if (!loopsAround) + Array.Copy(_items, _frontArrayIndex, array, arrayIndex, Count); + else + { + Array.Copy(_items, _frontArrayIndex, array, arrayIndex, Capacity - _frontArrayIndex); + Array.Copy(_items, 0, array, arrayIndex + Capacity - _frontArrayIndex, + _frontArrayIndex + (Count - Capacity)); + } + } + catch (ArrayTypeMismatchException) + { + throw new ArgumentException( + "Target array type is not compatible with the type of items in the collection."); + } + } + + /// <summary> + /// Sets the capacity to the actual number of elements in the <see cref="Deque{T}" />, if that number is less than a + /// threshold value. + /// </summary> + /// <remarks> + /// <para> + /// This method can be used to minimize the <see cref="Deque{T}" />'s memory overhead if no new elements will be + /// added. The cost of reallocating and copying the elements of a <see cref="Deque{T}" /> can be considerable. + /// However, the <see cref="TrimExcess" /> method does nothing if the <see cref="Count" /> is more than 90% of + /// <see cref="Capacity" />. This avoids incurring a large reallocation cost for a relatively small gain. + /// </para> + /// <para> + /// If <see cref="Count" /> is more than 90% of <see cref="Capacity" />, this method is an O(1) operation; O(n) + /// otherwise, where n is <see cref="Count" />. + /// </para> + /// <para> + /// To reset a <see cref="Deque{T}" /> to its initial state, call the <see cref="Clear" /> method before calling + /// the <see cref="TrimExcess" /> method. Trimming an empty <see cref="Deque{T}" /> sets <see cref="Capacity" /> to + /// the default capacity. + /// </para> + /// <para>The capacity can also be set using the <see cref="Capacity" /> property.</para> + /// </remarks> + public void TrimExcess() + { + if (Count > (int)(_items.Length * 0.9)) + return; + Capacity = Count; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetArrayIndex(int index) + { + if ((index < 0) || (index >= Count)) + return -1; + return _items.Length != 0 ? (_frontArrayIndex + index) % _items.Length : 0; + } + + private void EnsureCapacity(int minimumCapacity) + { + if (_items.Length >= minimumCapacity) + return; + var newCapacity = _defaultCapacity; + if (_items.Length > 0) + newCapacity = _resizeFunction(_items.Length); + newCapacity = Math.Max(newCapacity, minimumCapacity); + Capacity = newCapacity; + } + + /// <summary> + /// Adds an element to the beginning of the <see cref="Deque{T}" />. + /// </summary> + /// <param name="item">The element to add to the <see cref="Deque{T}" />. The value can be <c>null</c>.</param> + /// <remarks> + /// <para> + /// As elements are added to a <see cref="Deque{T}" />, <see cref="Capacity" /> is automatically increased by + /// <see cref="ResizeFunction" /> as required by reallocating the internal circular array. + /// </para> + /// <para> + /// If <see cref="Count" /> is less than <see cref="Capacity" />, this method is an O(1) operation. Otherwise the + /// internal circular array needs to be resized to accommodate the new element and this method becomes an O(n) + /// operation, where n is <see cref="Count" />. + /// </para> + /// </remarks> + public void AddToFront(T item) + { + EnsureCapacity(Count + 1); + _frontArrayIndex = (_frontArrayIndex - 1 + _items.Length) % _items.Length; + _items[_frontArrayIndex] = item; + Count++; + } + + /// <summary> + /// Adds an element to the end of the <see cref="Deque{T}" />. + /// </summary> + /// <param name="item">The element to add to the <see cref="Deque{T}" />. The value can be <c>null</c>.</param> + /// <remarks> + /// <para> + /// As elements are added to a <see cref="Deque{T}" />, <see cref="Capacity" /> is automatically increased by + /// <see cref="ResizeFunction" /> as required by reallocating the internal circular array. + /// </para> + /// <para> + /// If <see cref="Count" /> is less than <see cref="Capacity" />, this method is an O(1) operation. Otherwise the + /// internal circular array needs to be resized to accommodate the new element and this method becomes an O(n) + /// operation, where n is <see cref="Count" />. + /// </para> + /// </remarks> + public void AddToBack(T item) + { + EnsureCapacity(Count + 1); + var index = (_frontArrayIndex + Count++) % _items.Length; + _items[index] = item; + } + + /// <summary> + /// Returns the element at the specified index of the <see cref="Deque{T}" />. + /// </summary> + /// <param name="index">The zero-based index of the element to get.</param> + /// <param name="item"> + /// When this method returns, contains the element at the specified index of the + /// <see cref="Deque{T}" />, if <paramref name="index" /> was non-negative and less than <see cref="Count" />; + /// otherwise, the default value for the type of the value parameter. This parameter is passed uninitialized. + /// </param> + /// <returns> + /// <c>true</c> if <paramref name="item" /> was successfully retrieved at <paramref name="index" /> from the of the + /// <see cref="Deque{T}" />; + /// otherwise, <c>false</c> if <paramref name="index" /> was non-negative and less than <see cref="Count" />. + /// </returns> + public bool Get(int index, out T item) + { + var arrayIndex = GetArrayIndex(index); + if (arrayIndex == -1) + { + item = default(T); + return false; + } + item = _items[arrayIndex]; + return true; + } + + /// <summary> + /// Returns the element at the beginning of the <see cref="Deque{T}" />. + /// </summary> + /// <param name="item"> + /// When this method returns, contains the element at the beginning of the <see cref="Deque{T}" />, if + /// <see cref="Deque{T}" /> was not empty; otherwise, the default value for the type of the value parameter. This + /// parameter is passed uninitialized. + /// </param> + /// <returns> + /// <c>true</c> if <paramref name="item" /> was successfully from the beginning of the <see cref="Deque{T}" />; + /// otherwise, <c>false</c> if the <see cref="Deque{T}" /> is empty. + /// </returns> + public bool GetFront(out T item) + { + return Get(0, out item); + } + + /// <summary> + /// Returns the element at the end of the <see cref="Deque{T}" />. + /// </summary> + /// <param name="item"> + /// When this method returns, contains the element at the end of the <see cref="Deque{T}" />, if + /// <see cref="Deque{T}" /> was not empty; otherwise, the default value for the type of the value parameter. This + /// parameter is passed uninitialized. + /// </param> + /// <returns> + /// <c>true</c> if <paramref name="item" /> was successfully from the end of the <see cref="Deque{T}" />; + /// otherwise, <c>false</c> if the <see cref="Deque{T}" /> is empty. + /// </returns> + public bool GetBack(out T item) + { + return Get(Count - 1, out item); + } + + /// <summary> + /// Removes the element at the beginning of the <see cref="Deque{T}" />. + /// </summary> + /// <param name="item"> + /// When this method returns, contains the element removed from the beginning of the + /// <see cref="Deque{T}" />, if the <see cref="Deque{T}" /> was not empty; otherwise, the default value for the type of + /// the value parameter. This parameter is passed uninitialized. + /// </param> + /// <returns> + /// <c>true</c> if <paramref name="item" /> was successfully removed from the beginning of the <see cref="Deque{T}" />; + /// otherwise, <c>false</c> if the <see cref="Deque{T}" /> is empty. + /// </returns> + /// <remarks> + /// <para> + /// This method is similar to the <see cref="GetFront" /> method, but <see cref="GetFront" /> does not + /// modify the <see cref="Deque{T}" />. + /// </para> + /// <para> + /// <c>null</c> can be added to the <see cref="Deque{T}" /> as a value. To distinguish between a null value and + /// the end of the <see cref="Deque{T}" />, check whether the return value of <see cref="RemoveFromFront(out T)" /> + /// is + /// <c>false</c> or + /// <see cref="Count" /> is <c>0</c>. + /// </para> + /// <para> + /// This method is an O(1) operation. + /// </para> + /// </remarks> + public bool RemoveFromFront(out T item) + { + if (Count == 0) + { + item = default(T); + return false; + } + + var index = _frontArrayIndex % _items.Length; + item = _items[index]; + _items[index] = default(T); + _frontArrayIndex = (_frontArrayIndex + 1) % _items.Length; + Count--; + return true; + } + + /// <summary> + /// Removes the element at the beginning of the <see cref="Deque{T}" />. + /// </summary> + /// <returns> + /// <c>true</c> if the element was successfully removed from the beginning of the <see cref="Deque{T}" />; + /// otherwise, <c>false</c> if the <see cref="Deque{T}" /> is empty. + /// </returns> + /// <remarks> + /// <para> + /// This method is similar to the <see cref="GetFront" /> method, but <see cref="GetFront" /> does not + /// modify the <see cref="Deque{T}" />. + /// </para> + /// <para> + /// <c>null</c> can be added to the <see cref="Deque{T}" /> as a value. To distinguish between a null value and + /// the end of the <see cref="Deque{T}" />, check whether the return value of <see cref="RemoveFromFront()" /> is + /// <c>false</c> or + /// <see cref="Count" /> is <c>0</c>. + /// </para> + /// <para> + /// This method is an O(1) operation. + /// </para> + /// </remarks> + public bool RemoveFromFront() + { + if (Count == 0) + return false; + + var index = _frontArrayIndex % _items.Length; + _items[index] = default(T); + _frontArrayIndex = (_frontArrayIndex + 1) % _items.Length; + Count--; + return true; + } + + /// <summary> + /// Removes the element at the end of the <see cref="Deque{T}" />. + /// </summary> + /// <param name="item"> + /// When this method returns, contains the element removed from the end of the + /// <see cref="Deque{T}" />, if the <see cref="Deque{T}" /> was not empty; otherwise, the default value for the type of + /// the value parameter. This parameter is passed uninitialized. + /// </param> + /// <returns> + /// <c>true</c> if <paramref name="item" /> was successfully removed from the end of the <see cref="Deque{T}" />; + /// otherwise, <c>false</c> if the <see cref="Deque{T}" /> is empty. + /// </returns> + /// <remarks> + /// <para> + /// This method is similar to the <see cref="GetBack" /> method, but <see cref="GetBack" /> does not + /// modify the <see cref="Deque{T}" />. + /// </para> + /// <para> + /// <c>null</c> can be added to the <see cref="Deque{T}" /> as a value. To distinguish between a null value and + /// the end of the <see cref="Deque{T}" />, check whether the return value of <see cref="RemoveFromBack(out T)" /> + /// is + /// <c>false</c> or + /// <see cref="Count" /> is <c>0</c>. + /// </para> + /// <para> + /// This method is an O(1) operation. + /// </para> + /// </remarks> + public bool RemoveFromBack(out T item) + { + if (Count == 0) + { + item = default(T); + return false; + } + + var circularBackIndex = (_frontArrayIndex + (Count - 1)) % _items.Length; + item = _items[circularBackIndex]; + _items[circularBackIndex] = default(T); + Count--; + return true; + } + + /// <summary> + /// Removes the element at the end of the <see cref="Deque{T}" />. + /// </summary> + /// <returns> + /// <c>true</c> if the element was successfully removed from the end of the <see cref="Deque{T}" />; + /// otherwise, <c>false</c> if the <see cref="Deque{T}" /> is empty. + /// </returns> + /// <remarks> + /// <para> + /// This method is similar to the <see cref="GetBack" /> method, but <see cref="GetBack" /> does not + /// modify the <see cref="Deque{T}" />. + /// </para> + /// <para> + /// <c>null</c> can be added to the <see cref="Deque{T}" /> as a value. To distinguish between a null value and + /// the end of the <see cref="Deque{T}" />, check whether the return value of <see cref="RemoveFromBack()" /> is + /// <c>false</c> or + /// <see cref="Count" /> is <c>0</c>. + /// </para> + /// <para> + /// This method is an O(1) operation. + /// </para> + /// </remarks> + public bool RemoveFromBack() + { + if (Count == 0) + return false; + + var circularBackIndex = (_frontArrayIndex + (Count - 1)) % _items.Length; + _items[circularBackIndex] = default(T); + Count--; + return true; + } + + /// <summary> + /// Removes and returns the last item. + /// </summary> + /// <returns>The item that was removed</returns> + public T Pop() + { + if (RemoveFromBack(out var item)) + return item; + + throw new InvalidOperationException(); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/DictionaryExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/DictionaryExtensions.cs new file mode 100644 index 0000000..ee581b7 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/DictionaryExtensions.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace MonoGame.Extended.Collections +{ + public static class DictionaryExtensions + { + public static TValue GetValueOrDefault<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey key) + { + return GetValueOrDefault(dictionary, key, default(TValue)); + } + + public static TValue GetValueOrDefault<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey key, TValue defaultValue) + { + TValue value; + return dictionary.TryGetValue(key, out value) ? value : defaultValue; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/IObservableCollection.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/IObservableCollection.cs new file mode 100644 index 0000000..bb4fe16 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/IObservableCollection.cs @@ -0,0 +1,26 @@ +using System; + +namespace MonoGame.Extended.Collections +{ + /// <summary>Interface for collections that can be observed</summary> + /// <typeparam name="T">Type of items managed in the collection</typeparam> + public interface IObservableCollection<T> + { + /// <summary>Raised when an item has been added to the collection</summary> + event EventHandler<ItemEventArgs<T>> ItemAdded; + + /// <summary>Raised when an item is removed from the collection</summary> + event EventHandler<ItemEventArgs<T>> ItemRemoved; + + /// <summary>Raised when the collection is about to be cleared</summary> + /// <remarks> + /// This could be covered by calling ItemRemoved for each item currently + /// contained in the collection, but it is often simpler and more efficient + /// to process the clearing of the entire collection as a special operation. + /// </remarks> + event EventHandler Clearing; + + /// <summary>Raised when the collection has been cleared of its items</summary> + event EventHandler Cleared; + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/IPoolable.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/IPoolable.cs new file mode 100644 index 0000000..cba24b8 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/IPoolable.cs @@ -0,0 +1,12 @@ +namespace MonoGame.Extended.Collections +{ + public delegate void ReturnToPoolDelegate(IPoolable poolable); + + public interface IPoolable + { + IPoolable NextNode { get; set; } + IPoolable PreviousNode { get; set; } + void Initialize(ReturnToPoolDelegate returnDelegate); + void Return(); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ItemEventArgs.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ItemEventArgs.cs new file mode 100644 index 0000000..0da853b --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ItemEventArgs.cs @@ -0,0 +1,20 @@ +using System; + +namespace MonoGame.Extended.Collections +{ + /// <summary> + /// Arguments class for collections wanting to hand over an item in an event + /// </summary> + public class ItemEventArgs<T> : EventArgs + { + /// <summary>Initializes a new event arguments supplier</summary> + /// <param name="item">Item to be supplied to the event handler</param> + public ItemEventArgs(T item) + { + Item = item; + } + + /// <summary>Obtains the collection item the event arguments are carrying</summary> + public T Item { get; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/KeyedCollection.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/KeyedCollection.cs new file mode 100644 index 0000000..e4ebeff --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/KeyedCollection.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace MonoGame.Extended.Collections +{ + public class KeyedCollection<TKey, TValue> : ICollection<TValue> + { + private readonly Func<TValue, TKey> _getKey; + private readonly Dictionary<TKey, TValue> _dictionary = new Dictionary<TKey, TValue>(); + + public KeyedCollection(Func<TValue, TKey> getKey) + { + _getKey = getKey; + } + + public TValue this[TKey key] => _dictionary[key]; + public ICollection<TKey> Keys => _dictionary.Keys; + public ICollection<TValue> Values => _dictionary.Values; + public int Count => _dictionary.Count; + public bool IsReadOnly => false; + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public IEnumerator<TValue> GetEnumerator() + { + return _dictionary.Values.GetEnumerator(); + } + + public void Add(TValue item) + { + _dictionary.Add(_getKey(item), item); + } + + public void Clear() + { + _dictionary.Clear(); + } + + public bool Contains(TValue item) + { + return _dictionary.ContainsKey(_getKey(item)); + } + + public void CopyTo(TValue[] array, int arrayIndex) + { + throw new NotSupportedException(); + } + + public bool Remove(TValue item) + { + return _dictionary.Remove(_getKey(item)); + } + + public bool ContainsKey(TKey key) + { + return _dictionary.ContainsKey(key); + } + + public bool TryGetValue(TKey key, out TValue value) + { + return _dictionary.TryGetValue(key, out value); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ListExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ListExtensions.cs new file mode 100644 index 0000000..9452a8e --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ListExtensions.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; + +namespace MonoGame.Extended.Collections +{ + public static class ListExtensions + { + public static IList<T> Shuffle<T>(this IList<T> list, Random random) + { + var n = list.Count; + while (n > 1) + { + n--; + var k = random.Next(n + 1); + var value = list[k]; + list[k] = list[n]; + list[n] = value; + } + return list; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ObjectPool.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ObjectPool.cs new file mode 100644 index 0000000..066fa96 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ObjectPool.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; + +namespace MonoGame.Extended.Collections +{ + public enum ObjectPoolIsFullPolicy + { + ReturnNull, + IncreaseSize, + KillExisting, + } + + public class ObjectPool<T> : IEnumerable<T> + where T : class, IPoolable + { + private readonly ReturnToPoolDelegate _returnToPoolDelegate; + + private readonly Deque<T> _freeItems; // circular buffer for O(1) operations + private T _headNode; // linked list for iteration + private T _tailNode; + + private readonly Func<T> _instantiationFunction; + + public ObjectPoolIsFullPolicy IsFullPolicy { get; } + public int Capacity { get; private set; } + public int TotalCount { get; private set; } + public int AvailableCount => _freeItems.Count; + public int InUseCount => TotalCount - AvailableCount; + + public event Action<T> ItemUsed; + public event Action<T> ItemReturned; + + public ObjectPool(Func<T> instantiationFunc, int capacity = 16, ObjectPoolIsFullPolicy isFullPolicy = ObjectPoolIsFullPolicy.ReturnNull) + { + if (instantiationFunc == null) + throw new ArgumentNullException(nameof(instantiationFunc)); + + _returnToPoolDelegate = Return; + + _instantiationFunction = instantiationFunc; + _freeItems = new Deque<T>(capacity); + IsFullPolicy = isFullPolicy; + + Capacity = capacity; + } + + public IEnumerator<T> GetEnumerator() + { + var node = _headNode; + while (node != null) + { + yield return node; + node = (T)node.NextNode; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public T New() + { + if (!_freeItems.RemoveFromFront(out var poolable)) + { + if (TotalCount <= Capacity) + { + poolable = CreateObject(); + } + else + { + switch (IsFullPolicy) + { + case ObjectPoolIsFullPolicy.ReturnNull: + return null; + case ObjectPoolIsFullPolicy.IncreaseSize: + Capacity++; + poolable = CreateObject(); + break; + case ObjectPoolIsFullPolicy.KillExisting: + if (_headNode == null) + return null; + var newHeadNode = (T)_headNode.NextNode; + _headNode.Return(); + _freeItems.RemoveFromBack(out poolable); + _headNode = newHeadNode; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + Use(poolable); + return poolable; + } + + private T CreateObject() + { + TotalCount++; + var item = _instantiationFunction(); + if (item == null) + throw new NullReferenceException($"The created pooled object of type '{typeof(T).Name}' is null."); + item.PreviousNode = _tailNode; + item.NextNode = null; + if (_headNode == null) + _headNode = item; + if (_tailNode != null) + _tailNode.NextNode = item; + _tailNode = item; + return item; + } + + private void Return(IPoolable item) + { + Debug.Assert(item != null); + + var poolable1 = (T) item; + + var previousNode = (T)item.PreviousNode; + var nextNode = (T)item.NextNode; + + if (previousNode != null) + previousNode.NextNode = nextNode; + if (nextNode != null) + nextNode.PreviousNode = previousNode; + + if (item == _headNode) + _headNode = nextNode; + if (item == _tailNode) + _tailNode = previousNode; + + if (_tailNode != null) + _tailNode.NextNode = null; + + _freeItems.AddToBack(poolable1); + + ItemReturned?.Invoke((T)item); + } + + private void Use(T item) + { + item.Initialize(_returnToPoolDelegate); + item.NextNode = null; + if (item != _tailNode) + { + item.PreviousNode = _tailNode; + _tailNode.NextNode = item; + _tailNode = item; + } + + ItemUsed?.Invoke(item); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ObservableCollection.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ObservableCollection.cs new file mode 100644 index 0000000..de827df --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/ObservableCollection.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace MonoGame.Extended.Collections +{ + public class ObservableCollection<T> : Collection<T>, IObservableCollection<T> + { + /// <summary> + /// Initializes a new instance of the ObservableCollection class that is empty. + /// </summary> + public ObservableCollection() + { + } + + /// <summary> + /// Initializes a new instance of the ObservableCollection class as a wrapper + /// for the specified list. + /// </summary> + /// <param name="list">The list that is wrapped by the new collection.</param> + /// <exception cref="System.ArgumentNullException"> + /// List is null. + /// </exception> + public ObservableCollection(IList<T> list) : base(list) + { + } + + /// <summary>Raised when an item has been added to the collection</summary> + public event EventHandler<ItemEventArgs<T>> ItemAdded; + + /// <summary>Raised when an item is removed from the collection</summary> + public event EventHandler<ItemEventArgs<T>> ItemRemoved; + + /// <summary>Raised when the collection is about to be cleared</summary> + /// <remarks> + /// This could be covered by calling ItemRemoved for each item currently + /// contained in the collection, but it is often simpler and more efficient + /// to process the clearing of the entire collection as a special operation. + /// </remarks> + public event EventHandler Clearing; + + /// <summary>Raised when the collection has been cleared</summary> + public event EventHandler Cleared; + + /// <summary>Removes all elements from the Collection</summary> + protected override void ClearItems() + { + OnClearing(); + base.ClearItems(); + OnCleared(); + } + + /// <summary> + /// Inserts an element into the ObservableCollection at the specified index + /// </summary> + /// <param name="index"> + /// The object to insert. The value can be null for reference types. + /// </param> + /// <param name="item">The zero-based index at which item should be inserted</param> + protected override void InsertItem(int index, T item) + { + base.InsertItem(index, item); + OnAdded(item); + } + + /// <summary> + /// Removes the element at the specified index of the ObservableCollection + /// </summary> + /// <param name="index">The zero-based index of the element to remove</param> + protected override void RemoveItem(int index) + { + var item = base[index]; + base.RemoveItem(index); + OnRemoved(item); + } + + /// <summary>Replaces the element at the specified index</summary> + /// <param name="index"> + /// The new value for the element at the specified index. The value can be null + /// for reference types + /// </param> + /// <param name="item">The zero-based index of the element to replace</param> + protected override void SetItem(int index, T item) + { + var oldItem = base[index]; + base.SetItem(index, item); + OnRemoved(oldItem); + OnAdded(item); + } + + /// <summary>Fires the 'ItemAdded' event</summary> + /// <param name="item">Item that has been added to the collection</param> + protected virtual void OnAdded(T item) + { + ItemAdded?.Invoke(this, new ItemEventArgs<T>(item)); + } + + /// <summary>Fires the 'ItemRemoved' event</summary> + /// <param name="item">Item that has been removed from the collection</param> + protected virtual void OnRemoved(T item) + { + ItemRemoved?.Invoke(this, new ItemEventArgs<T>(item)); + } + + /// <summary>Fires the 'Clearing' event</summary> + protected virtual void OnClearing() + { + Clearing?.Invoke(this, EventArgs.Empty); + } + + /// <summary>Fires the 'Cleared' event</summary> + protected virtual void OnCleared() + { + Cleared?.Invoke(this, EventArgs.Empty); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Pool.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Pool.cs new file mode 100644 index 0000000..7c1546f --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Collections/Pool.cs @@ -0,0 +1,51 @@ +using System; + +namespace MonoGame.Extended.Collections +{ + public class Pool<T> + where T : class + { + private readonly Func<T> _createItem; + private readonly Action<T> _resetItem; + private readonly Deque<T> _freeItems; + private readonly int _maximum; + + public Pool(Func<T> createItem, Action<T> resetItem, int capacity = 16, int maximum = int.MaxValue) + { + _createItem = createItem; + _resetItem = resetItem; + _maximum = maximum; + _freeItems = new Deque<T>(capacity); + } + + public Pool(Func<T> createItem, int capacity = 16, int maximum = int.MaxValue) + : this(createItem, _ => { }, capacity, maximum) + { + } + + public int AvailableCount => _freeItems.Count; + + public T Obtain() + { + if (_freeItems.Count > 0) + return _freeItems.Pop(); + + return _createItem(); + } + + public void Free(T item) + { + if (item == null) throw new ArgumentNullException(nameof(item)); + + if (_freeItems.Count < _maximum) + _freeItems.AddToBack(item); + + _resetItem(item); + } + + public void Clear() + { + _freeItems.Clear(); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/ColorExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ColorExtensions.cs new file mode 100644 index 0000000..452ad37 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ColorExtensions.cs @@ -0,0 +1,85 @@ +using System; +using System.Globalization; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + /// <summary> + /// Provides additional methods for working with color + /// </summary> + public static class ColorExtensions + { + public static Color FromHex(string value) + { + var r = int.Parse(value.Substring(1, 2), NumberStyles.HexNumber); + var g = int.Parse(value.Substring(3, 2), NumberStyles.HexNumber); + var b = int.Parse(value.Substring(5, 2), NumberStyles.HexNumber); + var a = value.Length > 7 ? int.Parse(value.Substring(7, 2), NumberStyles.HexNumber) : 255; + return new Color(r, g, b, a); + } + + public static Color ToRgb(this HslColor c) + { + var h = c.H; + var s = c.S; + var l = c.L; + + if (s == 0f) + return new Color(l, l, l); + + h = h/360f; + var max = l < 0.5f ? l*(1 + s) : l + s - l*s; + var min = 2f*l - max; + + return new Color( + ComponentFromHue(min, max, h + 1f/3f), + ComponentFromHue(min, max, h), + ComponentFromHue(min, max, h - 1f/3f)); + } + + private static float ComponentFromHue(float m1, float m2, float h) + { + h = (h + 1f)%1f; + if (h*6f < 1) + return m1 + (m2 - m1)*6f*h; + if (h*2 < 1) + return m2; + if (h*3 < 2) + return m1 + (m2 - m1)*(2f/3f - h)*6f; + return m1; + } + + public static HslColor ToHsl(this Color c) + { + var r = c.R/255f; + var b = c.B/255f; + var g = c.G/255f; + + var max = Math.Max(Math.Max(r, g), b); + var min = Math.Min(Math.Min(r, g), b); + var chroma = max - min; + var sum = max + min; + + var l = sum*0.5f; + + if (chroma == 0) + return new HslColor(0f, 0f, l); + + float h; + + if (r == max) + h = (60*(g - b)/chroma + 360)%360; + else + { + if (g == max) + h = 60*(b - r)/chroma + 120f; + else + h = 60*(r - g)/chroma + 240f; + } + + var s = l <= 0.5f ? chroma/sum : chroma/(2f - sum); + + return new HslColor(h, s, l); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/ColorHelper.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ColorHelper.cs new file mode 100644 index 0000000..370af3d --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ColorHelper.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + public static class ColorHelper + { + //http://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion + public static Color FromHsl(float hue, float saturation, float lightness) + { + var hsl = new Vector4(hue, saturation, lightness, 1); + var color = new Vector4(0, 0, 0, hsl.W); + + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (hsl.Y == 0.0f) + color.X = color.Y = color.Z = hsl.Z; + else + { + var q = hsl.Z < 0.5f ? hsl.Z*(1.0f + hsl.Y) : hsl.Z + hsl.Y - hsl.Z*hsl.Y; + var p = 2.0f*hsl.Z - q; + + color.X = HueToRgb(p, q, hsl.X + 1.0f/3.0f); + color.Y = HueToRgb(p, q, hsl.X); + color.Z = HueToRgb(p, q, hsl.X - 1.0f/3.0f); + } + + return new Color(color); + } + + private static float HueToRgb(float p, float q, float t) + { + if (t < 0.0f) t += 1.0f; + if (t > 1.0f) t -= 1.0f; + if (t < 1.0f/6.0f) return p + (q - p)*6.0f*t; + if (t < 1.0f/2.0f) return q; + if (t < 2.0f/3.0f) return p + (q - p)*(2.0f/3.0f - t)*6.0f; + return p; + } + + public static Color FromHex(string value) + { + if (string.IsNullOrEmpty(value)) + return Color.Transparent; + var startIndex = 0; + if (value.StartsWith("#")) + startIndex++; + var r = int.Parse(value.Substring(startIndex, 2), NumberStyles.HexNumber); + var g = int.Parse(value.Substring(startIndex + 2, 2), NumberStyles.HexNumber); + var b = int.Parse(value.Substring(startIndex + 4, 2), NumberStyles.HexNumber); + var a = value.Length > 6 + startIndex ? int.Parse(value.Substring(startIndex + 6, 2), NumberStyles.HexNumber) : 255; + + return new Color(r, g, b, a); + } + + public static string ToHex(Color color) + { + var rx = $"{color.R:x2}"; + var gx = $"{color.G:x2}"; + var bx = $"{color.B:x2}"; + var ax = $"{color.A:x2}"; + return $"#{rx}{gx}{bx}{ax}"; + } + + private static readonly Dictionary<string, Color> _colorsByName = typeof(Color) + .GetRuntimeProperties() + .Where(p => p.PropertyType == typeof(Color)) + .ToDictionary(p => p.Name, p => (Color) p.GetValue(null), StringComparer.OrdinalIgnoreCase); + + public static Color FromName(string name) + { + Color color; + + if(_colorsByName.TryGetValue(name, out color)) + return color; + + throw new InvalidOperationException($"{name} is not a valid color"); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Content/ContentManagerExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Content/ContentManagerExtensions.cs new file mode 100644 index 0000000..69e7730 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Content/ContentManagerExtensions.cs @@ -0,0 +1,51 @@ +using System.IO; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Content +{ + public interface IContentLoader<out T> + { + T Load(ContentManager contentManager, string path); + } + + public interface IContentLoader + { + T Load<T>(ContentManager contentManager, string path); + } + + public static class ContentManagerExtensions + { + public const string DirectorySeparatorChar = "/"; + + public static Stream OpenStream(this ContentManager contentManager, string path) + { + return TitleContainer.OpenStream(contentManager.RootDirectory + DirectorySeparatorChar + path); + } + + public static GraphicsDevice GetGraphicsDevice(this ContentManager contentManager) + { + // http://konaju.com/?p=21 + var serviceProvider = contentManager.ServiceProvider; + var graphicsDeviceService = (IGraphicsDeviceService) serviceProvider.GetService(typeof(IGraphicsDeviceService)); + return graphicsDeviceService.GraphicsDevice; + } + + /// <summary> + /// Loads the content using a custom content loader. + /// </summary> + public static T Load<T>(this ContentManager contentManager, string path, IContentLoader contentLoader) + { + return contentLoader.Load<T>(contentManager, path); + } + + /// <summary> + /// Loads the content using a custom content loader. + /// </summary> + public static T Load<T>(this ContentManager contentManager, string path, IContentLoader<T> contentLoader) + { + return contentLoader.Load(contentManager, path); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Content/ContentReaderExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Content/ContentReaderExtensions.cs new file mode 100644 index 0000000..f4c51a4 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Content/ContentReaderExtensions.cs @@ -0,0 +1,44 @@ +using System; +using System.IO; +using System.Reflection; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Content +{ + public static class ContentReaderExtensions + { + private static readonly FieldInfo _contentReaderGraphicsDeviceFieldInfo = typeof(ContentReader).GetTypeInfo().GetDeclaredField("graphicsDevice"); + + public static GraphicsDevice GetGraphicsDevice(this ContentReader contentReader) + { + return (GraphicsDevice)_contentReaderGraphicsDeviceFieldInfo.GetValue(contentReader); + } + + public static string RemoveExtension(string path) + { + return Path.ChangeExtension(path, null).TrimEnd('.'); + } + + public static string GetRelativeAssetName(this ContentReader contentReader, string relativeName) + { + var assetDirectory = Path.GetDirectoryName(contentReader.AssetName); + var assetName = RemoveExtension(Path.Combine(assetDirectory, relativeName).Replace('\\', '/')); + + return ShortenRelativePath(assetName); + } + + public static string ShortenRelativePath(string relativePath) + { + var ellipseIndex = relativePath.IndexOf("/../", StringComparison.Ordinal); + while (ellipseIndex != -1) + { + var lastDirectoryIndex = relativePath.LastIndexOf('/', ellipseIndex - 1) + 1; + relativePath = relativePath.Remove(lastDirectoryIndex, ellipseIndex + 4 - lastDirectoryIndex); + ellipseIndex = relativePath.IndexOf("/../", StringComparison.Ordinal); + } + + return relativePath; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/FramesPerSecondCounter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/FramesPerSecondCounter.cs new file mode 100644 index 0000000..432f1ac --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/FramesPerSecondCounter.cs @@ -0,0 +1,34 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + public class FramesPerSecondCounter : IUpdate + { + private static readonly TimeSpan _oneSecondTimeSpan = new TimeSpan(0, 0, 1); + private int _framesCounter; + private TimeSpan _timer = _oneSecondTimeSpan; + + public FramesPerSecondCounter() + { + } + + public int FramesPerSecond { get; private set; } + + public void Update(GameTime gameTime) + { + _timer += gameTime.ElapsedGameTime; + if (_timer <= _oneSecondTimeSpan) + return; + + FramesPerSecond = _framesCounter; + _framesCounter = 0; + _timer -= _oneSecondTimeSpan; + } + + public void Draw(GameTime gameTime) + { + _framesCounter++; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/FramesPerSecondCounterComponent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/FramesPerSecondCounterComponent.cs new file mode 100644 index 0000000..ef36072 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/FramesPerSecondCounterComponent.cs @@ -0,0 +1,27 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + public class FramesPerSecondCounterComponent : DrawableGameComponent + { + private readonly FramesPerSecondCounter _fpsCounter; + + public FramesPerSecondCounterComponent(Game game) + : base(game) + { + _fpsCounter = new FramesPerSecondCounter(); + } + + public int FramesPerSecond => _fpsCounter.FramesPerSecond; + + public override void Update(GameTime gameTime) + { + _fpsCounter.Update(gameTime); + } + + public override void Draw(GameTime gameTime) + { + _fpsCounter.Draw(gameTime); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/GameComponentCollectionExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/GameComponentCollectionExtensions.cs new file mode 100644 index 0000000..c470a12 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/GameComponentCollectionExtensions.cs @@ -0,0 +1,24 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + public static class GameComponentCollectionExtensions + { + public static T Add<T>(this GameComponentCollection collection) + where T : IGameComponent, new() + { + var gameComponent = new T(); + collection.Add(gameComponent); + return gameComponent; + } + + public static T Add<T>(this GameComponentCollection collection, Func<T> createGameComponent) + where T : IGameComponent + { + var gameComponent = createGameComponent(); + collection.Add(gameComponent); + return gameComponent; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/GameTimeExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/GameTimeExtensions.cs new file mode 100644 index 0000000..01c8790 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/GameTimeExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + public static class GameTimeExtensions + { + public static float GetElapsedSeconds(this GameTime gameTime) + { + return (float) gameTime.ElapsedGameTime.TotalSeconds; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/HslColor.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/HslColor.cs new file mode 100644 index 0000000..628073d --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/HslColor.cs @@ -0,0 +1,270 @@ +using System; +using System.Globalization; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + /// <summary> + /// An immutable data structure representing a 24bit color composed of separate hue, saturation and lightness channels. + /// </summary> + //[Serializable] + public struct HslColor : IEquatable<HslColor>, IComparable<HslColor> + { + /// <summary> + /// Gets the value of the hue channel in degrees. + /// </summary> + public readonly float H; + + /// <summary> + /// Gets the value of the saturation channel. + /// </summary> + public readonly float S; + + /// <summary> + /// Gets the value of the lightness channel. + /// </summary> + public readonly float L; + + private static float NormalizeHue(float h) + { + if (h < 0) return h + 360*((int) (h/360) + 1); + return h%360; + } + + /// <summary> + /// Initializes a new instance of the <see cref="HslColor" /> structure. + /// </summary> + /// <param name="h">The value of the hue channel.</param> + /// <param name="s">The value of the saturation channel.</param> + /// <param name="l">The value of the lightness channel.</param> + public HslColor(float h, float s, float l) : this() + { + // normalize the hue + H = NormalizeHue(h); + S = MathHelper.Clamp(s, 0f, 1f); + L = MathHelper.Clamp(l, 0f, 1f); + } + + /// <summary> + /// Copies the individual channels of the color to the specified memory location. + /// </summary> + /// <param name="destination">The memory location to copy the axis to.</param> + public void CopyTo(out HslColor destination) + { + destination = new HslColor(H, S, L); + } + + /// <summary> + /// Destructures the color, exposing the individual channels. + /// </summary> + public void Destructure(out float h, out float s, out float l) + { + h = H; + s = S; + l = L; + } + + /// <summary> + /// Exposes the individual channels of the color to the specified matching function. + /// </summary> + /// <param name="callback">The function which matches the individual channels of the color.</param> + /// <exception cref="T:System.ArgumentNullException"> + /// Thrown if the value passed to the <paramref name="callback" /> parameter is <c>null</c>. + /// </exception> + public void Match(Action<float, float, float> callback) + { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + callback(H, S, L); + } + + /// <summary> + /// Exposes the individual channels of the color to the specified mapping function and returns the + /// result; + /// </summary> + /// <typeparam name="T">The type being mapped to.</typeparam> + /// <param name="map"> + /// A function which maps the color channels to an instance of <typeparamref name="T" />. + /// </param> + /// <returns> + /// The result of the <paramref name="map" /> function when passed the individual X and Y components. + /// </returns> + /// <exception cref="T:System.ArgumentNullException"> + /// Thrown if the value passed to the <paramref name="map" /> parameter is <c>null</c>. + /// </exception> + public T Map<T>(Func<float, float, float, T> map) + { + if (map == null) + throw new ArgumentNullException(nameof(map)); + + return map(H, S, L); + } + + public static HslColor operator +(HslColor a, HslColor b) + { + return new HslColor(a.H + b.H, a.S + b.S, a.L + b.L); + } + + public static implicit operator HslColor(string value) + { + return Parse(value); + } + + public int CompareTo(HslColor other) + { + // ReSharper disable ImpureMethodCallOnReadonlyValueField + return H.CompareTo(other.H)*100 + S.CompareTo(other.S)*10 + L.CompareTo(L); + // ReSharper restore ImpureMethodCallOnReadonlyValueField + } + + /// <summary> + /// Determines whether the specified <see cref="System.Object" /> is equal to this instance. + /// </summary> + /// <param name="obj">The <see cref="System.Object" /> to compare with this instance.</param> + /// <returns> + /// <c>true</c> if the specified <see cref="System.Object" /> is equal to this instance; otherwise, <c>false</c>. + /// </returns> + public override bool Equals(object obj) + { + if (obj is HslColor) + return Equals((HslColor) obj); + + return base.Equals(obj); + } + + /// <summary> + /// Determines whether the specified <see cref="HslColor" /> is equal to this instance. + /// </summary> + /// <param name="value">The <see cref="HslColor" /> to compare with this instance.</param> + /// <returns> + /// <c>true</c> if the specified <see cref="HslColor" /> is equal to this instance; otherwise, <c>false</c>. + /// </returns> + public bool Equals(HslColor value) + { + // ReSharper disable ImpureMethodCallOnReadonlyValueField + return H.Equals(value.H) && S.Equals(value.S) && L.Equals(value.L); + // ReSharper restore ImpureMethodCallOnReadonlyValueField + } + + /// <summary> + /// Returns a hash code for this instance. + /// </summary> + /// <returns> + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// </returns> + public override int GetHashCode() + { + return H.GetHashCode() ^ + S.GetHashCode() ^ + L.GetHashCode(); + } + + /// <summary> + /// Returns a <see cref="System.String" /> that represents this instance. + /// </summary> + /// <returns> + /// A <see cref="System.String" /> that represents this instance. + /// </returns> + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "H:{0:N1}° S:{1:N1} L:{2:N1}", + H, 100*S, 100*L); + } + + public static HslColor Parse(string s) + { + var hsl = s.Split(','); + var hue = float.Parse(hsl[0].TrimEnd('°'), CultureInfo.InvariantCulture.NumberFormat); + var sat = float.Parse(hsl[1], CultureInfo.InvariantCulture.NumberFormat); + var lig = float.Parse(hsl[2], CultureInfo.InvariantCulture.NumberFormat); + + return new HslColor(hue, sat, lig); + } + + /// <summary> + /// Implements the operator ==. + /// </summary> + /// <param name="x">The lvalue.</param> + /// <param name="y">The rvalue.</param> + /// <returns> + /// <c>true</c> if the lvalue <see cref="HslColor" /> is equal to the rvalue; otherwise, <c>false</c>. + /// </returns> + public static bool operator ==(HslColor x, HslColor y) + { + return x.Equals(y); + } + + /// <summary> + /// Implements the operator !=. + /// </summary> + /// <param name="x">The lvalue.</param> + /// <param name="y">The rvalue.</param> + /// <returns> + /// <c>true</c> if the lvalue <see cref="HslColor" /> is not equal to the rvalue; otherwise, <c>false</c>. + /// </returns> + public static bool operator !=(HslColor x, HslColor y) + { + return !x.Equals(y); + } + + public static HslColor operator -(HslColor a, HslColor b) + { + return new HslColor(a.H - b.H, a.S - b.S, a.L - b.L); + } + + public static HslColor Lerp(HslColor c1, HslColor c2, float t) + { + // loop around if c2.H < c1.H + var h2 = c2.H >= c1.H ? c2.H : c2.H + 360; + return new HslColor( + c1.H + t*(h2 - c1.H), + c1.S + t*(c2.S - c1.S), + c1.L + t*(c2.L - c1.L)); + } + + public static HslColor FromRgb(Color color) + { + // derived from http://www.geekymonkey.com/Programming/CSharp/RGB2HSL_HSL2RGB.htm + var r = color.R / 255f; + var g = color.G / 255f; + var b = color.B / 255f; + var h = 0f; // default to black + var s = 0f; + var l = 0f; + var v = Math.Max(r, g); + v = Math.Max(v, b); + + var m = Math.Min(r, g); + m = Math.Min(m, b); + l = (m + v) / 2.0f; + + if (l <= 0.0) + return new HslColor(h, s, l); + + var vm = v - m; + s = vm; + + if (s > 0.0) + s /= l <= 0.5f ? v + m : 2.0f - v - m; + else + return new HslColor(h, s, l); + + var r2 = (v - r) / vm; + var g2 = (v - g) / vm; + var b2 = (v - b) / vm; + + if (Math.Abs(r - v) < float.Epsilon) + h = Math.Abs(g - m) < float.Epsilon ? 5.0f + b2 : 1.0f - g2; + else if (Math.Abs(g - v) < float.Epsilon) + h = Math.Abs(b - m) < float.Epsilon ? 1.0f + r2 : 3.0f - b2; + else + h = Math.Abs(r - m) < float.Epsilon ? 3.0f + g2 : 5.0f - r2; + + h *= 60; + h = NormalizeHue(h); + + return new HslColor(h, s, l); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/IColorable.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IColorable.cs new file mode 100644 index 0000000..ad3c35c --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IColorable.cs @@ -0,0 +1,9 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + public interface IColorable + { + Color Color { get; set; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/IEquatableByRef.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IEquatableByRef.cs new file mode 100644 index 0000000..678e510 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IEquatableByRef.cs @@ -0,0 +1,21 @@ +namespace MonoGame.Extended +{ + /// <summary> + /// Defines a generalized method that a value type or class implements to create a type-specific method for + /// determining equality of instances by reference. + /// </summary> + /// <typeparam name="T">The type of values or objects to compare.</typeparam> + public interface IEquatableByRef<T> + { + /// <summary> + /// Indicates whether the current value or object is equal to another value or object of the same type by + /// reference. + /// </summary> + /// <returns> + /// <c>true</c> if the current value or object is equal to the <paramref name="other" /> parameter; otherwise, + /// <c>false</c>. + /// </returns> + /// <param name="other">A value or object to compare with this value or object.</param> + bool Equals(ref T other); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/IMovable.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IMovable.cs new file mode 100644 index 0000000..a851088 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IMovable.cs @@ -0,0 +1,9 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + public interface IMovable + { + Vector2 Position { get; set; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/IRectangular.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IRectangular.cs new file mode 100644 index 0000000..a8b1eab --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IRectangular.cs @@ -0,0 +1,14 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + public interface IRectangular + { + Rectangle BoundingRectangle { get; } + } + + public interface IRectangularF + { + RectangleF BoundingRectangle { get; } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/IRotatable.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IRotatable.cs new file mode 100644 index 0000000..7a7d622 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IRotatable.cs @@ -0,0 +1,7 @@ +namespace MonoGame.Extended +{ + public interface IRotatable + { + float Rotation { get; set; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/IScalable.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IScalable.cs new file mode 100644 index 0000000..70a527b --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IScalable.cs @@ -0,0 +1,9 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + public interface IScalable + { + Vector2 Scale { get; set; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/ISizable.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ISizable.cs new file mode 100644 index 0000000..5b55058 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ISizable.cs @@ -0,0 +1,8 @@ + +namespace MonoGame.Extended +{ + public interface ISizable + { + Size2 Size { get; set; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/IUpdate.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IUpdate.cs new file mode 100644 index 0000000..6240e04 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/IUpdate.cs @@ -0,0 +1,9 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + public interface IUpdate + { + void Update(GameTime gameTime); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Angle.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Angle.cs new file mode 100644 index 0000000..19f4855 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Angle.cs @@ -0,0 +1,224 @@ +/* +* ----------------------------------------------------------------------------- +* Original code from SlimMath project. http://code.google.com/p/slimmath/ +* Greetings to SlimDX Group. Original code published with the following license: +* ----------------------------------------------------------------------------- +* +* Copyright (c) 2007-2010 SlimDX Group +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +* THE SOFTWARE. +*/ + +using System; +using System.Diagnostics; +using System.Runtime.Serialization; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + public enum AngleType : byte + { + Radian = 0, + Degree, + Revolution, //or Turn / cycle + Gradian // or Gon + } + + [DataContract] + [DebuggerDisplay("{ToString(),nq}")] + public struct Angle : IComparable<Angle>, IEquatable<Angle> + { + private const float _tau = (float) (Math.PI*2.0); + private const float _tauInv = (float) (0.5/Math.PI); + private const float _degreeRadian = (float) (Math.PI/180.0); + private const float _radianDegree = (float) (180.0/Math.PI); + private const float _gradianRadian = (float) (Math.PI/200.0); + private const float _radianGradian = (float) (200.0/Math.PI); + + [DataMember] + public float Radians { get; set; } + + public float Degrees + { + get => Radians*_radianDegree; + set => Radians = value*_degreeRadian; + } + + public float Gradians + { + get => Radians*_radianGradian; + set => Radians = value*_gradianRadian; + } + + public float Revolutions + { + get => Radians*_tauInv; + set => Radians = value*_tau; + } + + public Angle(float value, AngleType angleType = AngleType.Radian) + { + switch (angleType) + { + default: + Radians = 0f; + break; + case AngleType.Radian: + Radians = value; + break; + case AngleType.Degree: + Radians = value*_degreeRadian; + break; + case AngleType.Revolution: + Radians = value*_tau; + break; + case AngleType.Gradian: + Radians = value*_gradianRadian; + break; + } + } + + public float GetValue(AngleType angleType) + { + switch (angleType) + { + default: + return 0f; + case AngleType.Radian: + return Radians; + case AngleType.Degree: + return Degrees; + case AngleType.Revolution: + return Revolutions; + case AngleType.Gradian: + return Gradians; + } + } + + public void Wrap() + { + var angle = Radians%_tau; + if (angle <= Math.PI) angle += _tau; + if (angle > Math.PI) angle -= _tau; + Radians = angle; + } + + public void WrapPositive() + { + Radians %= _tau; + if (Radians < 0d) Radians += _tau; + Radians = Radians; + } + + public static Angle FromVector(Vector2 vector) + { + return new Angle((float) Math.Atan2(-vector.Y, vector.X)); + } + + public Vector2 ToUnitVector() => ToVector(1); + + public Vector2 ToVector(float length) + { + return new Vector2(length*(float) Math.Cos(Radians), -length*(float) Math.Sin(Radians)); + } + + public static bool IsBetween(Angle value, Angle min, Angle end) + { + return end < min + ? (value >= min) || (value <= end) + : (value >= min) && (value <= end); + } + + public int CompareTo(Angle other) + { + WrapPositive(); + other.WrapPositive(); + return Radians.CompareTo(other.Radians); + } + + public bool Equals(Angle other) + { + WrapPositive(); + other.WrapPositive(); + return Radians.Equals(other.Radians); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + return obj is Angle a && Equals(a); + } + + public override int GetHashCode() + { + // ReSharper disable once NonReadonlyMemberInGetHashCode + return Radians.GetHashCode(); + } + + public static implicit operator float(Angle angle) + { + return angle.Radians; + } + + public static explicit operator Angle(float angle) + { + return new Angle(angle); + } + + public static Angle operator -(Angle angle) + { + return new Angle(-angle.Radians); + } + + public static bool operator ==(Angle a, Angle b) + { + return a.Equals(b); + } + + public static bool operator !=(Angle a, Angle b) + { + return !a.Equals(b); + } + + public static Angle operator -(Angle left, Angle right) + { + return new Angle(left.Radians - right.Radians); + } + + public static Angle operator *(Angle left, float right) + { + return new Angle(left.Radians*right); + } + + public static Angle operator *(float left, Angle right) + { + return new Angle(right.Radians*left); + } + + public static Angle operator +(Angle left, Angle right) + { + return new Angle(left.Radians + right.Radians); + } + + public override string ToString() + { + return $"{Radians} Radians"; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/BoundingRectangle.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/BoundingRectangle.cs new file mode 100644 index 0000000..324ebb2 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/BoundingRectangle.cs @@ -0,0 +1,579 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.2; Bounding Volumes - Axis-aligned Bounding Boxes (AABBs). pg 77 + + /// <summary> + /// An axis-aligned, four sided, two dimensional box defined by a centre <see cref="Point2" /> and a radii + /// <see cref="Vector2" />. + /// </summary> + /// <remarks> + /// <para> + /// An <see cref="BoundingRectangle" /> is categorized by having its faces oriented in such a way that its + /// face normals are at all times parallel with the axes of the given coordinate system. + /// </para> + /// <para> + /// The <see cref="BoundingRectangle" /> of a rotated <see cref="BoundingRectangle" /> will be equivalent or larger + /// in size + /// than the original depending on the angle of rotation. + /// </para> + /// </remarks> + /// <seealso cref="IEquatable{T}" /> + /// <seealso cref="IEquatableByRef{T}" /> + [DebuggerDisplay("{" + nameof(DebugDisplayString) + ",nq}")] + public struct BoundingRectangle : IEquatable<BoundingRectangle>, + IEquatableByRef<BoundingRectangle> + { + /// <summary> + /// The <see cref="BoundingRectangle" /> with <see cref="Center" /> <see cref="Point2.Zero"/> and + /// <see cref="HalfExtents" /> set to <see cref="Vector2.Zero"/>. + /// </summary> + public static readonly BoundingRectangle Empty = new BoundingRectangle(); + + /// <summary> + /// The centre position of this <see cref="BoundingRectangle" />. + /// </summary> + public Point2 Center; + + /// <summary> + /// The distance from the <see cref="Center" /> point along both axes to any point on the boundary of this + /// <see cref="BoundingRectangle" />. + /// </summary> + public Vector2 HalfExtents; + + /// <summary> + /// Initializes a new instance of the <see cref="BoundingRectangle" /> structure from the specified centre + /// <see cref="Point2" /> and the radii <see cref="Size2" />. + /// </summary> + /// <param name="center">The centre <see cref="Point2" />.</param> + /// <param name="halfExtents">The radii <see cref="Vector2" />.</param> + public BoundingRectangle(Point2 center, Size2 halfExtents) + { + Center = center; + HalfExtents = halfExtents; + } + + /// <summary> + /// Computes the <see cref="BoundingRectangle" /> from a minimum <see cref="Point2" /> and maximum + /// <see cref="Point2" />. + /// </summary> + /// <param name="minimum">The minimum point.</param> + /// <param name="maximum">The maximum point.</param> + /// <param name="result">The resulting bounding rectangle.</param> + public static void CreateFrom(Point2 minimum, Point2 maximum, out BoundingRectangle result) + { + result.Center = new Point2((maximum.X + minimum.X) * 0.5f, (maximum.Y + minimum.Y) * 0.5f); + result.HalfExtents = new Vector2((maximum.X - minimum.X) * 0.5f, (maximum.Y - minimum.Y) * 0.5f); + } + + /// <summary> + /// Computes the <see cref="BoundingRectangle" /> from a minimum <see cref="Point2" /> and maximum + /// <see cref="Point2" />. + /// </summary> + /// <param name="minimum">The minimum point.</param> + /// <param name="maximum">The maximum point.</param> + /// <returns>The resulting <see cref="BoundingRectangle" />.</returns> + public static BoundingRectangle CreateFrom(Point2 minimum, Point2 maximum) + { + BoundingRectangle result; + CreateFrom(minimum, maximum, out result); + return result; + } + + /// <summary> + /// Computes the <see cref="BoundingRectangle" /> from a list of <see cref="Point2" /> structures. + /// </summary> + /// <param name="points">The points.</param> + /// <param name="result">The resulting bounding rectangle.</param> + public static void CreateFrom(IReadOnlyList<Point2> points, out BoundingRectangle result) + { + Point2 minimum; + Point2 maximum; + PrimitivesHelper.CreateRectangleFromPoints(points, out minimum, out maximum); + CreateFrom(minimum, maximum, out result); + } + + /// <summary> + /// Computes the <see cref="BoundingRectangle" /> from a list of <see cref="Point2" /> structures. + /// </summary> + /// <param name="points">The points.</param> + /// <returns>The resulting <see cref="BoundingRectangle" />.</returns> + public static BoundingRectangle CreateFrom(IReadOnlyList<Point2> points) + { + BoundingRectangle result; + CreateFrom(points, out result); + return result; + } + + /// <summary> + /// Computes the <see cref="BoundingRectangle" /> from the specified <see cref="BoundingRectangle" /> transformed by + /// the + /// specified <see cref="Matrix2" />. + /// </summary> + /// <param name="boundingRectangle">The bounding rectangle.</param> + /// <param name="transformMatrix">The transform matrix.</param> + /// <param name="result">The resulting bounding rectangle.</param> + /// <returns> + /// The <see cref="BoundingRectangle" /> from the <paramref name="boundingRectangle" /> transformed by the + /// <paramref name="transformMatrix" />. + /// </returns> + /// <remarks> + /// <para> + /// If a transformed <see cref="BoundingRectangle" /> is used for <paramref name="boundingRectangle" /> then the + /// resulting <see cref="BoundingRectangle" /> will have the compounded transformation, which most likely is + /// not desired. + /// </para> + /// </remarks> + public static void Transform(ref BoundingRectangle boundingRectangle, + ref Matrix2 transformMatrix, out BoundingRectangle result) + { + PrimitivesHelper.TransformRectangle(ref boundingRectangle.Center, ref boundingRectangle.HalfExtents, ref transformMatrix); + result.Center = boundingRectangle.Center; + result.HalfExtents = boundingRectangle.HalfExtents; + } + + /// <summary> + /// Computes the <see cref="BoundingRectangle" /> from the specified <see cref="BoundingRectangle" /> transformed by + /// the + /// specified <see cref="Matrix2" />. + /// </summary> + /// <param name="boundingRectangle">The bounding rectangle.</param> + /// <param name="transformMatrix">The transform matrix.</param> + /// <returns> + /// The <see cref="BoundingRectangle" /> from the <paramref name="boundingRectangle" /> transformed by the + /// <paramref name="transformMatrix" />. + /// </returns> + /// <remarks> + /// <para> + /// If a transformed <see cref="BoundingRectangle" /> is used for <paramref name="boundingRectangle" /> then the + /// resulting <see cref="BoundingRectangle" /> will have the compounded transformation, which most likely is + /// not desired. + /// </para> + /// </remarks> + public static BoundingRectangle Transform(BoundingRectangle boundingRectangle, + ref Matrix2 transformMatrix) + { + BoundingRectangle result; + Transform(ref boundingRectangle, ref transformMatrix, out result); + return result; + } + + /// <summary> + /// Computes the <see cref="BoundingRectangle" /> that contains the two specified + /// <see cref="BoundingRectangle" /> structures. + /// </summary> + /// <param name="first">The first bounding rectangle.</param> + /// <param name="second">The second bounding rectangle.</param> + /// <param name="result">The resulting bounding rectangle that contains both the <paramref name="first" /> and the + /// <paramref name="second" />.</param> + public static void Union(ref BoundingRectangle first, ref BoundingRectangle second, out BoundingRectangle result) + { + // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 6.5; Bounding Volume Hierarchies - Merging Bounding Volumes. pg 267 + + var firstMinimum = first.Center - first.HalfExtents; + var firstMaximum = first.Center + first.HalfExtents; + var secondMinimum = second.Center - second.HalfExtents; + var secondMaximum = second.Center + second.HalfExtents; + + var minimum = Point2.Minimum(firstMinimum, secondMinimum); + var maximum = Point2.Maximum(firstMaximum, secondMaximum); + + result = CreateFrom(minimum, maximum); + } + + /// <summary> + /// Computes the <see cref="BoundingRectangle" /> that contains the two specified + /// <see cref="BoundingRectangle" /> structures. + /// </summary> + /// <param name="first">The first bounding rectangle.</param> + /// <param name="second">The second bounding rectangle.</param> + /// <returns> + /// A <see cref="BoundingRectangle" /> that contains both the <paramref name="first" /> and the + /// <paramref name="second" />. + /// </returns> + public static BoundingRectangle Union(BoundingRectangle first, BoundingRectangle second) + { + BoundingRectangle result; + Union(ref first, ref second, out result); + return result; + } + + /// <summary> + /// Computes the <see cref="BoundingRectangle" /> that contains both the specified + /// <see cref="BoundingRectangle" /> and this <see cref="BoundingRectangle" />. + /// </summary> + /// <param name="boundingRectangle">The bounding rectangle.</param> + /// <returns> + /// A <see cref="BoundingRectangle" /> that contains both the <paramref name="boundingRectangle" /> and + /// this + /// <see cref="BoundingRectangle" />. + /// </returns> + public BoundingRectangle Union(BoundingRectangle boundingRectangle) + { + return Union(this, boundingRectangle); + } + + /// <summary> + /// Computes the <see cref="BoundingRectangle" /> that is in common between the two specified + /// <see cref="BoundingRectangle" /> structures. + /// </summary> + /// <param name="first">The first bounding rectangle.</param> + /// <param name="second">The second bounding rectangle.</param> + /// <param name="result">The resulting bounding rectangle that is in common between both the <paramref name="first" /> and + /// the <paramref name="second" />, if they intersect; otherwise, <see cref="Empty"/>.</param> + public static void Intersection(ref BoundingRectangle first, + ref BoundingRectangle second, out BoundingRectangle result) + { + var firstMinimum = first.Center - first.HalfExtents; + var firstMaximum = first.Center + first.HalfExtents; + var secondMinimum = second.Center - second.HalfExtents; + var secondMaximum = second.Center + second.HalfExtents; + + var minimum = Point2.Maximum(firstMinimum, secondMinimum); + var maximum = Point2.Minimum(firstMaximum, secondMaximum); + + if ((maximum.X < minimum.X) || (maximum.Y < minimum.Y)) + result = new BoundingRectangle(); + else + result = CreateFrom(minimum, maximum); + } + + /// <summary> + /// Computes the <see cref="BoundingRectangle" /> that is in common between the two specified + /// <see cref="BoundingRectangle" /> structures. + /// </summary> + /// <param name="first">The first bounding rectangle.</param> + /// <param name="second">The second bounding rectangle.</param> + /// <returns> + /// A <see cref="BoundingRectangle" /> that is in common between both the <paramref name="first" /> and + /// the <paramref name="second" />, if they intersect; otherwise, <see cref="Empty"/>. + /// </returns> + public static BoundingRectangle Intersection(BoundingRectangle first, + BoundingRectangle second) + { + BoundingRectangle result; + Intersection(ref first, ref second, out result); + return result; + } + + /// <summary> + /// Computes the <see cref="BoundingRectangle" /> that is in common between the specified + /// <see cref="BoundingRectangle" /> and this <see cref="BoundingRectangle" />. + /// </summary> + /// <param name="boundingRectangle">The bounding rectangle.</param> + /// <returns> + /// A <see cref="BoundingRectangle" /> that is in common between both the <paramref name="boundingRectangle" /> and + /// this <see cref="BoundingRectangle"/>, if they intersect; otherwise, <see cref="Empty"/>. + /// </returns> + public BoundingRectangle Intersection(BoundingRectangle boundingRectangle) + { + BoundingRectangle result; + Intersection(ref this, ref boundingRectangle, out result); + return result; + } + + /// <summary> + /// Determines whether the two specified <see cref="BoundingRectangle" /> structures intersect. + /// </summary> + /// <param name="first">The first bounding rectangle.</param> + /// <param name="second">The second bounding rectangle.</param> + /// <returns> + /// <c>true</c> if the <paramref name="first" /> intersects with the <see cref="second" />; otherwise, <c>false</c>. + /// </returns> + public static bool Intersects(ref BoundingRectangle first, ref BoundingRectangle second) + { + // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.2; Bounding Volumes - Axis-aligned Bounding Boxes (AABBs). pg 80 + + var distance = first.Center - second.Center; + var radii = first.HalfExtents + second.HalfExtents; + return Math.Abs(distance.X) <= radii.X && Math.Abs(distance.Y) <= radii.Y; + } + + /// <summary> + /// Determines whether the two specified <see cref="BoundingRectangle" /> structures intersect. + /// </summary> + /// <param name="first">The first bounding rectangle.</param> + /// <param name="second">The second bounding rectangle.</param> + /// <returns> + /// <c>true</c> if the <paramref name="first" /> intersects with the <see cref="second" />; otherwise, <c>false</c>. + /// </returns> + public static bool Intersects(BoundingRectangle first, BoundingRectangle second) + { + return Intersects(ref first, ref second); + } + + /// <summary> + /// Determines whether the specified <see cref="BoundingRectangle" /> intersects with this + /// <see cref="BoundingRectangle" />. + /// </summary> + /// <param name="boundingRectangle">The bounding rectangle.</param> + /// <returns> + /// <c>true</c> if the <paramref name="boundingRectangle" /> intersects with this + /// <see cref="BoundingRectangle" />; otherwise, + /// <c>false</c>. + /// </returns> + public bool Intersects(ref BoundingRectangle boundingRectangle) + { + return Intersects(ref this, ref boundingRectangle); + } + + /// <summary> + /// Determines whether the specified <see cref="BoundingRectangle" /> intersects with this + /// <see cref="BoundingRectangle" />. + /// </summary> + /// <param name="boundingRectangle">The bounding rectangle.</param> + /// <returns> + /// <c>true</c> if the <paramref name="boundingRectangle" /> intersects with this + /// <see cref="BoundingRectangle" />; otherwise, + /// <c>false</c>. + /// </returns> + public bool Intersects(BoundingRectangle boundingRectangle) + { + return Intersects(ref this, ref boundingRectangle); + } + + /// <summary> + /// Updates this <see cref="BoundingRectangle" /> from a list of <see cref="Point2" /> structures. + /// </summary> + /// <param name="points">The points.</param> + public void UpdateFromPoints(IReadOnlyList<Point2> points) + { + var boundingRectangle = CreateFrom(points); + Center = boundingRectangle.Center; + HalfExtents = boundingRectangle.HalfExtents; + } + + /// <summary> + /// Determines whether the specified <see cref="BoundingRectangle" /> contains the specified + /// <see cref="Point2" />. + /// </summary> + /// <param name="boundingRectangle">The bounding rectangle.</param> + /// <param name="point">The point.</param> + /// <returns> + /// <c>true</c> if the <paramref name="boundingRectangle" /> contains the <paramref name="point" />; otherwise, + /// <c>false</c>. + /// </returns> + public static bool Contains(ref BoundingRectangle boundingRectangle, ref Point2 point) + { + // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.2; Bounding Volumes - Axis-aligned Bounding Boxes (AABBs). pg 78 + + var distance = boundingRectangle.Center - point; + var radii = boundingRectangle.HalfExtents; + + return (Math.Abs(distance.X) <= radii.X) && (Math.Abs(distance.Y) <= radii.Y); + } + + /// <summary> + /// Determines whether the specified <see cref="BoundingRectangle" /> contains the specified + /// <see cref="Point2" />. + /// </summary> + /// <param name="boundingRectangle">The bounding rectangle.</param> + /// <param name="point">The point.</param> + /// <returns> + /// <c>true</c> if the <paramref name="boundingRectangle" /> contains the <paramref name="point" />; otherwise, + /// <c>false</c>. + /// </returns> + public static bool Contains(BoundingRectangle boundingRectangle, Point2 point) + { + return Contains(ref boundingRectangle, ref point); + } + + /// <summary> + /// Determines whether this <see cref="BoundingRectangle" /> contains the specified <see cref="Point2" />. + /// </summary> + /// <param name="point">The point.</param> + /// <returns> + /// <c>true</c> if this <see cref="BoundingRectangle" /> contains the <paramref name="point" />; otherwise, + /// <c>false</c>. + /// </returns> + public bool Contains(Point2 point) + { + return Contains(this, point); + } + + /// <summary> + /// Computes the squared distance from this <see cref="BoundingRectangle"/> to a <see cref="Point2"/>. + /// </summary> + /// <param name="point">The point.</param> + /// <returns>The squared distance from this <see cref="BoundingRectangle"/> to the <paramref name="point"/>.</returns> + public float SquaredDistanceTo(Point2 point) + { + return PrimitivesHelper.SquaredDistanceToPointFromRectangle(Center - HalfExtents, Center + HalfExtents, point); + } + + /// <summary> + /// Computes the closest <see cref="Point2" /> on this <see cref="BoundingRectangle" /> to a specified + /// <see cref="Point2" />. + /// </summary> + /// <param name="point">The point.</param> + /// <returns>The closest <see cref="Point2" /> on this <see cref="BoundingRectangle" /> to the <paramref name="point" />.</returns> + public Point2 ClosestPointTo(Point2 point) + { + Point2 result; + PrimitivesHelper.ClosestPointToPointFromRectangle(Center - HalfExtents, Center + HalfExtents, point, out result); + return result; + } + + /// <summary> + /// Compares two <see cref="BoundingRectangle" /> structures. The result specifies whether the values of the + /// <see cref="Center" /> and <see cref="HalfExtents" /> fields of the two <see cref="BoundingRectangle" /> structures + /// are equal. + /// </summary> + /// <param name="first">The first bounding rectangle.</param> + /// <param name="second">The second bounding rectangle.</param> + /// <returns> + /// <c>true</c> if the <see cref="Center" /> and <see cref="HalfExtents" /> fields of the two + /// <see cref="BoundingRectangle" /> structures are equal; otherwise, <c>false</c>. + /// </returns> + public static bool operator ==(BoundingRectangle first, BoundingRectangle second) + { + return first.Equals(ref second); + } + + /// <summary> + /// Compares two <see cref="BoundingRectangle" /> structures. The result specifies whether the values of the + /// <see cref="Center" /> and <see cref="HalfExtents" /> fields of the two <see cref="BoundingRectangle" /> structures + /// are unequal. + /// </summary> + /// <param name="first">The first bounding rectangle.</param> + /// <param name="second">The second bounding rectangle.</param> + /// <returns> + /// <c>true</c> if the <see cref="Center" /> and <see cref="HalfExtents" /> fields of the two + /// <see cref="BoundingRectangle" /> structures are unequal; otherwise, <c>false</c>. + /// </returns> + public static bool operator !=(BoundingRectangle first, BoundingRectangle second) + { + return !(first == second); + } + + /// <summary> + /// Indicates whether this <see cref="BoundingRectangle" /> is equal to another + /// <see cref="BoundingRectangle" />. + /// </summary> + /// <param name="boundingRectangle">The bounding rectangle.</param> + /// <returns> + /// <c>true</c> if this <see cref="BoundingRectangle" /> is equal to the <paramref name="boundingRectangle" />; + /// otherwise, <c>false</c>. + /// </returns> + public bool Equals(BoundingRectangle boundingRectangle) + { + return Equals(ref boundingRectangle); + } + + /// <summary> + /// Indicates whether this <see cref="BoundingRectangle" /> is equal to another <see cref="BoundingRectangle" />. + /// </summary> + /// <param name="boundingRectangle">The bounding rectangle.</param> + /// <returns> + /// <c>true</c> if this <see cref="BoundingRectangle" /> is equal to the <paramref name="boundingRectangle" />; + /// otherwise, + /// <c>false</c>. + /// </returns> + public bool Equals(ref BoundingRectangle boundingRectangle) + { + return (boundingRectangle.Center == Center) && (boundingRectangle.HalfExtents == HalfExtents); + } + + /// <summary> + /// Returns a value indicating whether this <see cref="BoundingRectangle" /> is equal to a specified object. + /// </summary> + /// <param name="obj">The object to make the comparison with.</param> + /// <returns> + /// <c>true</c> if this <see cref="BoundingRectangle" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>. + /// </returns> + public override bool Equals(object obj) + { + if (obj is BoundingRectangle) + return Equals((BoundingRectangle)obj); + return false; + } + + /// <summary> + /// Returns a hash code of this <see cref="BoundingRectangle" /> suitable for use in hashing algorithms and data + /// structures like a hash table. + /// </summary> + /// <returns> + /// A hash code of this <see cref="BoundingRectangle" />. + /// </returns> + public override int GetHashCode() + { + unchecked + { + return (Center.GetHashCode() * 397) ^ HalfExtents.GetHashCode(); + } + } + + /// <summary> + /// Performs an implicit conversion from a <see cref="Rectangle" /> to a <see cref="BoundingRectangle" />. + /// </summary> + /// <param name="rectangle">The rectangle.</param> + /// <returns> + /// The resulting <see cref="BoundingRectangle" />. + /// </returns> + public static implicit operator BoundingRectangle(Rectangle rectangle) + { + var radii = new Size2(rectangle.Width * 0.5f, rectangle.Height * 0.5f); + var centre = new Point2(rectangle.X + radii.Width, rectangle.Y + radii.Height); + return new BoundingRectangle(centre, radii); + } + + /// <summary> + /// Performs an implicit conversion from a <see cref="BoundingRectangle" /> to a <see cref="Rectangle" />. + /// </summary> + /// <param name="boundingRectangle">The bounding rectangle.</param> + /// <returns> + /// The resulting <see cref="Rectangle" />. + /// </returns> + public static implicit operator Rectangle(BoundingRectangle boundingRectangle) + { + var minimum = boundingRectangle.Center - boundingRectangle.HalfExtents; + return new Rectangle((int)minimum.X, (int)minimum.Y, (int)boundingRectangle.HalfExtents.X * 2, + (int)boundingRectangle.HalfExtents.Y * 2); + } + + /// <summary> + /// Performs an implicit conversion from a <see cref="RectangleF" /> to a <see cref="BoundingRectangle" />. + /// </summary> + /// <param name="rectangle">The rectangle.</param> + /// <returns> + /// The resulting <see cref="BoundingRectangle" />. + /// </returns> + public static implicit operator BoundingRectangle(RectangleF rectangle) + { + var radii = new Size2(rectangle.Width * 0.5f, rectangle.Height * 0.5f); + var centre = new Point2(rectangle.X + radii.Width, rectangle.Y + radii.Height); + return new BoundingRectangle(centre, radii); + } + + /// <summary> + /// Performs an implicit conversion from a <see cref="BoundingRectangle" /> to a <see cref="RectangleF" />. + /// </summary> + /// <param name="boundingRectangle">The bounding rectangle.</param> + /// <returns> + /// The resulting <see cref="Rectangle" />. + /// </returns> + public static implicit operator RectangleF(BoundingRectangle boundingRectangle) + { + var minimum = boundingRectangle.Center - boundingRectangle.HalfExtents; + return new RectangleF(minimum.X, minimum.Y, boundingRectangle.HalfExtents.X * 2, + boundingRectangle.HalfExtents.Y * 2); + } + + /// <summary> + /// Returns a <see cref="string" /> that represents this <see cref="BoundingRectangle" />. + /// </summary> + /// <returns> + /// A <see cref="string" /> that represents this <see cref="BoundingRectangle" />. + /// </returns> + public override string ToString() + { + return $"Centre: {Center}, Radii: {HalfExtents}"; + } + + internal string DebugDisplayString => ToString(); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/CircleF.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/CircleF.cs new file mode 100644 index 0000000..70f9307 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/CircleF.cs @@ -0,0 +1,519 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.3; Bounding Volumes - Spheres. pg 88 + + /// <summary> + /// A two dimensional circle defined by a centre <see cref="Point2" /> and a radius <see cref="float" />. + /// </summary> + /// <remarks> + /// <para> + /// An <see cref="CircleF" /> is categorized by the set of all points in a plane that are at equal distance from + /// the + /// centre. + /// </para> + /// </remarks> + /// <seealso cref="IEquatable{T}" /> + /// <seealso cref="IEquatableByRef{T}" /> + [DataContract] + public struct CircleF : IEquatable<CircleF>, IEquatableByRef<CircleF>, IShapeF + { + /// <summary> + /// The centre position of this <see cref="CircleF" />. + /// </summary> + [DataMember] public Point2 Center; + + /// <summary> + /// The distance from the <see cref="Center" /> point to any point on the boundary of this <see cref="CircleF" />. + /// </summary> + [DataMember] public float Radius; + + /// <summary> + /// Gets or sets the position of the circle. + /// </summary> + public Point2 Position + { + get => Center; + set => Center = value; + } + + public RectangleF BoundingRectangle + { + get + { + var minX = Center.X - Radius; + var minY = Center.Y - Radius; + return new RectangleF(minX, minY, Diameter, Diameter); + } + } + + /// <summary> + /// Gets the distance from a point to the opposite point, both on the boundary of this <see cref="CircleF" />. + /// </summary> + public float Diameter => 2 * Radius; + + /// <summary> + /// Gets the distance around the boundary of this <see cref="CircleF" />. + /// </summary> + public float Circumference => 2 * MathHelper.Pi * Radius; + + /// <summary> + /// Initializes a new instance of the <see cref="CircleF" /> structure from the specified centre + /// <see cref="Point2" /> and the radius <see cref="float" />. + /// </summary> + /// <param name="center">The centre point.</param> + /// <param name="radius">The radius.</param> + public CircleF(Point2 center, float radius) + { + Center = center; + Radius = radius; + } + + /// <summary> + /// Computes the bounding <see cref="CircleF" /> from a minimum <see cref="Point2" /> and maximum + /// <see cref="Point2" />. + /// </summary> + /// <param name="minimum">The minimum point.</param> + /// <param name="maximum">The maximum point.</param> + /// <param name="result">The resulting circle.</param> + public static void CreateFrom(Point2 minimum, Point2 maximum, out CircleF result) + { + result.Center = new Point2((maximum.X + minimum.X) * 0.5f, (maximum.Y + minimum.Y) * 0.5f); + var distanceVector = maximum - minimum; + result.Radius = distanceVector.X > distanceVector.Y ? distanceVector.X * 0.5f : distanceVector.Y * 0.5f; + } + + /// <summary> + /// Computes the bounding <see cref="CircleF" /> from a minimum <see cref="Point2" /> and maximum + /// <see cref="Point2" />. + /// </summary> + /// <param name="minimum">The minimum point.</param> + /// <param name="maximum">The maximum point.</param> + /// <returns>An <see cref="CircleF" />.</returns> + public static CircleF CreateFrom(Point2 minimum, Point2 maximum) + { + CircleF result; + CreateFrom(minimum, maximum, out result); + return result; + } + + /// <summary> + /// Computes the bounding <see cref="CircleF" /> from a list of <see cref="Point2" /> structures. + /// </summary> + /// <param name="points">The points.</param> + /// <param name="result">The resulting circle.</param> + public static void CreateFrom(IReadOnlyList<Point2> points, out CircleF result) + { + // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.3; Bounding Volumes - Spheres. pg 89-90 + + if (points == null || points.Count == 0) + { + result = default(CircleF); + return; + } + + var minimum = new Point2(float.MaxValue, float.MaxValue); + var maximum = new Point2(float.MinValue, float.MinValue); + + // ReSharper disable once ForCanBeConvertedToForeach + for (var index = points.Count - 1; index >= 0; --index) + { + var point = points[index]; + minimum = Point2.Minimum(minimum, point); + maximum = Point2.Maximum(maximum, point); + } + + CreateFrom(minimum, maximum, out result); + } + + /// <summary> + /// Computes the bounding <see cref="CircleF" /> from a list of <see cref="Point2" /> structures. + /// </summary> + /// <param name="points">The points.</param> + /// <returns>An <see cref="CircleF" />.</returns> + public static CircleF CreateFrom(IReadOnlyList<Point2> points) + { + CircleF result; + CreateFrom(points, out result); + return result; + } + + /// <summary> + /// Determines whether the two specified <see cref="CircleF" /> structures intersect. + /// </summary> + /// <param name="first">The first circle.</param> + /// <param name="second">The second circle.</param> + /// <returns> + /// <c>true</c> if the <paramref name="first" /> intersects with the <see cref="second" />; otherwise, <c>false</c>. + /// </returns> + public static bool Intersects(ref CircleF first, ref CircleF second) + { + // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.3; Bounding Volumes - Spheres. pg 88 + + // Calculate squared distance between centers + var distanceVector = first.Center - second.Center; + var distanceSquared = distanceVector.Dot(distanceVector); + var radiusSum = first.Radius + second.Radius; + return distanceSquared <= radiusSum * radiusSum; + } + + /// <summary> + /// Determines whether the two specified <see cref="CircleF" /> structures intersect. + /// </summary> + /// <param name="first">The first circle.</param> + /// <param name="second">The second circle.</param> + /// <returns> + /// <c>true</c> if the <paramref name="first" /> intersects with the <see cref="second" />; otherwise, <c>false</c>. + /// </returns> + public static bool Intersects(CircleF first, CircleF second) + { + return Intersects(ref first, ref second); + } + + /// <summary> + /// Determines whether the specified <see cref="CircleF" /> intersects with this <see cref="CircleF" />. + /// </summary> + /// <param name="circle">The circle.</param> + /// <returns> + /// <c>true</c> if the <paramref name="circle" /> intersects with this <see cref="CircleF" />; otherwise, + /// <c>false</c>. + /// </returns> + public bool Intersects(ref CircleF circle) + { + return Intersects(ref this, ref circle); + } + + /// <summary> + /// Determines whether the specified <see cref="CircleF" /> intersects with this <see cref="CircleF" />. + /// </summary> + /// <param name="circle">The circle.</param> + /// <returns> + /// <c>true</c> if the <paramref name="circle" /> intersects with this <see cref="CircleF" />; otherwise, + /// <c>false</c>. + /// </returns> + public bool Intersects(CircleF circle) + { + return Intersects(ref this, ref circle); + } + + /// <summary> + /// Determines whether the specified <see cref="CircleF" /> and <see cref="BoundingRectangle" /> structures intersect. + /// </summary> + /// <param name="circle">The circle.</param> + /// <param name="rectangle">The rectangle.</param> + /// <returns> + /// <c>true</c> if the <paramref name="circle" /> intersects with the <see cref="rectangle" />; otherwise, <c>false</c> + /// . + /// </returns> + public static bool Intersects(ref CircleF circle, ref BoundingRectangle rectangle) + { + // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 5.25; Basic Primitives Test - Testing Sphere Against AABB. pg 165-166 + + // Compute squared distance between sphere center and AABB boundary + var distanceSquared = rectangle.SquaredDistanceTo(circle.Center); + // Circle and AABB intersect if the (squared) distance between the AABB's boundary and the circle is less than the (squared) circle's radius + return distanceSquared <= circle.Radius * circle.Radius; + } + + /// <summary> + /// Determines whether the specified <see cref="CircleF" /> and <see cref="BoundingRectangle" /> structures intersect. + /// </summary> + /// <param name="circle">The circle.</param> + /// <param name="rectangle">The rectangle.</param> + /// <returns> + /// <c>true</c> if the <paramref name="circle" /> intersects with the <see cref="rectangle" />; otherwise, <c>false</c> + /// . + /// </returns> + public static bool Intersects(CircleF circle, BoundingRectangle rectangle) + { + return Intersects(ref circle, ref rectangle); + } + + /// <summary> + /// Determines whether the specified <see cref="CircleF" /> intersects with this <see cref="BoundingRectangle" />. + /// </summary> + /// <param name="rectangle">The rectangle.</param> + /// <returns> + /// <c>true</c> if the <paramref name="rectangle" /> intersects with this <see cref="CircleF" />; otherwise, + /// <c>false</c>. + /// </returns> + public bool Intersects(ref BoundingRectangle rectangle) + { + return Intersects(ref this, ref rectangle); + } + + /// <summary> + /// Determines whether the specified <see cref="CircleF" /> intersects with this <see cref="BoundingRectangle" />. + /// </summary> + /// <param name="rectangle">The rectangle.</param> + /// <returns> + /// <c>true</c> if the <paramref name="rectangle" /> intersects with this <see cref="CircleF" />; otherwise, + /// <c>false</c>. + /// </returns> + public bool Intersects(BoundingRectangle rectangle) + { + return Intersects(ref this, ref rectangle); + } + + /// <summary> + /// Determines whether the specified <see cref="CircleF" /> contains the specified + /// <see cref="Point2" />. + /// </summary> + /// <param name="circle">The circle.</param> + /// <param name="point">The point.</param> + /// <returns> + /// <c>true</c> if the <paramref name="circle" /> contains the <paramref name="point" />; otherwise, + /// <c>false</c>. + /// </returns> + public static bool Contains(ref CircleF circle, Point2 point) + { + var dx = circle.Center.X - point.X; + var dy = circle.Center.Y - point.Y; + var d2 = dx * dx + dy * dy; + var r2 = circle.Radius * circle.Radius; + return d2 <= r2; + } + + /// <summary> + /// Determines whether the specified <see cref="CircleF" /> contains the specified + /// <see cref="Point2" />. + /// </summary> + /// <param name="circle">The circle.</param> + /// <param name="point">The point.</param> + /// <returns> + /// <c>true</c> if the <paramref name="circle" /> contains the <paramref name="point" />; otherwise, + /// <c>false</c>. + /// </returns> + public static bool Contains(CircleF circle, Point2 point) + { + return Contains(ref circle, point); + } + + /// <summary> + /// Determines whether this <see cref="CircleF" /> contains the specified <see cref="Point2" />. + /// </summary> + /// <param name="point">The point.</param> + /// <returns> + /// <c>true</c> if this <see cref="BoundingRectangle" /> contains the <paramref name="point" />; otherwise, + /// <c>false</c>. + /// </returns> + public bool Contains(Point2 point) + { + return Contains(ref this, point); + } + + /// <summary> + /// Computes the closest <see cref="Point2" /> on this <see cref="CircleF" /> to a specified + /// <see cref="Point2" />. + /// </summary> + /// <param name="point">The point.</param> + /// <returns>The closest <see cref="Point2" /> on this <see cref="CircleF" /> to the <paramref name="point" />.</returns> + public Point2 ClosestPointTo(Point2 point) + { + var distanceVector = point - Center; + var lengthSquared = distanceVector.Dot(distanceVector); + if (lengthSquared <= Radius * Radius) + return point; + distanceVector.Normalize(); + return Center + Radius * distanceVector; + } + + /// <summary> + /// Computes the <see cref="Point2" /> on the boundary of of this <see cref="CircleF" /> using the specified angle. + /// </summary> + /// <param name="angle">The angle in radians.</param> + /// <returns>The <see cref="Point2" /> on the boundary of this <see cref="CircleF" /> using <paramref name="angle" />.</returns> + public Point2 BoundaryPointAt(float angle) + { + var direction = new Vector2((float) Math.Cos(angle), (float) Math.Sin(angle)); + return Center + Radius * direction; + } + + [Obsolete("Circle.GetPointAlongEdge() may be removed in the future. Use BoundaryPointAt() instead.")] + public Point2 GetPointAlongEdge(float angle) + { + return Center + new Vector2(Radius * (float) Math.Cos(angle), Radius * (float) Math.Sin(angle)); + } + + /// <summary> + /// Compares two <see cref="CircleF" /> structures. The result specifies whether the values of the + /// <see cref="Center" /> and <see cref="Radius" /> fields of the two <see cref="CircleF" /> structures + /// are equal. + /// </summary> + /// <param name="first">The first circle.</param> + /// <param name="second">The second circle.</param> + /// <returns> + /// <c>true</c> if the <see cref="Center" /> and <see cref="Radius" /> fields of the two + /// <see cref="BoundingRectangle" /> structures are equal; otherwise, <c>false</c>. + /// </returns> + public static bool operator ==(CircleF first, CircleF second) + { + return first.Equals(ref second); + } + + /// <summary> + /// Compares two <see cref="CircleF" /> structures. The result specifies whether the values of the + /// <see cref="Center" /> and <see cref="Radius" /> fields of the two <see cref="CircleF" /> structures + /// are unequal. + /// </summary> + /// <param name="first">The first circle.</param> + /// <param name="second">The second circle.</param> + /// <returns> + /// <c>true</c> if the <see cref="Center" /> and <see cref="Radius" /> fields of the two + /// <see cref="CircleF" /> structures are unequal; otherwise, <c>false</c>. + /// </returns> + public static bool operator !=(CircleF first, CircleF second) + { + return !(first == second); + } + + /// <summary> + /// Indicates whether this <see cref="CircleF" /> is equal to another <see cref="CircleF" />. + /// </summary> + /// <param name="circle">The circle.</param> + /// <returns> + /// <c>true</c> if this <see cref="CircleF" /> is equal to the <paramref name="circle" />; otherwise, <c>false</c>. + /// </returns> + public bool Equals(CircleF circle) + { + return Equals(ref circle); + } + + /// <summary> + /// Indicates whether this <see cref="CircleF" /> is equal to another <see cref="CircleF" />. + /// </summary> + /// <param name="circle">The bounding rectangle.</param> + /// <returns> + /// <c>true</c> if this <see cref="CircleF" /> is equal to the <paramref name="circle" />; + /// otherwise,<c>false</c>. + /// </returns> + public bool Equals(ref CircleF circle) + { + // ReSharper disable once CompareOfFloatsByEqualityOperator + return circle.Center == Center && circle.Radius == Radius; + } + + /// <summary> + /// Returns a value indicating whether this <see cref="CircleF" /> is equal to a specified object. + /// </summary> + /// <param name="obj">The object to make the comparison with.</param> + /// <returns> + /// <c>true</c> if this <see cref="CircleF" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>. + /// </returns> + public override bool Equals(object obj) + { + return obj is CircleF && Equals((CircleF) obj); + } + + /// <summary> + /// Returns a hash code of this <see cref="CircleF" /> suitable for use in hashing algorithms and data + /// structures like a hash table. + /// </summary> + /// <returns> + /// A hash code of this <see cref="CircleF" />. + /// </returns> + public override int GetHashCode() + { + unchecked + { + return (Center.GetHashCode() * 397) ^ Radius.GetHashCode(); + } + } + + /// <summary> + /// Performs an explicit conversion from a <see cref="CircleF" /> to a <see cref="Rectangle" />. + /// </summary> + /// <param name="circle">The circle.</param> + /// <returns> + /// The resulting <see cref="Rectangle" />. + /// </returns> + public static explicit operator Rectangle(CircleF circle) + { + var diameter = (int) circle.Diameter; + return new Rectangle((int) (circle.Center.X - circle.Radius), (int) (circle.Center.Y - circle.Radius), + diameter, diameter); + } + + /// <summary> + /// Performs a conversion from a specified <see cref="CircleF" /> to a <see cref="Rectangle" />. + /// </summary> + /// <returns> + /// The resulting <see cref="Rectangle" />. + /// </returns> + public Rectangle ToRectangle() + { + return (Rectangle)this; + } + + /// <summary> + /// Performs an explicit conversion from a <see cref="Rectangle" /> to a <see cref="CircleF" />. + /// </summary> + /// <param name="rectangle">The rectangle.</param> + /// <returns> + /// The resulting <see cref="CircleF" />. + /// </returns> + public static explicit operator CircleF(Rectangle rectangle) + { + var halfWidth = rectangle.Width / 2; + var halfHeight = rectangle.Height / 2; + return new CircleF(new Point2(rectangle.X + halfWidth, rectangle.Y + halfHeight), + halfWidth > halfHeight ? halfWidth : halfHeight); + } + + /// <summary> + /// Performs an explicit conversion from a <see cref="CircleF" /> to a <see cref="RectangleF" />. + /// </summary> + /// <param name="circle">The circle.</param> + /// <returns> + /// The resulting <see cref="RectangleF" />. + /// </returns> + public static explicit operator RectangleF(CircleF circle) + { + var diameter = circle.Diameter; + return new RectangleF(circle.Center.X - circle.Radius, circle.Center.Y - circle.Radius, diameter, diameter); + } + + /// <summary> + /// Performs a conversion from a specified <see cref="CircleF" /> to a <see cref="RectangleF" />. + /// </summary> + /// <returns> + /// The resulting <see cref="RectangleF" />. + /// </returns> + public RectangleF ToRectangleF() + { + return (RectangleF)this; + } + + /// <summary> + /// Performs an explicit conversion from a <see cref="RectangleF" /> to a <see cref="CircleF" />. + /// </summary> + /// <param name="rectangle">The rectangle.</param> + /// <returns> + /// The resulting <see cref="CircleF" />. + /// </returns> + public static explicit operator CircleF(RectangleF rectangle) + { + var halfWidth = rectangle.Width * 0.5f; + var halfHeight = rectangle.Height * 0.5f; + return new CircleF(new Point2(rectangle.X + halfWidth, rectangle.Y + halfHeight), + halfWidth > halfHeight ? halfWidth : halfHeight); + } + + /// <summary> + /// Returns a <see cref="string" /> that represents this <see cref="CircleF" />. + /// </summary> + /// <returns> + /// A <see cref="string" /> that represents this <see cref="CircleF" />. + /// </returns> + public override string ToString() + { + return $"Centre: {Center}, Radius: {Radius}"; + } + + internal string DebugDisplayString => ToString(); + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/EllipseF.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/EllipseF.cs new file mode 100644 index 0000000..684a770 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/EllipseF.cs @@ -0,0 +1,101 @@ +using System; +using System.Runtime.Serialization; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + [DataContract] + public struct EllipseF : IEquatable<EllipseF>, IEquatableByRef<EllipseF>, IShapeF + { + [DataMember] public Vector2 Center { get; set; } + [DataMember] public float RadiusX { get; set; } + [DataMember] public float RadiusY { get; set; } + + public Point2 Position + { + get => Center; + set => Center = value; + } + + public EllipseF(Vector2 center, float radiusX, float radiusY) + { + Center = center; + RadiusX = radiusX; + RadiusY = radiusY; + } + + public float Left => Center.X - RadiusX; + public float Top => Center.Y - RadiusY; + public float Right => Center.X + RadiusX; + public float Bottom => Center.Y + RadiusY; + + public RectangleF BoundingRectangle + { + get + { + var minX = Left; + var minY = Top; + var maxX = Right; + var maxY = Bottom; + return new RectangleF(minX, minY, maxX - minX, maxY - minY); + } + } + + public bool Contains(float x, float y) + { + float xCalc = (float) (Math.Pow(x - Center.X, 2) / Math.Pow(RadiusX, 2)); + float yCalc = (float) (Math.Pow(y - Center.Y, 2) / Math.Pow(RadiusY, 2)); + + return xCalc + yCalc <= 1; + } + + public bool Contains(Vector2 point) + { + return Contains(point.X, point.Y); + } + + public bool Equals(EllipseF ellispse) + { + return Equals(ref ellispse); + } + + public bool Equals(ref EllipseF ellispse) + { + // ReSharper disable once CompareOfFloatsByEqualityOperator + return ellispse.Center == Center + && ellispse.RadiusX == RadiusX + && ellispse.RadiusY == RadiusY; + } + + public override bool Equals(object obj) + { + return obj is EllipseF && Equals((EllipseF)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = Center.GetHashCode(); + hashCode = (hashCode * 397) ^ RadiusX.GetHashCode(); + hashCode = (hashCode * 397) ^ RadiusY.GetHashCode(); + return hashCode; + } + } + + public override string ToString() + { + return $"Centre: {Center}, RadiusX: {RadiusX}, RadiusY: {RadiusY}"; + } + + public static bool operator ==(EllipseF first, EllipseF second) + { + return first.Equals(ref second); + } + + public static bool operator !=(EllipseF first, EllipseF second) + { + return !(first == second); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/FastRandom.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/FastRandom.cs new file mode 100644 index 0000000..472518b --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/FastRandom.cs @@ -0,0 +1,127 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + /// <summary> + /// A random number generator that uses a fast algorithm to generate random values. + /// The speed comes at the price of true 'randomness' though, there are noticeable + /// patterns & it compares quite unfavourably to other algorithms in that respect. + /// It's a good choice in situations where speed is more desirable than a + /// good random distribution, and a poor choice when random distribution is important. + /// </summary> + public class FastRandom + { + private int _state; + + public FastRandom() + : this(1) + { + } + + public FastRandom(int seed) + { + if (seed < 1) + throw new ArgumentOutOfRangeException(nameof(seed), "seed must be greater than zero"); + + _state = seed; + } + + /// <summary> + /// Gets the next random integer value. + /// </summary> + /// <returns>A random positive integer.</returns> + public int Next() + { + _state = 214013*_state + 2531011; + return (_state >> 16) & 0x7FFF; + } + + /// <summary> + /// Gets the next random integer value which is greater than zero and less than or equal to + /// the specified maxmimum value. + /// </summary> + /// <param name="max">The maximum random integer value to return.</param> + /// <returns>A random integer value between zero and the specified maximum value.</returns> + public int Next(int max) + { + return (int) (max*NextSingle() + 0.5f); + } + + /// <summary> + /// Gets the next random integer between the specified minimum and maximum values. + /// </summary> + /// <param name="min">The inclusive minimum value.</param> + /// <param name="max">The inclusive maximum value.</param> + public int Next(int min, int max) + { + return (int) ((max - min)*NextSingle() + 0.5f) + min; + } + + /// <summary> + /// Gets the next random integer between the specified range of values. + /// </summary> + /// <param name="range">A range representing the inclusive minimum and maximum values.</param> + /// <returns>A random integer between the specified minumum and maximum values.</returns> + public int Next(Range<int> range) + { + return Next(range.Min, range.Max); + } + + /// <summary> + /// Gets the next random single value. + /// </summary> + /// <returns>A random single value between 0 and 1.</returns> + public float NextSingle() + { + return Next()/(float) short.MaxValue; + } + + /// <summary> + /// Gets the next random single value which is greater than zero and less than or equal to + /// the specified maxmimum value. + /// </summary> + /// <param name="max">The maximum random single value to return.</param> + /// <returns>A random single value between zero and the specified maximum value.</returns> + public float NextSingle(float max) + { + return max*NextSingle(); + } + + /// <summary> + /// Gets the next random single value between the specified minimum and maximum values. + /// </summary> + /// <param name="min">The inclusive minimum value.</param> + /// <param name="max">The inclusive maximum value.</param> + /// <returns>A random single value between the specified minimum and maximum values.</returns> + public float NextSingle(float min, float max) + { + return (max - min)*NextSingle() + min; + } + + /// <summary> + /// Gets the next random single value between the specified range of values. + /// </summary> + /// <param name="range">A range representing the inclusive minimum and maximum values.</param> + /// <returns>A random single value between the specified minimum and maximum values.</returns> + public float NextSingle(Range<float> range) + { + return NextSingle(range.Min, range.Max); + } + + /// <summary> + /// Gets the next random angle value. + /// </summary> + /// <returns>A random angle value.</returns> + public float NextAngle() + { + return NextSingle(-MathHelper.Pi, MathHelper.Pi); + } + + public void NextUnitVector(out Vector2 vector) + { + var angle = NextAngle(); + vector = new Vector2((float) Math.Cos(angle), (float) Math.Sin(angle)); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/FloatHelper.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/FloatHelper.cs new file mode 100644 index 0000000..a3792e6 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/FloatHelper.cs @@ -0,0 +1,15 @@ +using System.Runtime.CompilerServices; + +namespace MonoGame.Extended +{ + public static class FloatHelper + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Swap(ref float value1, ref float value2) + { + var temp = value1; + value1 = value2; + value2 = temp; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Matrix2.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Matrix2.cs new file mode 100644 index 0000000..cb56710 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Matrix2.cs @@ -0,0 +1,1038 @@ +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using Microsoft.Xna.Framework; + +// ReSharper disable CompareOfFloatsByEqualityOperator + +namespace MonoGame.Extended +{ + // https://en.wikipedia.org/wiki/Matrix_(mathematics) + // "Immersive Linear Algebra"; Jacob Ström, Kalle Åström & Tomas Akenine-Möller; 2015-2016. Chapter 6: The Matrix. http://immersivemath.com/ila/ch06_matrices/ch06.html + // "Real-Time Collision Detection"; Christer Ericson; 2005. Chapter 3.1: A Math and Geometry Primer - Matrices. pg 23-34 + + // Original code was from Matrix2D.cs in the Nez Library: https://github.com/prime31/Nez/ + + /// <summary> + /// Defines a 3x3 matrix using floating point numbers which can store two dimensional translation, scale and rotation + /// information in a right-handed coordinate system. + /// </summary> + /// <remarks> + /// <para> + /// Matrices use a row vector layout in the XNA / MonoGame Framework but, in general, matrices can be either have + /// a row vector or column vector layout. Row vector matrices view vectors as a row from left to right, while + /// column vector matrices view vectors as a column from top to bottom. For example, the <see cref="Translation" /> + /// corresponds to the fields <see cref="M31" /> and <see cref="M32" />. + /// </para> + /// <para> + /// The fields M13 and M23 always have a value of <code>0.0f</code>, and thus are removed from the + /// <see cref="Matrix2" /> to reduce its memory footprint. Same is true for the field M33, except it always has a + /// value of <code>1.0f</code>. + /// </para> + /// </remarks> + [DebuggerDisplay("{DebugDisplayString,nq}")] + public struct Matrix2 : IEquatable<Matrix2>, IEquatableByRef<Matrix2> + { + public float M11; // x scale, also used for rotation + public float M12; // used for rotation + + public float M21; // used for rotation + public float M22; // y scale, also used for rotation + + public float M31; // x translation + public float M32; // y translation + + /// <summary> + /// Gets the identity matrix. + /// </summary> + /// <value> + /// The identity matrix. + /// </value> + public static Matrix2 Identity { get; } = new Matrix2(1f, 0f, 0f, 1f, 0f, 0f); + + /// <summary> + /// Gets the translation. + /// </summary> + /// <value> + /// The translation. + /// </value> + /// <remarks>The <see cref="Translation" /> is equal to the vector <code>(M31, M32)</code>.</remarks> + public Vector2 Translation => new Vector2(M31, M32); + + /// <summary> + /// Gets the rotation angle in radians. + /// </summary> + /// <value> + /// The rotation angle in radians. + /// </value> + /// <remarks> + /// The <see cref="Rotation" /> is equal to <code>Atan2(M21, M11)</code>. + /// </remarks> + public float Rotation => (float)Math.Atan2(M21, M11); + + /// <summary> + /// Gets the scale. + /// </summary> + /// <value> + /// The scale. + /// </value> + /// <remarks> + /// The <see cref="Scale" /> is equal to the vector + /// <code>(Sqrt(M11 * M11 + M21 * M21), Sqrt(M12 * M12 + M22 * M22))</code>. + /// </remarks> + public Vector2 Scale + { + get + { + var scaleX = (float)Math.Sqrt(M11 * M11 + M21 * M21); + var scaleY = (float)Math.Sqrt(M12 * M12 + M22 * M22); + return new Vector2(scaleX, scaleY); + } + } + + /// <summary> + /// Initializes a new instance of the <see cref="Matrix2" /> struct. + /// </summary> + /// <param name="m11">The value to initialize <see cref="M11" /> to.</param> + /// <param name="m12">The value to initialize <see cref="M12" /> to.</param> + /// <param name="m21">The value to initialize <see cref="M21" /> to.</param> + /// <param name="m22">The value to initialize <see cref="M22" /> to.</param> + /// <param name="m31">The value to initialize <see cref="M31" /> to.</param> + /// <param name="m32">The value to initialize <see cref="M32" /> to.</param> + /// <remarks> + /// <para> + /// The fields M13 and M23 always have a value of <code>0.0f</code>, and thus are removed from the + /// <see cref="Matrix2" /> to reduce its memory footprint. Same is true for the field M33, except it always has a + /// value of <code>1.0f</code>. + /// </para> + /// </remarks> + public Matrix2(float m11, float m12, float m21, float m22, float m31, float m32) + { + M11 = m11; + M12 = m12; + + M21 = m21; + M22 = m22; + + M31 = m31; + M32 = m32; + } + + /// <summary> + /// Transforms the specified <see cref="Vector2" /> by this <see cref="Matrix2" />. + /// </summary> + /// <param name="vector">The vector.</param> + /// <returns>The resulting <see cref="Vector2" />.</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector2 Transform(Vector2 vector) + { + Vector2 result; + Transform(vector, out result); + return result; + } + + /// <summary> + /// Transforms the specified <see cref="Vector2" /> by this <see cref="Matrix2" />. + /// </summary> + /// <param name="vector">The vector.</param> + /// <param name="result">The resulting <see cref="Vector2" />.</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Transform(Vector2 vector, out Vector2 result) + { + result.X = vector.X * M11 + vector.Y * M21 + M31; + result.Y = vector.X * M12 + vector.Y * M22 + M32; + } + + /// <summary> + /// Transforms the specified <see cref="Vector2" /> by this <see cref="Matrix2" />. + /// </summary> + /// <param name="x">The x value of the vector.</param> + /// <param name="y">The y value of the vector.</param> + /// <param name="result">The resulting <see cref="Vector2" />.</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Transform(float x, float y, out Vector2 result) + { + result.X = x * M11 + y * M21 + M31; + result.Y = x * M12 + y * M22 + M32; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Transform(float x, float y, ref Vector3 result) + { + result.X = x * M11 + y * M21 + M31; + result.Y = x * M12 + y * M22 + M32; + } + + /// <summary> + /// Initializes a new instance of the <see cref="Matrix2" /> struct that can be used to translate, rotate, and scale a set of vertices in two dimensions. + /// </summary> + /// <param name="position">The amounts to translate by on the x and y axes.</param> + /// <param name="rotation">The amount, in radians, in which to rotate around the z-axis.</param> + /// <param name="scale">The amount to scale by on the x and y axes.</param> + /// <param name="origin">The point which to rotate and scale around.</param> + /// <param name="transformMatrix">The resulting <see cref="Matrix2" /></param> + public static void CreateFrom(Vector2 position, float rotation, Vector2? scale, Vector2? origin, + out Matrix2 transformMatrix) + { + transformMatrix = Identity; + + if (origin.HasValue) + { + transformMatrix.M31 = -origin.Value.X; + transformMatrix.M32 = -origin.Value.Y; + } + + if (scale.HasValue) + { + var scaleMatrix = CreateScale(scale.Value); + Multiply(ref transformMatrix, ref scaleMatrix, out transformMatrix); + } + + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (rotation != 0f) + { + var rotationMatrix = CreateRotationZ(-rotation); + Multiply(ref transformMatrix, ref rotationMatrix, out transformMatrix); + } + + var translationMatrix = CreateTranslation(position); + Multiply(ref transformMatrix, ref translationMatrix, out transformMatrix); + } + + /// <summary> + /// Initializes a new instance of the <see cref="Matrix2" /> struct that can be used to translate, rotate, and scale a set of vertices in two dimensions. + /// </summary> + /// <param name="position">The amounts to translate by on the x and y axes.</param> + /// <param name="rotation">The amount, in radians, in which to rotate around the z-axis.</param> + /// <param name="scale">The amount to scale by on the x and y axes.</param> + /// <param name="origin">The point which to rotate and scale around.</param> + /// <returns>The resulting <see cref="Matrix2" />.</returns> + public static Matrix2 CreateFrom(Vector2 position, float rotation, Vector2? scale = null, Vector2? origin = null) + { + var transformMatrix = Identity; + + if (origin.HasValue) + { + transformMatrix.M31 = -origin.Value.X; + transformMatrix.M32 = -origin.Value.Y; + } + + if (scale.HasValue) + { + var scaleMatrix = CreateScale(scale.Value); + transformMatrix = Multiply(transformMatrix, scaleMatrix); + } + + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (rotation != 0f) + { + var rotationMatrix = CreateRotationZ(-rotation); + transformMatrix = Multiply(transformMatrix, rotationMatrix); + } + + var translationMatrix = CreateTranslation(position); + transformMatrix = Multiply(transformMatrix, translationMatrix); + + return transformMatrix; + } + + /// <summary> + /// Initializes a new instance of the <see cref="Matrix2" /> struct that can be used to rotate a set of vertices + /// around the z-axis. + /// </summary> + /// <param name="radians">The amount, in radians, in which to rotate around the z-axis.</param> + /// <returns>The resulting <see cref="Matrix2" />.</returns> + public static Matrix2 CreateRotationZ(float radians) + { + Matrix2 result; + CreateRotationZ(radians, out result); + return result; + } + + /// <summary> + /// Calculates the <see cref="Matrix2" /> struct that can be used to rotate a set of vertices around the z-axis. + /// </summary> + /// <param name="radians">The amount, in radians, in which to rotate around the z-axis.</param> + /// <param name="result">The resulting <see cref="Matrix2" />.</param> + public static void CreateRotationZ(float radians, out Matrix2 result) + { + var val1 = (float)Math.Cos(radians); + var val2 = (float)Math.Sin(radians); + + result = new Matrix2 + { + M11 = val1, + M12 = val2, + M21 = -val2, + M22 = val1, + M31 = 0, + M32 = 0 + }; + } + + /// <summary> + /// Initializes a new instance of the <see cref="Matrix2" /> struct that can be used to scale a set vertices. + /// </summary> + /// <param name="scale">The amount to scale by on the x and y axes.</param> + /// <returns>The resulting <see cref="Matrix2" />.</returns> + public static Matrix2 CreateScale(float scale) + { + Matrix2 result; + CreateScale(scale, scale, out result); + return result; + } + + /// <summary> + /// Calculates the <see cref="Matrix2" /> struct that can be used to scale a set vertices. + /// </summary> + /// <param name="scale">The amount to scale by on the x and y axes.</param> + /// <param name="result">The resulting <see cref="Matrix2" />.</param> + public static void CreateScale(float scale, out Matrix2 result) + { + CreateScale(scale, scale, out result); + } + + /// <summary> + /// Initializes a new instance of the <see cref="Matrix2" /> struct that can be used to scale a set vertices. + /// </summary> + /// <param name="xScale">The amount to scale by on the x-axis.</param> + /// <param name="yScale">The amount to scale by on the y-axis.</param> + /// <returns>The resulting <see cref="Matrix2" />.</returns> + public static Matrix2 CreateScale(float xScale, float yScale) + { + Matrix2 result; + CreateScale(xScale, yScale, out result); + return result; + } + + /// <summary> + /// Calculates the <see cref="Matrix2" /> struct that can be used to scale a set vertices. + /// </summary> + /// <param name="xScale">The amount to scale by on the x-axis.</param> + /// <param name="yScale">The amount to scale by on the y-axis.</param> + /// <param name="result">The resulting <see cref="Matrix2" />.</param> + public static void CreateScale(float xScale, float yScale, out Matrix2 result) + { + result = new Matrix2 + { + M11 = xScale, + M12 = 0, + M21 = 0, + M22 = yScale, + M31 = 0, + M32 = 0 + }; + } + + /// <summary> + /// Initializes a new instance of the <see cref="Matrix2" /> struct that can be used to scale a set vertices. + /// </summary> + /// <param name="scale">The amounts to scale by on the x and y axes.</param> + /// <returns>The resulting <see cref="Matrix2" />.</returns> + public static Matrix2 CreateScale(Vector2 scale) + { + Matrix2 result; + CreateScale(ref scale, out result); + return result; + } + + /// <summary> + /// Calculates the <see cref="Matrix2" /> struct that can be used to scale a set vertices. + /// </summary> + /// <param name="scale">The amounts to scale by on the x and y axes.</param> + /// <param name="result">The resulting <see cref="Matrix2" />.</param> + public static void CreateScale(ref Vector2 scale, out Matrix2 result) + { + result = new Matrix2 + { + M11 = scale.X, + M12 = 0, + M21 = 0, + M22 = scale.Y, + M31 = 0, + M32 = 0 + }; + } + + /// <summary> + /// Initializes a new instance of the <see cref="Matrix2" /> struct that can be used to translate a set vertices. + /// </summary> + /// <param name="xPosition">The amount to translate by on the x-axis.</param> + /// <param name="yPosition">The amount to translate by on the y-axis.</param> + /// <returns>The resulting <see cref="Matrix2" />.</returns> + public static Matrix2 CreateTranslation(float xPosition, float yPosition) + { + Matrix2 result; + CreateTranslation(xPosition, yPosition, out result); + return result; + } + + /// <summary> + /// Calculates the <see cref="Matrix2" /> struct that can be used to translate a set vertices. + /// </summary> + /// <param name="xPosition">The amount to translate by on the x-axis.</param> + /// <param name="yPosition">The amount to translate by on the y-axis.</param> + /// <param name="result">The resulting <see cref="Matrix2" />.</param> + public static void CreateTranslation(float xPosition, float yPosition, out Matrix2 result) + { + result = new Matrix2 + { + M11 = 1, + M12 = 0, + M21 = 0, + M22 = 1, + M31 = xPosition, + M32 = yPosition + }; + } + + /// <summary> + /// Initializes a new instance of the <see cref="Matrix2" /> struct that can be used to translate a set vertices. + /// </summary> + /// <param name="position">The amounts to translate by on the x and y axes.</param> + /// <returns>The resulting <see cref="Matrix2" />.</returns> + public static Matrix2 CreateTranslation(Vector2 position) + { + Matrix2 result; + CreateTranslation(ref position, out result); + return result; + } + + /// <summary> + /// Calculates the <see cref="Matrix2" /> struct that can be used to translate a set vertices. + /// </summary> + /// <param name="position">The amounts to translate by on the x and y axes.</param> + /// <param name="result">The resulting <see cref="Matrix2" />.</param> + public static void CreateTranslation(ref Vector2 position, out Matrix2 result) + { + result = new Matrix2 + { + M11 = 1, + M12 = 0, + M21 = 0, + M22 = 1, + M31 = position.X, + M32 = position.Y + }; + } + + /// <summary> + /// Calculates the determinant of the <see cref="Matrix2" />. + /// </summary> + /// <returns>The determinant of the <see cref="Matrix2" />.</returns> + public float Determinant() + { + return M11 * M22 - M12 * M21; + } + + /// <summary> + /// Initializes a new instance of the <see cref="Matrix2" /> struct with the summation of two <see cref="Matrix2" /> + /// s. + /// </summary> + /// <param name="matrix1">The first <see cref="Matrix2" />.</param> + /// <param name="matrix2">The second <see cref="Matrix2" />.</param> + /// <returns>The resulting <see cref="Matrix2" />.</returns> + public static Matrix2 Add(Matrix2 matrix1, Matrix2 matrix2) + { + matrix1.M11 += matrix2.M11; + matrix1.M12 += matrix2.M12; + + matrix1.M21 += matrix2.M21; + matrix1.M22 += matrix2.M22; + + matrix1.M31 += matrix2.M31; + matrix1.M32 += matrix2.M32; + + return matrix1; + } + + /// <summary> + /// Calculates the <see cref="Matrix2" /> summation of two <see cref="Matrix2" />s. + /// </summary> + /// <param name="matrix1">The first <see cref="Matrix2" />.</param> + /// <param name="matrix2">The second <see cref="Matrix2" />.</param> + /// <param name="result">The resulting <see cref="Matrix2" />.</param> + public static void Add(ref Matrix2 matrix1, ref Matrix2 matrix2, out Matrix2 result) + { + result = new Matrix2 + { + M11 = matrix1.M11 + matrix2.M11, + M12 = matrix1.M12 + matrix2.M12, + M21 = matrix1.M21 + matrix2.M21, + M22 = matrix1.M22 + matrix2.M22, + M31 = matrix1.M31 + matrix2.M31, + M32 = matrix1.M32 + matrix2.M32 + }; + } + + /// <summary> + /// Initializes a new instance of the <see cref="Matrix2" /> struct with the summation of two <see cref="Matrix2" /> + /// s. + /// </summary> + /// <param name="matrix1">The first <see cref="Matrix2" />.</param> + /// <param name="matrix2">The second <see cref="Matrix2" />.</param> + /// <returns>The resulting <see cref="Matrix2" />.</returns> + public static Matrix2 operator +(Matrix2 matrix1, Matrix2 matrix2) + { + matrix1.M11 = matrix1.M11 + matrix2.M11; + matrix1.M12 = matrix1.M12 + matrix2.M12; + + matrix1.M21 = matrix1.M21 + matrix2.M21; + matrix1.M22 = matrix1.M22 + matrix2.M22; + + matrix1.M31 = matrix1.M31 + matrix2.M31; + matrix1.M32 = matrix1.M32 + matrix2.M32; + + return matrix1; + } + + /// <summary> + /// Initializes a new instance of the <see cref="Matrix2" /> struct with the substraction of two + /// <see cref="Matrix2" /> + /// s. + /// </summary> + /// <param name="matrix1">The first <see cref="Matrix2" />.</param> + /// <param name="matrix2">The second <see cref="Matrix2" />.</param> + /// <returns>The resulting <see cref="Matrix2" />.</returns> + public static Matrix2 Subtract(Matrix2 matrix1, Matrix2 matrix2) + { + matrix1.M11 = matrix1.M11 - matrix2.M11; + matrix1.M12 = matrix1.M12 - matrix2.M12; + + matrix1.M21 = matrix1.M21 - matrix2.M21; + matrix1.M22 = matrix1.M22 - matrix2.M22; + + matrix1.M31 = matrix1.M31 - matrix2.M31; + matrix1.M32 = matrix1.M32 - matrix2.M32; + return matrix1; + } + + /// <summary> + /// Calculates the <see cref="Matrix2" /> substraction of two <see cref="Matrix2" />s. + /// </summary> + /// <param name="matrix1">The first <see cref="Matrix2" />.</param> + /// <param name="matrix2">The second <see cref="Matrix2" />.</param> + /// <param name="result">The resulting <see cref="Matrix2" />.</param> + public static void Subtract(ref Matrix2 matrix1, ref Matrix2 matrix2, out Matrix2 result) + { + result = new Matrix2 + { + M11 = matrix1.M11 - matrix2.M11, + M12 = matrix1.M12 - matrix2.M12, + M21 = matrix1.M21 - matrix2.M21, + M22 = matrix1.M22 - matrix2.M22, + M31 = matrix1.M31 - matrix2.M31, + M32 = matrix1.M32 - matrix2.M32 + }; + } + + /// <summary> + /// Initializes a new instance of the <see cref="Matrix2" /> struct with the substraction of two + /// <see cref="Matrix2" /> + /// s. + /// </summary> + /// <param name="matrix1">The first <see cref="Matrix2" />.</param> + /// <param name="matrix2">The second <see cref="Matrix2" />.</param> + /// <returns>The resulting <see cref="Matrix2" />.</returns> + public static Matrix2 operator -(Matrix2 matrix1, Matrix2 matrix2) + { + matrix1.M11 = matrix1.M11 - matrix2.M11; + matrix1.M12 = matrix1.M12 - matrix2.M12; + + matrix1.M21 = matrix1.M21 - matrix2.M21; + matrix1.M22 = matrix1.M22 - matrix2.M22; + + matrix1.M31 = matrix1.M31 - matrix2.M31; + matrix1.M32 = matrix1.M32 - matrix2.M32; + return matrix1; + } + + /// <summary> + /// Initializes a new instance of the <see cref="Matrix2" /> struct with the multiplication of two + /// <see cref="Matrix2" /> + /// s. + /// </summary> + /// <param name="matrix1">The first <see cref="Matrix2" />.</param> + /// <param name="matrix2">The second <see cref="Matrix2" />.</param> + /// <returns>The resulting <see cref="Matrix2" />.</returns> + public static Matrix2 Multiply(Matrix2 matrix1, Matrix2 matrix2) + { + var m11 = matrix1.M11 * matrix2.M11 + matrix1.M12 * matrix2.M21; + var m12 = matrix1.M11 * matrix2.M12 + matrix1.M12 * matrix2.M22; + + var m21 = matrix1.M21 * matrix2.M11 + matrix1.M22 * matrix2.M21; + var m22 = matrix1.M21 * matrix2.M12 + matrix1.M22 * matrix2.M22; + + var m31 = matrix1.M31 * matrix2.M11 + matrix1.M32 * matrix2.M21 + matrix2.M31; + var m32 = matrix1.M31 * matrix2.M12 + matrix1.M32 * matrix2.M22 + matrix2.M32; + + matrix1.M11 = m11; + matrix1.M12 = m12; + + matrix1.M21 = m21; + matrix1.M22 = m22; + + matrix1.M31 = m31; + matrix1.M32 = m32; + + return matrix1; + } + + /// <summary> + /// Calculates the <see cref="Matrix2" /> multiplication of two <see cref="Matrix2" />s. + /// </summary> + /// <param name="matrix1">The first <see cref="Matrix2" />.</param> + /// <param name="matrix2">The second <see cref="Matrix2" />.</param> + /// <param name="result">The resulting <see cref="Matrix2" />.</param> + public static void Multiply(ref Matrix2 matrix1, ref Matrix2 matrix2, out Matrix2 result) + { + var m11 = matrix1.M11 * matrix2.M11 + matrix1.M12 * matrix2.M21; + var m12 = matrix1.M11 * matrix2.M12 + matrix1.M12 * matrix2.M22; + + var m21 = matrix1.M21 * matrix2.M11 + matrix1.M22 * matrix2.M21; + var m22 = matrix1.M21 * matrix2.M12 + matrix1.M22 * matrix2.M22; + + var m31 = matrix1.M31 * matrix2.M11 + matrix1.M32 * matrix2.M21 + matrix2.M31; + var m32 = matrix1.M31 * matrix2.M12 + matrix1.M32 * matrix2.M22 + matrix2.M32; + + result = new Matrix2 + { + M11 = m11, + M12 = m12, + M21 = m21, + M22 = m22, + M31 = m31, + M32 = m32 + }; + } + + /// <summary> + /// Initializes a new instance of the <see cref="Matrix2" /> struct with the division of a <see cref="Matrix2" /> by + /// a scalar. + /// </summary> + /// <param name="matrix">The <see cref="Matrix2" />.</param> + /// <param name="scalar">The amount to divide the <see cref="Matrix2" /> by.</param> + /// <returns>The resulting <see cref="Matrix2" />.</returns> + public static Matrix2 Multiply(Matrix2 matrix, float scalar) + { + matrix.M11 *= scalar; + matrix.M12 *= scalar; + + matrix.M21 *= scalar; + matrix.M22 *= scalar; + + matrix.M31 *= scalar; + matrix.M32 *= scalar; + return matrix; + } + + /// <summary> + /// Calculates the multiplication of a <see cref="Matrix2" /> by a scalar. + /// </summary> + /// <param name="matrix">The <see cref="Matrix2" />.</param> + /// <param name="scalar">The amount to multiple the <see cref="Matrix2" /> by.</param> + /// <param name="result">The resulting <see cref="Matrix2" />.</param> + public static void Multiply(ref Matrix2 matrix, float scalar, out Matrix2 result) + { + result = new Matrix2 + { + M11 = matrix.M11 * scalar, + M12 = matrix.M12 * scalar, + M21 = matrix.M21 * scalar, + M22 = matrix.M22 * scalar, + M31 = matrix.M31 * scalar, + M32 = matrix.M32 * scalar + }; + } + + /// <summary> + /// Initializes a new instance of the <see cref="Matrix2" /> struct with the multiplication of two + /// <see cref="Matrix2" /> + /// s. + /// </summary> + /// <param name="matrix1">The first <see cref="Matrix2" />.</param> + /// <param name="matrix2">The second <see cref="Matrix2" />.</param> + /// <returns>The resulting <see cref="Matrix2" />.</returns> + public static Matrix2 operator *(Matrix2 matrix1, Matrix2 matrix2) + { + var m11 = matrix1.M11 * matrix2.M11 + matrix1.M12 * matrix2.M21; + var m12 = matrix1.M11 * matrix2.M12 + matrix1.M12 * matrix2.M22; + + var m21 = matrix1.M21 * matrix2.M11 + matrix1.M22 * matrix2.M21; + var m22 = matrix1.M21 * matrix2.M12 + matrix1.M22 * matrix2.M22; + + var m31 = matrix1.M31 * matrix2.M11 + matrix1.M32 * matrix2.M21 + matrix2.M31; + var m32 = matrix1.M31 * matrix2.M12 + matrix1.M32 * matrix2.M22 + matrix2.M32; + + matrix1.M11 = m11; + matrix1.M12 = m12; + + matrix1.M21 = m21; + matrix1.M22 = m22; + + matrix1.M31 = m31; + matrix1.M32 = m32; + + return matrix1; + } + + /// <summary> + /// Initializes a new instance of the <see cref="Matrix2" /> struct with the division of a <see cref="Matrix2" /> by + /// a scalar. + /// </summary> + /// <param name="matrix">The <see cref="Matrix2" />.</param> + /// <param name="scalar">The amount to divide the <see cref="Matrix2" /> by.</param> + /// <returns>The resulting <see cref="Matrix2" />.</returns> + public static Matrix2 operator *(Matrix2 matrix, float scalar) + { + matrix.M11 = matrix.M11 * scalar; + matrix.M12 = matrix.M12 * scalar; + + matrix.M21 = matrix.M21 * scalar; + matrix.M22 = matrix.M22 * scalar; + + matrix.M31 = matrix.M31 * scalar; + matrix.M32 = matrix.M32 * scalar; + + return matrix; + } + + /// <summary> + /// Initializes a new instance of the <see cref="Matrix2" /> struct with the division of two <see cref="Matrix2" /> + /// s. + /// </summary> + /// <param name="matrix1">The first <see cref="Matrix2" />.</param> + /// <param name="matrix2">The second <see cref="Matrix2" />.</param> + /// <returns>The resulting <see cref="Matrix2" />.</returns> + public static Matrix2 Divide(Matrix2 matrix1, Matrix2 matrix2) + { + matrix1.M11 = matrix1.M11 / matrix2.M11; + matrix1.M12 = matrix1.M12 / matrix2.M12; + + matrix1.M21 = matrix1.M21 / matrix2.M21; + matrix1.M22 = matrix1.M22 / matrix2.M22; + + matrix1.M31 = matrix1.M31 / matrix2.M31; + matrix1.M32 = matrix1.M32 / matrix2.M32; + return matrix1; + } + + /// <summary> + /// Calculates the <see cref="Matrix2" /> division of two <see cref="Matrix2" />s. + /// </summary> + /// <param name="matrix1">The first <see cref="Matrix2" />.</param> + /// <param name="matrix2">The second <see cref="Matrix2" />.</param> + /// <param name="result">The resulting <see cref="Matrix2" />.</param> + public static void Divide(ref Matrix2 matrix1, ref Matrix2 matrix2, out Matrix2 result) + { + result = new Matrix2 + { + M11 = matrix1.M11 / matrix2.M11, + M12 = matrix1.M12 / matrix2.M12, + M21 = matrix1.M21 / matrix2.M21, + M22 = matrix1.M22 / matrix2.M22, + M31 = matrix1.M31 / matrix2.M31, + M32 = matrix1.M32 / matrix2.M32 + }; + } + + /// <summary> + /// Initializes a new instance of the <see cref="Matrix2" /> struct with the division of a <see cref="Matrix2" /> by + /// a scalar. + /// </summary> + /// <param name="matrix">The <see cref="Matrix2" />.</param> + /// <param name="scalar">The amount to divide the <see cref="Matrix2" /> by.</param> + /// <returns>The resulting <see cref="Matrix2" />.</returns> + public static Matrix2 Divide(Matrix2 matrix, float scalar) + { + var num = 1f / scalar; + matrix.M11 = matrix.M11 * num; + matrix.M12 = matrix.M12 * num; + + matrix.M21 = matrix.M21 * num; + matrix.M22 = matrix.M22 * num; + + matrix.M31 = matrix.M31 * num; + matrix.M32 = matrix.M32 * num; + + return matrix; + } + + /// <summary> + /// Calculates the division of a <see cref="Matrix2" /> by a scalar. + /// </summary> + /// <param name="matrix">The <see cref="Matrix2" />.</param> + /// <param name="scalar">The amount to divide the <see cref="Matrix2" /> by.</param> + /// <param name="result">The resulting <see cref="Matrix2" />.</param> + public static void Divide(ref Matrix2 matrix, float scalar, out Matrix2 result) + { + var num = 1f / scalar; + result = new Matrix2 + { + M11 = matrix.M11 * num, + M12 = matrix.M12 * num, + M21 = matrix.M21 * num, + M22 = matrix.M22 * num, + M31 = matrix.M31 * num, + M32 = matrix.M32 * num + }; + } + + /// <summary> + /// Initializes a new instance of the <see cref="Matrix2" /> struct with the division of two <see cref="Matrix2" /> + /// s. + /// </summary> + /// <param name="matrix1">The first <see cref="Matrix2" />.</param> + /// <param name="matrix2">The second <see cref="Matrix2" />.</param> + /// <returns>The resulting <see cref="Matrix2" />.</returns> + public static Matrix2 operator /(Matrix2 matrix1, Matrix2 matrix2) + { + matrix1.M11 = matrix1.M11 / matrix2.M11; + matrix1.M12 = matrix1.M12 / matrix2.M12; + + matrix1.M21 = matrix1.M21 / matrix2.M21; + matrix1.M22 = matrix1.M22 / matrix2.M22; + + matrix1.M31 = matrix1.M31 / matrix2.M31; + matrix1.M32 = matrix1.M32 / matrix2.M32; + return matrix1; + } + + /// <summary> + /// Initializes a new instance of the <see cref="Matrix2" /> struct with the division of a <see cref="Matrix2" /> by + /// a scalar. + /// </summary> + /// <param name="matrix">The <see cref="Matrix2" />.</param> + /// <param name="scalar">The amount to divide the <see cref="Matrix2" /> by.</param> + /// <returns>The resulting <see cref="Matrix2" />.</returns> + public static Matrix2 operator /(Matrix2 matrix, float scalar) + { + var num = 1f / scalar; + matrix.M11 = matrix.M11 * num; + matrix.M12 = matrix.M12 * num; + + matrix.M21 = matrix.M21 * num; + matrix.M22 = matrix.M22 * num; + + matrix.M31 = matrix.M31 * num; + matrix.M32 = matrix.M32 * num; + return matrix; + } + + /// <summary> + /// Initializes a new instance of the <see cref="Matrix2" /> struct with the inversion of a <see cref="Matrix2" />. + /// </summary> + /// <param name="matrix">The <see cref="Matrix2" />.</param> + /// <returns>The resulting <see cref="Matrix2" />.</returns> + public static Matrix2 Invert(Matrix2 matrix) + { + var det = 1 / matrix.Determinant(); + + var m11 = matrix.M22 * det; + var m12 = -matrix.M12 * det; + + var m21 = -matrix.M21 * det; + var m22 = matrix.M11 * det; + + var m31 = (matrix.M32 * matrix.M21 - matrix.M31 * matrix.M22) * det; + var m32 = -(matrix.M32 * matrix.M11 - matrix.M31 * matrix.M12) * det; + + return new Matrix2(m11, m12, m21, m22, m31, m32); + } + + /// <summary> + /// Calculates the inversion of a <see cref="Matrix2" />. + /// </summary> + /// <param name="matrix">The <see cref="Matrix2" />.</param> + /// <param name="result">The resulting <see cref="Matrix2" />.</param> + public static void Invert(ref Matrix2 matrix, out Matrix2 result) + { + var det = 1 / matrix.Determinant(); + + result = new Matrix2 + { + M11 = matrix.M22 * det, + M12 = -matrix.M12 * det, + M21 = -matrix.M21 * det, + M22 = matrix.M11 * det, + M31 = (matrix.M32 * matrix.M21 - matrix.M31 * matrix.M22) * det, + M32 = -(matrix.M32 * matrix.M11 - matrix.M31 * matrix.M12) * det + }; + } + + /// <summary> + /// Initializes a new instance of the <see cref="Matrix2" /> struct with the inversion of a <see cref="Matrix2" />. + /// </summary> + /// <param name="matrix">The <see cref="Matrix2" />.</param> + /// <returns>The resulting <see cref="Matrix2" />.</returns> + public static Matrix2 operator -(Matrix2 matrix) + { + matrix.M11 = -matrix.M11; + matrix.M12 = -matrix.M12; + + matrix.M21 = -matrix.M21; + matrix.M22 = -matrix.M22; + + matrix.M31 = -matrix.M31; + matrix.M32 = -matrix.M32; + return matrix; + } + + /// <summary> + /// Compares a <see cref="Matrix2" /> for equality with another <see cref="Matrix2" /> without any tolerance. + /// </summary> + /// <param name="matrix1">The first <see cref="Matrix2" />.</param> + /// <param name="matrix2">The second <see cref="Matrix2" />.</param> + /// <returns><c>true</c> if the <see cref="Matrix2" />s are equal; <c>false</c> otherwise.</returns> + public static bool operator ==(Matrix2 matrix1, Matrix2 matrix2) + { + return (matrix1.M11 == matrix2.M11) && (matrix1.M12 == matrix2.M12) && (matrix1.M21 == matrix2.M21) && + (matrix1.M22 == matrix2.M22) && (matrix1.M31 == matrix2.M31) && (matrix1.M32 == matrix2.M32); + } + + /// <summary> + /// Compares a <see cref="Matrix2" /> for inequality with another <see cref="Matrix2" /> without any tolerance. + /// </summary> + /// <param name="matrix1">The first <see cref="Matrix2" />.</param> + /// <param name="matrix2">The second <see cref="Matrix2" />.</param> + /// <returns><c>true</c> if the <see cref="Matrix2" />s are not equal; <c>false</c> otherwise.</returns> + public static bool operator !=(Matrix2 matrix1, Matrix2 matrix2) + { + return (matrix1.M11 != matrix2.M11) || (matrix1.M12 != matrix2.M12) || (matrix1.M21 != matrix2.M21) || + (matrix1.M22 != matrix2.M22) || (matrix1.M31 != matrix2.M31) || (matrix1.M32 != matrix2.M32); + } + + /// <summary> + /// Returns a value that indicates whether the current <see cref="Matrix2" /> is equal to a specified + /// <see cref="Matrix2" />. + /// </summary> + /// <param name="matrix">The <see cref="Matrix2" /> with which to make the comparison.</param> + /// <returns> + /// <c>true</c> if the current <see cref="Matrix2" /> is equal to the specified <see cref="Matrix2" />; + /// <c>false</c> otherwise. + /// </returns> + public bool Equals(ref Matrix2 matrix) + { + return (M11 == matrix.M11) && (M12 == matrix.M12) && (M21 == matrix.M21) && (M22 == matrix.M22) && + (M31 == matrix.M31) && (M32 == matrix.M32); + } + + /// <summary> + /// Returns a value that indicates whether the current <see cref="Matrix2" /> is equal to a specified + /// <see cref="Matrix2" />. + /// </summary> + /// <param name="matrix">The <see cref="Matrix2" /> with which to make the comparison.</param> + /// <returns> + /// <c>true</c> if the current <see cref="Matrix2" /> is equal to the specified <see cref="Matrix2" />; + /// <c>false</c> otherwise. + /// </returns> + public bool Equals(Matrix2 matrix) + { + return Equals(ref matrix); + } + + /// <summary> + /// Returns a value that indicates whether the current <see cref="Matrix2" /> is equal to a specified object. + /// </summary> + /// <param name="obj">The object with which to make the comparison.</param> + /// <returns> + /// <c>true</c> if the current <see cref="Matrix2" /> is equal to the specified object; + /// <c>false</c> otherwise. + /// </returns> + public override bool Equals(object obj) + { + return obj is Matrix2 && Equals((Matrix2)obj); + } + + /// <summary> + /// Returns a hash code for this <see cref="Matrix2" />. + /// </summary> + /// <returns> + /// A hash code for this <see cref="Matrix2" />, suitable for use in hashing algorithms and data structures like a + /// hash table. + /// </returns> + public override int GetHashCode() + { + // ReSharper disable NonReadonlyMemberInGetHashCode + return M11.GetHashCode() + M12.GetHashCode() + M21.GetHashCode() + M22.GetHashCode() + M31.GetHashCode() + + M32.GetHashCode(); + // ReSharper restore NonReadonlyMemberInGetHashCode + } + + /// <summary> + /// Performs an implicit conversion from <see cref="Matrix2" /> to <see cref="Matrix" />. + /// </summary> + /// <param name="matrix">The <see cref="Matrix2" />.</param> + /// <returns> + /// The resulting <see cref="Matrix" />. + /// </returns> + public static implicit operator Matrix(Matrix2 matrix) + { + return new Matrix(matrix.M11, matrix.M12, 0, 0, matrix.M21, matrix.M22, 0, 0, 0, 0, 1, 0, matrix.M31, + matrix.M32, 0, 1); + } + + /// <summary> + /// Performs an explicit conversion from a specified <see cref="Matrix2" /> to a <see cref="Matrix" />. + /// </summary> + /// <param name="matrix">The <see cref="Matrix2" />.</param> + /// <param name="depth">The depth value.</param> + /// <param name="result">The resulting <see cref="Matrix" />.</param> + public static void ToMatrix(ref Matrix2 matrix, float depth, out Matrix result) + { + result.M11 = matrix.M11; + result.M12 = matrix.M12; + result.M13 = 0; + result.M14 = 0; + + result.M21 = matrix.M21; + result.M22 = matrix.M22; + result.M23 = 0; + result.M24 = 0; + + result.M31 = 0; + result.M32 = 0; + result.M33 = 1; + result.M34 = 0; + + result.M41 = matrix.M31; + result.M42 = matrix.M32; + result.M43 = depth; + result.M44 = 1; + } + + /// <summary> + /// Performs an explicit conversion from a specified <see cref="Matrix2" /> to a <see cref="Matrix" />. + /// </summary> + /// <param name="depth">The depth value.</param> + /// <returns>The resulting <see cref="Matrix" />.</returns> + public Matrix ToMatrix(float depth = 0) + { + Matrix result; + ToMatrix(ref this, depth, out result); + return result; + } + + /// <summary> + /// Gets the debug display string. + /// </summary> + /// <value> + /// The debug display string. + /// </value> + internal string DebugDisplayString => this == Identity + ? "Identity" + : $"T:({Translation.X:0.##},{Translation.Y:0.##}), R:{MathHelper.ToDegrees(Rotation):0.##}°, S:({Scale.X:0.##},{Scale.Y:0.##})" + ; + + /// <summary> + /// Returns a <see cref="string" /> that represents this <see cref="Matrix2" />. + /// </summary> + /// <returns> + /// A <see cref="string" /> that represents this <see cref="Matrix2" />. + /// </returns> + public override string ToString() + { + return $"{{M11:{M11} M12:{M12}}} {{M21:{M21} M22:{M22}}} {{M31:{M31} M32:{M32}}}"; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/MatrixExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/MatrixExtensions.cs new file mode 100644 index 0000000..f69da97 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/MatrixExtensions.cs @@ -0,0 +1,28 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + public static class MatrixExtensions + { + public static bool Decompose(this Matrix matrix, out Vector2 position, out float rotation, out Vector2 scale) + { + Vector3 position3, scale3; + Quaternion rotationQuaternion; + + if (matrix.Decompose(out scale3, out rotationQuaternion, out position3)) + { + var direction = Vector2.Transform(Vector2.UnitX, rotationQuaternion); + rotation = (float) Math.Atan2(direction.Y, direction.X); + position = new Vector2(position3.X, position3.Y); + scale = new Vector2(scale3.X, scale3.Y); + return true; + } + + position = Vector2.Zero; + rotation = 0; + scale = Vector2.One; + return false; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/OrientedRectangle.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/OrientedRectangle.cs new file mode 100644 index 0000000..1439e9f --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/OrientedRectangle.cs @@ -0,0 +1,302 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.Xna.Framework; +using SharpDX.Direct3D; + +namespace MonoGame.Extended +{ + // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.4; Bounding Volumes - Oriented Bounding Boxes (OBBs), pg 101. + + /// <summary> + /// An oriented bounding rectangle is a rectangular block, much like a bounding rectangle + /// <see cref="BoundingRectangle" /> but with an arbitrary orientation <see cref="Vector2" />. + /// </summary> + /// <seealso cref="IEquatable{T}" /> + [DebuggerDisplay($"{{{nameof(DebugDisplayString)},nq}}")] + public struct OrientedRectangle : IEquatable<OrientedRectangle>, IShapeF + { + /// <summary> + /// The centre position of this <see cref="OrientedRectangle" />. + /// </summary> + public Point2 Center; + + /// <summary> + /// The distance from the <see cref="Center" /> point along both axes to any point on the boundary of this + /// <see cref="OrientedRectangle" />. + /// </summary> + /// + public Vector2 Radii; + + /// <summary> + /// The rotation matrix <see cref="Matrix2" /> of the bounding rectangle <see cref="OrientedRectangle" />. + /// </summary> + public Matrix2 Orientation; + + /// <summary> + /// Initializes a new instance of the <see cref="BoundingRectangle" /> structure from the specified centre + /// <see cref="Point2" /> and the radii <see cref="Size2" />. + /// </summary> + /// <param name="center">The centre <see cref="Point2" />.</param> + /// <param name="radii">The radii <see cref="Vector2" />.</param> + /// <param name="orientation">The orientation <see cref="Matrix2" />.</param> + public OrientedRectangle(Point2 center, Size2 radii, Matrix2 orientation) + { + Center = center; + Radii = radii; + Orientation = orientation; + } + + /// <summary> + /// Gets a list of points defining the corner points of the oriented rectangle. + /// </summary> + public IReadOnlyList<Vector2> Points + { + get + { + var topLeft = -Radii; + var bottomLeft = -new Vector2(Radii.X, -Radii.Y); + var topRight = (Vector2)new Point2(Radii.X, -Radii.Y); + var bottomRight = Radii; + + return new List<Vector2> + { + Vector2.Transform(topRight, Orientation) + Center, + Vector2.Transform(topLeft, Orientation) + Center, + Vector2.Transform(bottomLeft, Orientation) + Center, + Vector2.Transform(bottomRight, Orientation) + Center + }; + } + } + + public Point2 Position + { + get => Vector2.Transform(-Radii, Orientation) + Center; + set => throw new NotImplementedException(); + } + + public RectangleF BoundingRectangle => (RectangleF)this; + + /// <summary> + /// Computes the <see cref="OrientedRectangle"/> from the specified <paramref name="rectangle"/> + /// transformed by <paramref name="transformMatrix"/>. + /// </summary> + /// <param name="rectangle">The <see cref="OrientedRectangle"/> to transform.</param> + /// <param name="transformMatrix">The <see cref="Matrix2"/> transformation.</param> + /// <returns>A new <see cref="OrientedRectangle"/>.</returns> + public static OrientedRectangle Transform(OrientedRectangle rectangle, ref Matrix2 transformMatrix) + { + Transform(ref rectangle, ref transformMatrix, out var result); + return result; + } + + private static void Transform(ref OrientedRectangle rectangle, ref Matrix2 transformMatrix, out OrientedRectangle result) + { + PrimitivesHelper.TransformOrientedRectangle( + ref rectangle.Center, + ref rectangle.Orientation, + ref transformMatrix); + result = new OrientedRectangle(); + result.Center = rectangle.Center; + result.Radii = rectangle.Radii; + result.Orientation = rectangle.Orientation; + } + + /// <summary> + /// Compares to two <see cref="OrientedRectangle"/> structures. The result specifies whether the + /// the values of the <see cref="Center"/>, <see cref="Radii"/> and <see cref="Orientation"/> are + /// equal. + /// </summary> + /// <param name="left">The left <see cref="OrientedRectangle" />.</param> + /// <param name="right">The right <see cref="OrientedRectangle" />.</param> + /// <returns><c>true</c> if left and right argument are equal; otherwise, <c>false</c>.</returns> + public static bool operator ==(OrientedRectangle left, OrientedRectangle right) + { + return left.Equals(right); + } + + /// <summary> + /// Compares to two <see cref="OrientedRectangle"/> structures. The result specifies whether the + /// the values of the <see cref="Center"/>, <see cref="Radii"/> or <see cref="Orientation"/> are + /// unequal. + /// </summary> + /// <param name="left">The left <see cref="OrientedRectangle" />.</param> + /// <param name="right">The right <see cref="OrientedRectangle" />.</param> + /// <returns><c>true</c> if left and right argument are unequal; otherwise, <c>false</c>.</returns> + public static bool operator !=(OrientedRectangle left, OrientedRectangle right) + { + return !left.Equals(right); + } + + /// <summary> + /// Determines whether two instances of <see cref="OrientedRectangle"/> are equal. + /// </summary> + /// <param name="other">The other <see cref="OrientedRectangle"/>.</param> + /// <returns><c>true</c> if the specified <see cref="OrientedRectangle"/> is equal + /// to the current <see cref="OrientedRectangle"/>; otherwise, <c>false</c>.</returns> + public bool Equals(OrientedRectangle other) + { + return Center.Equals(other.Center) && Radii.Equals(other.Radii) && Orientation.Equals(other.Orientation); + } + + /// <summary> + /// Determines whether two instances of <see cref="OrientedRectangle"/> are equal. + /// </summary> + /// <param name="obj">The <see cref="OrientedRectangle"/> to compare to.</param> + /// <returns><c>true</c> if the specified <see cref="OrientedRectangle"/> is equal + /// to the current <see cref="OrientedRectangle"/>; otherwise, <c>false</c>.</returns> + public override bool Equals(object obj) + { + return obj is OrientedRectangle other && Equals(other); + } + + /// <summary> + /// Returns a hash code for this object instance. + /// </summary> + /// <returns></returns> + public override int GetHashCode() + { + return HashCode.Combine(Center, Radii, Orientation); + } + + /// <summary> + /// Performs an implicit conversion from a <see cref="RectangleF" /> to <see cref="OrientedRectangle" />. + /// </summary> + /// <param name="rectangle">The rectangle to convert.</param> + /// <returns>The resulting <see cref="OrientedRectangle" />.</returns> + public static explicit operator OrientedRectangle(RectangleF rectangle) + { + var radii = new Size2(rectangle.Width * 0.5f, rectangle.Height * 0.5f); + var centre = new Point2(rectangle.X + radii.Width, rectangle.Y + radii.Height); + + return new OrientedRectangle(centre, radii, Matrix2.Identity); + } + + public static explicit operator RectangleF(OrientedRectangle orientedRectangle) + { + var topLeft = -orientedRectangle.Radii; + var rectangle = new RectangleF(topLeft, orientedRectangle.Radii * 2); + var orientation = orientedRectangle.Orientation * Matrix2.CreateTranslation(orientedRectangle.Center); + return RectangleF.Transform(rectangle, ref orientation); + } + + /// <summary> + /// See: + /// https://www.flipcode.com/archives/2D_OBB_Intersection.shtml + /// https://dyn4j.org/2010/01/sat + /// </summary> + /// <param name="rectangle"></param> + /// <param name="other"></param> + /// <returns></returns> + /// <exception cref="NotImplementedException"></exception> + public static (bool Intersects, Vector2 MinimumTranslationVector) Intersects( + OrientedRectangle rectangle, OrientedRectangle other) + { + var corners = rectangle.Points; + var otherCorners = other.Points; + + var allAxis = new[] + { + corners[1] - corners[0], + corners[3] - corners[0], + otherCorners[1] - otherCorners[0], + otherCorners[3] - otherCorners[0], + }; + var normalizedAxis = new[] + { + allAxis[0], + allAxis[1], + allAxis[2], + allAxis[3] + }; + var overlap = 0f; + var minimumTranslationVector = Vector2.Zero; + + // Make the length of each axis 1/edge length, so we know any + // dot product must be less than 1 to fall within the edge. + for (var a = 0; a < normalizedAxis.Length; a++) + { + normalizedAxis[a] /= normalizedAxis[a].LengthSquared(); + } + + for (var a = 0; a < normalizedAxis.Length; a++) + { + var axisProjectedOnto = normalizedAxis[a]; + var originalAxis = allAxis[a]; + + var p1 = Project(corners, axisProjectedOnto); + var p2 = Project(otherCorners, axisProjectedOnto); + + if (!IsOverlapping(p1, p2)) + { + // There was no intersection along this dimension; + // the boxes cannot possibly overlap. + return (false, Vector2.Zero); + } + + var o = GetOverlap(p1, p2); + if (o < overlap || overlap == 0f) + { + overlap = o; + minimumTranslationVector = originalAxis * overlap; + if (p1.Min > p2.Min) + { + minimumTranslationVector = -minimumTranslationVector; + } + } + } + + // There was no dimension along which there is no intersection. + // Therefore, the boxes overlap. + return (true, minimumTranslationVector); + + (float Min, float Max) Project(IReadOnlyList<Vector2> vertices, Vector2 axis) + { + var t = vertices[0].Dot(axis); + + // Find the extent of box 2 on axis a + var min = t; + var max = t; + + for (var c = 1; c < 4; c++) + { + t = vertices[c].Dot(axis); + + if (t < min) + { + min = t; + } + else if (t > max) + { + max = t; + } + } + + return (min, max); + } + + bool IsOverlapping((float Min, float Max) p1, (float Min, float Max) p2) + { + return p1.Min <= p2.Max && p1.Max >= p2.Min; + } + + float GetOverlap((float Min, float Max) p1, (float Min, float Max) p2) + { + return Math.Min(p1.Max, p2.Max) - Math.Max(p1.Min, p2.Min); + } + } + + /// <summary> + /// Returns a <see cref="string" /> that represents this <see cref="OrientedRectangle" />. + /// </summary> + /// <returns> + /// A <see cref="string" /> that represents this <see cref="OrientedRectangle" />. + /// </returns> + public override string ToString() + { + return $"Centre: {Center}, Radii: {Radii}, Orientation: {Orientation}"; + } + + internal string DebugDisplayString => ToString(); + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Point2.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Point2.cs new file mode 100644 index 0000000..5f8de92 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Point2.cs @@ -0,0 +1,380 @@ +using System; +using System.Diagnostics; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 3.2; A Math and Geometry Primer - Coordinate Systems and Points. pg 35 + + /// <summary> + /// A two-dimensional point defined by a 2-tuple of real numbers, (x, y). + /// </summary> + /// <remarks> + /// <para> + /// A point is a position in two-dimensional space, the location of which is described in terms of a + /// two-dimensional coordinate system, given by a reference point, called the origin, and two coordinate axes. + /// </para> + /// <para> + /// A common two-dimensional coordinate system is the Cartesian (or rectangular) coordinate system where the + /// coordinate axes, conventionally denoted the X axis and Y axis, are perpindicular to each other. For the + /// three-dimensional rectangular coordinate system, the third axis is called the Z axis. + /// </para> + /// </remarks> + /// <seealso cref="IEquatable{T}" /> + /// <seealso cref="IEquatableByRef{Point2}" /> + [DebuggerDisplay("{DebugDisplayString,nq}")] + public struct Point2 : IEquatable<Point2>, IEquatableByRef<Point2> + { + /// <summary> + /// Returns a <see cref="Point2" /> with <see cref="X" /> and <see cref="Y" /> equal to <c>0.0f</c>. + /// </summary> + public static readonly Point2 Zero = new Point2(); + + /// <summary> + /// Returns a <see cref="Point2" /> with <see cref="X" /> and <see cref="Y" /> set to not a number. + /// </summary> + public static readonly Point2 NaN = new Point2(float.NaN, float.NaN); + + /// <summary> + /// The x-coordinate of this <see cref="Point2" />. + /// </summary> + public float X; + + /// <summary> + /// The y-coordinate of this <see cref="Point2" />. + /// </summary> + public float Y; + + /// <summary> + /// Initializes a new instance of the <see cref="Point2" /> structure from the specified coordinates. + /// </summary> + /// <param name="x">The x-coordinate.</param> + /// <param name="y">The y-coordinate.</param> + public Point2(float x, float y) + { + X = x; + Y = y; + } + + /// <summary> + /// Compares two <see cref="Point2" /> structures. The result specifies + /// whether the values of the <see cref="X" /> and <see cref="Y" /> + /// fields of the two <see cref="Point2" /> + /// structures are equal. + /// </summary> + /// <param name="first">The first point.</param> + /// <param name="second">The second point.</param> + /// <returns> + /// <c>true</c> if the <see cref="X" /> and <see cref="Y" /> + /// fields of the two <see cref="Point2" /> + /// structures are equal; otherwise, <c>false</c>. + /// </returns> + public static bool operator ==(Point2 first, Point2 second) + { + return first.Equals(ref second); + } + + /// <summary> + /// Indicates whether this <see cref="Point2" /> is equal to another <see cref="Point2" />. + /// </summary> + /// <param name="point">The point.</param> + /// <returns> + /// <c>true</c> if this <see cref="Point2" /> is equal to the <paramref name="point" />; otherwise, + /// <c>false</c>. + /// </returns> + public bool Equals(Point2 point) + { + return Equals(ref point); + } + + /// <summary> + /// Indicates whether this <see cref="Point2" /> is equal to another <see cref="Point2" />. + /// </summary> + /// <param name="point">The point.</param> + /// <returns> + /// <c>true</c> if this <see cref="Point2" /> is equal to the <paramref name="point" /> parameter; otherwise, + /// <c>false</c>. + /// </returns> + public bool Equals(ref Point2 point) + { + // ReSharper disable CompareOfFloatsByEqualityOperator + return (point.X == X) && (point.Y == Y); + // ReSharper restore CompareOfFloatsByEqualityOperator + } + + /// <summary> + /// Returns a value indicating whether this <see cref="Point2" /> is equal to a specified object. + /// </summary> + /// <param name="obj">The object to make the comparison with.</param> + /// <returns> + /// <c>true</c> if this <see cref="Point2" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>. + /// </returns> + public override bool Equals(object obj) + { + if (obj is Point2) + return Equals((Point2) obj); + return false; + } + + /// <summary> + /// Compares two <see cref="Point2" /> structures. The result specifies + /// whether the values of the <see cref="X" /> or <see cref="Y" /> + /// fields of the two <see cref="Point2" /> + /// structures are unequal. + /// </summary> + /// <param name="first">The first point.</param> + /// <param name="second">The second point.</param> + /// <returns> + /// <c>true</c> if the <see cref="X" /> or <see cref="Y" /> + /// fields of the two <see cref="Point2" /> + /// structures are unequal; otherwise, <c>false</c>. + /// </returns> + public static bool operator !=(Point2 first, Point2 second) + { + return !(first == second); + } + + /// <summary> + /// Calculates the <see cref="Point2" /> representing the addition of a <see cref="Point2" /> and a + /// <see cref="Vector2" />. + /// </summary> + /// <param name="point">The point.</param> + /// <param name="vector">The vector.</param> + /// <returns> + /// The <see cref="Point2" /> representing the addition of a <see cref="Point2" /> and a <see cref="Vector2" />. + /// </returns> + public static Point2 operator +(Point2 point, Vector2 vector) + { + return Add(point, vector); + } + + /// <summary> + /// Calculates the <see cref="Point2" /> representing the addition of a <see cref="Point2" /> and a + /// <see cref="Vector2" />. + /// </summary> + /// <param name="point">The point.</param> + /// <param name="vector">The vector.</param> + /// <returns> + /// The <see cref="Point2" /> representing the addition of a <see cref="Point2" /> and a <see cref="Vector2" />. + /// </returns> + public static Point2 Add(Point2 point, Vector2 vector) + { + Point2 p; + p.X = point.X + vector.X; + p.Y = point.Y + vector.Y; + return p; + } + + /// <summary> + /// Calculates the <see cref="Point2" /> representing the subtraction of a <see cref="Point2" /> and a + /// <see cref="Vector2" />. + /// </summary> + /// <param name="point">The point.</param> + /// <param name="vector">The vector.</param> + /// <returns> + /// The <see cref="Point2" /> representing the substraction of a <see cref="Point2" /> and a <see cref="Vector2" />. + /// </returns> + public static Point2 operator -(Point2 point, Vector2 vector) + { + return Subtract(point, vector); + } + + /// <summary> + /// Calculates the <see cref="Point2" /> representing the addition of a <see cref="Point2" /> and a + /// <see cref="Vector2" />. + /// </summary> + /// <param name="point">The point.</param> + /// <param name="vector">The vector.</param> + /// <returns> + /// The <see cref="Point2" /> representing the substraction of a <see cref="Point2" /> and a <see cref="Vector2" />. + /// </returns> + public static Point2 Subtract(Point2 point, Vector2 vector) + { + Point2 p; + p.X = point.X - vector.X; + p.Y = point.Y - vector.Y; + return p; + } + + /// <summary> + /// Calculates the <see cref="Vector2" /> representing the displacement of two <see cref="Point2" /> structures. + /// </summary> + /// <param name="point2">The second point.</param> + /// <param name="point1">The first point.</param> + /// <returns> + /// The <see cref="Vector2" /> representing the displacement of two <see cref="Point2" /> structures. + /// </returns> + public static Vector2 operator -(Point2 point1, Point2 point2) + { + return Displacement(point1, point2); + } + + /// <summary> + /// Calculates the <see cref="Vector2" /> representing the displacement of two <see cref="Point2" /> structures. + /// </summary> + /// <param name="point2">The second point.</param> + /// <param name="point1">The first point.</param> + /// <returns> + /// The <see cref="Vector2" /> representing the displacement of two <see cref="Point2" /> structures. + /// </returns> + public static Vector2 Displacement(Point2 point2, Point2 point1) + { + Vector2 vector; + vector.X = point2.X - point1.X; + vector.Y = point2.Y - point1.Y; + return vector; + } + + /// <summary> + /// Translates a <see cref='Point2' /> by a given <see cref='Size2' />. + /// </summary> + /// <param name="point">The point.</param> + /// <param name="size">The size.</param> + /// <returns> + /// The result of the operator. + /// </returns> + public static Point2 operator +(Point2 point, Size2 size) + { + return Add(point, size); + } + + /// <summary> + /// Translates a <see cref='Point2' /> by a given <see cref='Size2' />. + /// </summary> + /// <param name="point">The point.</param> + /// <param name="size">The size.</param> + /// <returns> + /// The result of the operator. + /// </returns> + public static Point2 Add(Point2 point, Size2 size) + { + return new Point2(point.X + size.Width, point.Y + size.Height); + } + + /// <summary> + /// Translates a <see cref='Point2' /> by the negative of a given <see cref='Size2' />. + /// </summary> + /// <param name="point">The point.</param> + /// <param name="size">The size.</param> + /// <returns> + /// The result of the operator. + /// </returns> + public static Point2 operator -(Point2 point, Size2 size) + { + return Subtract(point, size); + } + + /// <summary> + /// Translates a <see cref='Point2' /> by the negative of a given <see cref='Size2' /> . + /// </summary> + /// <param name="point">The point.</param> + /// <param name="size">The size.</param> + /// <returns> + /// The result of the operator. + /// </returns> + public static Point2 Subtract(Point2 point, Size2 size) + { + return new Point2(point.X - size.Width, point.Y - size.Height); + } + + /// <summary> + /// Returns a hash code of this <see cref="Point2" /> suitable for use in hashing algorithms and data + /// structures like a hash table. + /// </summary> + /// <returns> + /// A hash code of this <see cref="Point2" />. + /// </returns> + public override int GetHashCode() + { + unchecked + { + return (X.GetHashCode()*397) ^ Y.GetHashCode(); + } + } + + /// <summary> + /// Calculates the <see cref="Point2" /> that contains the minimal coordinate values from two <see cref="Point2" /> + /// structures. + /// structures. + /// </summary> + /// <param name="first">The first point.</param> + /// <param name="second">The second point.</param> + /// <returns> + /// The the <see cref="Point2" /> that contains the minimal coordinate values from two <see cref="Point2" /> + /// structures. + /// </returns> + public static Point2 Minimum(Point2 first, Point2 second) + { + return new Point2(first.X < second.X ? first.X : second.X, + first.Y < second.Y ? first.Y : second.Y); + } + + /// <summary> + /// Calculates the <see cref="Point2" /> that contains the maximal coordinate values from two <see cref="Point2" /> + /// structures. + /// structures. + /// </summary> + /// <param name="first">The first point.</param> + /// <param name="second">The second point.</param> + /// <returns> + /// The the <see cref="Point2" /> that contains the maximal coordinate values from two <see cref="Point2" /> + /// structures. + /// </returns> + public static Point2 Maximum(Point2 first, Point2 second) + { + return new Point2(first.X > second.X ? first.X : second.X, + first.Y > second.Y ? first.Y : second.Y); + } + + /// <summary> + /// Performs an implicit conversion from a <see cref="Point2" /> to a position <see cref="Vector2" />. + /// </summary> + /// <param name="point">The point.</param> + /// <returns> + /// The resulting <see cref="Vector2" />. + /// </returns> + public static implicit operator Vector2(Point2 point) + { + return new Vector2(point.X, point.Y); + } + + /// <summary> + /// Performs an implicit conversion from a <see cref="Vector2" /> to a position <see cref="Point2" />. + /// </summary> + /// <param name="vector">The vector.</param> + /// <returns> + /// The resulting <see cref="Point2" />. + /// </returns> + public static implicit operator Point2(Vector2 vector) + { + return new Point2(vector.X, vector.Y); + } + + + /// <summary> + /// Performs an implicit conversion from a <see cref="Point" /> to a <see cref="Point2" />. + /// </summary> + /// <param name="point">The point.</param> + /// <returns> + /// The resulting <see cref="Point2" />. + /// </returns> + public static implicit operator Point2(Point point) + { + return new Point2(point.X, point.Y); + } + + + /// <summary> + /// Returns a <see cref="string" /> that represents this <see cref="Point2" />. + /// </summary> + /// <returns> + /// A <see cref="string" /> that represents this <see cref="Point2" />. + /// </returns> + public override string ToString() + { + return $"({X}, {Y})"; + } + + internal string DebugDisplayString => ToString(); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Point3.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Point3.cs new file mode 100644 index 0000000..cb7b249 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Point3.cs @@ -0,0 +1,379 @@ +using System; +using System.Diagnostics; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + /// <summary> + /// A three-dimensional point defined by a 3-tuple of real numbers, (x, y, z). + /// </summary> + /// <remarks> + /// <para> + /// A point is a position in three-dimensional space, the location of which is described in terms of a + /// three-dimensional coordinate system, given by a reference point, called the origin, and three coordinate axes. + /// </para> + /// <para> + /// A common three-dimensional coordinate system is the Cartesian (or rectangular) coordinate system where the + /// coordinate axes, conventionally denoted the X axis, Y axis and Z axis, are perpindicular to each other. + /// </para> + /// </remarks> + /// <seealso cref="IEquatable{T}" /> + /// <seealso cref="IEquatableByRef{Point3}" /> + [DebuggerDisplay("{DebugDisplayString,nq}")] + public struct Point3 : IEquatable<Point3>, IEquatableByRef<Point3> + { + /// <summary> + /// Returns a <see cref="Point3" /> with <see cref="X" /> <see cref="Y" /> and <see cref="Z" /> equal to <c>0.0f</c>. + /// </summary> + public static readonly Point3 Zero = new Point3(); + + /// <summary> + /// Returns a <see cref="Point3" /> with <see cref="X" /> <see cref="Y" /> and <see cref="Z" /> set to not a number. + /// </summary> + public static readonly Point3 NaN = new Point3(float.NaN, float.NaN, float.NaN); + + /// <summary> + /// The x-coordinate of this <see cref="Point3" />. + /// </summary> + public float X; + + /// <summary> + /// The y-coordinate of this <see cref="Point3" />. + /// </summary> + public float Y; + + /// <summary> + /// The z-coordinate of this <see cref="Point3" />. + /// </summary> + public float Z; + + /// <summary> + /// Initializes a new instance of the <see cref="Point3" /> structure from the specified coordinates. + /// </summary> + /// <param name="x">The x-coordinate.</param> + /// <param name="y">The y-coordinate.</param> + /// <param name="z">The z-coordinate.</param> + public Point3(float x, float y, float z) + { + X = x; + Y = y; + Z = z; + } + + /// <summary> + /// Compares two <see cref="Point3" /> structures. The result specifies + /// whether the values of the <see cref="X" /> <see cref="Y" /> and <see cref="Z" /> + /// fields of the two <see cref="Point3" /> + /// structures are equal. + /// </summary> + /// <param name="first">The first point.</param> + /// <param name="second">The second point.</param> + /// <returns> + /// <c>true</c> if the <see cref="X" /> <see cref="Y" /> and <see cref="Z" /> + /// fields of the two <see cref="Point3" /> + /// structures are equal; otherwise, <c>false</c>. + /// </returns> + public static bool operator ==(Point3 first, Point3 second) + { + return first.Equals(ref second); + } + + /// <summary> + /// Indicates whether this <see cref="Point3" /> is equal to another <see cref="Point3" />. + /// </summary> + /// <param name="point">The point.</param> + /// <returns> + /// <c>true</c> if this <see cref="Point3" /> is equal to the <paramref name="point" />; otherwise, + /// <c>false</c>. + /// </returns> + public bool Equals(Point3 point) + { + return Equals(ref point); + } + + /// <summary> + /// Indicates whether this <see cref="Point3" /> is equal to another <see cref="Point3" />. + /// </summary> + /// <param name="point">The point.</param> + /// <returns> + /// <c>true</c> if this <see cref="Point3" /> is equal to the <paramref name="point" /> parameter; otherwise, + /// <c>false</c>. + /// </returns> + public bool Equals(ref Point3 point) + { + // ReSharper disable CompareOfFloatsByEqualityOperator + return (point.X == X) && (point.Y == Y) && (point.Z == Z); + // ReSharper restore CompareOfFloatsByEqualityOperator + } + + /// <summary> + /// Returns a value indicating whether this <see cref="Point3" /> is equal to a specified object. + /// </summary> + /// <param name="obj">The object to make the comparison with.</param> + /// <returns> + /// <c>true</c> if this <see cref="Point3" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>. + /// </returns> + public override bool Equals(object obj) + { + if (obj is Point3) + return Equals((Point3) obj); + return false; + } + + /// <summary> + /// Compares two <see cref="Point3" /> structures. The result specifies + /// whether the values of the <see cref="X" /> <see cref="Y" /> or <see cref="Z" /> + /// fields of the two <see cref="Point3" /> + /// structures are unequal. + /// </summary> + /// <param name="first">The first point.</param> + /// <param name="second">The second point.</param> + /// <returns> + /// <c>true</c> if the <see cref="X" /> <see cref="Y" /> or <see cref="Z" /> + /// fields of the two <see cref="Point3" /> + /// structures are unequal; otherwise, <c>false</c>. + /// </returns> + public static bool operator !=(Point3 first, Point3 second) + { + return !(first == second); + } + + /// <summary> + /// Calculates the <see cref="Point3" /> representing the addition of a <see cref="Point3" /> and a + /// <see cref="Vector2" />. + /// </summary> + /// <param name="point">The point.</param> + /// <param name="vector">The vector.</param> + /// <returns> + /// The <see cref="Point3" /> representing the addition of a <see cref="Point3" /> and a <see cref="Vector3" />. + /// </returns> + public static Point3 operator +(Point3 point, Vector3 vector) + { + return Add(point, vector); + } + + /// <summary> + /// Calculates the <see cref="Point3" /> representing the addition of a <see cref="Point3" /> and a + /// <see cref="Vector3" />. + /// </summary> + /// <param name="point">The point.</param> + /// <param name="vector">The vector.</param> + /// <returns> + /// The <see cref="Point3" /> representing the addition of a <see cref="Point3" /> and a <see cref="Vector3" />. + /// </returns> + public static Point3 Add(Point3 point, Vector3 vector) + { + Point3 p; + p.X = point.X + vector.X; + p.Y = point.Y + vector.Y; + p.Z = point.Z + vector.Z; + return p; + } + + /// <summary> + /// Calculates the <see cref="Point3" /> representing the subtraction of a <see cref="Point3" /> and a + /// <see cref="Vector3" />. + /// </summary> + /// <param name="point">The point.</param> + /// <param name="vector">The vector.</param> + /// <returns> + /// The <see cref="Point3" /> representing the substraction of a <see cref="Point3" /> and a <see cref="Vector3" />. + /// </returns> + public static Point3 operator -(Point3 point, Vector3 vector) + { + return Subtract(point, vector); + } + + /// <summary> + /// Calculates the <see cref="Point3" /> representing the addition of a <see cref="Point3" /> and a + /// <see cref="Vector3" />. + /// </summary> + /// <param name="point">The point.</param> + /// <param name="vector">The vector.</param> + /// <returns> + /// The <see cref="Point3" /> representing the substraction of a <see cref="Point3" /> and a <see cref="Vector3" />. + /// </returns> + public static Point3 Subtract(Point3 point, Vector3 vector) + { + Point3 p; + p.X = point.X - vector.X; + p.Y = point.Y - vector.Y; + p.Z = point.Z - vector.Z; + return p; + } + + /// <summary> + /// Calculates the <see cref="Vector3" /> representing the displacement of two <see cref="Point3" /> structures. + /// </summary> + /// <param name="point2">The second point.</param> + /// <param name="point1">The first point.</param> + /// <returns> + /// The <see cref="Vector3" /> representing the displacement of two <see cref="Point3" /> structures. + /// </returns> + public static Vector3 operator -(Point3 point1, Point3 point2) + { + return Displacement(point1, point2); + } + + /// <summary> + /// Calculates the <see cref="Vector3" /> representing the displacement of two <see cref="Point3" /> structures. + /// </summary> + /// <param name="point2">The second point.</param> + /// <param name="point1">The first point.</param> + /// <returns> + /// The <see cref="Vector3" /> representing the displacement of two <see cref="Point3" /> structures. + /// </returns> + public static Vector3 Displacement(Point3 point2, Point3 point1) + { + Vector3 vector; + vector.X = point2.X - point1.X; + vector.Y = point2.Y - point1.Y; + vector.Z = point2.Z - point1.Z; + return vector; + } + + /// <summary> + /// Translates a <see cref='Point3' /> by a given <see cref='Size3' />. + /// </summary> + /// <param name="point">The point.</param> + /// <param name="size">The size.</param> + /// <returns> + /// The result of the operator. + /// </returns> + public static Point3 operator +(Point3 point, Size3 size) + { + return Add(point, size); + } + + /// <summary> + /// Translates a <see cref='Point3' /> by a given <see cref='Size3' />. + /// </summary> + /// <param name="point">The point.</param> + /// <param name="size">The size.</param> + /// <returns> + /// The result of the operator. + /// </returns> + public static Point3 Add(Point3 point, Size3 size) + { + return new Point3(point.X + size.Width, point.Y + size.Height, point.Z + size.Depth); + } + + /// <summary> + /// Translates a <see cref='Point3' /> by the negative of a given <see cref='Size3' />. + /// </summary> + /// <param name="point">The point.</param> + /// <param name="size">The size.</param> + /// <returns> + /// The result of the operator. + /// </returns> + public static Point3 operator -(Point3 point, Size3 size) + { + return Subtract(point, size); + } + + /// <summary> + /// Translates a <see cref='Point3' /> by the negative of a given <see cref='Size3' /> . + /// </summary> + /// <param name="point">The point.</param> + /// <param name="size">The size.</param> + /// <returns> + /// The result of the operator. + /// </returns> + public static Point3 Subtract(Point3 point, Size3 size) + { + return new Point3(point.X - size.Width, point.Y - size.Height, point.Z - size.Depth); + } + + /// <summary> + /// Returns a hash code of this <see cref="Point3" /> suitable for use in hashing algorithms and data + /// structures like a hash table. + /// </summary> + /// <returns> + /// A hash code of this <see cref="Point3" />. + /// </returns> + public override int GetHashCode() + { + unchecked + { + int hash = 17; + hash = hash * 23 + X.GetHashCode(); + hash = hash * 23 + Y.GetHashCode(); + hash = hash * 23 + Z.GetHashCode(); + return hash; + } + } + + /// <summary> + /// Calculates the <see cref="Point3" /> that contains the minimal coordinate values from two <see cref="Point3" /> + /// structures. + /// structures. + /// </summary> + /// <param name="first">The first point.</param> + /// <param name="second">The second point.</param> + /// <returns> + /// The the <see cref="Point3" /> that contains the minimal coordinate values from two <see cref="Point3" /> + /// structures. + /// </returns> + public static Point3 Minimum(Point3 first, Point3 second) + { + return new Point3(first.X < second.X ? first.X : second.X, + first.Y < second.Y ? first.Y : second.Y, + first.Z < second.Z ? first.Z : second.Z); + } + + /// <summary> + /// Calculates the <see cref="Point3" /> that contains the maximal coordinate values from two <see cref="Point3" /> + /// structures. + /// structures. + /// </summary> + /// <param name="first">The first point.</param> + /// <param name="second">The second point.</param> + /// <returns> + /// The the <see cref="Point3" /> that contains the maximal coordinate values from two <see cref="Point3" /> + /// structures. + /// </returns> + public static Point3 Maximum(Point3 first, Point3 second) + { + return new Point3(first.X > second.X ? first.X : second.X, + first.Y > second.Y ? first.Y : second.Y, + first.Z > second.Z ? first.Z : second.Z); + } + + /// <summary> + /// Performs an implicit conversion from a <see cref="Point3" /> to a position <see cref="Vector3" />. + /// </summary> + /// <param name="point">The point.</param> + /// <returns> + /// The resulting <see cref="Vector3" />. + /// </returns> + public static implicit operator Vector3(Point3 point) + { + return new Vector3(point.X, point.Y, point.Z); + } + + /// <summary> + /// Performs an implicit conversion from a <see cref="Vector3" /> to a position <see cref="Point3" />. + /// </summary> + /// <param name="vector">The vector.</param> + /// <returns> + /// The resulting <see cref="Point3" />. + /// </returns> + public static implicit operator Point3(Vector3 vector) + { + return new Point3(vector.X, vector.Y, vector.Z); + } + + /// <summary> + /// Returns a <see cref="string" /> that represents this <see cref="Point3" />. + /// </summary> + /// <returns> + /// A <see cref="string" /> that represents this <see cref="Point3" />. + /// </returns> + public override string ToString() + { + return $"({X}, {Y}, {Z})"; + } + + internal string DebugDisplayString => ToString(); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/PrimitivesHelper.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/PrimitivesHelper.cs new file mode 100644 index 0000000..ed08270 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/PrimitivesHelper.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + internal class PrimitivesHelper + { + // Used by Ray2 and Segment2 + internal static bool IntersectsSlab(float positionCoordinate, float directionCoordinate, float slabMinimum, + float slabMaximum, ref float rayMinimumDistance, ref float rayMaximumDistance) + { + // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 5.3; Basic Primitive Tests - Intersecting Lines, Rays, and (Directed Segments). pg 179-181 + + if (Math.Abs(directionCoordinate) < float.Epsilon) + return (positionCoordinate >= slabMinimum) && (positionCoordinate <= slabMaximum); + + // Compute intersection values of ray with near and far plane of slab + var rayNearDistance = (slabMinimum - positionCoordinate)/directionCoordinate; + var rayFarDistance = (slabMaximum - positionCoordinate)/directionCoordinate; + + if (rayNearDistance > rayFarDistance) + { + // Swap near and far distance + var temp = rayFarDistance; + rayNearDistance = rayFarDistance; + rayFarDistance = temp; + } + + // Compute the intersection of slab intersection intervals + rayMinimumDistance = rayNearDistance > rayMinimumDistance ? rayNearDistance : rayMinimumDistance; + rayMaximumDistance = rayFarDistance < rayMaximumDistance ? rayFarDistance : rayMaximumDistance; + + // Exit with no collision as soon as slab intersection becomes empty + return rayMinimumDistance <= rayMaximumDistance; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void CreateRectangleFromPoints(IReadOnlyList<Point2> points, out Point2 minimum, out Point2 maximum) + { + // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.2; Bounding Volumes - Axis-aligned Bounding Boxes (AABBs). pg 82-84 + + if (points == null || points.Count == 0) + { + minimum = Point2.Zero; + maximum = Point2.Zero; + return; + } + + minimum = maximum = points[0]; + + // ReSharper disable once ForCanBeConvertedToForeach + for (var index = points.Count - 1; index > 0; --index) + { + var point = points[index]; + minimum = Point2.Minimum(minimum, point); + maximum = Point2.Maximum(maximum, point); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void TransformRectangle(ref Point2 center, ref Vector2 halfExtents, ref Matrix2 transformMatrix) + { + // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.2; Bounding Volumes - Axis-aligned Bounding Boxes (AABBs). pg 86-87 + + center = transformMatrix.Transform(center); + var xRadius = halfExtents.X; + var yRadius = halfExtents.Y; + halfExtents.X = xRadius * Math.Abs(transformMatrix.M11) + yRadius * Math.Abs(transformMatrix.M12); + halfExtents.Y = xRadius * Math.Abs(transformMatrix.M21) + yRadius * Math.Abs(transformMatrix.M22); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void TransformOrientedRectangle( + ref Point2 center, + ref Matrix2 orientation, + ref Matrix2 transformMatrix) + { + // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.4; Oriented Bounding Boxes (OBBs), pg 101-105. + + center = transformMatrix.Transform(center); + orientation *= transformMatrix; + // Reset the translation since orientation is only about rotation + orientation.M31 = 0; + orientation.M32 = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static float SquaredDistanceToPointFromRectangle(Point2 minimum, Point2 maximum, Point2 point) + { + // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 5.1.3.1; Basic Primitive Tests - Closest-point Computations - Distance of Point to AABB. pg 130-131 + var squaredDistance = 0.0f; + + // for each axis add up the excess distance outside the box + + // x-axis + if (point.X < minimum.X) + { + var distance = minimum.X - point.X; + squaredDistance += distance * distance; + } + else if (point.X > maximum.X) + { + var distance = maximum.X - point.X; + squaredDistance += distance * distance; + } + + // y-axis + if (point.Y < minimum.Y) + { + var distance = minimum.Y - point.Y; + squaredDistance += distance * distance; + } + else if (point.Y > maximum.Y) + { + var distance = maximum.Y - point.Y; + squaredDistance += distance * distance; + } + return squaredDistance; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void ClosestPointToPointFromRectangle(Point2 minimum, Point2 maximum, Point2 point, out Point2 result) + { + // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 5.1.2; Basic Primitive Tests - Closest-point Computations. pg 130-131 + + result = point; + + // For each coordinate axis, if the point coordinate value is outside box, clamp it to the box, else keep it as is + if (result.X < minimum.X) + result.X = minimum.X; + else if (result.X > maximum.X) + result.X = maximum.X; + + if (result.Y < minimum.Y) + result.Y = minimum.Y; + else if (result.Y > maximum.Y) + result.Y = maximum.Y; + } + + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RandomExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RandomExtensions.cs new file mode 100644 index 0000000..14abb5d --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RandomExtensions.cs @@ -0,0 +1,44 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + public static class RandomExtensions + { + public static int Next(this Random random, Range<int> range) + { + return random.Next(range.Min, range.Max); + } + + public static float NextSingle(this Random random, float min, float max) + { + return (max - min)*NextSingle(random) + min; + } + + public static float NextSingle(this Random random, float max) + { + return max*NextSingle(random); + } + + public static float NextSingle(this Random random) + { + return (float) random.NextDouble(); + } + + public static float NextSingle(this Random random, Range<float> range) + { + return NextSingle(random, range.Min, range.Max); + } + + public static float NextAngle(this Random random) + { + return NextSingle(random, -MathHelper.Pi, MathHelper.Pi); + } + + public static void NextUnitVector(this Random random, out Vector2 vector) + { + var angle = NextAngle(random); + vector = new Vector2((float) Math.Cos(angle), (float) Math.Sin(angle)); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Range.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Range.cs new file mode 100644 index 0000000..5402233 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Range.cs @@ -0,0 +1,84 @@ +using System; + +namespace MonoGame.Extended +{ + /// <summary> + /// Represents a closed interval defined by a minimum and a maximum value of a give type. + /// </summary> + public struct Range<T> : IEquatable<Range<T>> where T : IComparable<T> + { + public Range(T min, T max) + { + if (min.CompareTo(max) > 0 || max.CompareTo(min) < 0) + throw new ArgumentException("Min has to be smaller than or equal to max."); + + Min = min; + Max = max; + } + + public Range(T value) + : this(value, value) + { + } + + /// <summary> + /// Gets the minium value of the <see cref="Range{T}" />. + /// </summary> + public T Min { get; } + + /// <summary> + /// Gets the maximum value of the <see cref="Range{T}" />. + /// </summary> + public T Max { get; } + + + /// <summary> + /// Returns wheter or not this <see cref="Range{T}" /> is degenerate. + /// (Min and Max are the same) + /// </summary> + public bool IsDegenerate => Min.Equals(Max); + + /// <summary> + /// Returns wheter or not this <see cref="Range{T}" /> is proper. + /// (Min and Max are not the same) + /// </summary> + public bool IsProper => !Min.Equals(Max); + + public bool Equals(Range<T> value) => Min.Equals(value.Min) && Max.Equals(value.Max); + + public override bool Equals(object obj) => obj is Range<T> && Equals((Range<T>) obj); + + public override int GetHashCode() => Min.GetHashCode() ^ Max.GetHashCode(); + + public static bool operator ==(Range<T> value1, Range<T> value2) => value1.Equals(value2); + + public static bool operator !=(Range<T> value1, Range<T> value2) => !value1.Equals(value2); + + public static implicit operator Range<T>(T value) => new Range<T>(value, value); + + public override string ToString() => $"Range<{typeof(T).Name}> [{Min} {Max}]"; + + /// <summary> + /// Returns wheter or not the value falls in this <see cref="Range{T}" />. + /// </summary> + public bool IsInBetween(T value, bool minValueExclusive = false, bool maxValueExclusive = false) + { + if (minValueExclusive) + { + if (value.CompareTo(Min) <= 0) + return false; + } + + if (value.CompareTo(Min) < 0) + return false; + + if (maxValueExclusive) + { + if (value.CompareTo(Max) >= 0) + return false; + } + + return value.CompareTo(Max) <= 0; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Ray2.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Ray2.cs new file mode 100644 index 0000000..c8ed7e5 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Ray2.cs @@ -0,0 +1,199 @@ +using System; +using System.Diagnostics; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 3.5; A Math and Geometry Primer - Lines, Rays, and Segments. pg 53-54 + /// <summary> + /// A two dimensional ray defined by a starting <see cref="Point2" /> and a direction <see cref="Vector2" />. + /// </summary> + /// <seealso cref="IEquatable{T}" /> + /// <seealso cref="IEquatableByRef{Ray2}" /> + [DebuggerDisplay("{DebugDisplayString,nq}")] + public struct Ray2 : IEquatable<Ray2>, IEquatableByRef<Ray2> + { + /// <summary> + /// The starting <see cref="Point2" /> of this <see cref="Ray2" />. + /// </summary> + public Point2 Position; + + /// <summary> + /// The direction <see cref="Vector2" /> of this <see cref="Ray2" />. + /// </summary> + public Vector2 Direction; + + /// <summary> + /// Initializes a new instance of the <see cref="Ray2" /> structure from the specified position and direction. + /// </summary> + /// <param name="position">The starting point.</param> + /// <param name="direction">The direction vector.</param> + public Ray2(Point2 position, Vector2 direction) + { + Position = position; + Direction = direction; + } + + /// <summary> + /// Determines whether this <see cref="Ray2" /> intersects with a specified <see cref="BoundingRectangle" />. + /// </summary> + /// <param name="boundingRectangle">The bounding rectangle.</param> + /// <param name="rayNearDistance"> + /// When this method returns, contains the distance along the ray to the first intersection + /// point with the <paramref name="boundingRectangle" />, if an intersection was found; otherwise, + /// <see cref="float.NaN" />. + /// This parameter is passed uninitialized. + /// </param> + /// <param name="rayFarDistance"> + /// When this method returns, contains the distance along the ray to the second intersection + /// point with the <paramref name="boundingRectangle" />, if an intersection was found; otherwise, + /// <see cref="float.NaN" />. + /// This parameter is passed uninitialized. + /// </param> + /// <returns> + /// <c>true</c> if this <see cref="Ray2" /> intersects with <paramref name="boundingRectangle" />; otherwise, + /// <c>false</c>. + /// </returns> + public bool Intersects(BoundingRectangle boundingRectangle, out float rayNearDistance, out float rayFarDistance) + { + // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 5.3; Basic Primitive Tests - Intersecting Lines, Rays, and (Directed Segments). pg 179-181 + + var minimum = boundingRectangle.Center - boundingRectangle.HalfExtents; + var maximum = boundingRectangle.Center + boundingRectangle.HalfExtents; + + // Set to the smallest possible value so the algorithm can find the first hit along the ray + var minimumDistanceAlongRay = float.MinValue; + // Set to the maximum possible value so the algorithm can find the last hit along the ray + var maximumDistanceAlongRay = float.MaxValue; + + // For all relevant slabs which in this case is two. + + // The first, horizontal, slab. + if (!PrimitivesHelper.IntersectsSlab(Position.X, Direction.X, minimum.X, maximum.X, + ref minimumDistanceAlongRay, + ref maximumDistanceAlongRay)) + { + rayNearDistance = rayFarDistance = float.NaN; + return false; + } + + // The second, vertical, slab. + if (!PrimitivesHelper.IntersectsSlab(Position.Y, Direction.Y, minimum.Y, maximum.Y, + ref minimumDistanceAlongRay, + ref maximumDistanceAlongRay)) + { + rayNearDistance = rayFarDistance = float.NaN; + return false; + } + + // Ray intersects the 2 slabs. + rayNearDistance = minimumDistanceAlongRay < 0 ? 0 : minimumDistanceAlongRay; + rayFarDistance = maximumDistanceAlongRay; + return true; + } + + /// <summary> + /// Compares two <see cref="Ray2" /> structures. The result specifies whether the values of the + /// <see cref="Position" /> + /// and <see cref="Direction" /> fields of the two <see cref="Ray2" /> structures are equal. + /// </summary> + /// <param name="first">The first ray.</param> + /// <param name="second">The second ray.</param> + /// <returns> + /// <c>true</c> if the <see cref="Position" /> and <see cref="Direction" /> + /// fields of the two <see cref="Ray2" /> + /// structures are equal; otherwise, <c>false</c>. + /// </returns> + public static bool operator ==(Ray2 first, Ray2 second) + { + return first.Equals(ref second); + } + + /// <summary> + /// Indicates whether this <see cref="Ray2" /> is equal to another <see cref="Ray2" />. + /// </summary> + /// <param name="ray">The ray.</param> + /// <returns> + /// <c>true</c> if this <see cref="Ray2" /> is equal to the <paramref name="ray" /> parameter; otherwise, + /// <c>false</c>. + /// </returns> + public bool Equals(Ray2 ray) + { + return Equals(ref ray); + } + + /// <summary> + /// Indicates whether this <see cref="Ray2" /> is equal to another <see cref="Ray2" />. + /// </summary> + /// <param name="ray">The ray.</param> + /// <returns> + /// <c>true</c> if this <see cref="Ray2" /> is equal to the <paramref name="ray" />; otherwise, + /// <c>false</c>. + /// </returns> + public bool Equals(ref Ray2 ray) + { + // ReSharper disable CompareOfFloatsByEqualityOperator + return (ray.Position == Position) && (ray.Direction == Direction); + // ReSharper restore CompareOfFloatsByEqualityOperator + } + + /// <summary> + /// Returns a value indicating whether this <see cref="Ray2" /> is equal to a specified object. + /// </summary> + /// <param name="obj">The object to make the comparison with.</param> + /// <returns> + /// <c>true</c> if this <see cref="Ray2" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>. + /// </returns> + public override bool Equals(object obj) + { + if (obj is Ray2) + return Equals((Ray2) obj); + return false; + } + + /// <summary> + /// Compares two <see cref="Ray2" /> structures. The result specifies whether the values of the + /// <see cref='Position' /> + /// and <see cref="Direction" /> fields of the two <see cref="Ray2" /> structures are unequal. + /// </summary> + /// <param name="first">The first ray.</param> + /// <param name="second">The second ray.</param> + /// <returns> + /// <c>true</c> if the <see cref="Position" /> and <see cref="Direction" /> + /// fields of the two <see cref="Ray2" /> + /// structures are unequal; otherwise, <c>false</c>. + /// </returns> + public static bool operator !=(Ray2 first, Ray2 second) + { + return !(first == second); + } + + /// <summary> + /// Returns a hash code of this <see cref="Ray2" /> suitable for use in hashing algorithms and data + /// structures like a hash table. + /// </summary> + /// <returns> + /// A hash code of this <see cref="Ray2" />. + /// </returns> + public override int GetHashCode() + { + unchecked + { + return (Position.GetHashCode()*397) ^ Direction.GetHashCode(); + } + } + + /// <summary> + /// Returns a <see cref="string" /> that represents this <see cref="Ray2" />. + /// </summary> + /// <returns> + /// A <see cref="string" /> that represents this <see cref="Ray2" />. + /// </returns> + public override string ToString() + { + return $"Position: {Position}, Direction: {Direction}"; + } + + internal string DebugDisplayString => ToString(); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RectangleExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RectangleExtensions.cs new file mode 100644 index 0000000..a6b917a --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RectangleExtensions.cs @@ -0,0 +1,71 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + public static class RectangleExtensions + { + /// <summary> + /// Gets the corners of the rectangle in a clockwise direction starting at the top left. + /// </summary> + public static Point[] GetCorners(this Rectangle rectangle) + { + var corners = new Point[4]; + corners[0] = new Point(rectangle.Left, rectangle.Top); + corners[1] = new Point(rectangle.Right, rectangle.Top); + corners[2] = new Point(rectangle.Right, rectangle.Bottom); + corners[3] = new Point(rectangle.Left, rectangle.Bottom); + return corners; + } + + /// <summary> + /// Gets the corners of the rectangle in a clockwise direction starting at the top left. + /// </summary> + public static Vector2[] GetCorners(this RectangleF rectangle) + { + var corners = new Vector2[4]; + corners[0] = new Vector2(rectangle.Left, rectangle.Top); + corners[1] = new Vector2(rectangle.Right, rectangle.Top); + corners[2] = new Vector2(rectangle.Right, rectangle.Bottom); + corners[3] = new Vector2(rectangle.Left, rectangle.Bottom); + return corners; + } + + public static Rectangle ToRectangle(this RectangleF rectangle) + { + return new Rectangle((int) rectangle.X, (int) rectangle.Y, (int) rectangle.Width, (int) rectangle.Height); + } + + public static RectangleF ToRectangleF(this Rectangle rectangle) + { + return new RectangleF(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height); + } + + public static Rectangle Clip(this Rectangle rectangle, Rectangle clippingRectangle) + { + var clip = clippingRectangle; + rectangle.X = clip.X > rectangle.X ? clip.X : rectangle.X; + rectangle.Y = clip.Y > rectangle.Y ? clip.Y : rectangle.Y; + rectangle.Width = rectangle.Right > clip.Right ? clip.Right - rectangle.X : rectangle.Width; + rectangle.Height = rectangle.Bottom > clip.Bottom ? clip.Bottom - rectangle.Y : rectangle.Height; + + if (rectangle.Width <= 0 || rectangle.Height <= 0) + return Rectangle.Empty; + + return rectangle; + } + + public static RectangleF Clip(this RectangleF rectangle, RectangleF clippingRectangle) + { + var clip = clippingRectangle; + rectangle.X = clip.X > rectangle.X ? clip.X : rectangle.X; + rectangle.Y = clip.Y > rectangle.Y ? clip.Y : rectangle.Y; + rectangle.Width = rectangle.Right > clip.Right ? clip.Right - rectangle.X : rectangle.Width; + rectangle.Height = rectangle.Bottom > clip.Bottom ? clip.Bottom - rectangle.Y : rectangle.Height; + + if(rectangle.Width <= 0 || rectangle.Height <= 0) + return RectangleF.Empty; + + return rectangle; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RectangleF.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RectangleF.cs new file mode 100644 index 0000000..1a1d685 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/RectangleF.cs @@ -0,0 +1,694 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Serialization; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.2; Bounding Volumes - Axis-aligned Bounding Boxes (AABBs). pg 77 + + /// <summary> + /// An axis-aligned, four sided, two dimensional box defined by a top-left position (<see cref="X" /> and + /// <see cref="Y" />) and a size (<see cref="Width" /> and <see cref="Height" />). + /// </summary> + /// <remarks> + /// <para> + /// An <see cref="RectangleF" /> is categorized by having its faces oriented in such a way that its + /// face normals are at all times parallel with the axes of the given coordinate system. + /// </para> + /// <para> + /// The bounding <see cref="RectangleF" /> of a rotated <see cref="RectangleF" /> will be equivalent or larger + /// in size than the original depending on the angle of rotation. + /// </para> + /// </remarks> + /// <seealso cref="IEquatable{T}" /> + /// <seealso cref="IEquatableByRef{T}" /> + [DataContract] + [DebuggerDisplay("{DebugDisplayString,nq}")] + public struct RectangleF : IEquatable<RectangleF>, IEquatableByRef<RectangleF>, IShapeF + { + /// <summary> + /// The <see cref="RectangleF" /> with <see cref="X" />, <see cref="Y" />, <see cref="Width" /> and + /// <see cref="Height" /> all set to <code>0.0f</code>. + /// </summary> + public static readonly RectangleF Empty = new RectangleF(); + + /// <summary> + /// The x-coordinate of the top-left corner position of this <see cref="RectangleF" />. + /// </summary> + [DataMember] public float X; + + /// <summary> + /// The y-coordinate of the top-left corner position of this <see cref="RectangleF" />. + /// </summary> + [DataMember] public float Y; + + /// <summary> + /// The width of this <see cref="RectangleF" />. + /// </summary> + [DataMember] public float Width; + + /// <summary> + /// The height of this <see cref="RectangleF" />. + /// </summary> + [DataMember] public float Height; + + /// <summary> + /// Gets the x-coordinate of the left edge of this <see cref="RectangleF" />. + /// </summary> + public float Left => X; + + /// <summary> + /// Gets the x-coordinate of the right edge of this <see cref="RectangleF" />. + /// </summary> + public float Right => X + Width; + + /// <summary> + /// Gets the y-coordinate of the top edge of this <see cref="RectangleF" />. + /// </summary> + public float Top => Y; + + /// <summary> + /// Gets the y-coordinate of the bottom edge of this <see cref="RectangleF" />. + /// </summary> + public float Bottom => Y + Height; + + /// <summary> + /// Gets a value indicating whether this <see cref="RectangleF" /> has a <see cref="X" />, <see cref="Y" />, + /// <see cref="Width" />, + /// <see cref="Height" /> all equal to <code>0.0f</code>. + /// </summary> + /// <value> + /// <c>true</c> if this instance is empty; otherwise, <c>false</c>. + /// </value> + public bool IsEmpty => Width.Equals(0) && Height.Equals(0) && X.Equals(0) && Y.Equals(0); + + /// <summary> + /// Gets the <see cref="Point2" /> representing the the top-left of this <see cref="RectangleF" />. + /// </summary> + public Point2 Position + { + get { return new Point2(X, Y); } + set + { + X = value.X; + Y = value.Y; + } + } + + public RectangleF BoundingRectangle => this; + + /// <summary> + /// Gets the <see cref="Size2" /> representing the extents of this <see cref="RectangleF" />. + /// </summary> + public Size2 Size + { + get { return new Size2(Width, Height); } + set + { + Width = value.Width; + Height = value.Height; + } + } + + /// <summary> + /// Gets the <see cref="Point2" /> representing the center of this <see cref="RectangleF" />. + /// </summary> + public Point2 Center => new Point2(X + Width * 0.5f, Y + Height * 0.5f); + + /// <summary> + /// Gets the <see cref="Point2" /> representing the top-left of this <see cref="RectangleF" />. + /// </summary> + public Point2 TopLeft => new Point2(X, Y); + + /// <summary> + /// Gets the <see cref="Point2" /> representing the top-right of this <see cref="RectangleF" />. + /// </summary> + public Point2 TopRight => new Point2(X + Width, Y); + + /// <summary> + /// Gets the <see cref="Point2" /> representing the bottom-left of this <see cref="RectangleF" />. + /// </summary> + public Point2 BottomLeft => new Point2(X, Y + Height); + + /// <summary> + /// Gets the <see cref="Point2" /> representing the bottom-right of this <see cref="RectangleF" />. + /// </summary> + public Point2 BottomRight => new Point2(X + Width, Y + Height); + + /// <summary> + /// Initializes a new instance of the <see cref="RectangleF" /> structure from the specified top-left xy-coordinate + /// <see cref="float" />s, width <see cref="float" /> and height <see cref="float" />. + /// </summary> + /// <param name="x">The x-coordinate.</param> + /// <param name="y">The y-coordinate.</param> + /// <param name="width">The width.</param> + /// <param name="height">The height.</param> + public RectangleF(float x, float y, float width, float height) + { + X = x; + Y = y; + Width = width; + Height = height; + } + + /// <summary> + /// Initializes a new instance of the <see cref="RectangleF" /> structure from the specified top-left + /// <see cref="Point2" /> and the extents <see cref="Size2" />. + /// </summary> + /// <param name="position">The top-left point.</param> + /// <param name="size">The extents.</param> + public RectangleF(Point2 position, Size2 size) + { + X = position.X; + Y = position.Y; + Width = size.Width; + Height = size.Height; + } + + /// <summary> + /// Computes the <see cref="RectangleF" /> from a minimum <see cref="Point2" /> and maximum + /// <see cref="Point2" />. + /// </summary> + /// <param name="minimum">The minimum point.</param> + /// <param name="maximum">The maximum point.</param> + /// <param name="result">The resulting rectangle.</param> + public static void CreateFrom(Point2 minimum, Point2 maximum, out RectangleF result) + { + result.X = minimum.X; + result.Y = minimum.Y; + result.Width = maximum.X - minimum.X; + result.Height = maximum.Y - minimum.Y; + } + + /// <summary> + /// Computes the <see cref="RectangleF" /> from a minimum <see cref="Point2" /> and maximum + /// <see cref="Point2" />. + /// </summary> + /// <param name="minimum">The minimum point.</param> + /// <param name="maximum">The maximum point.</param> + /// <returns>The resulting <see cref="RectangleF" />.</returns> + public static RectangleF CreateFrom(Point2 minimum, Point2 maximum) + { + RectangleF result; + CreateFrom(minimum, maximum, out result); + return result; + } + + /// <summary> + /// Computes the <see cref="RectangleF" /> from a list of <see cref="Point2" /> structures. + /// </summary> + /// <param name="points">The points.</param> + /// <param name="result">The resulting rectangle.</param> + public static void CreateFrom(IReadOnlyList<Point2> points, out RectangleF result) + { + Point2 minimum; + Point2 maximum; + PrimitivesHelper.CreateRectangleFromPoints(points, out minimum, out maximum); + CreateFrom(minimum, maximum, out result); + } + + /// <summary> + /// Computes the <see cref="RectangleF" /> from a list of <see cref="Point2" /> structures. + /// </summary> + /// <param name="points">The points.</param> + /// <returns>The resulting <see cref="RectangleF" />.</returns> + public static RectangleF CreateFrom(IReadOnlyList<Point2> points) + { + RectangleF result; + CreateFrom(points, out result); + return result; + } + + /// <summary> + /// Computes the <see cref="RectangleF" /> from the specified <see cref="RectangleF" /> transformed by + /// the specified <see cref="Matrix2" />. + /// </summary> + /// <param name="rectangle">The rectangle to be transformed.</param> + /// <param name="transformMatrix">The transform matrix.</param> + /// <param name="result">The resulting transformed rectangle.</param> + /// <returns> + /// The <see cref="Extended.BoundingRectangle" /> from the <paramref name="rectangle" /> transformed by the + /// <paramref name="transformMatrix" />. + /// </returns> + /// <remarks> + /// <para> + /// If a transformed <see cref="Extended.BoundingRectangle" /> is used for <paramref name="rectangle" /> then the + /// resulting <see cref="Extended.BoundingRectangle" /> will have the compounded transformation, which most likely is + /// not desired. + /// </para> + /// </remarks> + public static void Transform(ref RectangleF rectangle, + ref Matrix2 transformMatrix, out RectangleF result) + { + var center = rectangle.Center; + var halfExtents = (Vector2)rectangle.Size * 0.5f; + + PrimitivesHelper.TransformRectangle(ref center, ref halfExtents, ref transformMatrix); + + result.X = center.X - halfExtents.X; + result.Y = center.Y - halfExtents.Y; + result.Width = halfExtents.X * 2; + result.Height = halfExtents.Y * 2; + } + + /// <summary> + /// Computes the <see cref="RectangleF" /> from the specified <see cref="Extended.BoundingRectangle" /> transformed by + /// the + /// specified <see cref="Matrix2" />. + /// </summary> + /// <param name="rectangle">The bounding rectangle.</param> + /// <param name="transformMatrix">The transform matrix.</param> + /// <returns> + /// The <see cref="Extended.BoundingRectangle" /> from the <paramref name="rectangle" /> transformed by the + /// <paramref name="transformMatrix" />. + /// </returns> + /// <remarks> + /// <para> + /// If a transformed <see cref="Extended.BoundingRectangle" /> is used for <paramref name="rectangle" /> then the + /// resulting <see cref="Extended.BoundingRectangle" /> will have the compounded transformation, which most likely is + /// not desired. + /// </para> + /// </remarks> + public static RectangleF Transform(RectangleF rectangle, ref Matrix2 transformMatrix) + { + RectangleF result; + Transform(ref rectangle, ref transformMatrix, out result); + return result; + } + + /// <summary> + /// Computes the <see cref="RectangleF" /> that contains the two specified + /// <see cref="RectangleF" /> structures. + /// </summary> + /// <param name="first">The first rectangle.</param> + /// <param name="second">The second rectangle.</param> + /// <param name="result">The resulting rectangle that contains both the <paramref name="first" /> and the + /// <paramref name="second" />.</param> + public static void Union(ref RectangleF first, ref RectangleF second, out RectangleF result) + { + result.X = Math.Min(first.X, second.X); + result.Y = Math.Min(first.Y, second.Y); + result.Width = Math.Max(first.Right, second.Right) - result.X; + result.Height = Math.Max(first.Bottom, second.Bottom) - result.Y; + } + + /// <summary> + /// Computes the <see cref="RectangleF" /> that contains the two specified + /// <see cref="RectangleF" /> structures. + /// </summary> + /// <param name="first">The first rectangle.</param> + /// <param name="second">The second rectangle.</param> + /// <returns> + /// An <see cref="RectangleF" /> that contains both the <paramref name="first" /> and the + /// <paramref name="second" />. + /// </returns> + public static RectangleF Union(RectangleF first, RectangleF second) + { + RectangleF result; + Union(ref first, ref second, out result); + return result; + } + + /// <summary> + /// Computes the <see cref="RectangleF" /> that contains both the specified <see cref="RectangleF" /> and this <see cref="RectangleF" />. + /// </summary> + /// <param name="rectangle">The rectangle.</param> + /// <returns> + /// An <see cref="RectangleF" /> that contains both the <paramref name="rectangle" /> and + /// this <see cref="RectangleF" />. + /// </returns> + public RectangleF Union(RectangleF rectangle) + { + RectangleF result; + Union(ref this, ref rectangle, out result); + return result; + } + + /// <summary> + /// Computes the <see cref="RectangleF" /> that is in common between the two specified + /// <see cref="RectangleF" /> structures. + /// </summary> + /// <param name="first">The first rectangle.</param> + /// <param name="second">The second rectangle.</param> + /// <param name="result">The resulting rectangle that is in common between both the <paramref name="first" /> and + /// the <paramref name="second" />, if they intersect; otherwise, <see cref="Empty"/>.</param> + public static void Intersection(ref RectangleF first, + ref RectangleF second, out RectangleF result) + { + var firstMinimum = first.TopLeft; + var firstMaximum = first.BottomRight; + var secondMinimum = second.TopLeft; + var secondMaximum = second.BottomRight; + + var minimum = Point2.Maximum(firstMinimum, secondMinimum); + var maximum = Point2.Minimum(firstMaximum, secondMaximum); + + if ((maximum.X < minimum.X) || (maximum.Y < minimum.Y)) + result = new RectangleF(); + else + result = CreateFrom(minimum, maximum); + } + + /// <summary> + /// Computes the <see cref="RectangleF" /> that is in common between the two specified + /// <see cref="RectangleF" /> structures. + /// </summary> + /// <param name="first">The first rectangle.</param> + /// <param name="second">The second rectangle.</param> + /// <returns> + /// A <see cref="RectangleF" /> that is in common between both the <paramref name="first" /> and + /// the <paramref name="second" />, if they intersect; otherwise, <see cref="Empty"/>. + /// </returns> + public static RectangleF Intersection(RectangleF first, + RectangleF second) + { + RectangleF result; + Intersection(ref first, ref second, out result); + return result; + } + + /// <summary> + /// Computes the <see cref="RectangleF" /> that is in common between the specified + /// <see cref="RectangleF" /> and this <see cref="RectangleF" />. + /// </summary> + /// <param name="rectangle">The rectangle.</param> + /// <returns> + /// A <see cref="RectangleF" /> that is in common between both the <paramref name="rectangle" /> and + /// this <see cref="RectangleF"/>, if they intersect; otherwise, <see cref="Empty"/>. + /// </returns> + public RectangleF Intersection(RectangleF rectangle) + { + RectangleF result; + Intersection(ref this, ref rectangle, out result); + return result; + } + + [Obsolete("RectangleF.Intersect() may be removed in the future. Use Intersection() instead.")] + public static RectangleF Intersect(RectangleF value1, RectangleF value2) + { + RectangleF rectangle; + Intersection(ref value1, ref value2, out rectangle); + return rectangle; + } + + [Obsolete("RectangleF.Intersect() may be removed in the future. Use Intersection() instead.")] + public static void Intersect(ref RectangleF value1, ref RectangleF value2, out RectangleF result) + { + Intersection(ref value1, ref value2, out result); + } + + /// <summary> + /// Determines whether the two specified <see cref="RectangleF" /> structures intersect. + /// </summary> + /// <param name="first">The first rectangle.</param> + /// <param name="second">The second rectangle.</param> + /// <returns> + /// <c>true</c> if the <paramref name="first" /> intersects with the <see cref="second" />; otherwise, <c>false</c>. + /// </returns> + public static bool Intersects(ref RectangleF first, ref RectangleF second) + { + return first.X < second.X + second.Width && first.X + first.Width > second.X && + first.Y < second.Y + second.Height && first.Y + first.Height > second.Y; + } + + /// <summary> + /// Determines whether the two specified <see cref="RectangleF" /> structures intersect. + /// </summary> + /// <param name="first">The first rectangle.</param> + /// <param name="second">The second rectangle.</param> + /// <returns> + /// <c>true</c> if the <paramref name="first" /> intersects with the <see cref="second" />; otherwise, <c>false</c>. + /// </returns> + public static bool Intersects(RectangleF first, RectangleF second) + { + return Intersects(ref first, ref second); + } + + /// <summary> + /// Determines whether the specified <see cref="RectangleF" /> intersects with this + /// <see cref="RectangleF" />. + /// </summary> + /// <param name="rectangle">The bounding rectangle.</param> + /// <returns> + /// <c>true</c> if the <paramref name="rectangle" /> intersects with this + /// <see cref="RectangleF" />; otherwise, + /// <c>false</c>. + /// </returns> + public bool Intersects(RectangleF rectangle) + { + return Intersects(ref this, ref rectangle); + } + + /// <summary> + /// Determines whether the specified <see cref="RectangleF" /> contains the specified + /// <see cref="Point2" />. + /// </summary> + /// <param name="rectangle">The rectangle.</param> + /// <param name="point">The point.</param> + /// <returns> + /// <c>true</c> if the <paramref name="rectangle" /> contains the <paramref name="point" />; otherwise, + /// <c>false</c>. + /// </returns> + public static bool Contains(ref RectangleF rectangle, ref Point2 point) + { + return rectangle.X <= point.X && point.X < rectangle.X + rectangle.Width && rectangle.Y <= point.Y && point.Y < rectangle.Y + rectangle.Height; + } + + /// <summary> + /// Determines whether the specified <see cref="RectangleF" /> contains the specified + /// <see cref="Point2" />. + /// </summary> + /// <param name="rectangle">The rectangle.</param> + /// <param name="point">The point.</param> + /// <returns> + /// <c>true</c> if the <paramref name="rectangle" /> contains the <paramref name="point" />; otherwise, + /// <c>false</c>. + /// </returns> + public static bool Contains(RectangleF rectangle, Point2 point) + { + return Contains(ref rectangle, ref point); + } + + /// <summary> + /// Determines whether this <see cref="RectangleF" /> contains the specified + /// <see cref="Point2" />. + /// </summary> + /// <param name="point">The point.</param> + /// <returns> + /// <c>true</c> if the this <see cref="RectangleF"/> contains the <paramref name="point" />; otherwise, + /// <c>false</c>. + /// </returns> + public bool Contains(Point2 point) + { + return Contains(ref this, ref point); + } + + /// <summary> + /// Updates this <see cref="RectangleF" /> from a list of <see cref="Point2" /> structures. + /// </summary> + /// <param name="points">The points.</param> + public void UpdateFromPoints(IReadOnlyList<Point2> points) + { + var rectangle = CreateFrom(points); + X = rectangle.X; + Y = rectangle.Y; + Width = rectangle.Width; + Height = rectangle.Height; + } + + /// <summary> + /// Computes the squared distance from this <see cref="RectangleF"/> to a <see cref="Point2"/>. + /// </summary> + /// <param name="point">The point.</param> + /// <returns>The squared distance from this <see cref="RectangleF"/> to the <paramref name="point"/>.</returns> + public float SquaredDistanceTo(Point2 point) + { + return PrimitivesHelper.SquaredDistanceToPointFromRectangle(TopLeft, BottomRight, point); + } + + /// <summary> + /// Computes the distance from this <see cref="RectangleF"/> to a <see cref="Point2"/>. + /// </summary> + /// <param name="point">The point.</param> + /// <returns>The distance from this <see cref="RectangleF"/> to the <paramref name="point"/>.</returns> + public float DistanceTo(Point2 point) + { + return (float)Math.Sqrt(SquaredDistanceTo(point)); + } + + /// <summary> + /// Computes the closest <see cref="Point2" /> on this <see cref="RectangleF" /> to a specified + /// <see cref="Point2" />. + /// </summary> + /// <param name="point">The point.</param> + /// <returns>The closest <see cref="Point2" /> on this <see cref="RectangleF" /> to the <paramref name="point" />.</returns> + public Point2 ClosestPointTo(Point2 point) + { + Point2 result; + PrimitivesHelper.ClosestPointToPointFromRectangle(TopLeft, BottomRight, point, out result); + return result; + } + + //TODO: Document this. + public void Inflate(float horizontalAmount, float verticalAmount) + { + X -= horizontalAmount; + Y -= verticalAmount; + Width += horizontalAmount * 2; + Height += verticalAmount * 2; + } + + //TODO: Document this. + public void Offset(float offsetX, float offsetY) + { + X += offsetX; + Y += offsetY; + } + + //TODO: Document this. + public void Offset(Vector2 amount) + { + X += amount.X; + Y += amount.Y; + } + + /// <summary> + /// Compares two <see cref="RectangleF" /> structures. The result specifies whether the values of the + /// <see cref="X" />, <see cref="Y"/>, <see cref="Width"/> and <see cref="Height" /> fields of the two <see cref="RectangleF" /> structures + /// are equal. + /// </summary> + /// <param name="first">The first rectangle.</param> + /// <param name="second">The second rectangle.</param> + /// <returns> + /// <c>true</c> if the values of the + /// <see cref="X" />, <see cref="Y"/>, <see cref="Width"/> and <see cref="Height" /> fields of the two <see cref="RectangleF" /> structures + /// are equal; otherwise, <c>false</c>. + /// </returns> + public static bool operator ==(RectangleF first, RectangleF second) + { + return first.Equals(ref second); + } + + /// <summary> + /// Compares two <see cref="RectangleF" /> structures. The result specifies whether the values of the + /// <see cref="X" />, <see cref="Y"/>, <see cref="Width"/> and <see cref="Height" /> fields of the two <see cref="RectangleF" /> structures + /// are unequal. + /// </summary> + /// <param name="first">The first rectangle.</param> + /// <param name="second">The second rectangle.</param> + /// <returns> + /// <c>true</c> if the values of the + /// <see cref="X" />, <see cref="Y"/>, <see cref="Width"/> and <see cref="Height" /> fields of the two <see cref="RectangleF" /> structures + /// are unequal; otherwise, <c>false</c>. + /// </returns> + public static bool operator !=(RectangleF first, RectangleF second) + { + return !(first == second); + } + + /// <summary> + /// Indicates whether this <see cref="RectangleF" /> is equal to another <see cref="RectangleF" />. + /// </summary> + /// <param name="rectangle">The rectangle.</param> + /// <returns> + /// <c>true</c> if this <see cref="RectangleF" /> is equal to the <paramref name="rectangle" />; otherwise, <c>false</c>. + /// </returns> + public bool Equals(RectangleF rectangle) + { + return Equals(ref rectangle); + } + + /// <summary> + /// Indicates whether this <see cref="RectangleF" /> is equal to another <see cref="RectangleF" />. + /// </summary> + /// <param name="rectangle">The rectangle.</param> + /// <returns> + /// <c>true</c> if this <see cref="RectangleF" /> is equal to the <paramref name="rectangle" />; otherwise, <c>false</c>. + /// </returns> + public bool Equals(ref RectangleF rectangle) + { + // ReSharper disable CompareOfFloatsByEqualityOperator + return X == rectangle.X && Y == rectangle.Y && Width == rectangle.Width && Height == rectangle.Height; + // ReSharper restore CompareOfFloatsByEqualityOperator + } + + /// <summary> + /// Returns a value indicating whether this <see cref="RectangleF" /> is equal to a specified object. + /// </summary> + /// <param name="obj">The object to make the comparison with.</param> + /// <returns> + /// <c>true</c> if this <see cref="RectangleF" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>. + /// </returns> + public override bool Equals(object obj) + { + return obj is RectangleF && Equals((RectangleF)obj); + } + + /// <summary> + /// Returns a hash code of this <see cref="RectangleF" /> suitable for use in hashing algorithms and data + /// structures like a hash table. + /// </summary> + /// <returns> + /// A hash code of this <see cref="RectangleF" />. + /// </returns> + public override int GetHashCode() + { + unchecked + { + var hashCode = X.GetHashCode(); + hashCode = (hashCode * 397) ^ Y.GetHashCode(); + hashCode = (hashCode * 397) ^ Width.GetHashCode(); + hashCode = (hashCode * 397) ^ Height.GetHashCode(); + return hashCode; + } + } + + /// <summary> + /// Performs an implicit conversion from a <see cref="Rectangle" /> to a <see cref="RectangleF" />. + /// </summary> + /// <param name="rectangle">The rectangle.</param> + /// <returns> + /// The resulting <see cref="RectangleF" />. + /// </returns> + public static implicit operator RectangleF(Rectangle rectangle) + { + return new RectangleF + { + X = rectangle.X, + Y = rectangle.Y, + Width = rectangle.Width, + Height = rectangle.Height + }; + } + + /// <summary> + /// Performs an explicit conversion from a <see cref="Rectangle" /> to a <see cref="RectangleF" />. + /// </summary> + /// <param name="rectangle">The rectangle.</param> + /// <returns> + /// The resulting <see cref="RectangleF" />. + /// </returns> + /// <remarks> + /// <para>A loss of precision may occur due to the truncation from <see cref="float" /> to <see cref="int" />.</para> + /// </remarks> + public static explicit operator Rectangle(RectangleF rectangle) + { + return new Rectangle((int)rectangle.X, (int)rectangle.Y, (int)rectangle.Width, (int)rectangle.Height); + } + + /// <summary> + /// Returns a <see cref="string" /> that represents this <see cref="RectangleF" />. + /// </summary> + /// <returns> + /// A <see cref="string" /> that represents this <see cref="RectangleF" />. + /// </returns> + public override string ToString() + { + return $"{{X: {X}, Y: {Y}, Width: {Width}, Height: {Height}"; + } + + internal string DebugDisplayString => string.Concat(X, " ", Y, " ", Width, " ", Height); + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Segment2.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Segment2.cs new file mode 100644 index 0000000..d8452ae --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Segment2.cs @@ -0,0 +1,317 @@ +using System; + +namespace MonoGame.Extended +{ + // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 3.5; A Math and Geometry Primer - Lines, Rays, and Segments. pg 53-54 + /// <summary> + /// A two dimensional line segment defined by two <see cref="Point2" /> structures, a starting point and an ending + /// point. + /// </summary> + /// <seealso cref="IEquatable{T}" /> + /// <seealso cref="IEquatableByRef{Segment2}" /> + public struct Segment2 : IEquatable<Segment2>, IEquatableByRef<Segment2> + { + /// <summary> + /// The starting <see cref="Point2" /> of this <see cref="Segment2" />. + /// </summary> + public Point2 Start; + + /// <summary> + /// The ending <see cref="Point2" /> of this <see cref="Segment2" />. + /// </summary> + public Point2 End; + + /// <summary> + /// Initializes a new instance of the <see cref="Segment2" /> structure from the specified starting and ending + /// <see cref="Point2" /> structures. + /// </summary> + /// <param name="start">The starting point.</param> + /// <param name="end">The ending point.</param> + public Segment2(Point2 start, Point2 end) + { + Start = start; + End = end; + } + + /// <summary> + /// Initializes a new instance of the <see cref="Segment2" /> structure. + /// </summary> + /// <param name="x1">The starting x-coordinate.</param> + /// <param name="y1">The starting y-coordinate.</param> + /// <param name="x2">The ending x-coordinate.</param> + /// <param name="y2">The ending y-coordinate.</param> + public Segment2(float x1, float y1, float x2, float y2) + : this(new Point2(x1, y1), new Point2(x2, y2)) + { + } + + // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 5.1.2; Basic Primitive Tests - Closest Point on Line Segment to Point. pg 127-130 + /// <summary> + /// Computes the closest <see cref="Point2" /> on this <see cref="Segment2" /> to a specified <see cref="Point2" />. + /// </summary> + /// <param name="point">The point.</param> + /// <returns>The closest <see cref="Point2" /> on this <see cref="Segment2" /> to the <paramref name="point" />.</returns> + public Point2 ClosestPointTo(Point2 point) + { + // Computes the parameterized position: d(t) = Start + t * (End – Start) + + var startToEnd = End - Start; + var startToPoint = point - Start; + // Project arbitrary point onto the line segment, deferring the division + var t = startToEnd.Dot(startToPoint); + // If outside segment, clamp t (and therefore d) to the closest endpoint + if (t <= 0) + return Start; + + // Always nonnegative since denom = (||vector||)^2 + var denominator = startToEnd.Dot(startToEnd); + if (t >= denominator) + return End; + + // The point projects inside the [Start, End] interval, must do deferred division now + t /= denominator; + startToEnd *= t; + return new Point2(Start.X + startToEnd.X, Start.Y + startToEnd.Y); + } + + // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 5.1.2.1; Basic Primitive Tests - Distance of Point to Segment. pg 127-130 + /// <summary> + /// Computes the squared distance from this <see cref="Segment2" /> to a specified <see cref="Point2" />. + /// </summary> + /// <param name="point">The point.</param> + /// <returns>The squared distance from this <see cref="Segment2" /> to a specified <see cref="Point2" />.</returns> + public float SquaredDistanceTo(Point2 point) + { + var startToEnd = End - Start; + var startToPoint = point - Start; + var endToPoint = point - End; + // Handle cases where the point projects outside the line segment + var dot = startToPoint.Dot(startToEnd); + var startToPointDistanceSquared = startToPoint.Dot(startToPoint); + if (dot <= 0.0f) + return startToPointDistanceSquared; + var startToEndDistanceSquared = startToEnd.Dot(startToEnd); + if (dot >= startToEndDistanceSquared) + endToPoint.Dot(endToPoint); + // Handle the case where the point projects onto the line segment + return startToPointDistanceSquared - dot*dot/startToEndDistanceSquared; + } + + /// <summary> + /// Computes the distance from this <see cref="Segment2" /> to a specified <see cref="Point2" />. + /// </summary> + /// <param name="point">The point.</param> + /// <returns>The distance from this <see cref="Segment2" /> to a specified <see cref="Point2" />.</returns> + public float DistanceTo(Point2 point) + { + return (float) Math.Sqrt(SquaredDistanceTo(point)); + } + + /// <summary> + /// Determines whether this <see cref="Segment2" /> intersects with the specified <see cref="BoundingRectangle" />. + /// </summary> + /// <param name="rectangle">The bounding box.</param> + /// <param name="intersectionPoint"> + /// When this method returns, contains the <see cref="Point2" /> of intersection, if an + /// intersection was found; otherwise, the <see cref="Point2.NaN" />. This parameter is passed uninitialized. + /// </param> + /// <returns> + /// <c>true</c> if this <see cref="Segment2" /> intersects with <paramref name="rectangle" />; otherwise, + /// <c>false</c>. + /// </returns> + public bool Intersects(RectangleF rectangle, out Point2 intersectionPoint) + { + // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 5.3; Basic Primitive Tests - Intersecting Lines, Rays, and (Directed Segments). pg 179-181 + + var minimumPoint = rectangle.TopLeft; + var maximumPoint = rectangle.BottomRight; + var minimumDistance = float.MinValue; + var maximumDistance = float.MaxValue; + + var direction = End - Start; + if ( + !PrimitivesHelper.IntersectsSlab(Start.X, direction.X, minimumPoint.X, maximumPoint.X, ref minimumDistance, + ref maximumDistance)) + { + intersectionPoint = Point2.NaN; + return false; + } + + if ( + !PrimitivesHelper.IntersectsSlab(Start.Y, direction.Y, minimumPoint.Y, maximumPoint.Y, ref minimumDistance, + ref maximumDistance)) + { + intersectionPoint = Point2.NaN; + return false; + } + + // Segment intersects the 2 slabs. + + if (minimumDistance <= 0) + intersectionPoint = Start; + else + { + intersectionPoint = minimumDistance * direction; + intersectionPoint.X += Start.X; + intersectionPoint.Y += Start.Y; + } + + return true; + } + + /// <summary> + /// Determines whether this <see cref="Segment2" /> intersects with the specified <see cref="BoundingRectangle" />. + /// </summary> + /// <param name="boundingRectangle">The bounding box.</param> + /// <param name="intersectionPoint"> + /// When this method returns, contains the <see cref="Point2" /> of intersection, if an + /// intersection was found; otherwise, the <see cref="Point2.NaN" />. This parameter is passed uninitialized. + /// </param> + /// <returns> + /// <c>true</c> if this <see cref="Segment2" /> intersects with <paramref name="boundingRectangle" />; otherwise, + /// <c>false</c>. + /// </returns> + public bool Intersects(BoundingRectangle boundingRectangle, out Point2 intersectionPoint) + { + // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 5.3; Basic Primitive Tests - Intersecting Lines, Rays, and (Directed Segments). pg 179-181 + + var minimumPoint = boundingRectangle.Center - boundingRectangle.HalfExtents; + var maximumPoint = boundingRectangle.Center + boundingRectangle.HalfExtents; + var minimumDistance = float.MinValue; + var maximumDistance = float.MaxValue; + + var direction = End - Start; + if ( + !PrimitivesHelper.IntersectsSlab(Start.X, direction.X, minimumPoint.X, maximumPoint.X, ref minimumDistance, + ref maximumDistance)) + { + intersectionPoint = Point2.NaN; + return false; + } + + if ( + !PrimitivesHelper.IntersectsSlab(Start.Y, direction.Y, minimumPoint.Y, maximumPoint.Y, ref minimumDistance, + ref maximumDistance)) + { + intersectionPoint = Point2.NaN; + return false; + } + + // Segment intersects the 2 slabs. + + if (minimumDistance <= 0) + intersectionPoint = Start; + else + { + intersectionPoint = minimumDistance*direction; + intersectionPoint.X += Start.X; + intersectionPoint.Y += Start.Y; + } + + return true; + } + + /// <summary> + /// Compares two <see cref="Segment2" /> structures. The result specifies + /// whether the values of the <see cref="Start" /> and <see cref="End" /> + /// fields of the two <see cref='Segment2' /> + /// structures are equal. + /// </summary> + /// <param name="first">The first segment.</param> + /// <param name="second">The second segment.</param> + /// <returns> + /// <c>true</c> if the <see cref="Start" /> and <see cref="End" /> + /// fields of the two <see cref="Segment2" /> + /// structures are equal; otherwise, <c>false</c>. + /// </returns> + public static bool operator ==(Segment2 first, Segment2 second) + { + return first.Equals(ref second); + } + + /// <summary> + /// Indicates whether this <see cref="Segment2" /> is equal to another <see cref="Segment2" />. + /// </summary> + /// <param name="segment">The segment.</param> + /// <returns> + /// <c>true</c> if this <see cref="Segment2" /> is equal to the <paramref name="segment" />; otherwise, <c>false</c>. + /// </returns> + public bool Equals(Segment2 segment) + { + return Equals(ref segment); + } + + /// <summary> + /// Indicates whether this <see cref="Segment2" /> is equal to another <see cref="Segment2" />. + /// </summary> + /// <param name="segment">The segment.</param> + /// <returns> + /// <c>true</c> if this <see cref="Segment2" /> is equal to the <paramref name="segment" /> parameter; otherwise, + /// <c>false</c>. + /// </returns> + public bool Equals(ref Segment2 segment) + { + return (Start == segment.Start) && (End == segment.End); + } + + /// <summary> + /// Returns a value indicating whether this <see cref="Segment2" /> is equal to a specified object. + /// </summary> + /// <param name="obj">The object to make the comparison with.</param> + /// <returns> + /// <c>true</c> if this <see cref="Segment2" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>. + /// </returns> + public override bool Equals(object obj) + { + if (obj is Segment2) + return Equals((Segment2) obj); + return false; + } + + /// <summary> + /// Compares two <see cref="Segment2" /> structures. The result specifies + /// whether the values of the <see cref="Start" /> and <see cref="End" /> + /// fields of the two <see cref="Segment2" /> + /// structures are unequal. + /// </summary> + /// <param name="first">The first point.</param> + /// <param name="second">The second point.</param> + /// <returns> + /// <c>true</c> if the <see cref="Start" /> and <see cref="End" /> + /// fields of the two <see cref="Segment2" /> + /// structures are unequal; otherwise, <c>false</c>. + /// </returns> + public static bool operator !=(Segment2 first, Segment2 second) + { + return !(first == second); + } + + /// <summary> + /// Returns a hash code of this <see cref="Segment2" /> suitable for use in hashing algorithms and data + /// structures like a hash table. + /// </summary> + /// <returns> + /// A hash code of this <see cref="Segment2" />. + /// </returns> + public override int GetHashCode() + { + unchecked + { + return (Start.GetHashCode()*397) ^ End.GetHashCode(); + } + } + + /// <summary> + /// Returns a <see cref="string" /> that represents this <see cref="Segment2" />. + /// </summary> + /// <returns> + /// A <see cref="string" /> that represents this <see cref="Segment2" />. + /// </returns> + public override string ToString() + { + return $"{Start} -> {End}"; + } + + internal string DebugDisplayString => ToString(); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/ShapeExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/ShapeExtensions.cs new file mode 100644 index 0000000..f1f6f10 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/ShapeExtensions.cs @@ -0,0 +1,332 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using MonoGame.Extended.Shapes; + +namespace MonoGame.Extended +{ + /// <summary> + /// Sprite batch extensions for drawing primitive shapes + /// </summary> + public static class ShapeExtensions + { + private static Texture2D _whitePixelTexture; + + private static Texture2D GetTexture(SpriteBatch spriteBatch) + { + if (_whitePixelTexture == null) + { + _whitePixelTexture = new Texture2D(spriteBatch.GraphicsDevice, 1, 1, false, SurfaceFormat.Color); + _whitePixelTexture.SetData(new[] { Color.White }); + spriteBatch.Disposing += (sender, args) => + { + _whitePixelTexture?.Dispose(); + _whitePixelTexture = null; + }; + } + + return _whitePixelTexture; + } + + /// <summary> + /// Draws a closed polygon from a <see cref="Polygon" /> shape + /// </summary> + /// <param name="spriteBatch">The destination drawing surface</param> + /// /// + /// <param name="position">Where to position the polygon</param> + /// <param name="polygon">The polygon to draw</param> + /// <param name="color">The color to use</param> + /// <param name="thickness">The thickness of the lines</param> + /// /// <param name="layerDepth">The depth of the layer of this shape</param> + public static void DrawPolygon(this SpriteBatch spriteBatch, Vector2 position, Polygon polygon, Color color, float thickness = 1f, float layerDepth = 0) + { + DrawPolygon(spriteBatch, position, polygon.Vertices, color, thickness, layerDepth); + } + + /// <summary> + /// Draws a closed polygon from an array of points + /// </summary> + /// <param name="spriteBatch">The destination drawing surface</param> + /// /// + /// <param name="offset">Where to offset the points</param> + /// <param name="points">The points to connect with lines</param> + /// <param name="color">The color to use</param> + /// <param name="thickness">The thickness of the lines</param> + /// <param name="layerDepth">The depth of the layer of this shape</param> + public static void DrawPolygon(this SpriteBatch spriteBatch, Vector2 offset, IReadOnlyList<Vector2> points, Color color, float thickness = 1f, float layerDepth = 0) + { + if (points.Count == 0) + return; + + if (points.Count == 1) + { + DrawPoint(spriteBatch, points[0], color, (int)thickness); + return; + } + + var texture = GetTexture(spriteBatch); + + for (var i = 0; i < points.Count - 1; i++) + DrawPolygonEdge(spriteBatch, texture, points[i] + offset, points[i + 1] + offset, color, thickness, layerDepth); + + DrawPolygonEdge(spriteBatch, texture, points[points.Count - 1] + offset, points[0] + offset, color, thickness, layerDepth); + } + + private static void DrawPolygonEdge(SpriteBatch spriteBatch, Texture2D texture, Vector2 point1, Vector2 point2, Color color, float thickness, float layerDepth) + { + var length = Vector2.Distance(point1, point2); + var angle = (float)Math.Atan2(point2.Y - point1.Y, point2.X - point1.X); + var scale = new Vector2(length, thickness); + spriteBatch.Draw(texture, point1, null, color, angle, Vector2.Zero, scale, SpriteEffects.None, layerDepth); + } + + /// <summary> + /// Draws a filled rectangle + /// </summary> + /// <param name="spriteBatch">The destination drawing surface</param> + /// <param name="rectangle">The rectangle to draw</param> + /// <param name="color">The color to draw the rectangle in</param> + /// <param name="layerDepth">The depth of the layer of this shape</param> + public static void FillRectangle(this SpriteBatch spriteBatch, RectangleF rectangle, Color color, float layerDepth = 0) + { + FillRectangle(spriteBatch, rectangle.Position, rectangle.Size, color, layerDepth); + } + + /// <summary> + /// Draws a filled rectangle + /// </summary> + /// <param name="spriteBatch">The destination drawing surface</param> + /// <param name="location">Where to draw</param> + /// <param name="size">The size of the rectangle</param> + /// <param name="color">The color to draw the rectangle in</param> + /// <param name="layerDepth">The depth of the layer of this shape</param> + public static void FillRectangle(this SpriteBatch spriteBatch, Vector2 location, Size2 size, Color color, float layerDepth = 0) + { + spriteBatch.Draw(GetTexture(spriteBatch), location, null, color, 0, Vector2.Zero, size, SpriteEffects.None, layerDepth); + } + + /// <summary> + /// Draws a filled rectangle + /// </summary> + /// <param name="spriteBatch">The destination drawing surface</param> + /// <param name="x">The X coord of the left side</param> + /// <param name="y">The Y coord of the upper side</param> + /// <param name="width">Width</param> + /// <param name="height">Height</param> + /// <param name="color">The color to draw the rectangle in</param> + /// <param name="layerDepth">The depth of the layer of this shape</param> + public static void FillRectangle(this SpriteBatch spriteBatch, float x, float y, float width, float height, Color color, float layerDepth = 0) + { + FillRectangle(spriteBatch, new Vector2(x, y), new Size2(width, height), color, layerDepth); + } + + /// <summary> + /// Draws a rectangle with the thickness provided + /// </summary> + /// <param name="spriteBatch">The destination drawing surface</param> + /// <param name="rectangle">The rectangle to draw</param> + /// <param name="color">The color to draw the rectangle in</param> + /// <param name="thickness">The thickness of the lines</param> + /// <param name="layerDepth">The depth of the layer of this shape</param> + public static void DrawRectangle(this SpriteBatch spriteBatch, RectangleF rectangle, Color color, float thickness = 1f, float layerDepth = 0) + { + var texture = GetTexture(spriteBatch); + var topLeft = new Vector2(rectangle.X, rectangle.Y); + var topRight = new Vector2(rectangle.Right - thickness, rectangle.Y); + var bottomLeft = new Vector2(rectangle.X, rectangle.Bottom - thickness); + var horizontalScale = new Vector2(rectangle.Width, thickness); + var verticalScale = new Vector2(thickness, rectangle.Height); + + spriteBatch.Draw(texture, topLeft, null, color, 0f, Vector2.Zero, horizontalScale, SpriteEffects.None, layerDepth); + spriteBatch.Draw(texture, topLeft, null, color, 0f, Vector2.Zero, verticalScale, SpriteEffects.None, layerDepth); + spriteBatch.Draw(texture, topRight, null, color, 0f, Vector2.Zero, verticalScale, SpriteEffects.None, layerDepth); + spriteBatch.Draw(texture, bottomLeft, null, color, 0f, Vector2.Zero, horizontalScale, SpriteEffects.None, layerDepth); + } + + /// <summary> + /// Draws a rectangle with the thickness provided + /// </summary> + /// <param name="spriteBatch">The destination drawing surface</param> + /// <param name="location">Where to draw</param> + /// <param name="size">The size of the rectangle</param> + /// <param name="color">The color to draw the rectangle in</param> + /// <param name="thickness">The thickness of the line</param> + /// <param name="layerDepth">The depth of the layer of this shape</param> + public static void DrawRectangle(this SpriteBatch spriteBatch, Vector2 location, Size2 size, Color color, float thickness = 1f, float layerDepth = 0) + { + DrawRectangle(spriteBatch, new RectangleF(location.X, location.Y, size.Width, size.Height), color, thickness, layerDepth); + } + + + /// <summary> + /// Draws a rectangle outline. + /// </summary> + public static void DrawRectangle(this SpriteBatch spriteBatch, float x, float y, float width, float height, Color color, float thickness = 1f, float layerDepth = 0) + { + DrawRectangle(spriteBatch, new RectangleF(x, y, width, height), color, thickness, layerDepth); + } + + /// <summary> + /// Draws a line from point1 to point2 with an offset + /// </summary> + /// <param name="spriteBatch">The destination drawing surface</param> + /// <param name="x1">The X coord of the first point</param> + /// <param name="y1">The Y coord of the first point</param> + /// <param name="x2">The X coord of the second point</param> + /// <param name="y2">The Y coord of the second point</param> + /// <param name="color">The color to use</param> + /// <param name="thickness">The thickness of the line</param> + /// <param name="layerDepth">The depth of the layer of this shape</param> + public static void DrawLine(this SpriteBatch spriteBatch, float x1, float y1, float x2, float y2, Color color, float thickness = 1f, float layerDepth = 0) + { + DrawLine(spriteBatch, new Vector2(x1, y1), new Vector2(x2, y2), color, thickness, layerDepth); + } + + /// <summary> + /// Draws a line from point1 to point2 with an offset + /// </summary> + /// <param name="spriteBatch">The destination drawing surface</param> + /// <param name="point1">The first point</param> + /// <param name="point2">The second point</param> + /// <param name="color">The color to use</param> + /// <param name="thickness">The thickness of the line</param> + /// <param name="layerDepth">The depth of the layer of this shape</param> + public static void DrawLine(this SpriteBatch spriteBatch, Vector2 point1, Vector2 point2, Color color, float thickness = 1f, float layerDepth = 0) + { + // calculate the distance between the two vectors + var distance = Vector2.Distance(point1, point2); + + // calculate the angle between the two vectors + var angle = (float)Math.Atan2(point2.Y - point1.Y, point2.X - point1.X); + + DrawLine(spriteBatch, point1, distance, angle, color, thickness, layerDepth); + } + + /// <summary> + /// Draws a line from point1 to point2 with an offset + /// </summary> + /// <param name="spriteBatch">The destination drawing surface</param> + /// <param name="point">The starting point</param> + /// <param name="length">The length of the line</param> + /// <param name="angle">The angle of this line from the starting point</param> + /// <param name="color">The color to use</param> + /// <param name="thickness">The thickness of the line</param> + /// <param name="layerDepth">The depth of the layer of this shape</param> + public static void DrawLine(this SpriteBatch spriteBatch, Vector2 point, float length, float angle, Color color, float thickness = 1f, float layerDepth = 0) + { + var origin = new Vector2(0f, 0.5f); + var scale = new Vector2(length, thickness); + spriteBatch.Draw(GetTexture(spriteBatch), point, null, color, angle, origin, scale, SpriteEffects.None, layerDepth); + } + + /// <summary> + /// Draws a point at the specified x, y position. The center of the point will be at the position. + /// </summary> + public static void DrawPoint(this SpriteBatch spriteBatch, float x, float y, Color color, float size = 1f, float layerDepth = 0) + { + DrawPoint(spriteBatch, new Vector2(x, y), color, size, layerDepth); + } + + /// <summary> + /// Draws a point at the specified position. The center of the point will be at the position. + /// </summary> + public static void DrawPoint(this SpriteBatch spriteBatch, Vector2 position, Color color, float size = 1f, float layerDepth = 0) + { + var scale = Vector2.One * size; + var offset = new Vector2(0.5f) - new Vector2(size * 0.5f); + spriteBatch.Draw(GetTexture(spriteBatch), position + offset, null, color, 0f, Vector2.Zero, scale, SpriteEffects.None, layerDepth); + } + + /// <summary> + /// Draw a circle from a <see cref="CircleF" /> shape + /// </summary> + /// <param name="spriteBatch">The destination drawing surface</param> + /// <param name="circle">The circle shape to draw</param> + /// <param name="sides">The number of sides to generate</param> + /// <param name="color">The color of the circle</param> + /// <param name="thickness">The thickness of the lines used</param> + /// <param name="layerDepth">The depth of the layer of this shape</param> + public static void DrawCircle(this SpriteBatch spriteBatch, CircleF circle, int sides, Color color, float thickness = 1f, float layerDepth = 0) + { + DrawCircle(spriteBatch, circle.Center, circle.Radius, sides, color, thickness, layerDepth); + } + + /// <summary> + /// Draw a circle + /// </summary> + /// <param name="spriteBatch">The destination drawing surface</param> + /// <param name="center">The center of the circle</param> + /// <param name="radius">The radius of the circle</param> + /// <param name="sides">The number of sides to generate</param> + /// <param name="color">The color of the circle</param> + /// <param name="thickness">The thickness of the lines used</param> + /// <param name="layerDepth">The depth of the layer of this shape</param> + public static void DrawCircle(this SpriteBatch spriteBatch, Vector2 center, float radius, int sides, Color color, float thickness = 1f, float layerDepth = 0) + { + DrawPolygon(spriteBatch, center, CreateCircle(radius, sides), color, thickness, layerDepth); + } + + /// <summary> + /// Draw a circle + /// </summary> + /// <param name="spriteBatch">The destination drawing surface</param> + /// <param name="x">The center X of the circle</param> + /// <param name="y">The center Y of the circle</param> + /// <param name="radius">The radius of the circle</param> + /// <param name="sides">The number of sides to generate</param> + /// <param name="color">The color of the circle</param> + /// <param name="thickness">The thickness of the line</param> + /// <param name="layerDepth">The depth of the layer of this shape</param> + public static void DrawCircle(this SpriteBatch spriteBatch, float x, float y, float radius, int sides, Color color, float thickness = 1f, float layerDepth = 0) + { + DrawPolygon(spriteBatch, new Vector2(x, y), CreateCircle(radius, sides), color, thickness, layerDepth); + } + + /// <summary> + /// Draw an ellipse. + /// </summary> + /// <param name="spriteBatch">The destination drawing surface</param> + /// <param name="center">Center of the ellipse</param> + /// <param name="radius">Radius of the ellipse</param> + /// <param name="sides">The number of sides to generate.</param> + /// <param name="color">The color of the ellipse.</param> + /// <param name="thickness">The thickness of the line around the ellipse.</param> + /// <param name="layerDepth">The depth of the layer of this shape</param> + public static void DrawEllipse(this SpriteBatch spriteBatch, Vector2 center, Vector2 radius, int sides, Color color, float thickness = 1f, float layerDepth = 0) + { + DrawPolygon(spriteBatch, center, CreateEllipse(radius.X, radius.Y, sides), color, thickness, layerDepth); + } + + private static Vector2[] CreateCircle(double radius, int sides) + { + const double max = 2.0 * Math.PI; + var points = new Vector2[sides]; + var step = max / sides; + var theta = 0.0; + + for (var i = 0; i < sides; i++) + { + points[i] = new Vector2((float)(radius * Math.Cos(theta)), (float)(radius * Math.Sin(theta))); + theta += step; + } + + return points; + } + + private static Vector2[] CreateEllipse(float rx, float ry, int sides) + { + var vertices = new Vector2[sides]; + + var t = 0.0; + var dt = 2.0 * Math.PI / sides; + for (var i = 0; i < sides; i++, t += dt) + { + var x = (float)(rx * Math.Cos(t)); + var y = (float)(ry * Math.Sin(t)); + vertices[i] = new Vector2(x, y); + } + return vertices; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/ShapeF.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/ShapeF.cs new file mode 100644 index 0000000..7478f75 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/ShapeF.cs @@ -0,0 +1,95 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + /// <summary> + /// Base class for shapes. + /// </summary> + /// <remakarks> + /// Created to allow checking intersection between shapes of different types. + /// </remakarks> + public interface IShapeF + { + /// <summary> + /// Gets or sets the position of the shape. + /// </summary> + Point2 Position { get; set; } + + /// <summary> + /// Gets escribed rectangle, which lying outside the shape + /// </summary> + RectangleF BoundingRectangle { get; } + } + + /// <summary> + /// Class that implements methods for shared <see cref="IShapeF" /> methods. + /// </summary> + public static class Shape + { + /// <summary> + /// Check if two shapes intersect. + /// </summary> + /// <param name="a">The first shape.</param> + /// <param name="b">The second shape.</param> + /// <returns>True if the two shapes intersect.</returns> + public static bool Intersects(this IShapeF a, IShapeF b) + { + return a switch + { + CircleF circleA when b is CircleF circleB => circleA.Intersects(circleB), + CircleF circleA when b is RectangleF rectangleB => circleA.Intersects(rectangleB), + CircleF circleA when b is OrientedRectangle orientedRectangleB => Intersects(circleA, orientedRectangleB), + + RectangleF rectangleA when b is CircleF circleB => Intersects(circleB, rectangleA), + RectangleF rectangleA when b is RectangleF rectangleB => rectangleA.Intersects(rectangleB), + RectangleF rectangleA when b is OrientedRectangle orientedRectangleB => Intersects(rectangleA, orientedRectangleB).Intersects, + + OrientedRectangle orientedRectangleA when b is CircleF circleB => Intersects(circleB, orientedRectangleA), + OrientedRectangle orientedRectangleA when b is RectangleF rectangleB => Intersects(rectangleB, orientedRectangleA).Intersects, + OrientedRectangle orientedRectangleA when b is OrientedRectangle orientedRectangleB + => OrientedRectangle.Intersects(orientedRectangleA, orientedRectangleB).Intersects, + + _ => throw new ArgumentOutOfRangeException(nameof(a)) + }; + } + + /// <summary> + /// Checks if a circle and rectangle intersect. + /// </summary> + /// <param name="circle">Circle to check intersection with rectangle.</param> + /// <param name="rectangle">Rectangle to check intersection with circle.</param> + /// <returns>True if the circle and rectangle intersect.</returns> + public static bool Intersects(CircleF circle, RectangleF rectangle) + { + var closestPoint = rectangle.ClosestPointTo(circle.Center); + return circle.Contains(closestPoint); + } + + /// <summary> + /// Checks whether a <see cref="CircleF"/> and <see cref="OrientedRectangle"/> intersects. + /// </summary> + /// <param name="circle"><see cref="CircleF"/>to use in intersection test.</param> + /// <param name="orientedRectangle"><see cref="OrientedRectangle"/>to use in intersection test.</param> + /// <returns>True if the circle and oriented bounded rectangle intersects, otherwise false.</returns> + public static bool Intersects(CircleF circle, OrientedRectangle orientedRectangle) + { + var rotation = Matrix2.CreateRotationZ(orientedRectangle.Orientation.Rotation); + var circleCenterInRectangleSpace = rotation.Transform(circle.Center - orientedRectangle.Center); + var circleInRectangleSpace = new CircleF(circleCenterInRectangleSpace, circle.Radius); + var boundingRectangle = new BoundingRectangle(new Point2(), orientedRectangle.Radii); + return circleInRectangleSpace.Intersects(boundingRectangle); + } + + /// <summary> + /// Checks if a <see cref="RectangleF"/> and <see cref="OrientedRectangle"/> intersects. + /// </summary> + /// <param name="rectangleF"></param> + /// <param name="orientedRectangle"></param> + /// <returns>True if objects are intersecting, otherwise false.</returns> + public static (bool Intersects, Vector2 MinimumTranslationVector) Intersects(RectangleF rectangleF, OrientedRectangle orientedRectangle) + { + return OrientedRectangle.Intersects(orientedRectangle, (OrientedRectangle)rectangleF); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size.cs new file mode 100644 index 0000000..be2122e --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size.cs @@ -0,0 +1,252 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + /// <summary> + /// A two dimensional size defined by two real numbers, a width and a height. + /// </summary> + /// <remarks> + /// <para> + /// A size is a subspace of two-dimensional space, the area of which is described in terms of a two-dimensional + /// coordinate system, given by a reference point and two coordinate axes. + /// </para> + /// </remarks> + /// <seealso cref="IEquatable{T}" /> + /// <seealso cref="IEquatableByRef{Size}" /> + public struct Size : IEquatable<Size>, IEquatableByRef<Size> + { + /// <summary> + /// Returns a <see cref="Size" /> with <see cref="Width" /> and <see cref="Height" /> equal to <c>0.0f</c>. + /// </summary> + public static readonly Size Empty = new Size(); + + /// <summary> + /// The horizontal component of this <see cref="Size" />. + /// </summary> + public int Width; + + /// <summary> + /// The vertical component of this <see cref="Size" />. + /// </summary> + public int Height; + + /// <summary> + /// Gets a value that indicates whether this <see cref="Size" /> is empty. + /// </summary> + public bool IsEmpty => Width == 0 && Height == 0; + + /// <summary> + /// Initializes a new instance of the <see cref="Size" /> structure from the specified dimensions. + /// </summary> + /// <param name="width">The width.</param> + /// <param name="height">The height.</param> + public Size(int width, int height) + { + Width = width; + Height = height; + } + + /// <summary> + /// Compares two <see cref="Size" /> structures. The result specifies + /// whether the values of the <see cref="Width" /> and <see cref="Height" /> + /// fields of the two <see cref="Point" /> structures are equal. + /// </summary> + /// <param name="first">The first size.</param> + /// <param name="second">The second size.</param> + /// <returns> + /// <c>true</c> if the <see cref="Width" /> and <see cref="Height" /> + /// fields of the two <see cref="Point" /> structures are equal; otherwise, <c>false</c>. + /// </returns> + public static bool operator ==(Size first, Size second) + { + return first.Equals(ref second); + } + + /// <summary> + /// Indicates whether this <see cref="Size" /> is equal to another <see cref="Size" />. + /// </summary> + /// <param name="size">The size.</param> + /// <returns> + /// <c>true</c> if this <see cref="Point" /> is equal to the <paramref name="size" /> parameter; otherwise, + /// <c>false</c>. + /// </returns> + public bool Equals(Size size) + { + return Equals(ref size); + } + + /// <summary> + /// Indicates whether this <see cref="Size" /> is equal to another <see cref="Size" />. + /// </summary> + /// <param name="size">The size.</param> + /// <returns> + /// <c>true</c> if this <see cref="Point" /> is equal to the <paramref name="size" />; otherwise, + /// <c>false</c>. + /// </returns> + public bool Equals(ref Size size) + { + return Width == size.Width && Height == size.Height; + } + + /// <summary> + /// Returns a value indicating whether this <see cref="Size" /> is equal to a specified object. + /// </summary> + /// <param name="obj">The object to make the comparison with.</param> + /// <returns> + /// <c>true</c> if this <see cref="Size" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>. + /// </returns> + public override bool Equals(object obj) + { + if (obj is Size) + return Equals((Size) obj); + return false; + } + + /// <summary> + /// Compares two <see cref="Size" /> structures. The result specifies + /// whether the values of the <see cref="Width" /> or <see cref="Height" /> + /// fields of the two <see cref="Size" /> structures are unequal. + /// </summary> + /// <param name="first">The first size.</param> + /// <param name="second">The second size.</param> + /// <returns> + /// <c>true</c> if the <see cref="Width" /> or <see cref="Height" /> + /// fields of the two <see cref="Size" /> structures are unequal; otherwise, <c>false</c>. + /// </returns> + public static bool operator !=(Size first, Size second) + { + return !(first == second); + } + + /// <summary> + /// Calculates the <see cref="Size" /> representing the vector addition of two <see cref="Size" /> structures as if + /// they + /// were <see cref="Vector2" /> structures. + /// </summary> + /// <param name="first">The first size.</param> + /// <param name="second">The second size.</param> + /// <returns> + /// The <see cref="Size" /> representing the vector addition of two <see cref="Size" /> structures as if they + /// were <see cref="Vector2" /> structures. + /// </returns> + public static Size operator +(Size first, Size second) + { + return Add(first, second); + } + + /// <summary> + /// Calculates the <see cref="Size" /> representing the vector addition of two <see cref="Size" /> structures. + /// </summary> + /// <param name="first">The first size.</param> + /// <param name="second">The second size.</param> + /// <returns> + /// The <see cref="Size" /> representing the vector addition of two <see cref="Size" /> structures. + /// </returns> + public static Size Add(Size first, Size second) + { + Size size; + size.Width = first.Width + second.Width; + size.Height = first.Height + second.Height; + return size; + } + + /// <summary> + /// Calculates the <see cref="Size" /> representing the vector subtraction of two <see cref="Size" /> structures. + /// </summary> + /// <param name="first">The first size.</param> + /// <param name="second">The second size.</param> + /// <returns> + /// The <see cref="Size" /> representing the vector subtraction of two <see cref="Size" /> structures. + /// </returns> + public static Size operator -(Size first, Size second) + { + return Subtract(first, second); + } + + public static Size operator /(Size size, int value) + { + return new Size(size.Width / value, size.Height / value); + } + + public static Size operator *(Size size, int value) + { + return new Size(size.Width * value, size.Height * value); + } + + /// <summary> + /// Calculates the <see cref="Size" /> representing the vector subtraction of two <see cref="Size" /> structures. + /// </summary> + /// <param name="first">The first size.</param> + /// <param name="second">The second size.</param> + /// <returns> + /// The <see cref="Size" /> representing the vector subtraction of two <see cref="Size" /> structures. + /// </returns> + public static Size Subtract(Size first, Size second) + { + Size size; + size.Width = first.Width - second.Width; + size.Height = first.Height - second.Height; + return size; + } + + /// <summary> + /// Returns a hash code of this <see cref="Size" /> suitable for use in hashing algorithms and data + /// structures like a hash table. + /// </summary> + /// <returns> + /// A hash code of this <see cref="Point" />. + /// </returns> + public override int GetHashCode() + { + unchecked + { + // ReSharper disable NonReadonlyMemberInGetHashCode + return (Width.GetHashCode()*397) ^ Height.GetHashCode(); + // ReSharper restore NonReadonlyMemberInGetHashCode + } + } + + /// <summary> + /// Performs an implicit conversion from a <see cref="Point" /> to a <see cref="Size" />. + /// </summary> + /// <param name="point">The point.</param> + /// <returns> + /// The resulting <see cref="Size" />. + /// </returns> + public static implicit operator Size(Point point) + { + return new Size(point.X, point.Y); + } + + /// <summary> + /// Performs an implicit conversion from a <see cref="Point" /> to a <see cref="Size" />. + /// </summary> + /// <param name="size">The size.</param> + /// <returns> + /// The resulting <see cref="Point" />. + /// </returns> + public static implicit operator Point(Size size) + { + return new Point(size.Width, size.Height); + } + + public static explicit operator Size(Size2 size) + { + return new Size((int) size.Width, (int) size.Height); + } + + /// <summary> + /// Returns a <see cref="string" /> that represents this <see cref="Size" />. + /// </summary> + /// <returns> + /// A <see cref="string" /> that represents this <see cref="Size" />. + /// </returns> + public override string ToString() + { + return $"Width: {Width}, Height: {Height}"; + } + + internal string DebugDisplayString => ToString(); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size2.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size2.cs new file mode 100644 index 0000000..19b22a2 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size2.cs @@ -0,0 +1,311 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + /// <summary> + /// A two dimensional size defined by two real numbers, a width and a height. + /// </summary> + /// <remarks> + /// <para> + /// A size is a subspace of two-dimensional space, the area of which is described in terms of a two-dimensional + /// coordinate system, given by a reference point and two coordinate axes. + /// </para> + /// </remarks> + /// <seealso cref="IEquatable{T}" /> + /// <seealso cref="IEquatableByRef{Size2}" /> + public struct Size2 : IEquatable<Size2>, IEquatableByRef<Size2> + { + /// <summary> + /// Returns a <see cref="Size2" /> with <see cref="Width" /> and <see cref="Height" /> equal to <c>0.0f</c>. + /// </summary> + public static readonly Size2 Empty = new Size2(); + + /// <summary> + /// The horizontal component of this <see cref="Size2" />. + /// </summary> + public float Width; + + /// <summary> + /// The vertical component of this <see cref="Size2" />. + /// </summary> + public float Height; + + /// <summary> + /// Gets a value that indicates whether this <see cref="Size2" /> is empty. + /// </summary> + // ReSharper disable CompareOfFloatsByEqualityOperator + public bool IsEmpty => (Width == 0) && (Height == 0); + + // ReSharper restore CompareOfFloatsByEqualityOperator + + /// <summary> + /// Initializes a new instance of the <see cref="Size2" /> structure from the specified dimensions. + /// </summary> + /// <param name="width">The width.</param> + /// <param name="height">The height.</param> + public Size2(float width, float height) + { + Width = width; + Height = height; + } + + /// <summary> + /// Compares two <see cref="Size2" /> structures. The result specifies + /// whether the values of the <see cref="Width" /> and <see cref="Height" /> + /// fields of the two <see cref="Point2" /> structures are equal. + /// </summary> + /// <param name="first">The first size.</param> + /// <param name="second">The second size.</param> + /// <returns> + /// <c>true</c> if the <see cref="Width" /> and <see cref="Height" /> + /// fields of the two <see cref="Point2" /> structures are equal; otherwise, <c>false</c>. + /// </returns> + public static bool operator ==(Size2 first, Size2 second) + { + return first.Equals(ref second); + } + + /// <summary> + /// Indicates whether this <see cref="Size2" /> is equal to another <see cref="Size2" />. + /// </summary> + /// <param name="size">The size.</param> + /// <returns> + /// <c>true</c> if this <see cref="Point2" /> is equal to the <paramref name="size" /> parameter; otherwise, + /// <c>false</c>. + /// </returns> + public bool Equals(Size2 size) + { + return Equals(ref size); + } + + /// <summary> + /// Indicates whether this <see cref="Size2" /> is equal to another <see cref="Size2" />. + /// </summary> + /// <param name="size">The size.</param> + /// <returns> + /// <c>true</c> if this <see cref="Point2" /> is equal to the <paramref name="size" />; otherwise, + /// <c>false</c>. + /// </returns> + public bool Equals(ref Size2 size) + { + // ReSharper disable CompareOfFloatsByEqualityOperator + return (Width == size.Width) && (Height == size.Height); + // ReSharper restore CompareOfFloatsByEqualityOperator + } + + /// <summary> + /// Returns a value indicating whether this <see cref="Size2" /> is equal to a specified object. + /// </summary> + /// <param name="obj">The object to make the comparison with.</param> + /// <returns> + /// <c>true</c> if this <see cref="Size2" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>. + /// </returns> + public override bool Equals(object obj) + { + if (obj is Size2) + return Equals((Size2) obj); + return false; + } + + /// <summary> + /// Compares two <see cref="Size2" /> structures. The result specifies + /// whether the values of the <see cref="Width" /> or <see cref="Height" /> + /// fields of the two <see cref="Size2" /> structures are unequal. + /// </summary> + /// <param name="first">The first size.</param> + /// <param name="second">The second size.</param> + /// <returns> + /// <c>true</c> if the <see cref="Width" /> or <see cref="Height" /> + /// fields of the two <see cref="Size2" /> structures are unequal; otherwise, <c>false</c>. + /// </returns> + public static bool operator !=(Size2 first, Size2 second) + { + return !(first == second); + } + + /// <summary> + /// Calculates the <see cref="Size2" /> representing the vector addition of two <see cref="Size2" /> structures as if + /// they + /// were <see cref="Vector2" /> structures. + /// </summary> + /// <param name="first">The first size.</param> + /// <param name="second">The second size.</param> + /// <returns> + /// The <see cref="Size2" /> representing the vector addition of two <see cref="Size2" /> structures as if they + /// were <see cref="Vector2" /> structures. + /// </returns> + public static Size2 operator +(Size2 first, Size2 second) + { + return Add(first, second); + } + + /// <summary> + /// Calculates the <see cref="Size2" /> representing the vector addition of two <see cref="Size2" /> structures. + /// </summary> + /// <param name="first">The first size.</param> + /// <param name="second">The second size.</param> + /// <returns> + /// The <see cref="Size2" /> representing the vector addition of two <see cref="Size2" /> structures. + /// </returns> + public static Size2 Add(Size2 first, Size2 second) + { + Size2 size; + size.Width = first.Width + second.Width; + size.Height = first.Height + second.Height; + return size; + } + + /// <summary> + /// Calculates the <see cref="Size2" /> representing the vector subtraction of two <see cref="Size2" /> structures. + /// </summary> + /// <param name="first">The first size.</param> + /// <param name="second">The second size.</param> + /// <returns> + /// The <see cref="Size2" /> representing the vector subtraction of two <see cref="Size2" /> structures. + /// </returns> + public static Size2 operator -(Size2 first, Size2 second) + { + return Subtract(first, second); + } + + public static Size2 operator /(Size2 size, float value) + { + return new Size2(size.Width / value, size.Height / value); + } + + public static Size2 operator *(Size2 size, float value) + { + return new Size2(size.Width * value, size.Height * value); + } + + /// <summary> + /// Calculates the <see cref="Size2" /> representing the vector subtraction of two <see cref="Size2" /> structures. + /// </summary> + /// <param name="first">The first size.</param> + /// <param name="second">The second size.</param> + /// <returns> + /// The <see cref="Size2" /> representing the vector subtraction of two <see cref="Size2" /> structures. + /// </returns> + public static Size2 Subtract(Size2 first, Size2 second) + { + Size2 size; + size.Width = first.Width - second.Width; + size.Height = first.Height - second.Height; + return size; + } + + /// <summary> + /// Returns a hash code of this <see cref="Size2" /> suitable for use in hashing algorithms and data + /// structures like a hash table. + /// </summary> + /// <returns> + /// A hash code of this <see cref="Point2" />. + /// </returns> + public override int GetHashCode() + { + unchecked + { + return (Width.GetHashCode()*397) ^ Height.GetHashCode(); + } + } + + /// <summary> + /// Performs an implicit conversion from a <see cref="Point2" /> to a <see cref="Size2" />. + /// </summary> + /// <param name="point">The point.</param> + /// <returns> + /// The resulting <see cref="Size2" />. + /// </returns> + public static implicit operator Size2(Point2 point) + { + return new Size2(point.X, point.Y); + } + + + /// <summary> + /// Performs an implicit conversion from a <see cref="Point" /> to a <see cref="Size2" />. + /// </summary> + /// <param name="point">The point.</param> + /// <returns> + /// The resulting <see cref="Size2" />. + /// </returns> + public static implicit operator Size2(Point point) + { + return new Size2(point.X, point.Y); + } + + /// <summary> + /// Performs an implicit conversion from a <see cref="Point2" /> to a <see cref="Size2" />. + /// </summary> + /// <param name="size">The size.</param> + /// <returns> + /// The resulting <see cref="Point2" />. + /// </returns> + public static implicit operator Point2(Size2 size) + { + return new Point2(size.Width, size.Height); + } + + /// <summary> + /// Performs an implicit conversion from a <see cref="Size2" /> to a <see cref="Vector2" />. + /// </summary> + /// <param name="size">The size.</param> + /// <returns> + /// The resulting <see cref="Vector2" />. + /// </returns> + public static implicit operator Vector2(Size2 size) + { + return new Vector2(size.Width, size.Height); + } + + /// <summary> + /// Performs an implicit conversion from a <see cref="Vector2" /> to a <see cref="Size2" />. + /// </summary> + /// <param name="vector">The vector.</param> + /// <returns> + /// The resulting <see cref="Size2" />. + /// </returns> + public static implicit operator Size2(Vector2 vector) + { + return new Size2(vector.X, vector.Y); + } + + ///// <summary> + ///// Performs an implicit conversion from a <see cref="Size" /> to a <see cref="Size2" />. + ///// </summary> + ///// <param name="size">The size.</param> + ///// <returns> + ///// The resulting <see cref="Size2" />. + ///// </returns> + //public static implicit operator Size2(Size size) + //{ + // return new Size2(size.Width, size.Height); + //} + + /// <summary> + /// Performs an explicit conversion from a <see cref="Size2" /> to a <see cref="Point" />. + /// </summary> + /// <param name="size">The size.</param> + /// <returns> + /// The resulting <see cref="Size2" />. + /// </returns> + public static explicit operator Point(Size2 size) + { + return new Point((int)size.Width, (int)size.Height); + } + + /// <summary> + /// Returns a <see cref="string" /> that represents this <see cref="Size2" />. + /// </summary> + /// <returns> + /// A <see cref="string" /> that represents this <see cref="Size2" />. + /// </returns> + public override string ToString() + { + return $"Width: {Width}, Height: {Height}"; + } + + internal string DebugDisplayString => ToString(); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size3.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size3.cs new file mode 100644 index 0000000..9872aee --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Size3.cs @@ -0,0 +1,289 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + /// <summary> + /// A three dimensional size defined by two real numbers, a width a height and a depth. + /// </summary> + /// <remarks> + /// <para> + /// A size is a subspace of three-dimensional space, the area of which is described in terms of a three-dimensional + /// coordinate system, given by a reference point and three coordinate axes. + /// </para> + /// </remarks> + /// <seealso cref="IEquatable{T}" /> + /// <seealso cref="IEquatableByRef{Size3}" /> + public struct Size3 : IEquatable<Size3>, IEquatableByRef<Size3> + { + /// <summary> + /// Returns a <see cref="Size3" /> with <see cref="Width" /> <see cref="Height" /> and <see cref="Depth" /> equal to <c>0.0f</c>. + /// </summary> + public static readonly Size3 Empty = new Size3(); + + /// <summary> + /// The horizontal component of this <see cref="Size3" />. + /// </summary> + public float Width; + + /// <summary> + /// The vertical component of this <see cref="Size3" />. + /// </summary> + public float Height; + + /// <summary> + /// The vertical component of this <see cref="Size3" />. + /// </summary> + public float Depth; + + /// <summary> + /// Gets a value that indicates whether this <see cref="Size3" /> is empty. + /// </summary> + // ReSharper disable CompareOfFloatsByEqualityOperator + public bool IsEmpty => (Width == 0) && (Height == 0) && (Depth == 0); + + // ReSharper restore CompareOfFloatsByEqualityOperator + + /// <summary> + /// Initializes a new instance of the <see cref="Size3" /> structure from the specified dimensions. + /// </summary> + /// <param name="width">The width.</param> + /// <param name="height">The height.</param> + /// <param name="depth">The depth.</param> + public Size3(float width, float height, float depth) + { + Width = width; + Height = height; + Depth = depth; + } + + /// <summary> + /// Compares two <see cref="Size3" /> structures. The result specifies + /// whether the values of the <see cref="Width" /> <see cref="Height" /> and <see cref="Depth" /> + /// fields of the two <see cref="Point3" /> structures are equal. + /// </summary> + /// <param name="first">The first size.</param> + /// <param name="second">The second size.</param> + /// <returns> + /// <c>true</c> if the <see cref="Width" /> <see cref="Height" /> and <see cref="Depth" /> + /// fields of the two <see cref="Point3" /> structures are equal; otherwise, <c>false</c>. + /// </returns> + public static bool operator ==(Size3 first, Size3 second) + { + return first.Equals(ref second); + } + + /// <summary> + /// Indicates whether this <see cref="Size3" /> is equal to another <see cref="Size3" />. + /// </summary> + /// <param name="size">The size.</param> + /// <returns> + /// <c>true</c> if this <see cref="Point3" /> is equal to the <paramref name="size" /> parameter; otherwise, + /// <c>false</c>. + /// </returns> + public bool Equals(Size3 size) + { + return Equals(ref size); + } + + /// <summary> + /// Indicates whether this <see cref="Size3" /> is equal to another <see cref="Size3" />. + /// </summary> + /// <param name="size">The size.</param> + /// <returns> + /// <c>true</c> if this <see cref="Point3" /> is equal to the <paramref name="size" />; otherwise, + /// <c>false</c>. + /// </returns> + public bool Equals(ref Size3 size) + { + // ReSharper disable CompareOfFloatsByEqualityOperator + return (Width == size.Width) && (Height == size.Height) && (Depth == size.Depth); + // ReSharper restore CompareOfFloatsByEqualityOperator + } + + /// <summary> + /// Returns a value indicating whether this <see cref="Size3" /> is equal to a specified object. + /// </summary> + /// <param name="obj">The object to make the comparison with.</param> + /// <returns> + /// <c>true</c> if this <see cref="Size3" /> is equal to <paramref name="obj" />; otherwise, <c>false</c>. + /// </returns> + public override bool Equals(object obj) + { + if (obj is Size3) + return Equals((Size3) obj); + return false; + } + + /// <summary> + /// Compares two <see cref="Size3" /> structures. The result specifies + /// whether the values of the <see cref="Width" /> <see cref="Height" /> or <see cref="Depth" /> + /// fields of the two <see cref="Size3" /> structures are unequal. + /// </summary> + /// <param name="first">The first size.</param> + /// <param name="second">The second size.</param> + /// <returns> + /// <c>true</c> if the <see cref="Width" /> <see cref="Height" /> or <see cref="Depth" /> + /// fields of the two <see cref="Size3" /> structures are unequal; otherwise, <c>false</c>. + /// </returns> + public static bool operator !=(Size3 first, Size3 second) + { + return !(first == second); + } + + /// <summary> + /// Calculates the <see cref="Size3" /> representing the vector addition of two <see cref="Size3" /> structures as if + /// they were <see cref="Vector3" /> structures. + /// </summary> + /// <param name="first">The first size.</param> + /// <param name="second">The second size.</param> + /// <returns> + /// The <see cref="Size3" /> representing the vector addition of two <see cref="Size3" /> structures as if they + /// were <see cref="Vector3" /> structures. + /// </returns> + public static Size3 operator +(Size3 first, Size3 second) + { + return Add(first, second); + } + + /// <summary> + /// Calculates the <see cref="Size3" /> representing the vector addition of two <see cref="Size3" /> structures. + /// </summary> + /// <param name="first">The first size.</param> + /// <param name="second">The second size.</param> + /// <returns> + /// The <see cref="Size3" /> representing the vector addition of two <see cref="Size3" /> structures. + /// </returns> + public static Size3 Add(Size3 first, Size3 second) + { + Size3 size; + size.Width = first.Width + second.Width; + size.Height = first.Height + second.Height; + size.Depth = first.Depth + second.Depth; + return size; + } + + /// <summary> + /// Calculates the <see cref="Size3" /> representing the vector subtraction of two <see cref="Size3" /> structures. + /// </summary> + /// <param name="first">The first size.</param> + /// <param name="second">The second size.</param> + /// <returns> + /// The <see cref="Size3" /> representing the vector subtraction of two <see cref="Size3" /> structures. + /// </returns> + public static Size3 operator -(Size3 first, Size3 second) + { + return Subtract(first, second); + } + + public static Size3 operator /(Size3 size, float value) + { + return new Size3(size.Width / value, size.Height / value, size.Depth / value); + } + + public static Size3 operator *(Size3 size, float value) + { + return new Size3(size.Width * value, size.Height * value, size.Depth * value); + } + + /// <summary> + /// Calculates the <see cref="Size3" /> representing the vector subtraction of two <see cref="Size3" /> structures. + /// </summary> + /// <param name="first">The first size.</param> + /// <param name="second">The second size.</param> + /// <returns> + /// The <see cref="Size3" /> representing the vector subtraction of two <see cref="Size3" /> structures. + /// </returns> + public static Size3 Subtract(Size3 first, Size3 second) + { + Size3 size; + size.Width = first.Width - second.Width; + size.Height = first.Height - second.Height; + size.Depth = first.Depth - second.Depth; + return size; + } + + /// <summary> + /// Returns a hash code of this <see cref="Size3" /> suitable for use in hashing algorithms and data + /// structures like a hash table. + /// </summary> + /// <returns> + /// A hash code of this <see cref="Point3" />. + /// </returns> + public override int GetHashCode() + { + unchecked + { + unchecked + { + int hash = 17; + hash = hash * 23 + Width.GetHashCode(); + hash = hash * 23 + Height.GetHashCode(); + hash = hash * 23 + Depth.GetHashCode(); + return hash; + } + } + } + + /// <summary> + /// Performs an implicit conversion from a <see cref="Point3" /> to a <see cref="Size3" />. + /// </summary> + /// <param name="point">The point.</param> + /// <returns> + /// The resulting <see cref="Size3" />. + /// </returns> + public static implicit operator Size3(Point3 point) + { + return new Size3(point.X, point.Y, point.Z); + } + + /// <summary> + /// Performs an implicit conversion from a <see cref="Point3" /> to a <see cref="Size3" />. + /// </summary> + /// <param name="size">The size.</param> + /// <returns> + /// The resulting <see cref="Point3" />. + /// </returns> + public static implicit operator Point3(Size3 size) + { + return new Point3(size.Width, size.Height, size.Depth); + } + + /// <summary> + /// Performs an implicit conversion from a <see cref="Size3" /> to a <see cref="Vector3" />. + /// </summary> + /// <param name="size">The size.</param> + /// <returns> + /// The resulting <see cref="Vector3" />. + /// </returns> + public static implicit operator Vector3(Size3 size) + { + return new Vector3(size.Width, size.Height, size.Depth); + } + + /// <summary> + /// Performs an implicit conversion from a <see cref="Vector3" /> to a <see cref="Size3" />. + /// </summary> + /// <param name="vector">The vector.</param> + /// <returns> + /// The resulting <see cref="Size3" />. + /// </returns> + public static implicit operator Size3(Vector3 vector) + { + return new Size3(vector.X, vector.Y, vector.Z); + } + + /// <summary> + /// Returns a <see cref="string" /> that represents this <see cref="Size3" />. + /// </summary> + /// <returns> + /// A <see cref="string" /> that represents this <see cref="Size3" />. + /// </returns> + public override string ToString() + { + return $"Width: {Width}, Height: {Height}, Depth: {Depth}"; + } + + internal string DebugDisplayString => ToString(); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Thickness.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Thickness.cs new file mode 100644 index 0000000..dfecbc9 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Thickness.cs @@ -0,0 +1,101 @@ +using System; +using System.Linq; + +namespace MonoGame.Extended +{ + public struct Thickness : IEquatable<Thickness> + { + public Thickness(int all) + : this(all, all, all, all) + { + } + + public Thickness(int leftRight, int topBottom) + : this(leftRight, topBottom, leftRight, topBottom) + { + } + + public Thickness(int left, int top, int right, int bottom) + : this() + { + Left = left; + Top = top; + Right = right; + Bottom = bottom; + } + + public int Left { get; set; } + public int Top { get; set; } + public int Right { get; set; } + public int Bottom { get; set; } + public int Width => Left + Right; + public int Height => Top + Bottom; + public Size Size => new Size(Width, Height); + + public static implicit operator Thickness(int value) + { + return new Thickness(value); + } + + public override bool Equals(object obj) + { + if (obj is Thickness) + { + var other = (Thickness)obj; + return Equals(other); + } + + return base.Equals(obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = Left; + hashCode = (hashCode * 397) ^ Top; + hashCode = (hashCode * 397) ^ Right; + hashCode = (hashCode * 397) ^ Bottom; + return hashCode; + } + } + + public bool Equals(Thickness other) + { + return Left == other.Left && Right == other.Right && Top == other.Top && Bottom == other.Bottom; + } + + public static Thickness FromValues(int[] values) + { + switch (values.Length) + { + case 1: + return new Thickness(values[0]); + case 2: + return new Thickness(values[0], values[1]); + case 4: + return new Thickness(values[0], values[1], values[2], values[3]); + default: + throw new FormatException("Invalid thickness"); + } + } + + public static Thickness Parse(string value) + { + var values = value + .Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) + .Select(int.Parse) + .ToArray(); + + return FromValues(values); + } + + public override string ToString() + { + if (Left == Right && Top == Bottom) + return Left == Top ? $"{Left}" : $"{Left} {Top}"; + + return $"{Left}, {Right}, {Top}, {Bottom}"; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/CyclicalList.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/CyclicalList.cs new file mode 100644 index 0000000..551e510 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/CyclicalList.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MonoGame.Extended.Triangulation +{ + /// <summary> + /// Implements a List structure as a cyclical list where indices are wrapped. + /// </summary> + /// MIT Licensed: https://github.com/nickgravelyn/Triangulator + /// <typeparam name="T">The Type to hold in the list.</typeparam> + class CyclicalList<T> : List<T> + { + public new T this[int index] + { + get + { + //perform the index wrapping + while (index < 0) + index = Count + index; + if (index >= Count) + index %= Count; + + return base[index]; + } + set + { + //perform the index wrapping + while (index < 0) + index = Count + index; + if (index >= Count) + index %= Count; + + base[index] = value; + } + } + + public CyclicalList() { } + + public CyclicalList(IEnumerable<T> collection) + : base(collection) + { + } + + public new void RemoveAt(int index) + { + Remove(this[index]); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/IndexableCyclicalLinkedList.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/IndexableCyclicalLinkedList.cs new file mode 100644 index 0000000..d84ee82 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/IndexableCyclicalLinkedList.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MonoGame.Extended.Triangulation +{ + /// <summary> + /// Implements a LinkedList that is both indexable as well as cyclical. Thus + /// indexing into the list with an out-of-bounds index will automatically cycle + /// around the list to find a valid node. + /// </summary> + /// MIT Licensed: https://github.com/nickgravelyn/Triangulator + class IndexableCyclicalLinkedList<T> : LinkedList<T> + { + /// <summary> + /// Gets the LinkedListNode at a particular index. + /// </summary> + /// <param name="index">The index of the node to retrieve.</param> + /// <returns>The LinkedListNode found at the index given.</returns> + public LinkedListNode<T> this[int index] + { + get + { + //perform the index wrapping + while (index < 0) + index = Count + index; + if (index >= Count) + index %= Count; + + //find the proper node + LinkedListNode<T> node = First; + for (int i = 0; i < index; i++) + node = node.Next; + + return node; + } + } + + /// <summary> + /// Removes the node at a given index. + /// </summary> + /// <param name="index">The index of the node to remove.</param> + public void RemoveAt(int index) + { + Remove(this[index]); + } + + /// <summary> + /// Finds the index of a given item. + /// </summary> + /// <param name="item">The item to find.</param> + /// <returns>The index of the item if found; -1 if the item is not found.</returns> + public int IndexOf(T item) + { + for (int i = 0; i < Count; i++) + if (this[i].Value.Equals(item)) + return i; + + return -1; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/LineSegment.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/LineSegment.cs new file mode 100644 index 0000000..bbe0c04 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/LineSegment.cs @@ -0,0 +1,61 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Text; + +namespace MonoGame.Extended.Triangulation +{ + /// <summary> + /// MIT Licensed: https://github.com/nickgravelyn/Triangulator + /// </summary> + struct LineSegment + { + public Vertex A; + public Vertex B; + + public LineSegment(Vertex a, Vertex b) + { + A = a; + B = b; + } + + public float? IntersectsWithRay(Vector2 origin, Vector2 direction) + { + float largestDistance = MathHelper.Max(A.Position.X - origin.X, B.Position.X - origin.X) * 2f; + LineSegment raySegment = new LineSegment(new Vertex(origin, 0), new Vertex(origin + (direction * largestDistance), 0)); + + Vector2? intersection = FindIntersection(this, raySegment); + float? value = null; + + if (intersection != null) + value = Vector2.Distance(origin, intersection.Value); + + return value; + } + + public static Vector2? FindIntersection(LineSegment a, LineSegment b) + { + float x1 = a.A.Position.X; + float y1 = a.A.Position.Y; + float x2 = a.B.Position.X; + float y2 = a.B.Position.Y; + float x3 = b.A.Position.X; + float y3 = b.A.Position.Y; + float x4 = b.B.Position.X; + float y4 = b.B.Position.Y; + + float denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1); + + float uaNum = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3); + float ubNum = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3); + + float ua = uaNum / denom; + float ub = ubNum / denom; + + if (MathHelper.Clamp(ua, 0f, 1f) != ua || MathHelper.Clamp(ub, 0f, 1f) != ub) + return null; + + return a.A.Position + (a.B.Position - a.A.Position) * ua; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Triangle.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Triangle.cs new file mode 100644 index 0000000..8c1d35c --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Triangle.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace MonoGame.Extended.Triangulation +{ + /// <summary> + /// A basic triangle structure that holds the three vertices that make up a given triangle. + /// MIT Licensed: https://github.com/nickgravelyn/Triangulator + /// </summary> + struct Triangle + { + public readonly Vertex A; + public readonly Vertex B; + public readonly Vertex C; + + public Triangle(Vertex a, Vertex b, Vertex c) + { + A = a; + B = b; + C = c; + } + + public bool ContainsPoint(Vertex point) + { + //return true if the point to test is one of the vertices + if (point.Equals(A) || point.Equals(B) || point.Equals(C)) + return true; + + bool oddNodes = false; + + if (checkPointToSegment(C, A, point)) + oddNodes = !oddNodes; + if (checkPointToSegment(A, B, point)) + oddNodes = !oddNodes; + if (checkPointToSegment(B, C, point)) + oddNodes = !oddNodes; + + return oddNodes; + } + + public static bool ContainsPoint(Vertex a, Vertex b, Vertex c, Vertex point) + { + return new Triangle(a, b, c).ContainsPoint(point); + } + + static bool checkPointToSegment(Vertex sA, Vertex sB, Vertex point) + { + if ((sA.Position.Y < point.Position.Y && sB.Position.Y >= point.Position.Y) || + (sB.Position.Y < point.Position.Y && sA.Position.Y >= point.Position.Y)) + { + float x = + sA.Position.X + + (point.Position.Y - sA.Position.Y) / + (sB.Position.Y - sA.Position.Y) * + (sB.Position.X - sA.Position.X); + + if (x < point.Position.X) + return true; + } + + return false; + } + + public override bool Equals(object obj) + { + if (obj.GetType() != typeof(Triangle)) + return false; + return Equals((Triangle)obj); + } + + public bool Equals(Triangle obj) + { + return obj.A.Equals(A) && obj.B.Equals(B) && obj.C.Equals(C); + } + + public override int GetHashCode() + { + unchecked + { + int result = A.GetHashCode(); + result = (result * 397) ^ B.GetHashCode(); + result = (result * 397) ^ C.GetHashCode(); + return result; + } + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Triangulator.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Triangulator.cs new file mode 100644 index 0000000..5e03089 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Triangulator.cs @@ -0,0 +1,567 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +namespace MonoGame.Extended.Triangulation +{ + /// <summary> + /// MIT Licensed: https://github.com/nickgravelyn/Triangulator + /// + /// A static class exposing methods for triangulating 2D polygons. This is the sole public + /// class in the entire library; all other classes/structures are intended as internal-only + /// objects used only to assist in triangulation. + /// + /// This class makes use of the DEBUG conditional and produces quite verbose output when built + /// in Debug mode. This is quite useful for debugging purposes, but can slow the process down + /// quite a bit. For optimal performance, build the library in Release mode. + /// + /// The triangulation is also not optimized for garbage sensitive processing. The point of the + /// library is a robust, yet simple, system for triangulating 2D shapes. It is intended to be + /// used as part of your content pipeline or at load-time. It is not something you want to be + /// using each and every frame unless you really don't care about garbage. + /// </summary> + public static class Triangulator + { + #region Fields + + static readonly IndexableCyclicalLinkedList<Vertex> polygonVertices = new IndexableCyclicalLinkedList<Vertex>(); + static readonly IndexableCyclicalLinkedList<Vertex> earVertices = new IndexableCyclicalLinkedList<Vertex>(); + static readonly CyclicalList<Vertex> convexVertices = new CyclicalList<Vertex>(); + static readonly CyclicalList<Vertex> reflexVertices = new CyclicalList<Vertex>(); + + #endregion + + #region Public Methods + + #region Triangulate + + /// <summary> + /// Triangulates a 2D polygon produced the indexes required to render the points as a triangle list. + /// </summary> + /// <param name="inputVertices">The polygon vertices in counter-clockwise winding order.</param> + /// <param name="desiredWindingOrder">The desired output winding order.</param> + /// <param name="outputVertices">The resulting vertices that include any reversals of winding order and holes.</param> + /// <param name="indices">The resulting indices for rendering the shape as a triangle list.</param> + public static void Triangulate( + Vector2[] inputVertices, + WindingOrder desiredWindingOrder, + out Vector2[] outputVertices, + out int[] indices) + { + Log("\nBeginning triangulation..."); + + List<Triangle> triangles = new List<Triangle>(); + + //make sure we have our vertices wound properly + if (DetermineWindingOrder(inputVertices) == WindingOrder.Clockwise) + outputVertices = ReverseWindingOrder(inputVertices); + else + outputVertices = (Vector2[])inputVertices.Clone(); + + //clear all of the lists + polygonVertices.Clear(); + earVertices.Clear(); + convexVertices.Clear(); + reflexVertices.Clear(); + + //generate the cyclical list of vertices in the polygon + for (int i = 0; i < outputVertices.Length; i++) + polygonVertices.AddLast(new Vertex(outputVertices[i], i)); + + //categorize all of the vertices as convex, reflex, and ear + FindConvexAndReflexVertices(); + FindEarVertices(); + + //clip all the ear vertices + while (polygonVertices.Count > 3 && earVertices.Count > 0) + ClipNextEar(triangles); + + //if there are still three points, use that for the last triangle + if (polygonVertices.Count == 3) + triangles.Add(new Triangle( + polygonVertices[0].Value, + polygonVertices[1].Value, + polygonVertices[2].Value)); + + //add all of the triangle indices to the output array + indices = new int[triangles.Count * 3]; + + //move the if statement out of the loop to prevent all the + //redundant comparisons + if (desiredWindingOrder == WindingOrder.CounterClockwise) + { + for (int i = 0; i < triangles.Count; i++) + { + indices[(i * 3)] = triangles[i].A.Index; + indices[(i * 3) + 1] = triangles[i].B.Index; + indices[(i * 3) + 2] = triangles[i].C.Index; + } + } + else + { + for (int i = 0; i < triangles.Count; i++) + { + indices[(i * 3)] = triangles[i].C.Index; + indices[(i * 3) + 1] = triangles[i].B.Index; + indices[(i * 3) + 2] = triangles[i].A.Index; + } + } + } + + #endregion + + #region CutHoleInShape + + /// <summary> + /// Cuts a hole into a shape. + /// </summary> + /// <param name="shapeVerts">An array of vertices for the primary shape.</param> + /// <param name="holeVerts">An array of vertices for the hole to be cut. It is assumed that these vertices lie completely within the shape verts.</param> + /// <returns>The new array of vertices that can be passed to Triangulate to properly triangulate the shape with the hole.</returns> + public static Vector2[] CutHoleInShape(Vector2[] shapeVerts, Vector2[] holeVerts) + { + Log("\nCutting hole into shape..."); + + //make sure the shape vertices are wound counter clockwise and the hole vertices clockwise + shapeVerts = EnsureWindingOrder(shapeVerts, WindingOrder.CounterClockwise); + holeVerts = EnsureWindingOrder(holeVerts, WindingOrder.Clockwise); + + //clear all of the lists + polygonVertices.Clear(); + earVertices.Clear(); + convexVertices.Clear(); + reflexVertices.Clear(); + + //generate the cyclical list of vertices in the polygon + for (int i = 0; i < shapeVerts.Length; i++) + polygonVertices.AddLast(new Vertex(shapeVerts[i], i)); + + CyclicalList<Vertex> holePolygon = new CyclicalList<Vertex>(); + for (int i = 0; i < holeVerts.Length; i++) + holePolygon.Add(new Vertex(holeVerts[i], i + polygonVertices.Count)); + +#if DEBUG + StringBuilder vString = new StringBuilder(); + foreach (Vertex v in polygonVertices) + vString.Append(string.Format("{0}, ", v)); + Log("Shape Vertices: {0}", vString); + + vString = new StringBuilder(); + foreach (Vertex v in holePolygon) + vString.Append(string.Format("{0}, ", v)); + Log("Hole Vertices: {0}", vString); +#endif + + FindConvexAndReflexVertices(); + FindEarVertices(); + + //find the hole vertex with the largest X value + Vertex rightMostHoleVertex = holePolygon[0]; + foreach (Vertex v in holePolygon) + if (v.Position.X > rightMostHoleVertex.Position.X) + rightMostHoleVertex = v; + + //construct a list of all line segments where at least one vertex + //is to the right of the rightmost hole vertex with one vertex + //above the hole vertex and one below + List<LineSegment> segmentsToTest = new List<LineSegment>(); + for (int i = 0; i < polygonVertices.Count; i++) + { + Vertex a = polygonVertices[i].Value; + Vertex b = polygonVertices[i + 1].Value; + + if ((a.Position.X > rightMostHoleVertex.Position.X || b.Position.X > rightMostHoleVertex.Position.X) && + ((a.Position.Y >= rightMostHoleVertex.Position.Y && b.Position.Y <= rightMostHoleVertex.Position.Y) || + (a.Position.Y <= rightMostHoleVertex.Position.Y && b.Position.Y >= rightMostHoleVertex.Position.Y))) + segmentsToTest.Add(new LineSegment(a, b)); + } + + //now we try to find the closest intersection point heading to the right from + //our hole vertex. + float? closestPoint = null; + LineSegment closestSegment = new LineSegment(); + foreach (LineSegment segment in segmentsToTest) + { + float? intersection = segment.IntersectsWithRay(rightMostHoleVertex.Position, Vector2.UnitX); + if (intersection != null) + { + if (closestPoint == null || closestPoint.Value > intersection.Value) + { + closestPoint = intersection; + closestSegment = segment; + } + } + } + + //if closestPoint is null, there were no collisions (likely from improper input data), + //but we'll just return without doing anything else + if (closestPoint == null) + return shapeVerts; + + //otherwise we can find our mutually visible vertex to split the polygon + Vector2 I = rightMostHoleVertex.Position + Vector2.UnitX * closestPoint.Value; + Vertex P = (closestSegment.A.Position.X > closestSegment.B.Position.X) + ? closestSegment.A + : closestSegment.B; + + //construct triangle MIP + Triangle mip = new Triangle(rightMostHoleVertex, new Vertex(I, 1), P); + + //see if any of the reflex vertices lie inside of the MIP triangle + List<Vertex> interiorReflexVertices = new List<Vertex>(); + foreach (Vertex v in reflexVertices) + if (mip.ContainsPoint(v)) + interiorReflexVertices.Add(v); + + //if there are any interior reflex vertices, find the one that, when connected + //to our rightMostHoleVertex, forms the line closest to Vector2.UnitX + if (interiorReflexVertices.Count > 0) + { + float closestDot = -1f; + foreach (Vertex v in interiorReflexVertices) + { + //compute the dot product of the vector against the UnitX + Vector2 d = Vector2.Normalize(v.Position - rightMostHoleVertex.Position); + float dot = Vector2.Dot(Vector2.UnitX, d); + + //if this line is the closest we've found + if (dot > closestDot) + { + //save the value and save the vertex as P + closestDot = dot; + P = v; + } + } + } + + //now we just form our output array by injecting the hole vertices into place + //we know we have to inject the hole into the main array after point P going from + //rightMostHoleVertex around and then back to P. + int mIndex = holePolygon.IndexOf(rightMostHoleVertex); + int injectPoint = polygonVertices.IndexOf(P); + + Log("Inserting hole at injection point {0} starting at hole vertex {1}.", + P, + rightMostHoleVertex); + for (int i = mIndex; i <= mIndex + holePolygon.Count; i++) + { + Log("Inserting vertex {0} after vertex {1}.", holePolygon[i], polygonVertices[injectPoint].Value); + polygonVertices.AddAfter(polygonVertices[injectPoint++], holePolygon[i]); + } + polygonVertices.AddAfter(polygonVertices[injectPoint], P); + +#if DEBUG + vString = new StringBuilder(); + foreach (Vertex v in polygonVertices) + vString.Append(string.Format("{0}, ", v)); + Log("New Shape Vertices: {0}\n", vString); +#endif + + //finally we write out the new polygon vertices and return them out + Vector2[] newShapeVerts = new Vector2[polygonVertices.Count]; + for (int i = 0; i < polygonVertices.Count; i++) + newShapeVerts[i] = polygonVertices[i].Value.Position; + + return newShapeVerts; + } + + #endregion + + #region EnsureWindingOrder + + /// <summary> + /// Ensures that a set of vertices are wound in a particular order, reversing them if necessary. + /// </summary> + /// <param name="vertices">The vertices of the polygon.</param> + /// <param name="windingOrder">The desired winding order.</param> + /// <returns>A new set of vertices if the winding order didn't match; otherwise the original set.</returns> + public static Vector2[] EnsureWindingOrder(Vector2[] vertices, WindingOrder windingOrder) + { + Log("\nEnsuring winding order of {0}...", windingOrder); + if (DetermineWindingOrder(vertices) != windingOrder) + { + Log("Reversing vertices..."); + return ReverseWindingOrder(vertices); + } + + Log("No reversal needed."); + return vertices; + } + + #endregion + + #region ReverseWindingOrder + + /// <summary> + /// Reverses the winding order for a set of vertices. + /// </summary> + /// <param name="vertices">The vertices of the polygon.</param> + /// <returns>The new vertices for the polygon with the opposite winding order.</returns> + public static Vector2[] ReverseWindingOrder(Vector2[] vertices) + { + Log("\nReversing winding order..."); + Vector2[] newVerts = new Vector2[vertices.Length]; + +#if DEBUG + StringBuilder vString = new StringBuilder(); + foreach (Vector2 v in vertices) + vString.Append(string.Format("{0}, ", v)); + Log("Original Vertices: {0}", vString); +#endif + + newVerts[0] = vertices[0]; + for (int i = 1; i < newVerts.Length; i++) + newVerts[i] = vertices[vertices.Length - i]; + +#if DEBUG + vString = new StringBuilder(); + foreach (Vector2 v in newVerts) + vString.Append(string.Format("{0}, ", v)); + Log("New Vertices After Reversal: {0}\n", vString); +#endif + + return newVerts; + } + + #endregion + + #region DetermineWindingOrder + + /// <summary> + /// Determines the winding order of a polygon given a set of vertices. + /// </summary> + /// <param name="vertices">The vertices of the polygon.</param> + /// <returns>The calculated winding order of the polygon.</returns> + public static WindingOrder DetermineWindingOrder(Vector2[] vertices) + { + int clockWiseCount = 0; + int counterClockWiseCount = 0; + Vector2 p1 = vertices[0]; + + for (int i = 1; i < vertices.Length; i++) + { + Vector2 p2 = vertices[i]; + Vector2 p3 = vertices[(i + 1) % vertices.Length]; + + Vector2 e1 = p1 - p2; + Vector2 e2 = p3 - p2; + + if (e1.X * e2.Y - e1.Y * e2.X >= 0) + clockWiseCount++; + else + counterClockWiseCount++; + + p1 = p2; + } + + return (clockWiseCount > counterClockWiseCount) + ? WindingOrder.Clockwise + : WindingOrder.CounterClockwise; + } + + #endregion + + #endregion + + #region Private Methods + + #region ClipNextEar + + private static void ClipNextEar(ICollection<Triangle> triangles) + { + //find the triangle + Vertex ear = earVertices[0].Value; + Vertex prev = polygonVertices[polygonVertices.IndexOf(ear) - 1].Value; + Vertex next = polygonVertices[polygonVertices.IndexOf(ear) + 1].Value; + triangles.Add(new Triangle(ear, next, prev)); + + //remove the ear from the shape + earVertices.RemoveAt(0); + polygonVertices.RemoveAt(polygonVertices.IndexOf(ear)); + Log("\nRemoved Ear: {0}", ear); + + //validate the neighboring vertices + ValidateAdjacentVertex(prev); + ValidateAdjacentVertex(next); + + //write out the states of each of the lists +#if DEBUG + StringBuilder rString = new StringBuilder(); + foreach (Vertex v in reflexVertices) + rString.Append(string.Format("{0}, ", v.Index)); + Log("Reflex Vertices: {0}", rString); + + StringBuilder cString = new StringBuilder(); + foreach (Vertex v in convexVertices) + cString.Append(string.Format("{0}, ", v.Index)); + Log("Convex Vertices: {0}", cString); + + StringBuilder eString = new StringBuilder(); + foreach (Vertex v in earVertices) + eString.Append(string.Format("{0}, ", v.Index)); + Log("Ear Vertices: {0}", eString); +#endif + } + + #endregion + + #region ValidateAdjacentVertex + + private static void ValidateAdjacentVertex(Vertex vertex) + { + Log("Validating: {0}...", vertex); + + if (reflexVertices.Contains(vertex)) + { + if (IsConvex(vertex)) + { + reflexVertices.Remove(vertex); + convexVertices.Add(vertex); + Log("Vertex: {0} now convex", vertex); + } + else + { + Log("Vertex: {0} still reflex", vertex); + } + } + + if (convexVertices.Contains(vertex)) + { + bool wasEar = earVertices.Contains(vertex); + bool isEar = IsEar(vertex); + + if (wasEar && !isEar) + { + earVertices.Remove(vertex); + Log("Vertex: {0} no longer ear", vertex); + } + else if (!wasEar && isEar) + { + earVertices.AddFirst(vertex); + Log("Vertex: {0} now ear", vertex); + } + else + { + Log("Vertex: {0} still ear", vertex); + } + } + } + + #endregion + + #region FindConvexAndReflexVertices + + private static void FindConvexAndReflexVertices() + { + for (int i = 0; i < polygonVertices.Count; i++) + { + Vertex v = polygonVertices[i].Value; + + if (IsConvex(v)) + { + convexVertices.Add(v); + Log("Convex: {0}", v); + } + else + { + reflexVertices.Add(v); + Log("Reflex: {0}", v); + } + } + } + + #endregion + + #region FindEarVertices + + private static void FindEarVertices() + { + for (int i = 0; i < convexVertices.Count; i++) + { + Vertex c = convexVertices[i]; + + if (IsEar(c)) + { + earVertices.AddLast(c); + Log("Ear: {0}", c); + } + } + } + + #endregion + + #region IsEar + + private static bool IsEar(Vertex c) + { + Vertex p = polygonVertices[polygonVertices.IndexOf(c) - 1].Value; + Vertex n = polygonVertices[polygonVertices.IndexOf(c) + 1].Value; + + Log("Testing vertex {0} as ear with triangle {1}, {0}, {2}...", c, p, n); + + foreach (Vertex t in reflexVertices) + { + if (t.Equals(p) || t.Equals(c) || t.Equals(n)) + continue; + + if (Triangle.ContainsPoint(p, c, n, t)) + { + Log("\tTriangle contains vertex {0}...", t); + return false; + } + } + + return true; + } + + #endregion + + #region IsConvex + + private static bool IsConvex(Vertex c) + { + Vertex p = polygonVertices[polygonVertices.IndexOf(c) - 1].Value; + Vertex n = polygonVertices[polygonVertices.IndexOf(c) + 1].Value; + + Vector2 d1 = Vector2.Normalize(c.Position - p.Position); + Vector2 d2 = Vector2.Normalize(n.Position - c.Position); + Vector2 n2 = new Vector2(-d2.Y, d2.X); + + return (Vector2.Dot(d1, n2) <= 0f); + } + + #endregion + + #region IsReflex + + private static bool IsReflex(Vertex c) + { + return !IsConvex(c); + } + + #endregion + + #region Log + + [Conditional("DEBUG")] + private static void Log(string format, params object[] parameters) + { + //System.Console.WriteLine(format, parameters); + } + + #endregion + + #endregion + } + + /// <summary> + /// Specifies a desired winding order for the shape vertices. + /// </summary> + public enum WindingOrder + { + Clockwise, + CounterClockwise + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Vertex.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Vertex.cs new file mode 100644 index 0000000..2630b07 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Triangulation/Vertex.cs @@ -0,0 +1,47 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Text; + +namespace MonoGame.Extended.Triangulation +{ + /// <summary> + /// MIT Licensed: https://github.com/nickgravelyn/Triangulator + /// </summary> + struct Vertex + { + public readonly Vector2 Position; + public readonly int Index; + + public Vertex(Vector2 position, int index) + { + Position = position; + Index = index; + } + + public override bool Equals(object obj) + { + if (obj.GetType() != typeof(Vertex)) + return false; + return Equals((Vertex)obj); + } + + public bool Equals(Vertex obj) + { + return obj.Position.Equals(Position) && obj.Index == Index; + } + + public override int GetHashCode() + { + unchecked + { + return (Position.GetHashCode() * 397) ^ Index; + } + } + + public override string ToString() + { + return string.Format("{0} ({1})", Position, Index); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Vector2Extensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Vector2Extensions.cs new file mode 100644 index 0000000..b50a366 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Math/Vector2Extensions.cs @@ -0,0 +1,249 @@ +using System; +using System.Runtime.CompilerServices; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + public static class Vector2Extensions + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 SetX(this Vector2 vector2, float x) => new Vector2(x, vector2.Y); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 SetY(this Vector2 vector2, float y) => new Vector2(vector2.X, y); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Translate(this Vector2 vector2, float x, float y) => new Vector2(vector2.X + x, vector2.Y + y); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Size2 ToSize(this Vector2 value) => new Size2(value.X, value.Y); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Size2 ToAbsoluteSize(this Vector2 value) + { + var x = Math.Abs(value.X); + var y = Math.Abs(value.Y); + return new Size2(x, y); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Round(this Vector2 value, int digits, MidpointRounding mode) + { + var x = (float)Math.Round(value.X, digits, mode); + var y = (float)Math.Round(value.Y, digits, mode); + return new Vector2(x, y); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Round(this Vector2 value, int digits) + { + var x = (float)Math.Round(value.X, digits); + var y = (float)Math.Round(value.Y, digits); + return new Vector2(x, y); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Round(this Vector2 value) + { + var x = (float)Math.Round(value.X); + var y = (float)Math.Round(value.Y); + return new Vector2(x, y); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool EqualsWithTolerence(this Vector2 value, Vector2 otherValue, float tolerance = 0.00001f) + { + return Math.Abs(value.X - otherValue.X) <= tolerance && (Math.Abs(value.Y - otherValue.Y) <= tolerance); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Rotate(this Vector2 value, float radians) + { + var cos = (float) Math.Cos(radians); + var sin = (float) Math.Sin(radians); + return new Vector2(value.X*cos - value.Y*sin, value.X*sin + value.Y*cos); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 NormalizedCopy(this Vector2 value) + { + var newVector2 = new Vector2(value.X, value.Y); + newVector2.Normalize(); + return newVector2; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 PerpendicularClockwise(this Vector2 value) => new Vector2(value.Y, -value.X); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 PerpendicularCounterClockwise(this Vector2 value) => new Vector2(-value.Y, value.X); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Truncate(this Vector2 value, float maxLength) + { + if (value.LengthSquared() > maxLength*maxLength) + return value.NormalizedCopy()*maxLength; + + return value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsNaN(this Vector2 value) => float.IsNaN(value.X) || float.IsNaN(value.Y); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float ToAngle(this Vector2 value) => (float) Math.Atan2(value.X, -value.Y); + + /// <summary> + /// Calculates the dot product of two vectors. If the two vectors are unit vectors, the dot product returns a floating + /// point value between -1 and 1 that can be used to determine some properties of the angle between two vectors. For + /// example, it can show whether the vectors are orthogonal, parallel, or have an acute or obtuse angle between them. + /// </summary> + /// <param name="vector1">The first vector.</param> + /// <param name="vector2">The second vector.</param> + /// <returns>The dot product of the two vectors.</returns> + /// <remarks> + /// <para>The dot product is also known as the inner product.</para> + /// <para> + /// For any two vectors, the dot product is defined as: <c>(vector1.X * vector2.X) + (vector1.Y * vector2.Y).</c> + /// The result of this calculation, plus or minus some margin to account for floating point error, is equal to: + /// <c>Length(vector1) * Length(vector2) * System.Math.Cos(theta)</c>, where <c>theta</c> is the angle between the + /// two vectors. + /// </para> + /// <para> + /// If <paramref name="vector1" /> and <paramref name="vector2" /> are unit vectors, the length of each + /// vector will be equal to 1. So, when <paramref name="vector1" /> and <paramref name="vector2" /> are unit + /// vectors, the dot product is simply equal to the cosine of the angle between the two vectors. For example, both + /// <c>cos</c> values in the following calcuations would be equal in value: + /// <c>vector1.Normalize(); vector2.Normalize(); var cos = vector1.Dot(vector2)</c>, + /// <c>var cos = System.Math.Cos(theta)</c>, where <c>theta</c> is angle in radians betwen the two vectors. + /// </para> + /// <para> + /// If <paramref name="vector1" /> and <paramref name="vector2" /> are unit vectors, without knowing the value of + /// <c>theta</c> or using a potentially processor-intensive trigonometric function, the value of the dot product + /// can tell us the + /// following things: + /// <list type="bullet"> + /// <item> + /// <description> + /// If <c>vector1.Dot(vector2) > 0</c>, the angle between the two vectors + /// is less than 90 degrees. + /// </description> + /// </item> + /// <item> + /// <description> + /// If <c>vector1.Dot(vector2) < 0</c>, the angle between the two vectors + /// is more than 90 degrees. + /// </description> + /// </item> + /// <item> + /// <description> + /// If <c>vector1.Dot(vector2) == 0</c>, the angle between the two vectors + /// is 90 degrees; that is, the vectors are othogonal. + /// </description> + /// </item> + /// <item> + /// <description> + /// If <c>vector1.Dot(vector2) == 1</c>, the angle between the two vectors + /// is 0 degrees; that is, the vectors point in the same direction and are parallel. + /// </description> + /// </item> + /// <item> + /// <description> + /// If <c>vector1.Dot(vector2) == -1</c>, the angle between the two vectors + /// is 180 degrees; that is, the vectors point in opposite directions and are parallel. + /// </description> + /// </item> + /// </list> + /// </para> + /// <note type="caution"> + /// Because of floating point error, two orthogonal vectors may not return a dot product that is exactly zero. It + /// might be zero plus some amount of floating point error. In your code, you will want to determine what amount of + /// error is acceptable in your calculation, and take that into account when you do your comparisons. + /// </note> + /// </remarks> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Dot(this Vector2 vector1, Vector2 vector2) + { + return vector1.X*vector2.X + vector1.Y*vector2.Y; + } + + /// <summary> + /// Calculates the scalar projection of one vector onto another. The scalar projection returns the length of the + /// orthogonal projection of the first vector onto a straight line parallel to the second vector, with a negative value + /// if the projection has an opposite direction with respect to the second vector. + /// </summary> + /// <param name="vector1">The first vector.</param> + /// <param name="vector2">The second vector.</param> + /// <returns>The scalar projection of <paramref name="vector1" /> onto <paramref name="vector2" />.</returns> + /// <remarks> + /// <para> + /// The scalar projection is also known as the scalar resolute of the first vector in the direction of the second + /// vector. + /// </para> + /// <para> + /// For any two vectors, the scalar projection is defined as: <c>vector1.Dot(vector2) / Length(vector2)</c>. The + /// result of this calculation, plus or minus some margin to account for floating point error, is equal to: + /// <c>Length(vector1) * System.Math.Cos(theta)</c>, where <c>theta</c> is the angle in radians between + /// <paramref name="vector1" /> and <paramref name="vector2" />. + /// </para> + /// <para> + /// The value of the scalar projection can tell us the following things: + /// <list type="bullet"> + /// <item> + /// <description> + /// If <c>vector1.ScalarProjectOnto(vector2) >= 0</c>, the angle between <paramref name="vector1" /> + /// and <paramref name="vector2" /> is between 0 degrees (exclusive) and 90 degrees (inclusive). + /// </description> + /// </item> + /// <item> + /// <description> + /// If <c>vector1.ScalarProjectOnto(vector2) < 0</c>, the angle between <paramref name="vector1" /> + /// and <paramref name="vector2" /> is between 90 degrees (exclusive) and 180 degrees (inclusive). + /// </description> + /// </item> + /// </list> + /// </para> + /// </remarks> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float ScalarProjectOnto(this Vector2 vector1, Vector2 vector2) + { + var dotNumerator = vector1.X*vector2.X + vector1.Y*vector2.Y; + var lengthSquaredDenominator = vector2.X*vector2.X + vector2.Y*vector2.Y; + return dotNumerator/(float) Math.Sqrt(lengthSquaredDenominator); + } + + /// <summary> + /// Calculates the vector projection of one vector onto another. The vector projection returns the orthogonal + /// projection of the first vector onto a straight line parallel to the second vector. + /// </summary> + /// <param name="vector1">The first vector.</param> + /// <param name="vector2">The second vector.</param> + /// <returns>The vector projection of <paramref name="vector1" /> onto <paramref name="vector2" />.</returns> + /// <remarks> + /// <para> + /// The vector projection is also known as the vector component or vector resolute of the first vector in the + /// direction of the second vector. + /// </para> + /// <para> + /// For any two vectors, the vector projection is defined as: + /// <c>( vector1.Dot(vector2) / Length(vector2)^2 ) * vector2</c>. + /// The + /// result of this calculation, plus or minus some margin to account for floating point error, is equal to: + /// <c>( Length(vector1) * System.Math.Cos(theta) ) * vector2 / Length(vector2)</c>, where <c>theta</c> is the + /// angle in radians between <paramref name="vector1" /> and <paramref name="vector2" />. + /// </para> + /// <para> + /// This function is easier to compute than <see cref="ScalarProjectOnto" /> since it does not use a square root. + /// When the vector projection and the scalar projection is required, consider using this function; the scalar + /// projection can be obtained by taking the length of the projection vector. + /// </para> + /// </remarks> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 ProjectOnto(this Vector2 vector1, Vector2 vector2) + { + var dotNumerator = vector1.X*vector2.X + vector1.Y*vector2.Y; + var lengthSquaredDenominator = vector2.X*vector2.X + vector2.Y*vector2.Y; + return dotNumerator/lengthSquaredDenominator*vector2; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/MonoGame.Extended.csproj b/Plugins/MonoGame.Extended/source/MonoGame.Extended/MonoGame.Extended.csproj new file mode 100644 index 0000000..e99bbe5 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/MonoGame.Extended.csproj @@ -0,0 +1,14 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <Description>It makes MonoGame more awesome.</Description> + <PackageTags>monogame extended pipeline bmfont tiled texture atlas input viewport fps shapes sprite</PackageTags> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="MonoGame.Framework.Content.Pipeline" + Version="3.8.1.303" + PrivateAssets="All" /> + </ItemGroup> + +</Project> diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/MonoGame.Extended.csproj.DotSettings b/Plugins/MonoGame.Extended/source/MonoGame.Extended/MonoGame.Extended.csproj.DotSettings new file mode 100644 index 0000000..66c073e --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/MonoGame.Extended.csproj.DotSettings @@ -0,0 +1,2 @@ +<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> + <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=math/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/OrthographicCamera.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/OrthographicCamera.cs new file mode 100644 index 0000000..ef98c99 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/OrthographicCamera.cs @@ -0,0 +1,209 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using MonoGame.Extended.ViewportAdapters; + +namespace MonoGame.Extended +{ + public sealed class OrthographicCamera : Camera<Vector2>, IMovable, IRotatable + { + private readonly ViewportAdapter _viewportAdapter; + private float _maximumZoom = float.MaxValue; + private float _minimumZoom; + private float _zoom; + + public OrthographicCamera(GraphicsDevice graphicsDevice) + : this(new DefaultViewportAdapter(graphicsDevice)) + { + } + + public OrthographicCamera(ViewportAdapter viewportAdapter) + { + _viewportAdapter = viewportAdapter; + + Rotation = 0; + Zoom = 1; + Origin = new Vector2(viewportAdapter.VirtualWidth/2f, viewportAdapter.VirtualHeight/2f); + Position = Vector2.Zero; + } + + public override Vector2 Position { get; set; } + public override float Rotation { get; set; } + public override Vector2 Origin { get; set; } + public override Vector2 Center => Position + Origin; + + public override float Zoom + { + get => _zoom; + set + { + if ((value < MinimumZoom) || (value > MaximumZoom)) + throw new ArgumentException("Zoom must be between MinimumZoom and MaximumZoom"); + + _zoom = value; + } + } + + public override float MinimumZoom + { + get => _minimumZoom; + set + { + if (value < 0) + throw new ArgumentException("MinimumZoom must be greater than zero"); + + if (Zoom < value) + Zoom = MinimumZoom; + + _minimumZoom = value; + } + } + + public override float MaximumZoom + { + get => _maximumZoom; + set + { + if (value < 0) + throw new ArgumentException("MaximumZoom must be greater than zero"); + + if (Zoom > value) + Zoom = value; + + _maximumZoom = value; + } + } + + 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 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) + { + ClampZoom(Zoom + deltaZoom); + } + + public override void ZoomOut(float deltaZoom) + { + ClampZoom(Zoom - deltaZoom); + } + + private void ClampZoom(float value) + { + if (value < MinimumZoom) + Zoom = MinimumZoom; + else + Zoom = value > MaximumZoom ? MaximumZoom : value; + } + + public override void LookAt(Vector2 position) + { + Position = position - new Vector2(_viewportAdapter.VirtualWidth/2f, _viewportAdapter.VirtualHeight/2f); + } + + public Vector2 WorldToScreen(float x, float y) + { + return WorldToScreen(new Vector2(x, y)); + } + + public override Vector2 WorldToScreen(Vector2 worldPosition) + { + var viewport = _viewportAdapter.Viewport; + return Vector2.Transform(worldPosition + new Vector2(viewport.X, viewport.Y), GetViewMatrix()); + } + + public Vector2 ScreenToWorld(float x, float y) + { + return ScreenToWorld(new Vector2(x, y)); + } + + public override Vector2 ScreenToWorld(Vector2 screenPosition) + { + var viewport = _viewportAdapter.Viewport; + return Vector2.Transform(screenPosition - new Vector2(viewport.X, viewport.Y), + Matrix.Invert(GetViewMatrix())); + } + + 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, 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); + } + + 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); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/GameScreen.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/GameScreen.cs new file mode 100644 index 0000000..7752c02 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/GameScreen.cs @@ -0,0 +1,19 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Screens +{ + public abstract class GameScreen : Screen + { + protected GameScreen(Game game) + { + Game = game; + } + + public Game Game { get; } + public ContentManager Content => Game.Content; + public GraphicsDevice GraphicsDevice => Game.GraphicsDevice; + public GameServiceContainer Services => Game.Services; + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Screen.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Screen.cs new file mode 100644 index 0000000..59137f4 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Screen.cs @@ -0,0 +1,17 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Screens +{ + public abstract class Screen : IDisposable + { + public ScreenManager ScreenManager { get; internal set; } + + public virtual void Dispose() { } + public virtual void Initialize() { } + public virtual void LoadContent() { } + public virtual void UnloadContent() { } + public abstract void Update(GameTime gameTime); + public abstract void Draw(GameTime gameTime); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/ScreenManager.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/ScreenManager.cs new file mode 100644 index 0000000..ad441c6 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/ScreenManager.cs @@ -0,0 +1,78 @@ +using Microsoft.Xna.Framework; +using MonoGame.Extended.Screens.Transitions; + +namespace MonoGame.Extended.Screens +{ + public class ScreenManager : SimpleDrawableGameComponent + { + public ScreenManager() + { + } + + private Screen _activeScreen; + //private bool _isInitialized; + //private bool _isLoaded; + private Transition _activeTransition; + + public void LoadScreen(Screen screen, Transition transition) + { + if(_activeTransition != null) + return; + + _activeTransition = transition; + _activeTransition.StateChanged += (sender, args) => LoadScreen(screen); + _activeTransition.Completed += (sender, args) => + { + _activeTransition.Dispose(); + _activeTransition = null; + }; + } + + public void LoadScreen(Screen screen) + { + _activeScreen?.UnloadContent(); + _activeScreen?.Dispose(); + + screen.ScreenManager = this; + + screen.Initialize(); + + screen.LoadContent(); + + _activeScreen = screen; + } + + public override void Initialize() + { + base.Initialize(); + _activeScreen?.Initialize(); + //_isInitialized = true; + } + + protected override void LoadContent() + { + base.LoadContent(); + _activeScreen?.LoadContent(); + //_isLoaded = true; + } + + protected override void UnloadContent() + { + base.UnloadContent(); + _activeScreen?.UnloadContent(); + //_isLoaded = false; + } + + public override void Update(GameTime gameTime) + { + _activeScreen?.Update(gameTime); + _activeTransition?.Update(gameTime); + } + + public override void Draw(GameTime gameTime) + { + _activeScreen?.Draw(gameTime); + _activeTransition?.Draw(gameTime); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/ExpandTransition.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/ExpandTransition.cs new file mode 100644 index 0000000..32a8cca --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/ExpandTransition.cs @@ -0,0 +1,42 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Screens.Transitions +{ + public class ExpandTransition : Transition + { + private readonly GraphicsDevice _graphicsDevice; + private readonly SpriteBatch _spriteBatch; + + public ExpandTransition(GraphicsDevice graphicsDevice, Color color, float duration = 1.0f) + : base(duration) + { + Color = color; + + _graphicsDevice = graphicsDevice; + _spriteBatch = new SpriteBatch(graphicsDevice); + } + + public override void Dispose() + { + _spriteBatch.Dispose(); + } + + public Color Color { get; } + + public override void Draw(GameTime gameTime) + { + var halfWidth = _graphicsDevice.Viewport.Width / 2f; + var halfHeight = _graphicsDevice.Viewport.Height / 2f; + var x = halfWidth * (1.0f - Value); + var y = halfHeight * (1.0f - Value); + var width = _graphicsDevice.Viewport.Width * Value; + var height = _graphicsDevice.Viewport.Height * Value; + var rectangle = new RectangleF(x, y, width, height); + + _spriteBatch.Begin(samplerState: SamplerState.PointClamp); + _spriteBatch.FillRectangle(rectangle, Color); + _spriteBatch.End(); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/FadeTransition.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/FadeTransition.cs new file mode 100644 index 0000000..49fab4a --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/FadeTransition.cs @@ -0,0 +1,34 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Screens.Transitions +{ + public class FadeTransition : Transition + { + private readonly GraphicsDevice _graphicsDevice; + private readonly SpriteBatch _spriteBatch; + + public FadeTransition(GraphicsDevice graphicsDevice, Color color, float duration = 1.0f) + : base(duration) + { + Color = color; + + _graphicsDevice = graphicsDevice; + _spriteBatch = new SpriteBatch(graphicsDevice); + } + + public override void Dispose() + { + _spriteBatch.Dispose(); + } + + public Color Color { get; } + + public override void Draw(GameTime gameTime) + { + _spriteBatch.Begin(samplerState: SamplerState.PointClamp); + _spriteBatch.FillRectangle(0, 0, _graphicsDevice.Viewport.Width, _graphicsDevice.Viewport.Height, Color * Value); + _spriteBatch.End(); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/Transition.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/Transition.cs new file mode 100644 index 0000000..8336a04 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Screens/Transitions/Transition.cs @@ -0,0 +1,58 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Screens.Transitions +{ + public enum TransitionState { In, Out } + + public abstract class Transition : IDisposable + { + private readonly float _halfDuration; + private float _currentSeconds; + + protected Transition(float duration) + { + Duration = duration; + _halfDuration = Duration / 2f; + } + + public abstract void Dispose(); + + public TransitionState State { get; private set; } = TransitionState.Out; + public float Duration { get; } + public float Value => MathHelper.Clamp(_currentSeconds / _halfDuration, 0f, 1f); + + public event EventHandler StateChanged; + public event EventHandler Completed; + + public void Update(GameTime gameTime) + { + var elapsedSeconds = gameTime.GetElapsedSeconds(); + + switch (State) + { + case TransitionState.Out: + _currentSeconds += elapsedSeconds; + + if (_currentSeconds >= _halfDuration) + { + State = TransitionState.In; + StateChanged?.Invoke(this, EventArgs.Empty); + } + break; + case TransitionState.In: + _currentSeconds -= elapsedSeconds; + + if (_currentSeconds <= 0.0f) + { + Completed?.Invoke(this, EventArgs.Empty); + } + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + public abstract void Draw(GameTime gameTime); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/BaseTypeJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/BaseTypeJsonConverter.cs new file mode 100644 index 0000000..cdbec1e --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/BaseTypeJsonConverter.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MonoGame.Extended.Serialization +{ + public abstract class BaseTypeJsonConverter<T> : JsonConverter<T> + { + private readonly string _suffix; + private readonly Dictionary<string, Type> _namesToTypes; + private readonly Dictionary<Type, string> _typesToNames; + private readonly JsonSerializerOptions _serializerOptions; + private readonly JsonNamingPolicy _namingPolicy = JsonNamingPolicy.CamelCase; + + protected BaseTypeJsonConverter(IEnumerable<TypeInfo> supportedTypes, string suffix) + { + _suffix = suffix; + _namesToTypes = supportedTypes + .ToDictionary(t => TrimSuffix(t.Name, suffix), t => t.AsType(), StringComparer.OrdinalIgnoreCase); + _typesToNames = _namesToTypes.ToDictionary(i => i.Value, i => i.Key); + + _serializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = _namingPolicy, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + _serializerOptions.Converters.Add(this); + } + + /// <inheritdoc /> + public override bool CanConvert(Type typeToConvert) => + _namesToTypes.ContainsValue(typeToConvert) || typeof(T) == typeToConvert; + + /// <inheritdoc /> + /// <exception cref="InvalidOperationException" /> + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using (JsonDocument doc = JsonDocument.ParseValue(ref reader)) + { + var jObject = doc.RootElement; + var key = jObject.GetProperty("type").GetString(); + + if (_namesToTypes.TryGetValue(key, out Type type)) + { + var value = JsonSerializer.Deserialize(jObject.GetRawText(), type, options); + return (T)value; + } + + throw new InvalidOperationException($"Unknown {_suffix} type '{key}'"); + } + } + + /// <inheritdoc /> + /// <exception cref="ArgumentNullException"> + /// Throw if <paramref name="writer"/> is <see langword="null"/>. + /// </exception> + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + var type = value.GetType(); + var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); + + writer.WriteStartObject(); + writer.WriteString("type", _typesToNames[type]); + + foreach (var property in properties) + { + var propertyName = _namingPolicy.ConvertName(property.Name); + writer.WritePropertyName(propertyName); + JsonSerializer.Serialize(writer, property.GetValue(value), property.PropertyType, options); + } + + writer.WriteEndObject(); + } + + private static string TrimSuffix(string input, string suffix) + { + if (input.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)) + return input.Substring(0, input.Length - suffix.Length); + + return input; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ColorJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ColorJsonConverter.cs new file mode 100644 index 0000000..41ceabe --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ColorJsonConverter.cs @@ -0,0 +1,34 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Serialization; + +/// <summary> +/// Converts a <see cref="Color"/> value to or from JSON. +/// </summary> +public class ColorJsonConverter : JsonConverter<Color> +{ + /// <inheritdoc /> + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Color); + + /// <inheritdoc /> + public override Color Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + return value[0] == '#' ? ColorHelper.FromHex(value) : ColorHelper.FromName(value); + } + + /// <inheritdoc /> + /// <exception cref="ArgumentNullException"> + /// Throw if <paramref name="writer"/> is <see langword="null"/>. + /// </exception> + public override void Write(Utf8JsonWriter writer, Color value, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(writer); + var hexValue = ColorHelper.ToHex(value); + writer.WriteStringValue(hexValue); + } +} + diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ContentManagerJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ContentManagerJsonConverter.cs new file mode 100644 index 0000000..6b57bfe --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ContentManagerJsonConverter.cs @@ -0,0 +1,49 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Xna.Framework.Content; + +namespace MonoGame.Extended.Serialization; + +/// <summary> +/// Loads content from a JSON file into the <see cref="ContentManager"/> using the asset name +/// </summary> +/// <typeparam name="T">The type of content to load</typeparam> +public class ContentManagerJsonConverter<T> : JsonConverter<T> +{ + private readonly ContentManager _contentManager; + private readonly Func<T, string> _getAssetName; + + /// <summary> + /// Initializes a new instance of the <see cref="ContentManagerJsonConverter{T}"/> class. + /// </summary> + /// <param name="contentManager">The <see cref="ContentManager"/> used to load content.</param> + /// <param name="getAssetName">A function that returns the asset name for a given instance of <typeparamref name="T"/>.</param> + public ContentManagerJsonConverter(ContentManager contentManager, Func<T, string> getAssetName) + { + _contentManager = contentManager; + _getAssetName = getAssetName; + } + + /// <inheritdoc /> + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(T); + + /// <inheritdoc /> + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var assetName = reader.GetString(); + return _contentManager.Load<T>(assetName); + } + + /// <inheritdoc /> + /// <exception cref="ArgumentNullException"> + /// Throw if <paramref name="writer"/> is <see langword="null"/>. + /// </exception> + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(writer); + var asset = (T)value; + var assetName = _getAssetName(asset); + writer.WriteStringValue(assetName); + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/FloatStringConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/FloatStringConverter.cs new file mode 100644 index 0000000..a19c589 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/FloatStringConverter.cs @@ -0,0 +1,34 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MonoGame.Extended.Serialization; + +public class FloatStringConverter : JsonConverter<float> +{ + /// <inheritdoc /> + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(float) || typeToConvert == typeof(string); + + /// <inheritdoc /> + public override float Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + if (float.TryParse(reader.GetString(), out float value)) + return value; + } + else if (reader.TokenType == JsonTokenType.Number) + { + return reader.GetSingle(); + } + + throw new JsonException($"Unable to convert value of type {reader.TokenType} to {typeof(float)}"); + } + + /// <inheritdoc /> + public override void Write(Utf8JsonWriter writer, float value, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(writer); + writer.WriteNumberValue(value); + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/HslColorJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/HslColorJsonConverter.cs new file mode 100644 index 0000000..6d63a15 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/HslColorJsonConverter.cs @@ -0,0 +1,33 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MonoGame.Extended.Serialization; + +/// <summary> +/// Converts a <see cref="HslColor"/> value to or from JSON. +/// </summary> +public class HslColorJsonConverter : JsonConverter<HslColor> +{ + private readonly ColorJsonConverter _colorConverter = new ColorJsonConverter(); + + /// <inheritdoc /> + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(HslColor); + + /// <inheritdoc /> + public override HslColor Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var color = _colorConverter.Read(ref reader, typeToConvert, options); + return HslColor.FromRgb(color); + } + + /// <inheritdoc /> + /// <exception cref="ArgumentNullException"> + /// Throw if <paramref name="writer"/> is <see langword="null"/>. + /// </exception> + public override void Write(Utf8JsonWriter writer, HslColor value, JsonSerializerOptions options) + { + var color = ((HslColor)value).ToRgb(); + _colorConverter.Write(writer, color, options); + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/JsonContentLoader.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/JsonContentLoader.cs new file mode 100644 index 0000000..69a1212 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/JsonContentLoader.cs @@ -0,0 +1,18 @@ +using System.Text.Json; +using Microsoft.Xna.Framework.Content; +using MonoGame.Extended.Content; + + +namespace MonoGame.Extended.Serialization +{ + public class JsonContentLoader : IContentLoader + { + public T Load<T>(ContentManager contentManager, string path) + { + + using var stream = contentManager.OpenStream(path); + var monoGameSerializerOptions = MonoGameJsonSerializerOptionsProvider.GetOptions(contentManager, path); + return JsonSerializer.Deserialize<T>(stream, monoGameSerializerOptions); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/JsonContentTypeReader.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/JsonContentTypeReader.cs new file mode 100644 index 0000000..1d5609e --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/JsonContentTypeReader.cs @@ -0,0 +1,16 @@ +using System.IO; +using System.Text.Json; +using Microsoft.Xna.Framework.Content; + + +namespace MonoGame.Extended.Serialization +{ + public class JsonContentTypeReader<T> : ContentTypeReader<T> + { + protected override T Read(ContentReader reader, T existingInstance) + { + var json = reader.ReadString(); + return JsonSerializer.Deserialize<T>(json); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/MonoGameJsonSerializerOptionsProvider.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/MonoGameJsonSerializerOptionsProvider.cs new file mode 100644 index 0000000..32a099f --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/MonoGameJsonSerializerOptionsProvider.cs @@ -0,0 +1,29 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Xna.Framework.Content; +using MonoGame.Extended.TextureAtlases; + +namespace MonoGame.Extended.Serialization; + +public static class MonoGameJsonSerializerOptionsProvider +{ + public static JsonSerializerOptions GetOptions(ContentManager contentManager, string contentPath) + { + var options = new JsonSerializerOptions + { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + options.Converters.Add(new RangeJsonConverter<int>()); + options.Converters.Add(new RangeJsonConverter<float>()); + options.Converters.Add(new RangeJsonConverter<HslColor>()); + options.Converters.Add(new ThicknessJsonConverter()); + options.Converters.Add(new RectangleFJsonConverter()); + options.Converters.Add(new TextureAtlasJsonConverter(contentManager, contentPath)); + options.Converters.Add(new Size2JsonConverter()); + + return options; + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/NinePatchRegion2DJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/NinePatchRegion2DJsonConverter.cs new file mode 100644 index 0000000..44bbbf7 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/NinePatchRegion2DJsonConverter.cs @@ -0,0 +1,94 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using MonoGame.Extended.TextureAtlases; + +namespace MonoGame.Extended.Serialization; + +/// <summary> +/// Converts a <see cref="NinePatchRegion2D"/> value to or from JSON. +/// </summary> +public class NinePatchRegion2DJsonConverter : JsonConverter<NinePatchRegion2D> +{ + private readonly ITextureRegionService _textureRegionService; + + /// <summary> + /// Initializes a new instance of the <see cref="NinePatchRegion2DJsonConverter"/> class. + /// </summary> + /// <param name="textureRegionService">The texture region service used to retrieve texture regions.</param> + public NinePatchRegion2DJsonConverter(ITextureRegionService textureRegionService) + { + _textureRegionService = textureRegionService; + } + + /// <inheritdoc /> + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(NinePatchRegion2D); + + /// <inheritdoc /> + /// <exception cref="JsonException"> + /// Thrown if the JSON property does not contain a properly formatted <see cref="NinePatchRegion2D"/> value + /// </exception> + public override NinePatchRegion2D Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException($"Expected {nameof(JsonTokenType.StartObject)} token"); + } + + string padding = string.Empty; + string regionName = string.Empty; + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + break; + } + + if (reader.TokenType == JsonTokenType.PropertyName) + { + var propertyName = reader.GetString(); + reader.Read(); + + if (propertyName.Equals("Padding", StringComparison.Ordinal)) + { + padding = reader.GetString(); + } + else if (propertyName.Equals("TextureRegion", StringComparison.Ordinal)) + { + regionName = reader.GetString(); + } + } + } + + if (string.IsNullOrEmpty(padding) || string.IsNullOrEmpty(regionName)) + { + throw new JsonException($"Missing required properties \"Padding\" and \"TextureRegion\""); + } + + var thickness = Thickness.Parse(padding); + var region = _textureRegionService.GetTextureRegion(regionName); + + return new NinePatchRegion2D(region, thickness.Left, thickness.Top, thickness.Right, thickness.Bottom); + } + + /// <inheritdoc /> + /// <exception cref="ArgumentNullException"> + /// Throw if <paramref name="writer"/> is <see langword="null"/>. + /// </exception> + public override void Write(Utf8JsonWriter writer, NinePatchRegion2D value, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(writer); + + if (value is null) + { + writer.WriteNullValue(); + return; + } + + writer.WriteStartObject(); + writer.WriteString("TextureRegion", value.Name); + writer.WriteString("Padding", value.Padding.ToString()); + writer.WriteEndObject(); + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/RangeJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/RangeJsonConverter.cs new file mode 100644 index 0000000..ef5b5cb --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/RangeJsonConverter.cs @@ -0,0 +1,50 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MonoGame.Extended.Serialization; + +/// <summary> +/// Converts a <see cref="Range{T}"/> value to or from JSON. +/// </summary> +public class RangeJsonConverter<T> : JsonConverter<Range<T>> where T : IComparable<T> +{ + /// <inheritdoc /> + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Range<T>); + + /// <inheritdoc /> + public override Range<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + Span<T> values = reader.ReadAsMultiDimensional<T>(); + + if (values.Length == 2) + { + if (values[0].CompareTo(values[1]) < 0) + { + return new Range<T>(values[0], values[1]); + } + + return new Range<T>(values[1], values[0]); + } + + if (values.Length == 1) + { + return new Range<T>(values[0], values[0]); + } + + throw new InvalidOperationException("Invalid range"); + } + + /// <inheritdoc /> + /// <exception cref="ArgumentNullException"> + /// Throw if <paramref name="writer"/> is <see langword="null"/>. + /// </exception> + public override void Write(Utf8JsonWriter writer, Range<T> value, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(writer); + writer.WriteStartArray(); + JsonSerializer.Serialize(writer, value.Min, options); + JsonSerializer.Serialize(writer, value.Max, options); + writer.WriteEndArray(); + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/RectangleFJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/RectangleFJsonConverter.cs new file mode 100644 index 0000000..080400a --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/RectangleFJsonConverter.cs @@ -0,0 +1,31 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MonoGame.Extended.Serialization; + +/// <summary> +/// Converts a <see cref="RectangleF"/> value to or from JSON. +/// </summary> +public class RectangleFJsonConverter : JsonConverter<RectangleF> +{ + /// <inheritdoc /> + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(RectangleF); + + /// <inheritdoc /> + public override RectangleF Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var values = reader.ReadAsMultiDimensional<float>(); + return new RectangleF(values[0], values[1], values[2], values[3]); + } + + /// <inheritdoc /> + /// <exception cref="ArgumentNullException"> + /// Throw if <paramref name="writer"/> is <see langword="null"/>. + /// </exception> + public override void Write(Utf8JsonWriter writer, RectangleF value, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(writer); + writer.WriteStringValue($"{value.Left} {value.Top} {value.Width} {value.Height}"); + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Size2JsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Size2JsonConverter.cs new file mode 100644 index 0000000..9eeb329 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Size2JsonConverter.cs @@ -0,0 +1,45 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MonoGame.Extended.Serialization; + +/// <summary> +/// Converts a <see cref="Size2"/> value to or from JSON. +/// </summary> +public class Size2JsonConverter : JsonConverter<Size2> +{ + /// <inheritdoc /> + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Size2); + + /// <inheritdoc /> + /// <exception cref="JsonException"> + /// Thrown if the JSON property does not contain a properly formatted <see cref="Size2"/> value + /// </exception> + public override Size2 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var values = reader.ReadAsMultiDimensional<float>(); + + if (values.Length == 2) + { + return new Size2(values[0], values[1]); + } + + if (values.Length == 1) + { + return new Size2(values[0], values[0]); + } + + throw new JsonException("Invalid Size2 property value"); + } + + /// <inheritdoc /> + /// <exception cref="ArgumentNullException"> + /// Throw if <paramref name="writer"/> is <see langword="null"/>. + /// </exception> + public override void Write(Utf8JsonWriter writer, Size2 value, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(writer); + writer.WriteStringValue($"{value.Width} {value.Height}"); + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/SizeJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/SizeJsonConverter.cs new file mode 100644 index 0000000..3b454a1 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/SizeJsonConverter.cs @@ -0,0 +1,45 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MonoGame.Extended.Serialization; + +/// <summary> +/// Converts a <see cref="Size"/> value to or from JSON. +/// </summary> +public class SizeJsonConverter : JsonConverter<Size> +{ + /// <inheritdoc /> + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Size); + + /// <inheritdoc /> + /// <exception cref="JsonException"> + /// Thrown if the JSON property does not contain a properly formatted <see cref="Size"/> value + /// </exception> + public override Size Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var values = reader.ReadAsMultiDimensional<int>(); + + if (values.Length == 2) + { + return new Size(values[0], values[1]); + } + + if (values.Length == 1) + { + return new Size(values[0], values[0]); + } + + throw new JsonException("Invalid Size property value"); + } + + /// <inheritdoc /> + /// <exception cref="ArgumentNullException"> + /// Throw if <paramref name="writer"/> is <see langword="null"/>. + /// </exception> + public override void Write(Utf8JsonWriter writer, Size value, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(writer); + writer.WriteStringValue($"{value.Width} {value.Height}"); + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextContentLoader.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextContentLoader.cs new file mode 100644 index 0000000..273aaed --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextContentLoader.cs @@ -0,0 +1,18 @@ +using System.IO; +using Microsoft.Xna.Framework.Content; +using MonoGame.Extended.Content; + +namespace MonoGame.Extended.Serialization +{ + public class TextContentLoader : IContentLoader<string> + { + public string Load(ContentManager contentManager, string path) + { + using (var stream = contentManager.OpenStream(path)) + using (var reader = new StreamReader(stream)) + { + return reader.ReadToEnd(); + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextureRegion2DJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextureRegion2DJsonConverter.cs new file mode 100644 index 0000000..dbcb66c --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextureRegion2DJsonConverter.cs @@ -0,0 +1,52 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using MonoGame.Extended.TextureAtlases; + +namespace MonoGame.Extended.Serialization; + +/// <summary> +/// Converts a <see cref="TextureRegion2D"/> value to or from JSON. +/// </summary> +public class TextureRegion2DJsonConverter : JsonConverter<TextureRegion2D> +{ + private readonly ITextureRegionService _textureRegionService; + + /// <summary> + /// Initializes a new instance of the <see cref="TextureRegion2DJsonConverter"/> class. + /// </summary> + /// <param name="textureRegionService">The texture region service to use for retrieving texture regions.</param> + /// <exception cref="ArgumentNullException"> + /// Thrown if <paramref name="textureRegionService"/> is <see langword="null"/>. + /// </exception> + public TextureRegion2DJsonConverter(ITextureRegionService textureRegionService) + { + ArgumentNullException.ThrowIfNull(textureRegionService); + _textureRegionService = textureRegionService; + } + + /// <inheritdoc /> + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(TextureRegion2D); + + /// <inheritdoc /> + public override TextureRegion2D Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var regionName = reader.GetString(); + return string.IsNullOrEmpty(regionName) ? null : _textureRegionService.GetTextureRegion(regionName); + } + + /// <inheritdoc /> + /// <exception cref="ArgumentNullException"> + /// Throw if <paramref name="writer"/> is <see langword="null"/>. + /// + /// -or- + /// + /// Thrown if <paramref name="value"/> is <see langword="null"/>. + /// </exception> + public override void Write(Utf8JsonWriter writer, TextureRegion2D value, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(writer); + ArgumentNullException.ThrowIfNull(value); + writer.WriteStringValue(value.Name); + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextureRegionService.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextureRegionService.cs new file mode 100644 index 0000000..3914730 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/TextureRegionService.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Linq; +using MonoGame.Extended.TextureAtlases; + +namespace MonoGame.Extended.Serialization +{ + public interface ITextureRegionService + { + TextureRegion2D GetTextureRegion(string name); + } + + public class TextureRegionService : ITextureRegionService + { + public TextureRegionService() + { + TextureAtlases = new List<TextureAtlas>(); + NinePatches = new List<NinePatchRegion2D>(); + } + + public IList<TextureAtlas> TextureAtlases { get; } + public IList<NinePatchRegion2D> NinePatches { get; } + + public TextureRegion2D GetTextureRegion(string name) + { + var ninePatch = NinePatches.FirstOrDefault(p => p.Name == name); + + if (ninePatch != null) + return ninePatch; + + return TextureAtlases + .Select(textureAtlas => textureAtlas.GetRegion(name)) + .FirstOrDefault(region => region != null); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ThicknessJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ThicknessJsonConverter.cs new file mode 100644 index 0000000..1a60573 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/ThicknessJsonConverter.cs @@ -0,0 +1,31 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MonoGame.Extended.Serialization; + +/// <summary> +/// Converts a <see cref="Thickness"/> value to or from JSON. +/// </summary> +public class ThicknessJsonConverter : JsonConverter<Thickness> +{ + /// <inheritdoc /> + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Thickness); + + /// <inheritdoc /> + public override Thickness Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var values = reader.ReadAsMultiDimensional<int>(); + return Thickness.FromValues(values); + } + + /// <inheritdoc /> + /// <exception cref="ArgumentNullException"> + /// Throw if <paramref name="writer"/> is <see langword="null"/> + /// </exception> + public override void Write(Utf8JsonWriter writer, Thickness value, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(writer); + writer.WriteStringValue($"{value.Left} {value.Top} {value.Right} {value.Bottom}"); + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Utf8JsonReaderExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Utf8JsonReaderExtensions.cs new file mode 100644 index 0000000..73ab532 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Utf8JsonReaderExtensions.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text.Json; + +namespace MonoGame.Extended.Serialization; + +/// <summary> +/// Provides extension methods for working with <see cref="Utf8JsonReader"/>. +/// </summary> +public static class Utf8JsonReaderExtensions +{ + private static readonly Dictionary<Type, Func<string, object>> s_stringParsers = new Dictionary<Type, Func<string, object>> + { + {typeof(int), s => int.Parse(s, CultureInfo.InvariantCulture.NumberFormat)}, + {typeof(float), s => float.Parse(s, CultureInfo.InvariantCulture.NumberFormat)}, + {typeof(HslColor), s => ColorExtensions.FromHex(s).ToHsl() } + }; + + /// <summary> + /// Reads a multi-dimensional JSON array and converts it to an array of the specified type. + /// </summary> + /// <typeparam name="T">The type of the array elements.</typeparam> + /// <param name="reader">The <see cref="Utf8JsonReader"/> to read from.</param> + /// <returns>An array of the specified type.</returns> + /// <exception cref="NotSupportedException">Thrown when the token type is not supported.</exception> + public static T[] ReadAsMultiDimensional<T>(this ref Utf8JsonReader reader) + { + var tokenType = reader.TokenType; + + switch (tokenType) + { + case JsonTokenType.StartArray: + return reader.ReadAsJArray<T>(); + + case JsonTokenType.String: + return reader.ReadAsDelimitedString<T>(); + + case JsonTokenType.Number: + return reader.ReadAsSingleValue<T>(); + + default: + throw new NotSupportedException($"{tokenType} is not currently supported in the multi-dimensional parser"); + } + } + + private static T[] ReadAsSingleValue<T>(this ref Utf8JsonReader reader) + { + var token = JsonDocument.ParseValue(ref reader).RootElement; + var value = JsonSerializer.Deserialize<T>(token.GetRawText()); + return new T[] { value }; + } + + private static T[] ReadAsJArray<T>(this ref Utf8JsonReader reader) + { + var items = new List<T>(); + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + { + if (reader.TokenType == JsonTokenType.EndArray) + { + break; + } + + items.Add(JsonSerializer.Deserialize<T>(ref reader)); + } + + return items.ToArray(); + } + + private static T[] ReadAsDelimitedString<T>(this ref Utf8JsonReader reader) + { + var value = reader.GetString(); + if (string.IsNullOrEmpty(value)) + { + return Array.Empty<T>(); + } + + Span<string> values = value.Split(' '); + var result = new T[values.Length]; + var parser = s_stringParsers[typeof(T)]; + + for (int i = 0; i < values.Length; i++) + { + result[i] = (T)parser(values[i]); + } + + return result; + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Vector2JsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Vector2JsonConverter.cs new file mode 100644 index 0000000..e10c72b --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Serialization/Vector2JsonConverter.cs @@ -0,0 +1,46 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Serialization; + +/// <summary> +/// Converts a <see cref="Vector2"/> value to or from JSON. +/// </summary> +public class Vector2JsonConverter : JsonConverter<Vector2> +{ + /// <inheritdoc /> + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(Vector2); + + /// <inheritdoc /> + /// <exception cref="JsonException"> + /// Thrown if the JSON property does not contain a properly formatted <see cref="Vector2"/> value + /// </exception> + public override Vector2 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var values = reader.ReadAsMultiDimensional<float>(); + + if (values.Length == 2) + { + return new Vector2(values[0], values[1]); + } + + if (values.Length == 1) + { + return new Vector2(values[0]); + } + + throw new JsonException("Invalid Size2 property value"); + } + + /// <inheritdoc /> + /// <exception cref="ArgumentNullException"> + /// Throw if <paramref name="writer"/> is <see langword="null"/>. + /// </exception> + public override void Write(Utf8JsonWriter writer, Vector2 value, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(writer); + writer.WriteStringValue($"{value.X} {value.Y}"); + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Shapes/Polygon.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Shapes/Polygon.cs new file mode 100644 index 0000000..841ce2b --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Shapes/Polygon.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Shapes +{ + public class Polygon : IEquatable<Polygon> + { + public Polygon(IEnumerable<Vector2> vertices) + { + _localVertices = vertices.ToArray(); + _transformedVertices = _localVertices; + _offset = Vector2.Zero; + _rotation = 0; + _scale = Vector2.One; + _isDirty = false; + } + + private readonly Vector2[] _localVertices; + private Vector2[] _transformedVertices; + private Vector2 _offset; + private float _rotation; + private Vector2 _scale; + private bool _isDirty; + + public Vector2[] Vertices + { + get + { + if (_isDirty) + { + _transformedVertices = GetTransformedVertices(); + _isDirty = false; + } + + return _transformedVertices; + } + } + + public float Left + { + get { return Vertices.Min(v => v.X); } + } + + public float Right + { + get { return Vertices.Max(v => v.X); } + } + + public float Top + { + get { return Vertices.Min(v => v.Y); } + } + + public float Bottom + { + get { return Vertices.Max(v => v.Y); } + } + + public void Offset(Vector2 amount) + { + _offset += amount; + _isDirty = true; + } + + public void Rotate(float amount) + { + _rotation += amount; + _isDirty = true; + } + + public void Scale(Vector2 amount) + { + _scale += amount; + _isDirty = true; + } + + private Vector2[] GetTransformedVertices() + { + var newVertices = new Vector2[_localVertices.Length]; + var isScaled = _scale != Vector2.One; + + for (var i = 0; i < _localVertices.Length; i++) + { + var p = _localVertices[i]; + + if (isScaled) + p *= _scale; + + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (_rotation != 0) + { + var cos = (float) Math.Cos(_rotation); + var sin = (float) Math.Sin(_rotation); + p = new Vector2(cos*p.X - sin*p.Y, sin*p.X + cos*p.Y); + } + + newVertices[i] = p + _offset; + } + + return newVertices; + } + + public Polygon TransformedCopy(Vector2 offset, float rotation, Vector2 scale) + { + var polygon = new Polygon(_localVertices); + polygon.Offset(offset); + polygon.Rotate(rotation); + polygon.Scale(scale - Vector2.One); + return new Polygon(polygon.Vertices); + } + + public RectangleF BoundingRectangle + { + get + { + var minX = Left; + var minY = Top; + var maxX = Right; + var maxY = Bottom; + return new RectangleF(minX, minY, maxX - minX, maxY - minY); + } + } + + public bool Contains(Vector2 point) + { + return Contains(point.X, point.Y); + } + + public bool Contains(float x, float y) + { + var intersects = 0; + var vertices = Vertices; + + for (var i = 0; i < vertices.Length; i++) + { + var x1 = vertices[i].X; + var y1 = vertices[i].Y; + var x2 = vertices[(i + 1)%vertices.Length].X; + var y2 = vertices[(i + 1)%vertices.Length].Y; + + if ((((y1 <= y) && (y < y2)) || ((y2 <= y) && (y < y1))) && (x < (x2 - x1)/(y2 - y1)*(y - y1) + x1)) + intersects++; + } + + return (intersects & 1) == 1; + } + + public static bool operator ==(Polygon a, Polygon b) + { + return a.Equals(b); + } + + public static bool operator !=(Polygon a, Polygon b) + { + return !(a == b); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + return obj is Polygon && Equals((Polygon) obj); + } + + public bool Equals(Polygon other) + { + return Vertices.SequenceEqual(other.Vertices); + } + + public override int GetHashCode() + { + unchecked + { + return Vertices.Aggregate(27, (current, v) => current + 13*current + v.GetHashCode()); + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Shapes/Polyline.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Shapes/Polyline.cs new file mode 100644 index 0000000..97b34ff --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Shapes/Polyline.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Shapes +{ + public class Polyline + { + public Polyline(IEnumerable<Vector2> points) + { + Points = points; + } + + public IEnumerable<Vector2> Points { get; private set; } + public float Left => Points.Min(p => p.X); + public float Top => Points.Min(p => p.Y); + public float Right => Points.Max(p => p.X); + public float Bottom => Points.Max(p => p.Y); + + public RectangleF BoundingRectangle + { + get + { + var minX = Left; + var minY = Top; + var maxX = Right; + var maxY = Bottom; + return new RectangleF(minX, minY, maxX - minX, maxY - minY); + } + } + + public bool Contains(float x, float y) + { + return false; + } + + public bool Contains(Vector2 point) + { + return Contains(point.X, point.Y); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/SimpleDrawableGameComponent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/SimpleDrawableGameComponent.cs new file mode 100644 index 0000000..5487c64 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/SimpleDrawableGameComponent.cs @@ -0,0 +1,47 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + public abstract class SimpleDrawableGameComponent : SimpleGameComponent, IDrawable + { + protected SimpleDrawableGameComponent() + { + } + + private bool _isVisible = true; + public bool Visible + { + get => _isVisible; + set + { + if (_isVisible == value) + return; + + _isVisible = value; + VisibleChanged?.Invoke(this, EventArgs.Empty); + } + } + + bool IDrawable.Visible => _isVisible; + + private int _drawOrder; + public int DrawOrder + { + get => _drawOrder; + set + { + if (_drawOrder == value) + return; + + _drawOrder = value; + DrawOrderChanged?.Invoke(this, EventArgs.Empty); + } + } + + public event EventHandler<EventArgs> DrawOrderChanged; + public event EventHandler<EventArgs> VisibleChanged; + + public abstract void Draw(GameTime gameTime); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/SimpleGameComponent.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/SimpleGameComponent.cs new file mode 100644 index 0000000..57cccfc --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/SimpleGameComponent.cs @@ -0,0 +1,80 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + public abstract class SimpleGameComponent : IGameComponent, IUpdateable, IDisposable, IComparable<GameComponent>, IComparable<SimpleGameComponent> + { + private bool _isInitialized; + + protected SimpleGameComponent() + { + } + + public virtual void Dispose() + { + if (_isInitialized) + { + UnloadContent(); + _isInitialized = false; + } + } + + private bool _isEnabled = true; + public bool IsEnabled + { + get => _isEnabled; + set + { + if (_isEnabled == value) + return; + + _isEnabled = value; + EnabledChanged?.Invoke(this, EventArgs.Empty); + } + } + + public virtual void Initialize() + { + if (!_isInitialized) + { + LoadContent(); + _isInitialized = true; + } + } + + protected virtual void LoadContent() { } + protected virtual void UnloadContent() { } + + bool IUpdateable.Enabled => _isEnabled; + + private int _updateOrder; + public int UpdateOrder + { + get => _updateOrder; + set + { + if (_updateOrder == value) + return; + + _updateOrder = value; + UpdateOrderChanged?.Invoke(this, EventArgs.Empty); + } + } + + public event EventHandler<EventArgs> EnabledChanged; + public event EventHandler<EventArgs> UpdateOrderChanged; + + public abstract void Update(GameTime gameTime); + + public int CompareTo(GameComponent other) + { + return other.UpdateOrder - UpdateOrder; + } + + public int CompareTo(SimpleGameComponent other) + { + return other.UpdateOrder - UpdateOrder; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/AnimatedSprite.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/AnimatedSprite.cs new file mode 100644 index 0000000..a6bf393 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/AnimatedSprite.cs @@ -0,0 +1,50 @@ +using System; +using System.Linq; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Sprites +{ + public class AnimatedSprite : Sprite + { + private readonly SpriteSheet _spriteSheet; + private SpriteSheetAnimation _currentAnimation; + + public AnimatedSprite(SpriteSheet spriteSheet, string playAnimation = null) + : base(spriteSheet.TextureAtlas[0]) + { + _spriteSheet = spriteSheet; + + if (playAnimation != null) + Play(playAnimation); + } + + public SpriteSheetAnimation Play(string name, Action onCompleted = null) + { + if (_currentAnimation == null || _currentAnimation.IsComplete || _currentAnimation.Name != name) + { + var cycle = _spriteSheet.Cycles[name]; + var keyFrames = cycle.Frames.Select(f => _spriteSheet.TextureAtlas[f.Index]).ToArray(); + _currentAnimation = new SpriteSheetAnimation(name, keyFrames, cycle.FrameDuration, cycle.IsLooping, cycle.IsReversed, cycle.IsPingPong); + + if(_currentAnimation != null) + _currentAnimation.OnCompleted = onCompleted; + } + + return _currentAnimation; + } + + public void Update(float deltaTime) + { + if (_currentAnimation != null && !_currentAnimation.IsComplete) + { + _currentAnimation.Update(deltaTime); + TextureRegion = _currentAnimation.CurrentFrame; + } + } + + public void Update(GameTime gameTime) + { + Update(gameTime.GetElapsedSeconds()); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/Animation.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/Animation.cs new file mode 100644 index 0000000..cd6e092 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/Animation.cs @@ -0,0 +1,86 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Sprites +{ + public abstract class Animation : IUpdate, IDisposable + { + private readonly bool _disposeOnComplete; + private readonly Action _onCompleteAction; + private bool _isComplete; + + protected Animation(Action onCompleteAction, bool disposeOnComplete) + { + _onCompleteAction = onCompleteAction; + _disposeOnComplete = disposeOnComplete; + IsPaused = false; + } + + public bool IsComplete + { + get { return _isComplete; } + protected set + { + if (_isComplete != value) + { + _isComplete = value; + + if (_isComplete) + { + _onCompleteAction?.Invoke(); + + if (_disposeOnComplete) + Dispose(); + } + } + } + } + + public bool IsDisposed { get; private set; } + public bool IsPlaying => !IsPaused && !IsComplete; + public bool IsPaused { get; private set; } + public float CurrentTime { get; protected set; } + + public virtual void Dispose() + { + IsDisposed = true; + } + + public void Update(GameTime gameTime) + { + Update(gameTime.GetElapsedSeconds()); + } + + public void Play() + { + IsPaused = false; + } + + public void Pause() + { + IsPaused = true; + } + + public void Stop() + { + Pause(); + Rewind(); + } + + public void Rewind() + { + CurrentTime = 0; + } + + protected abstract bool OnUpdate(float deltaTime); + + public void Update(float deltaTime) + { + if (!IsPlaying) + return; + + CurrentTime += deltaTime; + IsComplete = OnUpdate(deltaTime); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/ISpriteBatchDrawable.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/ISpriteBatchDrawable.cs new file mode 100644 index 0000000..ab31417 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/ISpriteBatchDrawable.cs @@ -0,0 +1,18 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using MonoGame.Extended.TextureAtlases; + +namespace MonoGame.Extended.Sprites +{ + public interface ISpriteBatchDrawable + { + bool IsVisible { get; } + TextureRegion2D TextureRegion { get; } + Vector2 Position { get; } + float Rotation { get; } + Vector2 Scale { get; } + Color Color { get; } + Vector2 Origin { get; } + SpriteEffects Effect { get; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/Sprite.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/Sprite.cs new file mode 100644 index 0000000..2174c8a --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/Sprite.cs @@ -0,0 +1,109 @@ +using System; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using MonoGame.Extended.TextureAtlases; + +namespace MonoGame.Extended.Sprites +{ + public class Sprite : IColorable + { + private TextureRegion2D _textureRegion; + + public Sprite(TextureRegion2D textureRegion) + { + if (textureRegion == null) throw new ArgumentNullException(nameof(textureRegion)); + + _textureRegion = textureRegion; + + Alpha = 1.0f; + Color = Color.White; + IsVisible = true; + Effect = SpriteEffects.None; + OriginNormalized = new Vector2(0.5f, 0.5f); + Depth = 0.0f; + } + + public Sprite(Texture2D texture) + : this(new TextureRegion2D(texture)) + { + } + + public float Alpha { get; set; } + public float Depth { get; set; } + public object Tag { get; set; } + + public Vector2 OriginNormalized + { + get => new Vector2(Origin.X/TextureRegion.Width, Origin.Y/TextureRegion.Height); + set => Origin = new Vector2(value.X*TextureRegion.Width, value.Y*TextureRegion.Height); + } + + public Color Color { get; set; } + + public RectangleF GetBoundingRectangle(Transform2 transform) + { + return GetBoundingRectangle(transform.Position, transform.Rotation, transform.Scale); + } + + public RectangleF GetBoundingRectangle(Vector2 position, float rotation, Vector2 scale) + { + var corners = GetCorners(position, rotation, scale); + var min = new Vector2(corners.Min(i => i.X), corners.Min(i => i.Y)); + var max = new Vector2(corners.Max(i => i.X), corners.Max(i => i.Y)); + return new RectangleF(min.X, min.Y, max.X - min.X, max.Y - min.Y); + } + + public bool IsVisible { get; set; } + public Vector2 Origin { get; set; } + public SpriteEffects Effect { get; set; } + + public TextureRegion2D TextureRegion + { + get => _textureRegion; + set + { + if (value == null) + throw new InvalidOperationException("TextureRegion cannot be null"); + + // preserve the origin if the texture size changes + var originNormalized = OriginNormalized; + _textureRegion = value; + OriginNormalized = originNormalized; + } + } + + public Vector2[] GetCorners(Vector2 position, float rotation, Vector2 scale) + { + var min = -Origin; + var max = min + new Vector2(TextureRegion.Width, TextureRegion.Height); + var offset = position; + + if (scale != Vector2.One) + { + min *= scale; + max = max * scale; + } + + var corners = new Vector2[4]; + corners[0] = min; + corners[1] = new Vector2(max.X, min.Y); + corners[2] = max; + corners[3] = new Vector2(min.X, max.Y); + + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (rotation != 0) + { + var matrix = Matrix.CreateRotationZ(rotation); + + for (var i = 0; i < 4; i++) + corners[i] = Vector2.Transform(corners[i], matrix); + } + + for (var i = 0; i < 4; i++) + corners[i] += offset; + + return corners; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteExtensions.cs new file mode 100644 index 0000000..983065b --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteExtensions.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.Sprites +{ + public static class SpriteExtensions + { + public static void Draw(this Sprite sprite, SpriteBatch spriteBatch, Vector2 position, float rotation, Vector2 scale) + { + Draw(spriteBatch, sprite, position, rotation, scale); + } + + public static void Draw(this SpriteBatch spriteBatch, Sprite sprite, Transform2 transform) + { + Draw(spriteBatch, sprite, transform.Position, transform.Rotation, transform.Scale); + } + + public static void Draw(this SpriteBatch spriteBatch, Sprite sprite, Vector2 position, float rotation = 0) + { + Draw(spriteBatch, sprite, position, rotation, Vector2.One); + } + + public static void Draw(this SpriteBatch spriteBatch, Sprite sprite, Vector2 position, float rotation, Vector2 scale) + { + if (sprite == null) throw new ArgumentNullException(nameof(sprite)); + + if (sprite.IsVisible) + { + var texture = sprite.TextureRegion.Texture; + var sourceRectangle = sprite.TextureRegion.Bounds; + spriteBatch.Draw(texture, position, sourceRectangle, sprite.Color*sprite.Alpha, rotation, sprite.Origin, scale, sprite.Effect, sprite.Depth); + } + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheet.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheet.cs new file mode 100644 index 0000000..6a8acb1 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheet.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Linq; +using MonoGame.Extended.TextureAtlases; + +namespace MonoGame.Extended.Sprites +{ + public class SpriteSheet + { + public SpriteSheet() + { + Cycles = new Dictionary<string, SpriteSheetAnimationCycle>(); + } + + public TextureAtlas TextureAtlas { get; set; } + public Dictionary<string, SpriteSheetAnimationCycle> Cycles { get; set; } + + public SpriteSheetAnimation CreateAnimation(string name) + { + var cycle = Cycles[name]; + var keyFrames = cycle.Frames + .Select(f => TextureAtlas[f.Index]) + .ToArray(); + + return new SpriteSheetAnimation(name, keyFrames, cycle.FrameDuration, cycle.IsLooping, cycle.IsReversed, cycle.IsPingPong); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimation.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimation.cs new file mode 100644 index 0000000..3b60e5e --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimation.cs @@ -0,0 +1,107 @@ +using System; +using System.Linq; +using MonoGame.Extended.TextureAtlases; + +namespace MonoGame.Extended.Sprites +{ + public class SpriteSheetAnimation : Animation + { + public const float DefaultFrameDuration = 0.2f; + + public SpriteSheetAnimation(string name, TextureAtlas textureAtlas, float frameDuration = DefaultFrameDuration, + bool isLooping = true, bool isReversed = false, bool isPingPong = false) + : this(name, textureAtlas.Regions.ToArray(), frameDuration, isLooping, isReversed, isPingPong) + { + } + + public SpriteSheetAnimation(string name, TextureRegion2D[] keyFrames, float frameDuration = DefaultFrameDuration, + bool isLooping = true, bool isReversed = false, bool isPingPong = false) + : base(null, false) + { + Name = name; + KeyFrames = keyFrames; + FrameDuration = frameDuration; + IsLooping = isLooping; + IsReversed = isReversed; + IsPingPong = isPingPong; + CurrentFrameIndex = IsReversed ? KeyFrames.Length - 1 : 0; + } + + public SpriteSheetAnimation(string name, TextureRegion2D[] keyFrames, SpriteSheetAnimationData data) + : this(name, keyFrames, data.FrameDuration, data.IsLooping, data.IsReversed, data.IsPingPong) + { + } + + public string Name { get; } + public TextureRegion2D[] KeyFrames { get; } + public float FrameDuration { get; set; } + public bool IsLooping { get; set; } + public bool IsReversed { get; set; } + public bool IsPingPong { get; set; } + public new bool IsComplete => CurrentTime >= AnimationDuration; + + public float AnimationDuration => IsPingPong + ? (KeyFrames.Length*2 - (IsLooping ? 2 : 1))*FrameDuration + : KeyFrames.Length*FrameDuration; + + public TextureRegion2D CurrentFrame => KeyFrames[CurrentFrameIndex]; + public int CurrentFrameIndex { get; private set; } + + public float FramesPerSecond + { + get => 1.0f/FrameDuration; + set => FrameDuration = value/1.0f; + } + + public Action OnCompleted { get; set; } + + protected override bool OnUpdate(float deltaTime) + { + if (IsComplete) + { + if (IsLooping) + CurrentTime %= AnimationDuration; + else + OnCompleted?.Invoke(); + } + + if (KeyFrames.Length == 1) + { + CurrentFrameIndex = 0; + return IsComplete; + } + + var frameIndex = (int) (CurrentTime/FrameDuration); + var length = KeyFrames.Length; + + if (IsPingPong) + { + if (IsComplete) + frameIndex = 0; + else + { + frameIndex = frameIndex % (length * 2 - 2); + + if (frameIndex >= length) + frameIndex = length - 2 - (frameIndex - length); + } + } + + if (IsLooping) + { + if (IsReversed) + { + frameIndex = frameIndex%length; + frameIndex = length - frameIndex - 1; + } + else + frameIndex = frameIndex%length; + } + else + frameIndex = IsReversed ? Math.Max(length - frameIndex - 1, 0) : Math.Min(length - 1, frameIndex); + + CurrentFrameIndex = frameIndex; + return IsComplete; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationCycle.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationCycle.cs new file mode 100644 index 0000000..d9e8674 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationCycle.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace MonoGame.Extended.Sprites +{ + public class SpriteSheetAnimationCycle + { + public SpriteSheetAnimationCycle() + { + Frames = new List<SpriteSheetAnimationFrame>(); + } + + public float FrameDuration { get; set; } = 0.2f; + public List<SpriteSheetAnimationFrame> Frames { get; set; } + public bool IsLooping { get; set; } + public bool IsReversed { get; set; } + public bool IsPingPong { get; set; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationData.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationData.cs new file mode 100644 index 0000000..0bdf89d --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationData.cs @@ -0,0 +1,21 @@ +namespace MonoGame.Extended.Sprites +{ + public class SpriteSheetAnimationData + { + public SpriteSheetAnimationData(int[] frameIndicies, float frameDuration = 0.2f, bool isLooping = true, + bool isReversed = false, bool isPingPong = false) + { + FrameIndicies = frameIndicies; + FrameDuration = frameDuration; + IsLooping = isLooping; + IsReversed = isReversed; + IsPingPong = isPingPong; + } + + public int[] FrameIndicies { get; } + public float FrameDuration { get; } + public bool IsLooping { get; } + public bool IsReversed { get; } + public bool IsPingPong { get; } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationFrame.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationFrame.cs new file mode 100644 index 0000000..2de8e39 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Sprites/SpriteSheetAnimationFrame.cs @@ -0,0 +1,52 @@ +using System; +using System.Diagnostics; +using System.Text.Json; +using System.Text.Json.Serialization; + + +namespace MonoGame.Extended.Sprites +{ + [JsonConverter(typeof(SpriteSheetAnimationFrameJsonConverter))] + [DebuggerDisplay("{Index} {Duration}")] + public class SpriteSheetAnimationFrame + { + public SpriteSheetAnimationFrame(int index, float duration = 0.2f) + { + Index = index; + Duration = duration; + } + + public int Index { get; set; } + public float Duration { get; set; } + } + + public class SpriteSheetAnimationFrameJsonConverter : JsonConverter<SpriteSheetAnimationFrame> + { + /// <inheritdoc /> + public override SpriteSheetAnimationFrame Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.Number: + var index = reader.GetInt32(); + return new SpriteSheetAnimationFrame(index); + + case JsonTokenType.StartObject: + var frame = JsonSerializer.Deserialize<SpriteSheetAnimationFrame>(ref reader, options); + return frame; + + case JsonTokenType.Null: + return null; + + default: + throw new JsonException(); + } + } + + public override void Write(Utf8JsonWriter writer, SpriteSheetAnimationFrame value, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(writer); + JsonSerializer.Serialize(writer, value, options); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/NinePatchRegion2D.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/NinePatchRegion2D.cs new file mode 100644 index 0000000..49323e0 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/NinePatchRegion2D.cs @@ -0,0 +1,84 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.TextureAtlases +{ + public class NinePatchRegion2D : TextureRegion2D + { + public Rectangle[] SourcePatches { get; } = new Rectangle[9]; + public Thickness Padding { get; } + public int LeftPadding => Padding.Left; + public int TopPadding => Padding.Top; + public int RightPadding => Padding.Right; + public int BottomPadding => Padding.Bottom; + + public NinePatchRegion2D(TextureRegion2D textureRegion, Thickness padding) + : base(textureRegion.Name, textureRegion.Texture, textureRegion.X, textureRegion.Y, textureRegion.Width, textureRegion.Height) + { + Padding = padding; + CachePatches(textureRegion.Bounds, SourcePatches); + } + + public NinePatchRegion2D(TextureRegion2D textureRegion, int padding) + : this(textureRegion, padding, padding, padding, padding) + { + } + + public NinePatchRegion2D(TextureRegion2D textureRegion, int leftRightPadding, int topBottomPadding) + : this(textureRegion, leftRightPadding, topBottomPadding, leftRightPadding, topBottomPadding) + { + } + + public NinePatchRegion2D(TextureRegion2D textureRegion, int leftPadding, int topPadding, int rightPadding, int bottomPadding) + : this(textureRegion, new Thickness(leftPadding, topPadding, rightPadding, bottomPadding)) + { + } + + public NinePatchRegion2D(Texture2D texture, Thickness thickness) + : this(new TextureRegion2D(texture), thickness) + { + } + + public const int TopLeft = 0; + public const int TopMiddle = 1; + public const int TopRight = 2; + public const int MiddleLeft = 3; + public const int Middle = 4; + public const int MiddleRight = 5; + public const int BottomLeft = 6; + public const int BottomMiddle = 7; + public const int BottomRight = 8; + + private readonly Rectangle[] _destinationPatches = new Rectangle[9]; + + public Rectangle[] CreatePatches(Rectangle rectangle) + { + CachePatches(rectangle, _destinationPatches); + return _destinationPatches; + } + + private void CachePatches(Rectangle sourceRectangle, Rectangle[] patchCache) + { + var x = sourceRectangle.X; + var y = sourceRectangle.Y; + var w = sourceRectangle.Width; + var h = sourceRectangle.Height; + var middleWidth = w - LeftPadding - RightPadding; + var middleHeight = h - TopPadding - BottomPadding; + var bottomY = y + h - BottomPadding; + var rightX = x + w - RightPadding; + var leftX = x + LeftPadding; + var topY = y + TopPadding; + + patchCache[TopLeft] = new Rectangle(x, y, LeftPadding, TopPadding); + patchCache[TopMiddle] = new Rectangle(leftX, y, middleWidth, TopPadding); + patchCache[TopRight] = new Rectangle(rightX, y, RightPadding, TopPadding); + patchCache[MiddleLeft] = new Rectangle(x, topY, LeftPadding, middleHeight); + patchCache[Middle] = new Rectangle(leftX, topY, middleWidth, middleHeight); + patchCache[MiddleRight] = new Rectangle(rightX, topY, RightPadding, middleHeight); + patchCache[BottomLeft] = new Rectangle(x, bottomY, LeftPadding, BottomPadding); + patchCache[BottomMiddle] = new Rectangle(leftX, bottomY, middleWidth, BottomPadding); + patchCache[BottomRight] = new Rectangle(rightX, bottomY, RightPadding, BottomPadding); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlas.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlas.cs new file mode 100644 index 0000000..d9fd41d --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlas.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.TextureAtlases +{ + /// <summary> + /// Defines a texture atlas which stores a source image and contains regions specifying its sub-images. + /// </summary> + /// <remarks> + /// <para> + /// Texture atlas (also called a tile map, tile engine, or sprite sheet) is a large image containing a collection, + /// or "atlas", of sub-images, each of which is a texture map for some part of a 2D or 3D model. + /// The sub-textures can be rendered by modifying the texture coordinates of the object's uvmap on the atlas, + /// essentially telling it which part of the image its texture is in. + /// In an application where many small textures are used frequently, it is often more efficient to store the + /// textures in a texture atlas which is treated as a single unit by the graphics hardware. + /// This saves memory and because there are less rendering state changes by binding once, it can be faster to bind + /// one large texture once than to bind many smaller textures as they are drawn. + /// Careful alignment may be needed to avoid bleeding between sub textures when used with mipmapping, and artefacts + /// between tiles for texture compression. + /// </para> + /// </remarks> + public class TextureAtlas : IEnumerable<TextureRegion2D> + { + /// <summary> + /// Initializes a new texture atlas with an empty list of regions. + /// </summary> + /// <param name="name">The asset name of this texture atlas</param> + /// <param name="texture">Source <see cref="Texture2D " /> image used to draw on screen.</param> + public TextureAtlas(string name, Texture2D texture) + { + Name = name; + Texture = texture; + + _regionsByName = new Dictionary<string, TextureRegion2D>(); + _regionsByIndex = new List<TextureRegion2D>(); + } + + /// <inheritdoc /> + /// <summary> + /// Initializes a new texture atlas and populates it with regions. + /// </summary> + /// <param name="name">The asset name of this texture atlas</param> + /// <param name="texture">Source <see cref="!:Texture2D " /> image used to draw on screen.</param> + /// <param name="regions">A collection of regions to populate the atlas with.</param> + public TextureAtlas(string name, Texture2D texture, Dictionary<string, Rectangle> regions) + : this(name, texture) + { + foreach (var region in regions) + CreateRegion(region.Key, region.Value.X, region.Value.Y, region.Value.Width, region.Value.Height); + } + + private readonly Dictionary<string, TextureRegion2D> _regionsByName; + private readonly List<TextureRegion2D> _regionsByIndex; + + public string Name { get; } + + /// <summary> + /// Gets a source <see cref="Texture2D" /> image. + /// </summary> + public Texture2D Texture { get; } + + /// <summary> + /// Gets a list of regions in the <see cref="TextureAtlas" />. + /// </summary> + public IEnumerable<TextureRegion2D> Regions => _regionsByIndex; + + /// <summary> + /// Gets the number of regions in the <see cref="TextureAtlas" />. + /// </summary> + public int RegionCount => _regionsByIndex.Count; + + public TextureRegion2D this[string name] => GetRegion(name); + public TextureRegion2D this[int index] => GetRegion(index); + + /// <summary> + /// Gets the enumerator of the <see cref="TextureAtlas" />' list of regions. + /// </summary> + /// <returns>The <see cref="IEnumerator" /> of regions.</returns> + public IEnumerator<TextureRegion2D> GetEnumerator() + { + return _regionsByIndex.GetEnumerator(); + } + + /// <summary> + /// Gets the enumerator of the <see cref="TextureAtlas" />' list of regions. + /// </summary> + /// <returns>The <see cref="IEnumerator" /> of regions</returns> + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// <summary> + /// Determines whether the texture atlas contains a region + /// </summary> + /// <param name="name">Name of the texture region.</param> + /// <returns></returns> + public bool ContainsRegion(string name) + { + return _regionsByName.ContainsKey(name); + } + + /// <summary> + /// Internal method for adding region + /// </summary> + /// <param name="region">Texture region.</param> + private void AddRegion(TextureRegion2D region) + { + _regionsByIndex.Add(region); + _regionsByName.Add(region.Name, region); + } + + /// <summary> + /// Creates a new texture region and adds it to the list of the <see cref="TextureAtlas" />' regions. + /// </summary> + /// <param name="name">Name of the texture region.</param> + /// <param name="x">X coordinate of the region's top left corner.</param> + /// <param name="y">Y coordinate of the region's top left corner.</param> + /// <param name="width">Width of the texture region.</param> + /// <param name="height">Height of the texture region.</param> + /// <returns>Created texture region.</returns> + public TextureRegion2D CreateRegion(string name, int x, int y, int width, int height) + { + if (_regionsByName.ContainsKey(name)) + throw new InvalidOperationException($"Region {name} already exists in the texture atlas"); + + var region = new TextureRegion2D(name, Texture, x, y, width, height); + AddRegion(region); + return region; + } + + /// <summary> + /// Creates a new nine patch texture region and adds it to the list of the <see cref="TextureAtlas" />' regions. + /// </summary> + /// <param name="name">Name of the texture region.</param> + /// <param name="x">X coordinate of the region's top left corner.</param> + /// <param name="y">Y coordinate of the region's top left corner.</param> + /// <param name="width">Width of the texture region.</param> + /// <param name="height">Height of the texture region.</param> + /// <param name="thickness">Thickness of the nine patch region.</param> + /// <returns>Created texture region.</returns> + public NinePatchRegion2D CreateNinePatchRegion(string name, int x, int y, int width, int height, Thickness thickness) + { + if (_regionsByName.ContainsKey(name)) + throw new InvalidOperationException($"Region {name} already exists in the texture atlas"); + + var textureRegion = new TextureRegion2D(name, Texture, x, y, width, height); + var ninePatchRegion = new NinePatchRegion2D(textureRegion, thickness); + AddRegion(ninePatchRegion); + return ninePatchRegion; + } + + /// <summary> + /// Removes a texture region from the <see cref="TextureAtlas" /> + /// </summary> + /// <param name="index">An index of the <see cref="TextureRegion2D" /> in <see cref="Region" /> to remove</param> + public void RemoveRegion(int index) + { + var region = _regionsByIndex[index]; + _regionsByIndex.RemoveAt(index); + + if(region.Name != null) + _regionsByName.Remove(region.Name); + } + + /// <summary> + /// Removes a texture region from the <see cref="TextureAtlas" /> + /// </summary> + /// <param name="name">Name of the <see cref="TextureRegion2D" /> to remove</param> + public void RemoveRegion(string name) + { + if (_regionsByName.TryGetValue(name, out var region)) + { + _regionsByName.Remove(name); + _regionsByIndex.Remove(region); + } + } + + /// <summary> + /// Gets a <see cref="TextureRegion2D" /> from the <see cref="TextureAtlas" />' list. + /// </summary> + /// <param name="index">An index of the <see cref="TextureRegion2D" /> in <see cref="Region" /> to get.</param> + /// <returns>The <see cref="TextureRegion2D" />.</returns> + public TextureRegion2D GetRegion(int index) + { + if (index < 0 || index >= _regionsByIndex.Count) + throw new IndexOutOfRangeException(); + + return _regionsByIndex[index]; + } + + /// <summary> + /// Gets a <see cref="TextureRegion2D" /> from the <see cref="TextureAtlas" />' list. + /// </summary> + /// <param name="name">Name of the <see cref="TextureRegion2D" /> to get.</param> + /// <returns>The <see cref="TextureRegion2D" />.</returns> + public TextureRegion2D GetRegion(string name) + { + return GetRegion<TextureRegion2D>(name); + } + + /// <summary> + /// Gets a texture region from the <see cref="TextureAtlas" /> of a specified type. + /// This is can be useful if the atlas contains <see cref="NinePatchRegion2D"/>'s. + /// </summary> + /// <typeparam name="T">Type of the region to get</typeparam> + /// <param name="name">Name of the region to get</param> + /// <returns>The texture region</returns> + public T GetRegion<T>(string name) where T : TextureRegion2D + { + if (_regionsByName.TryGetValue(name, out var region)) + return (T)region; + + throw new KeyNotFoundException(name); + } + + /// <summary> + /// Creates a new <see cref="TextureAtlas" /> and populates it with a grid of <see cref="TextureRegion2D" />. + /// </summary> + /// <param name="name">The name of this texture atlas</param> + /// <param name="texture">Source <see cref="Texture2D" /> image used to draw on screen</param> + /// <param name="regionWidth">Width of the <see cref="TextureRegion2D" />.</param> + /// <param name="regionHeight">Height of the <see cref="TextureRegion2D" />.</param> + /// <param name="maxRegionCount">The number of <see cref="TextureRegion2D" /> to create.</param> + /// <param name="margin">Minimum distance of the regions from the border of the source <see cref="Texture2D" /> image.</param> + /// <param name="spacing">Horizontal and vertical space between regions.</param> + /// <returns>A created and populated <see cref="TextureAtlas" />.</returns> + public static TextureAtlas Create(string name, Texture2D texture, int regionWidth, int regionHeight, + int maxRegionCount = int.MaxValue, int margin = 0, int spacing = 0) + { + var textureAtlas = new TextureAtlas(name, texture); + var count = 0; + var width = texture.Width - margin; + var height = texture.Height - margin; + var xIncrement = regionWidth + spacing; + var yIncrement = regionHeight + spacing; + + for (var y = margin; y < height; y += yIncrement) + { + for (var x = margin; x < width; x += xIncrement) + { + var regionName = $"{texture.Name ?? "region"}{count}"; + textureAtlas.CreateRegion(regionName, x, y, regionWidth, regionHeight); + count++; + + if (count >= maxRegionCount) + return textureAtlas; + } + } + + return textureAtlas; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasExtensions.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasExtensions.cs new file mode 100644 index 0000000..0ec6b86 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasExtensions.cs @@ -0,0 +1,113 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.TextureAtlases +{ + public static class TextureAtlasExtensions + { + public static void Draw(this SpriteBatch spriteBatch, TextureRegion2D textureRegion, Vector2 position, Color color, Rectangle? clippingRectangle = null) + { + Draw(spriteBatch, textureRegion, position, color, 0, Vector2.Zero, Vector2.One, SpriteEffects.None, 0, clippingRectangle); + } + + public static void Draw(this SpriteBatch spriteBatch, TextureRegion2D textureRegion, Vector2 position, Color color, + float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth, Rectangle? clippingRectangle = null) + { + var sourceRectangle = textureRegion.Bounds; + + if (clippingRectangle.HasValue) + { + var x = (int)(position.X - origin.X); + var y = (int)(position.Y - origin.Y); + var width = (int)(textureRegion.Width * scale.X); + var height = (int)(textureRegion.Height * scale.Y); + var destinationRectangle = new Rectangle(x, y, width, height); + + sourceRectangle = ClipSourceRectangle(textureRegion.Bounds, destinationRectangle, clippingRectangle.Value); + position.X += sourceRectangle.X - textureRegion.Bounds.X; + position.Y += sourceRectangle.Y - textureRegion.Bounds.Y; + + if(sourceRectangle.Width <= 0 || sourceRectangle.Height <= 0) + return; + } + + spriteBatch.Draw(textureRegion.Texture, position, sourceRectangle, color, rotation, origin, scale, effects, layerDepth); + } + + public static void Draw(this SpriteBatch spriteBatch, TextureRegion2D textureRegion, Rectangle destinationRectangle, Color color, Rectangle? clippingRectangle = null) + { + var ninePatchRegion = textureRegion as NinePatchRegion2D; + + if (ninePatchRegion != null) + Draw(spriteBatch, ninePatchRegion, destinationRectangle, color, clippingRectangle); + else + Draw(spriteBatch, textureRegion.Texture, textureRegion.Bounds, destinationRectangle, color, clippingRectangle); + } + + public static void Draw(this SpriteBatch spriteBatch, NinePatchRegion2D ninePatchRegion, Rectangle destinationRectangle, Color color, Rectangle? clippingRectangle = null) + { + var destinationPatches = ninePatchRegion.CreatePatches(destinationRectangle); + var sourcePatches = ninePatchRegion.SourcePatches; + + for (var i = 0; i < sourcePatches.Length; i++) + { + var sourcePatch = sourcePatches[i]; + var destinationPatch = destinationPatches[i]; + + if (clippingRectangle.HasValue) + { + sourcePatch = ClipSourceRectangle(sourcePatch, destinationPatch, clippingRectangle.Value); + destinationPatch = ClipDestinationRectangle(destinationPatch, clippingRectangle.Value); + Draw(spriteBatch, ninePatchRegion.Texture, sourcePatch, destinationPatch, color, clippingRectangle); + } + else + { + if (destinationPatch.Width > 0 && destinationPatch.Height > 0) + spriteBatch.Draw(ninePatchRegion.Texture, sourceRectangle: sourcePatch, destinationRectangle: destinationPatch, color: color); + } + } + } + + public static void Draw(this SpriteBatch spriteBatch, Texture2D texture, Rectangle sourceRectangle, Rectangle destinationRectangle, Color color, Rectangle? clippingRectangle) + { + if (clippingRectangle.HasValue) + { + sourceRectangle = ClipSourceRectangle(sourceRectangle, destinationRectangle, clippingRectangle.Value); + destinationRectangle = ClipDestinationRectangle(destinationRectangle, clippingRectangle.Value); + } + + if (destinationRectangle.Width > 0 && destinationRectangle.Height > 0) + spriteBatch.Draw(texture, destinationRectangle, sourceRectangle, color); + } + + private static Rectangle ClipSourceRectangle(Rectangle sourceRectangle, Rectangle destinationRectangle, Rectangle clippingRectangle) + { + var left = (float)(clippingRectangle.Left - destinationRectangle.Left); + var right = (float)(destinationRectangle.Right - clippingRectangle.Right); + var top = (float)(clippingRectangle.Top - destinationRectangle.Top); + var bottom = (float)(destinationRectangle.Bottom - clippingRectangle.Bottom); + var x = left > 0 ? left : 0; + var y = top > 0 ? top : 0; + var w = (right > 0 ? right : 0) + x; + var h = (bottom > 0 ? bottom : 0) + y; + + var scaleX = (float)destinationRectangle.Width / sourceRectangle.Width; + var scaleY = (float)destinationRectangle.Height / sourceRectangle.Height; + x /= scaleX; + y /= scaleY; + w /= scaleX; + h /= scaleY; + + return new Rectangle((int)(sourceRectangle.X + x), (int)(sourceRectangle.Y + y), (int)(sourceRectangle.Width - w), (int)(sourceRectangle.Height - h)); + } + + private static Rectangle ClipDestinationRectangle(Rectangle destinationRectangle, Rectangle clippingRectangle) + { + var left = clippingRectangle.Left < destinationRectangle.Left ? destinationRectangle.Left : clippingRectangle.Left; + var top = clippingRectangle.Top < destinationRectangle.Top ? destinationRectangle.Top : clippingRectangle.Top; + var bottom = clippingRectangle.Bottom < destinationRectangle.Bottom ? clippingRectangle.Bottom : destinationRectangle.Bottom; + var right = clippingRectangle.Right < destinationRectangle.Right ? clippingRectangle.Right : destinationRectangle.Right; + return new Rectangle(left, top, right - left, bottom - top); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasJsonContentTypeReader.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasJsonContentTypeReader.cs new file mode 100644 index 0000000..20a7ae6 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasJsonContentTypeReader.cs @@ -0,0 +1,41 @@ +using System.IO; +using System.Text.Json; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using MonoGame.Extended.Content; +using MonoGame.Extended.Serialization; +using ContentReaderExtensions = MonoGame.Extended.Content.ContentReaderExtensions; + +namespace MonoGame.Extended.TextureAtlases +{ + public class TextureAtlasJsonContentTypeReader : JsonContentTypeReader<TextureAtlas> + { + private static TexturePackerFile Load(ContentReader reader) + { + var json = reader.ReadString(); + return JsonSerializer.Deserialize<TexturePackerFile>(json); + } + + protected override TextureAtlas Read(ContentReader reader, TextureAtlas existingInstance) + { + var texturePackerFile = Load(reader); + var assetName = reader.GetRelativeAssetName(texturePackerFile.Metadata.Image); + var texture = reader.ContentManager.Load<Texture2D>(assetName); + var atlas = new TextureAtlas(assetName, texture); + + var regionCount = texturePackerFile.Regions.Count; + + for (var i = 0; i < regionCount; i++) + { + atlas.CreateRegion( + ContentReaderExtensions.RemoveExtension(texturePackerFile.Regions[i].Filename), + texturePackerFile.Regions[i].Frame.X, + texturePackerFile.Regions[i].Frame.Y, + texturePackerFile.Regions[i].Frame.Width, + texturePackerFile.Regions[i].Frame.Height); + } + + return atlas; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasJsonConverter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasJsonConverter.cs new file mode 100644 index 0000000..2d88709 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasJsonConverter.cs @@ -0,0 +1,84 @@ +using System; +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using MonoGame.Extended.Content; +using MonoGame.Extended.Serialization; + +namespace MonoGame.Extended.TextureAtlases +{ + public class TextureAtlasJsonConverter : JsonConverter<TextureAtlas> + { + private readonly ContentManager _contentManager; + private readonly string _path; + + public TextureAtlasJsonConverter(ContentManager contentManager, string path) + { + _contentManager = contentManager; + _path = path; + } + + /// <inheritdoc /> + public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(TextureAtlas); + + public override TextureAtlas Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if(reader.TokenType == JsonTokenType.String) + { + // TODO: (Aristurtle 05/20/2024) What is this for? It's just an if block that throws an exception. Need + // to investigate. + var textureAtlasAssetName = reader.GetString(); + var contentPath = GetContentPath(textureAtlasAssetName); + var texturePackerFile = _contentManager.Load<TexturePackerFile>(contentPath, new JsonContentLoader()); + var texture = _contentManager.Load<Texture2D>(texturePackerFile.Metadata.Image); + //return TextureAtlas.Create(texturePackerFile.Metadata.Image, texture ); + throw new NotImplementedException(); + } + else + { + var metadata = JsonSerializer.Deserialize<InlineTextureAtlas>(ref reader, options); + + // TODO: When we get to .NET Standard 2.1 it would be more robust to use + // [Path.GetRelativePath](https://docs.microsoft.com/en-us/dotnet/api/system.io.path.getrelativepath?view=netstandard-2.1) + var textureName = Path.GetFileNameWithoutExtension(metadata.Texture); + var textureDirectory = Path.GetDirectoryName(metadata.Texture); + var directory = Path.GetDirectoryName(_path); + var relativePath = Path.Combine(_contentManager.RootDirectory, directory, textureDirectory, textureName); + var resolvedAssetName = Path.GetFullPath(relativePath); + Texture2D texture; + try + { + texture = _contentManager.Load<Texture2D>(resolvedAssetName); + } + catch (Exception ex) + { + if (textureDirectory == null || textureDirectory == "") + texture = _contentManager.Load<Texture2D>(textureName); + else + texture = _contentManager.Load<Texture2D>(textureDirectory + "/" + textureName); + } + return TextureAtlas.Create(resolvedAssetName, texture, metadata.RegionWidth, metadata.RegionHeight); + } + } + + /// <inheritdoc /> + public override void Write(Utf8JsonWriter writer, TextureAtlas value, JsonSerializerOptions options) { } + + + // ReSharper disable once ClassNeverInstantiated.Local + private class InlineTextureAtlas + { + public string Texture { get; set; } + public int RegionWidth { get; set; } + public int RegionHeight { get; set; } + } + + private string GetContentPath(string relativePath) + { + var directory = Path.GetDirectoryName(_path); + return Path.Combine(directory, relativePath); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasReader.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasReader.cs new file mode 100644 index 0000000..bfc7934 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureAtlasReader.cs @@ -0,0 +1,30 @@ +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using MonoGame.Extended.Content; + +namespace MonoGame.Extended.TextureAtlases +{ + public class TextureAtlasReader : ContentTypeReader<TextureAtlas> + { + protected override TextureAtlas Read(ContentReader reader, TextureAtlas existingInstance) + { + var assetName = reader.GetRelativeAssetName(reader.ReadString()); + var texture = reader.ContentManager.Load<Texture2D>(assetName); + var atlas = new TextureAtlas(assetName, texture); + + var regionCount = reader.ReadInt32(); + + for (var i = 0; i < regionCount; i++) + { + atlas.CreateRegion( + reader.ReadString(), + reader.ReadInt32(), + reader.ReadInt32(), + reader.ReadInt32(), + reader.ReadInt32()); + } + + return atlas; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerFile.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerFile.cs new file mode 100644 index 0000000..b2cc243 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerFile.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace MonoGame.Extended.TextureAtlases +{ + public class TexturePackerFile + { + [JsonPropertyName("frames")] + public List<TexturePackerRegion> Regions { get; set; } + + [JsonPropertyName("meta")] + public TexturePackerMeta Metadata { get; set; } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerMeta.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerMeta.cs new file mode 100644 index 0000000..5cdf21d --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerMeta.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; +using MonoGame.Extended.Serialization; + +namespace MonoGame.Extended.TextureAtlases +{ + public class TexturePackerMeta + { + [JsonPropertyName("app")] + public string App { get; set; } + + [JsonPropertyName("version")] + public string Version { get; set; } + + [JsonPropertyName("image")] + public string Image { get; set; } + + [JsonPropertyName("format")] + public string Format { get; set; } + + [JsonPropertyName("size")] + public TexturePackerSize Size { get; set; } + + [JsonPropertyName("scale")] + [JsonConverter(typeof(FloatStringConverter))] + public float Scale { get; set; } + + [JsonPropertyName("smartupdate")] + public string SmartUpdate { get; set; } + + public override string ToString() + { + return Image; + } + } +} + diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerPoint.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerPoint.cs new file mode 100644 index 0000000..c4b5acd --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerPoint.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace MonoGame.Extended.TextureAtlases +{ + public class TexturePackerPoint + { + [JsonPropertyName("x")] + public double X { get; set; } + + [JsonPropertyName("y")] + public double Y { get; set; } + + public override string ToString() + { + return string.Format("{0} {1}", X, Y); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerRectangle.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerRectangle.cs new file mode 100644 index 0000000..21f3d3b --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerRectangle.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace MonoGame.Extended.TextureAtlases +{ + public class TexturePackerRectangle + { + [JsonPropertyName("x")] + public int X { get; set; } + + [JsonPropertyName("y")] + public int Y { get; set; } + + [JsonPropertyName("w")] + public int Width { get; set; } + + [JsonPropertyName("h")] + public int Height { get; set; } + + public override string ToString() + { + return string.Format("{0} {1} {2} {3}", X, Y, Width, Height); + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerRegion.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerRegion.cs new file mode 100644 index 0000000..8664aa6 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerRegion.cs @@ -0,0 +1,33 @@ +using System.Text.Json.Serialization; + +namespace MonoGame.Extended.TextureAtlases +{ + public class TexturePackerRegion + { + [JsonPropertyName("filename")] + public string Filename { get; set; } + + [JsonPropertyName("frame")] + public TexturePackerRectangle Frame { get; set; } + + [JsonPropertyName("rotated")] + public bool IsRotated { get; set; } + + [JsonPropertyName("trimmed")] + public bool IsTrimmed { get; set; } + + [JsonPropertyName("spriteSourceSize")] + public TexturePackerRectangle SourceRectangle { get; set; } + + [JsonPropertyName("sourceSize")] + public TexturePackerSize SourceSize { get; set; } + + [JsonPropertyName("pivot")] + public TexturePackerPoint PivotPoint { get; set; } + + public override string ToString() + { + return $"{Filename} {Frame}"; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerSize.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerSize.cs new file mode 100644 index 0000000..05351d0 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TexturePackerSize.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace MonoGame.Extended.TextureAtlases +{ + public class TexturePackerSize + { + [JsonPropertyName("w")] + public int Width { get; set; } + + [JsonPropertyName("h")] + public int Height { get; set; } + + public override string ToString() + { + return $"{Width} {Height}"; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureRegion2D.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureRegion2D.cs new file mode 100644 index 0000000..05da555 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/TextureAtlases/TextureRegion2D.cs @@ -0,0 +1,54 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.TextureAtlases +{ + public class TextureRegion2D + { + public TextureRegion2D(Texture2D texture, int x, int y, int width, int height) + : this(null, texture, x, y, width, height) + { + } + + public TextureRegion2D(Texture2D texture, Rectangle region) + : this(null, texture, region.X, region.Y, region.Width, region.Height) + { + } + + public TextureRegion2D(string name, Texture2D texture, Rectangle region) + : this(name, texture, region.X, region.Y, region.Width, region.Height) + { + } + + public TextureRegion2D(Texture2D texture) + : this(texture.Name, texture, 0, 0, texture.Width, texture.Height) + { + } + + public TextureRegion2D(string name, Texture2D texture, int x, int y, int width, int height) + { + Name = name; + Texture = texture; + X = x; + Y = y; + Width = width; + Height = height; + } + + public string Name { get; } + public Texture2D Texture { get; protected set; } + public int X { get; } + public int Y { get; } + public int Width { get; } + public int Height { get; } + public Size2 Size => new Size2(Width, Height); + public object Tag { get; set; } + public Rectangle Bounds => new Rectangle(X, Y, Width, Height); + + public override string ToString() + { + return $"{Name ?? string.Empty} {Bounds}"; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/ContinuousClock.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/ContinuousClock.cs new file mode 100644 index 0000000..c31930b --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/ContinuousClock.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Timers +{ + public class ContinuousClock : GameTimer + { + public ContinuousClock(double intervalSeconds) + : base(intervalSeconds) + { + } + + public ContinuousClock(TimeSpan interval) + : base(interval) + { + } + + public TimeSpan NextTickTime { get; protected set; } + + public event EventHandler Tick; + + protected override void OnStopped() + { + NextTickTime = CurrentTime + Interval; + } + + protected override void OnUpdate(GameTime gameTime) + { + if (CurrentTime >= NextTickTime) + { + NextTickTime = CurrentTime + Interval; + Tick?.Invoke(this, EventArgs.Empty); + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/CountdownTimer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/CountdownTimer.cs new file mode 100644 index 0000000..fea7659 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/CountdownTimer.cs @@ -0,0 +1,42 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Timers +{ + public class CountdownTimer : GameTimer + { + public CountdownTimer(double intervalSeconds) + : base(intervalSeconds) + { + } + + public CountdownTimer(TimeSpan interval) + : base(interval) + { + } + + public TimeSpan TimeRemaining { get; private set; } + + public event EventHandler TimeRemainingChanged; + public event EventHandler Completed; + + protected override void OnStopped() + { + CurrentTime = TimeSpan.Zero; + } + + protected override void OnUpdate(GameTime gameTime) + { + TimeRemaining = Interval - CurrentTime; + TimeRemainingChanged?.Invoke(this, EventArgs.Empty); + + if (CurrentTime >= Interval) + { + State = TimerState.Completed; + CurrentTime = Interval; + TimeRemaining = TimeSpan.Zero; + Completed?.Invoke(this, EventArgs.Empty); + } + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/GameTimer.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/GameTimer.cs new file mode 100644 index 0000000..6a3acfe --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/GameTimer.cs @@ -0,0 +1,65 @@ +using System; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.Timers +{ + public abstract class GameTimer : IUpdate + { + protected GameTimer(double intervalSeconds) + : this(TimeSpan.FromSeconds(intervalSeconds)) + { + } + + protected GameTimer(TimeSpan interval) + { + Interval = interval; + Restart(); + } + + public TimeSpan Interval { get; set; } + public TimeSpan CurrentTime { get; protected set; } + public TimerState State { get; protected set; } + + public void Update(GameTime gameTime) + { + if (State != TimerState.Started) + return; + + CurrentTime += gameTime.ElapsedGameTime; + OnUpdate(gameTime); + } + + public event EventHandler Started; + public event EventHandler Stopped; + public event EventHandler Paused; + + public void Start() + { + State = TimerState.Started; + Started?.Invoke(this, EventArgs.Empty); + } + + public void Stop() + { + State = TimerState.Stopped; + CurrentTime = TimeSpan.Zero; + OnStopped(); + Stopped?.Invoke(this, EventArgs.Empty); + } + + public void Restart() + { + Stop(); + Start(); + } + + public void Pause() + { + State = TimerState.Paused; + Paused?.Invoke(this, EventArgs.Empty); + } + + protected abstract void OnStopped(); + protected abstract void OnUpdate(GameTime gameTime); + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/TimerState.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/TimerState.cs new file mode 100644 index 0000000..1ab74ce --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Timers/TimerState.cs @@ -0,0 +1,10 @@ +namespace MonoGame.Extended.Timers +{ + public enum TimerState + { + Started, + Stopped, + Paused, + Completed + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/Transform.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Transform.cs new file mode 100644 index 0000000..b0a0be4 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/Transform.cs @@ -0,0 +1,456 @@ +using System; +using System.ComponentModel; +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended +{ + // Code derived from top answer: http://gamedev.stackexchange.com/questions/113977/should-i-store-local-forward-right-up-vector-or-calculate-when-necessary + + [Flags] + internal enum TransformFlags : byte + { + WorldMatrixIsDirty = 1 << 0, + LocalMatrixIsDirty = 1 << 1, + All = WorldMatrixIsDirty | LocalMatrixIsDirty + } + + /// <summary> + /// Represents the base class for the position, rotation, and scale of a game object in two-dimensions or + /// three-dimensions. + /// </summary> + /// <typeparam name="TMatrix">The type of the matrix.</typeparam> + /// <remarks> + /// <para> + /// Every game object has a transform which is used to store and manipulate the position, rotation and scale + /// of the object. Every transform can have a parent, which allows to apply position, rotation and scale to game + /// objects hierarchically. + /// </para> + /// <para> + /// This class shouldn't be used directly. Instead use either of the derived classes; <see cref="Transform2" /> or + /// Transform3D. + /// </para> + /// </remarks> + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract class BaseTransform<TMatrix> + where TMatrix : struct + { + private TransformFlags _flags = TransformFlags.All; // dirty flags, set all dirty flags when created + private TMatrix _localMatrix; // model space to local space + private BaseTransform<TMatrix> _parent; // parent + private TMatrix _worldMatrix; // local space to world space + + // internal contructor because people should not be using this class directly; they should use Transform2D or Transform3D + internal BaseTransform() + { + } + + /// <summary> + /// Gets the model-to-local space <see cref="Matrix2" />. + /// </summary> + /// <value> + /// The model-to-local space <see cref="Matrix2" />. + /// </value> + public TMatrix LocalMatrix + { + get + { + RecalculateLocalMatrixIfNecessary(); // attempt to update local matrix upon request if it is dirty + return _localMatrix; + } + } + + /// <summary> + /// Gets the local-to-world space <see cref="Matrix2" />. + /// </summary> + /// <value> + /// The local-to-world space <see cref="Matrix2" />. + /// </value> + public TMatrix WorldMatrix + { + get + { + RecalculateWorldMatrixIfNecessary(); // attempt to update world matrix upon request if it is dirty + return _worldMatrix; + } + } + + /// <summary> + /// Gets or sets the parent instance. + /// </summary> + /// <value> + /// The parent instance. + /// </value> + /// <remarks> + /// <para> + /// Setting <see cref="Parent" /> to a non-null instance enables this instance to + /// inherit the position, rotation, and scale of the parent instance. Setting <see cref="Parent" /> to + /// <code>null</code> disables the inheritance altogether for this instance. + /// </para> + /// </remarks> + [EditorBrowsable(EditorBrowsableState.Never)] + public BaseTransform<TMatrix> Parent + { + get { return _parent; } + set + { + if (_parent == value) + return; + + var oldParentTransform = Parent; + _parent = value; + OnParentChanged(oldParentTransform, value); + } + } + + public event Action TransformBecameDirty; // observer pattern for when the world (or local) matrix became dirty + public event Action TranformUpdated; // observer pattern for after the world (or local) matrix was re-calculated + + /// <summary> + /// Gets the model-to-local space <see cref="Matrix2" />. + /// </summary> + /// <param name="matrix">The model-to-local space <see cref="Matrix2" />.</param> + public void GetLocalMatrix(out TMatrix matrix) + { + RecalculateLocalMatrixIfNecessary(); + matrix = _localMatrix; + } + + /// <summary> + /// Gets the local-to-world space <see cref="Matrix2" />. + /// </summary> + /// <param name="matrix">The local-to-world space <see cref="Matrix2" />.</param> + public void GetWorldMatrix(out TMatrix matrix) + { + RecalculateWorldMatrixIfNecessary(); + matrix = _worldMatrix; + } + + protected internal void LocalMatrixBecameDirty() + { + _flags |= TransformFlags.LocalMatrixIsDirty; + } + + protected internal void WorldMatrixBecameDirty() + { + _flags |= TransformFlags.WorldMatrixIsDirty; + TransformBecameDirty?.Invoke(); + } + + private void OnParentChanged(BaseTransform<TMatrix> oldParent, BaseTransform<TMatrix> newParent) + { + var parent = oldParent; + while (parent != null) + { + parent.TransformBecameDirty -= ParentOnTransformBecameDirty; + parent = parent.Parent; + } + + parent = newParent; + while (parent != null) + { + parent.TransformBecameDirty += ParentOnTransformBecameDirty; + parent = parent.Parent; + } + } + + private void ParentOnTransformBecameDirty() + { + _flags |= TransformFlags.All; + } + + private void RecalculateWorldMatrixIfNecessary() + { + if ((_flags & TransformFlags.WorldMatrixIsDirty) == 0) + return; + + RecalculateLocalMatrixIfNecessary(); + RecalculateWorldMatrix(ref _localMatrix, out _worldMatrix); + + _flags &= ~TransformFlags.WorldMatrixIsDirty; + TranformUpdated?.Invoke(); + } + + protected internal abstract void RecalculateWorldMatrix(ref TMatrix localMatrix, out TMatrix matrix); + + private void RecalculateLocalMatrixIfNecessary() + { + if ((_flags & TransformFlags.LocalMatrixIsDirty) == 0) + return; + + RecalculateLocalMatrix(out _localMatrix); + + _flags &= ~TransformFlags.LocalMatrixIsDirty; + WorldMatrixBecameDirty(); + } + + protected internal abstract void RecalculateLocalMatrix(out TMatrix matrix); + } + + /// <summary> + /// Represents the position, rotation, and scale of a two-dimensional game object. + /// </summary> + /// <seealso cref="BaseTransform{Matrix2D}" /> + /// <seealso cref="IMovable" /> + /// <seealso cref="IRotatable" /> + /// <seealso cref="IScalable" /> + /// <remarks> + /// <para> + /// Every game object has a transform which is used to store and manipulate the position, rotation and scale + /// of the object. Every transform can have a parent, which allows to apply position, rotation and scale to game + /// objects hierarchically. + /// </para> + /// </remarks> + public class Transform2 : BaseTransform<Matrix2>, IMovable, IRotatable, IScalable + { + private Vector2 _position; + private float _rotation; + private Vector2 _scale = Vector2.One; + + public Transform2() + : this(Vector2.Zero, 0, Vector2.One) + { + } + + public Transform2(float x, float y, float rotation = 0, float scaleX = 1, float scaleY = 1) + : this(new Vector2(x, y), rotation, new Vector2(scaleX, scaleY)) + { + } + + public Transform2(Vector2? position = null, float rotation = 0, Vector2? scale = null) + { + Position = position ?? Vector2.Zero; + Rotation = rotation; + Scale = scale ?? Vector2.One; + } + + /// <summary> + /// Gets the world position. + /// </summary> + /// <value> + /// The world position. + /// </value> + public Vector2 WorldPosition => WorldMatrix.Translation; + + /// <summary> + /// Gets the world scale. + /// </summary> + /// <value> + /// The world scale. + /// </value> + public Vector2 WorldScale => WorldMatrix.Scale; + + /// <summary> + /// Gets the world rotation angle in radians. + /// </summary> + /// <value> + /// The world rotation angle in radians. + /// </value> + public float WorldRotation => WorldMatrix.Rotation; + + /// <summary> + /// Gets or sets the local position. + /// </summary> + /// <value> + /// The local position. + /// </value> + public Vector2 Position + { + get { return _position; } + set + { + _position = value; + LocalMatrixBecameDirty(); + WorldMatrixBecameDirty(); + } + } + + /// <summary> + /// Gets or sets the local rotation angle in radians. + /// </summary> + /// <value> + /// The local rotation angle in radians. + /// </value> + public float Rotation + { + get { return _rotation; } + set + { + _rotation = value; + LocalMatrixBecameDirty(); + WorldMatrixBecameDirty(); + } + } + + /// <summary> + /// Gets or sets the local scale. + /// </summary> + /// <value> + /// The local scale. + /// </value> + public Vector2 Scale + { + get { return _scale; } + set + { + _scale = value; + LocalMatrixBecameDirty(); + WorldMatrixBecameDirty(); + } + } + + protected internal override void RecalculateWorldMatrix(ref Matrix2 localMatrix, out Matrix2 matrix) + { + if (Parent != null) + { + Parent.GetWorldMatrix(out matrix); + Matrix2.Multiply(ref localMatrix, ref matrix, out matrix); + } + else + { + matrix = localMatrix; + } + } + + protected internal override void RecalculateLocalMatrix(out Matrix2 matrix) + { + matrix = Matrix2.CreateScale(_scale) * + Matrix2.CreateRotationZ(_rotation) * + Matrix2.CreateTranslation(_position); + } + + public override string ToString() + { + return $"Position: {Position}, Rotation: {Rotation}, Scale: {Scale}"; + } + } + + + /// <summary> + /// Represents the position, rotation, and scale of a three-dimensional game object. + /// </summary> + /// <seealso cref="BaseTransform{Matrix}" /> + /// <remarks> + /// <para> + /// Every game object has a transform which is used to store and manipulate the position, rotation and scale + /// of the object. Every transform can have a parent, which allows to apply position, rotation and scale to game + /// objects hierarchically. + /// </para> + /// </remarks> + public class Transform3 : BaseTransform<Matrix> { + private Vector3 _position; + private Quaternion _rotation; + private Vector3 _scale = Vector3.One; + + public Transform3(Vector3? position = null, Quaternion? rotation = null, Vector3? scale = null) { + Position = position ?? Vector3.Zero; + Rotation = rotation ?? Quaternion.Identity; + Scale = scale ?? Vector3.One; + } + + /// <summary> + /// Gets the world position. + /// </summary> + /// <value> + /// The world position. + /// </value> + public Vector3 WorldPosition => WorldMatrix.Translation; + + /// <summary> + /// Gets the world scale. + /// </summary> + /// <value> + /// The world scale. + /// </value> + public Vector3 WorldScale { + get { + Vector3 scale = Vector3.Zero; + Quaternion rotation = Quaternion.Identity; + Vector3 translation = Vector3.Zero; + WorldMatrix.Decompose(out scale, out rotation, out translation); + return scale; + } + } + + + /// <summary> + /// Gets the world rotation quaternion in radians. + /// </summary> + /// <value> + /// The world rotation quaternion in radians. + /// </value> + public Quaternion WorldRotation { + get { + Vector3 scale = Vector3.Zero; + Quaternion rotation = Quaternion.Identity; + Vector3 translation = Vector3.Zero; + WorldMatrix.Decompose(out scale, out rotation, out translation); + return rotation; + } + } + + /// <summary> + /// Gets or sets the local position. + /// </summary> + /// <value> + /// The local position. + /// </value> + public Vector3 Position { + get { return _position; } + set { + _position = value; + LocalMatrixBecameDirty(); + WorldMatrixBecameDirty(); + } + } + + /// <summary> + /// Gets or sets the local rotation quaternion in radians. + /// </summary> + /// <value> + /// The local rotation quaternion in radians. + /// </value> + public Quaternion Rotation { + get { return _rotation; } + set { + _rotation = value; + LocalMatrixBecameDirty(); + WorldMatrixBecameDirty(); + } + } + + /// <summary> + /// Gets or sets the local scale. + /// </summary> + /// <value> + /// The local scale. + /// </value> + public Vector3 Scale { + get { return _scale; } + set { + _scale = value; + LocalMatrixBecameDirty(); + WorldMatrixBecameDirty(); + } + } + + protected internal override void RecalculateWorldMatrix(ref Matrix localMatrix, out Matrix matrix) { + if (Parent != null) { + Parent.GetWorldMatrix(out matrix); + Matrix.Multiply(ref localMatrix, ref matrix, out matrix); + } + else { + matrix = localMatrix; + } + } + + protected internal override void RecalculateLocalMatrix(out Matrix matrix) { + matrix = Matrix.CreateScale(_scale) * + Matrix.CreateFromQuaternion(_rotation) * + Matrix.CreateTranslation(_position); + } + + public override string ToString() { + return $"Position: {Position}, Rotation: {Rotation}, Scale: {Scale}"; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/VectorDraw/PrimitiveBatch.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/VectorDraw/PrimitiveBatch.cs new file mode 100644 index 0000000..621a627 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/VectorDraw/PrimitiveBatch.cs @@ -0,0 +1,183 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Collections.Generic; +using System.Text; + +namespace MonoGame.Extended.VectorDraw +{ + //Taken from Velcro Physics + //Used with permission: https://github.com/craftworkgames/MonoGame.Extended/issues/574 + + public class PrimitiveBatch : IDisposable + { + private const int DefaultBufferSize = 500; + + // a basic effect, which contains the shaders that we will use to draw our + // primitives. + private readonly BasicEffect _basicEffect; + + // the device that we will issue draw calls to. + private readonly GraphicsDevice _device; + + private readonly VertexPositionColor[] _lineVertices; + private readonly VertexPositionColor[] _triangleVertices; + + // hasBegun is flipped to true once Begin is called, and is used to make + // sure users don't call End before Begin is called. + private bool _hasBegun; + + private bool _isDisposed; + private int _lineVertsCount; + private int _triangleVertsCount; + + public PrimitiveBatch(GraphicsDevice graphicsDevice, int bufferSize = DefaultBufferSize) + { + if (graphicsDevice == null) + throw new ArgumentNullException(nameof(graphicsDevice)); + + _device = graphicsDevice; + + _triangleVertices = new VertexPositionColor[bufferSize - bufferSize % 3]; + _lineVertices = new VertexPositionColor[bufferSize - bufferSize % 2]; + + // set up a new basic effect, and enable vertex colors. + _basicEffect = new BasicEffect(graphicsDevice); + _basicEffect.VertexColorEnabled = true; + } + + #region IDisposable Members + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + + public void SetProjection(ref Matrix projection) + { + _basicEffect.Projection = projection; + } + + protected virtual void Dispose(bool disposing) + { + if (disposing && !_isDisposed) + { + if (_basicEffect != null) + _basicEffect.Dispose(); + + _isDisposed = true; + } + } + + /// <summary> + /// Begin is called to tell the PrimitiveBatch what kind of primitives will be + /// drawn, and to prepare the graphics card to render those primitives. + /// </summary> + /// <param name="projection">The projection.</param> + /// <param name="view">The view.</param> + public void Begin(ref Matrix projection, ref Matrix view) + { + if (_hasBegun) + throw new InvalidOperationException("End must be called before Begin can be called again."); + + //tell our basic effect to begin. + _basicEffect.Projection = projection; + _basicEffect.View = view; + _basicEffect.CurrentTechnique.Passes[0].Apply(); + + // flip the error checking boolean. It's now ok to call AddVertex, Flush, + // and End. + _hasBegun = true; + } + + public bool IsReady() + { + return _hasBegun; + } + + public void AddVertex(Vector2 vertex, Color color, PrimitiveType primitiveType) + { + if (!_hasBegun) + throw new InvalidOperationException("Begin must be called before AddVertex can be called."); + + if (primitiveType == PrimitiveType.LineStrip || primitiveType == PrimitiveType.TriangleStrip) + throw new NotSupportedException("The specified primitiveType is not supported by PrimitiveBatch."); + + if (primitiveType == PrimitiveType.TriangleList) + { + if (_triangleVertsCount >= _triangleVertices.Length) + FlushTriangles(); + + _triangleVertices[_triangleVertsCount].Position = new Vector3(vertex, -0.1f); + _triangleVertices[_triangleVertsCount].Color = color; + _triangleVertsCount++; + } + + if (primitiveType == PrimitiveType.LineList) + { + if (_lineVertsCount >= _lineVertices.Length) + FlushLines(); + + _lineVertices[_lineVertsCount].Position = new Vector3(vertex, 0f); + _lineVertices[_lineVertsCount].Color = color; + _lineVertsCount++; + } + } + + /// <summary> + /// End is called once all the primitives have been drawn using AddVertex. + /// it will call Flush to actually submit the draw call to the graphics card, and + /// then tell the basic effect to end. + /// </summary> + public void End() + { + if (!_hasBegun) + { + throw new InvalidOperationException("Begin must be called before End can be called."); + } + + // Draw whatever the user wanted us to draw + FlushTriangles(); + FlushLines(); + + _hasBegun = false; + } + + private void FlushTriangles() + { + if (!_hasBegun) + { + throw new InvalidOperationException("Begin must be called before Flush can be called."); + } + if (_triangleVertsCount >= 3) + { + int primitiveCount = _triangleVertsCount / 3; + + // submit the draw call to the graphics card + _device.SamplerStates[0] = SamplerState.AnisotropicClamp; + _device.DrawUserPrimitives(PrimitiveType.TriangleList, _triangleVertices, 0, primitiveCount); + _triangleVertsCount -= primitiveCount * 3; + } + } + + private void FlushLines() + { + if (!_hasBegun) + { + throw new InvalidOperationException("Begin must be called before Flush can be called."); + } + if (_lineVertsCount >= 2) + { + int primitiveCount = _lineVertsCount / 2; + + // submit the draw call to the graphics card + _device.SamplerStates[0] = SamplerState.AnisotropicClamp; + _device.DrawUserPrimitives(PrimitiveType.LineList, _lineVertices, 0, primitiveCount); + _lineVertsCount -= primitiveCount * 2; + } + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/VectorDraw/PrimitiveDrawing.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/VectorDraw/PrimitiveDrawing.cs new file mode 100644 index 0000000..fcf5791 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/VectorDraw/PrimitiveDrawing.cs @@ -0,0 +1,255 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using MonoGame.Extended.Triangulation; +using System; +using System.Collections.Generic; +using System.Text; + +namespace MonoGame.Extended.VectorDraw +{ + public class PrimitiveDrawing + { + //Drawing + private PrimitiveBatch _primitiveBatch; + + private SpriteBatch _batch; + private SpriteFont _font; + private GraphicsDevice _device; + private readonly Vector2[] _tempVertices = new Vector2[1000]; //TODO: something else... + + //private Matrix _localProjection; + //private Matrix _localView; + + //TODO: do we need to split this based on platform? +#if XBOX || WINDOWS_PHONE + public const int CircleSegments = 16; +#else + public const int CircleSegments = 32; +#endif + + public PrimitiveDrawing(PrimitiveBatch primitiveBatch) + { + _primitiveBatch = primitiveBatch; + } + + public void DrawPoint(Vector2 center, Color color) + { + if (!_primitiveBatch.IsReady()) + throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything."); + + //Add two points or the PrimitiveBatch acts up + _primitiveBatch.AddVertex(center, color, PrimitiveType.LineList); + _primitiveBatch.AddVertex(center, color, PrimitiveType.LineList); + } + + public void DrawRectangle(Vector2 location, float width, float height, Color color) + { + if (!_primitiveBatch.IsReady()) + throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything."); + + Vector2[] rectVerts = new Vector2[4] + { + new Vector2(0, 0), + new Vector2(width, 0), + new Vector2(width, height), + new Vector2(0, height) + }; + + //Location is offset here + DrawPolygon(location, rectVerts, color); + } + + public void DrawSolidRectangle(Vector2 location, float width, float height, Color color) + { + if (!_primitiveBatch.IsReady()) + throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything."); + + Vector2[] rectVerts = new Vector2[4] + { + new Vector2(0, 0), + new Vector2(width, 0), + new Vector2(width, height), + new Vector2(0, height) + }; + + DrawSolidPolygon(location, rectVerts, color); + } + + public void DrawCircle(Vector2 center, float radius, Color color) + { + if (!_primitiveBatch.IsReady()) + throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything."); + + const double increment = Math.PI * 2.0 / CircleSegments; + double theta = 0.0; + + for (int i = 0; i < CircleSegments; i++) + { + Vector2 v1 = center + radius * new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta)); + Vector2 v2 = center + radius * new Vector2((float)Math.Cos(theta + increment), (float)Math.Sin(theta + increment)); + + _primitiveBatch.AddVertex(v1, color, PrimitiveType.LineList); + _primitiveBatch.AddVertex(v2, color, PrimitiveType.LineList); + + theta += increment; + } + } + + public void DrawSolidCircle(Vector2 center, float radius, Color color) + { + if (!_primitiveBatch.IsReady()) + throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything."); + + const double increment = Math.PI * 2.0 / CircleSegments; + double theta = 0.0; + + Color colorFill = color * 0.5f; + + Vector2 v0 = center + radius * new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta)); + theta += increment; + + for (int i = 1; i < CircleSegments - 1; i++) + { + Vector2 v1 = center + radius * new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta)); + Vector2 v2 = center + radius * new Vector2((float)Math.Cos(theta + increment), (float)Math.Sin(theta + increment)); + + _primitiveBatch.AddVertex(v0, colorFill, PrimitiveType.TriangleList); + _primitiveBatch.AddVertex(v1, colorFill, PrimitiveType.TriangleList); + _primitiveBatch.AddVertex(v2, colorFill, PrimitiveType.TriangleList); + + theta += increment; + } + + DrawCircle(center, radius, color); + } + public void DrawSolidCircle(Vector2 center, float radius, Color color, Color fillcolor) + { + if (!_primitiveBatch.IsReady()) + throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything."); + + const double increment = Math.PI * 2.0 / CircleSegments; + double theta = 0.0; + + Vector2 v0 = center + radius * new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta)); + theta += increment; + + for (int i = 1; i < CircleSegments - 1; i++) + { + Vector2 v1 = center + radius * new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta)); + Vector2 v2 = center + radius * new Vector2((float)Math.Cos(theta + increment), (float)Math.Sin(theta + increment)); + + _primitiveBatch.AddVertex(v0, fillcolor, PrimitiveType.TriangleList); + _primitiveBatch.AddVertex(v1, fillcolor, PrimitiveType.TriangleList); + _primitiveBatch.AddVertex(v2, fillcolor, PrimitiveType.TriangleList); + + theta += increment; + } + + DrawCircle(center, radius, color); + } + + public void DrawSegment(Vector2 start, Vector2 end, Color color) + { + if (!_primitiveBatch.IsReady()) + throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything."); + + _primitiveBatch.AddVertex(start, color, PrimitiveType.LineList); + _primitiveBatch.AddVertex(end, color, PrimitiveType.LineList); + } + + public void DrawPolygon(Vector2 position, Vector2[] vertices, Color color, bool closed = true) + { + if (!_primitiveBatch.IsReady()) + throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything."); + + int count = vertices.Length; + + for (int i = 0; i < count - 1; i++) + { + //translate the vertices according to the position passed + _primitiveBatch.AddVertex(new Vector2(vertices[i].X + position.X, vertices[i].Y + position.Y), color, PrimitiveType.LineList); + _primitiveBatch.AddVertex(new Vector2(vertices[i + 1].X + position.X, vertices[i + 1].Y + position.Y), color, PrimitiveType.LineList); + } + if (closed) + { + //TODO: verify closed is working as expected + _primitiveBatch.AddVertex(new Vector2(vertices[count - 1].X + position.X, vertices[count - 1].Y + position.Y), color, PrimitiveType.LineList); + _primitiveBatch.AddVertex(new Vector2(vertices[0].X + position.X, vertices[0].Y + position.Y), color, PrimitiveType.LineList); + } + } + + public void DrawSolidPolygon(Vector2 position, Vector2[] vertices, Color color, bool outline = true) + { + if (!_primitiveBatch.IsReady()) + throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything."); + + int count = vertices.Length; + + if (count == 2) + { + DrawPolygon(position, vertices, color); + return; + } + + Color colorFill = color * (outline ? 0.5f : 1.0f); + + Vector2[] outVertices; + int[] outIndices; + Triangulator.Triangulate(vertices, WindingOrder.CounterClockwise, out outVertices, out outIndices); + + for(int i = 0; i < outIndices.Length - 2; i += 3) + { + _primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i]].X + position.X, outVertices[outIndices[i]].Y + position.Y), colorFill, PrimitiveType.TriangleList); + _primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i + 1]].X + position.X, outVertices[outIndices[i + 1]].Y + position.Y), colorFill, PrimitiveType.TriangleList); + _primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i + 2]].X + position.X, outVertices[outIndices[i + 2]].Y + position.Y), colorFill, PrimitiveType.TriangleList); + } + + if (outline) + DrawPolygon(position, vertices, color); + } + + public void DrawEllipse(Vector2 center, Vector2 radius, int sides, Color color) + { + if (!_primitiveBatch.IsReady()) + throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything."); + + DrawPolygon(center, CreateEllipse(radius.X, radius.Y, sides), color); + } + + public void DrawSolidEllipse(Vector2 center, Vector2 radius, int sides, Color color, bool outline = true) + { + if (!_primitiveBatch.IsReady()) + throw new InvalidOperationException("BeginCustomDraw must be called before drawing anything."); + + Color colorFill = color * (outline ? 0.5f : 1.0f); + + Vector2[] vertices = CreateEllipse(radius.X, radius.Y, sides); + + Vector2[] outVertices; + int[] outIndices; + Triangulator.Triangulate(vertices, WindingOrder.CounterClockwise, out outVertices, out outIndices); + + for (int i = 0; i < outIndices.Length - 2; i += 3) + { + _primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i]].X + center.X, outVertices[outIndices[i]].Y + center.Y), colorFill, PrimitiveType.TriangleList); + _primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i + 1]].X + center.X, outVertices[outIndices[i + 1]].Y + center.Y), colorFill, PrimitiveType.TriangleList); + _primitiveBatch.AddVertex(new Vector2(outVertices[outIndices[i + 2]].X + center.X, outVertices[outIndices[i + 2]].Y + center.Y), colorFill, PrimitiveType.TriangleList); + } + } + + private static Vector2[] CreateEllipse(float rx, float ry, int sides) + { + var vertices = new Vector2[sides]; + + var t = 0.0; + var dt = 2.0 * Math.PI / sides; + for (var i = 0; i < sides; i++, t += dt) + { + var x = (float)(rx * Math.Cos(t)); + var y = (float)(ry * Math.Sin(t)); + vertices[i] = new Vector2(x, y); + } + return vertices; + } + } +} diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/BoxingViewportAdapter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/BoxingViewportAdapter.cs new file mode 100644 index 0000000..ea80a5d --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/BoxingViewportAdapter.cs @@ -0,0 +1,96 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +// ReSharper disable once CheckNamespace + +namespace MonoGame.Extended.ViewportAdapters +{ + public enum BoxingMode + { + None, + Letterbox, + Pillarbox + } + + public class BoxingViewportAdapter : ScalingViewportAdapter + { + private readonly GameWindow _window; + private readonly GraphicsDevice _graphicsDevice; + + /// <summary> + /// Initializes a new instance of the <see cref="BoxingViewportAdapter" />. + /// </summary> + public BoxingViewportAdapter(GameWindow window, GraphicsDevice graphicsDevice, int virtualWidth, int virtualHeight, int horizontalBleed = 0, int verticalBleed = 0) + : base(graphicsDevice, virtualWidth, virtualHeight) + { + _window = window; + _graphicsDevice = graphicsDevice; + window.ClientSizeChanged += OnClientSizeChanged; + HorizontalBleed = horizontalBleed; + VerticalBleed = verticalBleed; + } + + public override void Dispose() + { + _window.ClientSizeChanged -= OnClientSizeChanged; + base.Dispose(); + } + + /// <summary> + /// Size of horizontal bleed areas (from left and right edges) which can be safely cut off + /// </summary> + public int HorizontalBleed { get; } + + /// <summary> + /// Size of vertical bleed areas (from top and bottom edges) which can be safely cut off + /// </summary> + public int VerticalBleed { get; } + + public BoxingMode BoxingMode { get; private set; } + + private void OnClientSizeChanged(object sender, EventArgs eventArgs) + { + var clientBounds = _window.ClientBounds; + + var worldScaleX = (float)clientBounds.Width / VirtualWidth; + var worldScaleY = (float)clientBounds.Height / VirtualHeight; + + var safeScaleX = (float)clientBounds.Width / (VirtualWidth - HorizontalBleed); + var safeScaleY = (float)clientBounds.Height / (VirtualHeight - VerticalBleed); + + var worldScale = MathHelper.Max(worldScaleX, worldScaleY); + var safeScale = MathHelper.Min(safeScaleX, safeScaleY); + var scale = MathHelper.Min(worldScale, safeScale); + + var width = (int)(scale * VirtualWidth + 0.5f); + var height = (int)(scale * VirtualHeight + 0.5f); + + if (height >= clientBounds.Height && width < clientBounds.Width) + BoxingMode = BoxingMode.Pillarbox; + else + { + if (width >= clientBounds.Height && height <= clientBounds.Height) + BoxingMode = BoxingMode.Letterbox; + else + BoxingMode = BoxingMode.None; + } + + var x = clientBounds.Width / 2 - width / 2; + var y = clientBounds.Height / 2 - height / 2; + GraphicsDevice.Viewport = new Viewport(x, y, width, height); + } + + public override void Reset() + { + base.Reset(); + OnClientSizeChanged(this, EventArgs.Empty); + } + + public override Point PointToScreen(int x, int y) + { + var viewport = GraphicsDevice.Viewport; + return base.PointToScreen(x - viewport.X, y - viewport.Y); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/DefaultViewportAdapter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/DefaultViewportAdapter.cs new file mode 100644 index 0000000..9026ce4 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/DefaultViewportAdapter.cs @@ -0,0 +1,28 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +// ReSharper disable once CheckNamespace + +namespace MonoGame.Extended.ViewportAdapters +{ + public class DefaultViewportAdapter : ViewportAdapter + { + private readonly GraphicsDevice _graphicsDevice; + + public DefaultViewportAdapter(GraphicsDevice graphicsDevice) + : base(graphicsDevice) + { + _graphicsDevice = graphicsDevice; + } + + public override int VirtualWidth => _graphicsDevice.Viewport.Width; + public override int VirtualHeight => _graphicsDevice.Viewport.Height; + public override int ViewportWidth => _graphicsDevice.Viewport.Width; + public override int ViewportHeight => _graphicsDevice.Viewport.Height; + + public override Matrix GetScaleMatrix() + { + return Matrix.Identity; + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/ScalingViewportAdapter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/ScalingViewportAdapter.cs new file mode 100644 index 0000000..674a62e --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/ScalingViewportAdapter.cs @@ -0,0 +1,27 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.ViewportAdapters +{ + public class ScalingViewportAdapter : ViewportAdapter + { + public ScalingViewportAdapter(GraphicsDevice graphicsDevice, int virtualWidth, int virtualHeight) + : base(graphicsDevice) + { + VirtualWidth = virtualWidth; + VirtualHeight = virtualHeight; + } + + public override int VirtualWidth { get; } + public override int VirtualHeight { get; } + public override int ViewportWidth => GraphicsDevice.Viewport.Width; + public override int ViewportHeight => GraphicsDevice.Viewport.Height; + + public override Matrix GetScaleMatrix() + { + var scaleX = (float) ViewportWidth/VirtualWidth; + var scaleY = (float) ViewportHeight/VirtualHeight; + return Matrix.CreateScale(scaleX, scaleY, 1.0f); + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/ViewportAdapter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/ViewportAdapter.cs new file mode 100644 index 0000000..eb0491e --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/ViewportAdapter.cs @@ -0,0 +1,46 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.ViewportAdapters +{ + public abstract class ViewportAdapter : IDisposable + { + protected ViewportAdapter(GraphicsDevice graphicsDevice) + { + GraphicsDevice = graphicsDevice; + } + + public virtual void Dispose() + { + } + + public GraphicsDevice GraphicsDevice { get; } + public Viewport Viewport => GraphicsDevice.Viewport; + + public abstract int VirtualWidth { get; } + public abstract int VirtualHeight { get; } + public abstract int ViewportWidth { get; } + public abstract int ViewportHeight { get; } + + public Rectangle BoundingRectangle => new Rectangle(0, 0, VirtualWidth, VirtualHeight); + public Point Center => BoundingRectangle.Center; + public abstract Matrix GetScaleMatrix(); + + public Point PointToScreen(Point point) + { + return PointToScreen(point.X, point.Y); + } + + public virtual Point PointToScreen(int x, int y) + { + var scaleMatrix = GetScaleMatrix(); + var invertedMatrix = Matrix.Invert(scaleMatrix); + return Vector2.Transform(new Vector2(x, y), invertedMatrix).ToPoint(); + } + + public virtual void Reset() + { + } + } +}
\ No newline at end of file diff --git a/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/WindowViewportAdapter.cs b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/WindowViewportAdapter.cs new file mode 100644 index 0000000..b995887 --- /dev/null +++ b/Plugins/MonoGame.Extended/source/MonoGame.Extended/ViewportAdapters/WindowViewportAdapter.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGame.Extended.ViewportAdapters +{ + public class WindowViewportAdapter : ViewportAdapter + { + protected readonly GameWindow Window; + + public WindowViewportAdapter(GameWindow window, GraphicsDevice graphicsDevice) + : base(graphicsDevice) + { + Window = window; + window.ClientSizeChanged += OnClientSizeChanged; + } + + public override int ViewportWidth => Window.ClientBounds.Width; + public override int ViewportHeight => Window.ClientBounds.Height; + public override int VirtualWidth => Window.ClientBounds.Width; + public override int VirtualHeight => Window.ClientBounds.Height; + + public override Matrix GetScaleMatrix() + { + return Matrix.Identity; + } + + private void OnClientSizeChanged(object sender, EventArgs eventArgs) + { + var x = Window.ClientBounds.Width; + var y = Window.ClientBounds.Height; + + GraphicsDevice.Viewport = new Viewport(0, 0, x, y); + } + } +}
\ No newline at end of file |