帮助游戏开发。渲染循环?

时间:2010-05-06 20:18:31

标签: game-engine game-loop

我正在开发一款简单的游戏,这是我的第一个游戏项目。

我找到的大多数样本都有一个渲染循环,其中所有的游戏逻辑也是如此,我只是不喜欢这样。 假设我有一个X = 0的球,一个X = 10的墙和一个慢机器,第一个循环将球置于X = 7,而在第二个循环中,它将球置于X = 14。它会让游戏崩溃!

这个“渲染循环”是制作游戏的正确方法吗?我应该编写代码来检查每一帧中的这类事情吗?例如,新帧X = 14,最后一帧有X = 7,所以我应检查是否有从X = 7到X = 14的任何内容

我在想,我应该为游戏逻辑和渲染循环创建一个独立的线程,我应该只是“拍摄当前游戏逻辑的快照”并显示,不是吗?

你们这些经验丰富的游戏开发者如何解决这个问题?

谢谢!

7 个答案:

答案 0 :(得分:5)

正如另一个答案所说,你所看到的问题被称为“隧道”这是“子弹穿纸”问题,子弹快速移动,纸张很薄,你怎么知道碰撞发生了?

如果您的世界边界很简单,那就很容易了。例如。在俄罗斯方块中,块只允许左右移动直到它们碰到两侧,并且很容易测试最底部的坐标是否击中“地面”。这些测试很简单,因为你可以一次做一个轴,而两侧的碰撞意味着不同于与底部碰撞的不同。如果你有一个长方形房间,只要移动物体通过夹住其坐标将其移动到室外,就“停止”移动物体。即如果房间宽度从-3到+3,并且您的对象的X值为5,则只需将其更改为3即可完成。

如果你想处理更复杂的世界,那就有点棘手了。你会想要阅读“扫掠”几何碰撞。基本上,如果你有一个圆圈,你需要用胶囊进行碰撞测试,而不是通过从起点到终点“扫过”圆圈来形成的形状。它就像一个两端都有半圆形的矩形。数学是出人意料的直截了当(恕我直言),但要做到正确并真正了解正在发生的事情可能会很棘手。这是值得的!

编辑:在线程问题上 - 无需复杂化。一个线程很好。跳过更新框架也会变得混乱,并且非常先进,因为您实际上需要弄清楚“未来”,然后对所有有趣的值进行插值。我不称之为“渲染”循环,我自己,因为渲染循环只是过程的一部分。

def GameLoop():
   while True:
      ReadInputs()
      FigureOutWhatStuffDoes()
      DrawItAll()

编辑2:这似乎是一个有趣的讨论:http://www.gamedev.net/community/forums/topic.asp?topic_id=482397

答案 1 :(得分:4)

如果为此创建单独的线程,则还会产生许多您可能不想处理的复杂性。一个线程和一个循环很容易处理。

基本上你想要做的是有一个循环同时执行逻辑和渲染,但不一定在每次迭代中。请参阅此伪代码:

while(true) {
   oldTime = currentTime;
   currentTime = systemTime();
   timeStep = currentTime - oldTime;

   // Only do logic x times / second
   if( currentTime > lastLogicTime + logicRefreshTime ){
      doGameLogic( currentTime - lastLogicTime );
      lastLogicTime = currentTime;
   }

   // Extrapolate all movements using timeStep
   renderGraphics( timeStep );

   wait( screenRefreshTime );
}

void doGameLogic( timeStep ) {
   // Update all objects
   for each( gameObject obj )
     obj.move( timeStep );
}

让所有固体可移动对象继承SolidObject类。当您调用SolidObject.move(timeStep)时,该方法会检查对象在给定timeStep内的移动距离。如果在此之前有一堵墙,那么物体应该停止,反弹并改变方向,死亡或任何你喜欢的东西。


修改

如果移动了两个对象,您可能需要检查如果 它们是否会发生碰撞。很多游戏都没有做到这一点,但这就是你如何做到的:

首先计算每个移动物体的oldTimecurrentTime之间的移动线。然后比较线条以查看两条线是否相交。请注意,您需要考虑对象的大小。交点是物体碰撞的地方。使用此方法,您可以准确地检测移动物体的碰撞。

答案 2 :(得分:3)

可以有一个单独的更新线程和一个绘图线程,但这并不容易!通常你需要做很多 mutex 检查以防止多线程访问相同的变量,所以这不是真的可行(加上你不想处理半更新状态)。为了正确实现,您确实需要具有最后渲染状态的某种形式的快照。如果你不介意所涉及的困难,可以在这里找到一个很好的实现:

http://blog.slapware.eu/game-engine/programming/multithreaded-renderloop-part1/

http://blog.slapware.eu/game-engine/programming/multithreaded-renderloop-part2/

不要让反对者劝阻你。这是可能的,它是可行和有效的。唯一的缺点是它很难实现,因此可能不值得花时间(除非你有一个非常耗费CPU的游戏)。

答案 3 :(得分:2)

不要穿线 - 你会遇到比你解决的问题更多的问题。你可以来处理事情并分离逻辑更新和渲染,但是要做到正确并且游戏循环的大部分本质上都是单线程的,这很棘手。

相反,请考虑使用增量时间推进游戏循环以扩展事物,以便逻辑更新在很大程度上独立于机器扼杀帧的能力。

简单来说,如果你使用三角形来缩放物体,无论穿过一个框架需要多长时间,从一个房间的一侧移动到另一个房间的球将花费相同的时间来完成它。一台非常快的PC而且速度很慢。

E.g。如果一个球在一秒内移动10个单位并且您可以确定自上次更新后已经过了0.1秒(使用高性能计时器或您可以使用的任何东西),您只需将移动缩放0.1并且球移动1个单位。

E.g。

private const float BallSpeedInMetresPerSecond = 10;

public void Update(float deltaTimeInSeconds)
{
    float adjustedSpeed = deltaTimeInSeconds * BallSpeedInMetresPerSecond;
    // set ball's speed / move it etc. using adjusted speed
}

这并不能完全解决你的问题(如果某些东西真的快,那么无论如何它都会陷入困境!),但它是一种简单而有效的方法来保持事物的可预测性和一致性直到你遇到更复杂的问题。

如果你开始工作,然后想要解决一个更复杂的问题,就像破折号说的那样,请查看扫描碰撞检测。

答案 4 :(得分:1)

  

我在想我应该有一个   用于游戏逻辑的分离线程   在渲染循环中,我应该   对当前游戏进行“快照”   逻辑和显示,没有?

没有办法简单,安全,快速地拍摄大量游戏状态的快照。你可以对它进行双重缓冲,这可能是下一个最好的事情。但它无论如何都无法解决问题,所以不,你不会这样做,至少不是为了这个目的。

  

假设我有一个X = 0的球,和   X = 10的墙和慢速机器,   第一个循环将球置于X = 7   在第二个循环中,它放置了   球在X = 14。它只会崩溃   游戏!

除非你能保证你使用的每台计算机总是足够快以检查X = 1,X = 2,X = 3 ...... X = 10,否则不会解决这个问题。你无法做出这种保证。即使你可以,也很少使用整数来表示头寸。你能迭代地检查X = 0.0000001,X = 0.0000002,X = 0.0000003 ...... X = 0.9999999,X = 10.00000?不。

  

你们这些经验丰富的游戏开发者如何解决这个问题?

我们通常还有一个循环。输入,更新,渲染,重复。您提到的碰撞问题通过使用碰撞检测方法来解决,该碰撞检测方法计算物体将通过的区域,例如。解析X = [0到17]。在一台非常慢的机器上,它可能是X = [0-50],在快速机器上它可能是X = [0-5],然后是X = [5-10],但每个都将按预期工作。

答案 5 :(得分:0)

根据我在游戏设计和AI方面的有限经验,我会说有一个逻辑循环和一个显示循环(很像XNA设置)。逻辑循环(XNA中的Update方法)基本上将处理更新位置,而不是,而显示循环(XNA中的Draw方法)将把所有内容都绘制到屏幕上。至于碰撞检测,我会亲自将它本地化为你的球。当它移动时,它会寻找碰撞并做出适当的反应。

线程是另一个话题,但在我看来,我会说不要分开更新和绘制。对我来说,似乎本质上错误的是可能有2次抽奖进行1次更新,反之亦然。如果没有更新任何内容,为什么要绘制......或者为什么在向用户显示正在发生的事情之前多次更新。

只是我的意见,希望我不会偏离基础。

答案 6 :(得分:0)

如果逻辑更新通常很便宜,并且渲染有时很昂贵,那么最简单的事情就是决定每秒进行N次逻辑更新。 N = 60是常见的 - 但你应该选择让游戏运作良好的最小值,选择一个值并调整游戏,直到它以该速率运行,或者(更可能)两者的某种组合。

在运行时,跟踪实际经过的时间,跟踪逻辑上已经过了多长时间(根据执行的更新),以及何时出现超过1.0 / N秒的差异(因为渲染时间过长)执行额外的更新以赶上。这比尝试一次性执行任意时间段更新更好,因为它更可预测。 (如果读者不同意,欢迎他们以艰难的方式找到它。)

这个系统的缺点是,如果渲染变得特别耗时,并且逻辑必须执行太多更新,因此这两者可能会有点不同步,并且逻辑永远不会赶上。如果你的目标是一个固定的系统,这只是表明你要做的很多,你必须以某种方式做得更少,或者(如果这种情况可能很少见)只是抛弃整个想法并做1:1渲染:更新。如果你的目标是像Windows PC这样的变量,你只需要限制追赶逻辑更新的数量,并希望这会让事情恢复正常。

(如果逻辑更昂贵,这种方法并不合适;但我从来没有参与过这个问题的游戏。)