javascript HTML5 Canvas 游戏循环增量时间计算

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/25612452/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-28 04:43:57  来源:igfitidea点击:

HTML5 Canvas game loop delta time calculations

javascripthtmlcanvasgame-enginetimedelta

提问by Kosmetika

I'm new to game development. Currently I'm doing a game for js13kgamescontest, so the game should be small and that's why I don't use any of modern popular frameworks.

我是游戏开发的新手。目前我正在为js13kgames比赛做一个游戏,所以游戏应该很小,这就是我不使用任何现代流行框架的原因。

While developing my infinite game loopI found several articles and pieces of advice to implement it. Right now it looks like this:

在开发我的无限游戏循环时,我找到了几篇文章和一些建议来实现它。现在它看起来像这样:

self.gameLoop = function () {
        self.dt = 0;

        var now;
        var lastTime = timestamp();
        var fpsmeter = new FPSMeter({decimals: 0, graph: true, theme: 'dark', left: '5px'});

        function frame () {
            fpsmeter.tickStart();
            now = window.performance.now();

            // first variant - delta is increasing..
            self.dt = self.dt + Math.min(1, (now-lastTime)/1000);

            // second variant - delta is stable.. 
            self.dt = (now - lastTime)/16;
            self.dt = (self.dt > 10) ? 10 : self.dt;

            self.clearRect();

            self.createWeapons();
            self.createTargets();

            self.update('weapons');
            self.render('weapons');

            self.update('targets');
            self.render('targets');

            self.ticks++;

            lastTime = now;
            fpsmeter.tick();
            requestAnimationFrame(frame);
        }

        requestAnimationFrame(frame);
};

So the problem is in self.dtI've eventually found out that first variant is not suitable for my game because it increases forever and the speed of weapons is increasing with it as well (e.g. this.position.x += (Math.cos(this.angle) * this.speed) * self.dt;..

所以问题在于self.dt我最终发现第一个变体不适合我的游戏,因为它会永远增加并且武器的速度也随之增加(例如this.position.x += (Math.cos(this.angle) * this.speed) * self.dt;..

Second variant looks more suitable, but does it correspond to this kind of loop (http://codeincomplete.com/posts/2013/12/4/javascript_game_foundations_the_game_loop/)?

第二个变体看起来更合适,但它是否对应于这种循环(http://codeincomplete.com/posts/2013/12/4/javascript_game_foundations_the_game_loop/)?

回答by d13

Here' an implementation of an HTML5 rendering system using a fixed time step with a variable rendering time:

这是使用固定时间步长和可变渲染时间的 HTML5 渲染系统的实现:

http://jsbin.com/ditad/10/edit?js,output

http://jsbin.com/ditad/10/edit?js,output

It's based on this article:

它基于这篇文章:

http://gameprogrammingpatterns.com/game-loop.html

http://gameprogrammingpatterns.com/game-loop.html

Here is the game loop:

这是游戏循环:

    //Set the frame rate
var fps = 60,
    //Get the start time
    start = Date.now(),
    //Set the frame duration in milliseconds
    frameDuration = 1000 / fps,
    //Initialize the lag offset
    lag = 0;

//Start the game loop
gameLoop();

function gameLoop() {
  requestAnimationFrame(gameLoop, canvas);

  //Calcuate the time that has elapsed since the last frame
  var current = Date.now(),
      elapsed = current - start;
  start = current;
  //Add the elapsed time to the lag counter
  lag += elapsed;

  //Update the frame if the lag counter is greater than or
  //equal to the frame duration
  while (lag >= frameDuration){  
    //Update the logic
    update();
    //Reduce the lag counter by the frame duration
    lag -= frameDuration;
  }
  //Calculate the lag offset and use it to render the sprites
  var lagOffset = lag / frameDuration;
  render(lagOffset);
}

The renderfunction calls a rendermethod on each sprite, with a reference to the lagOffset

render函数render在每个精灵上调用一个方法,并引用lagOffset

function render(lagOffset) {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  sprites.forEach(function(sprite){
    ctx.save();
    //Call the sprite's `render` method and feed it the
    //canvas context and lagOffset
    sprite.render(ctx, lagOffset);
    ctx.restore();
  });
}

Here's the sprite's render method that uses the lag offset to interpolate the sprite's render position on the canvas.

这是精灵的渲染方法,它使用滞后偏移量来插入精灵在画布上的渲染位置。

o.render = function(ctx, lagOffset) {
    //Use the `lagOffset` and previous x/y positions to
    //calculate the render positions
    o.renderX = (o.x - o.oldX) * lagOffset + o.oldX;
    o.renderY = (o.y - o.oldY) * lagOffset + o.oldY;

    //Render the sprite
    ctx.strokeStyle = o.strokeStyle;
    ctx.lineWidth = o.lineWidth;
    ctx.fillStyle = o.fillStyle;
    ctx.translate(
      o.renderX + (o.width / 2),
      o.renderY + (o.height / 2)
     );
    ctx.beginPath();
    ctx.rect(-o.width / 2, -o.height / 2, o.width, o.height);
    ctx.stroke();
    ctx.fill();

    //Capture the sprite's current positions to use as 
    //the previous position on the next frame
    o.oldX = o.x;
    o.oldY = o.y;
  };

The important part is this bit of code that uses the lagOffset and the difference in the sprite's rendered position between frames to figure out its new current canvas position:

重要的部分是这段代码,它使用 lagOffset 和帧之间精灵渲染位置的差异来确定其新的当前画布位置:

o.renderX = (o.x - o.oldX) * lagOffset + o.oldX;
o.renderY = (o.y - o.oldY) * lagOffset + o.oldY;

Notice that the oldXand oldYvalues are being re-calculated each frame at the end of the method, so that they can be used in the next frame to help figure out the difference.

请注意,在方法结束时每帧都会重新计算oldXoldY值,以便可以在下一帧中使用它们来帮助找出差异。

o.oldX = o.x;
o.oldY = o.y;

I'm actually not sure if this interpolation is completely correct or if this is best way to do it. If anyone out there reading this knows that it's wrong, please let us know :)

我实际上不确定这种插值是否完全正确,或者这是否是最好的方法。如果有人在读这篇文章时知道这是错误的,请告诉我们:)

回答by Patrick W. McMahon

A great solution to your game engine would be to think in objects and entities. You can think of everything in your world as objects and entities. Then you want to make a game object manager that will have a list of all your game objects. Then you want to make a common communication method in the engine so game objects can make event triggers. The entities in your game for example a player would not need to inherent from anything to get the ability to render to the screen or have collision detection. You would simple make common methods in the entity that the game engine is looking for. Then let the game engine handle the entity as it would like. Entities in your game can be created or destroyed at anytime in the game so you should not hard-code any entities at all in the game loop.

游戏引擎的一个很好的解决方案是考虑对象和实体。您可以将世界中的一切视为对象和实体。然后你想制作一个游戏对象管理器,其中包含所有游戏对象的列表。然后你想在引擎中创建一个通用的通信方法,以便游戏对象可以触发事件。游戏中的实体(例如玩家)不需要从任何东西中获得能够渲染到屏幕或进行碰撞检测的能力。您可以简单地在游戏引擎正在寻找的实体中创建通用方法。然后让游戏引擎根据需要处理实体。游戏中的实体可以在游戏中随时创建或销毁,因此您根本不应在游戏循环中对任何实体进行硬编码。

You will want other objects in your game engine to respond to event triggers that the engine has received. This can be done using methods in the entity that the game engine will check to see if the method is available and if it is would pass the events to the entity. Do not hard code any of your game logic into the engine it messes up portability and limits your ability to expand on the game later on.

您将希望游戏引擎中的其他对象响应引擎收到的事件触发器。这可以使用实体中的方法来完成,游戏引擎将检查该方法是否可用以及是否将事件传递给实体。不要将您的任何游戏逻辑硬编码到引擎中,这会破坏可移植性并限制您以后扩展游戏的能力。

The problem with your code is first your calling different objects render and updates not in the correct order. You need to call ALL your updatesthen call ALL your rendersin that order. Another is your method of hard coding objects into the loop is going to give you a lot of problems, when you want one of the objects to no longer be in the game or if you want to add more objects into the game later on.

您的代码的问题首先是您调用不同的对象渲染和更新的顺序不正确。您需要调用所有更新,然后按该顺序调用所有渲染。另一个是将对象硬编码到循环中的方法会给您带来很多问题,当您希望其中一个对象不再存在于游戏中时,或者您想稍后在游戏中添加更多对象时。

Your game objects will have an update()and a render()your game engine will look for that function in the object/entity and call it every frame. You can get very fancy and make the engine work in a way to check if the game object/entity has the functions prior to calling them. for example maybe you want an object that has an update()but never renders anything to the screen. You could make the game object functions optional by making the engine check prior to calling them. Its also good practice to have an init()function for all game objects. When the game engine starts up the scene and creates the objects it will start by calling the game objects init()when first creating the object then every frame calling update()that way you can have a function that you only run one time on creation and another that runs every frame.

您的游戏对象将有一个update()并且render()您的游戏引擎将在对象/实体中查找该函数并在每一帧中调用它。您可以变得非常花哨,并使引擎以某种方式工作,以在调用它们之前检查游戏对象/实体是否具有这些函数。例如,也许您想要一个具有update()但从不向屏幕呈现任何内容的对象。您可以通过在调用它们之前进行引擎检查来使游戏对象函数成为可选的。init()为所有游戏对象设置一个函数也是一种很好的做法。当游戏引擎启动场景并创建对象时,它将首先init()在第一次创建对象时调用游戏对象,然后每帧调用update()这样你就可以拥有一个在创建时只运行一次的函数,而另一个在每一帧运行。

delta time is not really needed as window.requestAnimationFrame(frame);will give you ~60fps. So if you're keeping track of the frame count you can tell how much time has passed. Different objects in your game can then, (based off of a set point in the game and what the frame count was) determine how long its been doing something based off its new frame count.

增量时间并不是真正需要的,因为它window.requestAnimationFrame(frame);会给你~60fps。因此,如果您正在跟踪帧数,您可以知道已经过去了多少时间。然后,游戏中的不同对象(基于游戏中的设置点和帧数)可以根据新的帧数确定其执行某事的时间。

window.requestAnimationFrame = window.requestAnimationFrame || function(callback){window.setTimeout(callback,16)};
gameEngine = function () {
        this.frameCount=0;
        self=this;

        this.update = function(){
           //loop over your objects and run each objects update function
        }

        this.render = function(){
          //loop over your objects and run each objects render function
        }

        this.frame = function() {
            self.update();
            self.render();
            self.frameCount++;
            window.requestAnimationFrame(frame);
        }
        this.frame();
};

I have created a full game engine located at https://github.com/Patrick-W-McMahon/Jinx-Engine/if you review the code at https://github.com/Patrick-W-McMahon/Jinx-Engine/blob/master/JinxEngine.jsyou will see a fully functional game engine built 100% in javascript. It includes event handlers and permits action calls between objects that are passed into the engine using the event call stack. check out some of the examples https://github.com/Patrick-W-McMahon/Jinx-Engine/tree/master/exampleswhere you will see how it works. The engine can run around 100,000 objects all being rendered and executed per frame at a rate of 60fps. This was tested on a core i5. different hardware may vary. mouse and keyboard events are built into the engine. objects passed into the engine just need to listen for the event passed by the engine. Scene management and multi scene support is currently being built in for more complex games. The engine also supports high pixel density screens.

我创建了位于一个完整的游戏引擎https://github.com/Patrick-W-McMahon/Jinx-Engine/如果您在检查代码https://github.com/Patrick-W-McMahon/Jinx-Engine /blob/master/JinxEngine.js你会看到一个 100% 用 javascript 构建的功能齐全的游戏引擎。它包括事件处理程序并允许使用事件调用堆栈传递到引擎的对象之间的动作调用。查看一些示例https://github.com/Patrick-W-McMahon/Jinx-Engine/tree/master/examples您将看到它是如何工作的。该引擎可以运行大约 100,000 个对象,所有对象都以 60fps 的速率每帧渲染和执行。这是在核心 i5 上测试的。不同的硬件可能会有所不同。鼠标和键盘事件内置于引擎中。传入引擎的对象只需要监听引擎传递的事件。目前正在为更复杂的游戏内置场景管理和多场景支持。该引擎还支持高像素密度屏幕。

Reviewing my source code should get you on the track for building a more fully functional game engine.

查看我的源代码应该会让您走上构建功能更全面的游戏引擎的轨道。

I would also like to point out that you should have requestAnimationFrame()called when you're ready to repaint and not prior (aka at the end of the game loop). One good example why you should not call requestAnimationFrame()at the beginning of the loop is if you're using a canvas buffer. If you call requestAnimationFrame()at the beginning, then begin to draw to the canvas buffer you can end up having it draw half of the new frame with the other half being the old frame. This will happen on every frame depending on the time it takes to finish the buffer in relation to the repaint cycle (60fps). But at the same time you would end up overlapping each frame so the buffer will get more messed up as it loops over its self. This is why you should only call requestAnimationFrame()when the buffer is fully ready to draw to the canvas. by having the requestAnimationFrame()at the end you can have it skip a repaint if the buffer is not ready to draw and so every repaint is drawn as it is expected. The position of requestAnimationFrame()in the game loop has a big difference.

我还想指出,您应该requestAnimationFrame()在准备重绘时而不是在之前(也就是在游戏循环结束时)调用。为什么不应该requestAnimationFrame()在循环开始时调用的一个很好的例子是,如果您使用的是画布缓冲区。如果您requestAnimationFrame()在开始时调用,然后开始绘制到画布缓冲区,您最终可以让它绘制新帧的一半,另一半是旧帧。这将发生在每一帧上,具体取决于完成与重绘周期 (60fps) 相关的缓冲区所需的时间。但与此同时,您最终会重叠每一帧,因此缓冲区在循环时会变得更加混乱。这就是为什么你应该只requestAnimationFrame()在缓冲区完全准备好绘制到画布时调用。通过拥有requestAnimationFrame()最后,如果缓冲区未准备好绘制,您可以让它跳过重绘,因此每次重绘都按预期绘制。requestAnimationFrame()在游戏循环中的位置有很大的不同。

回答by markE

The modern version of requestAnimationFrame now sends in a timestamp that you can use to calculate elapsed time. When your desired time interval has elapsed you can do your update, create and render tasks.

requestAnimationFrame 的现代版本现在发送一个时间戳,您可以使用它来计算经过的时间。当您想要的时间间隔过去后,您可以进行更新、创建和渲染任务。

Here's example code:

这是示例代码:

var lastTime;
var requiredElapsed=1000/100; // desired interval is 10fps

requestAnimationFrame(loop);

function loop(now){
    requestAnimationFrame(loop);

    if(!lastTime){lastTime=now;}
    var elapsed=lastTime-now;

    if(elapsed>requiredElapsed){
        // do stuff
        lastTime=now;
    }

}

回答by SimonR

This isn't reallyan answer to your question, and without knowing more about the particular game I can't say for sure if it will help you, but do you really need to know dt(and FPS)?

这并不是您问题的真正答案,在不了解特定游戏的更多信息的情况下,我不能肯定地说它是否对您有帮助,但是您真的需要知道dt(和 FPS)吗?

In my limited forays into JS game development I've found that often you don't really need to to calculate any kind of dtas you can usually come up with a sensible default value based on your expected frame rate, and make anything time-based (such as weapon reloading) simply work based on the number of ticks (i.e. a bow might take 60 ticks to reload (~1 second @ ~60FPS)).

在我对 JS 游戏开发的有限尝试中,我发现通常你不需要计算任何类型的,dt因为你通常可以根据你的预期帧率提出一个合理的默认值,并根据时间做出任何事情(例如武器重新加载)只是根据滴答声工作(即弓可能需要 60 滴答声来重新加载(~1 秒@~60FPS))。

I usually use window.setTimeout()rather than window.requestAnimationFrame(), which I've found generally provides a more stable frame rate which will allow you to define a sensible default to use in place of dt. On the down-side the game will be more of a resource hog and less performant on slower machines (or if the user has a lot of other things running), but depending on your use case those may not be real concerns.

我通常使用window.setTimeout()而不是window.requestAnimationFrame(),我发现它通常提供更稳定的帧速率,这将允许您定义一个合理的默认值来代替dt. 不利的一面是,在较慢的机器上(或者如果用户有很多其他东西在运行),游戏将更多地占用资源并且性能较低,但根据您的用例,这些可能不是真正的问题。

Now this is purely anecdotal advice so you should take it with a pinch of salt, but it has served me pretty well in the past. It all depends on whether you mind the game running more slowly on older/less powerful machines, and how efficient your game loop is. If it's something simple that doesn't need to display real times you might be able to do away with dtcompletely.

现在这纯粹是轶事建议,所以你应该加一点盐,但它在过去对我很有帮助。这完全取决于您是否介意游戏在较旧/功能较弱的机器上运行得更慢,以及您的游戏循环效率如何。如果它是不需要显示实时的简单内容,您可能可以dt完全取消。

回答by Alex

I did not check the logic of the math in your code .. however here what works for me:

我没有检查你代码中的数学逻辑..但是这里对我有用:

GameBox = function()
{
    this.lastFrameTime = Date.now();
    this.currentFrameTime = Date.now();
    this.timeElapsed = 0;
    this.updateInterval = 2000;       //in ms
}

GameBox.prototype.gameLoop = function()
{
   window.requestAnimationFrame(this.gameLoop.bind(this));
   this.lastFrameTime = this.currentFrameTime;
   this.currentFrameTime = Date.now();
   this.timeElapsed +=  this.currentFrameTime - this.lastFrameTime ;
   if(this.timeElapsed >= this.updateInterval)
   {
      this.timeElapsed = 0;
      this.update(); //modify data which is used to render
   }
   this.render();
}

This implementation is idenpendant from the CPU-speed(ticks). Hope you can make use of it!

此实现与 CPU 速度(滴答)无关。希望你能好好利用!

回答by Spencer

At some point you will want to think about decoupling your physics from your rendering. Otherwise your players could have inconsistent physics. For example, someone with a beefy machine getting 300fps will have very sped up physics compared to someone chugging along at 30fps. This could manifest in the first player cruising around in a mario-like scrolling game at super speed and the other player crawling at half speed (if you did all your testing at 60fps). A way to fix that is to introduce delta time steps. The idea is that you find the time between each frame and use that as part of your physics calculations. It keeps the gameplay consistent regardless of frame rate. Here is a good article to get you started: http://gafferongames.com/game-physics/fix-your-timestep/

在某些时候,您会想要考虑将物理与渲染分离。否则你的玩家可能会出现不一致的物理现象。例如,与以 30fps 快速前进的人相比,拥有 300fps 的强大机器的人的物理速度会非常快。这可能体现在第一个玩家在类似马里奥的滚动游戏中以超速巡航,而另一个玩家以半速爬行(如果您以 60fps 进行所有测试)。解决这个问题的一种方法是引入增量时间步长。这个想法是你找到每帧之间的时间并将其用作物理计算的一部分。无论帧速率如何,它都能保持游戏玩法的一致性。这是一篇让您入门的好文章:http: //gafferongames.com/game-physics/fix-your-timestep/

requestAnimationFrame will not fix this inconsistency, but it is still a good thing to use sometimes as it has battery saving advantages. Here is a source for more info http://www.chandlerprall.com/2012/06/requestanimationframe-is-not-your-logics-friend/

requestAnimationFrame 不会解决这种不一致的问题,但有时使用它仍然是一件好事,因为它具有省电优势。这是更多信息的来源http://www.chandlerprall.com/2012/06/requestanimationframe-is-not-your-logics-friend/