mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-09-02 10:07:15 +02:00
* [Dependency] source generator No more reflection, no more codegen at runtime Also various changes to Roslyn helpers to make this easier to write. Requires all types with dependencies to be partial and not have readonly dependency fields. An analyzer enforces this at warning level, the previous injection strategies have remained in the code *for now* as a fallback. No fallback is available for [field: Dependency] properties, due to a Roslyn bug. Code Fixes exist. We love Roslyn * Apply dependencies generator changes to all code * Release notes * Preprocessor got hands * Handle nullable dependencies These are bad but gotta deal with it. * Apply suggestions from code review Co-authored-by: Moony <moony@hellomouse.net> * Fine, let's not use collection expressions --------- Co-authored-by: Moony <moony@hellomouse.net>
352 lines
15 KiB
C#
352 lines
15 KiB
C#
using System;
|
|
using Prometheus;
|
|
using Robust.Shared.Configuration;
|
|
using Robust.Shared.Containers;
|
|
using Robust.Shared.GameObjects;
|
|
using Robust.Shared.GameStates;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Map.Components;
|
|
using Robust.Shared.Physics.Collision;
|
|
using Robust.Shared.Physics.Components;
|
|
using Robust.Shared.Physics.Events;
|
|
using Robust.Shared.Threading;
|
|
using Robust.Shared.Timing;
|
|
using Robust.Shared.Utility;
|
|
using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
|
|
|
|
namespace Robust.Shared.Physics.Systems
|
|
{
|
|
public abstract partial class SharedPhysicsSystem : EntitySystem
|
|
{
|
|
/*
|
|
* TODO:
|
|
|
|
* TOI Solver (continuous collision detection)
|
|
* Poly cutting
|
|
*/
|
|
|
|
public static readonly Histogram TickUsageControllerBeforeSolveHistogram = Metrics.CreateHistogram("robust_entity_physics_controller_before_solve",
|
|
"Amount of time spent running a controller's UpdateBeforeSolve", new HistogramConfiguration
|
|
{
|
|
LabelNames = new[] {"controller"},
|
|
Buckets = Histogram.ExponentialBuckets(0.000_001, 1.5, 25)
|
|
});
|
|
|
|
public static readonly Histogram TickUsageControllerAfterSolveHistogram = Metrics.CreateHistogram("robust_entity_physics_controller_after_solve",
|
|
"Amount of time spent running a controller's UpdateAfterSolve", new HistogramConfiguration
|
|
{
|
|
LabelNames = new[] {"controller"},
|
|
Buckets = Histogram.ExponentialBuckets(0.000_001, 1.5, 25)
|
|
});
|
|
|
|
[Dependency] private IConfigurationManager _cfg = default!;
|
|
[Dependency] private IManifoldManager _manifoldManager = default!;
|
|
[Dependency] private IParallelManager _parallel = default!;
|
|
[Dependency] private EntityLookupSystem _lookup = default!;
|
|
[Dependency] private SharedBroadphaseSystem _broadphase = default!;
|
|
[Dependency] private SharedContainerSystem _containerSystem = default!;
|
|
[Dependency] private SharedDebugPhysicsSystem _debugPhysics = default!;
|
|
[Dependency] private SharedJointSystem _joints = default!;
|
|
[Dependency] private SharedTransformSystem _transform = default!;
|
|
[Dependency] private CollisionWakeSystem _wakeSystem = default!;
|
|
|
|
private int _substeps;
|
|
|
|
/// <summary>
|
|
/// A variation of <see cref="IGameTiming.CurTime"/> that takes into account the current physics sub-step.
|
|
/// Useful for some entities that need to interpolate their positions during sub-steps.
|
|
/// </summary>
|
|
public TimeSpan? EffectiveCurTime;
|
|
|
|
public bool MetricsEnabled { get; protected set; }
|
|
|
|
private EntityQuery<CollideOnAnchorComponent> _anchorQuery;
|
|
private EntityQuery<FixturesComponent> _fixturesQuery;
|
|
private EntityQuery<JointComponent> JointQuery;
|
|
private EntityQuery<JointRelayTargetComponent> RelayTargetQuery;
|
|
private EntityQuery<MapGridComponent> _gridQuery;
|
|
protected EntityQuery<MapComponent> MapQuery;
|
|
protected EntityQuery<PhysicsComponent> PhysicsQuery;
|
|
protected EntityQuery<TransformComponent> XformQuery;
|
|
|
|
private ComponentRegistration _physicsReg = default!;
|
|
private byte _angularVelocityIndex;
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
|
|
_physicsReg = EntityManager.ComponentFactory.GetRegistration(CompIdx.Index<PhysicsComponent>());
|
|
|
|
// TODO PHYSICS STATE
|
|
// Consider condensing the possible fields into just Linear velocity, angular velocity, and "Other"
|
|
// Or maybe even just "velocity" & "other"
|
|
// Then get-state doesn't have to iterate over a 10-element array.
|
|
// And it simplifies the DirtyField calls.
|
|
// Though I guess combining fixtures & physics will complicate it a bit more again.
|
|
|
|
// If you update this then update the delta state + GetState + HandleState!
|
|
EntityManager.ComponentFactory.RegisterNetworkedFields(_physicsReg,
|
|
nameof(PhysicsComponent.CanCollide),
|
|
nameof(PhysicsComponent.BodyStatus),
|
|
nameof(PhysicsComponent.BodyType),
|
|
nameof(PhysicsComponent.SleepingAllowed),
|
|
nameof(PhysicsComponent.FixedRotation),
|
|
nameof(PhysicsComponent.Friction),
|
|
nameof(PhysicsComponent.Force),
|
|
nameof(PhysicsComponent.Torque),
|
|
nameof(PhysicsComponent.LinearDamping),
|
|
nameof(PhysicsComponent.AngularDamping),
|
|
nameof(PhysicsComponent.AngularVelocity),
|
|
nameof(PhysicsComponent.LinearVelocity));
|
|
|
|
_angularVelocityIndex = 10;
|
|
|
|
_anchorQuery = GetEntityQuery<CollideOnAnchorComponent>();
|
|
_fixturesQuery = GetEntityQuery<FixturesComponent>();
|
|
JointQuery = GetEntityQuery<JointComponent>();
|
|
RelayTargetQuery = GetEntityQuery<JointRelayTargetComponent>();
|
|
MapQuery = GetEntityQuery<MapComponent>();
|
|
_gridQuery = GetEntityQuery<MapGridComponent>();
|
|
PhysicsQuery = GetEntityQuery<PhysicsComponent>();
|
|
XformQuery = GetEntityQuery<TransformComponent>();
|
|
|
|
SubscribeLocalEvent<GridAddEvent>(OnGridAdd);
|
|
SubscribeLocalEvent<CollisionChangeEvent>(OnCollisionChange);
|
|
SubscribeLocalEvent<PhysicsComponent, EntGotRemovedFromContainerMessage>(HandleContainerRemoved);
|
|
SubscribeLocalEvent<PhysicsComponent, ComponentInit>(OnPhysicsInit);
|
|
SubscribeLocalEvent<PhysicsComponent, ComponentShutdown>(OnPhysicsShutdown);
|
|
SubscribeLocalEvent<PhysicsComponent, ComponentGetState>(OnPhysicsGetState);
|
|
SubscribeLocalEvent<PhysicsComponent, ComponentHandleState>(OnPhysicsHandleState);
|
|
InitializeIsland();
|
|
InitializeContacts();
|
|
|
|
Subs.CVar(_cfg, CVars.AutoClearForces, OnAutoClearChange, true);
|
|
Subs.CVar(_cfg, CVars.NetTickrate, UpdateSubsteps, true);
|
|
Subs.CVar(_cfg, CVars.TargetMinimumTickrate, UpdateSubsteps, true);
|
|
}
|
|
|
|
private void OnPhysicsShutdown(EntityUid uid, PhysicsComponent component, ComponentShutdown args)
|
|
{
|
|
SetCanCollide(uid, false, false, body: component);
|
|
DebugTools.Assert(!component.Awake);
|
|
|
|
if (LifeStage(uid) <= EntityLifeStage.MapInitialized)
|
|
RemComp<FixturesComponent>(uid);
|
|
}
|
|
|
|
private void OnCollisionChange(ref CollisionChangeEvent ev)
|
|
{
|
|
var uid = ev.BodyUid;
|
|
var mapId = Transform(uid).MapID;
|
|
|
|
if (mapId == MapId.Nullspace)
|
|
return;
|
|
|
|
if (!ev.CanCollide)
|
|
{
|
|
DestroyContacts(ev.Body);
|
|
}
|
|
}
|
|
|
|
private void OnAutoClearChange(bool value)
|
|
{
|
|
_autoClearForces = value;
|
|
}
|
|
|
|
private void UpdateSubsteps(int obj)
|
|
{
|
|
var targetMinTickrate = (float) _cfg.GetCVar(CVars.TargetMinimumTickrate);
|
|
var serverTickrate = (float) _cfg.GetCVar(CVars.NetTickrate);
|
|
_substeps = (int)Math.Ceiling(targetMinTickrate / serverTickrate);
|
|
}
|
|
|
|
internal void OnParentChange(Entity<TransformComponent, MetaDataComponent> ent, EntityUid oldParent, EntityUid? oldMap)
|
|
{
|
|
// We do not have a directed/body subscription, because the entity changing parents may not have a physics component, but one of its children might.
|
|
var (uid, xform, meta) = ent;
|
|
|
|
// If this entity has yet to be initialized, then we can skip this as equivalent code will get run during
|
|
// init anyways. HOWEVER: it is possible that one of the children of this entity are already post-init, in
|
|
// which case they still need to handle map changes. This frequently happens when clients receives a server
|
|
// state where a known/old entity gets attached to a new, previously unknown, entity. The new entity will be
|
|
// uninitialized but have an initialized child.
|
|
if (xform.ChildCount == 0 && meta.EntityLifeStage < EntityLifeStage.Initialized)
|
|
return;
|
|
|
|
// Is this entity getting recursively detached after it's parent was already detached to null?
|
|
if (oldMap == null && xform.MapUid == null)
|
|
return;
|
|
|
|
var body = PhysicsQuery.CompOrNull(uid);
|
|
|
|
// Handle map changes
|
|
if (oldMap != xform.MapUid)
|
|
{
|
|
// This will also handle broadphase updating & joint clearing.
|
|
HandleMapChange(uid, xform, body);
|
|
return;
|
|
}
|
|
|
|
if (body != null)
|
|
HandleParentChangeVelocity(uid, body, oldParent, xform);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Recursively add/remove from awake bodies, clear joints, remove from move buffer, and update broadphase.
|
|
/// </summary>
|
|
private void HandleMapChange(EntityUid uid, TransformComponent xform, PhysicsComponent? body)
|
|
{
|
|
RecursiveMapUpdate(uid, xform, body);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Recursively add/remove from awake bodies, clear joints, remove from move buffer, and update broadphase.
|
|
/// </summary>
|
|
private void RecursiveMapUpdate(
|
|
EntityUid uid,
|
|
TransformComponent xform,
|
|
PhysicsComponent? body)
|
|
{
|
|
DebugTools.Assert(!Deleted(uid));
|
|
_joints.ClearJoints(uid);
|
|
|
|
foreach (var child in xform._children)
|
|
{
|
|
if (XformQuery.TryGetComponent(child, out var childXform))
|
|
{
|
|
PhysicsQuery.TryGetComponent(child, out var childBody);
|
|
RecursiveMapUpdate(child, childXform, childBody);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnGridAdd(GridAddEvent ev)
|
|
{
|
|
var guid = ev.EntityUid;
|
|
|
|
// If it's mapgrid then no physics.
|
|
if (HasComp<MapComponent>(guid))
|
|
return;
|
|
|
|
var body = EnsureComp<PhysicsComponent>(guid);
|
|
var manager = EnsureComp<FixturesComponent>(guid);
|
|
|
|
SetCanCollide(guid, true, manager: manager, body: body);
|
|
SetBodyType(guid, BodyType.Static, manager: manager, body: body);
|
|
}
|
|
|
|
public override void Shutdown()
|
|
{
|
|
base.Shutdown();
|
|
|
|
ShutdownContacts();
|
|
}
|
|
|
|
private void HandleContainerRemoved(EntityUid uid, PhysicsComponent physics, EntGotRemovedFromContainerMessage message)
|
|
{
|
|
// If entity being deleted then the parent change will already be handled elsewhere and we don't want to re-add it to the map.
|
|
if (MetaData(uid).EntityLifeStage >= EntityLifeStage.Terminating) return;
|
|
|
|
// If this entity is only meant to collide when anchored, return early.
|
|
if (_anchorQuery.TryGetComponent(uid, out var collideComp) && collideComp.Enable)
|
|
return;
|
|
|
|
WakeBody(uid, body: physics);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Simulates the physical world for a given amount of time.
|
|
/// </summary>
|
|
/// <param name="deltaTime">Delta Time in seconds of how long to simulate the world.</param>
|
|
/// <param name="prediction">Should only predicted entities be considered in this simulation step?</param>
|
|
protected void SimulateWorld(float deltaTime, bool prediction)
|
|
{
|
|
var frameTime = deltaTime / _substeps;
|
|
|
|
EffectiveCurTime = _gameTiming.CurTime;
|
|
for (int i = 0; i < _substeps; i++)
|
|
{
|
|
var updateBeforeSolve = new PhysicsUpdateBeforeSolveEvent(prediction, frameTime);
|
|
RaiseLocalEvent(ref updateBeforeSolve);
|
|
|
|
// Find new contacts and (TODO: temporary) update any per-map virtual controllers
|
|
|
|
// 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.
|
|
_broadphase.FindNewContacts();
|
|
|
|
// TODO PHYSICS Fix Collision Mispredicts
|
|
// If a physics update induces a position update that brings fixtures into contact, the collision starts in the NEXT tick,
|
|
// as positions are updated after CollideContacts() gets called.
|
|
//
|
|
// If a player input induces a position update that brings fixtures into contact, the collision happens on the SAME tick,
|
|
// as inputs are handled before system updates.
|
|
//
|
|
// When applying a server's game state with new positions, the client won't know what caused the positions to update,
|
|
// and thus can't know whether the collision already occurred (i.e., whether its effects are already contained within the
|
|
// game state currently being applied), or whether it should start on the next tick and needs to predict the start of
|
|
// the collision.
|
|
//
|
|
// Currently the client assumes that any position updates happened due to physics steps. I.e., positions are reset, then
|
|
// contacts are reset via ResetContacts(), then positions are updated using the new game state. Alternatively, we could
|
|
// call ResetContacts() AFTER applying the server state, which would correspond to assuming that the collisions have
|
|
// already "started" in the state, and we don't want to re-raise the events.
|
|
//
|
|
// Currently there is no way to avoid mispredicts from happening. E.g., a simple collision-counter component will always
|
|
// either mispredict on physics induced position changes, or on player/input induced updates. The easiest way I can think
|
|
// of to fix this would be to always call `CollideContacts` again at the very end of a physics update.
|
|
// But that might be unnecessarily expensive for what are hopefully only infrequent mispredicts.
|
|
|
|
CollideContacts();
|
|
|
|
Step(frameTime, prediction);
|
|
|
|
var updateAfterSolve = new PhysicsUpdateAfterSolveEvent(prediction, frameTime);
|
|
RaiseLocalEvent(ref updateAfterSolve);
|
|
|
|
// On last substep (or main step where no substeps occured) we'll update all of the lerp data.
|
|
if (i == _substeps - 1)
|
|
{
|
|
FinalStep();
|
|
}
|
|
|
|
EffectiveCurTime = EffectiveCurTime.Value + TimeSpan.FromSeconds(frameTime);
|
|
}
|
|
|
|
EffectiveCurTime = null;
|
|
}
|
|
|
|
protected virtual void FinalStep()
|
|
{
|
|
|
|
}
|
|
}
|
|
|
|
[ByRefEvent]
|
|
public readonly struct PhysicsUpdateAfterSolveEvent
|
|
{
|
|
public readonly bool Prediction;
|
|
public readonly float DeltaTime;
|
|
|
|
public PhysicsUpdateAfterSolveEvent(bool prediction, float deltaTime)
|
|
{
|
|
Prediction = prediction;
|
|
DeltaTime = deltaTime;
|
|
}
|
|
}
|
|
|
|
[ByRefEvent]
|
|
public readonly struct PhysicsUpdateBeforeSolveEvent
|
|
{
|
|
public readonly bool Prediction;
|
|
public readonly float DeltaTime;
|
|
|
|
public PhysicsUpdateBeforeSolveEvent(bool prediction, float deltaTime)
|
|
{
|
|
Prediction = prediction;
|
|
DeltaTime = deltaTime;
|
|
}
|
|
}
|
|
}
|