diff --git a/Robust.Client/Physics/PhysicsSystem.cs b/Robust.Client/Physics/PhysicsSystem.cs index 47b868d2ea..62c745a522 100644 --- a/Robust.Client/Physics/PhysicsSystem.cs +++ b/Robust.Client/Physics/PhysicsSystem.cs @@ -1,10 +1,6 @@ using System; -using System.Collections.Generic; -using System.Linq; using JetBrains.Annotations; -using Robust.Shared.GameObjects.Components; using Robust.Shared.GameObjects.Systems; -using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.Timing; using Robust.Shared.IoC; @@ -14,7 +10,6 @@ namespace Robust.Client.Physics public class PhysicsSystem : SharedPhysicsSystem { [Dependency] private readonly IGameTiming _gameTiming = default!; - [Dependency] private readonly IComponentManager _componentManager = default!; private TimeSpan _lastRem; @@ -22,7 +17,7 @@ namespace Robust.Client.Physics { _lastRem = _gameTiming.CurTime; - SimulateWorld(frameTime, EntityManager.ComponentManager.EntityQuery().ToList(), !_gameTiming.InSimulation || !_gameTiming.IsFirstTimePredicted); + SimulateWorld(frameTime, !_gameTiming.InSimulation || !_gameTiming.IsFirstTimePredicted); } public override void FrameUpdate(float frameTime) @@ -34,14 +29,7 @@ namespace Robust.Client.Physics var diff = _gameTiming.TickRemainder - _lastRem; _lastRem = _gameTiming.TickRemainder; - SimulateWorld((float) diff.TotalSeconds, ActuallyRelevant(), true); - } - - private List ActuallyRelevant() - { - var relevant = _componentManager.EntityQuery().Where(p => p.Predict) - .ToList(); - return relevant; + SimulateWorld((float) diff.TotalSeconds, true); } } } diff --git a/Robust.Server/GameObjects/EntitySystems/PhysicsSystem.cs b/Robust.Server/GameObjects/EntitySystems/PhysicsSystem.cs index d89bdf79c3..34a81069ca 100644 --- a/Robust.Server/GameObjects/EntitySystems/PhysicsSystem.cs +++ b/Robust.Server/GameObjects/EntitySystems/PhysicsSystem.cs @@ -11,11 +11,7 @@ namespace Robust.Server.GameObjects.EntitySystems /// public override void Update(float frameTime) { - var collidableComponents = EntityManager.ComponentManager - .EntityQuery() - .ToList(); - - SimulateWorld(frameTime, collidableComponents, false); + SimulateWorld(frameTime, false); } } } diff --git a/Robust.Shared/GameObjects/ComponentMessages/Messages.cs b/Robust.Shared/GameObjects/ComponentMessages/Messages.cs index 588379dda3..f8e7636571 100644 --- a/Robust.Shared/GameObjects/ComponentMessages/Messages.cs +++ b/Robust.Shared/GameObjects/ComponentMessages/Messages.cs @@ -14,11 +14,6 @@ namespace Robust.Shared.GameObjects } } - public class EntityMovementMessage : ComponentMessage - { - public EntityMovementMessage() { } - } - /// /// The entity transform parent has been changed. /// diff --git a/Robust.Shared/GameObjects/Components/Collidable/CollidableComponent.Collision.cs b/Robust.Shared/GameObjects/Components/Collidable/CollidableComponent.Collision.cs index bafcbd644a..8ee0d81a54 100644 --- a/Robust.Shared/GameObjects/Components/Collidable/CollidableComponent.Collision.cs +++ b/Robust.Shared/GameObjects/Components/Collidable/CollidableComponent.Collision.cs @@ -69,8 +69,22 @@ namespace Robust.Shared.GameObjects.Components public BodyType BodyType { get; set; } = BodyType.Static; /// - public int SleepAccumulator { get; set; } + public int SleepAccumulator + { + get => _sleepAccumulator; + set + { + if (_sleepAccumulator == value) + return; + + _sleepAccumulator = value; + Awake = _physicsManager.SleepTimeThreshold > SleepAccumulator; + } + } + private int _sleepAccumulator; + + // TODO: When SleepTimeThreshold updates we need to update Awake public int SleepThreshold { get => _physicsManager.SleepTimeThreshold; @@ -79,7 +93,20 @@ namespace Robust.Shared.GameObjects.Components /// [ViewVariables] - public bool Awake => _physicsManager.SleepTimeThreshold > SleepAccumulator; + public bool Awake + { + get => _awake; + private set + { + if (_awake == value) + return; + + _awake = value; + Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new CollidableUpdateMessage(this)); + } + } + + private bool _awake = true; /// public void WakeBody() @@ -280,6 +307,12 @@ namespace Robust.Shared.GameObjects.Components new CollisionChangeMessage(Owner.Uid, _canCollide)); } + public override void OnAdd() + { + base.OnAdd(); + Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new CollidableUpdateMessage(this)); + } + public override void OnRemove() { base.OnRemove(); @@ -292,6 +325,7 @@ namespace Robust.Shared.GameObjects.Components // Should we not call this if !_canCollide? PathfindingSystem doesn't care at least. Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new CollisionChangeMessage(Owner.Uid, false)); + Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new CollidableUpdateMessage(this)); } private void ShapeAdded(IPhysShape shape) @@ -473,4 +507,17 @@ namespace Robust.Shared.GameObjects.Components OnGround, InAir } + + /// + /// Sent whenever a collidable component is changed. + /// + public sealed class CollidableUpdateMessage : EntitySystemMessage + { + public ICollidableComponent Component { get; } + + public CollidableUpdateMessage(ICollidableComponent component) + { + Component = component; + } + } } diff --git a/Robust.Shared/GameObjects/Components/Collidable/CollidableComponent.Physics.cs b/Robust.Shared/GameObjects/Components/Collidable/CollidableComponent.Physics.cs index 18b138a83c..a545d1c47c 100644 --- a/Robust.Shared/GameObjects/Components/Collidable/CollidableComponent.Physics.cs +++ b/Robust.Shared/GameObjects/Components/Collidable/CollidableComponent.Physics.cs @@ -332,7 +332,20 @@ namespace Robust.Shared.GameObjects.Components public event Action? AnchoredChanged; [ViewVariables(VVAccess.ReadWrite)] - public bool Predict { get; set; } + public bool Predict + { + get => _predict; + set + { + if (_predict == value) + return; + + _predict = value; + Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new CollidableUpdateMessage(this)); + } + } + + private bool _predict; Dictionary ICollidableComponent.Controllers { diff --git a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs index 3069e474fa..5392fe39d4 100644 --- a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs +++ b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs @@ -76,6 +76,13 @@ namespace Robust.Shared.GameObjects.Components.Transform } } + /// + public bool DeferUpdates { get; set; } + + // Deferred fields + private Angle? _oldLocalRotation; + private EntityCoordinates? _oldCoords; + /// [ViewVariables(VVAccess.ReadWrite)] [Animatable] @@ -92,12 +99,20 @@ namespace Robust.Shared.GameObjects.Components.Transform // Set _nextRotation to null to break any active lerps if this is a client side prediction. _nextRotation = null; SetRotation(value); - Owner.EntityManager.EventBus.RaiseEvent( - EventSource.Local, new RotateEvent(Owner, oldRotation, _localRotation)); - RebuildMatrices(); Dirty(); - UpdateEntityTree(); - UpdatePhysicsTree(); + + if (!DeferUpdates) + { + RebuildMatrices(); + UpdateEntityTree(); + UpdatePhysicsTree(); + Owner.EntityManager.EventBus.RaiseEvent( + EventSource.Local, new RotateEvent(Owner, oldRotation, _localRotation)); + } + else + { + _oldLocalRotation ??= oldRotation; + } } } @@ -239,18 +254,25 @@ namespace Robust.Shared.GameObjects.Components.Transform } _localPosition = value.Position; - - //TODO: This is a hack, look into WHY we can't call GridPosition before the comp is Running - if (Running) - { - RebuildMatrices(); - Owner.EntityManager.EventBus.RaiseEvent( - EventSource.Local, new MoveEvent(Owner, oldPosition, Coordinates)); - } - Dirty(); - UpdateEntityTree(); - UpdatePhysicsTree(); + + if (!DeferUpdates) + { + //TODO: This is a hack, look into WHY we can't call GridPosition before the comp is Running + if (Running) + { + RebuildMatrices(); + Owner.EntityManager.EventBus.RaiseEvent( + EventSource.Local, new MoveEvent(Owner, oldPosition, Coordinates)); + } + + UpdateEntityTree(); + UpdatePhysicsTree(); + } + else + { + _oldCoords ??= oldPosition; + } } } @@ -268,12 +290,42 @@ namespace Robust.Shared.GameObjects.Components.Transform _nextPosition = null; var oldGridPos = Coordinates; SetPosition(value); - RebuildMatrices(); Dirty(); - UpdateEntityTree(); - UpdatePhysicsTree(); + + if (!DeferUpdates) + { + RebuildMatrices(); + UpdateEntityTree(); + UpdatePhysicsTree(); + Owner.EntityManager.EventBus.RaiseEvent( + EventSource.Local, new MoveEvent(Owner, oldGridPos, Coordinates)); + } + else + { + _oldCoords ??= oldGridPos; + } + } + } + + /// + public void RunCollidableDeferred() + { + RebuildMatrices(); + UpdateEntityTree(); + UpdatePhysicsTree(); + + if (_oldCoords != null) + { Owner.EntityManager.EventBus.RaiseEvent( - EventSource.Local, new MoveEvent(Owner, oldGridPos, Coordinates)); + EventSource.Local, new MoveEvent(Owner, _oldCoords.Value, Coordinates)); + _oldCoords = null; + } + + if (_oldLocalRotation != null) + { + Owner.EntityManager.EventBus.RaiseEvent( + EventSource.Local, new RotateEvent(Owner, _oldLocalRotation.Value, _localRotation)); + _oldLocalRotation = null; } } diff --git a/Robust.Shared/GameObjects/Systems/SharedPhysicsSystem.cs b/Robust.Shared/GameObjects/Systems/SharedPhysicsSystem.cs index af9896a8b0..d1f03ab8a9 100644 --- a/Robust.Shared/GameObjects/Systems/SharedPhysicsSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedPhysicsSystem.cs @@ -6,7 +6,6 @@ using Robust.Shared.Interfaces.Map; using Robust.Shared.Interfaces.Physics; using Robust.Shared.Interfaces.Random; using Robust.Shared.Interfaces.Timing; -using Robust.Shared.Log; using Robust.Shared.Maths; using Robust.Shared.Physics; using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute; @@ -24,28 +23,95 @@ namespace Robust.Shared.GameObjects.Systems private const float Epsilon = 1.0e-6f; private readonly List _collisionCache = new List(); + + /// + /// Collidable objects that are awake and usable for world simulation. + /// private readonly HashSet _awakeBodies = new HashSet(); /// - /// Simulates the physical world for a given amount of time. + /// Collidable objects that are awake and predicted and usable for world simulation. + /// + private readonly HashSet _predictedAwakeBodies = new HashSet(); + + /// + /// VirtualControllers on applicable ICollidableComponents + /// + private Dictionary> _controllers = + new Dictionary>(); + + // We'll defer changes to ICollidable until each step is done. + private readonly List _queuedDeletions = new List(); + private readonly List _queuedUpdates = new List(); + + /// + /// Updates to EntityTree etc. that are deferred until the end of physics. + /// + private readonly HashSet _deferredUpdates = new HashSet(); + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(HandleCollidableUpdateMessage); + } + + private void HandleCollidableUpdateMessage(CollidableUpdateMessage message) + { + if (message.Component.Deleted || !message.Component.Awake) + { + _queuedDeletions.Add(message.Component); + } + else + { + _queuedUpdates.Add(message.Component); + } + } + + /// + /// Process the changes to cached ICollidables + /// + private void ProcessQueue() + { + // At this stage only the dynamictree cares about asleep bodies + // Implicitly awake bodies so don't need to check .Awake again + // Controllers should wake their body up (inside) + foreach (var collidable in _queuedUpdates) + { + if (collidable.Predict) + _predictedAwakeBodies.Add(collidable); + + _awakeBodies.Add(collidable); + + if (collidable.Controllers.Count > 0 && !_controllers.ContainsKey(collidable)) + _controllers.Add(collidable, collidable.Controllers.Values); + + } + + _queuedUpdates.Clear(); + + foreach (var collidable in _queuedDeletions) + { + _awakeBodies.Remove(collidable); + _predictedAwakeBodies.Remove(collidable); + _controllers.Remove(collidable); + } + + _queuedDeletions.Clear(); + } + + /// + /// Simulates the physical world for a given amount of time. /// /// Delta Time in seconds of how long to simulate the world. - /// List of all possible physics bodes /// Should only predicted entities be considered in this simulation step? - protected void SimulateWorld(float deltaTime, List physicsComponents, bool prediction) + protected void SimulateWorld(float deltaTime, bool prediction) { - _awakeBodies.Clear(); - - foreach (var body in physicsComponents) + var simulatedBodies = prediction ? _predictedAwakeBodies : _awakeBodies; + + ProcessQueue(); + + foreach (var body in simulatedBodies) { - if(prediction && !body.Predict) - continue; - - if(!body.Awake) - continue; - - _awakeBodies.Add(body); - // running prediction updates will not cause a body to go to sleep. if(!prediction) body.SleepAccumulator++; @@ -80,27 +146,27 @@ namespace Robust.Shared.GameObjects.Systems } // Calculate collisions and store them in the cache - ProcessCollisions(physicsComponents); - + ProcessCollisions(_awakeBodies); + // Remove all entities that were deleted during collision handling - physicsComponents.RemoveAll(p => p.Deleted); + ProcessQueue(); // Process frictional forces - foreach (var physics in physicsComponents) + foreach (var physics in _awakeBodies) { ProcessFriction(physics, deltaTime); } - - foreach (var physics in physicsComponents) + + foreach (var (_, controllers) in _controllers) { - foreach (var controller in physics.Controllers.Values) + foreach (var controller in controllers) { controller.UpdateAfterProcessing(); } } - + // Remove all entities that were deleted due to the controller - physicsComponents.RemoveAll(p => p.Deleted); + ProcessQueue(); const int solveIterationsAt60 = 4; @@ -116,14 +182,10 @@ namespace Robust.Shared.GameObjects.Systems for (var i = 0; i < divisions; i++) { - foreach (var physics in physicsComponents) + foreach (var collidable in simulatedBodies) { - // TODO: Remove this once we are not sending *every* body to the solver - if(prediction && !physics.Predict) - continue; - - if(physics.Awake && physics.CanMove()) - UpdatePosition(physics, deltaTime / divisions); + if(collidable.CanMove()) + UpdatePosition(collidable, deltaTime / divisions); } for (var j = 0; j < divisions; ++j) @@ -134,18 +196,25 @@ namespace Robust.Shared.GameObjects.Systems } } } + + // As we also defer the updates for the _collisionCache we need to update all entities + foreach (var collidable in _deferredUpdates) + { + var transform = collidable.Owner.Transform; + transform.DeferUpdates = false; + transform.RunCollidableDeferred(); + } + + _deferredUpdates.Clear(); } // Runs collision behavior and updates cache - private void ProcessCollisions(IEnumerable bodies) + private void ProcessCollisions(IEnumerable awakeBodies) { _collisionCache.Clear(); var combinations = new HashSet<(EntityUid, EntityUid)>(); - foreach (var aCollidable in bodies) + foreach (var aCollidable in awakeBodies) { - if(!aCollidable.Awake) - continue; - foreach (var b in _physicsManager.GetCollidingEntities(aCollidable, Vector2.Zero)) { var aUid = aCollidable.Entity.Uid; @@ -281,29 +350,28 @@ namespace Robust.Shared.GameObjects.Systems body.LinearVelocity += frictionVelocityChange; } - private static void UpdatePosition(IPhysBody body, float frameTime) + private void UpdatePosition(ICollidableComponent collidable, float frameTime) { - var ent = body.Entity; + var ent = collidable.Entity; - if (!body.CanMove() || (body.LinearVelocity.LengthSquared < Epsilon && MathF.Abs(body.AngularVelocity) < Epsilon)) + if (!collidable.CanMove() || (collidable.LinearVelocity.LengthSquared < Epsilon && MathF.Abs(collidable.AngularVelocity) < Epsilon)) return; - if (body.LinearVelocity != Vector2.Zero) + if (collidable.LinearVelocity != Vector2.Zero) { - var entityMoveMessage = new EntityMovementMessage(); - ent.SendMessage(ent.Transform, entityMoveMessage); - if (ContainerHelpers.IsInContainer(ent)) { var relayEntityMoveMessage = new RelayMovementEntityMessage(ent); ent.Transform.Parent!.Owner.SendMessage(ent.Transform, relayEntityMoveMessage); // This prevents redundant messages from being sent if solveIterations > 1 and also simulates the entity "colliding" against the locker door when it opens. - body.LinearVelocity = Vector2.Zero; + collidable.LinearVelocity = Vector2.Zero; } } - - body.WorldRotation += body.AngularVelocity * frameTime; - body.WorldPosition += body.LinearVelocity * frameTime; + + collidable.Owner.Transform.DeferUpdates = true; + _deferredUpdates.Add(collidable); + collidable.WorldRotation += collidable.AngularVelocity * frameTime; + collidable.WorldPosition += collidable.LinearVelocity * frameTime; } // Based off of Randy Gaul's ImpulseEngine code @@ -327,9 +395,18 @@ namespace Robust.Shared.GameObjects.Systems done = false; var correction = collision.Normal * Math.Abs(penetration) * percent; if (collision.A.CanMove()) + { + collision.A.Owner.Transform.DeferUpdates = true; + _deferredUpdates.Add(collision.A); collision.A.Owner.Transform.WorldPosition -= correction; + } + if (collision.B.CanMove()) + { + collision.B.Owner.Transform.DeferUpdates = true; + _deferredUpdates.Add(collision.B); collision.B.Owner.Transform.WorldPosition += correction; + } } return done; diff --git a/Robust.Shared/Interfaces/GameObjects/Components/ITransformComponent.cs b/Robust.Shared/Interfaces/GameObjects/Components/ITransformComponent.cs index 0707daee7d..338973ef25 100644 --- a/Robust.Shared/Interfaces/GameObjects/Components/ITransformComponent.cs +++ b/Robust.Shared/Interfaces/GameObjects/Components/ITransformComponent.cs @@ -96,10 +96,25 @@ namespace Robust.Shared.Interfaces.GameObjects.Components /// Returns the index of the grid which this object is on /// GridId GridID { get; } + + /// + /// Whether external system updates should run or not (e.g. EntityTree, Matrices, PhysicsTree). + /// These should be manually run later. + /// + bool DeferUpdates { get; set; } void AttachToGridOrMap(); void AttachParent(ITransformComponent parent); void AttachParent(IEntity parent); + + /// + /// Run the updates marked as deferred (UpdateEntityTree and movement events). + /// Don't call this unless you REALLY need to. + /// + /// + /// Physics optimisation so these aren't spammed during physics updates. + /// + void RunCollidableDeferred(); IEnumerable Children { get; } int ChildCount { get; }