Smooth docking traversal (#10822)

This commit is contained in:
metalgearsloth 2022-08-29 15:05:53 +10:00 committed by GitHub
parent 0ebc733b3a
commit 5b42861539
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 515 additions and 204 deletions

View File

@ -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);

View File

@ -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<EntityUid, EyeLerpInformation> _activeEyes = new();
private readonly List<EntityUid> _toRemove = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<EyeComponent, ComponentStartup>(OnEyeStartup);
SubscribeLocalEvent<EyeComponent, ComponentShutdown>(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<TransformComponent>();
TryComp<InputMoverComponent>(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<InputMoverComponent>();
var xformQuery = GetEntityQuery<TransformComponent>();
var foundEyes = new ValueList<EntityUid>(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<TransformComponent> 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<EyeComponent>();
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;
}
/// <summary>
/// If we go to a new map then don't lerp and snap instantly.
/// </summary>
public MapId MapId;
}
}

View File

@ -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<RelayInputMoverComponent>(player, out var relayMover))
{
if (relayMover.RelayEntity != null)
{
if (TryComp<InputMoverComponent>(player, out var mover) &&
TryComp<InputMoverComponent>(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<TransformComponent>();
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<InputMoverComponent>(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()

View File

@ -44,7 +44,7 @@ namespace Content.Server.Administration.Commands
var coordinates = player.AttachedEntity != null
? _entities.GetComponent<TransformComponent>(player.AttachedEntity.Value).Coordinates
: EntitySystem.Get<GameTicker>().GetObserverSpawnPoint();
var ghost = _entities.SpawnEntity("AdminObserver", coordinates.ToMap(_entities));
var ghost = _entities.SpawnEntity("AdminObserver", coordinates);
if (canReturn)
{

View File

@ -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);
}

View File

@ -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;
/// <summary>
/// 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<SpawnPointComponent, TransformComponent>(true))
{
if (point.SpawnType == SpawnPointType.Observer)
_possiblePositions.Add(transform.Coordinates);
if (point.SpawnType != SpawnPointType.Observer)
continue;
_possiblePositions.Add(transform.Coordinates);
}
var metaQuery = GetEntityQuery<MetaDataComponent>();
// 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
}

View File

@ -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

View File

@ -28,10 +28,23 @@ namespace Content.Server.Physics.Controllers
var bodyQuery = GetEntityQuery<PhysicsComponent>();
var relayQuery = GetEntityQuery<RelayInputMoverComponent>();
var xformQuery = GetEntityQuery<TransformComponent>();
var moverQuery = GetEntityQuery<InputMoverComponent>();
foreach (var (mover, xform) in EntityQuery<InputMoverComponent, TransformComponent>(true))
foreach (var mover in EntityQuery<InputMoverComponent>(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<InputMoverComponent>(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);

View File

@ -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.
/// <summary>
/// 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.
/// </summary>
public static readonly CVarDef<bool> CameraRotationLocked =
CVarDef.Create("shuttle.camera_rotation_locked", true, CVar.REPLICATED);
/// <summary>
/// Whether cargo shuttles are enabled.
/// </summary>

View File

@ -40,8 +40,30 @@ namespace Content.Shared.Movement.Components
public MoveButtons HeldMoveButtons = MoveButtons.None;
/// <summary>
/// Entity our movement is relative to.
/// </summary>
public EntityUid? RelativeEntity;
/// <summary>
/// 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
/// </summary>
[ViewVariables]
public Angle LastGridAngle { get; set; } = new(0);
public Angle TargetRelativeRotation = Angle.Zero;
/// <summary>
/// The current relative rotation. This will lerp towards the <see cref="TargetRelativeRotation"/>.
/// </summary>
[ViewVariables] public Angle RelativeRotation;
/// <summary>
/// If we traverse on / off a grid then set a timer to update our relative inputs.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float LerpAccumulator;
public const float LerpTime = 1.0f;
public bool Sprinting => (HeldMoveButtons & MoveButtons.Walk) == 0x0;

View File

@ -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
/// </summary>
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<InputMoverComponent, ComponentInit>(OnInputInit);
SubscribeLocalEvent<InputMoverComponent, ComponentGetState>(OnInputGetState);
SubscribeLocalEvent<InputMoverComponent, ComponentHandleState>(OnInputHandleState);
SubscribeLocalEvent<InputMoverComponent, EntParentChangedMessage>(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<SharedMoverController>();
_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<InputMoverComponent>(uid, out var mover))
return;
mover.TargetRelativeRotation += angle;
Dirty(mover);
}
public void ResetCamera(EntityUid uid)
{
if (CameraRotationLocked || !TryComp<InputMoverComponent>(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<TransformComponent>(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)
/// <summary>
/// Our current rotation for movement purposes. This is lerping towards <see cref="TargetRelativeRotation"/>
/// </summary>
public Angle RelativeRotation;
/// <summary>
/// Target rotation relative to the <see cref="RelativeEntity"/>. Typically 0
/// </summary>
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)
{

View File

@ -44,6 +44,8 @@ namespace Content.Shared.Movement.Systems
private const float FootstepVolume = 3f;
private const float FootstepWalkingAddedVolumeMultiplier = 0f;
protected ISawmill Sawmill = default!;
/// <summary>
/// <see cref="CCVars.StopSpeed"/>
/// </summary>
@ -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;
}
/// <summary>
/// Movement while considering actionblockers, weightlessness, etc.
/// </summary>
@ -102,7 +97,8 @@ namespace Content.Shared.Movement.Systems
InputMoverComponent mover,
PhysicsComponent physicsComponent,
TransformComponent xform,
float frameTime)
float frameTime,
EntityQuery<TransformComponent> xformQuery)
{
DebugTools.Assert(!UsedMobMovement.ContainsKey(mover.Owner));
@ -134,12 +130,6 @@ namespace Content.Shared.Movement.Systems
if (!touching && TryComp<MobMoverComponent>(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<MobMoverComponent>(mover.Owner, out var mobMover) &&

View File

@ -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

View File

@ -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