How to implement a deterministic/tick-based game loop? - javascript

First of all, I'm trying to make a "simple" 3D game using Three.js and in the future some network framework to make it multiplayer, since I plan on doing the network part in the future I searched a little and discovered that most "action" game use a "tick" based game loop to make it possible to sync the clients and the server, and then interpolate between the ticks to make it smooth.
I already have some "working" code of the tick (handle input, update, draw) function, what I want to know is if my implementation is right, and how this "deterministic" loop should work, supposing that my implementation is working, when I increase the "tick rate" the game gets faster (the update function is running more times), is this right?
this.loops = 0;
this.tick_rate = 20;
this.skip_ticks = 1000 / this.tick_rate;
this.max_frame_skip = 10;
this.next_game_tick = performance.now();
This first part of the code is inside the constructor of the Game class
Game.prototype.run = function () {
this.handle_input();
this.loops = 0;
while (performance.now() > this.next_game_tick && this.loops < this.max_frame_skip){
this.up_stats.update();
this.update();
this.next_game_tick += this.skip_ticks;
this.loops++;
}
this.draw();
//monitor performance
this.stats.update();
//next update
requestAnimationFrame(this.run.bind(this));
};
Full code at: https://github.com/derezzedex/first_three_js/blob/master/js/game/main.js

This looks pretty reasonable to me and I've used similar patterns in the past.
Structuring synchronized simulations is a huge topic, but what you have is a good starting point and might be enough depending on the complexity of your game.
edit: A bit more detail...
Yes it works the same, except that this.dt is always the same. i.e. 1000 / your desired FPS for the game loop.
If you want to do the smoothing/interpolation in between frames... you'll have to record the previous state of your object as well.. and you probably won't want to use the Euler rotations, since eulers don't interpolate well. because an angle of 360 degrees, flips back to 0, so the interpolation logic gets weird...
But instead.. you can record the state before and after the update...
and interpolate the .quaternion instead.. which for small changes in rotation works fine just linear interpolating .. If the changes are too big, you can use quaternion.slerp() which can handle interpolating over big distances.
So you've got lastTickTime, and currentTime, and nextTickTime ....
each frame.. you're doing something like:
To interpolate you do something like:
var alpha= (currentTime-lastTickTime) / (nextTickTime-lastTickTime);//nextTickTime-lastTickTime = your framerate delta so for 60fps = 1000/60 = 16.666666666666668
var recip = 1.0 - alpha;
object.position.x = (object.lastPosition.x * recip)+(object.nextPosition.x*alpha)
object.position.y = (object.lastPosition.y * recip)+(object.nextPosition.y*alpha)
object.position.z = (object.lastPosition.z * recip)+(object.nextPosition.z*alpha)
object.scale.x = (object.lastScale.x * recip)+(object.nextScale.x*alpha)
object.scale.y = (object.lastScale.y * recip)+(object.nextScale.y*alpha)
object.scale.z = (object.lastScale.z * recip)+(object.nextScale.z*alpha)
object.quaternion.x = (object.lastQuaternion.x * recip)+(object.nextQuaternion.x*alpha)
object.quaternion.y = (object.lastQuaternion.y * recip)+(object.nextQuaternion.y*alpha)
object.quaternion.z = (object.lastQuaternion.z * recip)+(object.nextQuaternion.z*alpha)
object.quaternion.w = (object.lastQuaternion.w * recip)+(object.nextQuaternion.w*alpha)
In a proper three app, you probably shouldn't store the lastPosition and nextPosition directly on the object, and instead put it in the object.userData, but whatever.. it will probably still work..

Related

Rope physics - rope not moving correctly under gravity

I am creating a circular motion simulation, where a box of mass m hangs freely on a string. A force will then be applied to the box which will either put it in circular motion, or put it in weird motion depending on the strength of the force applied.
The rope physics works fine - at least it looks ropelike, and I used a Verlet algorithm based upon suggestions from other threads on stackoverflow and various YouTube videos after failing to apply a forces and angles based approach. This is the first time I have used this method.
The problem is that the time dependency doesn't seem to work correctly, its far too slow. I have created a stack blitz with the base code for this part of the sim. Without timeSimSpeed it looks ropey and like I would expect in real life, except far too fast.
I assumed it is something to do how I applied the force and time dependency to the sim:
processPoints2() {
for(var i = 0; i < this.points.length; i++) {
var p = this.points[i];
var timeSimSpeed = ((this.elapsedSinceFrame + this.previousElapsedSinceFrame) / 2000) * this.simulationSpeed;
if(!p.fixed) {
var vx = (p.x - p.oldx);
var vy = (p.y - p.oldy);
p.oldx = p.x;
p.oldy = p.y;
p.x += vx * timeSimSpeed;
p.y += (this.gravity + vy) * timeSimSpeed;
}
}
}
But this, logically at least, seems to make sense to me. Elasped and previousElasped are in ms and averaged which is why I have 2000 there, but even using only the current frame speed its the same outcome. I'm not familiar enough with Verlet methods to be able to work through the mathematics of whether the sticks could be restricting the motion and creating this speed issue.
Any help is much appreciated, even if it is a link to somewhere this has been addressed before that I have failed to see.
EDIT:
OK so after a bunch of comments on this I have gotten around to make some modifications. I have dropped the weird way of calculating velocity and am explicit with that now, and have also been explicit with the distance units (before it was 1 pixel per meter, but this is now defined, and I changed it to 10meters per pixel. I have also updated my stackblitz.
You will notice there are still some errors (the rope has no maximum extension and will keep stretching!) but the major one for me is that it still doesnt fall under gravity correctly...
The processing looks a lot simpler now:
processPoints2() {
for(var i = 0; i < this.points.length; i++) {
var p: point = this.points[i];
var timeSimSpeed: number = (this.elapsedSinceFrame / 1000) * this.simulationSpeed;
if(!p.fixed) {
p.vx += p.ax * timeSimSpeed;
p.vy += p.ay * timeSimSpeed;
p.x += p.vx * this.pixelsPerMeter * timeSimSpeed;
p.y += p.vy * this.pixelsPerMeter * timeSimSpeed;
}
}
}
As it just uses a classical method of calculating velocity and position.
Any help would continue to be appreciated. It has been pointed out that this isnt verlet so I have amended the title and tags to appropriately represent the question.

Three.js: Takes too much time to render first frame

I use Three.js (with WegGL) to render alternating scenes of many image tiles (a few thousands) animating at space. Animations are handled by Tween.js. I use Chrome for testing.
To optimize the image loading, I preload all the texture images before the first scene is displayed. All textures are then saved in memory as THREE.Texture. Now when I prepare a scene for display, I do something like this:
let tile = null, tweens = [], cameraZ = 1000;
for (let y = 0; y < rows; y++){
for (let x = 0; x < columns; x++){
tile = await this.createTile(x, y, [textureSize]);
tile.position.x = x * this.tileWidth - this.picWidth / 2;
tile.position.y = -y * this.tileHeight + this.picHeight / 2;
tile.position.z = cameraZ * 1.1;
tweens.push(new TWEEN.Tween(tile.position)
.to({z: 0}, 4000)
.delay(200 + x * 120 + Math.random() * 1000)
.easing(TWEEN.Easing.Cubic.InOut));
this.scene.add(tile);
}
}
tweens.map(t => t.start());
The scene preparation also include camera and a point light, and takes about 400 ms to complete.
I then render the scene like this:
function render(){
requestAnimationFrame(render);
TWEEN.update();
renderer.render(this.scene, this.camera);
}
render();
Everything is displayed properly by when measuring some processing durations, I see that the first rendering call takes about 1400 ms! Other calls take between 70 to 100 ms.
My final goal is to have multiple scenes like this, play one after another without any freezes. Given that all the required assets are already loaded, what might be the problem and how can I optimize that?
Thanks
During the first frame of rendering, all of your assets and shaders are being compiled and uploaded to the GPU. If you want to avoid that, you'll have to do some tricks behind the scenes.. like forcibly rendering each object once after you load it, perhaps by adding it to a single scene, and calling renderer.render on it. Depending on what the bottleneck is (shader compilation vs asset uploading) this may or may not help.. but the workaround is doing some kind of pre-rendering to force the uploads to the card one at a time, rather than all at once.
Also, as a previous commenter noted, your render loop above has a typo in it.
it should be requestAnimationFrame(render);

Three.js / ShaderParticleEngine — SPE.Group.tick bad delta argument?

I'm using the ShaderParticleEngine library for Three.js to create particle emitters.
I picked several code snippets on the Internet to have a working emitter.
Firstly I believed that is wasn't working.
But in fact, the emitter was displayed on the map, but a single motionless particle was on the screen.
After some debugging I undestood that the particle was moving but infinitely slowly. I need to use tick(delta * 1000) to see the emitter in action. And the result is quite ugly (full of gaps, alone particles).I have no problem of low FPS.
The only solution I found is to remove delta argument in the tick function call: particleGroup.tick().
The result is better but is still deceiving, judge by yourself:
Online Emitter Editor:
My result:
I can't understand. I use the same code proposed in the library examples and I use the export feature in the emitter editor.
If I try other variations (eg. on particle life/velocity) I get a very different result in my game, maybe the particle life is not computed correctly because delta argument isn't given?
My game loop:
var animate = function () {
requestAnimationFrame( animate );
render();
stats.update();
};
var render = function() {
time = ctx3d.clock.getElapsedTime();
delta = ctx3d.clock.getDelta();
particleGroup.tick(delta);
if(ctx3d.move)
{
ctx3d.ship.position.z += delta * 500 * 3000;
//ctx3d.camera.position.x = ctx3d.ship.position.x;
//ctx3d.camera.position.z = ctx3d.ship.position.z;
}
ctx3d.renderer.render(ctx3d.scene, ctx3d.camera);
}
Delta value loop by loop:
30.0000010000003385357559
9.999985195463523e-7
30.0000020000006770715117
0.0000010000003385357559
30.0000020000006770715117
0.0000010000003385357559
0.0000020000006770715117
30.0000010000003385357559
0.000002999999196617864
0.0000010000003385357559
9.999985195463523e-7
0.000002999999196617864
0.0000010000003385357559
0.000001999998858082108
0.0000010000003385357559
20.0000020000006770715117
9.999985195463523e-7
0.0000010000003385357559
To solve smoothness, try the following:
function makeSmoothSPETick(simulator, timeDelta, maxSubstepSize){
var numSubsteps = Math.floor(timeDelta/maxSubstepSize);
var leftOverTime = timeDelta%maxSubstepSize;
while(numSubsteps-->0){
simulator.tick(maxSubstepSize);
}
if(leftOverTime> 0){
//handle the rest
simulator.tick(leftOverTime);
}
}
If you use this function in your code - it will allow you to essentially subdivide steps that are too large into smaller ones of fixed size. As SquareFeet pointed out, say 16ms for 60FPS - you could use something like this:
var render = function() {
time = ctx3d.clock.getElapsedTime();
delta = ctx3d.clock.getDelta();
makeSmoothSPETick(particleGroup, delta, 0.016);
if(ctx3d.move)
{
ctx3d.ship.position.z += delta * 500 * 3000;
//ctx3d.camera.position.x = ctx3d.ship.position.x;
//ctx3d.camera.position.z = ctx3d.ship.position.z;
}
ctx3d.renderer.render(ctx3d.scene, ctx3d.camera);
}
You should get results visually similar to what you'd expect if you were running at smooth 60fps. Beware though, if target hardware can't handle these substeps - you may need to get more logic into your solver algorithm. I'd suggest keeping statistics for past 100 frames or so, and using that to decide how much you can split your incoming step value.
EDIT:
To make sure your timing isn't getting mangled, please try the following:
var lastFrameTime = Date.now()/1000;
var animate = function () {
requestAnimationFrame( animate );
render();
stats.update();
};
var render = function() {
time = Date.now()/1000; //getting current time in seconds since epoch
delta = time-lastFrameTime;
lastFrameTime = time;
particleGroup.tick(delta);
if(ctx3d.move)
{
ctx3d.ship.position.z += delta * 500 * 3000;
//ctx3d.camera.position.x = ctx3d.ship.position.x;
//ctx3d.camera.position.z = ctx3d.ship.position.z;
}
ctx3d.renderer.render(ctx3d.scene, ctx3d.camera);
}
I hope posting this as an answer is okay...
I've bumped the particle engine up a minor version to 0.7.7, having implemented a fix for your issue of "not-very-smooth-looking" emitters.
What was happening before was this:
SPE.Emitter.tick() called with a dt value
This tick function determines how many particles should be marked alive based on the dt argument passed to it. For larger dt values, more particles are marked as alive, for smaller values fewer are marked as alice.
The emitter then resets these particles and waits for the next call.
Assuming more than one particle is going to be marked as alive per frame, and they all originate at the same position in space, then all the particles will be at the same place when they're activated. This is why you saw some "clumping" happening.
What happens now is this:
SPE.Emitter.tick() called with a dt value, just as before.
The tick function now determines how many particles should be marked as alive, and whilst marking them so, sets each particles age to be a fraction of the dt value passed in.
So (!), assuming 100 particles are emitted per frame, and a dt value of 0.016 is passed to the emitter's tick function, each of those 100 particles that will be marked as alive is assigned an age value of (0.016 / 100) * i where i is the particle index (in this case, a value of 0 to 100).
I hope that makes sense. You can see the changes here: https://github.com/squarefeet/ShaderParticleEngine/blob/master/src/ShaderParticleEmitter.js#L240-L246
Master branch has been updated.

requestAnimFrame can't give a constant frame rate, but my physics engine needs it

I use Box2D with WebGL.
Box2D demands a constant frame rate (time steps for it's "world" updates).
function update(time) {//update of box2d world
world.Step(
1/60 // 1 / frame-rate
, 3 //velocity iterations
, 8 //position iterations
);
But I've read that requestAnimFrame defined as below is the right way to go.
requestAnimFrame = (function() {
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(/* function FrameRequestCallback */ callback, /* DOMElement Element */ element) {
window.setTimeout(callback, 1000/60);
};
})();
requestAnimFrame doesn't give me a constant frame rate and so my Box2D's variables are going unsynchronized.
Is there a fix to this?
[EDIT]
John's (Cutch) solution when implemented looks like this:
function interpolate(dt) {
var t = dt/time_step;
body_coordinates = (1-t) * body_coordinates + t * next_body_coordinates;
}
var physicsDt = 0;
function tick() {
var time_now = new Date().getTime();
var dt = time_now - last_time; //Note that last_time is initialized priorly
last_time = time_now;
physicsDt += dt;
clear_the_screen();
requestAnimFrame(tick);
drawEverything();
if(physicsDt >= time_step) {
update();
physicsDt -= time_step;
}
interpolate(dt);
}
Note that my physics update function takes care that the next_attribues are set.
And also, a physics update is called before this, to keep the physics world ahead by 1 frame.
The result
The animation is fairly smooth, except for those times when I can see some really bad jumps and random appearing micro-movements.
I thought the following issues were not addressed in the solution :
----> 1) dt may become larger than time_step : This would make dt/time_step greater than 1 which would ruin the interpolation equations.
When dt remains larger than time_step consistently, problems would increase.
Is it possible to overcome the problem of the time gap becoming larger than time_step ?
I mean, even if we keep the world one frame ahead of the rendering, if time gaps consistently stay greater than time_step, it wouldn't take long pass that "ahead" frame.
----> 2) Imagine dt being lesser than time_step by 1 ms. Then, the world is not updated that one time. Now interpolation is done and the approximate position is found(1 ms behind where it should have been).
Lets say the next time no difference is seen between dt and time_step.
Now, no interpolation is done considering that dt and time_step are equal. So, the next that is drawn is the "ahead" frame in the world, right?(using those equations, with t = 1)
But accurately, the rendered world should be that 1ms behind which it was before.
I mean, that 1ms by which it was behind the world frame should not vanish.
But with t = 1, draws the physics world frame and forgets that 1ms.
Am I wrong about the code or the above 2 points?
I request you to clarify these issues.
[EDIT]
I asked the author of this webpage, for a way to efficiently draw many shapes, in the comments there.
I learnt to do it this way :
I'm saving bufferData calls by keeping separate buffers for each shape and calling createBuffer, bindBuffer, bufferData only once during init.
Everytime I refresh the screen, I have to iterate over all the shapes and
I have to call enableVertexAttribArray and vertexAttribPointer after binding the required shape's buffer(using bindBuffer).
My shapes don't change with time.
There are just a variety of them (like polygons, circles, triangles) that stay from beginning to end.
You must decouple your physics simulation stepping timing from your render vsync timing. The easiest solution is to do this:
frameCallback(dt) {
physicsDt += dt;
if (physicsDt > 16) {
stepPhysics();
physicsDt -= 16;
}
renderWorld();
requestAnimFrame(frameCallback);
}
The biggest issue here is that sometimes you'll be rendering with an outdated physics world, for example, if physicsDt was 15 no simulation update will occur but your objects would have moved almost an entire frame by that point in time. You can work around this by keeping the physics 1 frame ahead of the rendering and linearly interpolating object positions in the renderer. Something like:
var t = dt/16.0;
framePosition = (1-t) * previousFramePositions + (t) * nextFramePositions;
That way your objects move smoothly even if you're rendering is out of sync with your physics simulation. Let me know if you have any questions.
John
requestAnimFrame isn't meant to guarantee a constant frame rate – it's designed so that the browser only does the calculations for frames it actually draws.
If you want/have to calculate frames that aren't drawn, then it isn't the way to go.

HTML Canvas Interval vs RequestAnimationFrame

So, maybe total brainfart here. The syntax for setInterval() is pretty clear. Do something every x miliseconds. How is this best translated to using the requestAnimationFrame() ?
I have about 300 objects and each is supposed to perform an animation sequence at a certain interval (every 8, 6, 2, etc seconds)? How can I best accomplish this using requestAnimationFrame() which gets called ~60 times a second? There is probably an easy answer, I just, for the life of me, can't figure it out.
To force requestAnimationFrame to stick to a specific FPS you can use both at once!
var fps = 15;
function draw() {
setTimeout(function() {
requestAnimationFrame(draw);
// Drawing code goes here
}, 1000 / fps);
}
A little weird, but noth the most confusing thing in the world.
You can also use requestAnimationFrame not with FPS but with elapsed time in order to draw objects that need to be updated based on the time difference since the last call:
var time;
function draw() {
requestAnimationFrame(draw);
var now = new Date().getTime(),
dt = now - (time || now);
time = now;
// Drawing code goes here... for example updating an 'x' position:
this.x += 10 * dt; // Increase 'x' by 10 units per millisecond
}
These two snippets are from this fine article, which contains additional details.
Good question by the way! I don't think I've seen this answered on SO either (and I'm here way too much)
requestAnimationFrame is pretty low level, it just does what you already said: roughly gets called at 60fps (assuming the browser can keep up with that pace). So typically you would need to build something on top of that, much like a game engine that has a game loop.
In my game engine, I have this (paraphased/simplified here):
window.requestAnimationFrame(this._doFrame);
...
_doFrame: function(timestamp) {
var delta = timestamp - (this._lastTimestamp || timestamp);
for(var i = 0, len = this.elements.length; i < len; ++i) {
this.elements[i].update(delta);
}
this._lastTimestamp = timestamp;
// I used underscore.js's 'bindAll' to make _doFrame always
// get called against my game engine object
window.requestAnimationFrame(this._doFrame);
}
Then each element in my game engine knows how to update themselves. In your case each element that should update every 2, 6, 8 seconds needs to keep track of how much time has passed and update accordingly:
update: function(delta) {
this.elapsed += delta;
// has 8 seconds passed?
if(this.elapsed >= 8000) {
this.elapsed -= 8000; // reset the elapsed counter
this.doMyUpdate(); // whatever it should be
}
}
The Canvas API along with requestAnimationFrame are rather low level, they are the building blocks for things like animation and game engines. If possible I'd try to use an existing one like cocos2d-js or whatever else is out there these days.

Categories

Resources