6

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

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

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

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

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

谢谢!

4

7 回答 7

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

于 2010-05-06T22:32:56.660 回答
3

If you create a separate thread for this you also create a lot of complexity that you might not want to deal with. It's easy to handle with one thread and one loop.

Basically what you want to do is have a loop that does both logic and rendering, but not necessarily in every iteration. See this pseudo-code:

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 );
}

Let all solid movable objects inherit the class SolidObject. When you call SolidObject.move(timeStep) that method checks to see how far the object can be moved within the given timeStep. If there is a wall before this point then the object should stop, bounce and change direction, die or whatever you like.


Edit:

If two objects move you might want to check if and where they collide. Lots of games don't do this very well, but here's how you do it:

First calculate the line of movement between the oldTime and the currentTime for every object that moves. Then compare the lines to see if two lines intersect. Note, you need to take the objects' size into account. The intersection point is where the objects collide. Using this method you can accurately detect collisions of moving objects.

于 2010-05-06T22:18:48.410 回答
3

可以有一个单独的update-thread和一个drawing-thread,但这并不容易!通常,您需要进行大量互斥体检查以防止多线程访问相同的变量,因此这实际上并不可行(而且您不想处理半更新状态)。对于正确的实现,您确实需要某种形式的最后渲染状态的快照。如果您不介意所涉及的困难,可以在这里找到一个很好的实现:

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

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

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

于 2012-10-11T03:04:26.213 回答
2

不要穿线它——你会引起比你解决的更多的问题。您可以将事物线程化并分离逻辑更新和渲染,但要正确处理很棘手,并且大部分游戏循环本质上是单线程的。

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

简而言之,如果您使用增量来缩放事物,无论通过一个框架需要多长时间,一个球从房间的一侧移动到另一侧都将需要相同的时间才能以非常快的速度完成PC和一个慢的。

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

例如

private const float BallSpeedInMetresPerSecond = 10;

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

这不会完全解决您的问题(如果某些东西真的很快,无论如何它都会卡在墙上!),但它是一种简单而有效的方法,可以让事情保持可预测和一致,直到您遇到更复杂的问题。

如果你得到了这个工作,然后想要解决一个更复杂的问题,就像 dash-tom-bang 所说的那样,请查看扫描碰撞检测。

于 2010-05-06T22:45:27.847 回答
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],但每个都会按预期工作。

于 2010-05-07T11:21:38.977 回答
0

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

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

只是我的意见,希望我没有离谱。

于 2010-05-06T21:19:55.213 回答
0

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

在运行时,跟踪实际经过的时间,跟踪逻辑上经过了多少时间(就执行的更新而言),当有超过 1.0/N 秒的差异时(因为渲染时间太长)执行额外的更新赶上。这比尝试一次性执行任意时间段的更新要好,因为它更可预测。(如果读者不同意,欢迎他们以艰难的方式发现这一点。)

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

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

于 2010-05-07T01:10:12.453 回答