这篇文章推荐从一个线程中调用pygame.display.flip
,在发帖之前我在mac、windows和linux上进行了广泛的测试,但是在一些读者的反馈之后,我意识到这个策略事实上并不是跨平台的;具体来说,如果你尝试这样做,linux上的nvidia驱动似乎要么崩溃,要么显示一个黑窗口。SDL的FAQ确实说过,你不能从多个线程中调用 "视频函数",而flip
在引擎盖下确实做到了这一点。 我确实计划再次更新这个帖子,要么用一种方法使之安全,要么用一种稍微复杂的计时启发式方法来完成同样的事情。 同时,请注意,这可能会给你的代码带来可移植性问题。
我以前写过这方面的文章,但在那个背景下,我主要写的是帧速率的独立性,只简单提到了垂直同步;标题还提到了Twisted,重新阅读后我意识到,许多可能从其技术中获得大量使用的朋友不会去读它,只是因为我把它说成是在一个游戏中的动画技术背景下的一个旁观者,而出于某种原因已经想使用Twisted,而不是一个全面的最佳实践。 不过现在Pygame 2.0已经出来了,而且每个人都可以更可靠地使用vsync=1
标志,我认为这值得重温一下。
根据外面的许多教程,包括官方教程,大多数Pygame主循环都是这样的。
1 2 3 4 5 6 7 8 |
|
显然,这样做还行,否则人们就不会这样做,但对于大多数Pygame初学者来说,这可能会给人一种缺乏修饰的印象。
关于这个成语,一直困扰着我个人的是:网络的发展方向是什么? 在花了很多年时间试图在Python中普及事件循环之后,我很难过地看到人们一次又一次地实现循环,却没有办法让网络、线程或定时器以标准的方式安排,这样就可以编写库而不需要应用程序每一帧都手动调用它们。
但是,谁在乎我的感受呢? 很多游戏都没有网络1,它有更普遍的问题。 具体来说,它可能会。
- 浪费能源,以及
- 看起来很糟糕。
浪费电力
为什么有人在制作视频游戏时要关心电力问题? 游戏不是应该把CPU和GPU当做早餐来吃吗,为了获得尽可能多的玩家体验,需要多少就烧掉多少?
很有可能,如果你正在制作一个你不认识的人玩的游戏,他们会在笔记本电脑上玩2。 Pygame可能有 "慢 "的名声,但对于一个只有几个精灵的简单2D游戏,Python可以轻松地每秒渲染几千帧。 即使是世界上最快的显示器也只能以360Hz的速度刷新3。 这还不到每秒一千帧。 普通的笔记本电脑显示屏会更像是60Hz,或者--如果你幸运的话--也许是120Hz。 通过渲染数以千计的用户根本看不到的帧,你让他们的CPU不舒服地升温4,而且你浪费了他们10倍(或更多)的电池做无用功。
在某些时候,你的游戏可能会有足够的内容,以至于它将全速运行CPU,如果是这样的话,这可能是好的;至少你会用掉这些热量和电池寿命,以便让他们的电脑做一些有用的事情。 但是,即使是这样,它也可能不是一直在做,而电池绝对是一个使用时间的问题。
看起来很糟糕
如果你不考虑vsync而直接向屏幕渲染,你的玩家就会遇到屏幕撕裂的问题,即当你在向屏幕绘图时,屏幕正在更新。 如果你的游戏是在背景上平移,这看起来尤其糟糕,对于通常的2D Pygame游戏类型来说,这是很可能发生的情况。
如何解决这个问题?
Pygame可以让你打开VSync,在Pygame 2中,你可以简单地通过传递pygame.SCALED
标志和vsync=1
参数给set_mode()
.
现在你的游戏将有丝般顺滑的动画和滚动5! 解决了!
但是......如果修复如此简单,为什么每个人--包括,特别是官方文档--都不推荐这样做呢?
这个解决方案产生了另一个问题:pygame.display.flip
,现在可能会阻塞,直到下一次显示刷新,这可能是许多毫秒。
更糟的是:注意 "可能 "这个词。 不幸的是,vsync的行为在不同的平台和驱动之间是相当不一致的,所以对于一个正确的跨平台游戏来说,可能有必要让用户选择一个帧率并在asyncio.sleep
,而不是在一个线程中运行flip
。 使用这个堆栈溢出答案中的技术,你可以为相关显示器的刷新率建立一个合理的启发式方法,但如果添加这些库和编写这些代码过于复杂,"60 "可能是一个足够好的开始值,即使用户的显示器可以快一点。 即使在你可以依靠flip
来告诉你显示器何时真正准备好的情况下,这也可能会节省一点电力;如果你的游戏无论如何只能可靠地渲染60FPS,因为有太多的Python游戏逻辑在进行,不能持续地更快,那么实现一个持续但较低的帧率比更快但不稳定要好。
不过需要处理潜在的阻塞问题,它有几个连锁反应。
首先,它使我的 "你把网络放在哪里 "的问题变得更糟:大多数网络框架希望能够每16毫秒发送一个以上的数据包。
然而,对大多数Pygame用户来说,更紧迫的是它造成了一个小小的性能问题。 你现在花了很多时间在现在阻塞的flip
,浪费了宝贵的几毫秒,你可以用这些时间来做与绘画无关的事情,比如处理用户输入、更新动画、运行AI等等。
问题是,你的Pygame主循环有3项工作。
- 绘图
- 游戏逻辑(AI等)
- 输入处理
为了保证最流畅的帧率,你想做的是在帧的开始阶段尽可能快地绘制所有的东西,然后立即调用flip
,以确保图形已经传递到屏幕上,它们不必等到下一次屏幕刷新。 然而,这与在调用flip
之前尽可能多地完成工作,并可能阻断1/60秒的需要是不一致的。
因此,要么你推迟调用flip
,如果你的人工智能有点慢的话,可能会有丢帧的风险;要么你太急于调用flip
,浪费了大量的时间来等待显示器刷新。 这对于像动画这样的东西来说尤其如此,你不能在绘制之前更新,因为你必须在担心下一帧之前绘制这一帧,但是等到flip
,就会浪费宝贵的时间;当你开始绘制下一帧时,你可能还有其他的代码需要运行,而你要在下一次调用flip
之前完成它。
现在,如果你的Python游戏逻辑实际上正在使你的CPU饱和--这并不难做到--无论如何你都会掉帧。 但是在很多边缘情况下,你有足够的CPU来做你需要做的事情而不掉帧,而且不断地检查时钟,看你是否有足够的帧预算在帧的最后期限前再做一个工作项目,或者,就这个问题而言,保持一个可行的启发式方法来确定帧的最后期限是什么时候,这可能是很大的开销。
避免这些问题的技术是非常简单的,事实上,在我之前的文章中已经介绍了deferToThread
的技巧。 但是,我们在这里不是为了讨论Twisted。 所以让我们用asyncio来做这个没有额外依赖的、只用stdlib的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
尽情发挥,更好地循环
在某些时候,我可能会发布我自己的包装库6,它可以做一些类似的事情,但我真的想把它作为一种技术来介绍,而不是作为一些打包的代码来使用,因为自己动手做主循环,并把依赖性降到最低,是Pygame社区文化的主要内容。
正如你所看到的,这个技术只比Pygame主循环的标准配方长了几行,但你现在可以获得大量的额外功能。
- 你可以在动画和游戏逻辑中管理你的帧速率的独立性,只需设置一些定时器,让帧在适当的时间更新;不要再担心自己在时钟上做数学题了
- 你想增加联网的多人游戏吗?没问题!联网都是在事件循环中进行的,你可以提出任何你想要的网络请求,再也不用担心在网络请求中阻断游戏的绘制了!
- 现在,你的玩家的笔记本电脑在游戏时运行得很好,而且图形不再有难看的撕裂痕迹了
我真的希望这能被更广泛地采用,这样 "用Python制作的独立游戏 "的描述就不再意味着 "在屏幕上滑动时运行得很热并经常撕裂"。 我也很想听听读者的意见,所以如果你最终使用这种技术取得了好的效果,请让我知道。7
-
而且,说实话,鉴于这些天来单人游戏体验中有多少不必要的始终在线的东西,少说也能忍受它。 但我想说的是。这就是为什么我在脚注中,这是个适合离题的地方。
-
"十多年来,笔记本电脑的全球销量已经超过了台式机。2019年,台式机销售总量为8840万台,而笔记本电脑为1.66亿台。预计到2023年,这一差距将增长到7900万对1.71亿。"
-
至少,Nvidia说,"世界上最快的电竞显示器 "都是360Hz,而且还支持G-Sync,我有什么理由不同意?
-
因为我当然 会
-
还有,比如说,如果这段代码中有可怕的bug,那么我就可以更新它。 这段代码超级简短和抽象,以显示它的通用性,但这也意味着不可能真正按原样测试它;我的完整工作代码的例子要长得多,而且肯定有可能在翻译中丢失了什么。