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

Kos*_*ika 3 javascript html5 canvas timedelta game-engine

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

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

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);
};
Run Code Online (Sandbox Code Playgroud)

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

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

mar*_*rkE 9

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

这是示例代码:

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

requestAnimationFrame(loop);

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

    if (elapsed > requiredElapsed) {
        // do stuff
        lastTime = now;
    }
    
}
Run Code Online (Sandbox Code Playgroud)

  • @PatrickW.McMahon,代码很好!`requestAnimationFrame` 只是**安排一个新的循环**,它不会立即触发该循环。其余代码确实达到了。;-) (7认同)
  • “requestAnimationFrame”只是授予浏览器在浏览器的最佳时间运行循环内代码的权限。循环代码是在浏览器方便的时候运行的,而不是在代码中使用“requestAnimationFrame”的地方。它放置在循环内的位置是任意的,markE 将其放置在循环的第一行就可以了。事实上,它可能是最好的地方,因为它清楚地表明了循环函数的目的以及它是如何工作的。 (2认同)

d13*_*d13 6

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

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

它基于这篇文章:

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

这是游戏循环:

    //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);
}
Run Code Online (Sandbox Code Playgroud)

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();
  });
}
Run Code Online (Sandbox Code Playgroud)

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

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;
  };
Run Code Online (Sandbox Code Playgroud)

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

o.renderX = (o.x - o.oldX) * lagOffset + o.oldX;
o.renderY = (o.y - o.oldY) * lagOffset + o.oldY;
Run Code Online (Sandbox Code Playgroud)

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

o.oldX = o.x;
o.oldY = o.y;
Run Code Online (Sandbox Code Playgroud)

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


Pat*_*hon 6

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

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

您的代码的问题首先是您调用不同的对象呈现和更新不正确的顺序.您需要调用所有更新,然后按顺序调用所有渲染.另一种是你的硬编码对象进入循环的方法会给你很多问题,当你想要的对象之一,不再是在游戏中,或者如果你想添加更多的物体进入游戏以后.

您的游戏对象将具有一个update()render()您的游戏引擎将在对象/实体中查找该函数并在每一帧中调用它.您可以非常喜欢并使引擎工作,以便在调用之前检查游戏对象/实体是否具有这些功能.例如,您可能想要一个具有update()但永远不会向屏幕呈现任何内容的对象.您可以通过在调用引擎之前进行引擎检查来使游戏对象功能可选.init()对所有游戏对象都有一个功能也是很好的做法.当游戏引擎启动时的情景,并通过调用游戏对象创建它将启动对象init()时首先创建对象,然后每一帧调用update()这样你可以有你是只在创建一个时间的函数,另一个跑每帧.

实际上并不需要delta时间,因为它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();
};
Run Code Online (Sandbox Code Playgroud)

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

查看我的源代码应该可以帮助您构建功能更全面的游戏引擎.

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

  • ' 增量时间并不是真正需要的,因为 window.requestAnimationFrame(frame); 会给你~60fps。因此,如果您跟踪帧计数,您就可以知道已经过去了多少时间。这是误导性的,您绝对应该保留 deltaTime,因为某些计算需要它。您不能指望它始终保持 60 fps。 (2认同)
  • -1 为了补充 cbethax 的评论,MDN 特别指出“回调次数通常为每秒 60 次,但通常会根据 W3C 建议与大多数 Web 浏览器中的显示刷新率相匹配”。因此,随着具有更高刷新率的显示器变得越来越普遍,您确实不能依赖 requestAnimationFrame 为您提供 60fps。 (2认同)

Ale*_*lex 1

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

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();
}
Run Code Online (Sandbox Code Playgroud)

此实现与 CPU 速度(刻度)无关。希望您能利用它!