Files
RobustToolbox/Robust.Client/GameObjects/EntitySystems/TransformSystem.cs
83c2a1be11 [Dependency] source generator part 2 (#6550)
* [Dependency] source generator

No more reflection, no more codegen at runtime

Also various changes to Roslyn helpers to make this easier to write.

Requires all types with dependencies to be partial and not have readonly dependency fields. An analyzer enforces this at warning level, the previous injection strategies have remained in the code *for now* as a fallback.

No fallback is available for [field: Dependency] properties, due to a Roslyn bug.

Code Fixes exist. We love Roslyn

* Apply dependencies generator changes to all code

* Release notes

* Preprocessor got hands

* Handle nullable dependencies

These are bad but gotta deal with it.

* Apply suggestions from code review

Co-authored-by: Moony <moony@hellomouse.net>

* Fine, let's not use collection expressions

---------

Co-authored-by: Moony <moony@hellomouse.net>
2026-05-08 12:38:33 +02:00

174 lines
7.8 KiB
C#

using System.Collections.Generic;
using System.Numerics;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Robust.Client.GameObjects
{
/// <summary>
/// Handles interpolation of transform positions.
/// </summary>
[UsedImplicitly]
public sealed partial class TransformSystem : SharedTransformSystem
{
// Max distance per tick how far an entity can move before it is considered teleporting.
// TODO: Make these values somehow dependent on server TPS.
private const float MaxInterpolationDistance = 2.0f;
private const float MaxInterpolationDistanceSquared = MaxInterpolationDistance * MaxInterpolationDistance;
private const float MinInterpolationDistance = 0.001f;
private const float MinInterpolationDistanceSquared = MinInterpolationDistance * MinInterpolationDistance;
[Dependency] private IGameTiming _gameTiming = default!;
// Only keep track of transforms actively lerping.
// Much faster than iterating 3000+ transforms every frame.
[ViewVariables] private readonly List<Entity<TransformComponent>> _lerpingTransforms = new();
public void Reset()
{
foreach (var (_, xform) in _lerpingTransforms)
{
xform.ActivelyLerping = false;
xform.NextPosition = null;
xform.NextRotation = null;
xform.LerpParent = EntityUid.Invalid;
}
_lerpingTransforms.Clear();
}
public override void ActivateLerp(EntityUid uid, TransformComponent xform)
{
// This lerping logic is pretty convoluted and generally assumes that the client does not mispredict.
// A more foolproof solution would be to just cache the coordinates at which any given entity was most
// recently rendered and using that as the lerp origin. However that'd require enumerating over all entities
// every tick which is pretty icky.
// The general considerations are:
// - If the client receives a server state for an entity moving from a->b and predicts nothing else, then it
// should show the entity lerping.
// - If the client predicts an entity will move while already lerping due to a state-application, it should
// clear the state's lerp, under the assumption that the client predicted the state and already rendered
// the entity in the state's final position.
// - If the client predicts that an entity moves, then we only lerp if this is the first time that the tick
// was predicted. I.e., we assume the entity was already rendered in the final position that was
// previously predicted.
// - If the client predicts that an entity should lerp twice in the same tick, then we need to combine them.
// I.e. moving from a->b then b->c, the client should lerp from a->c.
// If the client predicts an entity moves while already lerping, it should clear the
// predict a->b, lerp a->b
// predicted a->b, then predict b->c. Lerp b->c
// predicted a->b, then predict b->c. Lerp b->c
// predicted a->b, predicted b->c, then predict c->d. Lerp c->d
// server state a->b, then predicted b->c, lerp b->c
// server state a->b, then predicted b->c, then predict d, lerp b->c
if (_gameTiming.ApplyingState)
{
if (xform.ActivelyLerping)
{
// This should not happen, but can happen if some bad component state application code modifies an entity's coordinates.
Log.Error($"Entity {(ToPrettyString(uid))} tried to lerp twice while applying component states.");
return;
}
_lerpingTransforms.Add((uid, xform));
xform.ActivelyLerping = true;
xform.PredictedLerp = false;
xform.LerpParent = xform.ParentUid;
xform.PrevRotation = xform._localRotation;
xform.PrevPosition = xform._localPosition;
xform.LastLerp = _gameTiming.CurTick;
return;
}
xform.LastLerp = _gameTiming.CurTick;
if (!_gameTiming.IsFirstTimePredicted)
{
xform.ActivelyLerping = false;
return;
}
if (!xform.ActivelyLerping)
{
_lerpingTransforms.Add((uid, xform));
xform.ActivelyLerping = true;
xform.PredictedLerp = true;
xform.PrevRotation = xform._localRotation;
xform.PrevPosition = xform._localPosition;
xform.LerpParent = xform.ParentUid;
return;
}
if (!xform.PredictedLerp || xform.LerpParent != xform.ParentUid)
{
// Existing lerp was not due to prediction, but due to state application. That lerp should already
// have been rendered, so we will start a new lerp from the current position.
xform.PrevRotation = xform._localRotation;
xform.PrevPosition = xform._localPosition;
xform.LerpParent = xform.ParentUid;
}
}
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
var step = (float) (_gameTiming.TickRemainder.TotalSeconds / _gameTiming.TickPeriod.TotalSeconds);
for (var i = 0; i < _lerpingTransforms.Count; i++)
{
var (uid, transform) = _lerpingTransforms[i];
var found = false;
// Only lerp if parent didn't change.
// E.g. entering lockers would do it.
if (transform.ActivelyLerping
&& transform.LerpParent == transform.ParentUid
&& transform.ParentUid.IsValid()
&& !transform.Deleted)
{
if (transform.NextPosition != null)
{
var lerpDest = transform.NextPosition.Value;
var lerpSource = transform.PrevPosition;
var distance = (lerpDest - lerpSource).LengthSquared();
if (distance is > MinInterpolationDistanceSquared and < MaxInterpolationDistanceSquared)
{
SetLocalPositionNoLerp(uid, Vector2.Lerp(lerpSource, lerpDest, step), transform);
transform.NextPosition = lerpDest;
found = true;
}
}
if (transform.NextRotation != null)
{
var lerpDest = transform.NextRotation.Value;
var lerpSource = transform.PrevRotation;
SetLocalRotationNoLerp(uid, Angle.Lerp(lerpSource, lerpDest, step), transform);
transform.NextRotation = lerpDest;
found = true;
}
}
// Transforms only get removed from the lerp list if they no longer are in here.
// This is much easier than having the transform itself tell us to remove it.
if (!found)
{
// Transform is no longer lerping, remove.
transform.ActivelyLerping = false;
_lerpingTransforms.RemoveSwap(i);
i -= 1;
}
}
}
}
}