diff --git a/Content.Client/Eye/EyeLerpingSystem.cs b/Content.Client/Eye/EyeLerpingSystem.cs new file mode 100644 index 0000000000..92cde50108 --- /dev/null +++ b/Content.Client/Eye/EyeLerpingSystem.cs @@ -0,0 +1,123 @@ +using System; +using Content.Shared.Movement.Components; +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.Maths; +using Robust.Shared.Timing; + +namespace Content.Client.Eye; + +public class EyeLerpingSystem : EntitySystem +{ + [Dependency] private readonly IEyeManager _eyeManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + + private Angle? _lastGridAngle; + private Angle? _lerpTo; + private Angle _lerpStartRotation; + private float _accumulator; + + // How fast the camera rotates in radians / s + private const float CameraRotateSpeed = MathF.PI; + + // Safety override + private const float LerpTimeMax = 1.5f; + + + public override void Initialize() + { + base.Initialize(); + + UpdatesAfter.Add(typeof(TransformSystem)); + UpdatesAfter.Add(typeof(PhysicsSystem)); + UpdatesBefore.Add(typeof(EyeUpdateSystem)); + } + + public override void FrameUpdate(float frameTime) + { + if (!_gameTiming.IsFirstTimePredicted) + return; + + var currentEye = _eyeManager.CurrentEye; + + if (_playerManager.LocalPlayer?.ControlledEntity is not {} mob || Deleted(mob)) + return; + + // We can't lerp if the mob can't move! + if (!TryComp(mob, out IMoverComponent? mover)) + return; + + var moverLastGridAngle = mover.LastGridAngle; + + // Let's not turn the camera into a washing machine when the game starts. + if (_lastGridAngle == null) + { + _lastGridAngle = moverLastGridAngle; + currentEye.Rotation = -moverLastGridAngle; + return; + } + + // Check if the last lerp grid angle we have is not the same as the last mover grid angle... + if (!_lastGridAngle.Value.EqualsApprox(moverLastGridAngle)) + { + // And now, we start lerping. + _lerpTo = moverLastGridAngle; + _lastGridAngle = moverLastGridAngle; + _lerpStartRotation = currentEye.Rotation; + _accumulator = 0f; + } + + if (_lerpTo != null) + { + _accumulator += frameTime; + + var lerpRot = -_lerpTo.Value.FlipPositive().Reduced(); + var startRot = _lerpStartRotation.FlipPositive().Reduced(); + + var changeNeeded = Angle.ShortestDistance(startRot, lerpRot); + + if (changeNeeded.EqualsApprox(Angle.Zero)) + { + // Nothing to do here! + CleanupLerp(); + 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)) * _accumulator * Math.Sign(changeNeeded); + + // How close is this from reaching the end? + var percentage = (float)Math.Abs(changeRot / changeNeeded); + + currentEye.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 || _accumulator >= LerpTimeMax) + { + CleanupLerp(); + } + + void CleanupLerp() + { + currentEye.Rotation = -_lerpTo.Value; + _lerpStartRotation = currentEye.Rotation; + _lerpTo = null; + _accumulator = 0f; + } + } + 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. + currentEye.Rotation = -moverLastGridAngle; + } + } +} diff --git a/Content.Client/Physics/Controllers/MoverController.cs b/Content.Client/Physics/Controllers/MoverController.cs index e9fbeafee1..2979af236b 100644 --- a/Content.Client/Physics/Controllers/MoverController.cs +++ b/Content.Client/Physics/Controllers/MoverController.cs @@ -5,6 +5,7 @@ using Content.Shared.Pulling.Components; using Robust.Client.Player; using Robust.Shared.GameObjects; using Robust.Shared.IoC; +using Robust.Shared.Map; using Robust.Shared.Physics; namespace Content.Client.Physics.Controllers @@ -19,11 +20,15 @@ namespace Content.Client.Physics.Controllers if (_playerManager.LocalPlayer?.ControlledEntity is not {Valid: true} player || !EntityManager.TryGetComponent(player, out IMoverComponent? mover) || - !EntityManager.TryGetComponent(player, out PhysicsComponent? body)) + !EntityManager.TryGetComponent(player, out PhysicsComponent? body) || + !EntityManager.TryGetComponent(player, out TransformComponent? xform)) { return; } + if (xform.GridID != GridId.Invalid) + 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. diff --git a/Content.Shared/Movement/Components/SharedPlayerInputMoverComponent.cs b/Content.Shared/Movement/Components/SharedPlayerInputMoverComponent.cs index 8dfdd9aaf6..f07b9651aa 100644 --- a/Content.Shared/Movement/Components/SharedPlayerInputMoverComponent.cs +++ b/Content.Shared/Movement/Components/SharedPlayerInputMoverComponent.cs @@ -50,6 +50,7 @@ namespace Content.Shared.Movement.Components private MoveButtons _heldMoveButtons = MoveButtons.None; + [ViewVariables] public Angle LastGridAngle { get; set; } = new(0); public float CurrentWalkSpeed => _movementSpeed?.CurrentWalkSpeed ?? MovementSpeedModifierComponent.DefaultBaseWalkSpeed; diff --git a/Content.Shared/Movement/SharedMoverController.cs b/Content.Shared/Movement/SharedMoverController.cs index 363f8a19d0..56bc2838af 100644 --- a/Content.Shared/Movement/SharedMoverController.cs +++ b/Content.Shared/Movement/SharedMoverController.cs @@ -5,6 +5,7 @@ using Content.Shared.Friction; using Content.Shared.MobState.Components; using Content.Shared.Movement.Components; using Content.Shared.Pulling.Components; +using JetBrains.Annotations; using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.IoC; @@ -60,6 +61,14 @@ namespace Content.Shared.Movement UsedMobMovement.Clear(); } + protected Angle GetParentGridAngle(TransformComponent xform, IMoverComponent mover) + { + if (xform.GridID == GridId.Invalid || !_mapManager.TryGetGrid(xform.GridID, out var grid)) + return mover.LastGridAngle; + + return grid.WorldRotation; + } + /// /// A generic kinematic mover for entities. /// @@ -68,7 +77,7 @@ namespace Content.Shared.Movement var (walkDir, sprintDir) = mover.VelocityDir; var transform = EntityManager.GetComponent(mover.Owner); - var parentRotation = transform.Parent!.WorldRotation; + var parentRotation = GetParentGridAngle(transform, mover); // Regular movement. // Target velocity. @@ -118,7 +127,7 @@ namespace Content.Shared.Movement if (!touching) { if (transform.GridID != GridId.Invalid) - mover.LastGridAngle = transform.Parent!.WorldRotation; + mover.LastGridAngle = GetParentGridAngle(transform, mover); transform.WorldRotation = physicsComponent.LinearVelocity.GetDir().ToAngle(); return; @@ -130,7 +139,7 @@ namespace Content.Shared.Movement // This is relative to the map / grid we're on. var total = walkDir * mover.CurrentWalkSpeed + sprintDir * mover.CurrentSprintSpeed; - var parentRotation = transform.Parent!.WorldRotation; + var parentRotation = GetParentGridAngle(transform, mover); var worldTotal = _relativeMovement ? parentRotation.RotateVec(total) : total;