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
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.MoveRight);
AddButton(EngineKeyFunctions.Walk); AddButton(EngineKeyFunctions.Walk);
AddHeader("ui-options-header-camera");
AddButton(EngineKeyFunctions.CameraRotateLeft);
AddButton(EngineKeyFunctions.CameraRotateRight);
AddButton(EngineKeyFunctions.CameraReset);
AddHeader("ui-options-header-interaction-basic"); AddHeader("ui-options-header-interaction-basic");
AddButton(EngineKeyFunctions.Use); AddButton(EngineKeyFunctions.Use);
AddButton(ContentKeyFunctions.UseItemInHand); AddButton(ContentKeyFunctions.UseItemInHand);

View File

@@ -1,41 +1,31 @@
using System;
using Content.Shared.Movement.Components; using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Client.Physics; using Robust.Client.Physics;
using Robust.Client.Player; using Robust.Client.Player;
using Robust.Shared.GameObjects; using Robust.Shared.Collections;
using Robust.Shared.IoC;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client.Eye; namespace Content.Client.Eye;
public sealed class EyeLerpingSystem : EntitySystem public sealed class EyeLerpingSystem : EntitySystem
{ {
[Dependency] private readonly IEyeManager _eyeManager = default!; [Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly SharedMoverController _mover = 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();
// Eyes other than the primary eye that are currently active. // Eyes other than the primary eye that are currently active.
private readonly Dictionary<EntityUid, EyeLerpInformation> _activeEyes = new(); private readonly Dictionary<EntityUid, EyeLerpInformation> _activeEyes = new();
private readonly List<EntityUid> _toRemove = new();
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<EyeComponent, ComponentStartup>(OnEyeStartup);
SubscribeLocalEvent<EyeComponent, ComponentShutdown>(OnEyeShutdown); SubscribeLocalEvent<EyeComponent, ComponentShutdown>(OnEyeShutdown);
UpdatesAfter.Add(typeof(TransformSystem)); UpdatesAfter.Add(typeof(TransformSystem));
@@ -43,6 +33,27 @@ public sealed class EyeLerpingSystem : EntitySystem
UpdatesBefore.Add(typeof(EyeUpdateSystem)); 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) private void OnEyeShutdown(EntityUid uid, EyeComponent component, ComponentShutdown args)
{ {
RemoveEye(uid); RemoveEye(uid);
@@ -50,145 +61,131 @@ public sealed class EyeLerpingSystem : EntitySystem
public void AddEye(EntityUid uid) public void AddEye(EntityUid uid)
{ {
if (!_activeEyes.ContainsKey(uid)) _activeEyes.TryAdd(uid, new EyeLerpInformation());
{
_activeEyes.Add(uid, new());
}
} }
public void RemoveEye(EntityUid uid) 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) public override void FrameUpdate(float frameTime)
{ {
if (!_gameTiming.IsFirstTimePredicted) var tickFraction = (float) _gameTiming.TickFraction / ushort.MaxValue;
return; var lerpMinimum = 0.01;
// Always do this one. foreach (var (eye, entity) in GetEyes())
LerpPlayerEye(frameTime);
foreach (var (entity, info) in _activeEyes)
{ {
LerpEntityEye(entity, info, frameTime); if (!_activeEyes.TryGetValue(entity, out var lerpInfo))
} continue;
if (_toRemove.Count != 0) var shortest = Angle.ShortestDistance(lerpInfo.LastRotation, lerpInfo.TargetRotation);
{
foreach (var entity in _toRemove) if (Math.Abs(shortest.Theta) < lerpMinimum)
{ {
RemoveEye(entity); eye.Rotation = lerpInfo.TargetRotation;
continue;
} }
_toRemove.Clear(); eye.Rotation = shortest * tickFraction + lerpInfo.LastRotation;
}
}
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;
} }
} }
private sealed class EyeLerpInformation private sealed class EyeLerpInformation
{ {
public Angle? LastGridAngle { get; set; } public Angle LastRotation;
public Angle? LerpTo { get; set; } public Angle TargetRotation;
public Angle LerpStartRotation { get; set; }
public float Accumulator { get; set; }
public void Cleanup(IEye eye) /// <summary>
{ /// If we go to a new map then don't lerp and snap instantly.
eye.Rotation = -LerpTo ?? Angle.Zero; /// </summary>
LerpStartRotation = eye.Rotation; public MapId MapId;
LerpTo = null;
Accumulator = 0;
}
} }
} }

View File

@@ -3,7 +3,6 @@ using Content.Shared.Movement.Systems;
using Content.Shared.Pulling.Components; using Content.Shared.Pulling.Components;
using Robust.Client.Player; using Robust.Client.Player;
using Robust.Shared.Physics; using Robust.Shared.Physics;
using Robust.Shared.Player;
using Robust.Shared.Timing; using Robust.Shared.Timing;
namespace Content.Client.Physics.Controllers namespace Content.Client.Physics.Controllers
@@ -23,7 +22,17 @@ namespace Content.Client.Physics.Controllers
if (TryComp<RelayInputMoverComponent>(player, out var relayMover)) if (TryComp<RelayInputMoverComponent>(player, out var relayMover))
{ {
if (relayMover.RelayEntity != null) 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(relayMover.RelayEntity.Value, frameTime);
}
} }
HandleClientsideMovement(player, frameTime); HandleClientsideMovement(player, frameTime);
@@ -31,8 +40,10 @@ namespace Content.Client.Physics.Controllers
private void HandleClientsideMovement(EntityUid player, float frameTime) private void HandleClientsideMovement(EntityUid player, float frameTime)
{ {
var xformQuery = GetEntityQuery<TransformComponent>();
if (!TryComp(player, out InputMoverComponent? mover) || if (!TryComp(player, out InputMoverComponent? mover) ||
!TryComp(player, out TransformComponent? xform)) !xformQuery.TryGetComponent(player, out var xform))
{ {
return; return;
} }
@@ -47,20 +58,12 @@ namespace Content.Client.Physics.Controllers
{ {
return; return;
} }
if (TryComp<InputMoverComponent>(xform.ParentUid, out var parentMover))
{
mover.LastGridAngle = parentMover.LastGridAngle;
}
} }
else if (!TryComp(player, out body)) else if (!TryComp(player, out body))
{ {
return; 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 // 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). // (i.e. only see what the server has sent us).
// The exception to this is joints. // 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 // 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() protected override bool CanSound()

View File

@@ -44,7 +44,7 @@ namespace Content.Server.Administration.Commands
var coordinates = player.AttachedEntity != null var coordinates = player.AttachedEntity != null
? _entities.GetComponent<TransformComponent>(player.AttachedEntity.Value).Coordinates ? _entities.GetComponent<TransformComponent>(player.AttachedEntity.Value).Coordinates
: EntitySystem.Get<GameTicker>().GetObserverSpawnPoint(); : EntitySystem.Get<GameTicker>().GetObserverSpawnPoint();
var ghost = _entities.SpawnEntity("AdminObserver", coordinates.ToMap(_entities)); var ghost = _entities.SpawnEntity("AdminObserver", coordinates);
if (canReturn) 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 (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)); RaiseLocalEvent(new PostGameMapLoad(map, targetMapId, entities, gridUids, stationName));
_spawnPoint = _mapManager.GetGrid(gridIds[0]).ToCoordinates();
return (entities, gridUids); return (entities, gridUids);
} }

View File

@@ -25,9 +25,6 @@ namespace Content.Server.GameTicking
{ {
private const string ObserverPrototypeName = "MobObserver"; 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> /// <summary>
/// How many players have joined the round through normal methods. /// 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. /// 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 #region Spawn Points
public EntityCoordinates GetObserverSpawnPoint() 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(); _possiblePositions.Clear();
foreach (var (point, transform) in EntityManager.EntityQuery<SpawnPointComponent, TransformComponent>(true)) foreach (var (point, transform) in EntityManager.EntityQuery<SpawnPointComponent, TransformComponent>(true))
{ {
if (point.SpawnType == SpawnPointType.Observer) if (point.SpawnType != SpawnPointType.Observer)
_possiblePositions.Add(transform.Coordinates); 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) 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 #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 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 // 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? // should work well enough?
if (TryComp(uid, out InputMoverComponent? mover)) if (_mapManager.TryGetGrid(xform.GridUid, out var grid))
worldRot = mover.LastGridAngle;
else if (_mapManager.TryGetGrid(xform.GridUid, out var grid))
worldRot = grid.WorldRotation; worldRot = grid.WorldRotation;
// update all sensors info // update all sensors info

View File

@@ -28,10 +28,23 @@ namespace Content.Server.Physics.Controllers
var bodyQuery = GetEntityQuery<PhysicsComponent>(); var bodyQuery = GetEntityQuery<PhysicsComponent>();
var relayQuery = GetEntityQuery<RelayInputMoverComponent>(); 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 (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; continue;
} }
@@ -46,18 +59,13 @@ namespace Content.Server.Physics.Controllers
{ {
continue; continue;
} }
if (TryComp<InputMoverComponent>(xform.ParentUid, out var parentMover))
{
mover.LastGridAngle = parentMover.LastGridAngle;
}
} }
else if (!bodyQuery.TryGetComponent(mover.Owner, out body)) else if (!bodyQuery.TryGetComponent(mover.Owner, out body))
{ {
continue; continue;
} }
HandleMobMovement(mover, body, xformMover, frameTime); HandleMobMovement(mover, body, xformMover, frameTime, xformQuery);
} }
HandleShuttleMovement(frameTime); HandleShuttleMovement(frameTime);

View File

@@ -908,6 +908,16 @@ namespace Content.Shared.CCVar
* Shuttles * 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> /// <summary>
/// Whether cargo shuttles are enabled. /// Whether cargo shuttles are enabled.
/// </summary> /// </summary>

View File

@@ -40,8 +40,30 @@ namespace Content.Shared.Movement.Components
public MoveButtons HeldMoveButtons = MoveButtons.None; 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] [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; public bool Sprinting => (HeldMoveButtons & MoveButtons.Walk) == 0x0;

View File

@@ -2,7 +2,7 @@ using Content.Shared.CCVar;
using Content.Shared.Input; using Content.Shared.Input;
using Content.Shared.Movement.Components; using Content.Shared.Movement.Components;
using Content.Shared.Movement.Events; using Content.Shared.Movement.Events;
using Content.Shared.Shuttles.Components; using Robust.Shared.Configuration;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Input; using Robust.Shared.Input;
using Robust.Shared.Input.Binding; using Robust.Shared.Input.Binding;
@@ -17,6 +17,8 @@ namespace Content.Shared.Movement.Systems
/// </summary> /// </summary>
public abstract partial class SharedMoverController public abstract partial class SharedMoverController
{ {
public bool CameraRotationLocked { get; private set; }
private void InitializeInput() private void InitializeInput()
{ {
var moveUpCmdHandler = new MoverDirInputCmdHandler(this, Direction.North); var moveUpCmdHandler = new MoverDirInputCmdHandler(this, Direction.North);
@@ -30,6 +32,9 @@ namespace Content.Shared.Movement.Systems
.Bind(EngineKeyFunctions.MoveRight, moveRightCmdHandler) .Bind(EngineKeyFunctions.MoveRight, moveRightCmdHandler)
.Bind(EngineKeyFunctions.MoveDown, moveDownCmdHandler) .Bind(EngineKeyFunctions.MoveDown, moveDownCmdHandler)
.Bind(EngineKeyFunctions.Walk, new WalkInputCmdHandler(this)) .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 // TODO: Relay
// Shuttle // Shuttle
.Bind(ContentKeyFunctions.ShuttleStrafeUp, new ShuttleInputCmdHandler(this, ShuttleButtons.StrafeUp)) .Bind(ContentKeyFunctions.ShuttleStrafeUp, new ShuttleInputCmdHandler(this, ShuttleButtons.StrafeUp))
@@ -44,6 +49,14 @@ namespace Content.Shared.Movement.Systems
SubscribeLocalEvent<InputMoverComponent, ComponentInit>(OnInputInit); SubscribeLocalEvent<InputMoverComponent, ComponentInit>(OnInputInit);
SubscribeLocalEvent<InputMoverComponent, ComponentGetState>(OnInputGetState); SubscribeLocalEvent<InputMoverComponent, ComponentGetState>(OnInputGetState);
SubscribeLocalEvent<InputMoverComponent, ComponentHandleState>(OnInputHandleState); 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) 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) 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.HeldMoveButtons = state.Buttons;
component.LastInputTick = GameTick.Zero; component.LastInputTick = GameTick.Zero;
component.LastInputSubTick = 0; component.LastInputSubTick = 0;
component.CanMove = state.CanMove; 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) 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() private void ShutdownInput()
{ {
CommandBinds.Unregister<SharedMoverController>(); CommandBinds.Unregister<SharedMoverController>();
_configManager.UnsubValueChanged(CCVars.CameraRotationLocked, SetCameraRotationLocked);
} }
public bool DiagonalMovementEnabled => _configManager.GetCVar(CCVars.GameDiagonalMovement); public bool DiagonalMovementEnabled => _configManager.GetCVar(CCVars.GameDiagonalMovement);
protected virtual void HandleShuttleInput(EntityUid uid, ShuttleButtons button, ushort subTick, bool state) {} 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) 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 // 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); 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) private void HandleRunChange(EntityUid uid, ushort subTick, bool walking)
@@ -300,9 +386,53 @@ namespace Content.Shared.Movement.Systems
return (buttons & flag) == flag; 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 sealed class MoverDirInputCmdHandler : InputCmdHandler
{ {
private SharedMoverController _controller; private readonly SharedMoverController _controller;
private readonly Direction _dir; private readonly Direction _dir;
public MoverDirInputCmdHandler(SharedMoverController controller, Direction dir) public MoverDirInputCmdHandler(SharedMoverController controller, Direction dir)
@@ -344,17 +474,33 @@ namespace Content.Shared.Movement.Systems
public MoveButtons Buttons { get; } public MoveButtons Buttons { get; }
public readonly bool CanMove; 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; Buttons = buttons;
CanMove = canMove; CanMove = canMove;
RelativeRotation = relativeRotation;
TargetRelativeRotation = targetRelativeRotation;
RelativeEntity = relativeEntity;
LerpAccumulator = lerpAccumulator;
} }
} }
private sealed class ShuttleInputCmdHandler : InputCmdHandler private sealed class ShuttleInputCmdHandler : InputCmdHandler
{ {
private SharedMoverController _controller; private readonly SharedMoverController _controller;
private ShuttleButtons _button; private readonly ShuttleButtons _button;
public ShuttleInputCmdHandler(SharedMoverController controller, 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 FootstepVolume = 3f;
private const float FootstepWalkingAddedVolumeMultiplier = 0f; private const float FootstepWalkingAddedVolumeMultiplier = 0f;
protected ISawmill Sawmill = default!;
/// <summary> /// <summary>
/// <see cref="CCVars.StopSpeed"/> /// <see cref="CCVars.StopSpeed"/>
/// </summary> /// </summary>
@@ -59,6 +61,7 @@ namespace Content.Shared.Movement.Systems
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
Sawmill = Logger.GetSawmill("mover");
InitializeFootsteps(); InitializeFootsteps();
InitializeInput(); InitializeInput();
InitializeMob(); InitializeMob();
@@ -87,14 +90,6 @@ namespace Content.Shared.Movement.Systems
UsedMobMovement.Clear(); UsedMobMovement.Clear();
} }
protected Angle GetParentGridAngle(TransformComponent xform, InputMoverComponent mover)
{
if (!_mapManager.TryGetGrid(xform.GridUid, out var grid))
return mover.LastGridAngle;
return grid.WorldRotation;
}
/// <summary> /// <summary>
/// Movement while considering actionblockers, weightlessness, etc. /// Movement while considering actionblockers, weightlessness, etc.
/// </summary> /// </summary>
@@ -102,7 +97,8 @@ namespace Content.Shared.Movement.Systems
InputMoverComponent mover, InputMoverComponent mover,
PhysicsComponent physicsComponent, PhysicsComponent physicsComponent,
TransformComponent xform, TransformComponent xform,
float frameTime) float frameTime,
EntityQuery<TransformComponent> xformQuery)
{ {
DebugTools.Assert(!UsedMobMovement.ContainsKey(mover.Owner)); DebugTools.Assert(!UsedMobMovement.ContainsKey(mover.Owner));
@@ -134,12 +130,6 @@ namespace Content.Shared.Movement.Systems
if (!touching && TryComp<MobMoverComponent>(xform.Owner, out var mobMover)) if (!touching && TryComp<MobMoverComponent>(xform.Owner, out var mobMover))
touching |= IsAroundCollider(PhysicsSystem, xform, mobMover, physicsComponent); touching |= IsAroundCollider(PhysicsSystem, xform, mobMover, physicsComponent);
} }
if (!touching)
{
if (xform.GridUid != null)
mover.LastGridAngle = GetParentGridAngle(xform, mover);
}
} }
// Regular movement. // Regular movement.
@@ -152,7 +142,92 @@ namespace Content.Shared.Movement.Systems
var total = walkDir * walkSpeed + sprintDir * sprintSpeed; 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; var worldTotal = _relativeMovement ? parentRotation.RotateVec(total) : total;
DebugTools.Assert(MathHelper.CloseToPercent(total.Length, worldTotal.Length)); DebugTools.Assert(MathHelper.CloseToPercent(total.Length, worldTotal.Length));
@@ -190,17 +265,11 @@ namespace Content.Shared.Movement.Systems
var minimumFrictionSpeed = moveSpeedComponent?.MinimumFrictionSpeed ?? MovementSpeedModifierComponent.DefaultMinimumFrictionSpeed; var minimumFrictionSpeed = moveSpeedComponent?.MinimumFrictionSpeed ?? MovementSpeedModifierComponent.DefaultMinimumFrictionSpeed;
Friction(minimumFrictionSpeed, frameTime, friction, ref velocity); Friction(minimumFrictionSpeed, frameTime, friction, ref velocity);
if (xform.GridUid != EntityUid.Invalid)
mover.LastGridAngle = parentRotation;
if (worldTotal != Vector2.Zero) if (worldTotal != Vector2.Zero)
{ {
// This should have its event run during island solver soooo // This should have its event run during island solver soooo
xform.DeferUpdates = true; xform.DeferUpdates = true;
xform.WorldRotation = worldTotal.ToWorldAngle();
xform.LocalRotation = xform.GridUid != null
? total.ToWorldAngle()
: worldTotal.ToWorldAngle();
xform.DeferUpdates = false; xform.DeferUpdates = false;
if (!weightless && TryComp<MobMoverComponent>(mover.Owner, out var mobMover) && 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-key-prompt = Press a key...
ui-options-header-movement = Movement ui-options-header-movement = Movement
ui-options-header-camera = Camera
ui-options-header-interaction-basic = Basic Interaction ui-options-header-interaction-basic = Basic Interaction
ui-options-header-interaction-adv = Advanced Interaction ui-options-header-interaction-adv = Advanced Interaction
ui-options-header-ui = User Interface 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-move-right = Move Right
ui-options-function-walk = Walk 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-use = Use
ui-options-function-wide-attack = Wide attack ui-options-function-wide-attack = Wide attack
ui-options-function-activate-item-in-hand = Activate item in hand ui-options-function-activate-item-in-hand = Activate item in hand

View File

@@ -61,13 +61,17 @@ binds:
- function: ShuttleBrake - function: ShuttleBrake
type: State type: State
key: Space key: Space
# Camera
- function: CameraRotateLeft - function: CameraRotateLeft
type: State type: State
key: NumpadNum7 key: NumpadNum7
- function: CameraRotateRight - function: CameraRotateRight
type: State type: State
key: NumpadNum9 key: NumpadNum9
- function: CameraReset
type: State
key: NumpadNum8
# Misc
- function: ShowEscapeMenu - function: ShowEscapeMenu
type: State type: State
key: Escape key: Escape