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 } }