Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

requestAnimationFrame loop not correct FPS

I have a javascript function that my game loops through (hopefully) 60 times a second that controls input, drawing, etc.

The way it is currently coded it seems to be always be around 52, noticeably lower than 60 fps, and it even dips to 25-30 fps even when nothing else is happening

function loop() {
    setTimeout(function () {
        requestAnimationFrame(loop);
        time += (1000 / 60);
        if (time % 600 == 0) {
            oldtick = tick;
            tick += 1;
            time = 0;
            aiMovement();
            combat();
        }
        context.clearRect(0, 0, c.width, c.height);
        drawMap();
        playerInput();
        movePlayer();
        drawEntities();
        drawPopups();
        var thisLoop = new Date;
        var fps = 1000 / (thisLoop - lastLoop);
        lastLoop = thisLoop;
        context.drawImage(cursor, mouse.x, mouse.y, 16, 16);
        context.fillStyle = "#ffff00";
        context.fillText("FPS: " + Math.floor(fps) + " Time: " + Math.floor(time) + " tick: " + tick, 10, 450);
        context.fillText("Gold: " + gold, 10, 460);

        //requestAnimationFrame(loop);
    }, 1000 / 60);
}

if I remove the setTimeout and the first requestAnimationFrame from the top and uncomment the reuqestAnimationFrame at the bottom and remove the other setTimeout things, the FPS improves to 58 but rapidly changes between 58 and 62, again, not statically 60. Does it have something to do with 1000/60 is not a whole number? How would people using requestAnimationFrame achieve 60 fps if this was true?

like image 442
baiomu Avatar asked Apr 12 '17 21:04

baiomu


1 Answers

Don`t use setTimeout or setInterval for animation.

The problem is that you are calling a timer event from within the request animation event. Remove the timeout and just use requestAnimationFrame.

function loop(time){  // microsecond timer 1/1,000,000 accuracy in ms 1/1000th
    // render code here
    requestAnimationFrame(loop);
    // or render code here makes no diff
}
requestAnimationFrame(loop); // to start

RequestAnimationFrame (rAF) is always in sync (unless the browser has vertical sync turned off). The next frame will be presented in 1/60th, 2/60th, 3/60th etc... of a second. You will not get 52frame per second using rAF, rather 60fps, 30fps, 15fps, etc...

The Demo below show the difference in use.

Because requestAnimationFrame uses some smarts to time the animation they can not both run at the same time so click on the canvas to start it.

You can also add a load to simulate rendering. There is a 14ms load and a 28 ms load. The 28ms load is design to mess up rAF as it will on many machines flick between 30 and 60 frames per second. The point is to show that rAF can only have 60, 30, 20,.. etc frames per second.

var ctx1 = can1.getContext("2d");
var ctx2 = can2.getContext("2d");
var ctx3 = can3.getContext("2d");
var lastTime1 = 0;
var lastTime2 = 0;
var lastTime3 = 0;
var frameFunction = frame1;
var frameText = "";
var drag = false;
var loadAmount = 14;
var stats = [{
     data : [],
     pos : 0,
     add(val){
         this.data[(this.pos ++) % 150] = val;
     }
   },{
     data : [],
     pos : 0,
     add(val){
         this.data[(this.pos ++) % 150] = val;
     }
   },{
     data : [],
     pos : 0,
     add(val){
         this.data[(this.pos ++) % 150] = val;
     }
   }   
];
for(let i = 0; i <  150; i += 1){
    stats[0].add(0);
    stats[1].add(0);
    stats[2].add(0);
}
setupContext(ctx1);
setupContext(ctx2);
setupContext(ctx3);
drawFrameTime(ctx1,0);
drawFrameTime(ctx2,0);
drawFrameTime(ctx3,0);
can1.addEventListener("click",()=>frameFunction = frame1);
can2.addEventListener("click",()=>frameFunction = frame2);
can3.addEventListener("click",()=>frameFunction = frame3);
load.addEventListener("click",()=>{
    if(drag){
        drag = false;
        load.value = "Add load.";
    }else{
        drag = true;
        load.value = "Remove load.";
    }
});
loadPlus.addEventListener("click",()=>{
    if(loadAmount === 14){
        loadAmount = 28;
        loadPlus.value = "28ms";
    }else{
        loadAmount = 14;
        loadPlus.value = "14ms";
    }
});

function CPULoad(){
    if(drag){
        var stopAt = performance.now() + loadAmount;
        while(performance.now() < stopAt);
    }
}    
function setupContext(ctx){
    ctx.font = "64px arial";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
}
function drawStats(ctx,stat){
    ctx.setTransform(1,0,0,1,0,64);
    ctx.strokeStyle = "red";
    ctx.strokeRect(-1,16.666,152,0);
    ctx.strokeStyle = "black";
    ctx.beginPath();
    var i = stat.pos + 149;
    var x = 0;
    ctx.moveTo(x,stat.data[(i++) % 150]);
    while(x ++ < 150 && stat.data[i % 150] !== undefined) {
        ctx.lineTo(x,stat.data[(i++) % 150]);
    }
    ctx.stroke();

}

function drawFrameTime(ctx,time){
    ctx.fillStyle = "black";
    ctx.setTransform(1,0,0,1,0,0);
    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    if(time > 0){
        ctx.fillStyle = drag ? "red" : "black";
        ctx.setTransform(1,0,0,1,ctx.canvas.width / 2,ctx.canvas.height *0.25);
        ctx.fillText(time,0,0);
        ctx.setTransform(0.4,0,0,0.4,ctx.canvas.width / 2,ctx.canvas.height * 0.75);
        
        ctx.fillText(Math.round(1000 /  Number(time)) + "fps",0,0);
    }else{
        ctx.setTransform(0.4,0,0,0.4,ctx.canvas.width / 2,ctx.canvas.height * 0.75);
        ctx.fillText("Click to Start.",0,0);
    
    }
    ctx.fillStyle = "black";
    ctx.setTransform(0.2,0,0,0.2,ctx.canvas.width / 2,ctx.canvas.height * 0.9);
    ctx.fillText(frameText,0,0);
    if(drag){
        ctx.fillStyle = "red";
        ctx.setTransform(0.2,0,0,0.2,ctx.canvas.width / 2,ctx.canvas.height * 0.5);
        ctx.fillText("Load " + loadAmount + "ms",0,0);
    
    }
    
}



function frame1(time){
     requestAnimationFrame(frameFunction);
     frameText = "Using rAF.";
     var frameTime = time - lastTime1;
     lastTime1 = time;
     stats[0].add(frameTime);
     drawFrameTime(ctx1,frameTime.toFixed(2));
     drawStats(ctx1,stats[0]);
     CPULoad()
}
    
function frame2() {
    setTimeout(function () {
        frameText = "Using rAF & setTimeout.";
        var time = performance.now();
        var frameTime = time - lastTime2;
        stats[1].add(frameTime);
        lastTime2 = time;
        drawFrameTime(ctx2, frameTime.toFixed(2));
        drawStats(ctx2,stats[1]);
        CPULoad();
        requestAnimationFrame(frameFunction);
    }, 1000 / 60);
}
function frame3() {
    setTimeout(frameFunction,1000/60);
    frameText = "SetTimeout by itself.";
    var time = performance.now();
    var frameTime = time - lastTime3;
    stats[2].add(frameTime);
    lastTime3 = time;
    drawFrameTime(ctx3, frameTime.toFixed(2));
    drawStats(ctx3,stats[2]);
    CPULoad();

}
requestAnimationFrame(frameFunction);
body {
    font-family : arial ;
}
canvas {
    border : 1px solid black;
}
div {
   text-align : center;
}
<div><h2>RequestAnimationFrame (rAF)</h2>
rAF V rAF & setTimeout V setTimeout<br>
<canvas id = can1 width = 150></canvas>
<canvas id = can2 width = 150></canvas>
<canvas id = can3 width = 150></canvas><br>
Click the frame to set the current test.<br>
The left frame is using rAF alone, the middle using setTimeout and rAf, and the rigth frame uses setTimeout alone.<br>
Click <input type="button" id=load value="add Load"></input> to simulate a rendering load of around <input type="button" id=loadPlus value="14ms" title="click to change CPU load between 14 and 28ms"></input> <br>
   Try draging and selecting this text and see how it effects the different methods.<br>
rAF is by far the most stable of the 3.<br>
</div>
like image 132
Blindman67 Avatar answered Sep 28 '22 21:09

Blindman67