Merge pull request #795 from toore/bug-update-bounds-collision-manager

OnCollision are called even after objects involved are moved and not intersecting
This commit is contained in:
Max Kopjev
2023-08-30 14:34:25 +03:00
committed by GitHub
4 changed files with 266 additions and 151 deletions
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.Xna.Framework;
namespace MonoGame.Extended.Collisions
@@ -12,8 +11,7 @@ namespace MonoGame.Extended.Collisions
/// </summary>
public class CollisionComponent : SimpleGameComponent
{
private readonly Dictionary<ICollisionActor, QuadtreeData> _targetDataDictionary =
new Dictionary<ICollisionActor, QuadtreeData>();
private readonly Dictionary<ICollisionActor, QuadtreeData> _targetDataDictionary = new();
private readonly Quadtree _collisionTree;
@@ -47,14 +45,13 @@ namespace MonoGame.Extended.Collisions
// Generate list of collision Infos
foreach (var other in collisions)
{
var collisionInfo = new CollisionEventArgs()
{
Other = other.Target,
PenetrationVector = CalculatePenetrationVector(value.Bounds, other.Bounds)
};
var collisionInfo = new CollisionEventArgs
{
Other = other.Target,
PenetrationVector = CalculatePenetrationVector(value.Bounds, other.Bounds)
};
target.OnCollision(collisionInfo);
value.Bounds = value.Target.Bounds;
}
_collisionTree.Insert(value);
}
@@ -230,4 +227,4 @@ namespace MonoGame.Extended.Collisions
#endregion
}
}
}
@@ -4,18 +4,32 @@ using System.Collections.Generic;
namespace MonoGame.Extended.Collisions
{
/// <summary>
/// Class for doing collision handling with a quad tree.
/// 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.
/// Creates a quad tree with the given bounds.
/// </summary>
/// <param name="bounds">The bounds of the new quad tree.</param>
public Quadtree(RectangleF bounds)
@@ -24,23 +38,31 @@ namespace MonoGame.Extended.Collisions
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.
/// 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.
/// 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.
/// Counts the number of unique targets in the current Quadtree.
/// </summary>
/// <returns>Returns the targets of objects found.</returns>
public int NumTargets()
@@ -74,21 +96,21 @@ namespace MonoGame.Extended.Collisions
}
}
}
for (var i = 0; i < dirtyItems.Count; i++)
foreach (var quadtreeData in dirtyItems)
{
dirtyItems[i].MarkClean();
quadtreeData.MarkClean();
}
return objectCount;
}
/// <summary>
/// Inserts the data into the tree.
/// Inserts the data into the tree.
/// </summary>
/// <param name="data">Data being inserted.</param>
public void Insert(QuadtreeData data)
{
var actorBounds = data.Target.Bounds;
var actorBounds = data.Bounds;
// Object doesn't fit into this node.
if (!NodeBounds.Intersects(actorBounds))
{
@@ -114,7 +136,7 @@ namespace MonoGame.Extended.Collisions
}
/// <summary>
/// Removes data from the Quadtree
/// Removes data from the Quadtree
/// </summary>
/// <param name="data">The data to be removed.</param>
public void Remove(QuadtreeData data)
@@ -126,58 +148,60 @@ namespace MonoGame.Extended.Collisions
}
else
{
throw new InvalidOperationException($"Cannot remove from a non leaf {nameof(Quadtree)}");
throw new InvalidOperationException($"Cannot remove from a non leaf {nameof(Quadtree)}");
}
}
/// <summary>
/// Removes unneccesary leaf nodes and simplifies the quad tree.
/// Removes unnecessary leaf nodes and simplifies the quad tree.
/// </summary>
public void Shake()
{
if (!IsLeaf)
if (IsLeaf)
{
List<QuadtreeData> dirtyItems = new List<QuadtreeData>();
return;
}
var numObjects = NumTargets();
if (numObjects == 0)
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)
{
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)
{
var processing = process.Dequeue();
if (!processing.IsLeaf)
foreach (var subTree in processing.Children)
{
foreach (var subTree in processing.Children)
{
process.Enqueue(subTree);
}
process.Enqueue(subTree);
}
else
}
else
{
foreach (var data in processing.Contents)
{
foreach (var data in processing.Contents)
if (data.Dirty == false)
{
if (data.Dirty == false)
{
AddToLeaf(data);
data.MarkDirty();
dirtyItems.Add(data);
}
AddToLeaf(data);
data.MarkDirty();
dirtyItems.Add(data);
}
}
}
Children.Clear();
}
Children.Clear();
}
for (var i = 0; i < dirtyItems.Count; i++)
{
dirtyItems[i].MarkClean();
}
foreach (var quadtreeData in dirtyItems)
{
quadtreeData.MarkClean();
}
}
@@ -188,7 +212,7 @@ namespace MonoGame.Extended.Collisions
}
/// <summary>
/// Splits a quadtree into quadrants.
/// Splits a quadtree into quadrants.
/// </summary>
public void Split()
{
@@ -233,7 +257,7 @@ namespace MonoGame.Extended.Collisions
}
/// <summary>
/// Queries the quadtree for targets that intersect with the given area.
/// 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>
@@ -241,16 +265,16 @@ namespace MonoGame.Extended.Collisions
{
var recursiveResult = new List<QuadtreeData>();
QueryWithoutReset(area, recursiveResult);
for (var i = 0; i < recursiveResult.Count; i++)
foreach (var quadtreeData in recursiveResult)
{
recursiveResult[i].MarkClean();
quadtreeData.MarkClean();
}
return recursiveResult;
}
private void QueryWithoutReset(IShapeF area, List<QuadtreeData> recursiveResult)
{
if (!NodeBounds.Intersects(area))
if (!NodeBounds.Intersects(area))
return;
if (IsLeaf)
@@ -273,4 +297,4 @@ namespace MonoGame.Extended.Collisions
}
}
}
}
}
@@ -1,65 +1,86 @@
using System.Collections.Generic;
using System.Linq;
namespace MonoGame.Extended.Collisions
namespace MonoGame.Extended.Collisions;
/// <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>
/// Data structure for the quad tree.
/// Holds the entity and collision data for it.
/// Initialize a new instance of QuadTreeData.
/// </summary>
public class QuadtreeData
/// <param name="target"></param>
public QuadtreeData(ICollisionActor target)
{
public QuadtreeData(ICollisionActor target)
{
Target = target;
Bounds = target.Bounds;
}
public void RemoveParent(Quadtree parent)
{
Parents.Remove(parent);
}
public void AddParent(Quadtree parent)
{
Parents.Add(parent);
}
public void RemoveFromAllParents()
{
foreach (Quadtree parent in Parents.ToList())
{
parent.Remove(this);
}
Parents.Clear();
}
public HashSet<Quadtree> Parents = new HashSet<Quadtree>();
/// <summary>
/// Gets or sets the Target for collision.
/// </summary>
public ICollisionActor Target { get; set; }
/// <summary>
/// Gets or sets whether Target has had its collision handled this
/// iteration.
/// </summary>
public bool Dirty { get; private set; }
public void MarkDirty()
{
Dirty = true;
}
public void MarkClean()
{
Dirty = false;
}
/// <summary>
/// Gets or sets the bounding box for collision detection.
/// </summary>
public IShapeF Bounds { get; set; }
_target = target;
}
}
/// <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);
}
/// <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 IShapeF Bounds => _target.Bounds;
/// <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;
}
}
@@ -13,16 +13,14 @@ namespace MonoGame.Extended.Collisions.Tests
/// </remarks>
public class CollisionComponentTests
{
private CollisionComponent collisionComponent;
private GameTime _gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromMilliseconds(16));
private readonly CollisionComponent _collisionComponent;
private readonly GameTime _gameTime = new GameTime(TimeSpan.Zero, TimeSpan.FromMilliseconds(16));
public CollisionComponentTests()
{
collisionComponent = new CollisionComponent(new RectangleF(Point2.Zero, new Point2(10, 10)));
_collisionComponent = new CollisionComponent(new RectangleF(Point2.Zero, new Point2(10, 10)));
}
#region Circle Circle
[Fact]
@@ -46,9 +44,9 @@ namespace MonoGame.Extended.Collisions.Tests
};
Assert.True(shape1.Intersects(shape2));
collisionComponent.Insert(actor1);
collisionComponent.Insert(actor2);
collisionComponent.Update(_gameTime);
_collisionComponent.Insert(actor1);
_collisionComponent.Insert(actor2);
_collisionComponent.Update(_gameTime);
Assert.True(Math.Abs(actor1.Position.Y - -4f) < float.Epsilon);
}
@@ -73,9 +71,9 @@ namespace MonoGame.Extended.Collisions.Tests
};
Assert.True(shape1.Intersects(shape2));
collisionComponent.Insert(actor1);
collisionComponent.Insert(actor2);
collisionComponent.Update(_gameTime);
_collisionComponent.Insert(actor1);
_collisionComponent.Insert(actor2);
_collisionComponent.Update(_gameTime);
// Actor should have moved up because the distance is shorter.
Assert.True(actor1.Position.Y > actor2.Position.Y);
// The circle centers should be about 4 units away after moving
@@ -103,9 +101,9 @@ namespace MonoGame.Extended.Collisions.Tests
};
Assert.True(shape1.Intersects(shape2));
collisionComponent.Insert(actor1);
collisionComponent.Insert(actor2);
collisionComponent.Update(_gameTime);
_collisionComponent.Insert(actor1);
_collisionComponent.Insert(actor2);
_collisionComponent.Update(_gameTime);
// Actor should have moved up because the distance is shorter.
Assert.True(actor1.Position.Y > actor2.Position.Y);
// The circle centers should be about 4 units away after moving
@@ -134,9 +132,9 @@ namespace MonoGame.Extended.Collisions.Tests
};
Assert.True(shape1.Intersects(shape2));
collisionComponent.Insert(actor1);
collisionComponent.Insert(actor2);
collisionComponent.Update(_gameTime);
_collisionComponent.Insert(actor1);
_collisionComponent.Insert(actor2);
_collisionComponent.Update(_gameTime);
// Actor should have moved up because the distance is shorter.
Assert.True(actor1.Position.Y > actor2.Position.Y);
// The circle centers should be about 4 units away after moving
@@ -169,9 +167,9 @@ namespace MonoGame.Extended.Collisions.Tests
};
Assert.True(shape1.Intersects(shape2));
collisionComponent.Insert(actor1);
collisionComponent.Insert(actor2);
collisionComponent.Update(_gameTime);
_collisionComponent.Insert(actor1);
_collisionComponent.Insert(actor2);
_collisionComponent.Update(_gameTime);
Assert.True(Math.Abs(actor1.Position.X - 0.0f) < float.Epsilon);
Assert.True(Math.Abs(actor1.Position.Y - 3.0f) < float.Epsilon);
}
@@ -198,9 +196,9 @@ namespace MonoGame.Extended.Collisions.Tests
};
Assert.True(shape1.Intersects(shape2));
collisionComponent.Insert(actor1);
collisionComponent.Insert(actor2);
collisionComponent.Update(_gameTime);
_collisionComponent.Insert(actor1);
_collisionComponent.Insert(actor2);
_collisionComponent.Update(_gameTime);
Assert.True(Math.Abs(actor1.Position.X - 0.0f) < float.Epsilon);
Assert.True(Math.Abs(actor1.Position.Y - -2.0f) < float.Epsilon);
}
@@ -227,9 +225,9 @@ namespace MonoGame.Extended.Collisions.Tests
};
Assert.True(shape1.Intersects(shape2));
collisionComponent.Insert(actor1);
collisionComponent.Insert(actor2);
collisionComponent.Update(_gameTime);
_collisionComponent.Insert(actor1);
_collisionComponent.Insert(actor2);
_collisionComponent.Update(_gameTime);
Assert.True(Math.Abs(actor1.Position.X - 2.0f) < float.Epsilon);
Assert.True(Math.Abs(actor1.Position.Y - 3.0f) < float.Epsilon);
}
@@ -260,9 +258,9 @@ namespace MonoGame.Extended.Collisions.Tests
};
Assert.True(shape1.Intersects(shape2));
collisionComponent.Insert(actor1);
collisionComponent.Insert(actor2);
collisionComponent.Update(_gameTime);
_collisionComponent.Insert(actor1);
_collisionComponent.Insert(actor2);
_collisionComponent.Update(_gameTime);
Assert.True(Math.Abs(actor1.Position.X - 0.0f) < float.Epsilon);
Assert.True(Math.Abs(actor1.Position.Y - 1.0f) < float.Epsilon);
}
@@ -289,15 +287,90 @@ namespace MonoGame.Extended.Collisions.Tests
};
Assert.True(shape1.Intersects(shape2));
collisionComponent.Insert(actor1);
collisionComponent.Insert(actor2);
collisionComponent.Update(_gameTime);
_collisionComponent.Insert(actor1);
_collisionComponent.Insert(actor2);
_collisionComponent.Update(_gameTime);
Assert.True(Math.Abs(actor1.Position.X - 4.0f) < float.Epsilon);
Assert.True(Math.Abs(actor1.Position.Y - 3.0f) < float.Epsilon);
}
#endregion
public class Behaviours : CollisionComponentTests
{
[Fact]
public void Actors_is_colliding()
{
var staticBounds = new RectangleF(new Point2(0, 0), new Size2(1, 1));
var anotherStaticBounds = new RectangleF(new Point2(0, 0), new Size2(1, 1));
var staticActor = new CollisionIndicatingActor(staticBounds);
var anotherStaticActor = new CollisionIndicatingActor(anotherStaticBounds);
_collisionComponent.Insert(staticActor);
_collisionComponent.Insert(anotherStaticActor);
_collisionComponent.Update(_gameTime);
Assert.True(staticActor.IsColliding);
Assert.True(anotherStaticActor.IsColliding);
}
[Fact]
public void Actors_is_not_colliding_when_dynamic_actor_is_moved_out_of_collision_bounds()
{
var staticBounds = new RectangleF(new Point2(0, 0), new Size2(1, 1));
var dynamicBounds = new RectangleF(new Point2(0, 0), new Size2(1, 1));
var staticActor = new CollisionIndicatingActor(staticBounds);
var dynamicActor = new CollisionIndicatingActor(dynamicBounds);
_collisionComponent.Insert(staticActor);
_collisionComponent.Insert(dynamicActor);
dynamicActor.MoveTo(new Point2(2, 2));
_collisionComponent.Update(_gameTime);
Assert.False(staticActor.IsColliding);
Assert.False(dynamicActor.IsColliding);
}
[Fact]
public void Actors_is_colliding_when_dynamic_actor_is_moved_into_collision_bounds()
{
var staticBounds = new RectangleF(new Point2(0, 0), new Size2(1, 1));
var dynamicBounds = new RectangleF(new Point2(2, 2), new Size2(1, 1));
var staticActor = new CollisionIndicatingActor(staticBounds);
var dynamicActor = new CollisionIndicatingActor(dynamicBounds);
_collisionComponent.Insert(staticActor);
_collisionComponent.Insert(dynamicActor);
dynamicActor.MoveTo(new Point2(0, 0));
_collisionComponent.Update(_gameTime);
Assert.True(staticActor.IsColliding);
Assert.True(dynamicActor.IsColliding);
}
private class CollisionIndicatingActor : ICollisionActor
{
private RectangleF _bounds;
public CollisionIndicatingActor(RectangleF bounds)
{
_bounds = bounds;
}
public IShapeF Bounds => _bounds;
public void OnCollision(CollisionEventArgs collisionInfo)
{
IsColliding = true;
}
public bool IsColliding { get; private set; }
public void MoveTo(Point2 position)
{
_bounds = new RectangleF(position, _bounds.Size);
}
}
}
}
}
}