From 5b4286153977763f2188c56b9447134c273682e1 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Mon, 29 Aug 2022 15:05:53 +1000 Subject: [PATCH] Smooth docking traversal (#10822) --- .../EscapeMenu/UI/Tabs/KeyRebindTab.xaml.cs | 5 + Content.Client/Eye/EyeLerpingSystem.cs | 263 +++++++++--------- .../Physics/Controllers/MoverController.cs | 25 +- .../Administration/Commands/AGhost.cs | 2 +- .../GameTicking/GameTicker.RoundFlow.cs | 3 +- .../GameTicking/GameTicker.Spawning.cs | 71 ++++- .../CrewMonitoringConsoleSystem.cs | 4 +- .../Physics/Controllers/MoverController.cs | 22 +- Content.Shared/CCVar/CCVars.cs | 10 + .../Components/InputMoverComponent.cs | 24 +- .../Systems/SharedMoverController.Input.cs | 164 ++++++++++- .../Movement/Systems/SharedMoverController.cs | 115 ++++++-- .../en-US/escape-menu/ui/options-menu.ftl | 5 + Resources/keybinds.yml | 6 +- 14 files changed, 515 insertions(+), 204 deletions(-) diff --git a/Content.Client/EscapeMenu/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/EscapeMenu/UI/Tabs/KeyRebindTab.xaml.cs index d1045b6cce..d983f09024 100644 --- a/Content.Client/EscapeMenu/UI/Tabs/KeyRebindTab.xaml.cs +++ b/Content.Client/EscapeMenu/UI/Tabs/KeyRebindTab.xaml.cs @@ -105,6 +105,11 @@ namespace Content.Client.EscapeMenu.UI.Tabs AddButton(EngineKeyFunctions.MoveRight); AddButton(EngineKeyFunctions.Walk); + AddHeader("ui-options-header-camera"); + AddButton(EngineKeyFunctions.CameraRotateLeft); + AddButton(EngineKeyFunctions.CameraRotateRight); + AddButton(EngineKeyFunctions.CameraReset); + AddHeader("ui-options-header-interaction-basic"); AddButton(EngineKeyFunctions.Use); AddButton(ContentKeyFunctions.UseItemInHand); diff --git a/Content.Client/Eye/EyeLerpingSystem.cs b/Content.Client/Eye/EyeLerpingSystem.cs index 7befae2fb6..dbda80ffa3 100644 --- a/Content.Client/Eye/EyeLerpingSystem.cs +++ b/Content.Client/Eye/EyeLerpingSystem.cs @@ -1,41 +1,31 @@ -using System; using Content.Shared.Movement.Components; +using Content.Shared.Movement.Systems; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.Physics; using Robust.Client.Player; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; +using Robust.Shared.Collections; using Robust.Shared.Map; -using Robust.Shared.Maths; using Robust.Shared.Timing; +using Robust.Shared.Utility; namespace Content.Client.Eye; public sealed class EyeLerpingSystem : EntitySystem { [Dependency] private readonly IEyeManager _eyeManager = default!; - [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; - - // How fast the camera rotates in radians / s - private const float CameraRotateSpeed = MathF.PI; - - // Safety override - private const float LerpTimeMax = 1.5f; - - // Lerping information for the player's active eye. - private readonly EyeLerpInformation _playerActiveEye = new(); + [Dependency] private readonly SharedMoverController _mover = default!; // Eyes other than the primary eye that are currently active. private readonly Dictionary _activeEyes = new(); - private readonly List _toRemove = new(); public override void Initialize() { base.Initialize(); + SubscribeLocalEvent(OnEyeStartup); SubscribeLocalEvent(OnEyeShutdown); UpdatesAfter.Add(typeof(TransformSystem)); @@ -43,6 +33,27 @@ public sealed class EyeLerpingSystem : EntitySystem UpdatesBefore.Add(typeof(EyeUpdateSystem)); } + private void OnEyeStartup(EntityUid uid, EyeComponent component, ComponentStartup args) + { + if (component.Eye == null) + return; + + // If the eye starts up then don't lerp at all. + var xformQuery = GetEntityQuery(); + TryComp(uid, out var mover); + xformQuery.TryGetComponent(uid, out var xform); + var lerpInfo = _activeEyes.GetOrNew(uid); + lerpInfo.TargetRotation = GetRotation(xformQuery, mover, xform); + lerpInfo.LastRotation = lerpInfo.TargetRotation; + + if (xform != null) + { + lerpInfo.MapId = xform.MapID; + } + + component.Eye.Rotation = lerpInfo.TargetRotation; + } + private void OnEyeShutdown(EntityUid uid, EyeComponent component, ComponentShutdown args) { RemoveEye(uid); @@ -50,145 +61,131 @@ public sealed class EyeLerpingSystem : EntitySystem public void AddEye(EntityUid uid) { - if (!_activeEyes.ContainsKey(uid)) - { - _activeEyes.Add(uid, new()); - } + _activeEyes.TryAdd(uid, new EyeLerpInformation()); } public void RemoveEye(EntityUid uid) { - if (_activeEyes.ContainsKey(uid)) + _activeEyes.Remove(uid); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + if (!_gameTiming.IsFirstTimePredicted) + return; + + var moverQuery = GetEntityQuery(); + var xformQuery = GetEntityQuery(); + var foundEyes = new ValueList(1); + + // Set all of our eye rotations to the relevant values. + foreach (var (eye, entity) in GetEyes()) { - _activeEyes.Remove(uid); + var lerpInfo = _activeEyes.GetOrNew(entity); + foundEyes.Add(entity); + moverQuery.TryGetComponent(entity, out var mover); + xformQuery.TryGetComponent(entity, out var xform); + lerpInfo.LastRotation = eye.Rotation; + lerpInfo.TargetRotation = GetRotation(xformQuery, mover, xform); + + if (xform != null) + { + // If we traverse maps then don't lerp. + if (xform.MapID != lerpInfo.MapId) + { + lerpInfo.LastRotation = lerpInfo.TargetRotation; + } + } + } + + foreach (var eye in foundEyes) + { + if (!_activeEyes.ContainsKey(eye)) + { + _activeEyes.Remove(eye); + } + } + } + + private Angle GetRotation(EntityQuery xformQuery, InputMoverComponent? mover = null, TransformComponent? xform = null) + { + // If we can move then tie our eye to our inputs (these also get lerped so it should be fine). + if (mover != null) + { + return -_mover.GetParentGridAngle(mover); + } + + // if not tied to a mover then lock it to map / grid + if (xform != null) + { + var relative = xform.GridUid; + relative ??= xform.MapUid; + + if (xformQuery.TryGetComponent(relative, out var relativeXform)) + { + return relativeXform.WorldRotation; + } + } + + return Angle.Zero; + } + + private IEnumerable<(IEye Eye, EntityUid Entity)> GetEyes() + { + if (_playerManager.LocalPlayer?.ControlledEntity is { } player && !Deleted(player)) + { + yield return (_eyeManager.CurrentEye, player); + } + + if (_activeEyes.Count == 0) + yield break; + + var eyeQuery = GetEntityQuery(); + + foreach (var (ent, info) in _activeEyes) + { + if (!eyeQuery.TryGetComponent(ent, out var eyeComp) || + eyeComp.Eye == null) + { + continue; + } + + yield return (eyeComp.Eye, ent); } } public override void FrameUpdate(float frameTime) { - if (!_gameTiming.IsFirstTimePredicted) - return; + var tickFraction = (float) _gameTiming.TickFraction / ushort.MaxValue; + var lerpMinimum = 0.01; - // Always do this one. - LerpPlayerEye(frameTime); - - foreach (var (entity, info) in _activeEyes) + foreach (var (eye, entity) in GetEyes()) { - LerpEntityEye(entity, info, frameTime); - } + if (!_activeEyes.TryGetValue(entity, out var lerpInfo)) + continue; - if (_toRemove.Count != 0) - { - foreach (var entity in _toRemove) + var shortest = Angle.ShortestDistance(lerpInfo.LastRotation, lerpInfo.TargetRotation); + + if (Math.Abs(shortest.Theta) < lerpMinimum) { - RemoveEye(entity); + eye.Rotation = lerpInfo.TargetRotation; + continue; } - _toRemove.Clear(); - } - } - - private void LerpPlayerEye(float frameTime) - { - if (_playerManager.LocalPlayer?.ControlledEntity is not {} mob || Deleted(mob)) - return; - - // We can't lerp if the mob can't move! - if (!TryComp(mob, out InputMoverComponent? mover)) - return; - - LerpEye(_eyeManager.CurrentEye, frameTime, mover.LastGridAngle, _playerActiveEye); - } - - private void LerpEntityEye(EntityUid uid, EyeLerpInformation info, float frameTime) - { - if (!TryComp(uid, out TransformComponent? transform) - || !TryComp(uid, out EyeComponent? eye) - || eye.Eye == null - || !_mapManager.TryGetGrid(transform.GridUid, out var grid)) - { - _toRemove.Add(uid); - return; - } - - LerpEye(eye.Eye, frameTime, grid.WorldRotation, info); - } - - private void LerpEye(IEye eye, float frameTime, Angle lastAngle, EyeLerpInformation lerpInfo) - { - - // Let's not turn the camera into a washing machine when the game starts. - if (lerpInfo.LastGridAngle == null) - { - lerpInfo.LastGridAngle = lastAngle; - eye.Rotation = -lastAngle; - return; - } - - // Check if the last lerp grid angle we have is not the same as the last mover grid angle... - if (!lerpInfo.LastGridAngle.Value.EqualsApprox(lastAngle)) - { - // And now, we start lerping. - lerpInfo.LerpTo = lastAngle; - lerpInfo.LastGridAngle = lastAngle; - lerpInfo.LerpStartRotation = eye.Rotation; - lerpInfo.Accumulator = 0f; - } - - if (lerpInfo.LerpTo != null) - { - lerpInfo.Accumulator += frameTime; - - var lerpRot = -lerpInfo.LerpTo.Value.FlipPositive().Reduced(); - var startRot = lerpInfo.LerpStartRotation.FlipPositive().Reduced(); - - var changeNeeded = Angle.ShortestDistance(startRot, lerpRot); - - if (changeNeeded.EqualsApprox(Angle.Zero)) - { - // Nothing to do here! - lerpInfo.Cleanup(eye); - return; - } - - // Get how much the camera should have moved by now. Make it faster depending on the change needed. - var changeRot = (CameraRotateSpeed * Math.Max(1f, Math.Abs(changeNeeded) * 0.75f)) * lerpInfo.Accumulator * Math.Sign(changeNeeded); - - // How close is this from reaching the end? - var percentage = (float)Math.Abs(changeRot / changeNeeded); - - eye.Rotation = Angle.Lerp(startRot, lerpRot, percentage); - - // Either we have overshot, or we have taken way too long on this, emergency reset time - if (percentage >= 1.0f || lerpInfo.Accumulator >= LerpTimeMax) - { - lerpInfo.Cleanup(eye); - } - } - else - { - // This makes it so rotating the camera manually is impossible... - // However, it is needed. Why? Because of a funny (hilarious, even) race condition involving - // ghosting, this system listening for attached mob changes, and the eye rotation being reset after our - // changes back to zero because of an EyeComponent state coming from the server being applied. - // At some point we'll need to come up with a solution for that. But for now, I just want to fix this. - eye.Rotation = -lastAngle; + eye.Rotation = shortest * tickFraction + lerpInfo.LastRotation; } } private sealed class EyeLerpInformation { - public Angle? LastGridAngle { get; set; } - public Angle? LerpTo { get; set; } - public Angle LerpStartRotation { get; set; } - public float Accumulator { get; set; } + public Angle LastRotation; + public Angle TargetRotation; - public void Cleanup(IEye eye) - { - eye.Rotation = -LerpTo ?? Angle.Zero; - LerpStartRotation = eye.Rotation; - LerpTo = null; - Accumulator = 0; - } + /// + /// If we go to a new map then don't lerp and snap instantly. + /// + public MapId MapId; } } diff --git a/Content.Client/Physics/Controllers/MoverController.cs b/Content.Client/Physics/Controllers/MoverController.cs index d9697ddea0..b023b835c6 100644 --- a/Content.Client/Physics/Controllers/MoverController.cs +++ b/Content.Client/Physics/Controllers/MoverController.cs @@ -3,7 +3,6 @@ using Content.Shared.Movement.Systems; using Content.Shared.Pulling.Components; using Robust.Client.Player; using Robust.Shared.Physics; -using Robust.Shared.Player; using Robust.Shared.Timing; namespace Content.Client.Physics.Controllers @@ -23,7 +22,17 @@ namespace Content.Client.Physics.Controllers if (TryComp(player, out var relayMover)) { if (relayMover.RelayEntity != null) + { + if (TryComp(player, out var mover) && + TryComp(relayMover.RelayEntity, out var relayed)) + { + relayed.RelativeEntity = mover.RelativeEntity; + relayed.RelativeRotation = mover.RelativeRotation; + relayed.TargetRelativeRotation = mover.RelativeRotation; + } + HandleClientsideMovement(relayMover.RelayEntity.Value, frameTime); + } } HandleClientsideMovement(player, frameTime); @@ -31,8 +40,10 @@ namespace Content.Client.Physics.Controllers private void HandleClientsideMovement(EntityUid player, float frameTime) { + var xformQuery = GetEntityQuery(); + if (!TryComp(player, out InputMoverComponent? mover) || - !TryComp(player, out TransformComponent? xform)) + !xformQuery.TryGetComponent(player, out var xform)) { return; } @@ -47,20 +58,12 @@ namespace Content.Client.Physics.Controllers { return; } - - if (TryComp(xform.ParentUid, out var parentMover)) - { - mover.LastGridAngle = parentMover.LastGridAngle; - } } else if (!TryComp(player, out body)) { return; } - if (xform.GridUid != null) - mover.LastGridAngle = GetParentGridAngle(xform, mover); - // Essentially we only want to set our mob to predicted so every other entity we just interpolate // (i.e. only see what the server has sent us). // The exception to this is joints. @@ -98,7 +101,7 @@ namespace Content.Client.Physics.Controllers } // Server-side should just be handled on its own so we'll just do this shizznit - HandleMobMovement(mover, body, xformMover, frameTime); + HandleMobMovement(mover, body, xformMover, frameTime, xformQuery); } protected override bool CanSound() diff --git a/Content.Server/Administration/Commands/AGhost.cs b/Content.Server/Administration/Commands/AGhost.cs index 76a85bfa0c..dce4d9d4a2 100644 --- a/Content.Server/Administration/Commands/AGhost.cs +++ b/Content.Server/Administration/Commands/AGhost.cs @@ -44,7 +44,7 @@ namespace Content.Server.Administration.Commands var coordinates = player.AttachedEntity != null ? _entities.GetComponent(player.AttachedEntity.Value).Coordinates : EntitySystem.Get().GetObserverSpawnPoint(); - var ghost = _entities.SpawnEntity("AdminObserver", coordinates.ToMap(_entities)); + var ghost = _entities.SpawnEntity("AdminObserver", coordinates); if (canReturn) { diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs index 20fe19a159..33edd3cbf7 100644 --- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs +++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs @@ -121,10 +121,9 @@ namespace Content.Server.GameTicking var (entities, gridIds) = _mapLoader.LoadMap(targetMapId, ev.GameMap.MapPath.ToString(), ev.Options); - var gridUids = gridIds.Select(g => (EntityUid)g).ToList(); + var gridUids = gridIds.Select(g => g).ToList(); RaiseLocalEvent(new PostGameMapLoad(map, targetMapId, entities, gridUids, stationName)); - _spawnPoint = _mapManager.GetGrid(gridIds[0]).ToCoordinates(); return (entities, gridUids); } diff --git a/Content.Server/GameTicking/GameTicker.Spawning.cs b/Content.Server/GameTicking/GameTicker.Spawning.cs index 6ec7fab1dc..0cb178d4a5 100644 --- a/Content.Server/GameTicking/GameTicker.Spawning.cs +++ b/Content.Server/GameTicking/GameTicker.Spawning.cs @@ -25,9 +25,6 @@ namespace Content.Server.GameTicking { private const string ObserverPrototypeName = "MobObserver"; - [ViewVariables(VVAccess.ReadWrite), Obsolete("Due for removal when observer spawning is refactored.")] // See also: MindComponent's OnShutdown shitcode - private EntityCoordinates _spawnPoint; - /// /// How many players have joined the round through normal methods. /// Useful for game rules to look at. Doesn't count observers, people in lobby, etc. @@ -280,24 +277,72 @@ namespace Content.Server.GameTicking #region Spawn Points public EntityCoordinates GetObserverSpawnPoint() { - // TODO rename this to TryGetObserverSpawnPoint to make it clear that the result might be invalid. Or at - // least try try more fallback values, like randomly spawning them in any available map or just creating a - // "we fucked up" map. Its better than dumping them into the void. - - var location = _spawnPoint.IsValid(EntityManager) ? _spawnPoint : EntityCoordinates.Invalid; - _possiblePositions.Clear(); foreach (var (point, transform) in EntityManager.EntityQuery(true)) { - if (point.SpawnType == SpawnPointType.Observer) - _possiblePositions.Add(transform.Coordinates); + if (point.SpawnType != SpawnPointType.Observer) + continue; + + _possiblePositions.Add(transform.Coordinates); + } + + var metaQuery = GetEntityQuery(); + + // Fallback to a random grid. + if (_possiblePositions.Count == 0) + { + foreach (var grid in _mapManager.GetAllGrids()) + { + if (!metaQuery.TryGetComponent(grid.GridEntityId, out var meta) || + meta.EntityPaused) + { + continue; + } + + _possiblePositions.Add(new EntityCoordinates(grid.GridEntityId, Vector2.Zero)); + } } if (_possiblePositions.Count != 0) - location = _robustRandom.Pick(_possiblePositions); + { + // TODO: This is just here for the eye lerping. + // Ideally engine would just spawn them on grid directly I guess? Right now grid traversal is handling it during + // update which means we need to add a hack somewhere around it. + var spawn = _robustRandom.Pick(_possiblePositions); + var toMap = spawn.ToMap(EntityManager); - return location; + if (_mapManager.TryFindGridAt(toMap, out var foundGrid)) + { + return new EntityCoordinates(foundGrid.GridEntityId, + foundGrid.InvWorldMatrix.Transform(toMap.Position)); + } + + return spawn; + } + + if (_mapManager.MapExists(DefaultMap)) + { + return new EntityCoordinates(_mapManager.GetMapEntityId(DefaultMap), Vector2.Zero); + } + + // Just pick a point at this point I guess. + foreach (var map in _mapManager.GetAllMapIds()) + { + var mapUid = _mapManager.GetMapEntityId(map); + + if (!metaQuery.TryGetComponent(mapUid, out var meta) || + meta.EntityPaused) + { + continue; + } + + return new EntityCoordinates(mapUid, Vector2.Zero); + } + + // AAAAAAAAAAAAA + _sawmill.Error("Found no observer spawn points!"); + return EntityCoordinates.Invalid; } #endregion } diff --git a/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs b/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs index 7df71a39d1..4456ba6ddb 100644 --- a/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs +++ b/Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs @@ -75,9 +75,7 @@ namespace Content.Server.Medical.CrewMonitoring // the monitor. But in the special case where the monitor IS a player (i.e., admin ghost), we base it off // the players eye rotation. We don't know what that is for sure, but we know their last grid angle, which // should work well enough? - if (TryComp(uid, out InputMoverComponent? mover)) - worldRot = mover.LastGridAngle; - else if (_mapManager.TryGetGrid(xform.GridUid, out var grid)) + if (_mapManager.TryGetGrid(xform.GridUid, out var grid)) worldRot = grid.WorldRotation; // update all sensors info diff --git a/Content.Server/Physics/Controllers/MoverController.cs b/Content.Server/Physics/Controllers/MoverController.cs index fa8df2b07b..a8fc3e298d 100644 --- a/Content.Server/Physics/Controllers/MoverController.cs +++ b/Content.Server/Physics/Controllers/MoverController.cs @@ -28,10 +28,23 @@ namespace Content.Server.Physics.Controllers var bodyQuery = GetEntityQuery(); var relayQuery = GetEntityQuery(); + var xformQuery = GetEntityQuery(); + var moverQuery = GetEntityQuery(); - foreach (var (mover, xform) in EntityQuery(true)) + foreach (var mover in EntityQuery(true)) { if (relayQuery.TryGetComponent(mover.Owner, out var relayed) && relayed.RelayEntity != null) + { + if (moverQuery.TryGetComponent(relayed.RelayEntity, out var relayMover)) + { + relayMover.RelativeEntity = mover.RelativeEntity; + relayMover.RelativeRotation = mover.RelativeRotation; + relayMover.TargetRelativeRotation = mover.TargetRelativeRotation; + continue; + } + } + + if (!xformQuery.TryGetComponent(mover.Owner, out var xform)) { continue; } @@ -46,18 +59,13 @@ namespace Content.Server.Physics.Controllers { continue; } - - if (TryComp(xform.ParentUid, out var parentMover)) - { - mover.LastGridAngle = parentMover.LastGridAngle; - } } else if (!bodyQuery.TryGetComponent(mover.Owner, out body)) { continue; } - HandleMobMovement(mover, body, xformMover, frameTime); + HandleMobMovement(mover, body, xformMover, frameTime, xformQuery); } HandleShuttleMovement(frameTime); diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 717709c3ef..1b866afb3c 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -908,6 +908,16 @@ namespace Content.Shared.CCVar * Shuttles */ + // Look this is technically eye behavior but its main impact is shuttles so I just dumped it here. + /// + /// If true then the camera will match the grid / map and is unchangeable. + /// - When traversing grids it will snap to 0 degrees rotation. + /// False means the player has control over the camera rotation. + /// - When traversing grids it will snap to the nearest cardinal which will generally be imperceptible. + /// + public static readonly CVarDef CameraRotationLocked = + CVarDef.Create("shuttle.camera_rotation_locked", true, CVar.REPLICATED); + /// /// Whether cargo shuttles are enabled. /// diff --git a/Content.Shared/Movement/Components/InputMoverComponent.cs b/Content.Shared/Movement/Components/InputMoverComponent.cs index c19a9959c6..b111f29c2c 100644 --- a/Content.Shared/Movement/Components/InputMoverComponent.cs +++ b/Content.Shared/Movement/Components/InputMoverComponent.cs @@ -40,8 +40,30 @@ namespace Content.Shared.Movement.Components public MoveButtons HeldMoveButtons = MoveButtons.None; + /// + /// Entity our movement is relative to. + /// + public EntityUid? RelativeEntity; + + /// + /// Although our movement might be relative to a particular entity we may have an additional relative rotation + /// e.g. if we've snapped to a different cardinal direction + /// [ViewVariables] - public Angle LastGridAngle { get; set; } = new(0); + public Angle TargetRelativeRotation = Angle.Zero; + + /// + /// The current relative rotation. This will lerp towards the . + /// + [ViewVariables] public Angle RelativeRotation; + + /// + /// If we traverse on / off a grid then set a timer to update our relative inputs. + /// + [ViewVariables(VVAccess.ReadWrite)] + public float LerpAccumulator; + + public const float LerpTime = 1.0f; public bool Sprinting => (HeldMoveButtons & MoveButtons.Walk) == 0x0; diff --git a/Content.Shared/Movement/Systems/SharedMoverController.Input.cs b/Content.Shared/Movement/Systems/SharedMoverController.Input.cs index 74af6c2f90..181c0fa5bb 100644 --- a/Content.Shared/Movement/Systems/SharedMoverController.Input.cs +++ b/Content.Shared/Movement/Systems/SharedMoverController.Input.cs @@ -2,7 +2,7 @@ using Content.Shared.CCVar; using Content.Shared.Input; using Content.Shared.Movement.Components; using Content.Shared.Movement.Events; -using Content.Shared.Shuttles.Components; +using Robust.Shared.Configuration; using Robust.Shared.GameStates; using Robust.Shared.Input; using Robust.Shared.Input.Binding; @@ -17,6 +17,8 @@ namespace Content.Shared.Movement.Systems /// public abstract partial class SharedMoverController { + public bool CameraRotationLocked { get; private set; } + private void InitializeInput() { var moveUpCmdHandler = new MoverDirInputCmdHandler(this, Direction.North); @@ -30,6 +32,9 @@ namespace Content.Shared.Movement.Systems .Bind(EngineKeyFunctions.MoveRight, moveRightCmdHandler) .Bind(EngineKeyFunctions.MoveDown, moveDownCmdHandler) .Bind(EngineKeyFunctions.Walk, new WalkInputCmdHandler(this)) + .Bind(EngineKeyFunctions.CameraRotateLeft, new CameraRotateInputCmdHandler(this, Direction.West)) + .Bind(EngineKeyFunctions.CameraRotateRight, new CameraRotateInputCmdHandler(this, Direction.East)) + .Bind(EngineKeyFunctions.CameraReset, new CameraResetInputCmdHandler(this)) // TODO: Relay // Shuttle .Bind(ContentKeyFunctions.ShuttleStrafeUp, new ShuttleInputCmdHandler(this, ShuttleButtons.StrafeUp)) @@ -44,6 +49,14 @@ namespace Content.Shared.Movement.Systems SubscribeLocalEvent(OnInputInit); SubscribeLocalEvent(OnInputGetState); SubscribeLocalEvent(OnInputHandleState); + SubscribeLocalEvent(OnInputParentChange); + + _configManager.OnValueChanged(CCVars.CameraRotationLocked, SetCameraRotationLocked, true); + } + + private void SetCameraRotationLocked(bool obj) + { + CameraRotationLocked = obj; } private void SetMoveInput(InputMoverComponent component, MoveButtons buttons) @@ -55,27 +68,98 @@ namespace Content.Shared.Movement.Systems private void OnInputHandleState(EntityUid uid, InputMoverComponent component, ref ComponentHandleState args) { - if (args.Current is not InputMoverComponentState state) return; + if (args.Current is not InputMoverComponentState state) + return; + component.HeldMoveButtons = state.Buttons; component.LastInputTick = GameTick.Zero; component.LastInputSubTick = 0; component.CanMove = state.CanMove; + + component.RelativeRotation = state.RelativeRotation; + component.TargetRelativeRotation = state.TargetRelativeRotation; + component.RelativeEntity = state.RelativeEntity; + component.LerpAccumulator = state.LerpAccumulator; } private void OnInputGetState(EntityUid uid, InputMoverComponent component, ref ComponentGetState args) { - args.State = new InputMoverComponentState(component.HeldMoveButtons, component.CanMove); + args.State = new InputMoverComponentState( + component.HeldMoveButtons, + component.CanMove, + component.RelativeRotation, + component.TargetRelativeRotation, + component.RelativeEntity, + component.LerpAccumulator); } private void ShutdownInput() { CommandBinds.Unregister(); + _configManager.UnsubValueChanged(CCVars.CameraRotationLocked, SetCameraRotationLocked); } public bool DiagonalMovementEnabled => _configManager.GetCVar(CCVars.GameDiagonalMovement); protected virtual void HandleShuttleInput(EntityUid uid, ShuttleButtons button, ushort subTick, bool state) {} + public void RotateCamera(EntityUid uid, Angle angle) + { + if (CameraRotationLocked || !TryComp(uid, out var mover)) + return; + + mover.TargetRelativeRotation += angle; + Dirty(mover); + } + + public void ResetCamera(EntityUid uid) + { + if (CameraRotationLocked || !TryComp(uid, out var mover) || mover.TargetRelativeRotation.Equals(Angle.Zero)) + return; + + mover.TargetRelativeRotation = Angle.Zero; + Dirty(mover); + } + + public Angle GetParentGridAngle(InputMoverComponent mover) + { + var rotation = mover.RelativeRotation; + + if (TryComp(mover.RelativeEntity, out var relativeXform)) + return (relativeXform.WorldRotation + rotation); + + return rotation; + } + + private void OnInputParentChange(EntityUid uid, InputMoverComponent component, ref EntParentChangedMessage args) + { + // If we change our grid / map then delay updating our LastGridAngle. + var relative = args.Transform.GridUid; + relative ??= args.Transform.MapUid; + + if (component.LifeStage < ComponentLifeStage.Running) + { + component.RelativeEntity = relative; + Dirty(component); + return; + } + + // If we go on a grid and back off then just reset the accumulator. + if (relative == component.RelativeEntity) + { + if (component.LerpAccumulator != 0f) + { + component.LerpAccumulator = 0f; + Dirty(component); + } + + return; + } + + component.LerpAccumulator = InputMoverComponent.LerpTime; + Dirty(component); + } + private void HandleDirChange(EntityUid entity, Direction dir, ushort subTick, bool state) { // Relayed movement just uses the same keybinds given we're moving the relayed entity @@ -124,9 +208,11 @@ namespace Content.Shared.Movement.Systems { var xform = Transform(uid); - if (!xform.ParentUid.IsValid()) return; + if (!xform.ParentUid.IsValid()) + return; - component.LastGridAngle = Transform(xform.ParentUid).WorldRotation; + component.RelativeEntity = xform.GridUid ?? xform.MapUid; + component.TargetRelativeRotation = Angle.Zero; } private void HandleRunChange(EntityUid uid, ushort subTick, bool walking) @@ -300,9 +386,53 @@ namespace Content.Shared.Movement.Systems return (buttons & flag) == flag; } + private sealed class CameraRotateInputCmdHandler : InputCmdHandler + { + private readonly SharedMoverController _controller; + private readonly Angle _angle; + + public CameraRotateInputCmdHandler(SharedMoverController controller, Direction direction) + { + _controller = controller; + _angle = direction.ToAngle(); + } + + public override bool HandleCmdMessage(ICommonSession? session, InputCmdMessage message) + { + if (message is not FullInputCmdMessage full || session?.AttachedEntity == null) return false; + + if (full.State != BoundKeyState.Up) + return false; + + _controller.RotateCamera(session.AttachedEntity.Value, _angle); + return false; + } + } + + private sealed class CameraResetInputCmdHandler : InputCmdHandler + { + private readonly SharedMoverController _controller; + + public CameraResetInputCmdHandler(SharedMoverController controller) + { + _controller = controller; + } + + public override bool HandleCmdMessage(ICommonSession? session, InputCmdMessage message) + { + if (message is not FullInputCmdMessage full || session?.AttachedEntity == null) return false; + + if (full.State != BoundKeyState.Up) + return false; + + _controller.ResetCamera(session.AttachedEntity.Value); + return false; + } + } + private sealed class MoverDirInputCmdHandler : InputCmdHandler { - private SharedMoverController _controller; + private readonly SharedMoverController _controller; private readonly Direction _dir; public MoverDirInputCmdHandler(SharedMoverController controller, Direction dir) @@ -344,17 +474,33 @@ namespace Content.Shared.Movement.Systems public MoveButtons Buttons { get; } public readonly bool CanMove; - public InputMoverComponentState(MoveButtons buttons, bool canMove) + /// + /// Our current rotation for movement purposes. This is lerping towards + /// + public Angle RelativeRotation; + + /// + /// Target rotation relative to the . Typically 0 + /// + public Angle TargetRelativeRotation; + public EntityUid? RelativeEntity; + public float LerpAccumulator = 0f; + + public InputMoverComponentState(MoveButtons buttons, bool canMove, Angle relativeRotation, Angle targetRelativeRotation, EntityUid? relativeEntity, float lerpAccumulator) { Buttons = buttons; CanMove = canMove; + RelativeRotation = relativeRotation; + TargetRelativeRotation = targetRelativeRotation; + RelativeEntity = relativeEntity; + LerpAccumulator = lerpAccumulator; } } private sealed class ShuttleInputCmdHandler : InputCmdHandler { - private SharedMoverController _controller; - private ShuttleButtons _button; + private readonly SharedMoverController _controller; + private readonly ShuttleButtons _button; public ShuttleInputCmdHandler(SharedMoverController controller, ShuttleButtons button) { diff --git a/Content.Shared/Movement/Systems/SharedMoverController.cs b/Content.Shared/Movement/Systems/SharedMoverController.cs index 40bf46003b..f99b19a780 100644 --- a/Content.Shared/Movement/Systems/SharedMoverController.cs +++ b/Content.Shared/Movement/Systems/SharedMoverController.cs @@ -44,6 +44,8 @@ namespace Content.Shared.Movement.Systems private const float FootstepVolume = 3f; private const float FootstepWalkingAddedVolumeMultiplier = 0f; + protected ISawmill Sawmill = default!; + /// /// /// @@ -59,6 +61,7 @@ namespace Content.Shared.Movement.Systems public override void Initialize() { base.Initialize(); + Sawmill = Logger.GetSawmill("mover"); InitializeFootsteps(); InitializeInput(); InitializeMob(); @@ -87,14 +90,6 @@ namespace Content.Shared.Movement.Systems UsedMobMovement.Clear(); } - protected Angle GetParentGridAngle(TransformComponent xform, InputMoverComponent mover) - { - if (!_mapManager.TryGetGrid(xform.GridUid, out var grid)) - return mover.LastGridAngle; - - return grid.WorldRotation; - } - /// /// Movement while considering actionblockers, weightlessness, etc. /// @@ -102,7 +97,8 @@ namespace Content.Shared.Movement.Systems InputMoverComponent mover, PhysicsComponent physicsComponent, TransformComponent xform, - float frameTime) + float frameTime, + EntityQuery xformQuery) { DebugTools.Assert(!UsedMobMovement.ContainsKey(mover.Owner)); @@ -134,12 +130,6 @@ namespace Content.Shared.Movement.Systems if (!touching && TryComp(xform.Owner, out var mobMover)) touching |= IsAroundCollider(PhysicsSystem, xform, mobMover, physicsComponent); } - - if (!touching) - { - if (xform.GridUid != null) - mover.LastGridAngle = GetParentGridAngle(xform, mover); - } } // Regular movement. @@ -152,7 +142,92 @@ namespace Content.Shared.Movement.Systems var total = walkDir * walkSpeed + sprintDir * sprintSpeed; - var parentRotation = GetParentGridAngle(xform, mover); + // Update relative movement + if (mover.LerpAccumulator > 0f) + { + Dirty(mover); + mover.LerpAccumulator -= frameTime; + + if (mover.LerpAccumulator <= 0f) + { + 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; + } + } + } + + var angleDiff = Angle.ShortestDistance(mover.RelativeRotation, mover.TargetRelativeRotation); + + // if we've just traversed then lerp to our target rotation. + if (!angleDiff.EqualsApprox(Angle.Zero, 0.005)) + { + var adjustment = angleDiff * 5f * frameTime; + var minAdjustment = 0.005 * frameTime; + + if (angleDiff < 0) + { + adjustment = Math.Min(adjustment, minAdjustment); + adjustment = Math.Clamp(adjustment, angleDiff, -angleDiff); + } + else + { + adjustment = Math.Max(adjustment, minAdjustment); + adjustment = Math.Clamp(adjustment, -angleDiff, angleDiff); + } + + mover.RelativeRotation += adjustment; + Dirty(mover); + } + else if (!angleDiff.Equals(Angle.Zero)) + { + mover.RelativeRotation = mover.TargetRelativeRotation; + Dirty(mover); + } + + var parentRotation = GetParentGridAngle(mover); var worldTotal = _relativeMovement ? parentRotation.RotateVec(total) : total; DebugTools.Assert(MathHelper.CloseToPercent(total.Length, worldTotal.Length)); @@ -190,17 +265,11 @@ namespace Content.Shared.Movement.Systems var minimumFrictionSpeed = moveSpeedComponent?.MinimumFrictionSpeed ?? MovementSpeedModifierComponent.DefaultMinimumFrictionSpeed; Friction(minimumFrictionSpeed, frameTime, friction, ref velocity); - if (xform.GridUid != EntityUid.Invalid) - mover.LastGridAngle = parentRotation; - if (worldTotal != Vector2.Zero) { // This should have its event run during island solver soooo xform.DeferUpdates = true; - - xform.LocalRotation = xform.GridUid != null - ? total.ToWorldAngle() - : worldTotal.ToWorldAngle(); + xform.WorldRotation = worldTotal.ToWorldAngle(); xform.DeferUpdates = false; if (!weightless && TryComp(mover.Owner, out var mobMover) && diff --git a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl index 1ebbd51992..c5ed8448ba 100644 --- a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl +++ b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl @@ -66,6 +66,7 @@ ui-options-bind-reset = Reset ui-options-key-prompt = Press a key... ui-options-header-movement = Movement +ui-options-header-camera = Camera ui-options-header-interaction-basic = Basic Interaction ui-options-header-interaction-adv = Advanced Interaction ui-options-header-ui = User Interface @@ -84,6 +85,10 @@ ui-options-function-move-down = Move Down ui-options-function-move-right = Move Right ui-options-function-walk = Walk +ui-options-function-camera-rotate-left = Rotate left +ui-options-function-camera-rotate-right = Rotate right +ui-options-function-camera-reset = Reset + ui-options-function-use = Use ui-options-function-wide-attack = Wide attack ui-options-function-activate-item-in-hand = Activate item in hand diff --git a/Resources/keybinds.yml b/Resources/keybinds.yml index a6fe73288f..1ad3947021 100644 --- a/Resources/keybinds.yml +++ b/Resources/keybinds.yml @@ -61,13 +61,17 @@ binds: - function: ShuttleBrake type: State key: Space - +# Camera - function: CameraRotateLeft type: State key: NumpadNum7 - function: CameraRotateRight type: State key: NumpadNum9 +- function: CameraReset + type: State + key: NumpadNum8 +# Misc - function: ShowEscapeMenu type: State key: Escape