如何停止 requestAnimationFrame 的递归/循环?

80
我正在使用Three.js与WebGL渲染器制作一个游戏,当点击“play”链接时,全屏显示。为了动画效果,我使用requestAnimationFrame。
我这样启动它:
self.animate = function()
{
    self.camera.lookAt(self.scene.position);

    self.renderer.render(self.scene, self.camera);

    if (self.willAnimate)
        window.requestAnimationFrame(self.animate, self.renderer.domElement);
}

self.startAnimating = function()
{
    self.willAnimate = true;
    self.animate();
}

self.stopAnimating = function()
{
    self.willAnimate = false;
}

当我想要时,我会调用 startAnimating 方法,它确实按预期工作。但是,当我调用 stopAnimating 函数时,出现问题!尽管没有报告任何错误...
设置基本上像这样:
- 页面上有一个播放链接 - 一旦用户单击链接,渲染器的domElement应全屏显示,并且实际上确实全屏了 - 调用 startAnimating 方法,渲染器开始渲染内容 - 一旦按下ESC键,我注册一个 fullscreenchange 事件并执行 stopAnimating 方法 - 页面尝试退出全屏,成功退出,但整个文档完全空白
我相信我的其他代码都没问题,而我可能以一种错误的方式停止了 requestAnimationFrame 。我的解释可能很糟糕,因此我将代码上传到了我的网站,你可以在这里看到它的运行情况:http://banehq.com/Placeholdername/main.html
这是版本,其中我不尝试调用动画方法,并且全屏进出正常工作:http://banehq.com/Correct/Placeholdername/main.html
一旦第一次单击 play ,游戏就会初始化并执行其 start 方法。一旦全屏退出,游戏的 stop 方法就会执行。每次单击 play ,游戏只会执行其 start 方法,因为没有必要再次初始化。
以下是它的外观:
var playLinkHasBeenClicked = function()
{
    if (!started)
    {
        started = true;

        game = new Game(container); //"container" is an empty div
    }

    game.start();
}

以下是startstop方法的样例代码:

self.start = function()
{
    self.container.appendChild(game.renderer.domElement); //Add the renderer's domElement to an empty div
    THREEx.FullScreen.request(self.container);  //Request fullscreen on the div
    self.renderer.setSize(screen.width, screen.height); //Adjust screensize

    self.startAnimating();
}

self.stop = function()
{
    self.container.removeChild(game.renderer.domElement); //Remove the renderer from the div
    self.renderer.setSize(0, 0); //I guess this isn't needed, but welp

    self.stopAnimating();
}

这个版本和正常工作的版本唯一的区别是,在 startstop 方法中,startAnimatingstopAnimating 方法调用被注释掉了。
6个回答

145

一种启动/停止的方法如下:

var requestId;

function loop(time) {
    requestId = undefined;

    ...
    // do stuff
    ...

    start();
}

function start() {
    if (!requestId) {
       requestId = window.requestAnimationFrame(loop);
    }
}

function stop() {
    if (requestId) {
       window.cancelAnimationFrame(requestId);
       requestId = undefined;
    }
}

工作示例:

const timeElem = document.querySelector("#time");
var requestId;

function loop(time) {
    requestId = undefined;
    
    doStuff(time)
    start();
}

function start() {
    if (!requestId) {
       requestId = window.requestAnimationFrame(loop);
    }
}

function stop() {
    if (requestId) {
       window.cancelAnimationFrame(requestId);
       requestId = undefined;
    }
}

function doStuff(time) {
  timeElem.textContent = (time * 0.001).toFixed(2);
}
  

document.querySelector("#start").addEventListener('click', function() {
  start();
});

document.querySelector("#stop").addEventListener('click', function() {
  stop();
});
<button id="start">start</button>
<button id="stop">stop</button>
<div id="time"></div>


7
这应该是“cancelAnimationFrame”而不是“cancelRequestAnimationFrame”吗? - leech
8
虽然我要为自己辩护,说我改了东西,但其实最初它的名字就叫做cancelRequestAnimationFrame。你可以在Chrome中打开控制台,输入webkitCancel,会发现在Chrome 33版本中仍然存在webkitCancelRequestAnimationFrame - gman
两个函数的文档可以在MDN上找到:https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame(感谢提供如此棒的信息!) - user2493235
1
我发现一件事,你不能在函数内部停止循环:这里是一个演示(http://jsbin.com/seqixag/1/edit?html,js,console,output)。只能从外部停止 :-) - Legends
3
实际上你可以在函数内部取消循环,像这样: setTimeout(function(){window.cancelAnimationFrame(requestId);},0); - Legends
显示剩余4条评论

24

停止很简单,只需要不再调用requestAnimationFrame函数,重新开始则需要再次调用它。

        var pause = false;
        function loop(){
                //... your stuff;
                if(pause) return;
                window.requestionAnimationFrame(loop);
        }
       loop(); //to start it off
       pause = true; //to stop it
       loop(); //to restart it

是的,那其实是最简单的方法。 - Roko C. Buljan
5
但这样做效率不高,因为您需要在每个动画帧内执行一次检查。我发现 cancelAnimationFrame() 在处理大量动画循环时更加高效。 - Sergey Lukin
1
我不明白为什么user3363398的解决方案比gman的效率低。在gman的解决方案中,循环内部有一个函数调用,而该函数调用又执行检查。这不是更加繁重吗?一个检查与函数调用+检查相比。 - Oortone
我非常喜欢这种方式,因为你不需要分配和跟踪一个ID,而且一个布尔值的暂停非常直观。就性能而言,这是一个赋值操作,而不是一个检查操作(最好的情况下),即写入与读取。这不会产生明显的差异。 - undefined

7
var myAnim //your requestId
function anim()
       {
       //bla bla bla
       //it's important to update the requestId each time you're calling reuestAnimationFrame
       myAnim=requestAnimationFrame(anim)
       }

让我们开始吧

myAnim=requestAnimationFrame(anim)

让我们停止这个行为

//the cancelation uses the last requestId
cancelAnimationFrame(myAnim)

参考文献


2
我玩了一下2D打砖块游戏的教程,他们也使用了requestAnimationFrame,并且我用一个简单的return停止了它。如果省略了return语句的值,则该语句将结束函数执行。
if(!lives) {
    alert("GAME OVER");
    return;
}

// looping the draw()
requestAnimationFrame(draw);

这将属于 @user3363398 建议的“不要初始化下一个周期”停止动画帧循环的方法,另一种方法是使用cancelAnimationFrame() - Magnus Lind Oxlund

1
所以,经过进一步的测试,我发现问题确实出在我的其他代码上,而不是动画停止(毕竟这只是一个简单的递归)。问题在于动态添加和删除渲染器的domElement。当我停止这样做时,因为真的没有必要这样做,并且将其包含在初始化发生的地方一次,一切都开始正常工作了。

1

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