using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Dynamics;
namespace Robust.Shared.GameObjects
{
/*
* Handles all of the public query methods for physics.
*/
public partial class SharedPhysicsSystem
{
///
/// Get the percentage that 2 bodies overlap. Ignores whether collision is turned on for either body.
///
///
///
/// 0 -> 1.0f based on WorldAABB overlap
public float IntersectionPercent(PhysicsComponent bodyA, PhysicsComponent bodyB)
{
// TODO: Use actual shapes and not just the AABB?
return bodyA.GetWorldAABB().IntersectPercentage(bodyB.GetWorldAABB());
}
///
/// Checks to see if the specified collision rectangle collides with any of the physBodies under management.
/// Also fires the OnCollide event of the first managed physBody to intersect with the collider.
///
/// Collision rectangle to check
/// Map to check on
///
/// true if collides, false if not
public bool TryCollideRect(Box2 collider, MapId mapId, bool approximate = true)
{
var state = (collider, mapId, found: false);
foreach (var broadphase in _broadphase.GetBroadphases(mapId, collider))
{
var gridCollider = EntityManager.GetComponent(broadphase.Owner).InvWorldMatrix.TransformBox(collider);
broadphase.Tree.QueryAabb(ref state, (ref (Box2 collider, MapId map, bool found) state, in FixtureProxy proxy) =>
{
if (proxy.Fixture.CollisionLayer == 0x0)
return true;
if (proxy.AABB.Intersects(gridCollider))
{
state.found = true;
return false;
}
return true;
}, gridCollider, approximate);
}
return state.found;
}
public IEnumerable GetCollidingEntities(PhysicsComponent body, Vector2 offset, bool approximate = true)
{
var broadphase = body.Broadphase;
if (broadphase == null || !EntityManager.TryGetComponent(body.Owner, out FixturesComponent? manager))
{
return Array.Empty();
}
var entities = new List();
var state = (body, entities);
foreach (var (_, fixture) in manager.Fixtures)
{
foreach (var proxy in fixture.Proxies)
{
broadphase.Tree.QueryAabb(ref state,
(ref (PhysicsComponent body, List entities) state,
in FixtureProxy other) =>
{
if (other.Fixture.Body.Deleted || other.Fixture.Body == body) return true;
if ((proxy.Fixture.CollisionMask & other.Fixture.CollisionLayer) == 0x0) return true;
if (!body.ShouldCollide(other.Fixture.Body)) return true;
state.entities.Add(other.Fixture.Body);
return true;
}, proxy.AABB, approximate);
}
}
return entities;
}
///
/// Get all the entities whose fixtures intersect the fixtures of the given entity. Basically a variant of
/// that allows the user to specify
/// their own collision mask.
///
public HashSet GetEntitiesIntersectingBody(
EntityUid uid,
int collisionMask,
bool approximate = true,
PhysicsComponent? body = null,
FixturesComponent? fixtureComp = null)
{
var entities = new HashSet();
if (!Resolve(uid, ref body, ref fixtureComp, false) || body.Broadphase == null)
return entities;
var state = (body, entities);
foreach (var (_, fixture) in fixtureComp.Fixtures)
{
foreach (var proxy in fixture.Proxies)
{
body.Broadphase.Tree.QueryAabb(ref state,
(ref (PhysicsComponent body, HashSet entities) state,
in FixtureProxy other) =>
{
if (other.Fixture.Body.Deleted || other.Fixture.Body == body) return true;
if ((collisionMask & other.Fixture.CollisionLayer) == 0x0) return true;
state.entities.Add(other.Fixture.Body.Owner);
return true;
}, proxy.AABB, approximate);
}
}
return entities;
}
///
/// Get all entities colliding with a certain body.
///
public IEnumerable GetCollidingEntities(MapId mapId, in Box2 worldAABB)
{
if (mapId == MapId.Nullspace) return Array.Empty();
var bodies = new HashSet();
foreach (var broadphase in _broadphase.GetBroadphases(mapId, worldAABB))
{
var gridAABB = EntityManager.GetComponent(broadphase.Owner).InvWorldMatrix.TransformBox(worldAABB);
foreach (var proxy in broadphase.Tree.QueryAabb(gridAABB, false))
{
bodies.Add(proxy.Fixture.Body);
}
}
return bodies;
}
///
/// Get all entities colliding with a certain body.
///
public IEnumerable GetCollidingEntities(MapId mapId, in Box2Rotated worldBounds)
{
if (mapId == MapId.Nullspace) return Array.Empty();
var bodies = new HashSet();
foreach (var broadphase in _broadphase.GetBroadphases(mapId, worldBounds.CalcBoundingBox()))
{
var gridAABB = EntityManager.GetComponent(broadphase.Owner).InvWorldMatrix.TransformBox(worldBounds);
foreach (var proxy in broadphase.Tree.QueryAabb(gridAABB, false))
{
bodies.Add(proxy.Fixture.Body);
}
}
return bodies;
}
public IEnumerable GetCollidingEntities(PhysicsComponent body)
{
var node = body.Contacts.First;
while (node != null)
{
var contact = node.Value;
node = node.Next;
var bodyA = contact.FixtureA!.Body;
var bodyB = contact.FixtureB!.Body;
var other = body == bodyA ? bodyB : bodyA;
yield return other;
}
}
// TODO: This will get every body but we don't need to do that
///
/// Checks whether a body is colliding
///
///
///
///
public bool IsColliding(PhysicsComponent body, Vector2 offset, bool approximate)
{
return GetCollidingEntities(body, offset, approximate).Any();
}
#region RayCast
///
/// Casts a ray in the world, returning the first entity it hits (or all entities it hits, if so specified)
///
///
/// Ray to cast in the world.
/// Maximum length of the ray in meters.
/// A predicate to check whether to ignore an entity or not. If it returns true, it will be ignored.
/// If true, will only include the first hit entity in results. Otherwise, returns all of them.
/// A result object describing the hit, if any.
// TODO: Make the parameter order here consistent with the other overload.
public IEnumerable IntersectRayWithPredicate(MapId mapId, CollisionRay ray,
float maxLength = 50F, Func? predicate = null, bool returnOnFirstHit = true)
{
// No, rider. This is better than a local function!
// ReSharper disable once ConvertToLocalFunction
var wrapper =
(EntityUid uid, Func? wrapped)
=> wrapped != null && wrapped(uid);
return IntersectRayWithPredicate(mapId, ray, predicate, wrapper, maxLength, returnOnFirstHit);
}
///
/// Casts a ray in the world, returning the first entity it hits (or all entities it hits, if so specified)
///
///
/// Ray to cast in the world.
/// Maximum length of the ray in meters.
/// A custom state to pass to the predicate.
/// A predicate to check whether to ignore an entity or not. If it returns true, it will be ignored.
/// If true, will only include the first hit entity in results. Otherwise, returns all of them.
/// You can avoid variable capture in many cases by using this method and passing a custom state to the predicate.
/// A result object describing the hit, if any.
public IEnumerable IntersectRayWithPredicate(MapId mapId, CollisionRay ray, TState state,
Func predicate, float maxLength = 50F, bool returnOnFirstHit = true)
{
List results = new();
var endPoint = ray.Position + ray.Direction.Normalized * maxLength;
var rayBox = new Box2(Vector2.ComponentMin(ray.Position, endPoint),
Vector2.ComponentMax(ray.Position, endPoint));
foreach (var broadphase in _broadphase.GetBroadphases(mapId, rayBox))
{
var (_, rot, matrix, invMatrix) = Transform(broadphase.Owner).GetWorldPositionRotationMatrixWithInv();
var position = invMatrix.Transform(ray.Position);
var gridRot = new Angle(-rot.Theta);
var direction = gridRot.RotateVec(ray.Direction);
var gridRay = new CollisionRay(position, direction, ray.CollisionMask);
broadphase.Tree.QueryRay((in FixtureProxy proxy, in Vector2 point, float distFromOrigin) =>
{
if (returnOnFirstHit && results.Count > 0)
return true;
if (distFromOrigin > maxLength)
return true;
if ((proxy.Fixture.CollisionLayer & ray.CollisionMask) == 0x0)
return true;
if (predicate.Invoke(proxy.Fixture.Body.Owner, state) == true)
return true;
// TODO: Shape raycast here
// Need to convert it back to world-space.
var result = new RayCastResults(distFromOrigin, matrix.Transform(point), proxy.Fixture.Body.Owner);
results.Add(result);
#if DEBUG
EntityManager.EventBus.QueueEvent(EventSource.Local,
new DebugDrawRayMessage(
new DebugRayData(ray, maxLength, result)));
#endif
return true;
}, gridRay);
}
#if DEBUG
if (results.Count == 0)
{
EntityManager.EventBus.QueueEvent(EventSource.Local,
new DebugDrawRayMessage(
new DebugRayData(ray, maxLength, null)));
}
#endif
results.Sort((a, b) => a.Distance.CompareTo(b.Distance));
return results;
}
///
/// Casts a ray in the world and returns the first entity it hits, or a list of all entities it hits.
///
///
/// Ray to cast in the world.
/// Maximum length of the ray in meters.
/// A single entity that can be ignored by the RayCast. Useful if the ray starts inside the body of an entity.
/// If false, will return a list of everything it hits, otherwise will just return a list of the first entity hit
/// An enumerable of either the first entity hit or everything hit
public IEnumerable IntersectRay(MapId mapId, CollisionRay ray, float maxLength = 50, EntityUid? ignoredEnt = null, bool returnOnFirstHit = true)
{
// ReSharper disable once ConvertToLocalFunction
var wrapper = (EntityUid uid, EntityUid? ignored)
=> uid == ignored;
return IntersectRayWithPredicate(mapId, ray, ignoredEnt, wrapper, maxLength, returnOnFirstHit);
}
///
/// Casts a ray in the world and returns the distance the ray traveled while colliding with entities
///
///
/// Ray to cast in the world.
/// Maximum length of the ray in meters.
/// A single entity that can be ignored by the RayCast. Useful if the ray starts inside the body of an entity.
/// The distance the ray traveled while colliding with entities
public float IntersectRayPenetration(MapId mapId, CollisionRay ray, float maxLength, EntityUid? ignoredEnt = null)
{
var penetration = 0f;
var endPoint = ray.Position + ray.Direction.Normalized * maxLength;
var rayBox = new Box2(Vector2.ComponentMin(ray.Position, endPoint),
Vector2.ComponentMax(ray.Position, endPoint));
foreach (var broadphase in _broadphase.GetBroadphases(mapId, rayBox))
{
var (_, rot, invMatrix) = Transform(broadphase.Owner).GetWorldPositionRotationInvMatrix();
var position = invMatrix.Transform(ray.Position);
var gridRot = new Angle(-rot.Theta);
var direction = gridRot.RotateVec(ray.Direction);
var gridRay = new CollisionRay(position, direction, ray.CollisionMask);
broadphase.Tree.QueryRay((in FixtureProxy proxy, in Vector2 point, float distFromOrigin) =>
{
if (distFromOrigin > maxLength || proxy.Fixture.Body.Owner == ignoredEnt) return true;
if ((proxy.Fixture.CollisionLayer & ray.CollisionMask) == 0x0)
{
return true;
}
if (new Ray(point + gridRay.Direction * proxy.AABB.Size.Length * 2, -gridRay.Direction).Intersects(
proxy.AABB, out _, out var exitPoint))
{
penetration += (point - exitPoint).Length;
}
return true;
}, gridRay);
}
#if DEBUG
if (penetration > 0f)
{
EntityManager.EventBus.QueueEvent(EventSource.Local,
new DebugDrawRayMessage(
new DebugRayData(ray, maxLength, null)));
}
#endif
return penetration;
}
#endregion
}
}