在Javascript中实现简单游戏循环的最佳方式是什么?

25

有没有一种简单的方法在JavaScript中创建游戏循环?就像这样...

onTimerTick() {
  // update game state
}

在 JavaScript 的事件驱动世界中,你可能不需要这样做。你为什么认为你需要这个? - Jon Cram
11
@Jon,如果没有触发事件,你会如何更新游戏?许多游戏即使在你不玩的时候也在进行一些操作... - James
有详细的解释如何在 JavaScript 中实现游戏循环 https://isaacsukin.com/news/2015/01/detailed-explanation-javascript-game-loops-and-timing - Maksim Shamihulau
7个回答

33

有许多种使用JavaScript实现的方法,取决于您的应用程序。setInterval()甚至while()语句都可以完成任务。但这对于游戏循环是行不通的。JavaScript由浏览器解释,因此容易被中断。中断将使您的游戏播放感觉不流畅。

CSS3的webkitRequestAnimationFrame属性旨在通过管理渲染循环来纠正这一问题。然而,如果您有大量更新的对象,则仍不是最有效的方法,并且容易出现抖动。

这是一个很好的网站入门:

http://nokarma.org/2011/02/02/javascript-game-development-the-game-loop/index.html

这个网站提供了一些关于制作游戏循环的基础信息。它并没有涉及任何面向对象设计方面的内容。使用日期函数是实现精确定时的最准确的方法。
while ((new Date).getTime() > nextGameTick && loops < maxFrameSkip) {
  Game.update();
  nextGameTick += skipTicks;
  loops++;
}

这并没有考虑setTimeout在高频率下的漂移。这也会导致事物失去同步,变得抖动不安。JavaScript每秒钟将漂移+/-18毫秒。
var start, tick = 0;
var f = function() {
    if (!start) start = new Date().getTime();
    var now = new Date().getTime();
    if (now < start + tick*1000) {
        setTimeout(f, 0);
    } else {
        tick++;
        var diff = now - start;
        var drift = diff % 1000;
        $('<li>').text(drift + "ms").appendTo('#results');
        setTimeout(f, 990);
    }
};

setTimeout(f, 990);

现在让我们将所有内容放入一个工作示例中。我们想要将游戏循环注入到WebKit的管理渲染循环中,这将有助于平滑渲染图形。我们还想拆分绘制和更新功能。这将在计算下一帧应该绘制之前更新呈现场景中的对象。如果更新时间过长,游戏循环还应跳过绘制帧。

Index.html

<html>
    <head>
        <!--load scripts--> 
    </head>
    <!-- 
        render canvas into body, alternative you can use div, but disable 
        right click and hide cursor on parent div
    -->
    <body oncontextmenu="return false" style="overflow:hidden;cursor:none;-webkit-user-select: none;-moz-user-select: none;-ms-user-select: none;user-select: none;">
        <script type="text/javascript" charset="utf-8">
            Game.initialize();
            window.onEachFrame(Game.run);
        </script>
    </body>
</html>

Game.js

var Game = {};

Game.fps = 60;
Game.maxFrameSkip = 10;
Game.skipTicks = 1000 / Game.fps;

Game.initialize = function() {
    this.entities = [];
    this.viewport = document.body;

    this.input = new Input();

    this.debug = new Debug();
    this.debug.initialize(this.viewport);

    this.screen = new Screen();
    this.screen.initialize(this.viewport);
    this.screen.setWorld(new World());
};

Game.update = function(tick) {
    Game.tick = tick;
    this.input.update();
    this.debug.update();
    this.screen.update();
};

Game.draw = function() {
    this.debug.draw();
    this.screen.clear();
    this.screen.draw();
};

Game.pause = function() {
    this.paused = (this.paused) ? false : true;
};

/*
 * Runs the actual loop inside browser
 */
Game.run = (function() {
    var loops = 0;
    var nextGameTick = (new Date).getTime();
    var startTime = (new Date).getTime();
    return function() {
        loops = 0;
        while (!Game.paused && (new Date).getTime() > nextGameTick && loops < Game.maxFrameSkip) {
            Game.update(nextGameTick - startTime);
            nextGameTick += Game.skipTicks;
            loops++;
        }
        Game.draw();
    };
})();

(function() {
    var onEachFrame;
    if (window.requestAnimationFrame) {
       onEachFrame = function(cb) {
          var _cb = function() {
                cb();
             requestAnimationFrame(_cb);
          };
          _cb();
       };
    } else if (window.webkitRequestAnimationFrame) {
       onEachFrame = function(cb) {
          var _cb = function() {
             cb();
             webkitRequestAnimationFrame(_cb);
          };
          _cb();
       };
    } else if (window.mozRequestAnimationFrame) {
        onEachFrame = function(cb) {
            var _cb = function() {
                cb();
                mozRequestAnimationFrame(_cb);
            };
            _cb();
        };
    } else {
        onEachFrame = function(cb) {
            setInterval(cb, Game.skipTicks);
        };
    }

    window.onEachFrame = onEachFrame;
})();

更多信息

您可以在此处找到完整的工作示例和所有代码。我已将此答案转换为可下载的JavaScript框架,您可以在其基础上构建游戏。

https://code.google.com/p/twod-js/


3
这个回答格式混乱、表述不清,有很多拼写和语法错误。请阅读 Answering How-To - Joe
3
您的答案是页面上最正确和最新的,但现在您有太多的示例代码。未来读者将不会关心HTML布局、world.js、player.js、camera.js或tile.js。请仅包括与使用requestAnimationFrame处理游戏循环的时间相关的答案元素。 - michael.bartnett
1
我按照你的建议删除了一些代码和类。请注意,现在这不是一个完全可用的示例,因为您需要添加世界加载、玩家和相机逻辑。所谓的HTML布局实际上是宿主页面,并且是示例所必需的,以便向您展示如何使用WebKit将游戏循环注入浏览器。 - 1-14x0r
顺便说一下,我发现了这个npm包,它将相同的东西捆绑成一个易于使用的模块。https://github.com/IceCreamYou/MainLoop.js - 1-14x0r

24
setInterval(onTimerTick, 33); // 33 milliseconds = ~ 30 frames per sec

function onTimerTick() {
    // Do stuff.
}

你可能想要颠倒第一行和其余部分的顺序。 ;) - Amber
12
@Dav: 没有必要这样做。函数声明在相同作用域内的分步代码之前生效。 - T.J. Crowder
引起紧张,只是一个存根。 - 1-14x0r
5
现代浏览器和各种JavaScript游戏引擎现在正在使用requestAnimationFrame - Cerlancism

21

requestAnimationFrame现在在大多数浏览器中都是一个很好的替代方法。它可以用来代替setInterval,但是以一种更加内置的方式实现。它最棒的地方可能在于只有在标签页处于激活状态时才会运行。如果你想让事情在后台运行,那可能是不使用它的一个原因,但通常情况下(特别是对于只有在可见时才有意义的渲染循环),当标签页不活动时不使用资源是很好的。

使用方法非常简单:

function gameLoop(){
  window.requestAnimationFrame(gameLoop);
  Game.update();
}

我不喜欢在过时的帖子上发帖,但这是“javascript游戏循环”的谷歌搜索结果之一,所以它真的需要包括requestAnimationFrame。

针对旧浏览器的Polyfill代码从paulirish复制:

(function() {
    var lastTime = 0;
    var vendors = ['ms', 'moz', 'webkit', 'o'];
    for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
        window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
        window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] 
                                   || window[vendors[x]+'CancelRequestAnimationFrame'];
    }

    if (!window.requestAnimationFrame)
        window.requestAnimationFrame = function(callback, element) {
            var currTime = new Date().getTime();
            var timeToCall = Math.max(0, 16 - (currTime - lastTime));
            var id = window.setTimeout(function() { callback(currTime + timeToCall); }, 
              timeToCall);
            lastTime = currTime + timeToCall;
            return id;
        };

    if (!window.cancelAnimationFrame)
        window.cancelAnimationFrame = function(id) {
            clearTimeout(id);
        };
}());

有关creativeJS的更深入的解释和使用技巧。


我不喜欢在旧帖子上发帖。StackOverflow不是一个论坛,而是一个词典。问题永远不会过时,除非相关技术已经不存在了! - undefined

11
许多旧的和次优的答案已经过时了。现在推荐使用requestAnimationFrame来完成此操作。
例如:
let previousTime = 0.0;

const loop = time => {
  // Compute the delta-time against the previous time
  const dt = time - previousTime;

  // Update the previous time
  previousTime = time;

  // Update your game
  update(dt);

  // Render your game
  render();

  // Repeat
  window.requestAnimationFrame(loop);
};

// Launch
window.requestAnimationFrame(time => {
  previousTime = time;

  window.requestAnimationFrame(loop);
});

固定时间步长

如果你想实现固定的时间步长,只需累加 delta-time 直到达到某个阈值,然后更新游戏。

const timeStep = 1.0 / 60.0;

let previousTime = 0.0;
let delta = 0.0;

const loop = time => {
  // Compute the delta-time against the previous time
  const dt = time - previousTime;

  // Accumulate delta time
  delta = delta + dt;

  // Update the previous time
  previousTime = time;

  // Update your game
  while (delta > timeStep) {
    update(timeStep);

    delta = delta - timeStep;
  }

  // Render your game
  render();

  // Repeat
  window.requestAnimationFrame(loop);
};

// Launch
window.requestAnimationFrame(time => {
  previousTime = time;

  window.requestAnimationFrame(loop);
});

使用requestAnimationFrame来处理与动画无关的逻辑或物理逻辑似乎不太合适。您是否看到过任何官方指导或任何负面影响/原因,说明这可能不是一个好主意? - undefined

4

是的,你需要 setInterval

function myMainLoop () {
  // do stuff...
}
setInterval(myMainLoop, 30);

4

这个可以吗?

setInterval(updateGameState, 1000 / 25);

其中25是您期望的FPS。您还可以将帧之间的毫秒数放在那里,以25 fps为例,该值应为40ms(1000/25 = 40)。


1
可能你想传递函数本身,而不是它的结果。 - just somebody
不处理漂移并会导致延迟。不使用 WebKit。 - 1-14x0r

0

不确定这种方法的效果如何,但这里有一种使用while循环和线程休眠的方法。这只是一个例子,您可以根据其他语言中的实现方式替换下面的游戏循环。您也可以在这里找到更好的while循环。

let gameRunning = false;

async function runGameThread(){
    if(!gameRunning){
        gameRunning = true;

        // this function allows the thread to sleep (found this a long time ago on an old stack overflow post)
        const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

        // the game loop
        while(gameRunning){
            let start = new Date().getTime();
            gameUpdate();
            await sleep(start + 20 - new Date().getTime());
        }

    }
}

function stopGameThread(){
    while(gameRunning){
        try{
            gameRunning = false;
        }catch(e){}
    }
}

function gameUpdate(){
    // do stuff...
}

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