电子游戏新手指南

我通过反复试验学到的东西,所以你不必这样做,

我是如何学会不再担心,学会去爱闪电侠的。

Pygame 是一个python包装器,用于 SDL, 作者:皮特·辛纳斯。这意味着,使用pyGame,您可以使用Python编写游戏或其他多媒体应用程序,这些游戏或其他多媒体应用程序可以在SDL支持的任何平台(Windows、Linux、Mac和其他平台)上原封不动地运行。

PYGAME可能很容易学习,但图形编程的世界可能会让新手感到相当困惑。我写这篇文章是为了尝试提炼我在过去一年左右的时间里在使用pyGame及其前身PySDL方面获得的实践知识。我试着将这些建议按重要性排序,但任何特定的提示有多重要将取决于你自己的背景和你项目的细节。

熟悉在Python中工作。

最重要的是对使用python感到自信。如果你也不熟悉你正在使用的语言,那么学习像图形编程这样可能很复杂的东西将是一件真正的苦差事。用python语言编写几个相当大的非图形程序--解析一些文本文件、编写一个猜谜游戏或一个日记条目程序等等。熟悉字符串和列表操作--知道如何拆分、切片和组合字符串和列表。知道如何操作 import Works--试着编写一个分布在多个源文件中的程序。编写您自己的函数,并练习处理数字和字符;知道如何在两者之间进行转换。到了使用列表和字典的语法是第二天性的地步--您不希望每次需要对列表进行切片或对一组键进行排序时都必须运行文档。熟悉使用文件路径--当您开始加载资源和创建保存文件时,这将在以后派上用场。

当你遇到麻烦时,抵制在网上寻求直接帮助的诱惑。相反,启动解释器并花几个小时处理问题,或者使用打印语句和调试工具来找出代码中的错误所在。养成在官员那里查资料的习惯 Python documentation ,并在谷歌上搜索错误消息以找出它们的含义。

这听起来可能非常乏味,但当您开始编写游戏时,通过熟悉Python而获得的信心将会创造奇迹。与编写真正的代码时所节省的时间相比,您花费在使python代码成为第二天性的时间将是微不足道的。

认清哪些部分是你真正需要的。

查看pyGame文档索引顶部的杂乱无章的类可能会令人困惑。重要的是要认识到,您只需使用很小的函数子集就可以做很多事情。许多您可能永远不会用到的类--一年来,我都没碰过 ChannelJoystickcursorssurfarrayversion 功能。

知道什么是曲面。

纸牌游戏最重要的部分是表面。只要把一个表面想象成一张白纸。你可以用一个表面做很多事情--你可以在它上面画线,用颜色填充它的一部分,向它复制图像和从它复制图像,以及在它上面设置或读取单独的像素颜色。曲面可以是任何大小(在合理范围内),您可以拥有任意数量的曲面(同样,在合理范围内)。有一个表面是特别的--你用来创建的那个 pygame.display.set_mode()Initialize a window or screen for display 。这个“显示界面”代表屏幕;无论你对它做什么,都会出现在用户的屏幕上。

那么,如何创建曲面呢?如上所述,您可以使用创建特殊的‘Display Surface’ pygame.display.set_mode() 。您可以通过使用创建包含图像的曲面 pygame.image.load()load new image from a file (or file-like object) ,或者您可以创建一个包含文本的图面 pygame.font.Font.render()draw text on a new Surface 。您甚至可以使用以下命令创建一个不包含任何内容的曲面 pygame.Surface()pygame object for representing images

大多数曲面函数都不是关键函数。只要学就行了 Surface.blit()Surface.fill()Surface.set_at()Surface.get_at() ,你就会好起来的。

使用Surface.Convert()。

当我第一次阅读文档时 Surface.convert() ,我不认为这是我必须担心的事情。我只使用PNG,所以我做的每件事都是相同的格式,所以我不需要 convert() ‘;。事实证明我大错特错了。

《格式》是指 convert() 指的不是 file 格式(即PNG、JPEG、GIF),这就是所谓的“像素格式”。这指的是曲面在特定像素中记录单个颜色的特定方式。如果表面格式与显示格式不同,SDL将不得不为每个Blit动态转换它--这是一个相当耗时的过程。别太在意解释,只要注意到就行了 convert() 如果你想从你的BLITS中获得任何形式的速度,这是必要的。

如何使用Convert?只需在使用 image.load() 功能。不是简单地做::

surface = pygame.image.load('foo.png')

DO::

surface = pygame.image.load('foo.png').convert()

就这么简单。当你从磁盘上加载一个图像时,你只需要为每个表面调用一次。您会对结果感到满意的;我看到通过调用 convert()

你唯一不想用的时候 convert() 当您确实需要绝对控制图像的内部格式时--比如您正在编写图像转换程序或其他东西,并且您需要确保输出文件与输入文件具有相同的像素格式。如果你在编写一款游戏,你需要速度。使用 convert()

警惕过时的、过时的和可选的建议。

从本世纪初开始,PyGame就出现了,从那时起,无论是在框架本身还是在更广泛的计算环境中,都发生了很大的变化。一定要检查你阅读的材料上的日期(包括这本指南!),并对旧的建议持保留态度。以下是一些让我印象深刻的常见事情:

肮脏的评论和表演的“把戏”

当你在线阅读一些较旧的电子游戏文档或指南时,你可能会看到,出于性能的考虑,你可能会看到一些强调只更新屏幕上脏的部分(在这种情况下,“脏”意味着区域自上一帧绘制以来发生了变化)。

一般来说,这需要打电话给 pygame.display.update()Update portions of the screen for software displays (带有矩形列表),而不是 pygame.display.flip()Update the full display Surface to the screen ,没有滚动的背景,甚至没有每一帧都用背景颜色填充屏幕,因为想必PYGAME不能处理它。PYGAME的一些API也被设计为支持这一范例(例如 pygame.sprite.RenderUpdates()Group sub-class that tracks dirty updates. ),这在PYGAME的早期是很有意义的。

然而,在今天(2022年),大多数普通的台式计算机的功能足以以每帧60 FPS甚至更高的速度刷新整个显示器一次。你可以有一个移动的摄像头,或者动态的背景,你的游戏应该可以在60FPS下运行得很好。现在的CPU功能更强大,您可以使用 display.flip() 无所畏惧。

话虽如此,但仍有一些时候,这种旧的技术仍然有用,以挤出一些额外的FPS。例如,用一款像小行星或太空入侵者这样的单屏游戏。以下是其工作原理的大致流程:

不是每一帧都更新整个屏幕,而是只更新自上一帧以来更改的部分。为此,您可以跟踪列表中的那些矩形,然后调用 update(the_dirty_rectangles) 在帧的末尾。关于一个移动的精灵的详细信息:

  • 在精灵的当前位置上绘制一段背景,并将其删除。

  • 将精灵的当前位置矩形追加到名为Diry_Rects的列表中。

  • 移动精灵。

  • 在新位置绘制精灵。

  • 将精灵的新位置追加到我的Diry_Rects列表中。

  • 打电话 display.update(dirty_rects)

即使用现代的CPU制作高性能的2D游戏并不需要这种技术,但了解它仍然是有用的。还有很多其他方法会意外地用优化不佳的渲染逻辑来降低游戏的性能。例如,即使在现代硬件上,调用它也可能太慢 set_at 在显示表面上每像素一次。注意表现仍然是你必须要做的事情。

只是没有那么多“一个巧妙的技巧来修复代码性能”的提示。每个游戏都是不同的,每种游戏都有不同的问题和不同的算法来有效地解决这些问题。几乎每次2D游戏代码无法达到合理的帧速率时,根本原因都是糟糕的算法或对基本游戏设计模式的误解。

如果您有性能问题,请首先确保您没有在游戏循环中重复加载文件,然后使用分析代码的众多选项中的一个来找出占用时间最多的是什么。一旦你对为什么你的游戏速度慢至少有了一些了解,试着问问互联网(通过谷歌),或者是pyGame社区,他们是否有更好的算法来帮助你。

HWSURFACE和DOUBLEBUF

HWSURFACE display.set_mode() FLAG在PYGAME版本2.0.0及更高版本中什么也不做(如果你不相信我,可以查看文档)!没有理由再用它了。即使在pyGame 1中,它的效果也是相当微妙的,而且通常被大多数pyGame用户误解。不幸的是,它从来都不是一面神奇的提速旗帜。

DOUBLEBUF仍然有一些用处,但也不是一面神奇的提速旗帜。

雪碧课程

您不需要使用内置的 SpriteGroup 如果你不想去上课的话。在许多教程中,它可能看起来像 Sprite 是pyGame的基本“GameObject”,所有其他对象都必须从它派生,但实际上它几乎只是一个包装 Rect 和一个 Surface ,以及一些额外的方便方法。您可能会发现从头开始编写游戏的核心逻辑和类会更直观(也更有趣)。

没有第六条规则。

不要被枝节问题分心。

有时,新的游戏程序员会花太多时间担心那些对他们的游戏成功并不真正关键的问题。想要解决次要问题的愿望是可以理解的,但在创建游戏的早期,你甚至不知道重要的问题是什么,更不用说你应该选择什么答案了。其结果可能是许多不必要的推诿。

例如,考虑如何组织图形文件的问题。每个帧都应该有自己的图形文件,还是每个子画面都有?也许所有的图形都应该压缩到一个存档中?在许多项目上浪费了大量的时间,在邮件列表上提出这些问题,辩论答案,剖析等等。这是一个次要的问题;花在讨论它的任何时间都应该花在编写实际游戏的代码上。

这里的见解是,拥有一个实际实现的“相当好”的解决方案要比你从未抽出时间编写一个完美的解决方案要好得多。

评论是你的朋友。

Pete Shinners的包装纸可能有很酷的阿尔法效果和快速的闪光速度,但我必须承认我最喜欢的一部分就是低级 Rect 班级。矩形只是一个矩形--仅由其左上角的位置、宽度和高度定义。许多PYGAME函数将RECT作为参数,并且它们还接受‘rectstyle’,这是一个与RECT具有相同值的序列。因此,如果我需要一个矩形来定义介于10、20和40、50之间的区域,我可以执行以下任一操作:

rect = pygame.Rect(10, 20, 30, 30)
rect = pygame.Rect((10, 20, 30, 30))
rect = pygame.Rect((10, 20), (30, 30))
rect = (10, 20, 30, 30)
rect = ((10, 20, 30, 30))

但是,如果您使用前三个版本中的任何一个,就可以访问RECT的实用函数。这些函数包括移动、收缩和充气矩形、查找两个矩形的并集以及各种碰撞检测函数。

例如,假设我想要获得包含一个点(x,y)的所有精灵的列表--可能是玩家点击了那里,或者可能是子弹的当前位置。如果每个精灵都有一个.rect成员,那就很简单了--我只需要做::

sprites_clicked = [sprite for sprite in all_my_sprites_list if sprite.rect.collidepoint(x, y)]

除了可以将它们用作参数之外,Rect与曲面或图形函数没有其他关系。您也可以在与图形无关但仍需要定义为矩形的地方使用它们。在每个项目中,我都会发现一些新的地方来使用RECT,我从来没有想过我会需要它们。

不要费心进行像素完美的碰撞检测。

所以你已经让你的精灵四处移动了,你需要知道它们是否相互碰撞。人们很容易写出下面这样的内容:

  • 检查长方形是否发生碰撞。如果他们不是,那就忽略他们。

  • 对于重叠区域中的每个像素,查看来自两个精灵的相应像素是否不透明。如果是这样的话,就会发生碰撞。

还有其他方法可以做到这一点,用Anding精灵面具等等,但无论你用哪种方法,它可能都会太慢。对于大多数游戏来说,最好的方法可能就是做“子矩形碰撞”--为每个比实际图像稍小的精灵创建一个矩形,然后用它来处理碰撞。它会快得多,而且在大多数情况下,玩家不会注意到不准确。

管理事件子系统。

PYGAME的赛事系统有点棘手。实际上,有两种不同的方法可以了解输入设备(键盘、鼠标或操纵杆)在做什么。

第一种是通过直接检查设备的状态。要做到这一点,你可以打电话,比如说 pygame.mouse.get_pos()get the mouse cursor positionpygame.key.get_pressed()get the state of all keyboard buttons 。这将告诉您该设备的状态 在您调用该函数的时刻。

第二种方法使用SDL事件队列。该队列是一个事件列表--事件在检测到时被添加到列表中,在读出时从队列中删除。

每一种制度都有其优缺点。状态检查(系统1)为您提供了精确度--您确切地知道给定的输入是在什么时候进行的--如果 mouse.get_pressed([0]) 为1,则表示鼠标左键按下 就在这一刻 。事件队列只报告鼠标在过去的某个时间按下;如果您经常检查队列,这是可以的,但如果您被其他代码延迟检查,输入延迟可能会增加。状态检查系统的另一个优点是它可以很容易地检测到“和弦”,即同时检测几个状态。如果您想知道是否 tf 按键同时按下,只需检查::

if key.get_pressed[K_t] and key.get_pressed[K_f]:
    print("Yup!")

然而,在队列系统中,每个按键都作为一个完全独立的事件进入队列,因此您需要记住 t Key向下,并且还没有出现,同时检查 f 钥匙。稍微复杂一点。

然而,国家体制有一个很大的弱点。它只报告设备在被调用时的状态;如果用户点击鼠标按钮,则在调用 mouse.get_pressed() ,鼠标按键将返回0-- get_pressed() 完全没有按下鼠标按钮。这两个事件, MOUSEBUTTONDOWNMOUSEBUTTONUP 但是,仍将位于事件队列中,等待检索和处理。

教训是:选择符合您要求的系统。如果您的循环中没有太多内容--假设您只是坐在一个 while True 循环,等待输入,使用 get_pressed() 或另一个状态函数;等待时间将更低。另一方面,如果每次按键都很重要,但延迟不是那么重要--假设您的用户正在编辑框中键入内容,则使用事件队列。有些按键可能会稍晚一些,但至少你会得到它们。

关于以下内容的说明 event.poll() v.v. wait() -- poll() 可能看起来更好,因为它不会阻止您的程序在等待输入时执行任何操作-- wait() 挂起程序,直到收到事件。然而, poll() 将在运行时消耗100%的可用CPU时间,并且它将用 NOEVENTS 。使用 set_blocked() 只选择您感兴趣的那些事件类型--您的队列将更易于管理。

关于事件队列的另一个注意事项--即使您不想使用它,您仍然必须定期清除它,因为当用户在窗口上按键和鼠标时,它仍然会在后台充满事件。在Windows上,如果你的游戏在没有清除队列的情况下运行太长时间,操作系统会认为它已经冻结,并显示一条“应用程序没有响应”的消息。迭代遍历 event.get() 或者干脆打电话给 event.clear() 每帧一次可以避免这种情况。

科罗基对阵阿尔法。

这两种技术有很多混淆之处,其中很大一部分来自所使用的术语。

“Colorkey blotting”指的是告诉PYGAME,某个图像中某一种颜色的所有像素都是透明的,而不是它们碰巧是什么颜色。当图像的其余部分被屏蔽时,这些透明像素不会被屏蔽,因此不要模糊背景。这就是我们制作不是长方形的精灵的方法。只需拨打 Surface.set_colorkey() ,并传入RGB元组--比方说(0,0,0)。这将使源图像中的每个像素透明而不是黑色。

“阿尔法”是不同的,它有两种口味。“图像阿尔法”适用于整个图像,很可能就是你想要的。Alpha恰如其分地被称为“半透明”,它使源图像中的每个像素仅 部分 不透明。例如,如果将曲面的Alpha设置为192,然后将其绘制到背景上,则每个像素的颜色的3/4将来自源图像,1/4来自背景。Alpha的测量范围为255到0,其中0表示完全透明,255表示完全不透明。请注意,Colorkey和Alpha Bitting可以组合在一起--这会产生在某些点上完全透明,而在另一些点上半透明的图像。

“Per-Pixel Alpha”是Alpha的另一种风格,它更复杂。基本上,源图像中的每个像素都有自己的Alpha值,范围从0到255。因此,每个像素在被放大到背景上时可以具有不同的不透明度。这种类型的Alpha不能与Colorkey Bitting混合使用,它会覆盖每幅图像的Alpha。每个像素的Alpha在游戏中很少使用,要使用它,你必须用一个特殊的图形编辑器保存你的源图像 阿尔法通道 。这很复杂--先别用它。

软件体系结构、设计模式和游戏。

您可能已经习惯了编写代码,能够在没有帮助的情况下解决复杂的问题,了解如何使用pyGame的大多数模块,但是,随着时间的推移,当您处理较大的项目时,这些模块似乎总是变得越来越杂乱和难以维护。这可以在许多方面表现出来--例如,在一个地方修复错误似乎总是在其他地方创建新的错误,找出 哪里 代码应该去可能会成为一个挑战,添加新的东西可能经常需要您重写许多其他的东西,等等。最后,你决定减少损失,重新开始新的事情。

这是一个常见的问题,可能会令人沮丧--一方面,您的编程技能正在提高,但由于某种模糊的组织问题,您无法完成开始的游戏。

这就引出了软件体系结构和设计模式的概念。您可能熟悉pyGame的“标准”基本模板(有许多与此相同的变体,所以不要过于强调小细节):

import pygame

pygame.init()

screen = pygame.display.set_mode((1280,720))

clock = pygame.time.Clock()

while True:
    # Process player inputs.
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            raise SystemExit

    # Do logical updates here.
    # ...

    screen.fill("purple")  # Fill the display with a solid color

    # Render the graphics here.
    # ...

    pygame.display.flip()  # Refresh on-screen display
    clock.tick(60)         # wait until next frame (at 60 FPS)

它进行一些初始设置,开始一个循环,然后继续重复收集输入,处理游戏的逻辑,并永远绘制当前帧,直到程序结束。这里显示的更新、渲染、等待循环实际上是一个设计模式,它充当大多数游戏的骨架--它之所以多产,是因为它干净、有组织、有效。(这里还有一个重要但容易被忽略的设计特性,即游戏逻辑和渲染例程之间的严格划分。仅这一决定就可以防止与对象更新和渲染相关的整个类别的潜在错误,这是很好的)。

事实证明,有许多这样的设计模式在游戏和软件开发中被频繁使用。关于这方面的一个特别好的资源,我强烈推荐 Game Programming Patterns ,关于这一主题的一本简短的免费电子书。它涵盖了一系列有用的模式和您可能想要使用它们的具体情况。它不会立刻让你成为一名更好的程序员,但学习一些关于软件体系结构的理论可以很大程度上帮助你摆脱停滞不前,更自信地处理更大的项目。

用毒蛇般的方式做事。

最后一点(这不是最不重要的一点;它只是在最后)。PyGame是围绕SDL的非常轻量级的包装器,而SDL又是围绕本机操作系统图形调用的非常轻量级的包装器。很有可能,如果您的代码仍然很慢,并且您已经完成了我上面提到的事情,那么问题就出在您在Python中处理数据的方式上。无论您做什么,某些习惯用法在Python中都将变得很慢。幸运的是,Python是一种非常清晰的语言--如果一段代码看起来笨拙或笨拙,它的速度也有可能提高。读一遍 Why Pygame is Slow 更深入地了解为什么PYGAME可能会被认为比其他框架/引擎慢,以及这在实践中实际上意味着什么。如果您确实被性能问题难住了,像这样的分析器 cProfile (或 SnakeViz, CProfile的可视化工具)可以帮助识别瓶颈(它们会告诉您执行代码的哪些部分花费的时间最长)。这就是说,过早优化是万恶之源;如果它已经足够快了,就不要为了让它更快而折磨代码。如果它足够快,那就顺其自然吧:)

这就对了。现在你几乎知道了我所知道的关于使用电子游戏的所有事情。现在,去写那个游戏吧!


David Clark is an avid pygame user and the editor of the Pygame Code Repository, a showcase for community-submitted python game code. He is also the author of Twitch, an entirely average pygame arcade game.

该指南在2022年进行了大幅更新。




Edit on GitHub