python

03 pygame编程入门之三:sprite模块简介

2019-07-16  本文已影响0人  易景漫游杨晓宏

pygame编程入门之三:sprite模块简介

作者: Pete Shinners(pete@shinners.org)

翻译: 杨晓宏(561071777@qq.com)

Pygame版本1.3附带了一个新模块Pygame.sprite。
这个模块是用Python编写的,并且包含一些高级类来管理游戏对象。
通过使用这个模块的全部功能,您可以轻松地管理和绘制您的游戏对象。sprite类是非常优化的,所以游戏在sprite模块上运行的速度会比没有使用sprite类快。

sprite模块也是通用的。事实证明,可以在任何类型的游戏中使用它。
所有这些灵活性都伴随着多一些学习,需要多一点理解才能正确使用。
sprite模块的参考文档可以让您前进,但是这里多一点解释,如何在自己的游戏中使用 pygame.sprite。
一些pygame示例(如“黑猩猩”和“外星人”)已经更新,已经使用sprite模块。可以先查看一下这些sprite模块的全部内容。前一章是黑猩猩模块的逐行教程,这有助于更多地理解python和pygame的编程。

请注意,本文将假设您有一些使用python的编程经验,并且对创建简单游戏的不同部分有些熟悉。在本教程中,偶尔使用“引用”一词。这代表引用一个python变量。可能有几个变量都指向同一个对象。

历史遗留

“sprite”一词是从老式电脑和游戏机的延续来的一个概念。
这些老式设备不能快速地绘制和删除正常的图形,使它们能够像游戏一样工作。这些机器有特殊的硬件来处理像物体,特别是这些物体需要非常快地动画时。
这些对象被称为“sprits”,有特别的局限性,但优点是可以快速绘制和更新。它们通常存在于视频的特殊覆盖缓冲区中。
现在,计算机已经变得足够快,可以不用专用硬件来处理sprite。sprite这个词仍然被用来表示任何在2D游戏中的动画物体。

srite模块有两个主要的类。第一种是Sprite,它应该作为所有游戏对象的基类。
这个类本身并不做任何事情,它只是包含了一些帮助管理游戏对象的功能。另一种类型是Group。Group类是不同sprite对象的容器。实际上有几种不同类型的group类。例如,有些group可以画出它们包含的所有元素。
这就是它的全部。我们将从对每种类型的类所做的描述开始,然后讨论使用这两个类的正确方法。

Sprite类

如前面所说,Sprite类被设计为所有游戏对象的基类。
无法单独使用Sprite类,因为它只有几个方法来帮助处理不同的group类。sprite会跟踪它所属的group的参数变化。
类构造器(init方法)接受sprite的实例 所属group(或group列表)的参数。
Sprite类可以使用add()和remove()方法改变sprite group的成员关系。
Sprite类还有一个group()方法,它返回包含sprite的当前的group的列表。
当使用sprite类时,最好将它们看作“有效的”或“活着的”。当从所有group中删除实例时,pygame将清理对象。(除非你在其他地方有实例引用。)
Sprite类kill()方法将sprite从它所属的所有group中删除。这将干净地删除sprite对象。如果把一些小游戏放在一起,你会知道有时候干净地删除一个游戏对象是很棘手的。
sprite还附带一个alive()方法,如果它还是任何group成员,它返回true。

Group类

Group类只是一个简单的容器。
与sprite类一样,它有一个add()和remove()方法,它可以改变sprite是否属于这个group。您还可以将sprite或sprite列表传递给构造器(init()方法),以创建包含一些初始sprite的group实例。
该group还有一些其他的方法,比如clear(),从group中删除所有的sprite和copy(),它将返回该group的副本,返回所有相同的成员。
has()方法会快速检查该group是否包含sprite或sprite列表。
经常使用的另一个功能是sprite()方法。这会返回一个对象,该对象可以以轮询方式访问该group每一个成员。
目前只是一个sprite的列表,但是在以后的python版本中,这可能会使用迭代器来获得更好的性能。
该group也有一个update()方法,它将调用group中的每个sprite上的update()方法。将相同的参数传递给每个对象。
通常在游戏中,你需要一些功能来更新游戏对象的状态。
使用group.sprites()方法,可以很容易地调用每个sprite自己的方法。
还要注意,base Sprite类有一个“dummy”更新()方法,它接受任何类型的参数,但什么都不做。
最后,该group类还有一些方法,允许使用内置的len()函数,获得包含的sprite数量,以及“truth”操作符,它允许您执行“if mygroup:”来检查group是否有任何sprite。

将group进行有效group合

这两个类看起来非常基础。
一个类不要做太多的事情,一个类可以表示一个简单的列表。使用Sprite和group有很大的优势。
sprite可以属于任意数量的group。记住,一旦它不属于任何group,通常会被清除(除非有“non-group”引用到该对象)。
第一件事是对sprite进行快速分类。例如有一个类似于“吃豆人”的游戏。可以为游戏中的不同类型的对象创建单独的group。
幽灵、吃豆人和豆子。当吃豆人吃一个豆子时,我们可以通过影响幽灵group来改变所有的幽灵物体的状态。这比循环遍历所有游戏对象的列表并检查哪些是幽灵要快和简单。
添加和删除group和sprite是一个非常快速的操作,比使用列表存储东西要快。
可以非常有效地改变group成员关系。group可以用来为所有游戏对象的某个相同属性而创建。
可以将它们添加到一个单独的group中,而不是像“close_to_player”这样的一些属性来跟踪一些敌人对象。当需要访问所有靠近玩家的敌人时,已经有了一个列表,而不是浏览所有敌人的列表,检查“close_to_player”标志。
稍后,游戏可以添加多个玩家,把玩家都放在同一个group中。而不是添加更多的“close_to_player2”,“close_to_player3”属性。
使用sprite和group的另一个重要好处是,group可以干净处理游戏对象的删除(或杀死)。
在一个许多物体都在引用其他对象的游戏中,有时删除一个对象可能是最困难的部分。
对象不能消失,直到它没有被任何人引用。假设有一个物体在“追逐”另一个物体。追赶者可以保留一个简单的group,它被正在追逐的对象(或objects)引用。
如果被追赶的对象碰巧被销毁,我们不需要通知追赶者停止追赶。因为追赶者现在它的团队是空的,它也许应该去找一个新的目标。
同样要记住,从group中添加和删除sprite是一个非常便宜/快速的操作。通过添加许多group来控制和group织您的游戏对象,可能是最好的选择。
有些group可能在游戏的大部分时间里都是空的,所以不需要任何多余的消耗来管理游戏。

更多group类型

上面例子只是展示了极少的一部分。使用sprite和group还有更多的便利。
另一个优点是sprite模块有几种不同类型的group。这些group像一个普通group一样工作,但是他们也有增加的功能(或者稍微不同的功能)。
下面是sprite模块包含的group类列表。

Group

这是上面解释的标准“无装饰”group。大多数其他group都继承于这个Group,但并非全部。

GroupSingle

这与普通的group类非常相似,但它只包含最近添加的sprite。因此,当你向这个group添加一个sprite时,它会“忘记”它之前的任何sprite。因此它总是只包含一个或零个sprite。

RenderPlain

这是一个从group中继承的标准group。它有一个draw()方法,它将它所包含的sprite绘制到屏幕上(或任何表面)。为了实现这一点,它需要内部所有sprite都有一个“image”和“rect”属性。它用这些来知道blit对象是什么,以及在哪里blit。

RenderClear

这是从RenderPlaingroup派生出来的,并添加了一个名为clear()的方法。这将擦除所有sprite的先前位置。它使用一个背景图像来填充sprite所在的区域。
它足够智能,可以处理被删除的sprite,并在调用clear()方法时清除屏幕上的sprite。

RenderUpdates

这是rendering Groups的凯迪拉克(高效,并且返回)。它是从RenderClear继承而来的,但改变了draw()方法,也返回了一个pygame Rects的列表,代表已改变的屏幕上的所有区域。

这些是不同类型的group列表,将在下一节中讨论更多关于这些rendering Groups的内容。
没有什么能阻止你创建自己的group类。它们只是python代码,所以可以从其中之一继承并添加/更改您想要的任何东西。
在未来,希望能在这个列表中添加更多的group。一个GroupMulti,就像GroupSingle一样,但可以保持一定数量的sprite(在某种循环缓冲中)。
或者添加一个super-rendergroup,可以在不需要背景图像的情况下清除旧sprite的位置(通过在blitting之前抓取一个屏幕拷贝)。
在将来我们可以在这个列表中添加更多的类,当然你也可以自己定义自己的超级类啊!

render Groups

从上面可以看到有三个不同的render Group。我们可能去掉renderupdate ,它增加了一些不必要的开销,比如卷屏游戏。
不同的group,适合不同的工作。
对于一个卷屏类型的游戏,背景在每一帧都会改变。
显然,我们不需要担心在调用display.update()中,python的更新矩形。应该使用RenderPlain group来管理渲染。
对于那些背景比较固定的游戏,肯定不希望pygame更新整个屏幕(因为它不需要)。这种类型的游戏通常包括擦除每个物体的旧位置,然后每个帧在新位置画出它。这样我们只会改变必要的东西。
大多数情况下,您只需要在这里使用renderupdate类。只需要将变更列表传递给display.update()函数,就可以了。
RenderUpdates类,也能减少更新后的矩形列表中的重叠区域。如果一个物体的前一个位置和当前位置重叠,把它们合并成一个矩形。
将其与已删除对象正确处理,这是一个强大的group类。
如果你已经写了一款游戏来管理游戏中的不断改变位置的对象,你就会知道游戏中很多混乱代码的原因了。
特别是抛出可以在任何时候被删除的对象时。所有这些工作都被RenderUpdates类简化为一个clear()和draw()方法。这个怪物类,加上重叠检查,它可能比你自己写的代码运行要快。
还要注意的是,在游戏中,没有什么能阻止你混合和匹配这些Rendergroup。当你想要把sprite做分层时,应该使用多个Render group。
另外,如果屏幕被分割成多个部分,那么屏幕每个部分都应该使用适当的Render group吗?

碰撞检测

sprite模块还附带两个非常通用的碰撞检测函数。对于更复杂的游戏,这些实际上无法直接使用,但是您可以从源代码根据需要修改它们。
以下是对它们功能和用法的总结。

spritecollide(sprite, group, dokill) -> list

这将检查一个sprite和一个group中的sprite之间的碰撞。所有的sprite都需要一个“rect”属性。它返回与第一个sprite有重叠的所有sprite的列表。
“dokill”参数是一个布尔参数。如果为true,函数将对所有sprite调用kill()方法。这意味着对每个sprite的最后引用在返回列表中。一旦名单消失,sprite也会消失。
一个简单例子,在循环中使用了此方法。

>>> for bomb in sprite.spritecollide(player, bombs, 1):
...     boom_sound.play()
...     Explosion(bomb, 0)

这找到了与玩家相撞的“bomb”group中的所有sprite。因为“dokill”,删除了所有的bomb。
对于每一颗发生碰撞的炸弹,它都会产生一种“轰隆”的声音效果,并在炸弹所在地方制造爆炸。
注意,这里的explosion类将每个实例添加到适当的类中,因此不需要将它存储在一个变量中,最后一行可能会让python程序员感到有点“有趣”。

groupcollide(group1, group2, dokill1, dokill2) -> dictionary

这类似于spritecollide()函数,但更复杂一些。
它检查一个group中所有sprite与另一个group的sprite的碰撞。每个列表都有一个关于sprite的dokill参数。当dokill1为真时,group1中的碰撞sprite将被杀死()。当dokill2是真的时,group2中的碰撞sprite将被杀死()。
它返回的字典是这样工作的;字典中的每个键都是来自group1的发生碰撞的sprite,这个键的值是它与之碰撞的sprite的列表。也许另一个代码示例解释得更好。

>>> for alien in sprite.groupcollide(aliens, shots, 1, 1).keys()
...     boom_sound.play()
...     Explosion(alien, 0)
...     kills += 1

这段代码检查了玩家子弹和可能相交的所有外星人之间的碰撞。
在这种情况下,我们只对字典键进行循环,但是如果我们想对与外星人发生碰撞的特定子弹做点什么,我们可以对values()或items()进行循环。
如果我们对values()进行循环,我们将循环遍历包含sprite的列表。
同一个sprite甚至可能在不同的循环中出现不止一次,因为同样的“射击”可能与多个“外星人”相撞。
这些是pygame的基本碰撞函数。
你应该通过使用“rect”的不同属性,很容易得到自己的要求。
或者试着通过改变碰撞对象来对代码进行微调,而不是重构建一个碰撞列表?
spritecollide函数的代码是非常优化的,可以通过去掉一些不需要的功能来加快它的速度。

常见问题

目前,有一个主要问题困住了新用户。
当使用Sprite base派生新的Sprite类时,必须从自己的类init()方法调用Sprite.init()方法。如果忘记调用Sprite.init()方法,就会得到一个典型错误,如下:

AttributeError:'mysprite'实例没有属性'Sprite g'

扩展您自己的类(高级)

由于速度的问题,当前的group类只做他们需要的事情,而不是处理很多一般情况。
如果您决定需要额外的属性,您可能要创建自己的group类。
sprite和group类被设计为可扩展,所以可以自由地创建自己的group类来做专门的事情。
最好的起点应该是这些实际项目的sprite模块python源代码。仔细查看当前的spritegroup,可以示范你创建和扩展你的新group了。
例如,这是一个rendering_Group的源代码,它为每个sprite调用render()方法,而不仅仅从自身调用一个“image”变量。
因为需要它也处理更新的区域,所以从原始的RenderUpdates_group的副本开始,这里是代码:

class RenderUpdatesDraw(RenderClear):
    """call sprite.draw(screen) to render sprites"""
    def draw(self, surface):
        dirty = self.lostsprites
        self.lostsprites = []
        for s, r in self.spritedict.items():
            newrect = s.draw(screen) #Here's the big change
            if r is 0:
                dirty.append(newrect)
            else:
                dirty.append(newrect.union(r))
            self.spritedict[s] = newrect
        return dirty

下面是关于如何从头创建sprite和group对象的更多信息。
sprite对象只“需要”两个方法。“add_internal()”和“remove_internal()”。group类调用这两个方法,从自己删除一个sprite。
add内力()和remove内力()有一个单独的参数,一个group。
sprite还需要一些方法来获取它所属的group。sprite类是否有其他方法和参数,也可用这个has()方法去测试。
如果不打算使用这些方法,那么肯定不需要测试了。
创建自己的group几乎相同要求。事实上,看一下源代码,会看到GroupSingle不是派生于group类,它只是实现了相同的方法,所以不能真正区分它们。
同样需要一个“add_internal()”和“remove_internal()”方法,当sprite们想要归属或从group中删除时,会调用这个方法。
add_internal()和remove_internal()有一个单独的参数,一个sprite。
group类惟一不同属性是有一个名为“spritegroup”的虚拟属性。只要属性存在,值是多少并不重要。
sprite类可以访问这个属性来确定“group”和任何普通python容器之间的区别。
(这很重要,因为几个sprite方法可以接受单个group参数,或者是一系列group参数。因为它们看起来都很相似,“spritegroup”是找出区别的最便捷方式。)
多观看文档中sprite模块的代码。虽然代码有点“调优”,但有足够的注释来帮助理解。
如果想做点贡献的话,在源代码中甚至还有TODO部分。

上一篇下一篇

猜你喜欢

热点阅读