游戏如何一次处理所有角色?


31

这个问题只是为了获得有关游戏如何一次处理多个角色的知识。我是游戏新手,所以请您原谅。

我正在创建一个塔防游戏,该游戏中有15个塔槽,可在其中建造塔,每个塔均以一定速率发射弹丸。假设每个塔每秒钟创造出2枚弹丸,并且有敌人在战场上行进,比如说70(每个都有10种属性,例如HP,法力等,随着它们在战场上移动而变化战场)。

摘要

塔数 =
每秒每座塔创建的 15个弹丸 = 2
每秒创建的弹丸总数 =
战场计数中的 30个单位 = 70

现在,游戏是否通过在100个不同的线程(对于PC来说太多了)或1个移动所有线程,降低其值的线程上处理它们,来处理这30个弹丸和70个单位?(这有点慢, 我认为)?

我对此一无所知,所以有人可以指导我如何解决吗?


评论不作进一步讨论;此对话已转移至聊天
MichaelHouse

除了其他答案外,还有一些大型游戏的示例。《天际》的大部分游戏逻辑都在一个线程上进行更新。它的管理方式如此出色,是根据其时间表来估算远处的NPC(相距数英里的NPC)。大多数MMO在单个线程上更新游戏逻辑,但地图的每个部分都在不同的线程或服务器机架上。
moonshineTheleocat

Answers:


77

现在,游戏如何在100个不同的线程上处理这30个投射物和70个单位

不,永远不要那样做。永远不要为每个资源创建一个新线程,这不会在网络中扩展,也不会在更新实体中扩展。(有人记得在Java中有一个线程可以读取每个套接字的时间吗?)

1个移动所有线程的线程会减少其值,等等吗?

是的,对于初学者来说,这是要走的路。“大型引擎”在线程之间分配了一些工作,但是启动像塔防游戏这样的简单游戏并不需要这样做。可能需要做更多的工作才能在此线程中完成每个刻度。哦,是的,当然还有渲染。

(我认为这有点慢)

好吧...你对的定义是什么?对于100个实体,根据您的代码质量和所使用的语言,时间不应超过半毫秒,甚至可能更短。即使花费了整整两毫秒的时间,它仍然足以达到60 tps(每秒点数,在这种情况下不讨论帧)。


34
除非您做一些奇怪的事情,否则更像半微秒。但最重要的是,将工作拆分到多个线程将使所有事情变得更糟,而不是更好。更不用说多线程非常困难。
a安

13
+1大多数现代游戏引擎实时渲染成千上万个多边形,这比跟踪内存中仅100个对象的运动要密集得多。
phyrfox

1
“像塔防游戏这样的简单游戏。” 嗯...您曾经玩过《防御网格:觉醒》或其续集吗?
梅森惠勒

4
“不要创建每个资源一个新的线程,这并不在网络规模......” 咳咳一些非常可扩展的架构做的正是这样!
NPSF3000 '16

2
@BarafuAlbino:说的很奇怪。有很多有效的理由来创建比可用内核更多的线程。像其他任何设计决策一样,这是复杂性/性能/等等的折衷。
Dietrich Epp 2016年

39

多线程规则的第一个规则是:除非需要在多个CPU内核上并行化以提高性能或响应能力,否则请不要使用它。“从用户的角度来看,x和y应该同时发生”的要求还不足以使用多线程。

为什么?

多线程很难。您无法控制每个线程何时执行,这可能导致各种无法重现的问题(“竞争条件”)。有一些方法可以避免这种情况(同步锁,关键部分),但是这些方法都有自己的一系列问题(“死锁”)。

通常游戏这对付这样的数目的对象为只有几百(是的,这是没有那么多的游戏开发),通常使用一个共同处理它们以串行方式对每个逻辑蜱for循环。

甚至相对较弱的智能手机CPU 每秒也可以执行数十亿条指令。这意味着即使对象的更新逻辑很复杂,每个对象和一个滴答需要大约1000条指令,并且您的目标是每秒100个滴答滴答,但您仍具有足够的CPU容量来容纳成千上万个对象。是的,这是一个大大简化了的信封计算,但它给了您一个主意。

同样,游戏开发中的常识是,游戏逻辑很少是游戏的瓶颈。性能至关重要的部分几乎总是图形。是的,即使是二维游戏。


1
“多线程规则的第一个规则是:不要使用它,除非您需要在多个CPU内核上并行化以提高性能或响应能力。” 游戏开发可能是正确的(但我什至对此表示怀疑)。在使用实时系统时,添加线程的主要原因是为了满足最后期限,并且逻辑上很简单。
山姆

6
@Sam在实时系统中满足最后期限是您需要多线程以提高响应速度的情况。但是,即使在那儿,您看似通过线程实现的逻辑简单性也常常令人生畏,因为它以死锁,争用条件和资源匮乏的形式创建了隐藏的复杂性。
菲利普

不幸的是,如果遇到寻路问题,我已经多次看到游戏逻辑使整个游戏陷入困境。
罗伦·佩希特尔

1
@LorenPechtel我也看到了这一点。但是通常,通过不执行不必要的路径计算(例如重新计算每个刻度线上的每个单个路径),缓存频繁请求的路径,使用多层路径查找和使用更合适的路径查找算法,可以解决此问题。这是熟练的程序员通常可以发现很多优化潜力的地方。
菲利普

1
@LorenPechtel For example in a tower defense game, you could use the fact that there are typically only a handful of destination points. So you could run Dijkstra's algorithm for each destination to compute a direction map which guides all the units. Even in a dynamic environment where you have to recompute these maps every frame, this should still be affordable.
CodesInChaos

26

The other answers have handled the threading and power of modern computers. To address the bigger question though, what you are trying to do here is avoid "n squared" situations.

For example if you have 1000 projectiles and 1000 enemies the naive solution is to just check them all against each other.

This means you end up with p*e = 1,000*1,000 = 1,000,000 different checks! This is O(n^2).

On the other hand if you organize your data better you can avoid a lot of that.

For example if you list on each square of the grid what enemies are in that square then you can loop through your 1000 projectiles and just check the square on the grid. Now you just need to check each projectile against the square, this is O(n). Instead of a million checks each frame you only need a thousand.

Thinking about organizing your data and processing it efficiently due to that organisation is the biggest single optimization you can ever make.


1
As an alternative to storing the entire grid in memory just to track a few elements, you could also use b-trees, one for each axis, to quickly search through possible candidates for collisions, etc. Some engines even do this for you "automatically"; you specify hit regions, and ask for a list of collisions, and the library gives it to you. This is one of many reasons why developers should be using an engine instead of writing from scratch (when possible, of course).
phyrfox

@phyrfox Certainly, there are any number of different ways to do it - depending on your use-case which is better will vary substantially.
Tim B

17

Do not create threads per resource/object but per section of your program logic. For example:

  1. Thread to update units and projectiles - logic thread
  2. Thread for rendering the screen - GUI thread
  3. Thread for network (eg. multiplayer) - IO thread

The advantage of this is that your GUI (eg. buttons) does not necessarily get stuck if your logic is slow. User can still pause and save the game. It's also good for preparing your game for multiplayer, now that you separate the graphic from the logic.


1
For a beginner I wouldn't recommend using separate graphic and logic threads, since unless you copy the required data, rendering the game state requires read access to the game state, so you can't modify the game state while drawing it.
CodesInChaos

1
Not drawing too often (eg. more than 50 times per second) is kinda important and this question was about performance. Dividing program is the simplest thing to do for a real performance benefit. It's truth this requires some knowledge about threads, but acquiring that knowledge is worthwhile.
Tomáš Zato - Reinstate Monica

Dividing a Program into multiple Threads is kind of the hardest thing for a programmer to do. Most really annoying bugs stem from multi-threading and it is a giant amount of hassle and most of the time just not worth it - First rule: Check if you have a performance problem, THEN optimize. And optimize right where the bottleneck is. Maybe a single external thread for a certain complex algorithm. But even then you have to think how your game logic will advance when this algorithm takes 3 seconds to finish...
Falco

@Falco You're overseeing the long term advantages of this model - both for the project and the programmer experience. Your claim that it's hardest think can't really be addressed, that's just an opinion. To me GUI design is much more terrifying. All evolved languages (C++, Java) have pretty clear multithreading models. And if you're really not sure, you can use actor model which doesn't suffer from beginner multithreading bugs. You know there's a reason why most applications are designed as I proposed, but feel free to argue about it further.
Tomáš Zato - Reinstate Monica

4

Even Space Invaders managed dozens of interacting objects. Whereas decoding one frame of HD H264 video involves hundreds of millions of arithmetic operations. You have a lot of processing power available.

That said, you can still make it slow if you waste it. The problem is not so much the number of objects as the number of collision tests performed; the simple approach of checking each object against each other object squares the number of calculations required. Testing 1001 objects for collisions this way would require a million comparisons. Often this is addressed by e.g. not checking projectiles for collision with each other.


2
I'm not sure Space Invaders is the best comparison to make. The reason it starts out slow and speeds up as you kill enemies isn't because it was designed that way, but because the hardware couldn't handle rendering that many enemies at once. en.wikipedia.org/wiki/Space_Invaders#Hardware
Mike Kellogg

What about, each object maintains a list of all objects that are close enough that it might collide with them in the next second, updated once a second or each time they change direction?
Random832

Depends on what you're modelling. Space partitioning solutions are another common approach: partition the world into regions (e.g. BSP which you may have to do anyway for rendering purposes, or quadtree), then you can only collide with objects in the same region.
pjc50

3

I am going to disagree with some of the other answers here. Separate logic threads are not only a good idea, but hugely beneficial to processing speed - if your logic is easily separable.

Your question is a good example of logic that is probably separable if you can add some additional logic on top of it. For example, you could run several hit detection threads either by locking the threads to specific regions of space, or mutexing the objects involved.

You probably do NOT want one thread for every possible collision, just because that is likely to bog down the scheduler; there is also a cost associated with creating and destroying threads. Better to make some number of threads around the system's cores (or utilize a metric like the old #cores * 2 + 4), then reuse them when their process finishes.

Not all logic is easily separable, though. Sometimes your operations can reach across all game data at once, which would make threading useless (in fact, harmful, because you would need to add checks to avoid threading issues). Further, if multiple stages of logic are highly dependent on each other occurring in specific orders, you will have to control the execution of threads in such a way as to ensure that does not give order-dependent results. However, that issue isn't eliminated by not using threads, threads just exacerbate it.

Most games don't do this simply because it is more complex than the average game developer is willing/able to handle for what is usually not the bottleneck in the first place. The vast majority of games are GPU-limited, not CPU-limited. While improving the CPU speed can help overall, it's usually not the focus.

That said, physics engines often employ multiple threads, and I can name several games I think that would have benefited from multiple logic threads (the Paradox RTS games like HOI3 and such, for example).

I do agree with other posts that you probably would have no need to employ threads in this specific example, even if it could be beneficial. Threading should be reserved to cases where you have excessive CPU load that cannot be optimized down via other methods. It is a huge undertaking and will affect the fundamental structure of an engine; it isn't something you can tack on after the fact.


2

I think the other answers miss an important part of the question by focusing too much on the threading part of the question.

A computer doesn't handle all objects in a game at once at all. It handles them in sequence.

A computer game progresses in discrete time-steps. Depending on the game and the speed of the PC, these steps are usually either 30 or 60 steps per second, or as many/few steps as the PC can calculate.

In one such step, a computer calculates what each of the game objects will do during that step and updates them accordingly, one after another. It could even do so in parallel, using threads to be faster, but as we'll soon see speed is not a concern at all.

An average CPU should be 2 GHz or faster, that means 109 clock cycles per second. If we calculate 60 timesteps per second, that leaves 109 / 60 clock cycles = 16,666,666 clock cycles per time step. With 70 units, we still have about 2,400,000 clock cycles per unit left. If we had to optimize, we might be able to update each unit in as little as 240 cycles, depending on the complexity of the game logic. As you can see, our computer is about 10,000 times faster than it needs to be for this task.


0

Disclaimer: My all time favourite type of game is text-based and I write this as a long time programmer of an old MUD.

I think an important question you need to ask yourself is this: Do you even need threads? I understand that a graphical game probably has more use of MTs but I think it also depends on the mechanics of the game. (It might also be worth considering that with GPUs, CPUs and all the other resources we have today are far more powerful which makes your concerns of resources as problematic as it might seem to you; indeed 100 objects is virtually zero). It also depends on how you define 'all characters at once'. Do you mean at the exact same time? You won't have that as Peter rightfully points out so all at once is irrelevant in the literal sense; it only appears this way.

Assuming you will go with threads: You definitely should not consider 100 threads (and I am not even going to get into whether it is too much for your CPU or not; I refer only to the complications and the practicality of it).

But remember this: multiple-threading is not easy (as Philipp points out) and has many problems. Others have much more experience (by a lot) than I do with MT but I would say they too would suggest the same thing (even though they would be more capable than I would be - especially without practise on my part).

Some argue that they disagree that threads aren't beneficial and some argue that each object should have a thread. But (and again this is all text but even if you consider more than one thread you need not - and should not - consider it for each object) as Philipp points out games tend to iterate through the lists. But yet it isn't only (as he suggests although I realise he is only responding to your parameters of so few objects) for so few objects. In the MUD I am a programmer for we have the following (and this isn't all the activity that happens in real-time so keep that in mind too):

(The number of instances do vary of course - higher and lower)

Mobiles (NPC i.e. non player character): 2614; prototypes: 1360 Objects: 4457; prototypes: 2281 Rooms: 7983; prototypes: 7983. Each room has its own instance usually but we also have dynamic rooms which is to say rooms within a room; or rooms inside a mobile e.g. the stomach of a dragon; or rooms in objects e.g. you enter a magical object). Keep in mind that these dynamic rooms exist per object/room/mobile that actually has them defined. Yes this is very much like World of Warcraft's (I don't play it but a friend had me play it when I had a Windows machine, for a while) idea of instances except we had it long before World of Warcraft even existed.

Scripts: 868 (currently) (oddly enough our statistics command doesn't show how many prototypes we have so I will be adding that). All of these are held in areas/zones and we have 103 of those. We also have special procedures that proc at different times. We also have other events. Then we also have connected sockets. Mobiles move around, do different activities (besides combat), have interactions with players, and so on. (So do other types of entities).

How do we handle all this without any delay?

  • sockets: select(), queues (input, output, events, other things), buffers (input, output, other things), etc. These are polled 10 times a second.

  • characters, objects, rooms, combat, everything: all in a central loop on different pulses.

We also (my implementation based on discussion between the founder/other programmer and myself) have extensive linked list tracking and pointer validity testing and we have more than enough free resources should we actually have a need for it. All of this (except we have expanded the world) existed years ago when there was less RAM, CPU power, hard disk space, etc. And indeed even then we had no problems. In the loops described (scripts cause this as do area resets/repopulations as do other things) monsters, objects (items), and other things are being created, freed, and so on. Connections are also accepted, polled, and everything else you would expect.

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.