requestAnimationFrame循环的FPS不正确

10
我有一个 JavaScript 函数,我的游戏每秒循环(希望)60 次,控制输入、绘图等等。
目前的代码似乎总是在 52 左右,明显低于 60 帧每秒,即使什么都不发生,它甚至会降至 25-30 帧每秒。
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);
}

如果我从顶部移除setTimeout和第一个requestAnimationFrame,并取消注释底部的reuqestAnimationFrame并删除其他setTimeout内容,则FPS提高到58,但在58和62之间迅速变化,仍然不是静态的60。这是否与1000/60不是整数有关?如果是这样,使用requestAnimationFrame的人们如何实现60 fps?


1
setInterval 也不能保证精确的时间,它可能会有相当大的偏差。更好的解决方案是利用作为 requestAnimationFrame 回调函数第一个参数提供的时间戳,并根据上次和当前回调执行之间的时间差计算任何运动学/动力学/物理学。 - le_m
3
解释:rAF是requestAnimationFrame的缩写,timeout是指定时器的一种方式。翻译:解除此超时对你的requestAnimationFrame的限制...这就像把法拉利停在卡车上,并希望卡车能像法拉利一样快,毫无意义。 - Kaiido
2个回答

36

不要使用setTimeout或setInterval进行动画。

问题在于你正在从请求动画事件中调用定时器事件。移除timeout,只需使用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)始终保持同步(除非浏览器关闭了垂直同步)。下一帧将在1/60、2/60、3/60秒等时间内呈现。使用rAF时,您不能获得每秒52帧,而是获得60fps、30fps、15fps等。以下演示显示了使用上的差异。由于requestAnimationFrame使用一些智能来计时动画,它们不能同时运行,因此单击画布以启动它。您还可以添加负载以模拟渲染。有一个14ms的负载和一个28ms的负载。设计28ms负载是为了让rAF出现问题,在许多机器上会在30和60帧之间闪烁。重点是要表明rAF只能每秒有60、30、20等帧。

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>


-1

该函数的目的不是为了达到60 FPS,而是在绘制帧时进行绘制并提高性能。没有计算机能够完美地保持60 FPS。 另外,为什么你的requestAnimationFrame在超时中?


我不是说大部分时间都是60帧每秒,我指的是它一会儿是58帧,然后就一直跳来跳去。我需要重新启动循环。 - baiomu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接