深度游戏示例

本教程将带你完成编写一个简单的小行星克隆的步骤。本文假定读者熟悉编写和运行Python程序。这不是编程教程,但希望它足够清楚,即使您是初学者也可以遵循。如果你卡住了,首先看一下编程指南的相关章节。完整的源代码也可以在 examples/game/ pyglet源目录的文件夹,您可以使用该文件夹。如果还有什么不清楚的地方,请告诉我们!

基本图形

我们开始吧!我们游戏的第一个版本将简单地显示一个零分,一个显示程序名称的标签,三个随机放置的小行星,以及玩家的飞船。什么都不会动。

设置

首先要做的是,确保你安装了pyglet。然后,我们将为我们的项目设置文件夹结构。因为这个示例游戏是分阶段编写的,所以我们将有几个 version 处于不同发展阶段的文件夹。在示例文件夹之外,我们还将拥有一个包含图像的共享资源文件夹,称为“Resources”。每个 version 文件夹包含名为的Python文件 asteroid.py 来运行游戏,以及一个名为 game 我们将在其中放置其他模块;这是大部分逻辑的位置。文件夹结构应如下所示::

game/
    resources/
        (images go here)
    version1/
        asteroid.py
        game/
            __init__.py

得到一扇窗户

要设置窗口,只需 import pyglet ,创建一个新的 pyglet.window.Window ,并呼叫 pyglet.app.run() **

import pyglet
game_window = pyglet.window.Window(800, 600)

if __name__ == '__main__':
    pyglet.app.run()

如果您运行上面的代码,您应该会看到一个充满垃圾的窗口,当您按Esc键时,它就会消失。(您看到的是未初始化的原始图形内存)。

加载和显示图像

由于我们的图像将驻留在示例的根目录以外的目录中,因此我们需要告诉pyglet在哪里可以找到它们::

import pyglet
pyglet.resource.path = ['../resources']
pyglet.resource.reindex()

小矮人的 pyglet.resource 模块消除了查找和加载游戏资源(如图像、声音等)的所有繁重工作。您所需要做的就是告诉它在哪里查找,并对其重新编制索引。在此示例游戏中,资源路径以 ../ 因为资源文件夹与 version1 文件夹。如果我们把它关掉,小矮人就会往里面看 version1/ 对于 resources/ 文件夹。

既然已经初始化了pyglet的资源模块,我们就可以轻松地用 image() 资源模块的功能::

player_image = pyglet.resource.image("player.png")
bullet_image = pyglet.resource.image("bullet.png")
asteroid_image = pyglet.resource.image("asteroid.png")

使图像居中

默认情况下,Pyglet将从图像的左下角开始绘制和定位所有图像。我们不希望图像出现这种行为,因为图像需要围绕其中心旋转。要实现这一点,我们所要做的就是设定它们的锚点。让我们创建一个函数来简化这一过程::

def center_image(image):
    """Sets an image's anchor point to its center"""
    image.anchor_x = image.width // 2
    image.anchor_y = image.height // 2

现在,我们只需对所有加载的图像调用Center_Image()::

center_image(player_image)
center_image(bullet_image)
center_image(asteroid_image)

请记住,必须先定义Center_Image()函数,然后才能在模块级别调用它。此外,请注意,在pyglet中,零度直接指向右侧,因此所有图像都是正面指向右侧的。

要访问asteroid.py中的图像,我们需要使用如下内容 from game import resources ,我们将在下一节中介绍。

正在初始化对象

我们希望在窗口顶部放置一些标签,为玩家提供一些关于分数和当前难度级别的信息。最终,我们将有一个分数显示、关卡名称和一排代表剩余生命数量的图标。

制作标签

要在Piglet中创建文本标签,只需初始化 pyglet.text.Label 对象::

score_label = pyglet.text.Label(text="Score: 0", x=10, y=460)
level_label = pyglet.text.Label(text="My Amazing Game",
                            x=game_window.width//2, y=game_window.height//2, anchor_x='center')

请注意,第二个标签的居中位置使用Anchor_x属性。

绘制标签

我们希望每当绘制窗口时,pyglet都能运行一些特定的代码。一个 on_draw() 事件被调度到窗口以使其有机会重绘其内容。Piglet提供了几种将事件处理程序附加到对象的方法;一种简单的方法是使用修饰符::

@game_window.event
def on_draw():
    # draw things here

这个 @game_window.event 修饰器让窗口实例知道我们的 on_draw() 函数是一个事件处理程序。这个 on_draw() 只要窗口需要重新绘制,就会触发事件--您猜对了。其他活动包括 on_mouse_press()on_key_press()

现在,我们可以用绘制标签所需的函数填充该方法。在我们画任何东西之前,我们应该清除屏幕。之后,我们只需调用每个对象的DRAW()函数::

@game_window.event
def on_draw():
    game_window.clear()

    level_label.draw()
    score_label.draw()

现在,当您运行asteroid.py时,您应该会看到一个左上角分数为零的窗口,并且在屏幕顶部有一个居中的标签,上面写着“我的神奇游戏”。

让玩家和小行星精灵

播放器应该是的实例或子类 pyglet.sprite.Sprite ,如下所示:

from game import resources
...
player_ship = pyglet.sprite.Sprite(img=resources.player_image, x=400, y=300)

要让玩家在屏幕上绘图,请添加一行到 on_draw() **

@game_window.event
def on_draw():
    ...
    player_ship.draw()

装载小行星有点复杂,因为我们需要在不会立即与玩家相撞的随机位置放置多个小行星。让我们将加载代码放入一个名为load.py::的新游戏子模块

import pyglet
import random
from . import resources

def asteroids(num_asteroids):
    asteroids = []
    for i in range(num_asteroids):
        asteroid_x = random.randint(0, 800)
        asteroid_y = random.randint(0, 600)
        new_asteroid = pyglet.sprite.Sprite(img=resources.asteroid_image,
                                            x=asteroid_x, y=asteroid_y)
        new_asteroid.rotation = random.randint(0, 360)
        asteroids.append(new_asteroid)
    return asteroids

我们在这里所做的就是制作一些具有随机位置的新精灵。然而,还有一个问题--一颗小行星可能会被随机放置在玩家所在的位置,导致立即死亡。要解决这个问题,我们需要能够告诉玩家新的小行星离玩家有多远。下面是一个计算距离简单函数:

import math
...
def distance(point_1=(0, 0), point_2=(0, 0)):
    """Returns the distance between two points"""
    return math.sqrt((point_1[0] - point_2[0]) ** 2 + (point_1[1] - point_2[1]) ** 2)

为了对照玩家的位置检查新的小行星,我们需要将玩家的位置传递到 asteroids() 函数并保持重新生成新的坐标,直到小行星足够远。pyglet精灵将它们的位置作为元组(Sprite.position)以及x、y和z属性(Sprite.x、Sprite.y、Sprite.z)进行跟踪。为了保持代码简短,我们只将位置元组传递给函数。我们没有使用z值,因此我们只使用了一个掷出变量::

def asteroids(num_asteroids, player_position):
    asteroids = []
    for i in range(num_asteroids):
        asteroid_x, asteroid_y, _ = player_position
        while distance((asteroid_x, asteroid_y), player_position) < 100:
            asteroid_x = random.randint(0, 800)
            asteroid_y = random.randint(0, 600)
        new_asteroid = pyglet.sprite.Sprite(
            img=resources.asteroid_image, x=asteroid_x, y=asteroid_y)
        new_asteroid.rotation = random.randint(0, 360)
        asteroids.append(new_asteroid)
    return asteroids

对于每颗小行星,它会随机选择位置,直到找到远离玩家的位置,创建精灵,并给它一个随机的旋转。每颗小行星都被附加到一个列表中,该列表将被返回。

现在您可以像这样加载三颗小行星::

from game import resources, load
...
asteroids = load.asteroids(3, player_ship.position)

小行星变量现在包含一个精灵列表。在屏幕上绘制它们就像对玩家的船一样简单-只需调用他们的 draw() 方法:

@game_window.event
def on_draw():
    ...
    for asteroid in asteroids:
        asteroid.draw()

这是第一节的总结。您的“游戏”还没有做很多事情,但我们将在接下来的部分中谈到这一点。您可能想要查看一下 examples/game/version1 文件夹中查看我们所做的工作,并找到一个功能正常的副本。

基本动作

在示例的第二个版本中,我们将介绍一种更简单、更快的方法来绘制所有游戏对象,以及添加指示剩余生命数量的图标行。我们还将编写一些代码,使玩家和小行星遵守物理定律。

使用批次绘制

调用每个对象的 draw() 如果有许多不同类型的对象,手动方法可能会变得繁琐乏味。如果您需要绘制大量对象,则效率也会非常低。小矮人 pyglet.graphics.Batch 类允许您通过一个函数调用来绘制所有对象,从而简化了绘制。您所需要做的就是创建一个批处理,将其传递给每个要绘制的对象,然后调用该批处理的 draw() 方法。

要创建新批处理,只需创建 pyglet.graphics.Batch **

main_batch = pyglet.graphics.Batch()

要使对象成为批处理的成员,只需将批处理作为批处理关键字参数传递到其构造函数中::

score_label = pyglet.text.Label(text="Score: 0", x=10, y=575, batch=main_batch)

将Batch关键字参数添加到在asteroid.py中创建的每个图形对象。

要将批处理与小行星精灵一起使用,我们需要将批处理传递到 game.load.asteroid() 函数,然后只需将其作为关键字参数添加到每个新的Sprite。更新函数::

def asteroids(num_asteroids, player_position, batch=None):
    ...
    new_asteroid = pyglet.sprite.Sprite(img=resources.asteroid_image,
                                        x=asteroid_x, y=asteroid_y,
                                        batch=batch)

并更新其名称为::的地方

asteroids = load.asteroids(3, player_ship.position, main_batch)

现在您可以替换这五行 draw() 只有一个::的呼叫

main_batch.draw()

现在,当您运行asteroid.py时,它看起来应该完全一样。

显示小船图标

为了显示玩家还活着多少人,我们需要在屏幕的右上角画一小排图标。由于我们将使用相同的模板创建多个模板,因此让我们创建一个名为 player_lives()load 模块来生成它们。图标看起来应该和玩家的飞船一样。我们可以使用图像编辑器创建一个缩放版本,或者我们可以只让pyglet来进行缩放。我不知道你是怎么想的,但我更喜欢工作量小的那个。

创建图标的功能与创建小行星的功能几乎完全相同。对于每个图标,我们只需创建一个精灵,为其指定位置和比例,并将其附加到返回列表::

def player_lives(num_icons, batch=None):
    player_lives = []
    for i in range(num_icons):
        new_sprite = pyglet.sprite.Sprite(img=resources.player_image,
                                          x=785-i*30, y=585, batch=batch)
        new_sprite.scale = 0.5
        player_lives.append(new_sprite)
    return player_lives

播放器图标是50x50像素,所以一半大小将是25x25。我们想要在每个图标之间留出一点空间,所以我们以30像素的间隔创建它们,从屏幕的右侧开始,向左移动。请注意,就像 asteroids() 函数, player_lives() 花了一个 batch 争论。

让东西动起来

如果屏幕上没有任何东西移动,这个游戏将会非常无聊。要实现运动,我们需要编写一组自己的类来处理逐帧运动计算。我们还需要编写一个Player类来响应键盘输入。

Creating the basic motion class

由于每个可见对象至少由一个Sprite表示,因此我们不妨将基本运动类设置为pyglet.sprite.Sprite的子类。另一种方法是让我们的类具有Sprite属性。

创建一个新的游戏子模块,名为Physicalobject.py,并声明一个PhysicalObject类。我们将添加的唯一新属性将存储对象的速度,因此构造函数将非常简单::

class PhysicalObject(pyglet.sprite.Sprite):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.velocity_x, self.velocity_y = 0.0, 0.0

每个对象都需要在每一帧中更新,所以让我们编写一个 update() 方法:

def update(self, dt):
    self.x += self.velocity_x * dt
    self.y += self.velocity_y * dt

什么是DT?这是“增量时间”,或“时间步长”。游戏画面不是即时的,它们绘制的时间也不总是相等的。如果你曾经尝试过在一台旧机器上玩一款现代游戏,你就知道帧速率可能会到处跳跃。有许多方法可以处理这个问题,最简单的方法是将所有对时间敏感的操作乘以dt。稍后我将向您展示如何计算此值。

如果我们给物体一个速度,然后让它们离开,它们很快就会飞离屏幕。由于我们正在制作小行星的克隆,我们宁愿它们只是包裹在屏幕上。以下是实现该目标的一个简单函数:

def check_bounds(self):
    min_x = -self.image.width / 2
    min_y = -self.image.height / 2
    max_x = 800 + self.image.width / 2
    max_y = 600 + self.image.height / 2
    if self.x < min_x:
        self.x = max_x
    elif self.x > max_x:
        self.x = min_x
    if self.y < min_y:
        self.y = max_y
    elif self.y > max_y:
        self.y = min_y

正如您所看到的,它只是检查对象是否在屏幕上不再可见,如果是,它会将它们移动到屏幕的另一边。若要使每个PhysicalObject都使用此行为,请向 self.check_bounds() 在…的末尾 update()

要让小行星使用我们的新运动代码,只需导入PhysicalObject模块并更改 new_asteroid = ... 创建新的 PhysicalObject 而不是一个 Sprite 。你还需要给它们一个随机的初始速度。以下是新的、经过改进的 load.asteroids() 功能:

def asteroids(num_asteroids, player_position, batch=None):
    ...
    new_asteroid = physicalobject.PhysicalObject(...)
    new_asteroid.rotation = random.randint(0, 360)
    new_asteroid.velocity_x = random.random()*40
    new_asteroid.velocity_y = random.random()*40
    ...

编写游戏更新函数

调用每个对象的 update() 方法,我们首先需要有这些对象的列表。目前,我们只需在设置所有其他对象后声明它:

game_objects = [player_ship] + asteroids

现在,我们可以编写一个简单的函数来迭代列表::

def update(dt):
    for obj in game_objects:
        obj.update(dt)

这个 update() 函数需要一个 dt 参数,因为它仍然不是实际时间点的源。

调用更新()函数

我们需要在每帧中至少更新一次对象。什么是相框?大多数屏幕的最大刷新率都是60赫兹。然而,如果我们将循环设置为恰好以60赫兹运行,运动将看起来有点抖动,因为它与屏幕不完全匹配。相反,我们可以让它以两倍的速度更新,每秒120次,以获得流畅的动画。

每秒调用函数120次的最好方法是让Piglet来做这件事。这个 pyglet.clock 模块包含许多定期或在将来某个指定时间调用函数的方法。我们想要的是 pyglet.clock.schedule_interval() **

pyglet.clock.schedule_interval(update, 1/120.0)

将这条线放在上面 pyglet.app.run() in the if _ _NAME__==‘__Main__’`块通知pyglet调用 `update() 每秒120次。小矮人将在经过的时间内通过,即 dt ,作为唯一的参数。

现在,当您运行asteroid.py时,您应该会看到以前静止的小行星在屏幕上平静地漂移,当它们滑出边缘时,它们会重新出现在屏幕的另一边。

编写Player类

除了遵守基本物理定律外,玩家对象还需要对键盘输入做出响应。首先创建一个 game.player 模块、导入适当的模块和子类化 PhysicalObject **

from . import physicalobject, resources


class Player(physicalobject.PhysicalObject):

    def __init__(self, *args, **kwargs):
        super().__init__(img=resources.player_image, *args, **kwargs)

到目前为止,玩家和PhysicalObject之间的唯一区别是玩家总是拥有相同的图像。但是玩家对象需要更多的属性。由于船总是以相同的力向它指向的方向推进,所以我们需要定义一个常量来表示力的大小。我们还应该为船的转速定义一个常量:

self.thrust = 300.0
self.rotate_speed = 200.0

现在,我们需要让类响应用户输入。Pyglet使用基于事件的方法进行输入,将按键和释放键事件发送到注册的事件处理程序。但在本例中,我们希望使用轮询方法,定期检查某个键是否按下。要做到这一点,一种方法是维护一本钥匙词典。首先,我们需要在构造函数中初始化字典::

self.keys = dict(left=False, right=False, up=False)

然后我们需要编写两个方法, on_key_press()on_key_release() 。当pyglet检查新的事件处理程序时,它查找这两个方法以及其他方法::

import math
from pyglet.window import key
from . import physicalobject, resources

class Player(physicalobject.PhysicalObject)

    def on_key_press(self, symbol, modifiers):
        if symbol == key.UP:
            self.keys['up'] = True
        elif symbol == key.LEFT:
            self.keys['left'] = True
        elif symbol == key.RIGHT:
            self.keys['right'] = True

    def on_key_release(self, symbol, modifiers):
        if symbol == key.UP:
            self.keys['up'] = False
        elif symbol == key.LEFT:
            self.keys['left'] = False
        elif symbol == key.RIGHT:
            self.keys['right'] = False

这看起来相当笨拙。有一种更好的方法可以做到这一点,我们将在后面看到,但现在,这个版本是一个很好的演示pyglet的事件系统。

我们需要做的最后一件事是编写 update() 方法。它遵循与PhysicalObject相同的行为,外加一点额外的功能,因此我们需要调用PhysicalObject的 update() 方法,然后响应输入::

def update(self, dt):
    super(Player, self).update(dt)

    if self.keys['left']:
        self.rotation -= self.rotate_speed * dt
    if self.keys['right']:
        self.rotation += self.rotate_speed * dt

到目前为止还很简单。要旋转播放器,我们只需将旋转速度与角度相加,再乘以dt即可计算时间。请注意,精灵对象的旋转属性以度为单位,顺时针方向为正方向。这意味着您需要调用 math.degrees()math.radians() 每当将Python的内置数学函数与Sprite类一起使用时,将结果设为负值,因为这些函数使用弧度而不是度数,并且它们的正方向是逆时针方向。使船向前推进的代码使用了这样一个转换的例子:

if self.keys['up']:
    angle_radians = -math.radians(self.rotation)
    force_x = math.cos(angle_radians) * self.thrust * dt
    force_y = math.sin(angle_radians) * self.thrust * dt
    self.velocity_x += force_x
    self.velocity_y += force_y

首先,我们将角度转换为弧度,以便 math.cos()math.sin() 将获得正确的值。然后,我们应用一些简单的物理方法来修改船的X和Y速度分量,并将船推向正确的方向。

我们现在有了一个完整的玩家类。如果我们将它添加到游戏中,并告诉Piglet它是一个事件处理程序,我们应该就可以开始了。

集成Player类

我们需要做的第一件事是使PlayerShip成为Player::的实例

from game import player
...
player_ship = player.Player(x=400, y=300, batch=main_batch)

现在我们需要告诉Piglet,PlayerShip是一个事件处理程序。为此,我们需要使用以下命令将其推送到事件堆栈 game_window.push_handlers() **

game_window.push_handlers(player_ship)

就这样!现在您应该能够运行游戏并使用箭头键移动玩家了。

给玩家一些事情去做

在任何一场好的比赛中,都需要有对球员不利的东西。就小行星而言,这是与小行星相撞的威胁。碰撞检测需要代码中的大量基础设施,因此本节将重点介绍如何使其工作。我们还将清理Player类,并显示一些推力的视觉反馈。

简化玩家输入

目前,Player类处理它自己的所有键盘事件。它花费了13行代码,除了在字典中设置布尔值之外,什么都不做。人们会认为会有更好的方法,而且确实有: pyglet.window.key.KeyStateHandler 。这个方便的类自动执行我们手动执行的操作:它跟踪键盘上每个键的状态。

要开始使用它,我们需要对其进行初始化,并将其推送到事件堆栈中,而不是Player类中。首先,让我们将其添加到Player的构造函数::

self.key_handler = key.KeyStateHandler()

我们还需要将KEY_HANDLER对象推送到事件堆栈。除了Player_Ship对象的按键处理程序之外,还要继续按下Player_Ship对象,因为稍后我们将需要它继续处理按键和释放事件::

game_window.push_handlers(player_ship.key_handler)

由于Player现在依赖KEY_HANDLER来读取键盘,因此我们需要更改 update() 方法来使用它。唯一的变化是IF条件::

if self.key_handler[key.LEFT]:
    ...
if self.key_handler[key.RIGHT]:
    ...
if self.key_handler[key.UP]:
    ...

现在我们可以删除 on_key_press()on_key_release() 类中的方法。事情就是这么简单。如果需要查看关键常量列表,可以查看下面的API文档 pyglet.window.key

添加引擎火焰

如果没有视觉反馈,就很难判断这艘船是否真的在向前推进,特别是对于一个只是看着别人玩游戏的观察者来说。提供视觉反馈的一种方式是,当球员在推进时,在球员身后显示引擎火焰。

加载火焰图像

玩家现在将由两个精灵组成。没有什么可以阻止我们让一个Sprite拥有另一个Sprite,所以我们只给玩家一个Engine_Sprite属性,并每一帧更新它。就我们的目的而言,这种方法将是最简单和最具伸缩性的。

为了让火焰画在正确的位置,我们可以在每一帧中做一些复杂的数学运算,或者我们可以只移动图像的锚点。首先,将图像加载到resource ces.py::

engine_image = pyglet.resource.image("engine_flame.png")

为了让火焰画在玩家身后,我们需要将火焰图像的旋转中心向右移动,越过图像的末端。要做到这一点,我们只需将其 anchor_xanchor_y 属性::

engine_image.anchor_x = engine_image.width * 1.5
engine_image.anchor_y = engine_image.height / 2

现在,该图像已准备好供Player类使用。如果仍对锚点感到困惑,请在完成本部分后尝试使用ENGINE_IMAGE的锚点值。

创造和绘制火焰

除了需要不同的图像并且最初必须不可见之外,Engine Sprite需要使用与Player相同的所有参数进行初始化。创建它的代码属于 Player.__init__() 非常直截了当:

self.engine_sprite = pyglet.sprite.Sprite(img=resources.engine_image, *args, **kwargs)
self.engine_sprite.visible = False

要使引擎子画面仅在选手冲刺时出现,我们需要在IF中添加一些逻辑 self.key_handler[key.UP] 块中的 update() 方法:

if self.key_handler[key.UP]:
    ...
    self.engine_sprite.visible = True
else:
    self.engine_sprite.visible = False

要使精灵显示在玩家的位置,我们还需要更新其位置和旋转属性::

if self.key_handler[key.UP]:
    ...
    self.engine_sprite.rotation = self.rotation
    self.engine_sprite.x = self.x
    self.engine_sprite.y = self.y
    self.engine_sprite.visible = True
else:
    self.engine_sprite.visible = False

死后清理

当玩家不可避免地被小行星砸成碎片时,他将从屏幕上消失。然而,仅仅从GAME_OBJECTS列表中删除播放器实例并不足以将其从图形批处理中删除。要做到这一点,我们需要调用其 delete() 方法。通常一杯雪碧是自己的 delete() 方法无需修改即可正常工作,但我们的子类有其自己的子Sprite(引擎火焰),在删除播放器实例时也必须将其删除。为了让两者都优雅地死去,我们必须编写一个简单但略有增强的 delete() 方法:

def delete(self):
    self.engine_sprite.delete()
    super(Player, self).delete()

Player类现在已经清理完毕,可以开始使用了。

正在检查冲突

要使对象从屏幕上消失,我们需要操作游戏对象列表。每个对象都需要对照自己的位置检查其他对象的位置,并且每个对象都必须决定是否应该将其从列表中删除。然后,游戏循环将检查死对象并将其从列表中删除。

正在检查所有对象对

我们需要将每一件物品与其他物品对照检查。最简单的方法是使用嵌套循环。这种方法对于大量对象来说效率不高,但对我们的目的是有效的。我们可以使用一个简单的优化,避免两次检查同一对对象。以下是循环的设置,它属于 update() 。它只需迭代所有对象对,而无需执行任何操作::

for i in range(len(game_objects)):
    for j in range(i+1, len(game_objects)):
        obj_1 = game_objects[i]
        obj_2 = game_objects[j]

我们需要一种方法来检查一个物体是否已经被杀死。我们现在可以转到PhysicalObject并将其放入,但让我们继续研究游戏循环,并在稍后实现该方法。目前,我们只假设GAME_OBJECTS中的所有内容都有一个Dead属性,在类将其设置为True之前,该属性将为False,此时它将被忽略并最终从列表中删除。

要执行实际的检查,我们还需要调用另外两个尚不存在的方法。一种方法将确定两个对象是否实际发生碰撞,另一种方法将为每个对象提供响应碰撞的机会。检查代码本身很容易理解,因此我不会进一步解释:

if not obj_1.dead and not obj_2.dead:
    if obj_1.collides_with(obj_2):
        obj_1.handle_collision_with(obj_2)
        obj_2.handle_collision_with(obj_1)

现在,我们要做的就是浏览列表并删除失效对象::

for to_remove in [obj for obj in game_objects if obj.dead]:
    to_remove.delete()
    game_objects.remove(to_remove)

如您所见,它只是调用对象的 delete() 方法将其从任何批处理中移除,则它会将其从列表中移除。如果您不经常使用列表理解,上面的代码可能看起来像是在遍历列表时从列表中删除对象。幸运的是,列表理解是在循环实际运行之前评估的,所以应该不会有问题。

实现碰撞功能

我们需要向PhysicalObject类添加三项内容:Dead属性、 collides_with() 方法,而 handle_collision_with() 方法。这个 collides_with() 方法将需要使用 distance() 函数,因此我们首先将该函数移到它自己的游戏子模块中,名为util.py::

import pyglet, math

def distance(point_1=(0, 0), point_2=(0, 0)):
    return math.sqrt(
        (point_1[0] - point_2[0]) ** 2 +
        (point_1[1] - point_2[1]) ** 2)

请记住从load.py中的util导入距离进行调用。现在我们可以写了 PhysicalObject.collides_with() 不复制代码::

def collides_with(self, other_object):
    collision_distance = self.image.width/2 + other_object.image.width/2
    actual_distance = util.distance(self.position, other_object.position)

    return (actual_distance <= collision_distance)

冲突处理函数甚至更简单,因为现在我们只希望每个对象在接触到另一个对象时立即消亡::

def handle_collision_with(self, other_object):
    self.dead = True

最后一件事:在PhysicalObject.__init__()中设置self.dead=FALSE。

就是这样!你应该能够在屏幕上快速移动,引擎一直在燃烧。如果你撞到了什么东西,你和你撞到的东西都应该从屏幕上消失。仍然没有比赛,但我们显然正在取得进展。

碰撞响应

在本节中,我们将添加项目符号。这一新特性将要求我们在游戏期间开始向GAME_OBJECTS列表添加东西,并让对象检查彼此的类型,以决定它们是否应该死亡。

在游戏中添加对象

How?

我们使用布尔标志处理对象移除。添加对象会稍微复杂一些。首先,一个对象不能只说“把我加到列表中!”它一定来自某个地方。另一方面,一个对象可能希望一次添加多个其他对象。

有几种方法可以解决这个问题。为了避免循环引用,保持构造函数的简洁,并避免添加额外的模块,我们将让每个对象保存一个要添加到GAME_OBJECTS的新子对象的列表。这种方法将使游戏中的任何对象都很容易产生更多的对象。

调整游戏循环

要检查子对象的对象并将这些子对象添加到列表中,最简单的方法是向GAME_OBJECTS循环添加两行代码。我们还没有实现new_Objects属性,但当我们实现时,它将是要添加的对象列表::

for obj in game_objects:
    obj.update(dt)
    game_objects.extend(obj.new_objects)
    obj.new_objects = []

不幸的是,这个简单的解决方案有问题。在迭代列表时修改它通常不是一个好主意。修复方法是简单地将新对象添加到单独的列表中,然后在完成迭代之后将单独列表中的对象添加到GAME_OBJECTS。

在循环的正上方声明一个TO_ADD列表,并向其中添加新对象。在...的最底层 update() ,在对象删除代码之后,将To_Add中的对象添加到GAME_OBJECTS::

...collision...

to_add = []

for obj in game_objects:
    obj.update(dt)
    to_add.extend(obj.new_objects)
    obj.new_objects = []

...removal...

game_objects.extend(to_add)

将属性放入PhysicalObject中

如前所述,我们所要做的就是在PhysicalObject类::中声明一个新的_对象属性

def __init__(self, *args, **kwargs):
    ....
    self.new_objects = []

要添加一个新对象,我们所要做的就是将一些内容放入new_Objects中,主循环将看到它,将其添加到GAME_OBJECTS列表中,然后清除new_Objects。

添加项目符号

Writing the bullet class

在很大程度上,子弹的作用与任何其他PhysicalObject一样,但它们有两个不同之处,至少在这款游戏中是这样:它们只与一些物体碰撞,几秒钟后就从屏幕上消失,以防止玩家在屏幕上充斥子弹。

首先,创建一个名为Bullet.py的新游戏子模块,并启动PhysicalObject::

import pyglet
from . import physicalobject, resources

class Bullet(physicalobject.PhysicalObject):
    """Bullets fired by the player"""

    def __init__(self, *args, **kwargs):
        super(Bullet, self).__init__(
            resources.bullet_image, *args, **kwargs)

为了让子弹在一段时间后消失,我们可以跟踪我们自己的年龄和寿命属性,或者我们可以让pyglet为我们做所有的工作。我不知道你怎么想,但我更喜欢第二种选择。首先,我们需要编写一个在子弹寿命结束时调用的函数::

def die(self, dt):
    self.dead = True

现在我们需要告诉Piglet在半秒左右的时间内呼叫它。一初始化对象,我们就可以通过将调用添加到 pyglet.clock.schedule_once() 到构造函数::

def __init__(self, *args, **kwargs):
    super(Bullet, self).__init__(resources.bullet_image, *args, **kwargs)
    pyglet.clock.schedule_once(self.die, 0.5)

关于Bullet类还有更多的工作要做,但在我们对类本身做更多工作之前,让我们将它们显示在屏幕上。

开枪射击

Player类将是唯一发射子弹的类,因此让我们打开它,导入Bullet模块,并向其构造函数添加Bullet_SPEED属性::

...
from . import bullet

class Player(physicalobject.PhysicalObject):
    def __init__(self, *args, **kwargs):
        super(Player, self).__init__(img=resources.player_image, *args, **kwargs)
        ...
        self.bullet_speed = 700.0

现在我们可以编写代码来创建新的子弹,并将其发射到太空中。首先,我们需要重新启动on_key_press()事件处理程序::

def on_key_press(self, symbol, modifiers):
    if symbol == key.SPACE:
        self.fire()

这个 fire() 方法本身会稍微复杂一些。大多数计算将与推力的计算非常相似,但会有一些不同。我们需要把子弹从船的前端取出,而不是在它的中心。我们还需要将飞船的现有速度与子弹的新速度相加,否则如果玩家速度足够快,子弹最终会比飞船慢。

像往常一样,转换为弧度并反转方向:

def fire(self):
    angle_radians = -math.radians(self.rotation)

接下来,计算子弹的位置并实例化它::

ship_radius = self.image.width/2
bullet_x = self.x + math.cos(angle_radians) * ship_radius
bullet_y = self.y + math.sin(angle_radians) * ship_radius
new_bullet = bullet.Bullet(bullet_x, bullet_y, batch=self.batch)

使用几乎相同的公式设置其速度:

bullet_vx = (
    self.velocity_x +
    math.cos(angle_radians) * self.bullet_speed
)
bullet_vy = (
    self.velocity_y +
    math.sin(angle_radians) * self.bullet_speed
)
new_bullet.velocity_x = bullet_vx
new_bullet.velocity_y = bullet_vy

最后,将其添加到new_Objects列表中,以便主循环获取它并将其添加到GAME_OBJECTS::

self.new_objects.append(new_bullet)

在这一点上,你应该能够发射子弹从你的船的前面。只有一个问题:一旦你开火,你的船就会消失。你可能已经注意到,小行星在彼此接触时也会消失。要解决这个问题,我们需要开始定制每个类的 handle_collision_with() 方法。

自定义碰撞行为

当前版本的游戏中有五种碰撞:子弹-小行星、子弹-玩家、小行星-玩家、子弹-子弹和小行星-小行星。在一个更复杂的游戏中,会有更多的人。

一般而言,相同类型的对象在发生冲突时不应被销毁,因此我们可以在PhysicalObject中推广该行为。其他交互将需要更多的工作。

Letting twins ignore each other

要让两颗小行星或两颗子弹从彼此身边擦肩而过,而没有表示感谢(或发生剧烈爆炸),我们只需在PhysicalObject.HandleCollision_with()方法中检查它们的类是否相等::

def handle_collision_with(self, other_object):
    if other_object.__class__ == self.__class__:
        self.dead = False
    else:
        self.dead = True

还有一些其他更优雅的方法来检查Python中的对象相等性,但上面的代码可以完成这项工作。

自定义项目符号碰撞

由于不同对象之间的子弹碰撞行为可能会有很大差异,因此让我们向PhysicalObject添加一个REACTS_TO_Bullets属性,Bullet类可以检查该属性以确定它是否应该注册碰撞。我们还应该添加IS_Bullet属性,以便可以从两个对象正确检查碰撞。

(这些不是“好的”设计决定,但它们会奏效。)

首先,在PhysicalObject构造函数中将REACTS_TO_Bullets属性初始化为True::

class PhysicalObject(pyglet.sprite.Sprite):
    def __init__(self, *args, **kwargs):
        ...
        self.reacts_to_bullets = True
        self.is_bullet = False
        ...

class Bullet(physicalobject.PhysicalObject):
    def __init__(self, *args, **kwargs):
        ...
        self.is_bullet = True

然后,将一些代码插入到 PhysicalObject.collides_with() 要在适当的情况下忽略项目符号:

def collides_with(self, other_object):
    if not self.reacts_to_bullets and other_object.is_bullet:
        return False
    if self.is_bullet and not other_object.reacts_to_bullets:
        return False
    ...

最后,在Player.__init__()中设置self.react_to_Bullets=FALSE。这个 Bullet 下课了!现在,让我们让子弹击中小行星时发生一些事情。

让小行星爆炸

小行星对玩家来说是一个挑战,因为每次你射击一颗小行星,它就会变成更多的小行星。如果我们想让我们的游戏变得有趣,我们需要模仿这种行为。我们已经完成了大部分困难的部分。剩下的工作就是创建另一个PhysicalObject子类并编写一个定制 handle_collision_with() 方法,以及几个维护调整。

编写小行星类

创建一个名为asteroid.py的新的游戏子模块。编写通常的构造函数来将特定图像传递给超类,并传递任何其他参数::

import pyglet
from . import resources, physicalobject

class Asteroid(physicalobject.PhysicalObject):
    def __init__(self, *args, **kwargs):
        super(Asteroid, self).__init__(resources.asteroid_image, *args, **kwargs)

现在我们需要写一个新的 handle_collision_with() 方法。它应该会产生随机数量的新的、更小的、具有随机速度的小行星。然而,只有在它足够大的情况下,它才应该这样做。一颗小行星最多应该分裂两次,如果我们每次将其缩小一半,那么当一颗小行星的大小是新小行星的四分之一时,它应该停止分裂。

我们希望保留忽略其他小行星的旧行为,因此以调用超类的方法开始该方法::

def handle_collision_with(self, other_object):
    super(Asteroid, self).handle_collision_with(other_object)

现在我们可以说,如果它应该消亡,而且它足够大,那么我们应该创造两到三颗新的小行星,它们的自转和速度是随机的。我们应该把旧的小行星的速度加到新的小行星上,让它们看起来像是来自同一个物体::

import random

class Asteroid:
    def handle_collision_with(self, other_object):
        super(Asteroid, self).handle_collision_with(other_object)
        if self.dead and self.scale > 0.25:
            num_asteroids = random.randint(2, 3)
            for i in range(num_asteroids):
                new_asteroid = Asteroid(x=self.x, y=self.y, batch=self.batch)
                new_asteroid.rotation = random.randint(0, 360)
                new_asteroid.velocity_x = (random.random() * 70 + self.velocity_x)
                new_asteroid.velocity_y = (random.random() * 70 + self.velocity_y)
                new_asteroid.scale = self.scale * 0.5
                self.new_objects.append(new_asteroid)

既然我们在这里,让我们通过让小行星旋转一点来给它们增加一点图形效果。要做到这一点,我们将添加一个ROTATE_SPEED属性并为其赋予一个随机值。然后我们将编写一个 update() 方法将该旋转应用于每一帧。

在构造函数中添加属性::

def __init__(self, *args, **kwargs):
    super(Asteroid, self).__init__(resources.asteroid_image, *args, **kwargs)
    self.rotate_speed = random.random() * 100.0 - 50.0

然后编写更新()方法::

def update(self, dt):
    super(Asteroid, self).update(dt)
    self.rotation += self.rotate_speed * dt

我们需要做的最后一件事是转到load.py,并让Asterid()方法创建一个新的Asterid,而不是PhysicalObject::

from . import asteroid

def asteroids(num_asteroids, player_position, batch=None):
    ...
    for i in range(num_asteroids):
        ...
        new_asteroid = asteroid.Asteroid(x=asteroid_x, y=asteroid_y, batch=batch)
        ...
    return asteroids

现在我们看到的是一个类似于游戏的东西。这很简单,但所有的基本知识都在那里。

下一步

因此,我不会带您完成标准的重构过程,而是将其作为练习,让您完成以下操作:

* Make the Score counter mean something
* Let the player restart the level if they die
* Implement lives and a “Game Over” screen
* Add particle effects

祝好运!只要稍加努力,你就应该能够自己弄明白这些事情中的大多数。如果你有困难,请加入我们的pyglet邮寄名单。

此外,除了这个示例游戏之外,还有 another 小行星克隆人可在 /examples/astraea/ Pyglet源目录中的文件夹。与我们刚刚完成的这个示例游戏练习相比,Astraea是一个完整的游戏,具有适当的菜单、评分系统和额外的图形效果。Astraea没有循序渐进的文档,但代码本身应该很容易理解,并说明了一些很好的技术。