Add sprite movement states (#22940)

* Add sprite movement states

Did it for borgs for reference for future implementations.

* Fix

* Fix prediction issue

* review
This commit is contained in:
metalgearsloth
2023-12-25 18:34:21 +11:00
committed by GitHub
parent 721a445bbd
commit 9a40cf81f5
13 changed files with 227 additions and 25 deletions

View File

@@ -0,0 +1,51 @@
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Events;
using Content.Shared.Movement.Systems;
using Robust.Client.GameObjects;
using Robust.Shared.Timing;
namespace Content.Client.Movement.Systems;
/// <summary>
/// Handles setting sprite states based on whether an entity has movement input.
/// </summary>
public sealed class SpriteMovementSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
private EntityQuery<SpriteComponent> _spriteQuery;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SpriteMovementComponent, MoveInputEvent>(OnSpriteMoveInput);
_spriteQuery = GetEntityQuery<SpriteComponent>();
}
private void OnSpriteMoveInput(EntityUid uid, SpriteMovementComponent component, ref MoveInputEvent args)
{
if (!_timing.IsFirstTimePredicted)
return;
var oldMoving = SharedMoverController.GetNormalizedMovement(args.OldMovement) != MoveButtons.None;
var moving = SharedMoverController.GetNormalizedMovement(args.Component.HeldMoveButtons) != MoveButtons.None;
if (oldMoving == moving || !_spriteQuery.TryGetComponent(uid, out var sprite))
return;
if (moving)
{
foreach (var (layer, state) in component.MovementLayers)
{
sprite.LayerSetData(layer, state);
}
}
else
{
foreach (var (layer, state) in component.NoMovementLayers)
{
sprite.LayerSetData(layer, state);
}
}
}
}

View File

@@ -1,12 +1,13 @@
using System.Numerics; using System.Numerics;
using Content.Shared.Movement.Systems; using Content.Shared.Movement.Systems;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Timing; using Robust.Shared.Timing;
namespace Content.Shared.Movement.Components namespace Content.Shared.Movement.Components
{ {
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] [RegisterComponent, NetworkedComponent]
public sealed partial class InputMoverComponent : Component public sealed partial class InputMoverComponent : Component
{ {
// This class has to be able to handle server TPS being lower than client FPS. // This class has to be able to handle server TPS being lower than client FPS.
@@ -40,32 +41,30 @@ namespace Content.Shared.Movement.Components
public Vector2 CurTickWalkMovement; public Vector2 CurTickWalkMovement;
public Vector2 CurTickSprintMovement; public Vector2 CurTickSprintMovement;
[AutoNetworkedField]
public MoveButtons HeldMoveButtons = MoveButtons.None; public MoveButtons HeldMoveButtons = MoveButtons.None;
/// <summary> /// <summary>
/// Entity our movement is relative to. /// Entity our movement is relative to.
/// </summary> /// </summary>
[AutoNetworkedField]
public EntityUid? RelativeEntity; public EntityUid? RelativeEntity;
/// <summary> /// <summary>
/// Although our movement might be relative to a particular entity we may have an additional relative rotation /// 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 /// e.g. if we've snapped to a different cardinal direction
/// </summary> /// </summary>
[ViewVariables, AutoNetworkedField] [ViewVariables]
public Angle TargetRelativeRotation = Angle.Zero; public Angle TargetRelativeRotation = Angle.Zero;
/// <summary> /// <summary>
/// The current relative rotation. This will lerp towards the <see cref="TargetRelativeRotation"/>. /// The current relative rotation. This will lerp towards the <see cref="TargetRelativeRotation"/>.
/// </summary> /// </summary>
[ViewVariables, AutoNetworkedField] [ViewVariables]
public Angle RelativeRotation; public Angle RelativeRotation;
/// <summary> /// <summary>
/// If we traverse on / off a grid then set a timer to update our relative inputs. /// If we traverse on / off a grid then set a timer to update our relative inputs.
/// </summary> /// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField] [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public TimeSpan LerpTarget; public TimeSpan LerpTarget;
@@ -73,7 +72,18 @@ namespace Content.Shared.Movement.Components
public bool Sprinting => (HeldMoveButtons & MoveButtons.Walk) == 0x0; public bool Sprinting => (HeldMoveButtons & MoveButtons.Walk) == 0x0;
[ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] [ViewVariables(VVAccess.ReadWrite)]
public bool CanMove { get; set; } = true; public bool CanMove = true;
}
[Serializable, NetSerializable]
public sealed class InputMoverComponentState : ComponentState
{
public MoveButtons HeldMoveButtons;
public NetEntity? RelativeEntity;
public Angle TargetRelativeRotation;
public Angle RelativeRotation;
public TimeSpan LerpTarget;
public bool CanMove;
} }
} }

View File

@@ -0,0 +1,22 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Movement.Components;
/// <summary>
/// Updates a sprite layer based on whether an entity is moving via input or not.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class SpriteMovementComponent : Component
{
/// <summary>
/// Layer and sprite state to use when moving.
/// </summary>
[DataField]
public Dictionary<string, PrototypeLayerData> MovementLayers = new();
/// <summary>
/// Layer and sprite state to use when not moving.
/// </summary>
[DataField]
public Dictionary<string, PrototypeLayerData> NoMovementLayers = new();
}

View File

@@ -1,15 +1,22 @@
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
namespace Content.Shared.Movement.Events; namespace Content.Shared.Movement.Events;
/// <summary> /// <summary>
/// Raised on an entity whenever it has a movement input. /// Raised on an entity whenever it has a movement input change.
/// </summary> /// </summary>
[ByRefEvent] [ByRefEvent]
public readonly struct MoveInputEvent public readonly struct MoveInputEvent
{ {
public readonly EntityUid Entity; public readonly EntityUid Entity;
public readonly InputMoverComponent Component;
public readonly MoveButtons OldMovement;
public MoveInputEvent(EntityUid entity) public MoveInputEvent(EntityUid entity, InputMoverComponent component, MoveButtons oldMovement)
{ {
Entity = entity; Entity = entity;
Component = component;
OldMovement = oldMovement;
} }
} }

View File

@@ -4,6 +4,7 @@ using Content.Shared.Follower.Components;
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 Robust.Shared.GameStates;
using Robust.Shared.Input; using Robust.Shared.Input;
using Robust.Shared.Input.Binding; using Robust.Shared.Input.Binding;
using Robust.Shared.Player; using Robust.Shared.Player;
@@ -48,7 +49,8 @@ namespace Content.Shared.Movement.Systems
.Register<SharedMoverController>(); .Register<SharedMoverController>();
SubscribeLocalEvent<InputMoverComponent, ComponentInit>(OnInputInit); SubscribeLocalEvent<InputMoverComponent, ComponentInit>(OnInputInit);
SubscribeLocalEvent<InputMoverComponent, AfterAutoHandleStateEvent>(OnInputHandleState); SubscribeLocalEvent<InputMoverComponent, ComponentGetState>(OnMoverGetState);
SubscribeLocalEvent<InputMoverComponent, ComponentHandleState>(OnMoverHandleState);
SubscribeLocalEvent<InputMoverComponent, EntParentChangedMessage>(OnInputParentChange); SubscribeLocalEvent<InputMoverComponent, EntParentChangedMessage>(OnInputParentChange);
SubscribeLocalEvent<AutoOrientComponent, EntParentChangedMessage>(OnAutoParentChange); SubscribeLocalEvent<AutoOrientComponent, EntParentChangedMessage>(OnAutoParentChange);
@@ -64,19 +66,76 @@ namespace Content.Shared.Movement.Systems
CameraRotationLocked = obj; CameraRotationLocked = obj;
} }
/// <summary>
/// Gets the buttons held with opposites cancelled out.
/// </summary>
public static MoveButtons GetNormalizedMovement(MoveButtons buttons)
{
var oldMovement = buttons;
if ((oldMovement & (MoveButtons.Left | MoveButtons.Right)) == (MoveButtons.Left | MoveButtons.Right))
{
oldMovement &= ~MoveButtons.Left;
oldMovement &= ~MoveButtons.Right;
}
if ((oldMovement & (MoveButtons.Up | MoveButtons.Down)) == (MoveButtons.Up | MoveButtons.Down))
{
oldMovement &= ~MoveButtons.Up;
oldMovement &= ~MoveButtons.Down;
}
return oldMovement;
}
protected void SetMoveInput(InputMoverComponent component, MoveButtons buttons) protected void SetMoveInput(InputMoverComponent component, MoveButtons buttons)
{ {
if (component.HeldMoveButtons == buttons) if (component.HeldMoveButtons == buttons)
return; return;
// Relay the fact we had any movement event.
// TODO: Ideally we'd do these in a tick instead of out of sim.
var moveEvent = new MoveInputEvent(component.Owner, component, component.HeldMoveButtons);
component.HeldMoveButtons = buttons; component.HeldMoveButtons = buttons;
Dirty(component); RaiseLocalEvent(component.Owner, ref moveEvent);
Dirty(component.Owner, component);
} }
private void OnInputHandleState(EntityUid uid, InputMoverComponent component, ref AfterAutoHandleStateEvent args) private void OnMoverHandleState(EntityUid uid, InputMoverComponent component, ComponentHandleState args)
{ {
if (args.Current is not InputMoverComponentState state)
return;
// Handle state
component.LerpTarget = state.LerpTarget;
component.RelativeRotation = state.RelativeRotation;
component.TargetRelativeRotation = state.TargetRelativeRotation;
component.CanMove = state.CanMove;
component.RelativeEntity = EnsureEntity<InputMoverComponent>(state.RelativeEntity, uid);
// Reset
component.LastInputTick = GameTick.Zero; component.LastInputTick = GameTick.Zero;
component.LastInputSubTick = 0; component.LastInputSubTick = 0;
if (component.HeldMoveButtons != state.HeldMoveButtons)
{
var moveEvent = new MoveInputEvent(uid, component, component.HeldMoveButtons);
component.HeldMoveButtons = state.HeldMoveButtons;
RaiseLocalEvent(uid, ref moveEvent);
}
}
private void OnMoverGetState(EntityUid uid, InputMoverComponent component, ref ComponentGetState args)
{
args.State = new InputMoverComponentState()
{
CanMove = component.CanMove,
RelativeEntity = GetNetEntity(component.RelativeEntity),
LerpTarget = component.LerpTarget,
HeldMoveButtons = component.HeldMoveButtons,
RelativeRotation = component.RelativeRotation,
TargetRelativeRotation = component.TargetRelativeRotation,
};
} }
private void ShutdownInput() private void ShutdownInput()
@@ -260,11 +319,6 @@ namespace Content.Shared.Movement.Systems
if (!MoverQuery.TryGetComponent(entity, out var moverComp)) if (!MoverQuery.TryGetComponent(entity, out var moverComp))
return; return;
// Relay the fact we had any movement event.
// TODO: Ideally we'd do these in a tick instead of out of sim.
var moveEvent = new MoveInputEvent(entity);
RaiseLocalEvent(entity, ref moveEvent);
// For stuff like "Moving out of locker" or the likes // For stuff like "Moving out of locker" or the likes
// We'll relay a movement input to the parent. // We'll relay a movement input to the parent.
if (_container.IsEntityInContainer(entity) && if (_container.IsEntityInContainer(entity) &&
@@ -276,7 +330,7 @@ namespace Content.Shared.Movement.Systems
RaiseLocalEvent(xform.ParentUid, ref relayMoveEvent); RaiseLocalEvent(xform.ParentUid, ref relayMoveEvent);
} }
SetVelocityDirection(moverComp, dir, subTick, state); SetVelocityDirection(entity, moverComp, dir, subTick, state);
} }
private void OnInputInit(EntityUid uid, InputMoverComponent component, ComponentInit args) private void OnInputInit(EntityUid uid, InputMoverComponent component, ComponentInit args)
@@ -308,7 +362,7 @@ namespace Content.Shared.Movement.Systems
if (moverComp == null) return; if (moverComp == null) return;
SetSprinting(moverComp, subTick, walking); SetSprinting(uid, moverComp, subTick, walking);
} }
public (Vector2 Walking, Vector2 Sprinting) GetVelocityInput(InputMoverComponent mover) public (Vector2 Walking, Vector2 Sprinting) GetVelocityInput(InputMoverComponent mover)
@@ -359,7 +413,7 @@ namespace Content.Shared.Movement.Systems
/// composed into a single direction vector, <see cref="VelocityDir"/>. Enabling /// composed into a single direction vector, <see cref="VelocityDir"/>. Enabling
/// opposite directions will cancel each other out, resulting in no direction. /// opposite directions will cancel each other out, resulting in no direction.
/// </summary> /// </summary>
public void SetVelocityDirection(InputMoverComponent component, Direction direction, ushort subTick, bool enabled) public void SetVelocityDirection(EntityUid entity, InputMoverComponent component, Direction direction, ushort subTick, bool enabled)
{ {
// Logger.Info($"[{_gameTiming.CurTick}/{subTick}] {direction}: {enabled}"); // Logger.Info($"[{_gameTiming.CurTick}/{subTick}] {direction}: {enabled}");
@@ -372,10 +426,10 @@ namespace Content.Shared.Movement.Systems
_ => throw new ArgumentException(nameof(direction)) _ => throw new ArgumentException(nameof(direction))
}; };
SetMoveInput(component, subTick, enabled, bit); SetMoveInput(entity, component, subTick, enabled, bit);
} }
private void SetMoveInput(InputMoverComponent component, ushort subTick, bool enabled, MoveButtons bit) private void SetMoveInput(EntityUid entity, InputMoverComponent component, ushort subTick, bool enabled, MoveButtons bit)
{ {
// Modifies held state of a movement button at a certain sub tick and updates current tick movement vectors. // Modifies held state of a movement button at a certain sub tick and updates current tick movement vectors.
ResetSubtick(component); ResetSubtick(component);
@@ -415,11 +469,11 @@ namespace Content.Shared.Movement.Systems
component.LastInputSubTick = 0; component.LastInputSubTick = 0;
} }
public void SetSprinting(InputMoverComponent component, ushort subTick, bool walking) public void SetSprinting(EntityUid entity, InputMoverComponent component, ushort subTick, bool walking)
{ {
// Logger.Info($"[{_gameTiming.CurTick}/{subTick}] Sprint: {enabled}"); // Logger.Info($"[{_gameTiming.CurTick}/{subTick}] Sprint: {enabled}");
SetMoveInput(component, subTick, walking, MoveButtons.Walk); SetMoveInput(entity, component, subTick, walking, MoveButtons.Walk);
} }
/// <summary> /// <summary>

View File

@@ -33,6 +33,7 @@
- type: Sprite - type: Sprite
layers: layers:
- state: miner - state: miner
map: ["movement"]
- state: miner_e_r - state: miner_e_r
map: ["enum.BorgVisualLayers.Light"] map: ["enum.BorgVisualLayers.Light"]
shader: unshaded shader: unshaded
@@ -41,6 +42,13 @@
shader: unshaded shader: unshaded
map: ["light"] map: ["light"]
visible: false visible: false
- type: SpriteMovement
movementLayers:
movement:
state: miner_moving
noMovementLayers:
movement:
state: miner
- type: BorgChassis - type: BorgChassis
maxModules: 4 maxModules: 4
moduleWhitelist: moduleWhitelist:
@@ -119,6 +127,7 @@
- type: Sprite - type: Sprite
layers: layers:
- state: janitor - state: janitor
map: ["movement"]
- state: janitor_e_r - state: janitor_e_r
map: ["enum.BorgVisualLayers.Light"] map: ["enum.BorgVisualLayers.Light"]
shader: unshaded shader: unshaded
@@ -127,6 +136,13 @@
shader: unshaded shader: unshaded
map: ["light"] map: ["light"]
visible: false visible: false
- type: SpriteMovement
movementLayers:
movement:
state: janitor_moving
noMovementLayers:
movement:
state: janitor
- type: BorgChassis - type: BorgChassis
maxModules: 4 maxModules: 4
moduleWhitelist: moduleWhitelist:
@@ -162,6 +178,7 @@
- type: Sprite - type: Sprite
layers: layers:
- state: medical - state: medical
map: ["movement"]
- state: medical_e_r - state: medical_e_r
map: ["enum.BorgVisualLayers.Light"] map: ["enum.BorgVisualLayers.Light"]
shader: unshaded shader: unshaded
@@ -170,6 +187,13 @@
shader: unshaded shader: unshaded
map: ["light"] map: ["light"]
visible: false visible: false
- type: SpriteMovement
movementLayers:
movement:
state: medical_moving
noMovementLayers:
movement:
state: medical
- type: BorgChassis - type: BorgChassis
maxModules: 4 maxModules: 4
moduleWhitelist: moduleWhitelist:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -41,6 +41,10 @@
}, },
{ {
"name": "janitor", "name": "janitor",
"directions": 4
},
{
"name": "janitor_moving",
"directions": 4, "directions": 4,
"delays": [ "delays": [
[ [
@@ -107,6 +111,32 @@
] ]
] ]
}, },
{
"name": "medical_moving",
"directions": 4,
"delays": [
[
0.1,
0.2,
0.1
],
[
0.1,
0.2,
0.1
],
[
0.1,
0.2,
0.1
],
[
0.1,
0.2,
0.1
]
]
},
{ {
"name": "medical_e", "name": "medical_e",
"directions": 4 "directions": 4
@@ -121,6 +151,10 @@
}, },
{ {
"name": "miner", "name": "miner",
"directions": 4
},
{
"name": "miner_moving",
"directions": 4, "directions": 4,
"delays": [ "delays": [
[ [

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB