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/)?
现代版本的 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)
这是使用固定时间步长和可变渲染时间的 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)
请注意,在方法结束时每帧都会重新计算oldX和oldY值,以便可以在下一帧中使用它们来帮助找出差异。
o.oldX = o.x;
o.oldY = o.y;
Run Code Online (Sandbox Code Playgroud)
我实际上不确定这种插值是否完全正确,或者这是否是最好的方法。如果有人在读这篇文章时知道这是错误的,请告诉我们:)
您的游戏引擎的一个很好的解决方案是在对象和实体中思考.您可以将世界上的一切都视为对象和实体.然后你想制作一个游戏对象管理器,它将包含你所有游戏对象的列表.然后,您希望在引擎中创建一个通用的通信方法,以便游戏对象可以创建事件触发器.游戏中的实体(例如玩家)不需要任何固有的东西来获得渲染到屏幕或进行碰撞检测的能力.您可以在游戏引擎正在寻找的实体中简单地制作常用方法.然后让游戏引擎按照自己的意愿处理实体.游戏中的实体可以在游戏中随时创建或销毁,因此您不应在游戏循环中对任何实体进行硬编码.
您将希望游戏引擎中的其他对象响应引擎已收到的事件触发器.这可以使用实体中的方法来完成,游戏引擎将检查该方法是否可用以及是否将事件传递给实体.不要将任何游戏逻辑硬编码到引擎中,这会影响可移植性并限制您稍后扩展游戏的能力.
您的代码的问题首先是您调用不同的对象呈现和更新不正确的顺序.您需要调用所有更新,然后按顺序调用所有渲染.另一种是你的硬编码对象进入循环的方法会给你很多问题,当你想要的对象之一,不再是在游戏中,或者如果你想添加更多的物体进入游戏以后.
您的游戏对象将具有一个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()游戏循环中的位置有很大差异.
我没有检查代码中的数学逻辑..但是这里对我有用:
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 速度(刻度)无关。希望您能利用它!