Python、PyGame 和树莓派游戏开发教程(四)

原文:Python, PyGame and Raspberry Pi Game Development

协议:CC BY-NC-SA 4.0

十九、有限状态机

状态可以被描述为程序或实体的条件。有限定义了程序或实体只能由一定数量的状态来定义。实体由一系列规则控制,这些规则决定程序或实体的下一个状态是什么。

有限状态机被用在视频游戏中,用于人工智能(AI)以及菜单系统和整体游戏状态。

游戏状态

游戏是一种计算机程序,具有独特的、离散的、分隔的状态,例如,闪屏、玩游戏、游戏结束、主菜单和选项菜单。每个部分都可以被视为一个独立的状态。

菜单系统

用于控制游戏各个方面的菜单系统也可以划分成不同的状态,例如,主菜单、显示选项、控制选项和声音选项。这些都是独立的州。

非玩家人工智能

这是有限状态机(FSM)最常见的用法,也是大多数人将它与 FSM 联系在一起的原因。基本上,玩家遇到的每个敌人都有一个有限状态机。所谓附加,我的意思是它引用了一个成员变量形式的有限状态机,例如“self.fsm”。

敌人的 FSM 可以彼此独立运行,或者可以有一个支配性的“群体 AI”来控制整个系列的敌人。例如,你可能有十个敌人,但是“背包 AI”会控制有多少敌人被用来攻击玩家,有多少敌人会“逃跑”,等等。

在具体情况下,我们举一个警卫的例子。他可能有两种状态:巡逻和攻击。守卫停留在巡逻状态,直到一个敌人(玩家)进入范围,比如说 50 个单位,然后他们进入攻击状态。

FSM 通常用图表来描述。每个方块代表状态,每个箭头显示规则和转换方向。也就是说,如果符合该规则,箭头将指向实体应该使用的状态。见图 19-1 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-1。

有限状态机显示了一个简单的双态巡逻/攻击敌人的人工智能

如果守卫处于巡逻状态,玩家进入近战范围,守卫会移动到攻击状态。这无疑会包含攻击玩家的代码。同样,如果守卫处于攻击状态,而玩家移动到近战范围之外,它将转换回巡逻状态。

有限状态机示例

本例显示了一个三态 FSM。每个状态都有以下方法:

  • 输入()

  • 退出()

  • 更新()

有一个 FSM 管理器控制程序的当前状态。这个管理器有两种方法:

  • changeState()

  • 更新()

changeState()方法将实体从一种状态转换到另一种状态,update()方法调用当前状态的 update()方法。

在下一节中,我们将创建一个示例有限状态机(FSM)。在“pygamebook”中创建一个名为“ch19”的新文件夹在“ch19”文件夹中,创建一个名为“fsm.py”的新 Python 文件。完成后,您将看到以下输出:

Entering State One
Hello from StateOne!
Hello from StateOne!
Hello from StateOne!
Hello from StateOne!
Hello from StateOne!
Exiting State One
Entering State Two
Hello from StateTwo!
Hello from StateTwo!
Hello from StateTwo!
Hello from StateTwo!
Hello from StateTwo!
Exiting State Two
Entering Quit
Quitting...

如果没有,请重新检查您的代码。

有限状态机管理器

有限机器管理器类定义如下。记得输入它(和剩下的代码!)明确。您可以在以后更改任何您想要的内容,但是首先要完全按照看到的内容键入代码。

FSM 管理器控制实体的当前状态。在我们的例子中,我们有三个状态。前两种状态显示“hello”消息,后者退出应用。转换规则如下图 19-2 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-2。

显示转换的 FSM 示例状态机

当计数达到零时,状态一转换到状态二。当计数达到零时,状态二转换到状态退出。StateQuit 调用 Python 的 exit()方法退出应用。

class FsmManager:
    def __init__(self):
        self.currentState = None

当前状态设置为“无”。我们将在下面程序的主要部分中显式调用 changeState()方法。

    def update(self):
        if (self.currentState != None):
            self.currentState.update()

update()方法检查我们是否有当前状态,如果有,我们调用它的 update()方法。注意,我们在这里使用 Python 的 duck 类型。

    def changeState(self, newState):
        if (self.currentState != None):
            self.currentState.exit()

当我们改变状态时,我们希望在转换到新状态之前给当前状态一个“关闭”或“清理”的机会。exit()方法就是这样做的,或者至少由实现状态的开发人员将他们想要的代码放入 exit()方法中。

        self.currentState = newState
        self.currentState.enter()

同样,当我们进入一个新的状态时,我们需要让状态知道这个事件已经发生。每个州的开发人员如果想要对该事件采取行动,将在 enter()方法中放置代码。

class StateOne:

一般来说,除了屏幕上显示的文本信息,状态一和状态二之间几乎没有区别。

class StateOne:
    def __init__(self, fsm):
        self.count = 5
        self.fsm = fsm
        self.nextState = None

我们将在程序的主要部分设置 nextState 字段。这是当前状态将转换到的下一个状态。还有更复杂的 FSM 系统,它们将规则应用于各种状态,从而形成一个更加灵活的系统。这是一个简单的例子,在每个状态中烘焙规则。

    def enter(self):
        print("Entering State One")

enter()方法用于为当前状态设置各种值。在这个例子中,我们只是在屏幕上写一条消息。

    def exit(self):
        print("Exiting State One")

exit()方法可以用来在当前状态转换到新状态之前清理它。在本例中,我们展示了一条简单的消息。

    def update(self):
        print("Hello from StateOne!")
        self.count -= 1
        if (self.count == 0):
            fsm.changeState(self.nextState)

FSM 管理器调用 update()方法。在我们的例子中,我们一直倒数到零,然后转换到下一个状态。

class StateTwo:
    def __init__(self, fsm):
        self.count = 5
        self.fsm = fsm
        self.nextState = None

    def enter(self):
        print("Entering State Two")

    def exit(self):
        print("Exiting State Two")

    def update(self):
        print("Hello from StateTwo!")
        self.count -= 1
        if (self.count == 0):
            fsm.changeState(self.nextState)

状态一和状态二没有太大区别。退出状态也很简单;它只是退出应用。

class StateQuit:
    def __init__(self, fsm):
        self.fsm = fsm

    def enter(self):
        print("Entering Quit")

    def exit(self):
        print("Exiting Quit")

    def update(self):
        print("Quitting...")
        exit()

我们不需要更新任何变量;我们只是在这个时候退出应用。

fsm = FsmManager()
stateOne = StateOne(fsm)
stateTwo = StateTwo(fsm)
stateQuit = StateQuit(fsm)

在这里,我们创建我们的 FSM 管理器和状态。每个状态都将 FSM 管理器作为构造函数中的一个参数。

stateOne.nextState = stateTwo
stateTwo.nextState = stateQuit

分配状态一和状态二的下一个状态。状态一的下一个状态是状态二,状态二的下一个状态是状态退出。

fsm.changeState(stateOne)

我们将 FSM 管理器的初始状态设置为 StateOne。

while True:
    fsm.update()

我们的 while 循环非常简单;只需调用 FSM 管理器的 update()方法。就是这样。我们的州从那里处理程序流。

保存并运行该文件,您应该会看到我们在本章开始时显示的输出。

结论

任何面向对象模式的目标都是使类和主程序尽可能小。这减少了您必须为特定类阅读的代码量,使其更容易理解。每个类都应该有一个单一的目的。我们的 FSM 管理器类只有一个目的:运行当前选择的状态。每个状态也只有一个目的:执行特定的动作,直到规则改变,然后转换到一个新的状态。

FSM 非常适合人工智能(AI ),因为你可以根据已知的标准设计非常复杂的交互:用户在武器射程之内吗?我能开枪吗?玩家能看到我吗?等等。等等。

您还可以使用 FSM 来控制程序状态。让我们以一个典型的游戏应用的流程为例。见图 19-3 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 19-3。

游戏的有限状态机

进入状态是 SplashScreen,该屏幕在 3 秒钟后转换到主菜单。主菜单给用户两个选择:玩游戏或退出操作系统。如果用户在玩游戏时死亡,游戏将转换到游戏结束状态。它保持这种状态 3 秒钟,之后,游戏转换到主菜单状态。

我们的下一个项目“入侵者”将我们的模型-视图-控制器(MVC)和有限状态机(FSM)知识联系在一起。

二十、游戏项目:入侵者

我们最后一个街机风格的游戏项目是《入侵者》,它汇集了我们到目前为止所做的一切。我们将声音、动画、MVC 和 FSM 都打包在一个游戏中。见图 20-1 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 20-1。

入侵者行动游戏

在我们开始之前,在“pygamebook”中创建一个名为“projects”的新文件夹,如果还没有的话。在“项目”中,创建另一个名为“入侵者”的文件夹这是我们为这个项目创建的所有文件的存储位置。我们将在这个项目中使用几个文件,它们是

  • bitmapfont . py–包含位图字体的 sprite 表

  • Bullet . py–项目符号类

  • Collision . py–碰撞类别

  • Interstitial . py–间隙屏幕,即“准备就绪”和“游戏结束”屏幕

  • invaders . py——真正可运行的游戏;这是“主”程序,它创建框架并实例化所有对象

  • invaders game . py–实际的“玩游戏”状态类

  • menu . py“类”菜单

  • Player . py–播放器类

  • raspi game . py——你可以用来为你自己的游戏扩展的基类

  • swarm . py–外来虫群类

我们的音效有三个 WAV 文件:

  • 爱丽丝梦游仙境

  • 播放器. wav

  • playershoot.wav

我们还有几个 PNG 文件,包含所有入侵者的动画帧、播放器、显示字体(我们使用位图字体)和项目符号:

  • alienbullet.png 档案

  • bullet.png 格式

  • 爆炸. png

  • fast tracker 2-style _ 12x 12 . png

  • invaders.png

  • 船.png

整个源代码和所有资源(图像和声音文件)都可以从 sloankelly 下载。net 在参考资料部分。

上层社会

以下类别将被定义为该项目的一部分:

  • BitmapFont–允许在 PyGame 表面绘制位图字体。

  • BulletController、BulletModel、bullet view——项目符号实体的 MVC 类。子弹可以被一群外星人或玩家“拥有”。

  • collision controller–处理游戏的碰撞检测。这包括玩家/子弹和外星人/子弹以及玩家/外星人碰撞检测。

  • 爆炸控制器、爆炸模型、爆炸模型列表、爆炸视图–爆炸实体的 MVC 类。当一个外来入侵者或玩家死亡时,会在他们的位置显示一个爆炸。

  • GameState–所有游戏状态的基类。

  • 间隙状态——间隙屏幕在视频游戏中用来显示“游戏结束”或“准备好”的信息。这是程序的“存在状态”;因此,InterstitialState 是从名为“GameState”的 State 基类派生的。

  • 入侵者模型,蜂群控制器,入侵者视图——外来入侵者蜂群的 MVC 类。每个外星人没有单独的控制者;取而代之的是“蜂群控制器”更新每个外星人的位置,并决定哪一个向玩家开火。

  • 玩游戏状态-玩游戏状态。

  • 主菜单状态–主菜单状态。

  • PlayerController、PlayerLivesView、PlayerModel、player view——“播放器”实体的 MVC 类。

  • RaspberryPiGame–包含我们在之前的程序中看到的主要更新循环。这实际上是有限状态管理器。

有限状态机

使用有限状态机(FSM)来控制游戏。图 20-2 中的图表显示了不同的状态以及游戏如何在它们之间转换。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 20-2。

“入侵者”游戏有限状态机

游戏以主菜单状态开始,以“退出”游戏状态结束。正如你将会看到的,“退出”游戏状态并不是真正的状态;这实际上是国家的缺失。我们将游戏的当前状态设置为‘None ’,代码通过干净利落地退出程序来处理这个问题。在我们的实现中,每个状态的基类被定义为“GameState”

MVC 和“入侵者”

每个实体(玩家、外星人群、外星人)都有相应的模型、视图和控制器类。对于外星入侵者,控制者处理不止一个外星实体。

框架

基本状态类和状态机管理器在一个名为“raspigame.py”的文件中定义。创建此文件并键入以下代码:

import pygame, os, sys
from pygame.locals import *

class GameState(object):

游戏状态类定义了一个由 RaspberryPiGame 类使用的接口。每个状态管理游戏的特定功能。例如:主菜单,实际游戏,和间隙屏幕。GameState 类使用新的类定义格式。每个使用新格式的类都必须从对象扩展。在 Python 中,扩展类意味着将基类的名称放在类名后面的括号中。

    def __init__(self, game):
        self.game = game

初始化游戏状态类。每个子类型都必须调用此方法。拿一个参数来说,就是游戏实例。

    def onEnter(self, previousState):
        pass

基类“GameState”不包含 onEnter()的任何代码。扩展“GameState”的类应该提供自己的定义。这个方法是游戏在第一次进入状态时调用的。

    def onExit(self):
        pass

基类“GameState”不包含 onExit()的任何代码。扩展“GameState”的类应该提供自己的定义。这个方法在离开状态时被游戏调用。

    def update(self, gameTime):
        pass

基类“GameState”不包含任何用于 update()的代码。扩展“GameState”的类应该提供自己的定义。这个方法由游戏调用,允许状态自我更新。游戏时间(以毫秒为单位)是自上次调用该方法以来的时间。

    def draw(self, surface):
        pass

基类“GameState”不包含 draw()的任何代码。扩展“GameState”的类应该提供自己的定义。这个方法被游戏调用,允许状态自己画。传递的图面是当前的绘图图面。

class RaspberryPiGame(object):

基本的游戏面向对象的框架为树莓 Pi。用户创建“状态”,在任何特定时间改变屏幕上显示/更新的内容。这实际上只是一个美化了的国家经理。

    def __init__(self, gameName, width, height):
        pygame.init()
        pygame.display.set_caption(gameName);

        self.fpsClock = pygame.time.Clock()
        self.mainwindow = pygame.display.set_mode((width, height))
        self.background = pygame.Color(0, 0, 0)
        self.currentState = None

类构造函数接受游戏的名字,这个名字将被用来改变窗口的标题栏。构造函数创建游戏的主窗口、FPS 时钟和默认背景色。当前状态最初设置为“无”

    def changeState(self, newState):
        if self.currentState != None:
            self.currentState.onExit()

        if newState == None:
            pygame.quit()
            sys.exit()

        oldState = self.currentState
        self.currentState = newState
        newState.onEnter(oldState)

此方法从一种状态转换到另一种状态。如果有一个已存在的状态,则调用该状态的 onExit()方法。这将清除当前状态,并在退出时执行该状态需要执行的任何任务。除非 new state 为’ None ',否则调用新状态的 onEnter 方法。如果新状态是“无”,那么游戏将终止。

    def run(self, initialState):
        self.changeState( initialState )

        while True:
            for event in pygame.event.get():
                if event.type == QUIT:
                    pygame.quit()
                    sys.exit()

            gameTime = self.fpsClock.get_time()

            if ( self.currentState != None ):
                self.currentState.update( gameTime )

            self.mainwindow.fill(self.background)
            if ( self.currentState != None ):
                self.currentState.draw ( self.mainwindow )

            pygame.display.update()
            self.fpsClock.tick(30)

我们的主游戏循环,我们之前已经见过几次了,已经被移到 run()方法中。这处理所有的事件管理、状态更新和显示。

保存文件。

位图字体

在我们测试玩家的坦克和子弹之前,我们必须首先定义位图字体类。普通字体包含每个字符的数学表示。位图字体提供了一个 sprite 表,其中包含组成字体的所有单个字符。然后,我们使用 PyGame 的内置功能将 sprite sheet“切割”成单独的角色。见图 20-3 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 20-3。

摘自 https://opengameart.org/content/8x8-ascii-bitmap-font-with-c-source 的位图字体示例

感谢 OpenGameArt 上的用户‘dark rose’(一个很棒的资源!)作为本例中使用的样本位图字体。从上图中可以看出,字母表中的每个字母和符号都显示在一个网格中。它们按照在 ASCII(美国信息交换标准代码)字符集中出现的顺序排列。第一个可打印的字符是 space,讽刺的是它打印的是一个空格。空格是 ASCII 字符集中的第 33 个字符,因为我们从零开始编号,所以空格是 ASCII 32。

切割图像

为了访问空格旁边的感叹号,ASCII 33,我们使用一些模和除法技巧来计算字符的行和列。

行的计算方法是:取字符的 ASCII 值(在本例中为 33)并除以列数:

33 / 16 = 2

通过获取字符的 ASCII 值并用列数对其进行修改来计算列数:

33 mod 16 = 1

所以,我们的性格(!)位于第 2 行第 1 列。然后,我们将这些值乘以每个单元中的像素数。我们的字符是从一个 8×8 的网格中生成的,所以我们将每个值乘以 8:

2 * 8 = 16

1 * 8 = 8

组成感叹号的 8×8 网格起点的 x 坐标和 y 坐标为(8,16),如图 20-4 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 20-4。

位图字体的特写,显示感叹号字符的 8×8 网格的像素起始位置

在“入侵者”游戏中,位图字体显示由 bitmap font 类处理。我们现在来定义这个类。创建一个新文件,并将其命名为“bitmapfont.py”。输入下面的代码并保存文件。

不过,这里有一个小小的转折。“入侵者”项目包含的字体没有第一个不可打印的 32 个字符。它以空格字符开始。这不是一个真正的问题,但它增加了一个额外的步骤,将字符下移 32 个位置。请注意 toIndex()方法。

import pygame, os, sys
from pygame.locals import *

class BitmapFont(object):
    def __init__(self, fontFile, width, height):
        self.image = pygame.image.load(fontFile)
        self.cellWidth = width
        self.cellHeight = height
        width = self.image.get_rect().width
        height = self.image.get_rect().height
        self.cols = width / self.cellWidth
        self.rows = height / self.cellHeight

构造函数加载文件,并根据每个字符的宽度和高度,计算字符表的列数和行数。

    def draw(self, surface, msg, x, y):
        for c in msg:
            ch = self.toIndex(c)
            ox = ( ch % self.cols ) * self.cellWidth
            oy = ( ch / self.cols ) * self.cellHeight

这是计算消息中当前字符在位图中的 x 和 y 偏移量的代码部分。

            cw = self.cellWidth
            ch = self.cellHeight
            sourceRect = (ox, oy, cw, ch)
            surface.blit(self.image, (x, y, cw, ch), sourceRect)
            x += self.cellWidth

最后,将部分图像用位图传送到表面。

    def centre(self, surface, msg, y):
        width = len(msg) * self.cellWidth
        halfWidth = surface.get_rect().width
        x = (halfWidth - width) / 2
        self.draw(surface, msg, x, y)

centre()方法计算消息的总宽度,并使其居中。

    def toIndex(self, char):
        return ord(char) - ord(' ')

我们用于“入侵者”的位图字体从空格开始(ASCII 32)。我们使用 Python 提供的 order()函数来获取字符的 ASCII 值。减去空间的 ASCII 值得到位图字体的索引值。

间隙筛

间隙屏幕是显示在不同级别之间的图像(“准备好!”)当显示暂停屏幕或玩家死亡时,即出现“游戏结束”屏幕。创建一个名为“interstitial.py”的新文件,并键入以下代码:

import pygame, os, sys
from pygame.locals import *
from bitmapfont import *
from raspigame import *
class InterstitialState(GameState):

我们的 InterstitialState 类扩展了 GameState。记住:如果我们从一个类扩展,我们把父类(或基类)的名字放在类名后面的括号里。

    def __init__(self, game, msg, waitTimeMs, nextState):
          super(InterstitialState, self).__init__(game)

必须调用基类的构造函数。在 Python 下,子类名称和子类实例‘self’必须传递给 super()方法。Python 3.0 通过“语法糖”的方式“解决”了这个问题,只允许你调用 super()。但与 Raspberry Pi 一起发布的 Python 版本并非如此。

我们还必须直接调用构造函数;这就是为什么调用 init()方法的原因。基类的构造函数需要 RaspiGame 的一个实例,因此它被适时地传递给基类的构造函数。

          self.nextState = nextState
          self.font = BitmapFont('fasttracker2-style_12x12.png', 12, 12)
          self.message = msg
          self.waitTimer = waitTimeMs

间隙状态的字段被初始化。

    def update(self, gameTime):
          self.waitTimer -= gameTime
          if ( self.waitTimer < 0 ):
              self.game.changeState(self.nextState)

更新方法会一直等到计时器停止计时。当计时器到达零时,游戏被告知进入下一个状态。

    def draw(self, surface):
          self.font.centre(surface, self.message, surface.get_rect().height / 2)

保存文件。

主菜单

主菜单包含两个项目:

  • 开始游戏

  • 放弃

和间隙屏幕一样,主菜单也是 GameState 的子类。创建一个名为“menu.py”的新文件,并输入以下代码:

import pygame, os, sys
from pygame.locals import *
from raspigame import *
from bitmapfont import *

我们的主菜单状态使用 bitmap 字体类在屏幕上绘制文本,并且导入 raspigame 文件,因为 main menu state 是 GameState 的子类。GameState 在 raspigame.py 文件中定义。

class MainMenuState(GameState):
    def __init__(self, game):
        super(MainMenuState, self).__init__(game)
        self.playGameState = None
        self.font = BitmapFont('fasttracker2-style_12x12.png', 12, 12)
        self.index = 0
        self.inputTick = 0
        self.menuItems = ['Start Game', 'Quit']

当前选定的项目存储在“索引”中,菜单项包含在“菜单项”列表中。

    def setPlayState(self, state):
        self.playGameState = state

当前播放状态设置为“状态”

    def update(self, gameTime):
        keys = pygame.key.get_pressed()
        if ( (keys[K_UP] or keys[K_DOWN]) and self.inputTick == 0):
            self.inputTick = 250
            if ( keys[K_UP] ):
                self.index -= 1
                if (self.index < 0):
                    self.index = len(self.menuItems) -1
            elif ( keys[K_DOWN] ):
                self.index += 1
                if (self.index == len(self.menuItems)):
                    self.index = 0

用户按向上和向下箭头键来选择菜单项。为了防止菜单选择失控,更新被限制在每秒四次(250 毫秒)。

        elif ( self.inputTick >0 ):
            self.inputTick -= gameTime
        if ( self.inputTick < 0 ):
            self.inputTick = 0

通过更新 inputTick 控制变量来防止选择旋转。一旦达到零,就允许再次输入。

        if ( keys[K_SPACE] ):
            if (self.index == 1):
                self.game.changeState(None) # exit the game
            elif (self.index == 0):
                self.game.changeState(self.playGameState)

当用户按空格键时,将测试当前选定的索引。如果用户选择了第零个元素,游戏将变为 playGameState。如果用户选择第一个元素,游戏退出。

    def draw(self, surface):
        self.font.centre(surface, "Invaders! From Space!", 48)

        count = 0
        y = surface.get_rect().height - len(self.menuItems) * 160
        for item in self.menuItems:
            itemText = "  "

            if ( count == self.index ):
                itemText = "> "

            itemText += item
            self.font.draw(surface, itemText, 25, y)
            y += 24
            count += 1

每个菜单项都绘制在屏幕上。选定的菜单项以“>”字符为前缀,以向玩家指示该项已被选定。

保存文件。

玩家和子弹

bullet 类处理已经发射的子弹的位置和集合。像这个游戏中的所有实体一样,项目符号被分成单独的模型、视图和控制器类。MVC 在这场比赛中起了很大的作用!

子弹类

在入侵者文件夹中创建一个新的 Python 文件,并将其命名为‘bullet . py’。输入以下文本:

import pygame, os, sys
from pygame.locals import *
class BulletModel(object):

我们的子弹模型超级简单。这是一个包含 x 和 y 坐标的类,表示子弹在 2D 空间中的位置。它有一个方法,并且只有一个名为 update()的方法,该方法采用单个增量值。这将添加到项目符号位置的 y 坐标中。

    def __init__(self, x, y):
        self.x = x
        self.y = y

将项目符号的位置设定为屏幕上的(x,y)。

    def update(self, delta):
        self.y = self.y + delta

更新项目符号的 y 坐标。

class BulletController(object):

项目符号控制器包含一个项目符号列表。每次调用 update()方法时,都会更新每个项目符号。

    def __init__(self, speed):
        self.countdown = 0
        self.bullets = []
        self.speed = speed

该构造函数创建一个空的子弹对象数组,并将每个子弹的速度设置为“speed”。倒计时变量被用作玩家的冷却时间。他们每 1000 毫秒只能发射一颗子弹。

    def clear(self):
        self.bullets[:] = []

清除项目列表。

    def canFire(self):
        return self.countdown == 0 and len(self.bullets) < 3

玩家只能在倒计时结束且子弹少于三发的情况下开火。

    def addBullet(self, x, y):
        self.bullets.append(BulletModel(x, y))
        self.countdown = 1000

系统中会添加一个项目符号,倒计时会重置为 1 秒(1000 毫秒)。当倒计时到零时,玩家可以再次开火。倒计时字段在 update()方法中更新。

    def removeBullet(self, bullet):
        self.bullets.remove(bullet)

当子弹杀死一个外星人或者从屏幕上方弹出时,它们会从列表中移除。

    def update(self, gameTime):
        killList = []

killList 包含将在本次更新中删除的项目符号。从屏幕顶部弹出的项目符号将从列表中移除。

        if (self.countdown > 0):
            self.countdown = self.countdown - gameTime
        else:
            self.countdown = 0

游戏时间(以毫秒为单位)从倒计时字段中减去。

当倒计时场达到零时,玩家可以再次开火。

        for b in self.bullets:
            b.update( self.speed * ( gameTime / 1000.0 ) )
            if (b.y < 0):
                killList.append(b)

每个项目符号都会更新。如果他们的 y 坐标小于零(子弹从屏幕顶部弹出),那么它被标记为删除。

        for b in killList:
            self.removeBullet(b)

我们最后的子弹类是视图。这将从项目符号控制器获取所有数据,并在屏幕上显示每个项目符号。

class BulletView(object):
    def __init__(self, bulletController, imgpath):
        self.BulletController = bulletController
        self.image = pygame.image.load(imgpath)

用项目符号控制器和项目符号图像的路径初始化项目符号视图。

    def render(self, surface):
        for b in self.BulletController.bullets:
            surface.blit(self.image, (b.x, b.y, 8, 8))

保存文件。

玩家等级

创建一个名为“player.py”的新文件,并输入以下代码。玩家实体的 MVC 组件包含在这个文件中。

import pygame, os, sys
from pygame.locals import *
from bullet import *
from bitmapfont import *

class PlayerModel(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.lives = 3
        self.score = 0
        self.speed = 100 # pixels per second

玩家模型包含玩家实体的所有数据:它在屏幕上的位置(以 x 和 y 坐标的形式)、生命的数量、玩家的分数以及他们的移动速度(以像素/秒为单位)。请记住:通过使用每秒像素,我们可以确保无论机器的速度如何,我们都可以获得一致的移动速度。

class PlayerController(object):
    def __init__(self, x, y):
        self.model = PlayerModel(x, y)
        self.isPaused = False
        self.bullets = BulletController(-200) # pixels per sec
        self.shootSound = pygame.mixer.Sound('playershoot.wav')

构造函数创建一个玩家模型和一个 BulletController 的实例。项目符号控制器接受一个参数,该参数以每秒像素为单位表示移动速度。这是一个负值,因为我们在屏幕上向上移动,屏幕趋向于零。为什么呢?请记住,在计算中,屏幕的左上角是位置(0,0),右下角是 x 轴和 y 轴上的最大值。

    def pause(self, isPaused):
        self.isPaused = isPaused

阻止玩家移动坦克。

    def update(self, gameTime):
        self.bullets.update(gameTime)

        if ( self.isPaused ):
            return

        keys = pygame.key.get_pressed()

        if (keys[K_RIGHT] and self.model.x < 800 - 32):
                self.model.x += ( gameTime/1000.0 ) * self.model.speed
        elif (keys[K_LEFT] and self.model.x > 0):
                self.model.x -= ( gameTime/1000.0 ) * self.model.speed

玩家可以使用键盘上的光标(箭头)键左右移动。位置根据游戏时间按移动速度的百分比更新。无论 CPU 的速度或帧速率如何,这都可以让我们流畅地移动。

        if (keys[K_SPACE] and self.bullets.canFire()):
            x = self.model.x + 9 # bullet is 8 pixels
            y = self.model.y - 16
            self.bullets.addBullet(x, y)
            self.shootSound.play()

当玩家点击空格键时,一颗子弹被添加到当前的子弹列表中,我们播放子弹射击的声音。激发受到“BulletController”类的 canFire()方法的限制。

    def hit(self, x, y, width, height):
        return (x >= self.model.x and y >= self.model.y and x + width <= self.model.x + 32 and y + height <= self.model.y + 32)

这种方法让我们可以测试与任何其他物体的碰撞,方法是将物体浓缩到最纯粹的形式:它在空间中的位置、宽度和高度。

玩家有两个视图类:PlayerView 在屏幕底部显示玩家的坦克,PlayerLivesView 显示玩家剩余的生命数。

class PlayerView(object):
    def __init__(self, player, imgpath):
        self.player = player
        self.image = pygame.image.load(imgpath)

    def render(self, surface):
        surface.blit(self.image, (self.player.model.x, self.player.model.y, 32, 32))

PlayerView 类有一个名为“render”的主要方法。这会在玩家的位置显示坦克。玩家模型被传递到视图中。

class PlayerLivesView(object):
    def __init__(self, player, imgpath):
        self.player = player
        self.image = pygame.image.load(imgpath)
        self.font = BitmapFont('fasttracker2-style_12x12.png', 12, 12)

该构造函数接受两个参数:播放器模型和一个表示位图字体的图像路径的字符串。

    def render(self, surface):
        x = 8

        for life in range(0, self.player.model.lives):
            surface.blit(self.image, (x, 8, 32, 32))
            x += 40

        self.font.draw(surface, '1UP SCORE: ' + str(self.player.model.score), 160, 12)

render 方法绘制船只图像“生命”的次数,然后将玩家的分数显示为“1UP SCORE: 00000”

测试播放器

我们可以通过向 player.py 文件添加以下代码来测试播放器类。这一部分是可选的,但是它给出了一个清晰的例子,可以在独立于主程序的情况下测试类。如果您不想添加此内容,您可以保存文件并转到下一部分。

if ( __name__ == '__main__'):

每个 Python 文件在运行时都有一个名字。如果这是主文件,也就是说,这是正在运行的文件,它被赋予一个特殊的名称“main”如果是这种情况,我们将初始化 PyGame 并创建代码来测试我们的类。

    pygame.init()
    fpsClock = pygame.time.Clock()

    surface = pygame.display.set_mode((800, 600))
    pygame.display.set_caption('Player Test')
    black = pygame.Color(0, 0, 0)

    player = PlayerController(0, 400)
    playerView = PlayerView(player, 'ship.png')
    playerLivesView = PlayerLivesView(player, 'ship.png')

为我们的玩家分别创建一个控制器、视图和生命视图。

    while True:

        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                sys.exit()

        player.update(fpsClock.get_time())

        surface.fill(black)
        playerView.render(surface)
        playerLivesView.render(surface)

        pygame.display.update()
        fpsClock.tick(30)

我们的主循环检查用户是否选择了“QUIT ”(即,他们关闭了窗口),如果没有,则调用 update()方法并呈现每个显示。保存“player.py”文件。

外星虫群类

创建一个名为“swarm.py”的新 Python 文件。我们将在该文件中实现以下类:

  • 入侵者模型

  • 群集控制器

  • 入侵者 w

import pygame, os, sys
from pygame.locals import *
from bullet import *

需要引用 PyGame 库来进行图像操作。alien swarm 也使用子弹,所以我们也需要导入‘bullet . py’文件。我们的入侵者模型包含最少的代码;在 AlienView 中,大部分只是用来描述外星人的数据。

每种异形都有两帧动画,也有两种异形。

class InvaderModel(object):
      def __init__(self, x, y, alientype):
            self.x = x
            self.y = y
            self.alientype = alientype
            self.animframe = 0

构造函数有三个参数,不包括“self”前两个是异形的起始坐标,最后一个是异形类型。有两种外星人类型:一种红色,一种绿色。他们击中时得分不同,这就是为什么我们需要存储这个模型代表什么类型的外星人。

      def flipframe(self):
            if self.animframe == 0:
                  self.animframe = 1
            else:
                  self.animframe = 0

flipframe()方法将动画的当前帧从 0 切换到 1,然后再回到 0。外星人只有两帧动画。

      def hit(self, x, y, width, height):
            return (x >= self.x and y >= self.y and x + width <= self.x + 32 and y + height <= self.y + 32)

hit()方法中的最后一行都在一行上。碰撞类使用 hit()方法来确定是否发生了碰撞。

SwarmController 类实际上是多个外星人的控制器。它使用合成,因为每个单独的外星人都是由虫群类创造和毁灭的。

class SwarmController(object):
      def __init__(self, scrwidth, offsety, initialframeticks):
            self.currentframecount = initialframeticks
            self.framecount = initialframeticks

当前动画帧从这里控制。这确保了每个外星人与其他外星人在时间上同步行进。

            self.invaders = []
            self.sx = -8
            self.movedown = False
            self.alienslanded = False

当前外来方向设置为负(左)方向。当外星人撞到一边时,必须在屏幕上向下移动,这时会设置“向下移动”标志。最后一个标志“alienslanded”意味着当这个标志为真时,玩家的游戏结束。

            self.bullets = BulletController(200) # pixels per sec

BulletController 类也是 SwarmController 的一部分。子弹速度的每秒像素值是正的,因为我们在屏幕上往下走。请记住,对于玩家来说,它是负的,因为玩家的子弹在屏幕上。

            self.alienShooter = 3 # each 3rd alien (to start with) fires
            self.bulletDropTime = 2500
            self.shootTimer = self.bulletDropTime # each bullet is fired in this ms interval
            self.currentShooter = 0 # current shooting alien

            for y in range(7):
                  for x in range(10):
                        invader = InvaderModel(160 + (x * 48) + 8, (y * 32) + offsety, y % 2)
                        self.invaders.append(invader)

嵌套的 for 循环用于生成异类群体。每个 swarm 成员都是 InvaderModel 类的一个实例。

      def reset(self, offsety, ticks):
            self.currentframecount = ticks
            self.framecount = ticks

            for y in range(7):
                  for x in range(10):
                        invader = InvaderModel(160 + (x * 48) + 8, (y * 32) + offsety, y % 2)
                        self.invaders.append(invader)

“重置”方法是用来重置下次攻击的外星生物群,加速他们的下降。

      def update(self, gameTime):
            self.bullets.update(gameTime)
            self.framecount -= gameTime
            movesideways = True

“framecount”成员字段用作计时器。游戏时间从“帧计数”中的当前时间中减去,当它达到零时,我们“滴答”蜂群。这就是我们如何控制对象的更新速度。我们可以指定不同的“滴答”时间。“帧计数”越小,更新就越快,因为我们必须减去更少的时间。

            if self.framecount < 0:
                  if self.movedown:
                        self.movedown = False
                        movesideways = False
                        self.sx *= -1
                        self.bulletDropTime -= 250
                        if ( self.bulletDropTime < 1000 ):
                              self.bulletDropTime = 1000
                        self.currentframecount -= 100
                        if self.currentframecount < 200: #clamp the speed of the aliens to 200ms
                              self.currentframecount = 200

                        for i in self.invaders:
                              i.y += 32

如果我们必须向下移动,在“if self.movedown”下的代码部分提供了将外星人群体向下移动到屏幕上所需的步骤。当群体向屏幕下方移动时,“当前帧计数”会更新。这是因为外星人每次向玩家下落的时候都会加速。

                  self.framecount = self.currentframecount + self.framecount
                  for i in self.invaders:
                        i.flipframe()

                  if movesideways:
                        for i in self.invaders:
                              i.x += self.sx

                  x, y, width, height = self.getarea()

                  if ( x <= 0 and self.sx < 0) or ( x + width >= 800 and self.sx > 0 ):
                        self.movedown = True

getarea()方法决定了游戏场上所有外星人使用的区域。然后,我们使用此信息来确定该区域是否“击中”了侧面。如果这个区域碰到了边上,我们标记蜂群在下一个滴答向下移动。

            self.shootTimer -= gameTime
            if ( self.shootTimer <= 0):
                        self.shootTimer += self.bulletDropTime # reset the timer
                        self.currentShooter += self.alienShooter

                        self.currentShooter = self.currentShooter % len(self.invaders)

                        shooter = self.invaders[self.currentShooter]
                        x = shooter.x + 9 # bullet is 8 pixels
                        y = shooter.y + 16
                        self.bullets.addBullet(x, y)

拍摄计时器的工作时间与帧更新时间不同。当定时器到零时,当前射手递增‘alien shooter’;因此它不是主要虫群蜱的一部分。

“当前射手”字段被固定在我们剩下的外星人数量上。这确保了我们不会试图访问我们列表之外的外星人。然后引用当前的射手,我们在射手的位置添加一颗子弹。我选择 3(三)作为增量手,因为它给了射击一种伪随机的感觉。

      def getarea(self):
            leftmost = 2000
            rightmost = -2000
            topmost = -2000
            bottommost = 2000

设置最大和最小边界。

            for i in self.invaders:
                  if i.x < leftmost:
                        leftmost = i.x

                  if i.x > rightmost:
                        rightmost = i.x

                  if i.y < bottommost:
                        bottommost = i.y

                  if i.y > topmost:
                        topmost = i.y

使用一些简单的范围检查,我们计算所有外星人最左边、最右边、最上面和最下面的点。

            width = ( rightmost - leftmost ) + 32
            height = ( topmost - bottommost ) + 32

            return (leftmost, bottommost, width, height)

我们最后的入侵者类是视图类。它使用聚合,因为它引用了 SwarmController 类。

class InvaderView:
      def __init__(self, swarm, imgpath):
            self.image = pygame.image.load(imgpath)
            self.swarm = swarm

构造函数接受两个参数。第一个是 SwarmController 实例,第二个是代表我们的外星精灵的图像文件的路径。

      def render(self, surface):
            for i in self.swarm.invaders:
                  surface.blit(self.image, (i.x, i.y, 32, 32), (i.animframe * 32, 32 * i.alientype, 32, 32))

“render”方法循环遍历 SwarmController 的“swarm”域中的所有入侵者,并将其显示在屏幕上。入侵者模型的“animframe”字段用于控制精灵片的切片向左移动多远。“alientype”字段是切片向上的距离。

保存文件。我们需要这个文件和其他文件来进行碰撞检测。

冲突检出

我们的碰撞检测类存储在“collision.py”文件中。创建一个新的空白文件,并将其命名为“collision.py”。这将包含以下类:

  • 爆炸模型

  • 爆炸模型列表

  • 分解视图

  • 爆炸控制器

  • 碰撞控制器

我们将按照它们在文件中出现的顺序来检查它们。

爆炸

动作游戏需要巨大的噪音和爆炸。我们的游戏没有什么不同!四个爆炸类——explosion model、ExplosionModelList、ExplosionView 和 explosion controller——由 CollisionController 用来创建和更新游戏中发生的各种爆炸。每个爆炸都是使用由一系列动画帧组成的 sprite sheet 在屏幕上绘制的。

我们的文件以熟悉的方式从一系列导入开始:

import pygame, os, sys
from pygame.locals import *
from player import *
from bullet import *
from swarm import *
from interstitial import *

我们自己的球员,子弹,蜂群和间质类是必需的。

class ExplosionModel(object):
    def __init__(self, x, y, maxFrames, speed, nextState = None):
        self.x = x
        self.y = y
        self.maxFrames = maxFrames
        self.speed = speed
        self.initialSpeed = speed
        self.frame = 0
        self.nextState = nextState

“ExplosionModel”类不包含任何方法,就像我们所有的其他模型一样。它只包含描述爆炸的字段;它的位置,帧数,更新速度,当前帧,下一个状态。

class ExplosionModelList(object):
    def __init__(self, game):
        self.explosions = []
        self.game = game

    def add(self, explosion, nextState = None):
        x, y, frames, speed = explosion
        exp = ExplosionModel(x, y, frames, speed, nextState)
        self.explosions.append(exp)

    def cleanUp(self):

        killList = []

        for e in self.explosions:
            if ( e.frame == e.maxFrames ):
                killList.append(e)

        nextState = None

        for e in killList:
            if (nextState == None and e.nextState != None):
                nextState = e.nextState

            self.explosions.remove(e)

        if (nextState != None):
            self.game.changeState(nextState)

cleanUp()方法需要一点解释。有了这个机制,我们可以在我们的爆炸中编码将游戏移动到另一个状态的能力。例如,当玩家死了,他们没有更多的生命,我们可以改变游戏的状态为“游戏结束”

class ExplosionView(object):
    def __init__(self, explosions, explosionImg, width, height):
        self.image = pygame.image.load(explosionImg)
        self.image.set_colorkey((255, 0, 255))
        self.explosions = explosions
        self.width = width
        self.height = height

    def render(self, surface):
        for e in self.explosions:
            surface.blit(self.image, ( e.x, e.y, self.width, self.height ), (e.frame * self.width, 0, self.width, self.height) )

“爆炸视图”循环显示所有爆炸,并依次显示每个爆炸。

class ExplosionController(object):
    def __init__(self, game):
        self.list = ExplosionModelList(game)

    def update(self, gameTime):
        for e in self.list.explosions:
            e.speed -= gameTime
            if ( e.speed < 0 ):
                e.speed += e.initialSpeed
                e.frame += 1
        self.list.cleanUp()

“爆炸控制器”是我们遇到的最简单的控制器。它有一个初始化方法和一个 update()方法,前者创建一个“ExplosionModelList”(一个组合的例子)。update()方法只需要增加帧计数。当计数达到最大帧数时,它会在“ExplosionModelList”类的 cleanUp()方法中自动移除。

碰撞控制器

“CollisionController”类不需要相应的模型或视图,因为它既不需要也不需要。它确实使用其他控制器和模型来确定是否发生了碰撞。如果有东西被击中,会发出适当的声音,并采取行动。

class CollisionController(object):
    def __init__(self, game, swarm, player, explosionController, playState):
        self.swarm = swarm
        self.player = player
        self.game = game
        self.BulletController = player.bullets
        self.EnemyBullets = swarm.bullets
        self.expCtrl = explosionController
        self.playGameState = playState
        self.alienDeadSound = pygame.mixer.Sound('aliendie.wav')
        self.playerDie = pygame.mixer.Sound('playerdie.wav')

“CollisionController”的构造函数接受游戏、群体控制器、玩家控制器、爆炸控制器实例和游戏状态。我们还加载了几个声音,当玩家撞到一个外星人时(“aliendie.wav”),或者如果一个外星人不幸撞到了玩家(“playerdie.wav”)。

    def update(self, gameTime):

        aliens = []
        bullets = []

        for b in self.BulletController.bullets:

            if (bullets.count(b)>0):
                continue

            for inv in self.swarm.invaders:
                if (inv.hit(b.x+3, b.y+3, 8, 12)):
                    aliens.append(inv)
                    bullets.append(b)
                    break

收集所有玩家的子弹和击中入侵者的外星人。

        for b in bullets:
            self.BulletController.removeBullet(b)

移除所有击中外星人的子弹

        for inv in aliens:
            self.swarm.invaders.remove(inv)
            self.player.model.score += (10 * (inv.alientype + 1))
            self.expCtrl.list.add((inv.x, inv.y, 6, 50))
            self.alienDeadSound.play()

移除所有被玩家子弹击中的外星人。这部分还会递增玩家的分数,播放外星人死亡音。

        playerHit = False

        for b in self.EnemyBullets.bullets:
            if ( self.player.hit (b.x+3, b.y+3, 8, 12 ) ):
                self.player.model.lives -= 1
                playerHit = True
                break

现在我们检查敌人的子弹。如果他们中的任何一个击中了玩家,我们将“玩家击中”标志设置为“真”并“中断”for 循环。如果我们已经击中了玩家,就没有必要继续在子弹中搜索了。

        if ( playerHit ):
            self.EnemyBullets.clear()
            self.player.bullets.clear()

            if ( self.player.model.lives > 0 ):
                self.player.pause(True)
                getReadyState = InterstitialState( self.game, 'Get Ready!', 2000, self.playGameState )
                self.expCtrl.list.add((self.player.model.x, self.player.model.y, 6, 50), getReadyState)

            self.playerDie.play()

如果玩家被击中,我们清除游戏中的所有子弹。如果玩家还有生命,我们暂停玩家,将游戏状态改为“准备好”屏幕,并添加一个爆炸来显示玩家的坦克被摧毁。记住:我们可以改变爆炸后的状态(参见“爆炸控制器”类),这就是我们在这里设置的。

我们快完成了!还有两个文件。这些是主程序和主游戏状态。

主程序

主程序是一个名为“invaders.py”的文件。创建一个名为“invaders.py”的新文件,并输入以下代码。我们之前创建的“RaspberryPiGame”类需要一个初始状态。我们的主程序的功能是创建有限状态机(FSM)使用的状态,并设置初始状态。

import pygame, os, sys
from pygame.locals import *
# Our imports
from raspigame import *
from interstitial import *
from menu import MainMenuState
from invadersgame import PlayGameState

我们通常为 OS 和 PyGame 模块加上我们自己的本地模块导入。我们正在安装“raspigame.py”和“interstitial.py”中的所有内容,但只有“menu.py”中的 MainMenuState 和“invadersgame.py”中的 PlayGameState。

invadersGame = RaspberryPiGame("Invaders", 800, 600)
mainMenuState = MainMenuState( invadersGame )
gameOverState = InterstitialState( invadersGame, 'G A M E  O V E R !', 5000, mainMenuState )
playGameState = PlayGameState( invadersGame, gameOverState )
getReadyState = InterstitialState( invadersGame, 'Get Ready!', 2000, playGameState )
mainMenuState.setPlayState( getReadyState )

创建游戏中使用的状态实例:主菜单、游戏结束、玩游戏和准备就绪状态。

invadersGame.run( mainMenuState )

将游戏的初始状态设置为主菜单。就这样——这是主程序。它的唯一目的是创建游戏状态之间的链接,并设置初始状态,这都是在六行代码中实现的。

保存此文件。剩下要做的最后一个类是主游戏状态。

主游戏状态

创建一个名为“invadersgame.py”的新文件。输入以下代码:

import pygame, os, sys
from pygame.locals import *
from raspigame import *
from swarm import *
from player import *
from collision import *

模块导入。

class PlayGameState(GameState):
    def __init__(self, game, gameOverState):
        super(PlayGameState, self).__init__(game)
        self.controllers = None
        self.renderers = None
        self.player_controller = None
        self.swarm_controller = None
        self.swarmSpeed = 500
        self.gameOverState = gameOverState
        self.initialise()

我们的“PlayGameState”类派生自“GameState ”,因此构造函数必须调用基类“constructor”。控制器和“游戏结束”状态的字段被初始化。为了尽量减少这个方法,调用了 initialise()方法。

    def onEnter(self, previousState):
        self.player_controller.pause(False)

onEnter()方法是 GameState 类的一部分。我们唯一需要做的就是告诉玩家控制器它是未暂停的。

    def initialise(self):
        self.swarm_controller = SwarmController(800, 48, self.swarmSpeed)
        swarm_renderer = InvaderView(self.swarm_controller, 'invaders.png')

        self.player_controller = PlayerController(0, 540)
        player_renderer = PlayerView(self.player_controller, 'ship.png')
        lives_renderer = PlayerLivesView(self.player_controller, 'ship.png')
        bullet_renderer = BulletView(self.player_controller.bullets, 'bullet.png')
        alienbullet_renderer = BulletView(self.swarm_controller.bullets, 'alienbullet.png')

        explosion_controller = ExplosionController(self.game)
        collision_controller = CollisionController(self.game, self.swarm_controller, self.player_controller, explosion_controller, self)

        explosion_view = ExplosionView(explosion_controller.list.explosions, 'explosion.png', 32, 32)

        self.renderers = [ alienbullet_renderer, swarm_renderer, bullet_renderer, player_renderer, lives_renderer, explosion_view ]
        self.controllers = [ self.swarm_controller, self.player_controller, collision_controller, explosion_controller ]

initialise()方法包含创建每个控制器和渲染器的实例的代码。这些然后被添加到“渲染器”和“控制器”字段。这些字段中的每一个都是一个列表,我们可以在 update()和 draw()方法中遍历它。

    def update(self, gameTime):
        for ctrl in self.controllers:
            ctrl.update(gameTime)

遍历所有控制器,并在每个控制器上调用 update()方法。因为我们已经将控制器存储在一个列表中,所以更新每个控制器是一段相当简单的代码

        if ( self.player_controller.model.lives == 0 ):
            self.game.changeState( self.gameOverState )

如果玩家没有更多的生命了,我们将游戏状态改为“游戏结束”状态。按照现在的情况,后面是什么行并不重要,但是您可能希望在这里添加一个“return”来退出该方法。

        if ( len(self.swarm_controller.invaders) == 0 ):
            self.swarmSpeed -= 50
            if ( self.swarmSpeed < 100 ):
                self.swarmSpeed = 100

如果屏幕上没有外星人了,我们就开始新的一关。外星人的速度降低了;这意味着更新之间的增量时间减少了。这也意味着我们得到了更快的外星人。

            self.swarm_controller.reset(48, self.swarmSpeed)
            levelUpMessage = InterstitialState( invadersGame, 'Congratulations! Level Up!', 2000, self )
            self.game.changeState ( levelUpMessage )

现在我们开始一个新的游戏(外星人群),我们告诉群控制器重置到新的速度,并改变游戏的状态以显示“升级”的消息。

    def draw(self, surface):
        for view in self.renderers:
            view.render(surface)

最后一个方法在屏幕上绘制所有对象。

保存文件。我们完了!你现在有一个完整的“入侵者”游戏。

运行游戏

要运行游戏,请在“invaders.py”的代码编辑器中按 F5,或者在命令提示符下从“pygamebook/projects/invaders”文件夹中键入以下内容:

$ python ./invaders.py

使用箭头键选择菜单选项,按空格键选择,如图 20-5 所示。玩得开心,这是你应得的!

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 20-5。

入侵者主菜单

结论

我们已经使用 Raspberry Pi、Python 和 PyGame 构建了复杂的街机风格的游戏。使用 MVC(模型视图控制器)这样的模式,我们的程序类可以保持相对较短,并专注于做一件事,无论是渲染精灵、控制角色还是作为数据模型。

在本书的最后几章中,我们将结合我们在 Python 和 PyGame 中学到的知识,使用 Raspberry Pi 的通用输入/输出(GPIO)引脚来控制 led 并接收按钮输入,从而创建与我们环境的其他交互。

二十一、带有 GPIO 引脚的简单电子器件

到目前为止,我们已经看到 Raspberry Pi 作为输入设备与键盘和鼠标通信,作为输出设备与显示器通信。Raspberry Pi 可以与各种各样的外围设备(这是一个可以添加的花哨名称)以及发光二极管(led)或开关等电子组件进行通信。这是通过树莓派顶部的销钉连接设备来实现的。这些引脚被称为通用输入/输出引脚,简称 GPIOs。

如图 21-1 所示,树莓 Pi 型号 B+上有 40 个引脚。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 21-1

Raspberry Pi 板上通用输入/输出(GPIO)引脚的位置

在连接和拆卸外围设备时,您应该始终小心,并且在这样做时应该关闭电源。保持电源接通并连接设备可能会损坏电脑。

将设备插入树莓派时要小心!在这样做之前,请务必关闭机器!

电压、电流和电阻

在我们将元件连接到 Raspberry Pi 之前,我们应该后退一步,讨论一些电子基础知识。当产生一条允许自由电子沿其移动的路径时,电子电路就形成了。这些电子的连续运动是一种电流,思河。导致这些电子流动的力被称为电压,是电路中两点之间势能的度量。

最后,还有阻力。不管你的电路设计得多好,你总会遇到一些阻力。这可能是由电线的缺陷、热损耗造成的能量损失等引起的。你甚至可以通过添加称为电阻器的装置来降低电流,从而增加不完美性。在图 21-2 中,我们看到一个包含一个发光二极管(一盏灯)、一个开关和一个电阻的电路。它们都与电池相连。电路是完整的,因为电池的两端——ve 和 ve 端——通过电阻、开关和 LED 连接。电路将完成,也就是说,当开关接通时,电子将自由地从电池的+ve 端流向–ve 端。这种流动称为常规电流

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 21-2

当左边的开关被按下时,一个简单的电路将点亮灯

当按下开关时,该电路将点亮 LED。在本章的后半部分,我们将使用下列元件构建此电路。

你需要什么

对于本书中的其余项目,在开始使用 Raspberry Pi 创建电路之前,您需要一些东西。至少你需要

  • 面包板

  • 分线板

  • 跳线

  • 发光二极管

  • 电阻器(330ω即可)

  • 开关

树莓派可以提供 3.3V 电源以及接地。再次,小心连接和断开电线。如果您不注意连接的引脚,可能会损坏您的 Raspberry Pi。

试验板

试验板用于制作电子电路的原型。它允许轻松放置和移除 led、电线和开关等组件,而无需焊接或脱焊这些组件。如图 21-2 所示,试验板由塑料外壳保护的微小连接器覆盖。微小的连接器以非常特殊的方式排列。

在顶部和底部有两条线,标记为+(正)和-(负)。这些是电源线(3.3 伏)和地线(0 伏)。如果您将 Raspberry PI 的 3.3V 输出连接到+轨,该轨上的所有连接器接收 3.3V 电压。这使得将元件连接到+ve(正极)和–ve(负极)供电轨变得非常容易。

中间部分从中间分开,将每一行分成两半。每个行部分的列是相连的。同样,这使得将多个输出连接到一个引脚变得更加容易。每一行不与另一行相连。试验板上通常有一个编号系统,以便于创建电路。在下面的例子中,列标记为“a”到“j ”,行以 5: 1、5、10、15 等间隔编号。

分线板

分线板是一种简单的设备,可以轻松地将 Raspberry Pi 连接到原型制作试验板上。如图 21-3 所示,将器件放置在试验板上。每行的其余列允许跳线或电阻连接到该行的相关引脚。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 21-3

用于制作电子电路原型的试验板

然后,可以使用随附的带状电缆连接 Raspberry Pi 和试验板。请阅读如何连接带状电缆的说明,因为它因供应商和型号而异。

大多数分线板如图 21-3 所示,但有些是 T 形的,以使连接更加容易。

请注意,分线板的侧面标有树莓 Pi 引脚。这使得在放置跳线时更容易识别管脚。Adafruit 为各种树莓 Pi 型号提供了一个名为“补鞋匠”的分线板。详见 www.adafruit.com/

跳线

跳线有各种长度和形状,如图 21-4 和 21-5 所示。有些是盒子里的预包装,有些是塑料袋里的随机分类。无论哪种方式,它们通常是一根坚固的电线,允许您连接发光二极管、开关等。,到树莓派别针。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 21-5。

实芯焊丝的选择

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 21-4

试验板上的分线板(图片中间),跳线和电阻连接到引脚

发光二极管

发光二极管(led)允许电流沿一个方向流动(这是二极管部分),同时发光。它用于提供廉价的低成本照明,有多种包装。对于大多数电子设备,使用熟悉的彩色圆顶版本,如图 21-6 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 21-6。

一个 LED。注意,一条腿比另一条腿长。较长的引脚是阳极,总是连接到正电源轨。

你可能已经注意到一条腿比另一条腿长。这称为阳极,总是连接到+ve (3.3V)供电轨。较短的一脚称为阴极,连接到地或 0V 线。如果你把它接反了,灯就不亮了。你不会弄坏灯,它就是不亮。

当使用发光二极管时,你还需要在电路上连接一个电阻,因为它们会从树莓皮中吸取更多的能量,从而损坏电脑。

电阻

电阻器限制通过电路的电流量。电阻的测量单位称为欧姆(ω),电阻值越大,对电流的限制就越大。计算电路中电压、电流(以安培为单位)和电阻(欧姆)的公式为

V = IR

其中 V 是电压,I 是电流,R 是电阻。如果要计算在 3.3V 电压下流经 330ω电阻的电流,则为

V = IR

这意味着 I = V / R:

I = 3.3 / 330

I = 0.01 amps

如果我们将该电阻增加到 470ω,它会将电流从 0.01 安培降至 0.006 安培,几乎是初始值的一半,这意味着通过电阻另一端的电流会减少。

一条电阻如图 21-7 所示。电阻值已经写在纸条上,以便更容易识别其值:330ω。电阻器周围的带表示其值。有三到六个色带,但是我用了五个,如表 21-1 所示:

表 21-1

电阻器色带位置和含义

|

颜色

|

第一乐队

|

第二波段

|

第三波段

|

乘数

|

容忍

|
| — | — | — | — | — | — |
| 黑色 | Zero | Zero | Zero | 1 小时 |   |
| 棕色 | one | one | one | 20+ | ± 1% |
| 红色 | Two | Two | Two | 1 号机 | ± 2% |
| 橙色 | three | three | three | 10 号 |   |
| 黄色 | four | four | four | 一百 |   |
| 绿色 | five | five | five | 1MΩ | ± 0.5% |
| 蓝色 | six | six | six | 10MΩ | ± 0.25% |
| 紫色 | seven | seven | seven | 1 兆焦耳 | ± 0.1% |
| 灰色 | eight | eight | eight | 0.1Ω | ± 0.05% |
| 白色 | nine | nine | nine | 0.01Ω |   |
| 黄金 |   |   |   |   | ± 5% |
| 白银 |   |   |   |   | ± 10% |

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 21-7

供应商提供的一条电阻器,其值标在纸带上

我拥有的电阻颜色如表 21-2 所示。

表 21-2

转换色带以确定电阻值

|

颜色

|

价值

|
| — | — |
| 橙色 | three |
| 橙色 | three |
| 黑色 | Zero |
| 黑色 | 1 哦 |
| 黄金 | ± 5% |

这是 330×1ω或 330ω,容差为 5%。

在电路中放置 LED 时,使用电阻非常重要。LED 将试图吸收尽可能多的电流,而电阻是限制这种吸收的一个很好的方法。电阻没有方向性,不像 led,所以你可以把它们放在你喜欢的任何地方,为了清晰起见,我建议让它们都面向同一方向。

开关

图 21-8 显示了一个简单的按钮轻触开关。它包含成两对排列的四个引脚。每一对都与另一对断开。当开关被按下时,封装内的触点完成线对之间的连接。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 21-8

一个简单的按钮轻触开关

建造一个电路

既然我们已经学习了电子电路的基础知识,让我们构建一个不需要编程的非常简单的电路。我们将根据图 21-1 重建电路。为此,我们需要

  • 一块试验板

  • 分线板

  • 三根跳线

  • S7-1200 可编程控制器

  • 轻触开关

  • 发光二极管

将分线板连接到树莓派

点击 Raspberry Pi 菜单并选择“关闭…”关闭 Raspberry Pi,如图 21-9 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 21-9

Raspberry Pi 系统菜单上的关闭菜单项

等到机器完全关闭。不要切断电源!我们需要能量来建造电路。

将分线板的带状电缆连接到 Raspberry Pi 的 GPIO 引脚。注意不要用力,因为如果电缆没有正确固定,这样可能会弯曲针脚。

带状电缆上通常有一根白线。这表示第一个引脚,用于将 Raspberry Pi 与分线板对齐。如图 21-10 所示,确保白色电线最靠近电路板顶部,即远离 USB 和网络端口。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 21-10

箭头指示电缆上白线的位置。请注意主板相对于 USB、网络和 HDMI 端口的方向

将分线板插入试验板上。分线板应跨在试验板的中间槽上,如图 21-4 所示。同样,注意不要弯曲任何引脚。

接下来,将带状电缆的另一端连接到分线板。在分线板的侧面有一个凹口,在带状连接器上有一个凸起部分,这就更容易做到了。它只能朝一个方向走,但可以肯定的是,电缆上的白线应该在电路板的顶部。

现在,您应该已经将覆盆子 Pi 连接到试验板了。即使树莓派是关闭的,电源仍然连接着它。我们可以用这个给我们的电路供电。

提供电源和接地

我们将添加的前两条跳线连接到 Raspberry Pi 的 3.3V 和接地引脚。这些位于分线板顶部附近,如图 21-11 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 21-11

3V3 引脚和接地引脚线

如图 21-11 所示,将电线的一端插入 3V3 (3.3 伏)引脚排。将另一端放在试验板的+ve 导轨上。这是我们的 3.3V 线。连接到那条线上的任何东西(在试验板上显示为红色)都将连接到 3.3V。

接下来拿另一根线,把它连接到标有 GND 或地的一排线上。这将是我们的地线。我们还没有地方放它,所以暂时让它漂浮着吧。

添加 LED

将 LED 放在主板上,阳极(较长的一条)放在一排,阴极(较短的一条)放在另一排。不要把针放在同一排,否则它不会工作!在图 21-12 中,我展示了如何放置 LED。只要你记得是哪个方向,哪个方向并不重要——箭头表示阳极。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 21-12

LED 引脚放置在不同的行上。箭头指向阳极引脚。

为了将 LED 连接到 3.3V 线路,我们将使用 330ω电阻。

记住!务必使用带 LED 的电阻器!

如图 21-13 所示,将电阻的一端连接到与阳极相同的行,另一端连接到+ve 轨。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 21-13

使用 330ω电阻将 LED 连接到 3.3V 线路

完成电路

我们现在将通过添加轻触开关和跳线来完成电路。如图 21-14 所示,在试验板上放置一个开关。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 21-14

一个轻触开关跨在试验板中间的槽上

我喜欢把它放在板中间的槽上,但是你可以把它放在任何你想放的地方。请记住,开关有两对,每对的引脚相互连接。这意味着,如图 21-14 所示,我们需要将电线连接到顶部和底部引脚,以便当轻触开关被按下时,电路接通。如果我们连接两个顶部引脚或两个底部引脚,电路将完成。

将之前悬空的 0V(接地)线插入与开关顶部引脚同一行的连接器中。取另一根跨接导线,将其连接到与开关底部引脚相同的行,并连接到 LED 的阴极(短引脚)。

你现在应该有一个类似于图 21-15 所示的电路。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 21-15。

完整的电路。分线板在图片顶部部分可见。

测试电路

完整电路如图 21-16 所示。您应该对照刚才制作的物理电路检查该电路中的连接。在按下轻触开关之前,确保所有连接都与图中的连接相匹配。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 21-16。

显示 Raspberry Pi 和试验板之间连接的电路图。在按下轻触开关之前,使用此图确保您的电路构建正确。

如果一切正常,按下轻触开关,电路应该亮了!如果没有,检查你的线路。如果您断开了 Raspberry Pi 的电源——记住,我们需要这个电路的电源——您应该重新连接电源。这将打开你的树莓皮,但这很好。对于这个练习,我们只需要来自引脚的功率。

Pin 含义

每个引脚都有特定的用途,Raspberry Pi 允许您使用 SPI(串行外设接口)、I2C(内部集成电路)或 GPIO 引脚直接连接到外设。在本书的剩余部分,我们将专注于 GPIO 引脚本身。我们将在后面看到,我们必须告诉 Raspberry Pi 我们将如何使用 GPIO 引脚,也就是什么操作模式。图 21-17 显示了物理引脚的映射。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 21-17

每个 GPIO、电压和接地引脚的位置

你会注意到的第一件事是编号的针没有按顺序排列。这些是 GPIO 引脚,可以通过 Python 直接访问。您可以连接开关、发光二极管等。,并让 Python 读取它们的值或向它们写入值以打开灯。

3v3 引脚输出 3.3 伏,5V 引脚输出 5 伏。建议您坚持使用 3.3V 线路,除非您尝试连接的外设需要 5v 电压。接地引脚是不可知的;不管你用的是 5 伏还是 3.3 伏,你的电路都使用相同的接地引脚。接地引脚在图 21-17 中标记为‘G’。

最后两个引脚是 GPIO 的引脚 0 和 1,不可访问。不应将设备连接到这两个引脚。在图中,那些针标有’-'。

让我们重建电路,用 Python 开灯。

gpiozero 库

为了与试验板上的电子元件“对话”,我们将使用 gpiozero 库。这是一组工具,可以轻松地与连接到 GPIO 引脚的组件通信并从中读取数据。

这本书将只涵盖这个库所涵盖的功能的很小一部分。如果您想连接运动传感器、温度计、电位计等。,那么我推荐在 https://gpiozero.readthedocs.io 通读文档。

我们将使用这个库创建的第一个程序是一个打开 LED 一秒钟然后关闭的程序。

该电路

图 21-18 所示的电路通过 GPIO 引脚 4 将 LED 的阳极(两个引脚中较长的一个)连接到 Raspberry Pi。电阻连接到 LED 的阴极(两个引脚中较短的一个),然后连接到 Raspberry Pi 的一个接地引脚。构建如图 21-18 所示的电路。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 21-18

Python 控制的 led 电路

此时,LED 不应亮起。我们将用代码来实现。

Python 程序

打开空闲的 IDE,创建一个名为“ch21-1.py”的新文件,并将其放在“pygamebook”文件夹内名为“ch21”的新文件夹中。在该文件中,键入以下内容:

from gpiozero import LED
from time import sleep

导入 gpiozero 库来访问 LED 类。这个包装类使得打开和关闭 led 变得更加容易。第二个导入是 sleep()函数。Sleep 有一个参数,即计算机在执行下一条语句之前应该等待的秒数

led = LED(4)

创建一个连接到物理 LED 的 LED 对象,该物理 LED 连接到 GPIO 引脚 4。

led.on()
sleep(1)

打开 LED 并等待一秒钟。

led.off()
sleep(1)

关闭 LED 并等待一秒钟。

保存并运行程序。观察试验板上发生的情况 LED 亮起一秒钟,然后熄灭!

其他功能

on()和 off()函数非常适合于关闭和打开 LED,但是您也可以切换 LED 的状态,因此如果灯亮了,调用 toggle()将关闭它,反之亦然。最后,blink()会做到这一点,它会使 LED 闪烁。您可以指定灯打开的持续时间和灯关闭的持续时间。

获取按钮输入

除了将值输出到 GPIO 引脚,Raspberry Pi 还可以读取值。在本节中,我们将在电路中添加一个按钮,并控制 LED 的闪烁。修改后的电路如图 21-19 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 21-19

轻触开关控制的发光二极管电路

LED 仍然连接到 GPIO 引脚 4,就像在以前的电路中一样。该开关一端连接到 GPIO 17,另一端连接到 Raspberry Pi 上的接地引脚。Button 对象可用于使读取按钮的状态(按下、释放)变得容易。

在 Python 中读取按钮输入

在与上一个程序相同的文件夹中创建一个名为“ch21-2.py”的新文件。准确输入所写的代码:

from gpiozero import LED, Button

从 gpiozero 库中导入 LED 和 Button 类。

led = LED(4)
button = Button(17)

创建一个 LED 对象,其中物理 LED 连接到 GPIO 引脚 4。创建一个按钮对象,其中物理轻触开关连接到 GPIO 引脚 17。

ledOn = False
wasPressed = False

用于记录 LED 状态的标志–LED on。当程序启动时,它是关闭的,所以’ ledOn '被设置为假。用于记住按钮最后状态的标志。这将防止按钮被持续按下和 LED 快速闪烁。

while True:

保持程序运行。要退出程序,请按 Ctrl+C。

    if not button.is_pressed and wasPressed:
        ledOn = not ledOn

如果按钮已被释放,即如果按钮已被按下且未被未被按下,则切换“ledOn”变量的状态。

        if ledOn:
            led.blink()
        else:
            led.off()
    wasPressed = button.is_pressed

如果 LED 应该亮起,将其设置为闪烁。否则,应该将其关闭。“状态”变量被设置为按钮被按下的当前状态。

保存并运行程序。

短暂按下试验板上的轻触开关。LED 将开始闪烁。再次按下按钮将停止 LED 闪烁。按键盘上的 Ctrl+C 退出程序。您可以向电路板添加另一个按钮,并查询该按钮以确定程序是否应该停止。听起来像一个有趣的练习!你会怎么做?

结论

通用输入/输出(GPIO)引脚允许 Raspberry Pi 与 led、开关、运动传感器、温度计等电子组件进行对话。它具有额外的模式,允许您连接到支持 SPI 或 I2C 标准的外设。

使用 GPIO,我们可以通过闪烁 led 或从轻触开关获取输入来扩展我们的程序。在接下来的几章中,我们将看到如何使用 GPIO 进行游戏输入和输出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值