/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ using System; // The one relation between simulation time and real time. Real seconds go in; whole fixed steps come out, each // handed to the caller's step action with the same Step. Nothing else in the game converts frame time to sim time, // and nothing integrates a sim quantity on this clock: presenters read Lead to draw ahead of the latest step // (DrawAhead) and decide no sim fact. public sealed class SimClock { // The longest real time one frame may feed. A hitch (a load, a breakpoint) is a slow frame, not a burst of // 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) { if (!(step > 0)) throw new ArgumentOutOfRangeException(nameof(step)); if (!(stepsPerRealSecond > 0)) throw new ArgumentOutOfRangeException(nameof(stepsPerRealSecond)); Step = step; StepsPerRealSecond = stepsPerRealSecond; _period = 1.0 / stepsPerRealSecond; } // Sim seconds per step, fixed for the clock's life. public float Step { get; } public float StepsPerRealSecond { get; } public long Steps { get; private set; } // 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; _advancing = true; try { while (_accumulator >= _period) { _accumulator -= _period; step(Step); Steps++; ran++; } } finally { _advancing = false; } return ran; } }