Fixed timestep API
GodotECS.Fixed — configurable fixed-step simulation, time singletons, and the interpolation pipeline. Source: addons/godotecs/src/fixed/. Concept overview: Core concepts.
FixedConfig and EcsTime
public struct FixedConfig : GodotECS.Core.ISingleton
{
public const float DefaultHz = 60f;
public const float MinHz = 10f;
public const float MaxHz = 240f;
public const int DefaultMaxSubsteps = 3;
public const int MaxClampedSubsteps = 64;
public float TargetHz;
public int MaxSubsteps;
public double Accumulator; // internal: unsimulated residue, seconds
public static FixedConfig Default { get; } // 60 Hz, 3 substeps, empty accumulator
public static float ClampHz(float hz); // NaN/Inf -> 60; clamps to [10, 240]
public static int ClampSubsteps(int n); // clamps to [1, 64]
public float FixedDelta { get; } // 1 / ClampHz(TargetHz); never <= 0, NaN, or Inf
public FixedConfig Sanitized(); // validated copy: Hz, substeps, finite Accumulator >= 0
}
public struct EcsTime : GodotECS.Core.ISingleton
{
public uint Tick;
public float FixedDelta; // from the active FixedConfig, e.g. 1/60
public float Delta; // variable render delta of the last TickFixed
public double Alpha; // 0..1 Prev -> Curr interpolation factor
public double Elapsed; // total seconds fed through TickFixed
}Both are World singletons (GetSingleton / SetSingleton). Tune per game by setting the world's FixedConfig; systems read timing once per OnUpdate via SystemState or the EcsTime singleton — never per entity in hot paths.
FixedStepper
public sealed class FixedStepper
{
public float FixedDelta { get; set; } = 1f / 60f;
public int MaxSubsteps { get; set; } = 3;
public uint Tick { get; private set; }
public double Alpha { get; private set; }
public ulong Spills { get; private set; }
public double Accumulator { get; }
// Adopt a validated config, importing its residue (smooth hot Hz changes).
public void Configure(in FixedConfig cfg);
// Calls step(tick, fixedDelta) 0..N times, then present(alpha). Null-tolerant.
public void Update(double frameDelta, System.Action<uint, float> step, System.Action<double> present);
public void Reset(); // Tick = 0, Alpha = 0, Spills = 0, accumulator = 0
}Update accumulates the variable frameDelta (negative/NaN/Infinity → 0), runs up to MaxSubsteps steps, then drops any remaining backlog and counts the event in Spills — the sim slows down under extreme lag instead of spiraling. Alpha (residue / FixedDelta, always in [0,1]) drives present interpolation. World.TickFixed implements the same algorithm against the world's accumulator, running the FixedStep group and refreshing EcsTime.
Time systems and RenderTransform
public struct RenderTransform : GodotECS.Core.IComponentData
{
public Godot.Vector3 Pos;
public Godot.Quaternion Rot;
public float Scale;
public static RenderTransform From(in LocalTransform t);
}
// First in FixedStep: snapshots Curr into Prev before integration.
public struct CopyPrevSystem : GodotECS.Core.ISystem
{
public void OnCreate(ref GodotECS.Core.SystemState state);
public void OnUpdate(ref GodotECS.Core.SystemState state);
public void OnDestroy(ref GodotECS.Core.SystemState state);
}
// First in Presentation: RenderTransform = Lerp/Slerp(Prev, Curr, Alpha).
public struct InterpolateSystem : GodotECS.Core.ISystem
{
public void OnCreate(ref GodotECS.Core.SystemState state);
public void OnUpdate(ref GodotECS.Core.SystemState state);
public void OnDestroy(ref GodotECS.Core.SystemState state);
}
// Presentation, [UpdateBefore(InterpolateSystem)]: equips LocalTransform-only
// entities with PrevTransform + RenderTransform (Prev = Render = Curr).
[UpdateBefore(typeof(InterpolateSystem))]
public struct EnsurePrevRenderSystem : GodotECS.Core.ISystem
{
public void OnCreate(ref GodotECS.Core.SystemState state);
public void OnUpdate(ref GodotECS.Core.SystemState state);
public void OnDestroy(ref GodotECS.Core.SystemState state);
public void Update(EntityManager em); // logic entry, usable without a SystemGroup
}Contract: simulation reads/writes LocalTransform; only Presentation reads PrevTransform + LocalTransform and writes the display-only RenderTransform. InterpolateSystem touches only entities having all three (position lerped, scale lerped, rotation slerped with clamped alpha) via a pure chunk job completed inside OnUpdate. CopyPrevSystem / InterpolateSystem hold their "first" slots by insertion convention — add them before other systems in their groups. EnsurePrevRenderSystem instead declares [UpdateBefore(typeof(InterpolateSystem))], collects entities missing PrevTransform / RenderTransform in two passes (collect, then add, so iteration is never invalidated by moves), and initializes them from current values — no one-frame pop for mid-tick spawns, ~zero steady-state cost. Its Update(EntityManager) overload throws ArgumentNullException on null and lazily creates its queries, so tests can drive it directly.