I am creating some simple user controlled simulations with JavaScript and the canvas element.
Currently I have a separate update loop (using setTimeout) and render loop (using requestAnimationFrame).
Updates are scaled using a time delta, so consistency is not critical in that sense. The reason is rather that I don't want any hick-ups in the render loop to swallow user input or otherwise make the simulation less responsive.
The update loop will likely run at a lower (but hopefully fixed) frame rate.
Is this a good practice in JavaScript, or are there any obvious pitfalls? My hope is that the update loop will receive priority, but my understanding of the event loop might be a bit simplistic. (In worst case, the behaviour differs between VM implementations.)
Example code:
function update() {
// Update simulation and process input
setTimeout(update, 1000 / UPDATE_RATE);
}
function render() {
// Render simulation onto canvas
requestAnimationFrame(render);
}
function init() {
update();
render();
}
These concerns have been addressed in Game Development with Three.js by Isaac Sukin. It covers both the case of low rendering frame rates, which was the primary concern of this question:
[...] at low frame rates and high speeds, your object will be moving large distances every frame, which can cause it to do strange things such as move through walls.
It also covers the converse case, with high rendering frame rates, and relatively slow physics computations:
At high frame rates, computing your physics might take longer than the amount of time between frames, which will cause your application to freeze or crash.
In addition, it also addresses the concept of determinism, which becomes important in multiplayer games, and games that rely on it for things like replays or anti-cheat mechanisms:
Additionally, we would like perfect reproducibility. That is, every time we run the application with the same input, we would like exactly the same output. If we have variable frame deltas, our output will diverge the longer the program runs due to accumulated rounding errors, even at normal frame rates.
The practice of running multiple loops is advised against, as this can have severe and hard to debug performance implications. Instead an approach is taken where time deltas are accumulated in the rendering loop, until a fixed, preset size is reached, at which point it is passed to the physics loop for processing:
A better solution is to separate physics update time-steps from frame refresh time-steps. The physics engine should receive fixed-size time deltas, while the rendering engine should determine how many physics updates should occur per frame.
Here's some example code, showing a minimum implementation in JavaScript:
var INVERSE_MAX_FPS = 1 / 60;
var frameDelta = 0;
var lastUpdate = Date.now();
function render() {
// Update and render simulation onto canvas
requestAnimationFrame(render);
var now = Date.now();
frameDelta += now - lastUpdate;
lastUpdate = now;
// Run as many physics updates as we have missed
while(frameDelta >= INVERSE_MAX_FPS) {
update();
frameDelta -= INVERSE_MAX_FPS;
}
}
function init() {
render();
}
With the following code, no matter how long since the last rendered frame, as many physics updates as required will be processed. Any residual time delta will be carried over to the next frame.
Note that the target maximum FPS might need to be adjusted depending on how slow the simulation runs.
Related
I've been facing a strange issue beyond my understanding of how to fix it.
I'm trying to create the following multiplayer game structure:
The server running at 20-30 fps
The client logic loop at the same FPS as the server
The client render loop
I'm using PixiJS for UI and here is where I got stuck.
( I've opened a thread here as well )
And I have a demo here: https://playcode.io/1045459
Ok, now let's explain the issue!
private update = () => {
let elapsed = PIXI.Ticker.shared.elapsedMS
if (elapsed > 1000) elapsed = this.frameDuration
this.lag += elapsed
//Update the frame if the lag counter is greater than or
//equal to the frame duration
while (this.lag >= this.frameDuration) {
//Update the logic
console.log(`[Update] FPS ${Math.round(PIXI.Ticker.shared.FPS)}`)
this.updateInputs(now())
//Reduce the lag counter by the frame duration
this.lag -= this.frameDuration
}
// Render elements in-between states
const lagOffset = this.lag / this.frameDuration
this.interpolateSpaceships(lagOffset)
}
In the client loop I keep track of both logic & render parts, limiting the logic one at 20FPS. It all works "cookies and clouds" until the browser has a sudden frame rate drop from 120fps to 60fps. Based on my investigation and a nice & confusing spreadsheet that I've put together when the frame rate drops, the "player" moves 2x more ( eg. 3.3 instead of 1.66 ) On paper it's normal and the math is correct, BUT this creates a small bounce / jitter / stutter or whatever naming this thing has.
In the demo that I've created in playcode it's not visible. My assumption is that the code is too basic and the framerate never drops.
Considering that the math and the algorithm are correct ( which I'm not yet sure ), I've turned my eyes to other parts that might affect this. I'm using pixi-viewport to follow the character. Could it be that the following part creates this bounce?
Does anyone have experience writing such a game loop?
Update:
Okkkk, mindblowing result. I just found out that this happens even with the most simple version of the game loop ever. Just by multiplying x = x + speed * delta every frame.
For the same reason. Sudden drops in FPS.
Ok, I've found the solution. Will post it here as there is not a lot of info about it. The solution is to smooth out sudden fps drops over multiple frames. Easy right? 😅
const ticker = new PIXI.Ticker();
// The number of frames to use for smoothing
const smoothingFrames = 10;
// The smoothed frame duration
let smoothedFrameDuration = 0;
ticker.add((deltaTime) => {
// Add the current frame duration to the smoothing array
smoothedFrameDuration = (smoothedFrameDuration * (smoothingFrames - 1) + deltaTime) / smoothingFrames;
// Update the game logic here
// Use the smoothed frame duration instead of the raw deltaTime value
});
ticker.start();
When using PixiJS, I noticed that sprites that I move in a straight line at constant speed sometimes "jump" seemingly randomly - usually falling slightly behind their expected position. This is especially noticeable with fast-moving projectiles.
I assume this may have to do with my update loop, and especially how I use Pixi's delta.
My way of adding my update loop to Pixi's ticker:
pixiApp.ticker.add(update);
My update loop using Pixi's delta:
update(delta) {
entity.x += direction * speed * delta;
animatedSprite.x = entity.x;
}
I assume what is happening is whenever delta drops, my entity moves a shorter distance that frame, leading to the "jumps" and "falling behind" I see in the entity's movement.
I have also read that - unlike other engines - in PixiJS, delta is not actually "the time elapsed since the last frame in ms", but rather something else that's not in ms and that I'm not sure I fully understand.
Is there a way around this? I read something about rolling your own update loop, and Pixi's ticker potentially only being meant for updating the sprite animations themselves, but not for movement and other gameplay mechanics in the way I do it here?
So I want to make a small JavaScript game.
And in order to do that I need to animate a few things.
I went researching and found about setInterval and requestAnimationFrame.
I can use either of those 2 for making my game work, however I understood that requestAnimationFrame is the better alternative there.
The problem I see with this is that while the function has its benefits , you are unable to set a framerate , or an update rate for it easily.
I found another thread that explained a way of making this work however it seemed somewhat complicated.
Controlling fps with requestAnimationFrame?
Is there an easier way of animating with a set framerate ?
Is there an easier way of animating with a set framerate ?
Simply put: no. Since rendering is one of the most computation heavy process for a browser, which can be triggered in various ways, it is not possible to foretell how long an update will take, since it can range from drawing one circle on a canvas up to a complete replace of all the visible content of the page.
To overcome this, browser offer a way to call a function a often as possible and the developer is responsible to make his animation sensitive to different time deltas/time steps.
One way to tackle that is to use the concept of velocity. velocity = distance / time. If you want an asset to have a constant velocity you can do the following, since distance = velocity * time follows:
var
asset_velocity = 1; //pixel per millisecond
last = new Date().getTime()
;
(function loop () {
var
now = new Date().getTime(),
delta = now - last,
distance = asset_velocity * delta
;
//update the asset
last = now;
window.requestAnimationFrame(loop)
})();
I am making a small game in HTML5 with the canvas elements. It runs great on MOST computers, but it is lags on others. However, it doesn't skip frames, it continues to render each frame and the game slows down. I am trying to write a function to skip frames, but I can't come up with a formula to do it.
I've tried searching around, but I have found nothing.
I have a function that renders the game called render and it is on a loop like this:
var renderTimer = setInterval("render(ctx)", 1000/CANVAS_FPS);
render()
{
/* render code here */
}
Thank you for any help,
Brandon Pfeifer
This pattern will allow you to skip frames on computers known to be slow
var isSlowComputer=true;
var FrameSkipper=5;
function render(){
// if this is a slow computer
// just draw every 5th frame
if(isSlowComputer && --FrameSkipper>0){ return; }
// reset the frame skipper
FrameSkipper=5;
// draw your frame now
}
If your target market is people with HTML5 capable browsers, you can just use window.requestAnimationFrame. This allows all of your rendering logic to be bound in a simple place, and it will slow down only if it has to. It will try hard to reach the 16ms per frame allocation, which gets you to your 60fps.
var canvas = document.getElementById("#canvas");
(function drawFrame(){
window.requestAnimationFrame(drawFrame, canvas);
// your main code would fire off here
}());
As long as you let the browser figure out the frame rate you're golden.
I've written some different experiences using the canvas before, and until I used requestAnimationFrame things were a little choppy.
One other thing to keep in mind is the double buffer. If you are going to write a lot of things to the screen at any given moment, I find it is easier to write them all to a second hidden canvas element, then just use context.drawImg(buffer,0,0); that will get rid of a lot of the chop. As long as you have thought your code through the canvas shouldn't get choppy even under a lot of streign.
Good Luck
How can I tell if the canvas's slow performance is caused by the drawing itself, or the underlying logic that calculates what should be drawn and where?
The second part of my question is: how to calculate canvas fps? Here's how I did it, seems logical to me, but I can be absolutely wrong too. Is this the right way to do it?
var fps = 0;
setInterval(draw, 1000/30);
setInterval(checkFps, 1000);
function draw() {
//...
fps++;
}
function checkFps() {
$("#fps").html(fps);
fps = 0;
}
Edit:
I replaced the above with the following, according to Nathan's comments:
var lastTimeStamp = new Date().getTime();
function draw() {
//...
var now = new Date().getTime();
$("#fps").html(Math.floor(1000/(now - lastTimeStamp)));
lastTimeStamp = now;
}
So how's this one? You could also calculate only the difference in ms since the last update, performance differences can be seen that way too. By the way, I also did a side-by-side comparison of the two, and they usually moved pretty much together (a difference of 2 at most), however, the latter one had bigger spikes, when performance was extraordinarily low.
Your FPS code is definitely wrong
setInterval(checkFps, 1000);
No-one assures this function will be called exactly every second (it could be more than 1000ms, or less - but probably more), so
function checkFps() {
$("#fps").html(fps);
fps = 0;
}
is wrong (if fps is 32 at that moment it is possible that you have 32 frames in 1.5s (extreme case))
beter is to see what was the real time passes since the last update and calculate the realtimepassed / frames (I'm sure javascript has function to get the time, but I'm not sure if it will be accurate enough = ms or better)
fps is btw not a good name, it contains the number of frames (since last update), not the number of frames per second, so frames would be a better name.
In the same way
setInterval(draw, 1000/30);
is wrong, since you want to achieve a FPS of 30, but since the setInterval is not very accurate (and is probably going to wait longer than you say, you will end up with lower FPS even if the CPU is able to handle the load)
Webkit and Firebug both provide profiling tools to see where CPU cycles are being spent in your javascript code. I'd recommend starting there.
For the FPS calculation, I don't think your code is going to work, but I don't have any good recommendation :(
Reason being: Most (all?) browsers use a dedicated thread for running javascript and a different thread for running UI updates. If the Javascript thread is busy, the UI thread won't be triggered.
So, you can run some javascript looping code that'll "update" the UI 1000 times in succession (for instance, setting the color of some text) - but unless you add a setTimeout to allow the UI thread to paint the change, you won't see any changes until the 1000 iterations are finished.
That said, I don't know if you can assertively increment your fps counter at the end of the draw() routine. Sure, your javascript function has finished, but did the browser actually draw?
Check if you dont use some innerHTML method to debug your project. This can slow your project in a way you can't imagine, especially if you do some concatenation like this innerHTML += newDebugValues;
Or like desau said, profile your cpu usage with firebug or webkit inner debugger.