From d8bc7e1cb721c58e64abeffeab151891b6bde261 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Wed, 30 Nov 2022 09:41:26 +1100 Subject: [PATCH] Multi-threaded mob movement (#12611) --- .../Physics/Controllers/MoverController.cs | 27 ++- .../Physics/Controllers/MoverController.cs | 78 ++++++++- .../Components/InputMoverComponent.cs | 5 +- .../Systems/SharedMoverController.Input.cs | 78 ++++++++- .../Movement/Systems/SharedMoverController.cs | 159 ++++++++---------- 5 files changed, 242 insertions(+), 105 deletions(-) diff --git a/Content.Client/Physics/Controllers/MoverController.cs b/Content.Client/Physics/Controllers/MoverController.cs index 7cf1e72255..09dba9ae5b 100644 --- a/Content.Client/Physics/Controllers/MoverController.cs +++ b/Content.Client/Physics/Controllers/MoverController.cs @@ -1,11 +1,14 @@ +using Content.Shared.Inventory; using Content.Shared.Movement.Components; using Content.Shared.Movement.Systems; using Content.Shared.Pulling.Components; using Robust.Client.GameObjects; using Robust.Client.Player; +using Robust.Shared.Containers; using Robust.Shared.Physics; using Robust.Shared.Physics.Components; using Robust.Shared.Timing; +using Robust.Shared.Utility; namespace Content.Client.Physics.Controllers { @@ -134,8 +137,30 @@ namespace Content.Client.Physics.Controllers } } + var mobQuery = GetEntityQuery(); + var inventoryQuery = GetEntityQuery(); + var containerQuery = GetEntityQuery(); + var footQuery = GetEntityQuery(); + DebugTools.Assert(!UsedMobMovement.ContainsKey(mover.Owner)); + // Server-side should just be handled on its own so we'll just do this shizznit - HandleMobMovement(mover, body, xformMover, frameTime, xformQuery); + HandleMobMovement(mover, body, xformMover, frameTime, xformQuery, mobQuery, inventoryQuery, containerQuery, footQuery, out var dirtyMover, out var linearVelocity, out var sound, out var audio); + + MetaDataComponent? metadata = null; + + if (dirtyMover) + { + Dirty(mover, metadata); + } + + if (linearVelocity != null) + { + PhysicsSystem.SetLinearVelocity(body, linearVelocity.Value, false); + PhysicsSystem.SetAngularVelocity(body, 0f, false); + Dirty(body, metadata); + } + + Audio.PlayPredicted(sound, mover.Owner, mover.Owner, audio); } protected override bool CanSound() diff --git a/Content.Server/Physics/Controllers/MoverController.cs b/Content.Server/Physics/Controllers/MoverController.cs index 0793b07e77..ce331cc37a 100644 --- a/Content.Server/Physics/Controllers/MoverController.cs +++ b/Content.Server/Physics/Controllers/MoverController.cs @@ -1,20 +1,28 @@ +using System.Buffers; +using System.Threading.Tasks; using Content.Server.Cargo.Components; using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Systems; +using Content.Shared.Inventory; using Content.Shared.Movement.Components; using Content.Shared.Movement.Systems; using Content.Shared.Shuttles.Components; using Content.Shared.Shuttles.Systems; using Robust.Server.GameObjects; +using Robust.Shared.Audio; +using Robust.Shared.Containers; using Robust.Shared.Map; using Robust.Shared.Physics.Components; using Robust.Shared.Player; +using Robust.Shared.Threading; +using Robust.Shared.Utility; namespace Content.Server.Physics.Controllers { public sealed class MoverController : SharedMoverController { [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IParallelManager _parallel = default!; [Dependency] private readonly ThrusterSystem _thruster = default!; private Dictionary> _shuttlePilots = new(); @@ -64,7 +72,12 @@ namespace Content.Server.Physics.Controllers var xformQuery = GetEntityQuery(); var moverQuery = GetEntityQuery(); - foreach (var mover in EntityQuery(true)) + var movers = AllEntityQuery(); + var totalCount = EntityManager.Count(); + var moveInput = ArrayPool<(InputMoverComponent Mover, TransformComponent Transform, PhysicsComponent Physics)>.Shared.Rent(totalCount); + var count = 0; + + while (movers.MoveNext(out var mover)) { if (relayQuery.TryGetComponent(mover.Owner, out var relayed) && relayed.RelayEntity != null) { @@ -83,13 +96,11 @@ namespace Content.Server.Physics.Controllers continue; } - PhysicsComponent? body = null; - TransformComponent? xformMover = xform; - + PhysicsComponent? body; if (mover.ToParent && relayQuery.HasComponent(xform.ParentUid)) { if (!bodyQuery.TryGetComponent(xform.ParentUid, out body) || - !TryComp(xform.ParentUid, out xformMover)) + !xformQuery.HasComponent(xform.ParentUid)) { continue; } @@ -99,9 +110,64 @@ namespace Content.Server.Physics.Controllers continue; } - HandleMobMovement(mover, body, xformMover, frameTime, xformQuery); + DebugTools.Assert(!UsedMobMovement.ContainsKey(mover.Owner)); + + // To avoid threading issues on adding dictionary entries later. + UsedMobMovement[mover.Owner] = false; + moveInput[count++] = (mover, xform, body); } + var moveResults = ArrayPool<(bool DirtyMover, Vector2? LinearVelocity, SoundSpecifier? sound, AudioParams audio)>.Shared.Rent(count); + var mobQuery = GetEntityQuery(); + var inventoryQuery = GetEntityQuery(); + var containerQuery = GetEntityQuery(); + var footQuery = GetEntityQuery(); + + var options = new ParallelOptions() + { + MaxDegreeOfParallelism = _parallel.ParallelProcessCount, + }; + + Parallel.For(0, count, options, i => + { + var (mover, xform, body) = moveInput[i]; + HandleMobMovement(mover, body, xform, frameTime, xformQuery, mobQuery, inventoryQuery, containerQuery, footQuery, out var dirtyMover, out var linearVelocity, out var sound, out var audio); + moveResults[i] = (dirtyMover, linearVelocity, sound, audio); + }); + + var metaQuery = GetEntityQuery(); + + for (var i = 0; i < count; i++) + { + var results = moveResults[i]; + var input = moveInput[i]; + MetaDataComponent? metadata = null; + + // Calling dirty isn't thread-safe sadly. + if (results.DirtyMover) + { + metadata ??= metaQuery.GetComponent(input.Mover.Owner); + Dirty(input.Mover, metadata); + } + + if (results.LinearVelocity != null) + { + metadata ??= metaQuery.GetComponent(input.Physics.Owner); + PhysicsSystem.SetLinearVelocity(input.Physics, results.LinearVelocity.Value, false); + PhysicsSystem.SetAngularVelocity(input.Physics, 0f, false); + Dirty(input.Physics, metadata); + } + + if (results.sound != null) + { + Audio.PlayPredicted(results.sound, input.Mover.Owner, input.Mover.Owner, results.audio); + } + + moveInput[i] = default; + } + + ArrayPool<(bool DirtyMover, Vector2? LinearVelocity, SoundSpecifier? Sound, AudioParams Audio)>.Shared.Return(moveResults); + ArrayPool<(InputMoverComponent, TransformComponent, PhysicsComponent)>.Shared.Return(moveInput); HandleShuttleMovement(frameTime); } diff --git a/Content.Shared/Movement/Components/InputMoverComponent.cs b/Content.Shared/Movement/Components/InputMoverComponent.cs index 49a82df1d3..0b87e4311d 100644 --- a/Content.Shared/Movement/Components/InputMoverComponent.cs +++ b/Content.Shared/Movement/Components/InputMoverComponent.cs @@ -1,5 +1,6 @@ using Content.Shared.Movement.Systems; using Robust.Shared.GameStates; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; using Robust.Shared.Timing; namespace Content.Shared.Movement.Components @@ -61,8 +62,8 @@ namespace Content.Shared.Movement.Components /// /// If we traverse on / off a grid then set a timer to update our relative inputs. /// - [ViewVariables(VVAccess.ReadWrite)] - public float LerpAccumulator; + [ViewVariables(VVAccess.ReadWrite), DataField("lerpTarget", customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan LerpTarget; public const float LerpTime = 1.0f; diff --git a/Content.Shared/Movement/Systems/SharedMoverController.Input.cs b/Content.Shared/Movement/Systems/SharedMoverController.Input.cs index 27286dc701..2ccdb30ff9 100644 --- a/Content.Shared/Movement/Systems/SharedMoverController.Input.cs +++ b/Content.Shared/Movement/Systems/SharedMoverController.Input.cs @@ -80,7 +80,7 @@ namespace Content.Shared.Movement.Systems component.RelativeRotation = state.RelativeRotation; component.TargetRelativeRotation = state.TargetRelativeRotation; component.RelativeEntity = state.RelativeEntity; - component.LerpAccumulator = state.LerpAccumulator; + component.LerpTarget = state.LerpTarget; } private void OnInputGetState(EntityUid uid, InputMoverComponent component, ref ComponentGetState args) @@ -91,7 +91,7 @@ namespace Content.Shared.Movement.Systems component.RelativeRotation, component.TargetRelativeRotation, component.RelativeEntity, - component.LerpAccumulator); + component.LerpTarget); } private void ShutdownInput() @@ -118,13 +118,73 @@ namespace Content.Shared.Movement.Systems public void ResetCamera(EntityUid uid) { - if (CameraRotationLocked || !TryComp(uid, out var mover) || mover.TargetRelativeRotation.Equals(Angle.Zero)) + if (CameraRotationLocked || + !TryComp(uid, out var mover)) + { + return; + } + + // If we updated parent then cancel the accumulator and force it now. + var xformQuery = GetEntityQuery(); + + if (!TryUpdateRelative(mover, xformQuery.GetComponent(uid), xformQuery) && mover.TargetRelativeRotation.Equals(Angle.Zero)) return; + mover.LerpTarget = TimeSpan.Zero; mover.TargetRelativeRotation = Angle.Zero; Dirty(mover); } + private bool TryUpdateRelative(InputMoverComponent mover, TransformComponent xform, EntityQuery xformQuery) + { + var relative = xform.GridUid; + relative ??= xform.MapUid; + + // So essentially what we want: + // 1. If we go from grid to map then preserve our rotation and continue as usual + // 2. If we go from grid -> grid then (after lerp time) snap to nearest cardinal (probably imperceptible) + // 3. If we go from map -> grid then (after lerp time) snap to nearest cardinal + + if (mover.RelativeEntity.Equals(relative)) + return false; + + // Okay need to get our old relative rotation with respect to our new relative rotation + // e.g. if we were right side up on our current grid need to get what that is on our new grid. + var currentRotation = Angle.Zero; + var targetRotation = Angle.Zero; + + // Get our current relative rotation + if (xformQuery.TryGetComponent(mover.RelativeEntity, out var oldRelativeXform)) + { + currentRotation = _transform.GetWorldRotation(oldRelativeXform, xformQuery) + mover.RelativeRotation; + } + + if (xformQuery.TryGetComponent(relative, out var relativeXform)) + { + // This is our current rotation relative to our new parent. + mover.RelativeRotation = (currentRotation - _transform.GetWorldRotation(relativeXform, xformQuery)).FlipPositive(); + } + + // If we went from grid -> map we'll preserve our worldrotation + if (relative != null && _mapManager.IsMap(relative.Value)) + { + targetRotation = currentRotation.FlipPositive().Reduced(); + } + // If we went from grid -> grid OR grid -> map then snap the target to cardinal and lerp there. + // OR just rotate to zero (depending on cvar) + else if (relative != null && _mapManager.IsGrid(relative.Value)) + { + if (CameraRotationLocked) + targetRotation = Angle.Zero; + else + targetRotation = mover.RelativeRotation.GetCardinalDir().ToAngle().Reduced(); + } + + mover.RelativeEntity = relative; + mover.TargetRelativeRotation = targetRotation; + return true; + } + public Angle GetParentGridAngle(InputMoverComponent mover, EntityQuery xformQuery) { var rotation = mover.RelativeRotation; @@ -161,16 +221,16 @@ namespace Content.Shared.Movement.Systems // If we go on a grid and back off then just reset the accumulator. if (relative == component.RelativeEntity) { - if (component.LerpAccumulator != 0f) + if (component.LerpTarget >= Timing.CurTime) { - component.LerpAccumulator = 0f; + component.LerpTarget = TimeSpan.Zero; Dirty(component); } return; } - component.LerpAccumulator = InputMoverComponent.LerpTime; + component.LerpTarget = TimeSpan.FromSeconds(InputMoverComponent.LerpTime) + Timing.CurTime; Dirty(component); } @@ -499,16 +559,16 @@ namespace Content.Shared.Movement.Systems /// public Angle TargetRelativeRotation; public EntityUid? RelativeEntity; - public float LerpAccumulator = 0f; + public TimeSpan LerpTarget; - public InputMoverComponentState(MoveButtons buttons, bool canMove, Angle relativeRotation, Angle targetRelativeRotation, EntityUid? relativeEntity, float lerpAccumulator) + public InputMoverComponentState(MoveButtons buttons, bool canMove, Angle relativeRotation, Angle targetRelativeRotation, EntityUid? relativeEntity, TimeSpan lerpTarget) { Buttons = buttons; CanMove = canMove; RelativeRotation = relativeRotation; TargetRelativeRotation = targetRelativeRotation; RelativeEntity = relativeEntity; - LerpAccumulator = lerpAccumulator; + LerpTarget = lerpTarget; } } diff --git a/Content.Shared/Movement/Systems/SharedMoverController.cs b/Content.Shared/Movement/Systems/SharedMoverController.cs index 10ab0e165f..d79f1a81f4 100644 --- a/Content.Shared/Movement/Systems/SharedMoverController.cs +++ b/Content.Shared/Movement/Systems/SharedMoverController.cs @@ -28,17 +28,17 @@ namespace Content.Shared.Movement.Systems /// public abstract partial class SharedMoverController : VirtualController { - [Dependency] private readonly IConfigurationManager _configManager = default!; + [Dependency] private readonly IConfigurationManager _configManager = default!; [Dependency] protected readonly IGameTiming Timing = default!; - [Dependency] private readonly IMapManager _mapManager = default!; - [Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!; - [Dependency] private readonly InventorySystem _inventory = default!; - [Dependency] private readonly SharedContainerSystem _container = default!; - [Dependency] private readonly SharedGravitySystem _gravity = default!; - [Dependency] private readonly SharedMobStateSystem _mobState = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly SharedTransformSystem _transform = default!; - [Dependency] private readonly TagSystem _tags = default!; + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!; + [Dependency] private readonly InventorySystem _inventory = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly SharedGravitySystem _gravity = default!; + [Dependency] private readonly SharedMobStateSystem _mobState = default!; + [Dependency] protected readonly SharedAudioSystem Audio = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly TagSystem _tags = default!; private const float StepSoundMoveDistanceRunning = 2; private const float StepSoundMoveDistanceWalking = 1.5f; @@ -96,69 +96,35 @@ namespace Content.Shared.Movement.Systems /// /// Movement while considering actionblockers, weightlessness, etc. /// + /// + /// Yes this signature is massive but this is also called a lot. + /// protected void HandleMobMovement( InputMoverComponent mover, PhysicsComponent physicsComponent, TransformComponent xform, float frameTime, - EntityQuery xformQuery) + EntityQuery xformQuery, + EntityQuery mobQuery, + EntityQuery inventoryQuery, + EntityQuery containerQuery, + EntityQuery footQuery, + out bool dirtyMover, + out Vector2? linearVelocity, + out SoundSpecifier? sound, + out AudioParams audio) { - DebugTools.Assert(!UsedMobMovement.ContainsKey(mover.Owner)); + dirtyMover = false; + linearVelocity = Vector2.Zero; + sound = null; + audio = default; // Update relative movement - if (mover.LerpAccumulator > 0f) + if (mover.LerpTarget < Timing.CurTime) { - Dirty(mover); - mover.LerpAccumulator -= frameTime; - - if (mover.LerpAccumulator <= 0f) + if (TryUpdateRelative(mover, xform, xformQuery)) { - mover.LerpAccumulator = 0f; - var relative = xform.GridUid; - relative ??= xform.MapUid; - - // So essentially what we want: - // 1. If we go from grid to map then preserve our rotation and continue as usual - // 2. If we go from grid -> grid then (after lerp time) snap to nearest cardinal (probably imperceptible) - // 3. If we go from map -> grid then (after lerp time) snap to nearest cardinal - - if (!mover.RelativeEntity.Equals(relative)) - { - // Okay need to get our old relative rotation with respect to our new relative rotation - // e.g. if we were right side up on our current grid need to get what that is on our new grid. - var currentRotation = Angle.Zero; - var targetRotation = Angle.Zero; - - // Get our current relative rotation - if (xformQuery.TryGetComponent(mover.RelativeEntity, out var oldRelativeXform)) - { - currentRotation = oldRelativeXform.WorldRotation + mover.RelativeRotation; - } - - if (xformQuery.TryGetComponent(relative, out var relativeXform)) - { - // This is our current rotation relative to our new parent. - mover.RelativeRotation = (currentRotation - relativeXform.WorldRotation).FlipPositive(); - } - - // If we went from grid -> map we'll preserve our worldrotation - if (relative != null && _mapManager.IsMap(relative.Value)) - { - targetRotation = currentRotation.FlipPositive().Reduced(); - } - // If we went from grid -> grid OR grid -> map then snap the target to cardinal and lerp there. - // OR just rotate to zero (depending on cvar) - else if (relative != null && _mapManager.IsGrid(relative.Value)) - { - if (CameraRotationLocked) - targetRotation = Angle.Zero; - else - targetRotation = mover.RelativeRotation.GetCardinalDir().ToAngle().Reduced(); - } - - mover.RelativeEntity = relative; - mover.TargetRelativeRotation = targetRotation; - } + dirtyMover = true; } } @@ -183,13 +149,13 @@ namespace Content.Shared.Movement.Systems mover.RelativeRotation += adjustment; mover.RelativeRotation.FlipPositive(); - Dirty(mover); + dirtyMover = true; } else if (!angleDiff.Equals(Angle.Zero)) { mover.TargetRelativeRotation.FlipPositive(); mover.RelativeRotation = mover.TargetRelativeRotation; - Dirty(mover); + dirtyMover = true; } if (!UseMobMovement(mover, physicsComponent)) @@ -217,7 +183,7 @@ namespace Content.Shared.Movement.Systems // No gravity: is our entity touching anything? touching = ev.CanMove; - if (!touching && TryComp(xform.Owner, out var mobMover)) + if (!touching && mobQuery.TryGetComponent(xform.Owner, out var mobMover)) touching |= IsAroundCollider(PhysicsSystem, xform, mobMover, physicsComponent); } } @@ -274,19 +240,20 @@ namespace Content.Shared.Movement.Systems { // This should have its event run during island solver soooo xform.DeferUpdates = true; - xform.WorldRotation = worldTotal.ToWorldAngle(); + _transform.SetWorldRotation(xform, worldTotal.ToWorldAngle(), xformQuery); xform.DeferUpdates = false; if (!weightless && TryComp(mover.Owner, out var mobMover) && - TryGetSound(weightless, mover, mobMover, xform, out var sound)) + TryGetSound(weightless, mover, mobMover, xform, inventoryQuery, containerQuery, footQuery, out var soundSpec)) { var soundModifier = mover.Sprinting ? 1.0f : FootstepWalkingAddedVolumeMultiplier; - var audioParams = sound.Params + var audioParams = soundSpec.Params .WithVolume(FootstepVolume * soundModifier) - .WithVariation(sound.Params.Variation ?? FootstepVariation); + .WithVariation(soundSpec.Params.Variation ?? FootstepVariation); - _audio.PlayPredicted(sound, mover.Owner, mover.Owner, audioParams); + sound = soundSpec; + audio = audioParams; } } @@ -295,10 +262,13 @@ namespace Content.Shared.Movement.Systems if (!weightless || touching) Accelerate(ref velocity, in worldTotal, accel, frameTime); - PhysicsSystem.SetLinearVelocity(physicsComponent, velocity); + if (physicsComponent.LinearVelocity.EqualsApprox(velocity, 0.0001f) && + MathHelper.CloseTo(physicsComponent.AngularVelocity, 0f)) + { + return; + } - // Ensures that players do not spiiiiiiin - PhysicsSystem.SetAngularVelocity(physicsComponent, 0); + linearVelocity = velocity; } private void Friction(float minimumFrictionSpeed, float frameTime, float friction, ref Vector2 velocity) @@ -379,7 +349,16 @@ namespace Content.Shared.Movement.Systems protected abstract bool CanSound(); - private bool TryGetSound(bool weightless, InputMoverComponent mover, MobMoverComponent mobMover, TransformComponent xform, [NotNullWhen(true)] out SoundSpecifier? sound) + private bool TryGetSound( + bool weightless, + InputMoverComponent mover, + MobMoverComponent mobMover, + TransformComponent xform, + EntityQuery inventoryQuery, + EntityQuery containerQuery, + EntityQuery footQuery, + + [NotNullWhen(true)] out SoundSpecifier? sound) { sound = null; @@ -413,18 +392,21 @@ namespace Content.Shared.Movement.Systems if (mobMover.StepSoundDistance < distanceNeeded) return false; mobMover.StepSoundDistance -= distanceNeeded; + EntityUid? shoes = null; - if (_inventory.TryGetSlotEntity(mover.Owner, "shoes", out var shoes) && - EntityManager.TryGetComponent(shoes, out var modifier)) + if (inventoryQuery.TryGetComponent(mover.Owner, out var inventory) && + containerQuery.TryGetComponent(mover.Owner, out var containerManager) && + _inventory.TryGetSlotEntity(mover.Owner, "shoes", out shoes, inventory, containerManager) && + footQuery.TryGetComponent(shoes, out var modifier)) { sound = modifier.Sound; return true; } - return TryGetFootstepSound(coordinates, shoes != null, out sound); + return TryGetFootstepSound(coordinates, shoes != null, footQuery, out sound); } - private bool TryGetFootstepSound(EntityCoordinates coordinates, bool haveShoes, [NotNullWhen(true)] out SoundSpecifier? sound) + private bool TryGetFootstepSound(EntityCoordinates coordinates, bool haveShoes, EntityQuery footQuery, [NotNullWhen(true)] out SoundSpecifier? sound) { sound = null; var gridUid = coordinates.GetGridUid(EntityManager); @@ -432,7 +414,7 @@ namespace Content.Shared.Movement.Systems // Fallback to the map if (gridUid == null) { - if (TryComp(coordinates.GetMapUid(EntityManager), out var modifier)) + if (footQuery.TryGetComponent(coordinates.GetMapUid(EntityManager), out var modifier)) { sound = modifier.Sound; return true; @@ -444,17 +426,20 @@ namespace Content.Shared.Movement.Systems var grid = _mapManager.GetGrid(gridUid.Value); var tile = grid.GetTileRef(coordinates); - if (tile.IsSpace(_tileDefinitionManager)) return false; + if (tile.IsSpace(_tileDefinitionManager)) + return false; // If the coordinates have a FootstepModifier component // i.e. component that emit sound on footsteps emit that sound - foreach (var maybeFootstep in grid.GetAnchoredEntities(tile.GridIndices)) + var anchoredEnumerator = grid.GetAnchoredEntitiesEnumerator(tile.GridIndices); + + while (anchoredEnumerator.MoveNext(out var maybeFootstep)) { - if (EntityManager.TryGetComponent(maybeFootstep, out FootstepModifierComponent? footstep)) - { - sound = footstep.Sound; - return true; - } + if (!footQuery.TryGetComponent(maybeFootstep, out var footstep)) + continue; + + sound = footstep.Sound; + return true; } // Walking on a tile.