/* * Farseer Physics Engine: * Copyright (c) 2012 Ian Qvist * * Original source Box2D: * Copyright (c) 2006-2011 Erin Catto http://www.box2d.org * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors be held liable for any damages * arising from the use of this software. * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely, subject to the following restrictions: * 1. The origin of this software must not be misrepresented; you must not * claim that you wrote the original software. If you use this software * in a product, an acknowledgment in the product documentation would be * appreciated but is not required. * 2. Altered source versions must be plainly marked as such, and must not be * misrepresented as being the original software. * 3. This notice may not be removed or altered from any source distribution. */ using System; using System.Collections.Generic; using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Physics.Dynamics.Contacts; using Robust.Shared.Physics.Dynamics.Joints; using Robust.Shared.Utility; using PhysicsComponent = Robust.Shared.GameObjects.PhysicsComponent; namespace Robust.Shared.Physics.Dynamics { public sealed class PhysicsMap { [Dependency] private readonly IConfigurationManager _configManager = default!; [Dependency] private readonly IEntityManager _entityManager = default!; // TODO: Pausing but need to merge in shared IPauseManager // AKA world. private SharedPhysicsSystem _physicsSystem = default!; internal ContactManager ContactManager = new(); private bool _autoClearForces; /// /// Change the global gravity vector. /// public Vector2 Gravity { get => _gravity; set { if (_gravity.EqualsApprox(value)) return; // Force every body awake just in case. foreach (var body in Bodies) { if (body.BodyType != BodyType.Dynamic) continue; body.Awake = true; } _gravity = value; } } private Vector2 _gravity; // TODO: Given physics bodies are a common thing to be listening for on moveevents it's probably beneficial to have 2 versions; one that includes the entity // and one that includes the body private List _deferredUpdates = new(); /// /// All bodies present on this map. /// public HashSet Bodies = new(); /// /// All awake bodies on this map. /// public HashSet AwakeBodies = new(); /// /// Temporary body storage during solving. /// private List _awakeBodyList = new(); /// /// Get all the joints on this map /// public List Joints { get; private set; } = new(); /// /// Temporarily store island-bodies for easier iteration. /// private HashSet _islandSet = new(); private HashSet _queuedJointAdd = new(); private HashSet _queuedJointRemove = new(); private HashSet _queuedWake = new(); private HashSet _queuedSleep = new(); private Queue _queuedCollisionMessages = new(); /// /// We'll re-use contacts where possible to save on allocations. /// internal Queue ContactPool = new(128); private PhysicsIsland _island = default!; /// /// To build islands we do a depth-first search of all colliding bodies and group them together. /// This stack is used to store bodies that are colliding. /// private PhysicsComponent[] _stack = new PhysicsComponent[64]; /// /// Store last tick's invDT /// private float _invDt0; public MapId MapId { get; } public PhysicsMap(MapId mapId) { MapId = mapId; _physicsSystem = EntitySystem.Get(); } public void Initialize() { IoCManager.InjectDependencies(this); ContactManager.Initialize(); ContactManager.MapId = MapId; _island = new PhysicsIsland(); _island.Initialize(); _autoClearForces = _configManager.GetCVar(CVars.AutoClearForces); _configManager.OnValueChanged(CVars.AutoClearForces, value => _autoClearForces = value); } #region AddRemove public void AddAwakeBody(PhysicsComponent body) { _queuedWake.Add(body); } public void RemoveBody(PhysicsComponent body) { Bodies.Remove(body); AwakeBodies.Remove(body); body.DestroyContacts(); } public void RemoveSleepBody(PhysicsComponent body) { _queuedSleep.Add(body); } public void AddJoint(Joint joint) { // TODO: Need static helper class to easily create Joints _queuedJointAdd.Add(joint); } public void RemoveJoint(Joint joint) { _queuedJointRemove.Add(joint); } #endregion #region Queue private void ProcessChanges() { ProcessBodyChanges(); ProcessWakeQueue(); ProcessSleepQueue(); ProcessAddedJoints(); ProcessRemovedJoints(); } private void ProcessBodyChanges() { while (_queuedCollisionMessages.Count > 0) { var message = _queuedCollisionMessages.Dequeue(); if (!message.Body.Deleted && message.Body.CanCollide) { AddBody(message.Body); } else { RemoveBody(message.Body); } } } public void AddBody(PhysicsComponent body) { if (Bodies.Contains(body)) return; // TODO: Kinda dodgy with this and wake shit. // Look at my note under ProcessWakeQueue if (body.Awake && body.BodyType != BodyType.Static) { _queuedWake.Remove(body); AwakeBodies.Add(body); } Bodies.Add(body); body.PhysicsMap = this; } private void ProcessWakeQueue() { foreach (var body in _queuedWake) { // Sloth note: So FPE doesn't seem to handle static bodies being woken gracefully as they never sleep // (No static body's an island so can't increase their min sleep time). // AFAIK not adding it to woken bodies shouldn't matter for anything tm... if (!Bodies.Contains(body) || !body.Awake || body.BodyType == BodyType.Static) continue; AwakeBodies.Add(body); } _queuedWake.Clear(); } private void ProcessSleepQueue() { foreach (var body in _queuedSleep) { if (body.Awake) continue; AwakeBodies.Remove(body); } _queuedSleep.Clear(); } private void ProcessAddedJoints() { foreach (var joint in _queuedJointAdd) { // TODO: Optimise dafuk out of this. if (Joints.Contains(joint)) continue; // Just end me, I fucken hate how garbage the physics compstate is. // because EACH body will have a joint update we needs to check if. PhysicsComponent? bodyA; PhysicsComponent? bodyB; // ReSharper disable once ConditionIsAlwaysTrueOrFalse if (joint.BodyA == null || joint.BodyB == null) { if (!_entityManager.TryGetEntity(joint.BodyAUid, out var bodyAEntity) || !_entityManager.TryGetEntity(joint.BodyBUid, out var bodyBEntity)) { continue; } if (!bodyAEntity.TryGetComponent(out bodyA) || !bodyBEntity.TryGetComponent(out bodyB)) { continue; } // TODO: Need to mark all this shit as nullable coming in from the state probably. joint.BodyA = bodyAEntity.GetComponent(); joint.BodyB = bodyBEntity.GetComponent(); } else { bodyA = joint.BodyA; bodyB = joint.BodyB; } // BodyA and BodyB should share joints so we can just check if BodyA already has this joint. for (var je = bodyA.JointEdges; je != null; je = je.Next) { if (je.Joint.Equals(joint)) continue; } // Connect to the world list. Joints.Add(joint); // Connect to the bodies' doubly linked lists. joint.EdgeA.Joint = joint; joint.EdgeA.Other = bodyB; joint.EdgeA.Prev = null; joint.EdgeA.Next = bodyA.JointEdges; if (bodyA.JointEdges != null) bodyA.JointEdges.Prev = joint.EdgeA; bodyA.JointEdges = joint.EdgeA; joint.EdgeB.Joint = joint; joint.EdgeB.Other = bodyA; joint.EdgeB.Prev = null; joint.EdgeB.Next = bodyB.JointEdges; if (bodyB.JointEdges != null) bodyB.JointEdges.Prev = joint.EdgeB; bodyB.JointEdges = joint.EdgeB; joint.BodyAUid = bodyA.Owner.Uid; joint.BodyBUid = bodyB.Owner.Uid; // If the joint prevents collisions, then flag any contacts for filtering. if (!joint.CollideConnected) { ContactEdge? edge = bodyB.ContactEdges; while (edge != null) { if (edge.Other == bodyA) { // Flag the contact for filtering at the next time step (where either // body is awake). edge.Contact!.FilterFlag = true; } edge = edge.Next; } } bodyA.Dirty(); bodyB.Dirty(); // Note: creating a joint doesn't wake the bodies. } _queuedJointAdd.Clear(); } private void ProcessRemovedJoints() { foreach (var joint in _queuedJointRemove) { bool collideConnected = joint.CollideConnected; // Remove from the world list. Joints.Remove(joint); // Disconnect from island graph. PhysicsComponent bodyA = joint.BodyA; PhysicsComponent bodyB = joint.BodyB; // Wake up connected bodies. bodyA.Awake = true; bodyB.Awake = true; // Remove from body 1. if (joint.EdgeA.Prev != null) { joint.EdgeA.Prev.Next = joint.EdgeA.Next; } if (joint.EdgeA.Next != null) { joint.EdgeA.Next.Prev = joint.EdgeA.Prev; } if (joint.EdgeA == bodyA.JointEdges) { bodyA.JointEdges = joint.EdgeA.Next; } joint.EdgeA.Prev = null; joint.EdgeA.Next = null; // Remove from body 2 if (joint.EdgeB.Prev != null) { joint.EdgeB.Prev.Next = joint.EdgeB.Next; } if (joint.EdgeB.Next != null) { joint.EdgeB.Next.Prev = joint.EdgeB.Prev; } if (joint.EdgeB == bodyB.JointEdges) { bodyB.JointEdges = joint.EdgeB.Next; } joint.EdgeB.Prev = null; joint.EdgeB.Next = null; // If the joint prevents collisions, then flag any contacts for filtering. if (!collideConnected) { ContactEdge? edge = bodyB.ContactEdges; while (edge != null) { if (edge.Other == bodyA) { // Flag the contact for filtering at the next time step (where either // body is awake). edge.Contact!.FilterFlag = true; } edge = edge.Next; } } } _queuedJointRemove.Clear(); } #endregion /// /// Where the magic happens. /// /// /// public void Step(float frameTime, bool prediction) { // The original doesn't call ProcessChanges quite so much but stuff like collision behaviors // can edit things during the solver so we'll just handle it as it comes up. ProcessChanges(); // Box2D does this at the end of a step and also here when there's a fixture update. // Given external stuff can move bodies we'll just do this here. // Unfortunately this NEEDS to be predicted to make pushing remotely fucking good. ContactManager.FindNewContacts(MapId); var invDt = frameTime > 0.0f ? 1.0f / frameTime : 0.0f; var dtRatio = _invDt0 * frameTime; foreach (var controller in _physicsSystem.Controllers) { controller.UpdateBeforeMapSolve(prediction, this, frameTime); } ContactManager.Collide(); // TODO: May move this as a PostSolve once we have broadphase collisions where contacts can be generated // even though the bodies may not technically be colliding if (!prediction) ContactManager.PreSolve(); // Remove all deleted entities etc. ProcessChanges(); // Integrate velocities, solve velocity constraints, and do integration. Solve(frameTime, dtRatio, invDt, prediction); // TODO: SolveTOI foreach (var controller in _physicsSystem.Controllers) { controller.UpdateAfterMapSolve(prediction, this, frameTime); } // Box2d recommends clearing (if you are) during fixed updates rather than variable if you are using it if (!prediction && _autoClearForces) ClearForces(); _invDt0 = invDt; } /// /// Go through all of the deferred MoveEvents and then run them /// public void ProcessQueue() { foreach (var transform in _deferredUpdates) { transform.RunDeferred(); } _deferredUpdates.Clear(); } private void Solve(float frameTime, float dtRatio, float invDt, bool prediction) { // Re-size island for worst-case -> TODO Probably smaller than this given everything's awake at the start? _island.Reset(Bodies.Count, ContactManager.ActiveContacts.Count, Joints.Count); DebugTools.Assert(_islandSet.Count == 0); foreach (var contact in ContactManager.ActiveContacts) { contact.IslandFlag = false; } foreach (var joint in Joints) { joint.IslandFlag = false; } // Build and simulated islands from awake bodies. // Ideally you don't need a stack size for all bodies but we'll optimise it later. var stackSize = Bodies.Count; if (stackSize > _stack.Length) { Array.Resize(ref _stack, Math.Max(_stack.Length * 2, stackSize)); } _awakeBodyList.AddRange(AwakeBodies); // Build the relevant islands / graphs for all bodies. foreach (var seed in _awakeBodyList) { // I tried not running prediction for non-contacted entities but unfortunately it looked like shit // when contact broke so if you want to try that then GOOD LUCK. // prediction && !seed.Predict || // AHHH need a way to ignore paused for mapping (seed.Paused && !seed.Owner.TryGetComponent(out IMoverComponent)) || if ((seed.Paused && !seed.IgnorePaused) || seed.Island || !seed.CanCollide || seed.BodyType == BodyType.Static) continue; // Start of a new island _island.Clear(); var stackCount = 0; _stack[stackCount++] = seed; _islandSet.Add(seed); seed.Island = true; while (stackCount > 0) { var body = _stack[--stackCount]; _island.Add(body); _islandSet.Add(body); body.Awake = true; // Static bodies don't propagate islands if (body.BodyType == BodyType.Static) continue; for (var contactEdge = body.ContactEdges; contactEdge != null; contactEdge = contactEdge.Next) { var contact = contactEdge.Contact!; // Has this contact already been added to an island? if (contact.IslandFlag) continue; // Is this contact solid and touching? if (!contact.Enabled || !contact.IsTouching) continue; // Skip sensors. if (contact.FixtureA?.Hard != true || contact.FixtureB?.Hard != true) continue; _island.Add(contact); contact.IslandFlag = true; var other = contactEdge.Other!; // Was the other body already added to this island? if (other.Island) continue; DebugTools.Assert(stackCount < stackSize); _stack[stackCount++] = other; if (!_islandSet.Contains(body)) _islandSet.Add(body); other.Island = true; } for (JointEdge? je = body.JointEdges; je != null; je = je.Next) { if (je.Joint.IslandFlag) { continue; } PhysicsComponent other = je.Other; // Don't simulate joints connected to inactive bodies. if (!other.CanCollide) continue; _island.Add(je.Joint); je.Joint.IslandFlag = true; if (other.Island) continue; DebugTools.Assert(stackCount < stackSize); _stack[stackCount++] = other; if (!_islandSet.Contains(body)) _islandSet.Add(body); other.Island = true; } } _island.Solve(Gravity, frameTime, dtRatio, invDt, prediction, _deferredUpdates); // Post-solve cleanup for island for (var i = 0; i < _island.BodyCount; i++) { var body = _island.Bodies[i]; // Static bodies can participate in other islands if (body.BodyType == BodyType.Static) { body.Island = false; } } } foreach (var body in _islandSet) { if (!body.Island || body.Deleted) { continue; } body.Island = false; DebugTools.Assert(body.BodyType != BodyType.Static); // So Box2D would update broadphase here buutttt we'll just wait until MoveEvent queue is used. } _islandSet.Clear(); _awakeBodyList.Clear(); ContactManager.PostSolve(); } private void ClearForces() { foreach (var body in AwakeBodies) { body.Force = Vector2.Zero; body.Torque = 0.0f; } } } }