SimClock owns the game tick: TickRate, GameStep, ForGame(); the 60 default and ActionGameManager's literal step are gone
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a19d0eb73b
commit
e2f0fc9dc4
6 changed files with 56 additions and 13 deletions
|
|
@ -248,7 +248,7 @@ public class ActionGameManager : MonoBehaviour
|
|||
|
||||
public ItemManager ItemManager { get; private set; }
|
||||
public Zone Zone { get; private set; }
|
||||
public static SimClock Clock { get; } = new SimClock(1f / 60f);
|
||||
public static SimClock Clock { get; } = SimClock.ForGame();
|
||||
private bool _zoneEntered;
|
||||
|
||||
private readonly (float2 direction, string name)[] _directions = {
|
||||
|
|
|
|||
|
|
@ -14,11 +14,19 @@ public sealed class SimClock
|
|||
// catch-up steps.
|
||||
public const float MaxFrameSeconds = .1f;
|
||||
|
||||
// Game steps per real second at design speed 1, and the sim seconds one step carries. The one owner of the game's
|
||||
// tick: every driver of a game history steps Zone.Update by GameStep. Design speed (ruling sim-speed-surface) will
|
||||
// set Step = DesignSpeed / TickRate in ForGame and nowhere else.
|
||||
public const int TickRate = 60;
|
||||
public const float GameStep = 1f / TickRate;
|
||||
|
||||
public static SimClock ForGame() => new SimClock(GameStep, TickRate);
|
||||
|
||||
private readonly double _period;
|
||||
private double _accumulator;
|
||||
private bool _advancing;
|
||||
|
||||
public SimClock(float step, float stepsPerRealSecond = 60)
|
||||
public SimClock(float step, float stepsPerRealSecond)
|
||||
{
|
||||
if (!(step > 0)) throw new ArgumentOutOfRangeException(nameof(step));
|
||||
if (!(stepsPerRealSecond > 0)) throw new ArgumentOutOfRangeException(nameof(stepsPerRealSecond));
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ public sealed partial class RunStartTests
|
|||
// each other, so they engage at step 0 (f19b9992); and Follow now flies only on an anchor the pilot itself tracks, so
|
||||
// once the sensors drop the staged anchor the follower stops following it (ce81a426).
|
||||
private const string TwoAiPinnedDigest = "ce81a426c40495e4b4b7b7f83ad23767cc781dce5523d713b4455e0aa5b7fe33";
|
||||
private const int TwoAiSteps = 3600;
|
||||
private const int TwoAiSteps = 60 * SimClock.TickRate;
|
||||
|
||||
[Fact]
|
||||
public void ItemManagerRefusesSeedZero()
|
||||
|
|
@ -214,7 +214,7 @@ public sealed partial class RunStartTests
|
|||
var history = new StringBuilder();
|
||||
for (var step = 0; step < TwoAiSteps; step++)
|
||||
{
|
||||
arena.Update(1f / 60f);
|
||||
arena.Update(SimClock.GameStep);
|
||||
history.Append(Snapshot(arena, shots)).Append('\n');
|
||||
}
|
||||
var digest = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(history.ToString()))).ToLowerInvariant();
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ public sealed class DrawnPoseTests : IDisposable
|
|||
{
|
||||
var w = Build();
|
||||
var ship = new Ship(w.Items, w.Zone, w.Hull, new EntitySettings());
|
||||
var clock = new SimClock(1f / 60f);
|
||||
var clock = new SimClock(1f / 60f, 60);
|
||||
var xs = new List<float>();
|
||||
var ys = new List<float>();
|
||||
var leads = new List<float>();
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ public sealed class SimClockTests
|
|||
{
|
||||
foreach (var frames in new[] { new[] { 1f / 60 }, Irregular, new[] { .09f, .004f, 0f, 3f } })
|
||||
{
|
||||
var clock = new SimClock(1f / 60f);
|
||||
var clock = new SimClock(1f / 60f, 60);
|
||||
var passed = Drive(clock, frames, 500);
|
||||
Assert.True(passed.Count >= 500);
|
||||
Assert.All(passed, dt => Assert.Equal(clock.Step, dt));
|
||||
|
|
@ -47,7 +47,7 @@ public sealed class SimClockTests
|
|||
public void StepsFollowRealTime()
|
||||
{
|
||||
// Ten real seconds of irregular frames.
|
||||
var clock = new SimClock(1f / 60f);
|
||||
var clock = new SimClock(1f / 60f, 60);
|
||||
var elapsed = 0.0;
|
||||
for (var frame = 0; elapsed < 10.0; frame++)
|
||||
{
|
||||
|
|
@ -71,7 +71,7 @@ public sealed class SimClockTests
|
|||
[Fact]
|
||||
public void AHitchIsClamped()
|
||||
{
|
||||
var clock = new SimClock(1f / 60f);
|
||||
var clock = new SimClock(1f / 60f, 60);
|
||||
var ran = clock.Advance(5f, _ => { });
|
||||
Assert.Equal(6, ran);
|
||||
Assert.Equal(6, clock.Steps);
|
||||
|
|
@ -80,7 +80,7 @@ public sealed class SimClockTests
|
|||
[Fact]
|
||||
public void AStepCannotResetItsClock()
|
||||
{
|
||||
var clock = new SimClock(1f / 60f);
|
||||
var clock = new SimClock(1f / 60f, 60);
|
||||
Assert.Throws<InvalidOperationException>(() => clock.Advance(1f / 60f, _ => clock.Reset()));
|
||||
clock.Advance(.005f, _ => { });
|
||||
Assert.True(clock.Lead > 0);
|
||||
|
|
@ -92,7 +92,7 @@ public sealed class SimClockTests
|
|||
[Fact]
|
||||
public void PauseFeedsNothing()
|
||||
{
|
||||
var clock = new SimClock(1f / 60f);
|
||||
var clock = new SimClock(1f / 60f, 60);
|
||||
clock.Advance(.025f, _ => { });
|
||||
var lead = clock.Lead;
|
||||
var steps = clock.Steps;
|
||||
|
|
@ -124,14 +124,14 @@ public sealed class SimClockTests
|
|||
[Fact]
|
||||
public void ABadClockIsRefusedByFieldName()
|
||||
{
|
||||
Assert.Equal("step", Assert.Throws<ArgumentOutOfRangeException>(() => new SimClock(0f)).ParamName);
|
||||
Assert.Equal("step", Assert.Throws<ArgumentOutOfRangeException>(() => new SimClock(0f, 60)).ParamName);
|
||||
Assert.Equal("stepsPerRealSecond", Assert.Throws<ArgumentOutOfRangeException>(() => new SimClock(1f, 0f)).ParamName);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed partial class RunStartTests
|
||||
{
|
||||
private const float ClockFrame = 1f / 60f;
|
||||
private const float ClockFrame = SimClock.GameStep;
|
||||
private const int HistorySteps = 1800;
|
||||
|
||||
// The Duel launched as the game launches it, its item random reseeded so play draws the same numbers every time.
|
||||
|
|
@ -173,7 +173,7 @@ public sealed partial class RunStartTests
|
|||
// Feeds the zone through one SimClock with the frames cycled, snapshotting every step until HistorySteps have run.
|
||||
private static List<string> History(Zone zone, float[] frames)
|
||||
{
|
||||
var clock = new SimClock(1f / 60f);
|
||||
var clock = new SimClock(1f / 60f, 60);
|
||||
var shots = new List<string>();
|
||||
using var _ = zone.ShotResolved.Subscribe(outcome => shots.Add($"{outcome.ShotId}:{outcome.Result}"));
|
||||
var history = new List<string>();
|
||||
|
|
|
|||
35
tests/Aetheria.Shared.Tests/SimRateTests.cs
Normal file
35
tests/Aetheria.Shared.Tests/SimRateTests.cs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using CultMath;
|
||||
using Xunit;
|
||||
using static CultMath.math;
|
||||
using float3 = CultMath.float3;
|
||||
using int2 = CultMath.int2;
|
||||
|
||||
// Cut sim-rate (ruling lower-game-sim-rate, map docs/aetheria-arena-sim-rate-map.md): the game's tick has one owner,
|
||||
// and content authored in seconds means the same at every tick, because a restarted timer keeps what the last step
|
||||
// overshot and passive sensing decays exactly.
|
||||
public sealed class SimRateClockTests
|
||||
{
|
||||
[Fact]
|
||||
public void OneTickOwner()
|
||||
{
|
||||
var clock = SimClock.ForGame();
|
||||
Assert.Equal(1f / SimClock.TickRate, SimClock.GameStep);
|
||||
Assert.Equal(SimClock.GameStep, clock.Step);
|
||||
Assert.Equal(SimClock.TickRate, clock.StepsPerRealSecond);
|
||||
|
||||
// Ten real seconds of irregular frames pay ten seconds of steps, so the game runs at real time.
|
||||
var frames = new[] { .004f, .031f, .016f, .0083f, .05f, .022f, .0125f, .027f };
|
||||
var real = 0f;
|
||||
for (var i = 0; real < 10f; i++)
|
||||
{
|
||||
var frame = Math.Min(frames[i % frames.Length], 10f - real);
|
||||
real += frame;
|
||||
clock.Advance(frame, _ => { });
|
||||
}
|
||||
Assert.InRange(clock.Steps, 10 * SimClock.TickRate - 1, 10 * SimClock.TickRate + 1);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue