-
Notifications
You must be signed in to change notification settings - Fork 0
/
mainloop.js
196 lines (183 loc) · 6.76 KB
/
mainloop.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
'use strict';
/**
* Start a main loop on the provided game with the provided options.
* @param {Array.<Object>} updateables Objects with two functions: update() and render().
* update(deltaTime) should update the game state. The deltaTime parameter
* is time passed since the last update in seconds.
* render() should draw the current game state and optionally return a
* CanvasRenderingContext2D that the following updateables in the array will use.
* Updateables that are processed after the first one receive this rendering context
* as a parameter.
* @param {Object} options Takes the following keys (all optional):
*
* updateFPS: number
* The rate at which the game state receives update() calls.
* Having a fixed update rate can help you to make the game deterministic
* and to keep physics calculations stable.
* Every update is not necessarily displayed on the screen.
*
* debugMode: boolean
* If Mousetrap is imported, you may hold F to speed up the game
* execution or G to slow it down while in debug mode.
*
* frameLog: boolean
* When frame log is on, a timeline of frames is drawn on the canvas returned
* from updateables[i].render().
* - Green in the log is an update which was rendered to the screen.
* - Orange in the log is an update which was not rendered to the screen.
* - White in the log is a frame on which the game state was not updated.
*
* onRefocus: function
* Function that should be called when the window becomes visible after it
* has been invisible for a while.
*/
var startMainLoop = function(updateables, options) {
var defaults = {
updateFPS: 60,
debugMode: false,
frameLog: false,
onRefocus: null
};
if (options === undefined) {
options = {};
}
for(var key in defaults) {
if(!options.hasOwnProperty(key)) {
options[key] = defaults[key];
}
}
if (!(updateables instanceof Array)) {
updateables = [updateables];
}
var now = function() {
if (typeof performance !== 'undefined' && 'now' in performance) {
return performance.now();
} else {
return Date.now();
}
};
var timePerUpdate = 1000 / options.updateFPS;
var nextFrameTime = -1;
var frameLog = [];
var logTimeToX = function(time, lastTime, canvasWidth) {
var fpsMult = Math.max(60, options.updateFPS) / 60;
return (canvasWidth - 1) - (Math.ceil(lastTime / 2000) * 2000 - time) * 0.2 * fpsMult;
}
var drawFrameLog = function(ctx, callbackTime) {
var w = ctx.canvas.width;
ctx.save();
ctx.globalAlpha = 0.5;
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, w, 10);
ctx.globalAlpha = 1.0;
ctx.fillStyle = '#0f0';
var lastTime = callbackTime;
var i = frameLog.length;
var x = ctx.canvas.width;
while (i > 0 && x > 0) {
--i;
var frameStats = frameLog[i];
if (frameStats.updates >= 1) {
ctx.fillRect(logTimeToX(frameStats.time, lastTime, w), 0, 2, 10);
if (frameStats.updates > 1) {
ctx.fillStyle = '#f84';
for (var j = 1; j < frameStats.updates; ++j) {
var updateX = logTimeToX(frameStats.time - (j - 0.5) * timePerUpdate, lastTime, w);
ctx.fillRect(Math.round(updateX), 0, 2, 10);
}
ctx.fillStyle = '#0f0';
}
} else {
ctx.fillStyle = '#fff';
ctx.fillRect(logTimeToX(frameStats.time, lastTime, w), 0, 2, 10);
ctx.fillStyle = '#0f0';
}
}
ctx.restore();
};
var visible = true;
var visibilityChange = function() {
visible = document.visibilityState == document.PAGE_VISIBLE || (document.hidden === false);
nextFrameTime = -1;
if (visible && options.onRefocus != null) {
options.onRefocus();
}
};
document.addEventListener('visibilitychange', visibilityChange);
var fastForward = false;
var slowedDown = false;
if (options.debugMode && typeof Mousetrap !== 'undefined' && 'bindGlobal' in Mousetrap) {
var speedUp = function() {
fastForward = true;
};
var noSpeedUp = function() {
fastForward = false;
};
var slowDown = function() {
slowedDown = true;
};
var noSlowDown = function() {
slowedDown = false;
};
Mousetrap.bindGlobal('f', speedUp, 'keydown');
Mousetrap.bindGlobal('f', noSpeedUp, 'keyup');
Mousetrap.bindGlobal('g', slowDown, 'keydown');
Mousetrap.bindGlobal('g', noSlowDown, 'keyup');
}
var frame = function() {
// Process a single requestAnimationFrame callback
if (!visible) {
requestAnimationFrame(frame);
return;
}
var time = now();
var callbackTime = time;
var updated = false;
var updates = 0;
if (nextFrameTime < 0) {
nextFrameTime = time - timePerUpdate * 0.5;
}
// If there's been a long time since the last callback, it can be a sign that the game
// is running very badly but it is possible that the game has gone out of focus entirely.
// In either case, it is reasonable to do a maximum of half a second's worth of updates
// at once.
if (time - nextFrameTime > 500) {
nextFrameTime = time - 500;
}
while (time > nextFrameTime) {
if (fastForward) {
nextFrameTime += timePerUpdate / 5;
} else {
if (slowedDown) {
nextFrameTime += timePerUpdate * 5;
} else {
nextFrameTime += timePerUpdate;
}
}
for (var i = 0; i < updateables.length; ++i) {
updateables[i].update(timePerUpdate * 0.001);
}
updates++;
}
if (options.frameLog) {
frameLog.push({time: callbackTime, updates: updates});
}
if (updates > 0) {
var ctx = updateables[0].render();
for (var i = 1; i < updateables.length; ++i) {
var candidateCtx = updateables[i].render(ctx);
if (candidateCtx !== undefined) {
ctx = candidateCtx;
}
}
if (options.frameLog && (ctx instanceof CanvasRenderingContext2D)) {
drawFrameLog(ctx, callbackTime);
if (frameLog.length >= 1024) {
frameLog.splice(0, 512);
}
}
}
requestAnimationFrame(frame);
};
frame();
};