555 lines
14 KiB
Haxe
555 lines
14 KiB
Haxe
package kha;
|
|
|
|
class TimeTask {
|
|
public var task: Void->Bool;
|
|
|
|
public var start: Float;
|
|
public var period: Float;
|
|
public var duration: Float;
|
|
public var next: Float;
|
|
|
|
public var id: Int;
|
|
public var groupId: Int;
|
|
public var active: Bool;
|
|
public var paused: Bool;
|
|
|
|
public function new() {}
|
|
}
|
|
|
|
class FrameTask {
|
|
public var task: Void->Bool;
|
|
public var priority: Int;
|
|
public var id: Int;
|
|
public var active: Bool;
|
|
public var paused: Bool;
|
|
|
|
public function new(task: Void->Bool, priority: Int, id: Int) {
|
|
this.task = task;
|
|
this.priority = priority;
|
|
this.id = id;
|
|
active = true;
|
|
paused = false;
|
|
}
|
|
}
|
|
|
|
class Scheduler {
|
|
static var timeTasks: Array<TimeTask>;
|
|
static var pausedTimeTasks: Array<TimeTask>;
|
|
static var outdatedTimeTasks: Array<TimeTask>;
|
|
static var timeTasksScratchpad: Array<TimeTask>;
|
|
static inline var timeWarpSaveTime: Float = 10.0;
|
|
|
|
static var frameTasks: Array<FrameTask>;
|
|
static var toDeleteFrame: Array<FrameTask>;
|
|
|
|
static var current: Float;
|
|
static var lastTime: Float;
|
|
static var lastFrameEnd: Float;
|
|
|
|
static var frame_tasks_sorted: Bool;
|
|
static var stopped: Bool;
|
|
static var vsync: Bool;
|
|
|
|
// Html5 target can update display frequency after some delay
|
|
#if kha_html5
|
|
static var onedifhz(get, never): Float;
|
|
|
|
static inline function get_onedifhz(): Float {
|
|
return 1.0 / Display.primary.frequency;
|
|
}
|
|
#else
|
|
static var onedifhz: Float;
|
|
#end
|
|
|
|
static var currentFrameTaskId: Int;
|
|
static var currentTimeTaskId: Int;
|
|
static var currentGroupId: Int;
|
|
|
|
static var DIF_COUNT = 3;
|
|
static var maxframetime = 0.5;
|
|
|
|
static var deltas: Array<Float>;
|
|
|
|
static var startTime: Float = 0;
|
|
|
|
static var activeTimeTask: TimeTask = null;
|
|
|
|
public static function init(): Void {
|
|
deltas = new Array<Float>();
|
|
for (i in 0...DIF_COUNT)
|
|
deltas[i] = 0;
|
|
|
|
stopped = true;
|
|
frame_tasks_sorted = true;
|
|
current = lastTime = lastFrameEnd = realTime();
|
|
|
|
currentFrameTaskId = 0;
|
|
currentTimeTaskId = 0;
|
|
currentGroupId = 0;
|
|
|
|
timeTasks = [];
|
|
pausedTimeTasks = [];
|
|
outdatedTimeTasks = [];
|
|
timeTasksScratchpad = [];
|
|
frameTasks = [];
|
|
toDeleteFrame = [];
|
|
}
|
|
|
|
public static function start(restartTimers: Bool = false): Void {
|
|
vsync = Window.get(0).vSynced;
|
|
#if !kha_html5
|
|
var hz = Display.primary != null ? Display.primary.frequency : 60;
|
|
if (hz >= 57 && hz <= 63)
|
|
hz = 60;
|
|
onedifhz = 1.0 / hz;
|
|
#end
|
|
|
|
stopped = false;
|
|
resetTime();
|
|
lastTime = realTime() - startTime;
|
|
for (i in 0...DIF_COUNT)
|
|
deltas[i] = 0;
|
|
|
|
if (restartTimers) {
|
|
for (timeTask in timeTasks) {
|
|
timeTask.paused = false;
|
|
}
|
|
|
|
for (frameTask in frameTasks) {
|
|
frameTask.paused = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
public static function stop(): Void {
|
|
stopped = true;
|
|
}
|
|
|
|
public static function isStopped(): Bool {
|
|
return stopped;
|
|
}
|
|
|
|
static function warpTimeTasksBack(time: Float, tasks: Array<TimeTask>): Void {
|
|
for (timeTask in tasks) {
|
|
if (timeTask.start >= time) {
|
|
timeTask.next = timeTask.start;
|
|
}
|
|
else if (timeTask.period > 0) {
|
|
var sinceStart = time - timeTask.start;
|
|
var times = Math.ceil(sinceStart / timeTask.period);
|
|
timeTask.next = timeTask.start + times * timeTask.period;
|
|
}
|
|
}
|
|
}
|
|
|
|
public static function warp(time: Float): Void {
|
|
if (time < lastTime) {
|
|
current = time;
|
|
lastTime = time;
|
|
lastFrameEnd = time;
|
|
|
|
warpTimeTasksBack(time, outdatedTimeTasks);
|
|
warpTimeTasksBack(time, timeTasks);
|
|
|
|
for (task in outdatedTimeTasks) {
|
|
if (task.next >= time) {
|
|
timeTasksScratchpad.push(task);
|
|
}
|
|
}
|
|
for (task in timeTasksScratchpad) {
|
|
outdatedTimeTasks.remove(task);
|
|
}
|
|
for (task in timeTasksScratchpad) {
|
|
insertSorted(timeTasks, task);
|
|
}
|
|
while (timeTasksScratchpad.length > 0) {
|
|
timeTasksScratchpad.remove(timeTasksScratchpad[0]);
|
|
}
|
|
}
|
|
else if (time > lastTime) {
|
|
// TODO: Changing startTime line prevents clients from falling into a
|
|
// warp-forward-then-wait-for-systemtime-to-catch-up-loop that causes
|
|
// choppy movement (e.g. every 3rd frame forward 3 times).
|
|
// But it causes backwards jumps in originally constant movements.
|
|
// And on HTML5 packets are received while no frames are executed,
|
|
// which causes the client to overtakes the server and then move
|
|
// farther away with each packet while being unable to synch back
|
|
// (backwards warping is not allowed to change startTime).
|
|
startTime -= (time - lastTime);
|
|
|
|
current = time;
|
|
lastTime = time;
|
|
lastFrameEnd = time;
|
|
|
|
executeTimeTasks(time);
|
|
}
|
|
}
|
|
|
|
public static function executeFrame(): Void {
|
|
var real = realTime();
|
|
var now: Float = real - startTime;
|
|
var delta = now - lastTime;
|
|
|
|
var frameEnd: Float = lastFrameEnd;
|
|
|
|
if (delta >= 0) {
|
|
if (kha.netsync.Session.the() == null) {
|
|
// tdif = 1.0 / 60.0; //force fixed frame rate
|
|
|
|
if (delta > maxframetime) {
|
|
startTime += delta - maxframetime;
|
|
now = real - startTime;
|
|
delta = maxframetime;
|
|
frameEnd += delta;
|
|
}
|
|
else {
|
|
if (vsync) {
|
|
// var measured = delta;
|
|
// this is optimized not to run at exact speed
|
|
// but to run as fluid as possible
|
|
var frames = Math.round(delta / onedifhz);
|
|
if (frames < 1) {
|
|
return;
|
|
}
|
|
|
|
var realdif = frames * onedifhz;
|
|
|
|
delta = realdif;
|
|
for (i in 0...DIF_COUNT - 2) {
|
|
delta += deltas[i];
|
|
deltas[i] = deltas[i + 1];
|
|
}
|
|
delta += deltas[DIF_COUNT - 2];
|
|
delta /= DIF_COUNT;
|
|
deltas[DIF_COUNT - 2] = realdif;
|
|
|
|
frameEnd += delta;
|
|
|
|
// trace("Measured: " + measured + " Frames: " + frames + " Delta: " + delta + " ");
|
|
}
|
|
else {
|
|
for (i in 0...DIF_COUNT - 1) {
|
|
deltas[i] = deltas[i + 1];
|
|
}
|
|
deltas[DIF_COUNT - 1] = delta;
|
|
|
|
var next: Float = 0;
|
|
for (i in 0...DIF_COUNT) {
|
|
next += deltas[i];
|
|
}
|
|
next /= DIF_COUNT;
|
|
|
|
// delta = interpolated_delta; // average the frame end estimation
|
|
|
|
// lastTime = now;
|
|
frameEnd += next;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
frameEnd += delta;
|
|
}
|
|
|
|
lastTime = now;
|
|
|
|
if (!stopped) { // Stop simulation time
|
|
lastFrameEnd = frameEnd;
|
|
}
|
|
|
|
// Extend endpoint by paused time (individually paused tasks)
|
|
for (pausedTask in pausedTimeTasks) {
|
|
pausedTask.next += delta;
|
|
}
|
|
|
|
if (stopped) {
|
|
// Extend endpoint by paused time (running tasks)
|
|
for (timeTask in timeTasks) {
|
|
timeTask.next += delta;
|
|
}
|
|
}
|
|
|
|
executeTimeTasks(frameEnd);
|
|
|
|
// Maintain outdated task list
|
|
for (task in outdatedTimeTasks) {
|
|
if (task.next < frameEnd - timeWarpSaveTime) {
|
|
timeTasksScratchpad.push(task);
|
|
}
|
|
}
|
|
for (task in timeTasksScratchpad) {
|
|
outdatedTimeTasks.remove(task);
|
|
}
|
|
while (timeTasksScratchpad.length > 0) {
|
|
timeTasksScratchpad.remove(timeTasksScratchpad[0]);
|
|
}
|
|
}
|
|
|
|
current = frameEnd;
|
|
|
|
sortFrameTasks();
|
|
for (frameTask in frameTasks) {
|
|
if (!stopped && !frameTask.paused && frameTask.active) {
|
|
if (!frameTask.task())
|
|
frameTask.active = false;
|
|
}
|
|
}
|
|
|
|
for (frameTask in frameTasks) {
|
|
if (!frameTask.active) {
|
|
toDeleteFrame.push(frameTask);
|
|
}
|
|
}
|
|
|
|
while (toDeleteFrame.length > 0) {
|
|
frameTasks.remove(toDeleteFrame.pop());
|
|
}
|
|
}
|
|
|
|
static function executeTimeTasks(until: Float) {
|
|
while (timeTasks.length > 0) {
|
|
activeTimeTask = timeTasks[0];
|
|
|
|
if (activeTimeTask.next <= until) {
|
|
current = activeTimeTask.next;
|
|
|
|
activeTimeTask.next += activeTimeTask.period;
|
|
timeTasks.remove(activeTimeTask);
|
|
|
|
if (activeTimeTask.active && activeTimeTask.task()) {
|
|
if (activeTimeTask.period > 0
|
|
&& (activeTimeTask.duration == 0 || activeTimeTask.duration >= activeTimeTask.start + activeTimeTask.next)) {
|
|
insertSorted(timeTasks, activeTimeTask);
|
|
}
|
|
else {
|
|
archiveTimeTask(activeTimeTask, until);
|
|
}
|
|
}
|
|
else {
|
|
activeTimeTask.active = false;
|
|
archiveTimeTask(activeTimeTask, until);
|
|
}
|
|
}
|
|
else {
|
|
break;
|
|
}
|
|
}
|
|
activeTimeTask = null;
|
|
}
|
|
|
|
static function archiveTimeTask(timeTask: TimeTask, frameEnd: Float) {
|
|
#if sys_server
|
|
if (timeTask.next > frameEnd - timeWarpSaveTime) {
|
|
outdatedTimeTasks.push(timeTask);
|
|
}
|
|
#end
|
|
}
|
|
|
|
/**
|
|
* An approximation of the amount of time (in fractional seconds) that elapsed while the game was active.
|
|
* This value is optimized for achieving smooth framerates.
|
|
*/
|
|
public static function time(): Float {
|
|
return current;
|
|
}
|
|
|
|
/**
|
|
* The amount of time (in fractional seconds) that elapsed since the game started.
|
|
*/
|
|
public static function realTime(): Float {
|
|
return System.time;
|
|
}
|
|
|
|
public static function resetTime(): Void {
|
|
var now = System.time;
|
|
var dif = now - startTime;
|
|
startTime = now;
|
|
for (timeTask in timeTasks) {
|
|
timeTask.start -= dif;
|
|
timeTask.next -= dif;
|
|
}
|
|
for (i in 0...DIF_COUNT)
|
|
deltas[i] = 0;
|
|
current = 0;
|
|
lastTime = 0;
|
|
lastFrameEnd = 0;
|
|
}
|
|
|
|
public static function addBreakableFrameTask(task: Void->Bool, priority: Int): Int {
|
|
frameTasks.push(new FrameTask(task, priority, ++currentFrameTaskId));
|
|
frame_tasks_sorted = false;
|
|
return currentFrameTaskId;
|
|
}
|
|
|
|
public static function addFrameTask(task: Void->Void, priority: Int): Int {
|
|
return addBreakableFrameTask(function() {
|
|
task();
|
|
return true;
|
|
}, priority);
|
|
}
|
|
|
|
public static function pauseFrameTask(id: Int, paused: Bool): Void {
|
|
for (frameTask in frameTasks) {
|
|
if (frameTask.id == id) {
|
|
frameTask.paused = paused;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
public static function removeFrameTask(id: Int): Void {
|
|
for (frameTask in frameTasks) {
|
|
if (frameTask.id == id) {
|
|
frameTask.active = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
public static function generateGroupId(): Int {
|
|
return ++currentGroupId;
|
|
}
|
|
|
|
public static function addBreakableTimeTaskToGroup(groupId: Int, task: Void->Bool, start: Float, period: Float = 0, duration: Float = 0): Int {
|
|
var t = new TimeTask();
|
|
t.active = true;
|
|
t.task = task;
|
|
t.id = ++currentTimeTaskId;
|
|
t.groupId = groupId;
|
|
|
|
t.start = current + start;
|
|
t.period = 0;
|
|
if (period != 0)
|
|
t.period = period;
|
|
t.duration = 0; // infinite
|
|
if (duration != 0)
|
|
t.duration = t.start + duration;
|
|
|
|
t.next = t.start;
|
|
insertSorted(timeTasks, t);
|
|
return t.id;
|
|
}
|
|
|
|
public static function addTimeTaskToGroup(groupId: Int, task: Void->Void, start: Float, period: Float = 0, duration: Float = 0): Int {
|
|
return addBreakableTimeTaskToGroup(groupId, function() {
|
|
task();
|
|
return true;
|
|
}, start, period, duration);
|
|
}
|
|
|
|
public static function addBreakableTimeTask(task: Void->Bool, start: Float, period: Float = 0, duration: Float = 0): Int {
|
|
return addBreakableTimeTaskToGroup(0, task, start, period, duration);
|
|
}
|
|
|
|
public static function addTimeTask(task: Void->Void, start: Float, period: Float = 0, duration: Float = 0): Int {
|
|
return addTimeTaskToGroup(0, task, start, period, duration);
|
|
}
|
|
|
|
static function getTimeTask(id: Int): TimeTask {
|
|
if (activeTimeTask != null && activeTimeTask.id == id)
|
|
return activeTimeTask;
|
|
for (timeTask in timeTasks) {
|
|
if (timeTask.id == id) {
|
|
return timeTask;
|
|
}
|
|
}
|
|
for (timeTask in pausedTimeTasks) {
|
|
if (timeTask.id == id) {
|
|
return timeTask;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public static function pauseTimeTask(id: Int, paused: Bool): Void {
|
|
var timeTask = getTimeTask(id);
|
|
if (timeTask != null) {
|
|
pauseRunningTimeTask(timeTask, paused);
|
|
}
|
|
if (activeTimeTask != null && activeTimeTask.id == id) {
|
|
activeTimeTask.paused = paused;
|
|
}
|
|
}
|
|
|
|
static function pauseRunningTimeTask(timeTask: TimeTask, paused: Bool): Void {
|
|
timeTask.paused = paused;
|
|
if (paused) {
|
|
timeTasks.remove(timeTask);
|
|
pausedTimeTasks.push(timeTask);
|
|
}
|
|
else {
|
|
insertSorted(timeTasks, timeTask);
|
|
pausedTimeTasks.remove(timeTask);
|
|
}
|
|
}
|
|
|
|
public static function pauseTimeTasks(groupId: Int, paused: Bool): Void {
|
|
if (paused) {
|
|
for (timeTask in timeTasks) {
|
|
if (timeTask.groupId == groupId) {
|
|
pauseRunningTimeTask(timeTask, paused);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
for (timeTask in pausedTimeTasks) {
|
|
if (timeTask.groupId == groupId) {
|
|
pauseRunningTimeTask(timeTask, paused);
|
|
}
|
|
}
|
|
}
|
|
if (activeTimeTask != null && activeTimeTask.groupId == groupId) {
|
|
activeTimeTask.paused = paused;
|
|
}
|
|
}
|
|
|
|
public static function removeTimeTask(id: Int): Void {
|
|
var timeTask = getTimeTask(id);
|
|
if (timeTask != null) {
|
|
timeTask.active = false;
|
|
timeTasks.remove(timeTask);
|
|
}
|
|
}
|
|
|
|
public static function removeTimeTasks(groupId: Int): Void {
|
|
for (timeTask in timeTasks) {
|
|
if (timeTask.groupId == groupId) {
|
|
timeTask.active = false;
|
|
timeTasksScratchpad.push(timeTask);
|
|
}
|
|
}
|
|
for (timeTask in timeTasksScratchpad) {
|
|
timeTasks.remove(timeTask);
|
|
}
|
|
while (timeTasksScratchpad.length > 0) {
|
|
timeTasksScratchpad.remove(timeTasksScratchpad[0]);
|
|
}
|
|
|
|
if (activeTimeTask != null && activeTimeTask.groupId == groupId) {
|
|
activeTimeTask.active = false;
|
|
}
|
|
}
|
|
|
|
public static function numTasksInSchedule(): Int {
|
|
return timeTasks.length + frameTasks.length;
|
|
}
|
|
|
|
static function insertSorted(list: Array<TimeTask>, task: TimeTask) {
|
|
for (i in 0...list.length) {
|
|
if (list[i].next > task.next) {
|
|
list.insert(i, task);
|
|
return;
|
|
}
|
|
}
|
|
list.push(task);
|
|
}
|
|
|
|
static function sortFrameTasks(): Void {
|
|
if (frame_tasks_sorted)
|
|
return;
|
|
frameTasks.sort(function(a: FrameTask, b: FrameTask): Int {
|
|
return a.priority > b.priority ? 1 : ((a.priority < b.priority) ? -1 : 0);
|
|
});
|
|
frame_tasks_sorted = true;
|
|
}
|
|
}
|