import 'dart:html' as html; import 'package:rules_of_living/src/Renderer.dart'; import 'package:rules_of_living/src/Simulation.dart'; class Engine { // Elapsed Time Counter - useful for Safety Timeout Stopwatch _elapsed = new Stopwatch(); /// Game Tick Rate /// /// *does* impact game speed; dictates how long each logic step takes. Only /// interesting for engine internal calculations, to set simulation speed /// from the outside use stepsPerSecond instead. int _MS_PER_STEP = 1000 ~/ 3; /// Set Logic Updates per Second /// /// Dictates simulation speed. Sets the amount of (logic) updates per second. /// Translations between number of updates and their timings is not exact so /// comparing this variable to fixed ints might not yield the expected results. /// Does not affect render frequency, which is handled by framesPerSecond. int get stepsPerSecond => 1000 ~/ _MS_PER_STEP; set stepsPerSecond(int val) => _MS_PER_STEP = 1000 ~/ val; // Max Frame (i.e. Rendering) rate - does *not* impact game speed final int _MS_PER_FRAME = 1000 ~/ 30; // ms stuck in updateloop after which game will declare itself unresponsive final int SAFETY_TIMEOUT = 2000; num _updateLag = 0.0; num _drawLag = 0.0; /// The Canvas to Paint on /// /// Manually define or change the canvas the engine should render to. Should /// be used if no canvas was defined at engine creation and it should be /// rendered later. html.CanvasElement canvas; Simulation _simulation; Map _lastSimulationUpdate = {}; Renderer _renderer; bool running = false; Engine() { _elapsed.start(); html.window.animationFrame.then(animFrame); } void animFrame(num now) { int elapsed = _elapsed.elapsedMilliseconds; _elapsed.reset(); process(elapsed, SAFETY_TIMEOUT, update: this.update, render: this.render); html.window.animationFrame.then(animFrame); } void process(int elapsed, int timeOut, {Function update, Function render}) { _drawLag += elapsed; _updateLag += elapsed; while (running == true && _shouldUpdate(_updateLag, elapsed, timeOut) == true) { _updateLag -= _MS_PER_STEP; if (update == null) break; update(); } if (_drawLag >= _MS_PER_FRAME) { _drawLag = 0; if (render == null) return; render(_updateLag / _MS_PER_STEP); } } // TODO Should be renamed to something more intention revealing like hasReachedUpdateTimestepThreshold bool _shouldUpdate(int updateLag, int elapsed, int timeOut) { if (updateLag < _MS_PER_STEP) return false; if (elapsed > timeOut) throw StackOverflowError; return true; } /// Update Engine Logic /// /// Updates the logic of the engine by one tick. Should usually not be called /// directly, since it is automatically taken care of by the processing function. /// If simulation should be advanced manually one time, prefer using step(). void update() { if (_simulation == null) return; _lastSimulationUpdate = _simulation.update(); _simulation.mergeStateChanges(_lastSimulationUpdate); if (_lastSimulationUpdate.length == 0) running = false; } /// Advances Logic One Update /// /// Moves logic of the engine one step forward and pauses the /// simulation. Does not automatically re-render the new state /// (though this should usually not pose a problem). void step() { update(); running = false; } /// Renders the Current Simulation State /// /// Renders the simulation once. Will usually automatically be called by /// the internal engine processing. Does not do anything if no canvas is /// defined. void render([num interp]) { if (canvas == null || _renderer == null) return; _renderer.render(canvas, _lastSimulationUpdate); } void set simulation(Simulation value) => _simulation = value; }