diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index 0476acde4..97c559d35 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -229,7 +229,8 @@ public class ActionGameManager : MonoBehaviour public ItemManager ItemManager { get; private set; } public Zone Zone { get; private set; } - public static SimClock Clock { get; private set; } + public static SimClock Clock { get; } = new SimClock(1f / 60f); + private bool _zoneEntered; private readonly (float2 direction, string name)[] _directions = { (float2(0, 1), "Front"), @@ -727,7 +728,7 @@ public class ActionGameManager : MonoBehaviour galaxyZone.Contents = new Zone(ItemManager, Settings.PlanetSettings, galaxyZone.PackedContents, galaxyZone, CurrentGalaxy); } Zone = galaxyZone.Contents; - Clock = new SimClock(1f / 60f); + _zoneEntered = true; // the clock forgets its lead between frames, never inside a step PlayMusic(MusicType.Overworld); Zone.Log = s => Debug.Log($"Zone: {s}"); @@ -1328,7 +1329,12 @@ public class ActionGameManager : MonoBehaviour } } // One clock steps the zone, paused or not: a paused frame feeds it no real time. - Clock.Advance(_paused ? 0f : Time.deltaTime, Zone.Update); + // A zone entered inside a step (a wormhole) ends this frame's stepping: the rest of its steps are the old zone's. + void EnterNewZone() { if (_zoneEntered) { Clock.Reset(); _zoneEntered = false; } } + EnterNewZone(); + var steppedZone = Zone; + Clock.Advance(_paused ? 0f : Time.deltaTime, delta => { if (Zone == steppedZone) steppedZone.Update(delta); }); + EnterNewZone(); } private static string ResultLabel(ShotResult result) => result switch diff --git a/Assets/Scripts/ServerShared/DrawAhead.cs b/Assets/Scripts/ServerShared/DrawAhead.cs index 52c92fee8..d26bf91b4 100644 --- a/Assets/Scripts/ServerShared/DrawAhead.cs +++ b/Assets/Scripts/ServerShared/DrawAhead.cs @@ -10,11 +10,16 @@ using static CultMath.math; // one, a previous state or a velocity of its own. public static class DrawAhead { + // A ship in a wormhole animation has its pose scripted by the step, not integrated from Velocity and TurnRate, + // so it is drawn where the step put it: extrapolating a scripted pose draws a streak that resets every step. public static float3 Position(Entity entity, float lead) => - entity.Position + float3(entity.Velocity.x, 0, entity.Velocity.y) * lead; + entity is Ship { WormholeAnimationInProgress: true } + ? entity.Position + : entity.Position + float3(entity.Velocity.x, 0, entity.Velocity.y) * lead; // The ship's rotation turned about its own up axis by the rate its last step turned, for lead seconds. - public static quaternion Rotation(Ship ship, float lead) => Turned(ship.Rotation, ship.TurnRate * lead); + public static quaternion Rotation(Ship ship, float lead) => + ship.WormholeAnimationInProgress ? ship.Rotation : Turned(ship.Rotation, ship.TurnRate * lead); // q turned about its own up axis by the angle, in the sense Direction turns for a positive TurnRate. public static quaternion Turned(quaternion q, float angle) @@ -32,7 +37,4 @@ public static class DrawAhead t.w * q.z + t.x * q.y - t.y * q.x + t.z * q.w, t.w * q.w - t.x * q.x - t.y * q.y - t.z * q.z)); } - - // Zone time for a presenter that evaluates a sim function of time (FireControl.RoundAt). - public static float Time(Zone zone, float lead) => zone.Time + lead; } diff --git a/Assets/Scripts/ServerShared/SimClock.cs b/Assets/Scripts/ServerShared/SimClock.cs index 15e132ac0..6d00c46d0 100644 --- a/Assets/Scripts/ServerShared/SimClock.cs +++ b/Assets/Scripts/ServerShared/SimClock.cs @@ -16,6 +16,7 @@ public sealed class SimClock private readonly double _period; private double _accumulator; + private bool _advancing; public SimClock(float step, float stepsPerRealSecond = 60) { @@ -36,19 +37,32 @@ public sealed class SimClock // Sim seconds since the last step, 0 <= Lead < Step. public float Lead => (float) (_accumulator * StepsPerRealSecond * Step); + // Forgets the real time paid toward the next step (a new zone starts between steps). A step cannot do this to the + // clock that is running it. + public void Reset() + { + if (_advancing) throw new InvalidOperationException("A step cannot reset the clock that is running it."); + _accumulator = 0; + } + // Feeds real seconds (negative counts as none, one frame counts at most MaxFrameSeconds) and runs the whole // steps they pay for. Returns how many ran. public int Advance(float realSeconds, Action step) { _accumulator += Math.Min(Math.Max(realSeconds, 0f), MaxFrameSeconds); var ran = 0; - while (_accumulator >= _period) + _advancing = true; + try { - _accumulator -= _period; - step(Step); - Steps++; - ran++; + while (_accumulator >= _period) + { + _accumulator -= _period; + step(Step); + Steps++; + ran++; + } } + finally { _advancing = false; } return ran; } } diff --git a/tests/Aetheria.Shared.Tests/SimClockTests.cs b/tests/Aetheria.Shared.Tests/SimClockTests.cs index 3a8f72fbd..db79647bb 100644 --- a/tests/Aetheria.Shared.Tests/SimClockTests.cs +++ b/tests/Aetheria.Shared.Tests/SimClockTests.cs @@ -77,6 +77,18 @@ public sealed class SimClockTests Assert.Equal(6, clock.Steps); } + [Fact] + public void AStepCannotResetItsClock() + { + var clock = new SimClock(1f / 60f); + Assert.Throws(() => clock.Advance(1f / 60f, _ => clock.Reset())); + clock.Advance(.005f, _ => { }); + Assert.True(clock.Lead > 0); + clock.Reset(); + Assert.Equal(0f, clock.Lead); + Assert.Equal(1, clock.Advance(1f / 60f, _ => { })); + } + [Fact] public void PauseFeedsNothing() { @@ -252,10 +264,30 @@ public sealed partial class RunStartTests Assert.Equal(player.Position.x + player.Velocity.x * lead, ahead.x, 6); Assert.Equal(player.Position.y, ahead.y); Assert.Equal(player.Position.z + player.Velocity.y * lead, ahead.z, 6); - Assert.Equal(arena.Time + lead, DrawAhead.Time(arena, lead)); Assert.Equal(before, Snapshot(arena, new List())); } + // A pose the step scripts (the wormhole animation) is drawn where the step put it, at any lead. + [Fact] + public void ScriptedPosesAreNotDrawnAhead() + { + var (arena, player) = LaunchDuel(); + for (var step = 0; step < 120; step++) arena.Update(ClockFrame); + player.EnterWormhole(player.Position.xz + float2(50, 0)); + arena.Update(ClockFrame); + Assert.True(player.WormholeAnimationInProgress); + Assert.True(length(player.Velocity) > .01f, "the ship carries speed into the animation"); + var position = DrawAhead.Position(player, ClockFrame); + Assert.Equal(player.Position.x, position.x); + Assert.Equal(player.Position.y, position.y); + Assert.Equal(player.Position.z, position.z); + var rotation = DrawAhead.Rotation(player, ClockFrame); + Assert.Equal(player.Rotation.x, rotation.x); + Assert.Equal(player.Rotation.y, rotation.y); + Assert.Equal(player.Rotation.z, rotation.z); + Assert.Equal(player.Rotation.w, rotation.w); + } + // The turn a presenter draws ahead is the turn the next step makes: a ship held in a steady turn, drawn one whole // step ahead of the pose it was last given, is where the sim puts that pose next. [Fact]