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

原文:Python, PyGame and Raspberry Pi Game Development

协议:CC BY-NC-SA 4.0

二十二、游戏项目:记忆

游戏《记忆》是第一个 GPIO 项目。如图 22-1 中的成品板所示,该板设置有两排:一排有四个发光二极管,另一排有四个按钮。

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

图 22-1

两行试验板的建议布局:一行有四个 led,另一行有四个按钮

当序列在 led 上播放时,玩家通过按下 led 下方的行上的相应按钮来重复该序列。游戏开始时只有一个发光二极管,但随着游戏的进行,会增加到四个发光二极管。

布置试验板

我们将按照特定的顺序构建试验板:先是一排 led,然后是一排轻触开关。在每一行构建完成后,编写一个小脚本来测试组件并确保连接正确。

放置发光二极管

如图 22-1 所示排列发光二极管和轻触开关。发光二极管应对齐,使较长的一条腿(阳极)位于右侧。这没有技术上的原因,但它保持了设计的一致性,并确保电线将被放置在电路板上的正确孔中。图 22-2 显示了发光二极管应该如何连接。

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

图 22-2

从 GPIO 引脚到相应 led 的连接

每个阴极(较短的引脚)通过一个 330ω电阻连接到接地引脚 31。阳极(较长的引脚)连接到特定的 GPIO 引脚,如图所示。为了更容易地创建电路,我在试验板上的 31 号引脚(地)和–ve 轨之间放置了一个小跳线,如图 22-3 所示。将电阻连接到引脚 31 意味着将电阻连接到–ve 供电轨。

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

图 22-3

将接地引脚 31 连接到–ve 轨使连接电阻更容易

请注意,在较高的盒子中,试验板上只有三个裸露的孔与引脚 31(地)相连。通过使–ve 轨与引脚 31 连接,我们有效地连接了大约 50 个孔,具体取决于您的试验板的尺寸。

从图 22-3 中可以看出,较轻的电线连接到引脚 23、12、16 和 21。

测试电路

为了测试电路,我们将编写一个小的 Python 脚本来按顺序打开和关闭 led。在“pygamebook”的“projects”文件夹中创建一个名为“memory”的新文件夹在该文件夹中创建一个名为“ledtest.py”的新 Python 脚本。这是一个非常短的程序,轮流打开每个 LED 半秒钟,然后移动到下一个。

from gpiozero import LED
from time import sleep

导入 gpiozero 库来访问 LED 类。睡眠功能的导入时间。

leds = [ LED(23), LED(12), LED(16), LED(21) ]

创建 LED 对象阵列。请注意,这些数字是按照指示灯从左到右的顺序排列的。

while True:
    for led in leds:
        led.on()
        sleep(0.5)
        led.off()

该循环将保持程序运行,循环通过所有的灯,一个接一个地打开然后关闭它们。保存并运行程序。要退出程序,请按 Ctrl+C。

如果有任何问题,请检查接线以及哪些针脚连接到 led。

放置轻触开关

按钮或轻触开关放置在板上,并连接到 GPIO 引脚和地,如图 22-4 所示的(简化)图所示。

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

图 22-4

轻触开关连接到 GPIO 引脚和地

连接到引脚 31(地)的电线可以通过将电线从开关放置到–ve 轨来实现,就像我们之前对 led 所做的那样。

测试按钮电路

对于这个测试,我们将编写一个脚本来在按下开关时打开相应的 LED。从逻辑上看,GPIO 4 上的按钮会打开连接 GPIO 23 的 LED,GPIO 17 上的按钮会打开连接 GPIO 12 的 LED,以此类推。

我们的程序将使用 LED 和按钮类元组。

在“memory”文件夹中创建一个名为“buttontest.py”的新脚本,并输入以下代码:

from gpiozero import LED, Button
from time import sleep

该计划的导入。gpiozero 用于 LED 和按钮类,time 用于睡眠功能。

pair1 = (LED(23), Button(4))
pair2 = (LED(12), Button(17))
pair3 = (LED(16), Button(22))
pair4 = (LED(21), Button(6))

配对将每个 LED 与相应的按钮相匹配。元组的第零个元素是 LED,元组的第一个元素是按钮。记住:我们可以使用整数索引值来访问元组部分。

pairs = [ pair1, pair2, pair3, pair4 ]

为了使我们的程序简短,我们将使用一个配对列表并遍历它们。

while True:
    for pair in pairs:
        if pair[1].is_pressed:
            pair[0].on()
        else:
            pair[0].off()

当我们测试按钮时,循环保持程序运行。列表中的每一对都是循环的。测试按钮的“is_pressed”属性,如果按钮被按下,相应的 LED 就会亮起。否则,LED 将关闭。

保存程序并运行它。依次按住每个开关。相应的 LED 应该点亮。如果没有,请检查您的接线并重试。

电路的完整接线如图 22-5 所示。

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

图 22-5

显示所有连接的游戏完成图

现在我们已经建立并测试了电路,我们可以制作游戏了。

记忆游戏

这个程序的基本算法是这样的:

  • 随机选择一个、两个、三个、四个指示灯

  • 播放发光二极管序列

  • 等待玩家将序列输入回来

  • 显示祝贺/坏运气消息(在控制台中)

  • 继续下一个序列

在“内存”中启动一个名为“buttonled.py”的新 Python 脚本文件。这将包含我们项目的两个助手类。第一个辅助类是一个 LED/Button 聚合类,第二个是这个类的实例集合,它将处理随机序列的选择。

ButtonLED 和 ButtonLEDCollection 类

输入以下代码:

from gpiozero import LED, Button
import random

ButtonLED 和 ButtonLEDCollection 类的导入。gpiozero 是为 LED 和 Button 类导入的,random 是因为我们需要随机打乱 LED 列表,以使游戏每次都不同。

class ButtonLED(object):
    def __init__(self, ledPin, buttonPin):
        self.led = LED(ledPin)
        self.button = Button(buttonPin)

构造函数接受两个参数:连接到 LED 的 GPIO 管脚号和连接到轻触开关的 GPIO 管脚号。

    def on(self):
        self.led.on()

打开 LED。

    def off(self):
        self.led.off()

关闭 LED。

    def wait(self, timeout):
        self.button.wait_for_press(timeout)
        return self.button.is_pressed

wait()方法将等待,停止程序执行,直到按钮被按下。如果在“超时”值(秒)后没有按下按钮,程序将恢复。按钮的当前按下状态被返回给调用者。程序将使用该方法来确定玩家是否在主程序中以正确的顺序单击了按钮。

class ButtonLEDCollection(object):
    def __init__(self):
       led1 = ButtonLED(23, 4)
       led2 = ButtonLED(12, 17)
       led3 = ButtonLED(16, 22)
       led4 = ButtonLED(21, 6)
       self.items = [ led1, led2, led3, led4 ]

该构造函数创建按钮式对象,并将它们添加到名为“items”的内部列表中。

    def pick(self, count):
        leds = self.items
        random.shuffle(leds)
        picked = []
        for n in range(0, count):
            picked.append(leds[n])
        return picked

pick()方法打乱 led 并选择第一个“计数”项目。假设初始序列指向 GPIO 引脚 6、7、8 和 9。洗牌后可能是 7,6,9,8。选择前三个将返回 7、6 和 9,表示第二、第一和第四个 led。这种方法是为记忆游戏创建随机发光二极管序列的核心。

    def waitForClick(self):
        isPressed = False
        while not isPressed:
            for led in self.items:
                isPressed = isPressed or led.button.is_pressed

这将等待玩家按下任何轻触开关。这是一个阻塞呼叫,程序将无法继续,直到按下按钮。

if __name__=='__main__':
    from time import sleep
    collection = ButtonLEDCollection()
    leds = collection.pick(4)
    for led in leds:
        led.on()
        sleep(1)
        led.off()

为了测试这些类并确保一切正常,创建了一个小的测试存根。它创建 ButtonLEDCollection 类的一个实例,并挑选四个 led 逐个打开和关闭。保存并运行脚本。如果你没有看到四个发光二极管以随机的顺序闪烁,你应该检查程序和接线,以确保你有正确的接线和编码。在进入主程序之前,请执行此操作。

主程序

主程序是一个名为“memorygame.py”的新文件。创建这个新文件,并输入以下代码:

#!/usr/bin/python3
import sys
from time import sleep
from buttonled import ButtonLEDCollection

游戏的导入包括在前面部分创建的“buttonled.py”文件。只有两个导入的类,我们可以使用*,但是我选择在这个实例中显式命名 ButtonLEDCollection,因为它是唯一需要的类。

collection = ButtonLEDCollection()

创建 ButtonLEDCollection 类的实例。

print ("Welcome to the Game of Memory!")
print ("A sequence of LEDs will flash, ")
print ("you will be asked to repeat the")
print ("pattern. Press any button to start")

向玩家显示欢迎消息。尽管大多数操作都发生在试验板上,但控制台上的一些信息还是很有帮助的。

collection.waitForClick()

等待玩家按下任何轻触开关。

for n in range(1, 5):

请记住,虽然范围值是从 1 到 n,但实际上值是从 1 到 n-1,这意味着它将在数字 1-4 之间循环。

    leds = collection.pick(n)
    print ("Remember this sequence")
    for led in leds:
        led.on()
        sleep(1)
        led.off()

随机选择一系列发光二极管。闪现序列,并告诉玩家他们需要记住序列。

    print("Your turn!")
    for led in leds:
        if led.wait(1):
            led.on()
            sleep(0.5)
            led.off()
        else:
           print ("Missed! Game Over!")
           sys.exit()

现在轮到玩家了。led 对象被再次循环——记住,那些是 LED/按钮集合对象——并且按钮被测试。如果在 1 秒的给定时间内按下,则选择下一个 led 对象。否则,游戏结束。

print ("Congratulations!")

如果玩家正确记住了所有四个序列,则显示一条祝贺消息。保存文件。

通过键入以下命令运行该程序:

$ python3 memorygame.py

或者,更改脚本的执行模式并自行运行:

$ chmod +x memorygame.py
$ ./memorygame.py

游戏将开始,您将看到一个、两个、三个、最后四个随机的发光二极管。祝你好运!

完整列表 buttonled.py

buttonled.py 的完整列表有助于调试您可能遇到的任何问题:

from gpiozero import LED, Button
import random

class ButtonLED(object):
    def __init__(self, ledPin, buttonPin):
        self.led = LED(ledPin)
        self.button = Button(buttonPin)

    def on(self):
        self.led.on()

    def off(self):
        self.led.off()

    def wait(self, timeout):
        self.button.wait_for_press(timeout)
        return self.button.is_pressed

class ButtonLEDCollection(object):
    def __init__(self):
       led1 = ButtonLED(23, 4)
       led2 = ButtonLED(12, 17)
       led3 = ButtonLED(16, 22)
       led4 = ButtonLED(21, 6)

       self.items = [ led1, led2, led3, led4 ]

    def pick(self, count):
        leds = self.items
        random.shuffle(leds)
        picked = []
        for n in range(0, count):
            picked.append(leds[n])
        return picked

    def waitForClick(self):
        isPressed = False
        while not isPressed:
            for led in self.items:
                isPressed = isPressed or led.button.is_pressed

if __name__=='__main__':
    from time import sleep
    collection = ButtonLEDCollection()

    leds = collection.pick(4)
    for led in leds:
        led.on()
        sleep(1)
        led.off()

完整列表 memorygame.py

memorygame.py 的完整列表有助于调试您可能遇到的任何问题:

#!/usr/bin/python3

import sys
from time import sleep
from buttonled import ButtonLED, ButtonLEDCollection

collection = ButtonLEDCollection()

print ("Welcome to the Game of Memory!")
print ("A sequence of LEDs will flash, ")
print ("you will be asked to repeat the")
print ("pattern. Press any button to start")

collection.waitForClick()

for n in range(1, 5):
    leds = collection.pick(n)
    print ("Remember this sequence")
    for led in leds:
        led.on()
        sleep(1)
        led.off()
    print("Your turn!")
    for led in leds:
        if led.wait(1):
            led.on()
            sleep(0.5)
            led.off()
        else:
           print ("Missed! Game Over!")
           sys.exit()

print ("Congratulations!")

结论

这是一个有趣的小游戏,让你习惯用 Raspberry Pi 和 Python 编写硬件游戏。gpiozero 库使得访问 GPIO 引脚变得非常容易。

我总是建议在着手编写实际游戏之前,为你的电路创建测试程序,以证明它们能够工作。事实上,写测试真的很重要,你写的测试越多,你就能更好地证明你的程序将完成它所设定的目标。

为了增强记忆游戏,你可以用一个专用的开始按钮来开始游戏,而不是四个播放按钮。此外,你可以让玩家选择他们的技能水平。传递给 wait()方法的超时值可以更改;轻松 1.5 秒,正常 1 秒,困难 0.5 秒。

二十三、游戏项目:问答

这本书的最后一个项目是一个双人沙发问答游戏。玩家会遇到一系列选择题,他们必须选择正确的答案。游戏混合使用 PyGame 和电子设备;问题显示在监视器上,所有输入来自两对三个轻触开关。部分游戏画面如图 23-1 所示。

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

图 23-1

问答游戏的屏幕:飞溅,准备好,问题,和分数屏幕

在“pygamebook”“projects”文件夹中创建一个名为“quiz”的新文件夹这是我们将为这个项目编写的所有脚本的位置。

电子产品

对于这个游戏的电路,你需要以下:

  • 一块试验板

  • 六个轻触开关

  • 不同长度的电线

图 23-2 显示了该项目的电路图。它由两对三个轻触开关组成。每个轻触开关通过 Raspberry Pi 上的引脚 31 接地。玩家 1 的按钮连接到 GPIO 引脚 4、17 和 22,玩家 2 的按钮连接到 GPIO 引脚 5、6 和 13。

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

图 23-2

显示两组轻触开关的问答游戏电路图

一旦电路在试验板上建立并连接到 Raspberry Pi,我们将使用一个简短的程序来测试按钮。为此,我们的程序将使用 PyGame 点亮屏幕显示。

测试按钮

测试程序如图 23-3 所示,显示两组三个圆。当轻触开关被按下时,圆圈“亮起来”,也就是说,红点以更亮的颜色出现。

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

图 23-3

测试程序运行显示三个轻触开关已被按下

在“测验”文件夹中创建一个名为“buttontest.py”的新脚本,并输入以下内容:

#!/usr/bin/python3
import pygame, os, sys
from pygame.locals import *
from gpiozero import Button

PyGame 和 gpiozero 库的标准导入。

def drawButtonState(surface, button, pos):
    color = 32
    if button.is_pressed:
        color = 192
    pygame.draw.circle(surface, (color, 0, 0), pos, 35)

绘制按钮的状态。如果按下按钮,会显示一个明亮的圆圈。

def drawPlayerState(surface, buttons, startx):
    x = startx
    for b in buttons:
        drawButtonState(surface, b, (x, 240))
        x = x + 80

    return x

循环浏览给定的按钮,并检测每个按钮是否被按下。调用 drawButtonState。

pygame.init()
fpsClock = pygame.time.Clock()
surface = pygame.display.set_mode((640, 480))

初始化 PyGame 并创建一个屏幕和时钟。

player1 = [ Button(4), Button(17), Button(22) ]
player2 = [ Button(5), Button(6), Button(13) ]

创建两个按钮列表。每个按钮都连接到指定 GPIO 引脚上的轻触开关。

background = (0, 0, 0) # Black

while True:
    surface.fill(background)

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

    x = 80
    x = drawPlayerState(surface, player1, x)
    x = x + 80
    drawPlayerState(surface, player2, x)

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

保存并运行程序。按住每个轻触开关。当按下开关时,屏幕上的彩色圆圈应该“亮起”。如果不是这样,请检查电路并再次尝试该程序。

如果电路工作正常,我们可以进入项目的视觉部分。这将需要我们创建一个状态机。

有限状态机

游戏中共有五种状态,它们的转换如图 23-4 所示。这五种状态是

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

图 23-4

问答游戏的有限状态机(FSM)

  • 闪屏–显示欢迎信息

  • 准备好–显示“准备好”信息

  • 选择问题–从列表中选择新问题

  • 显示问题–显示问题、三个选项和倒计时

  • 显示分数–显示双方玩家的当前分数

  • 游戏结束–这与“显示分数”状态相同,但包含一个显示谁赢得了整个游戏的指示器

因为“显示分数”和“游戏结束”非常相似,我们只需要为这两种状态创建一个类,以及“准备好”和“闪屏”状态。表 23-1 显示了状态之间的移动规则,也称为状态转换。

表 23-1。

游戏状态、转换规则和职业

|

状态

|

次状态

|

班级

|

转换条件

|
| — | — | — | — |
| 启动画面 | 准备好 | 标题文本屏幕 | 其中一个玩家按下了触动开关 |
| 准备好 | 选择问题 | 标题文本屏幕 | 在一定的持续时间后自动移动到下一个状态 |
| 选择问题 | 显示问题或游戏结束 | 选择一个问题 | 自动移动到下一个状态。如果没有更多的问题,下一个状态是“游戏结束” |
| 显示问题 | 显示分数 | 显示问题 | 在时间限制(倒计时)达到零时,或者双方玩家都选择了答案后,自动进入下一个状态 |
| 显示分数 | 选择问题 | ShowScore | 在一定的持续时间后自动移动到下一个状态 |
| 游戏结束 | 无-游戏结束 | ShowScore | 没有条件。游戏结束 |

制作游戏

除了前面提到的状态之外,还有一些额外的类需要构建。这些是

  • 问题反序列化

  • 基本状态类

  • 游戏赛跑者

  • UI 助手类

我们将依次了解每一项。

这些问题

测验的问题来自于 Pub 测验问题 HQ ( https://pubquizquestionshq.com/ ),这是一个免费开放的问题资源。这些问题被格式化在一个网页上,所以我花了一些时间将它们组织成一个 JSON 文件。生成的数据文件应作为“questions.json”保存到“测验”文件夹中:

{
      "questions":
      [
            {
                  "question": "New York City Hall is in which Borough?",
                  "answer": "Manhattan",
                  "answers": [
                        "Queens",
                        "Brooklyn"
                  ]
            },
            {
                  "question": "Which was the first baseball team                                in Texas to make it to the World                                Series?",
                  "answer": "Houston Astros",
                  "answers": [
                        "Houston Oilers",
                        "Texas Rangers"
                  ]
            },
            {
                  "question": "Dwight D. Eisenhower was President from 1953 to 1961, but who was his Vice President?",

                  "answer": "Richard Nixon",
                  "answers": [
                        "John Kennedy",
                        "Lyndon Johnson"
                  ]
            },
            {
                  "question": "Which was the most successful NFL team of the decade beginning in Jan 2000 with 4 Super Bowl wins?",
                  "answer": "New England Patriots",
                  "answers": [
                        "Buffalo Bills",
                        "San Diego Chargers"
                  ]
            },
            {
                  "question": "Why was there no World Series played in 1994?",
                  "answer": "Player's strike",
                  "answers": [
                        "No one bought tickets",
                        "Ban on baseballs"
                  ]
            },
            {
                  "question": "Lansing is the state capital of which northern state in America?",

                  "answer": "Michigan",
                  "answers": [
                        "Ilinois",
                        "Wisconsin"
                  ]
            },
            {
                  "question": "As of 2013 the most widely circulated newspaper in the USA was The Wall Street Journal. Which company owns it?",
                  "answer": "News Corporation

                  "answers": [
                        "Chicago Tribune",
                        "Conde Nast"
                  ]
            },
            {
                  "question": "Out of which city were Aerosmith formed?",
                  "answer": "Boston",
                  "answers": [
                        "New York",
                        "Los Angeles"
                  ]
            },
            {
                  "question": "Which future president gained national fame through his role in the War of 1812, most famously where he won a decisive victory at the Battle of New Orleans?",
                  "answer": "Andrew Jackson",
                  "answers": [
                        "George Washington",
                        "Abraham Lincoln"
                  ]

            },
            {
                  "question": "Born in Massachusetts, which painter's most famous work is 'Arrangement in Grey and Black No.1'?",
                  "answer": "James Abbott McNeill Whistler",
                  "answers": [
                        "Andy Warhol",
                        "Phillipe Stark"
                  ]
            }
      ]
}

JSON 文件被格式化为一个带有名为“questions”的列表属性的对象。数组中的每个对象都具有以下属性:

  • 问题–问题的文本

  • 答案–问题的正确答案

  • 答案–不正确答案的列表

我选择使用一个函数来创建一系列问题。该函数从“questions.json”文件中加载问题,并填充“Question”对象列表。

创建一个名为“questions.py”的新文件,并输入以下内容:

#!/usr/bin/python3
import json
import random

JSON 序列化/反序列化的导入。随机导入将用于随机化问题和答案的顺序

class Question(object):
    def __init__(self, jsonQuestion):
        self.question = jsonQuestion['question']
        self.answers = jsonQuestion['answers']
        self.answer = jsonQuestion['answer']
        self.answers.append(jsonQuestion['answer'])
        random.shuffle(self.answers)
        index = 0
        for a in self.answers:
            if a == jsonQuestion['answer']:
                self.answerIndex = index
            index = index + 1

“问题”类用于存储问题文本、正确答案和其他建议。正确答案的索引也被存储。这将使确定玩家是否选择了正确答案变得更容易一些;第一个按钮映射到第一个选择,依此类推。为了让游戏更有趣,每次玩这个游戏时,答案都会用“random.shuffle()”方法进行洗牌。这个简便的方法打乱了列表的元素。我们将在下面的“loadQuestions()”函数中看到它的使用。

def loadQuestions(filename):
    f = open(filename)
    questionFile = json.load(f)
    f.close()

将问题文件的全部内容载入内存。

    questions = []
    for q in questionFile['questions']:
        questions.append(Question(q))

对于文件中的每个问题,创建一个“问题”类的新实例,并将其附加到“问题”列表中。

    random.shuffle(questions)
    return questions

一旦所有的问题都被添加到列表中,再次使用“random.shuffle()”方法对问题进行重新排序,这样就不会有两个游戏是相同的。

if __name__ == '__main__':
    questions = loadQuestions("questions.json")
    for q in questions:
        print(q.question)
        print("Answer index %d" % q.answerIndex)
        for a in q.answers:
            if a == q.answer:
                print("\t* %s" % a)
            else:
                print("\t%s" % a)

为了测试代码是否运行,我在文件的底部添加了一个测试存根。它加载到“questions.json”文件中,并显示问题和答案。正确答案标有星号(*)。

保存文件并运行它。在运行它之前,您必须添加执行位:

$ chmod +x questions.py
$ ./questions.py

您应该会看到屏幕上显示的问题列表。如果没有,请检查代码。

UI 助手类

UI 助手类包含在一个文件中。这些类别是

  • 文本-基本文本组件

  • 问题–显示问题和答案

  • 倒计时–显示一个从 30 秒倒计时到 0 的进度条

创建一个名为“ui.py”的新文件,并输入以下文本:

import pygame
from pygame.locals import *

导入 PyGame 模块。

class Text(object):
    def __init__(self, size, colour):
        self.size = size
        self.colour = colour
        self.font = pygame.font.Font(None, size)

    def draw(self, surface, msg, pos, centred = False):
        x, y = pos
        tempSurface = self.font.render(msg, True, self.colour)
        if centred:
            x = x - tempSurface.get_width() / 2
            y = y + tempSurface.get_height() / 4
            pos = (x, y)
        surface.blit(tempSurface, pos)

Text 类是现有 PyGame 字体类的包装。它使文本在屏幕上的定位更容易,并提供了一种方便的方式来绘制以特定点为中心的文本。

class QuestionText(object):
    def __init__(self):
        self.questionText = Text(32, (255, 255, 0))
        self.answerText = Text(32, (255, 255, 255))
        self.disabledText = Text(32, (56, 56, 56))

QuestionText 类的构造函数。这将创建三个单独的文本实例:一个用于问题文本,一个用于答案文本,一个用于禁用状态。当这一轮结束时,正确答案会突出显示。禁用文本用于得出两个不正确的答案。

    def draw(self, surface, question, answer, answers, showAnswer = False):
        y = 64
        maxWidth = 60
        lineHeight = 32
        if len(question) > maxWidth:
            question.split(" ")
            temp = ""
            for word in question:
                temp = temp + word
                if len(temp) > maxWidth:
                    pos = (400, y)
                    self.questionText.draw(surface, temp, pos, True)
                    temp = ""
                    y = y + lineHeight
            self.questionText.draw(surface, temp, (400, y), True)
        else:
            self.questionText.draw(surface, question, (400, y), True)

如果问题文本长于屏幕宽度,它将被拆分成单独的单词。每个单词都被添加到列表中,直到达到最大宽度。然后,该文本被绘制到屏幕上。然后处理剩余的文本,直到显示出整个问题。如果问题文本小于屏幕宽度,则正常显示。

        y = y + lineHeight * 2
        label = "A"
        for a in answers:
            font = self.answerText
            if showAnswer and a != answer:
                font = self.disabledText

            font.draw(surface, "%s. %s" % (label, a), (100, y), False)
            labelChar = ord(label)
            labelChar = labelChar + 1
            label = chr(labelChar)
            y = y + 40

每个答案前面都会显示 A、B 或 C。为了达到这种“效果”,我们必须首先将当前标签转换成一个数字——这就是“ord()”函数的作用。它查找 ASCII(美国信息交换标准代码)表,并根据字符返回一个数字。第一次运行循环时,label = 'A ‘和 so ord()将返回 65,因为’ A '位于 ASCII 表的第 65 位。该值递增到下一个字符,因此 65 将变成 66,并使用“chr()”函数将其转换为一个字符。ASCII 码中的 66 是 b。

class Countdown(object)

:
    def __init__(self, seconds, pos, width, height, innerColour, borderColour, text):
        self.maxSeconds = seconds
        self.seconds = seconds
        self.pos = pos
        self.width = width
        self.height = height
        self.finished = False
        self.text = text
        self.innerColour = innerColour
        self.borderColour = borderColour
        self.fullRect = Rect(pos, (width, height))
        self.rect = Rect(pos, (width, height))

这是一个相当长的构造函数!这些参数将被用来绘制一个进度条形状的倒计时器,它在屏幕上停留的时间越长越短。

    def draw(self, surface):
        pygame.draw.rect(surface, self.innerColour, self.rect)
        pygame.draw.rect(surface, self.borderColour, self.fullRect, 2)

要绘制进度条,我们将使用 PyGame 提供的’ draw.rect()'方法。它可以用两种方法之一绘制:填充或带边框。进度条的“内部”将被绘制成一个填充的矩形,进度条的“外部”将被绘制成一个边框。

倒计时的当前大小从“self.rect”中提取,完整的矩形“self.fullRect”被绘制在顶部,如图 23-5 所示。

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

图 23-5

问答游戏的进度条

        x, y = self.pos
        x = x + self.width / 2
        pos = (x, y)
        self.text.draw(surface, "%02d" % self.seconds, pos, True)

剩余的秒数绘制在进度条的顶部。

    def reset(self):
        self.finished = False
        self.seconds = self.maxSeconds

每次显示问题时重置倒计时。

    def update(self, deltaTime):
        if self.seconds == 0:
            return

        self.seconds = self.seconds - deltaTime
        if self.seconds < 0:
            self.seconds = 0
            self.finished = True
        progressWidth = self.width * (self.seconds / self.maxSeconds)
        self.rect = Rect(self.pos, (progressWidth, self.height))

通过减少“self.seconds”中的当前秒数来更新倒计时。如果秒数达到 0,那么我们不更新。如果定时器到达零,则‘self . finished’被设置为真。最后,为“draw()”方法计算并存储进度条内部的当前宽度。

保存文件。

游戏运行程序和基本状态类

游戏运行者是一个非常基本的框架类,它允许游戏在不同的状态之间转换。要创建编程接口,需要创建一个基本状态类。这也将用作所有其他状态类的基础。

“NullState”类将为游戏的 FSM 中的其他状态提供基础。“游戏玩家”类将

  • 初始化 PyGame

  • 更新当前状态

  • 绘制当前状态

游戏更新方法也将在各种状态之间转换。稍后我们将为程序编写一个主入口点,它将创建一个“GameRunner”类的实例。

创建一个名为“gamerunner.py”的新文件,并输入以下内容:

import pygame
from pygame.locals import *

PyGame 的进口。

class NullState(object):
    def update(self, deltaTime):
        return None

    def draw(self, surface):

        pass

    def onEnter(self):
        pass

    def onExit(self):
        pass

’ NullState '类是游戏中其他状态的基础。它包含四种方法,用于

  • 更新

  • 通知状态正在进入

  • 通知该州它正在被转移出去

class GameRunner(object):
    def __init__(self, dimensions, title, backColour, initialState):
        self.state = initialState
        self.clock = pygame.time.Clock()
        self.backColour = backColour
        self.surface = pygame.display.set_mode(dimensions)
        pygame.display.set_caption(title)

初始化 PyGame 并创建一个时钟。这将创建显示并设置窗口的标题。

    def update(self):
        deltaTime = self.clock.tick(30) / 1000.0
        if self.state != None:
            self.state = self.state.update(deltaTime)

        return self.state

计算从上次运行该方法到现在的时间,并存储在“deltaTime”中时间是以毫秒为单位的,所以为了把它变成秒,我们要除以 1000。调用当前状态的“update()”方法。状态的“update()”方法返回要转换到的下一个状态。当前状态被返回给调用者。调用者将是我们后面要写的主程序。

    def draw(self):
        self.surface.fill(self.backColour)
        if self.state != None:
            self.state.draw(self.surface)

        pygame.display.update()

这将清除主表面,并获取当前状态以在顶部绘制自己,然后更新显示。

保存文件。

玩家输入

如果没有玩家的参与,我们将会制作电影!对于这个游戏,玩家的输入是使用“PlayerController”类捕获的。这个类也包含玩家当前的分数。创建一个名为“playercontroller.py”的新文件,并输入以下文本:

from gpiozero import Button

gpiozero 库的导入。

class PlayerController(object):
    def __init__(self, pins):
        self.buttons = []
        self.score = 0
        for pin in pins:
            self.buttons.append(Button(pin))

“PlayerController”类的构造函数。请注意,它从传递给它的“pin”列表中创建了一个按钮列表。

    def anyButton(self):
        for button in self.buttons:
            if button.is_pressed:
                return True

        return False

方法来确定是否按下了任何按钮。

    def playerChoice(self):
        index = 0
        for button in self.buttons:
            if button.is_pressed:
                return index
            index = index + 1

        return -1

方法来确定玩家的答案选择。如果玩家没有选择,这个方法返回–1,或者玩家按下的按钮的“self.buttons”列表中的索引。

州级

将为游戏中的状态创建以下类:

  • 选择一个问题

  • 标题文本屏幕

  • 显示问题

  • ShowScore

每个游戏状态都是重入。这意味着在程序执行期间,状态可以运行任意次。当通过调用“onEnter()”方法进入每个状态时,以及当通过调用“onExit()”方法不再是当前状态时,每个状态都会被告知。

当您创建自己的状态时,应该在“onEnter()”方法中执行状态的设置代码,并且应该在“onExit()”方法中执行拆卸(清理)操作。

分离阶级和国家

有限状态机(FSM)的状态是一个类的实例。没有必要创建多个执行相同或相似操作的类,因为它们代表不同的状态。在这个游戏中有两种使用相同职业的方法:

  • header text Screen–由“准备就绪”和“闪屏”状态使用

  • Show Score–由“显示分数”和“游戏结束”状态使用

当我们创建主文件时,将再次讨论这个主题。

维护游戏状态

游戏的当前状态分为两部分:当前正在执行的动作和动作正在处理的数据。数据存储在当前问题和每个玩家的控制器中。我们已经为玩家设置了单独的职业(“玩家控制器”),但是我们需要为当前的问题设置一个职业。创建名为“currentquestion.py”的新文件。这个文件中有一个当前显示问题的类定义。该信息将由“选择问题”状态改变,并由“显示问题”状态显示。

应该注意的是,正如我们将在后面看到的,其他状态不需要知道当前问题,因此没有给出该数据。

在“currentquestion.py”中输入以下代码:

class CurrentQuestion(object):
    def __init__(self):
        self.question = ""
        self.answer = ""
        self.answerIndex = -1
        self.answers = []

仅此而已;只是当前问题的信息。保存文件。

选择问题类

“选择问题”状态选择创建一个名为“选择问题. py”的新文件。这个类将用于从问题列表中选择当前的问题。

from gamerunner import NullState

“ChooseQuestion”类扩展了“NullState ”,因此我们必须将“NullState”导入到该文件中。

class ChooseQuestion(NullState):
    def __init__(self, nextState, gameOverState, currentQuestion, questions):
        self.questions = questions
        self.nextState = nextState
        self.gameOverState = gameOverState
        self.current = -1
        self.currentQuestion = currentQuestion

构造函数接受四个参数。第一个是默认的游戏状态,如果有另一个问题要转换到这个状态。正如我们从表 23-1 中看到的,这通常是“展示问题”状态。但是,如果达到“游戏结束”条件,游戏将转换到“游戏结束”状态。

“当前问题”是在维护游戏状态中谈到的游戏状态的实例。最后一个参数是从包含问题的 JSON 文件中加载的“问题”实例列表。

    def update(self, deltaTime):
        self.current = self.current + 1
        if self.current == len(self.questions):
            self.currentQuestion.question = "
            self.currentQuestion.answer = "
            self.currentQuestion.answerIndex = -1
            self.currentQuestion.answers = []
            return self.gameOverState
        else:
            question = self.questions[self.current]
            self.currentQuestion.question = question.question
            self.currentQuestion.answer = question.answer
            self.currentQuestion.answers = question.answers
            self.currentQuestion.answerIndex = question.answerIndex
        return self.nextState

索引“self.current”递增。如果该值等于“self.questions”的长度,则游戏结束。否则,设置当前问题的数据并返回“nextState”。

“ChooseQuestion”类没有“draw()”方法,因此我们不需要在这里为它添加重写方法;“NullState”已经提供了一个基本的“draw()”方法。保存文件。

HeaderTextScreen 类

“闪屏”和“准备就绪”状态都使用 HeaderTextScreen 向玩家显示信息文本。在闪屏的情况下,游戏的名称与“按任意按钮”一起显示以继续。使用“准备好”,显示文本“准备好”。这两种状态的区别在于闪屏需要玩家输入,而“准备好”实例在设定的持续时间后会自动转换到下一个状态。

创建一个名为“headertextscreen.py”的新文件,并输入以下文本:

from ui import *
from playercontroller import *
from gamerunner import NullState

必需的进口。

class HeaderTextScreen(NullState):
    def __init__(self, nextState, player1, player2, waitTime = 0):
        self.nextState = nextState
        self.player1 = player1
        self.player2 = player2
        self.big = Text(128, (255, 192, 0))
        self.small = Text(36, (255, 255, 255))
        self.waitTime = waitTime
        self.currentTime = 0
        self.header = ""
        self.subHeader = ""

构造函数接受四个参数:下一个状态、玩家控制器和等待时间。如果等待时间为零,则假设需要一些玩家交互,也就是说,其中一个玩家必须按下按钮才能移动到下一个状态。

    def setHeader(self, header):
        self.header = header

设置标题文本。

    def setSub(self, subHeader):
        self.subHeader = subHeader

设置副标题文本。

    def setNextState(self, nextState):
        self.nextState = nextState

设置下一个状态。

    def update(self, deltaTime):
        if self.waitTime > 0:
            self.currentTime = self.currentTime + deltaTime
            if self.currentTime >= self.waitTime:
                return self.nextState
        elif self.player1.anyButton() or self.player2.anyButton():
            return self.nextState
        return self

这将执行状态转换。如果“self.waitTime”大于零,则它是自动倒计时版本,否则它是用户控制的状态版本。

    def draw(self, surface):
        self.big.draw(surface, self.header, (400, 200), True)
        self.small.draw(surface, self.subHeader, (400, 300), True)

保存文件。

ShowQuestion 类

“显示问题”状态显示当前问题、答案和倒计时。当倒计时到达 0(从 30 秒开始)或两个玩家都做出选择时,状态转换到下一个状态。国家利用“游戏者控制器”;每个玩家和“当前问题”实例各一个。

创建名为“showquestion.py”的新文件,并输入以下文本:

from gamerunner import NullState
from ui import Text, QuestionText, Countdown

正在为“NullState”类导入“gamerunner”文件。该类使用“ui”中的“Text”、“Countdown”和“QuestionText”类

class ShowQuestion(NullState):
    def __init__(self, nextState, currentQuestion, player1, player2):
        self.nextState = nextState
        self.player1 = player1
        self.player2 = player2
        self.player1Choice = -1
        self.player2Choice = -1
        self.currentQuestion = currentQuestion
        self.showAnswer = False
        self.endCount = 3
        self.questionText = QuestionText()

        text = Text(32, (255, 255, 255))
        self.countdown = Countdown(30, (80, 560), 640, 32, (128, 0, 0), (255, 0, 0), text)

ShowQuestion 的构造函数有四个参数:要转换到的下一个状态、当前的问题实例和两个从它们那里获取输入的播放器控制器。

    def calcScore(self):
        if self.player1Choice == self.currentQuestion.answerIndex:
            self.player1.score = self.player1.score + 1
        if self.player2Choice == self.currentQuestion.answerIndex:
            self.player2.score = self.player2.score + 1

计算玩家分数的辅助函数。

    def update(self, deltaTime):
        if self.player1Choice == -1:
            p1 = self.player1.playerChoice()
            if p1 >= 0:
                self.player1Choice = p1

        if self.player2Choice == -1:
            p2 = self.player2.playerChoice()
            if p2 >= 0:
                self.player2Choice = p2

        if self.player1Choice >= 0 and self.player2Choice >= 0:
            self.showAnswer = True

        if not self.showAnswer:
            self.countdown.update(deltaTime)
            if self.countdown.finished:
                self.showAnswer = True
        else:
            self.endCount = self.endCount - deltaTime
            if self.endCount <= 0:
                self.calcScore()
                return self.nextState

        return self

如果“self.showAnswer”为 False,则 update 方法会启动倒计时计时器。当倒数计时器到达零或者两个玩家都做出选择时,“self.showAnswer”被设置为真。一旦玩家选择了答案,他们就不能更改。

    def draw(self, surface):
        self.questionText.draw(surface, self.currentQuestion.question, self.currentQuestion.answer, self.currentQuestion.answers, self.showAnswer)
        if not self.showAnswer:
            self.countdown.draw(surface)

绘制问题和答案,将“self.showAnswer”字段值传递给 questionText 的“Draw()”方法以突出显示正确的答案。如果倒计时激活,显示出来。

    def onExit(self):
        self.endCount = 3
        self.showAnswer = False
        self.countdown.reset()

退出时清除当前状态。

    def onEnter(self):
        self.player1Choice = -1
        self.player2Choice = -1

在进入状态时设置玩家数据。

保存文件。

ShowScore 类

“显示分数”和“游戏结束”状态都属于这个类。在每个问题之间,会显示玩家的分数。显示“游戏结束”屏幕时,会显示得分和“赢家”或“平局”。“赢家”标签显示在赢得游戏的玩家下方。

对于这个文件,我创建了一个简单的测试存根来验证屏幕上文本的位置。

创建一个名为“showscore.py”的新文件,并输入以下文本:

#!/usr/bin/python3

import pygame
from pygame.locals import *
from gamerunner import NullState
from ui import Text

“ShowScore”类所需的导入。

class ShowScore(NullState):

    def __init__(self, nextState, player1, player2, showWinner = False):
        self.nextState = nextState
        self.player1 = player1
        self.player2 = player2
        self.counter = 3
        self.showWinner = showWinner
        self.scoreText = Text(300, (255, 255, 0))
        self.playerText = Text(128, (255, 255, 255))

“ShowScore”构造函数有四个参数。第一个是要转换到的下一个状态,接下来是第一个和第二个玩家的控制器。这些是“PlayerController”类的“score”字段所必需的。最后,“showWinner”参数用于显示“赢家”或“平局”,这取决于当所有问题都被问完时游戏的结束状态。

    def update(self, deltaTime):
        self.counter = self.counter - deltaTime
        if self.counter <= 0:
            return self.nextState

        return self

分数屏幕仅在特定时间内显示。一旦该时间到期,状态转换到下一个。

    def draw(self, surface):
        self.playerText.draw(surface, "Player 1", (200, 85), True)
        self.playerText.draw(surface, "Player 2", (600, 85), True)

        self.scoreText.draw(surface, str(self.player1.score), (200, 150), True)
        self.scoreText.draw(surface, str(self.player2.score), (600, 150), True)

        if self.showWinner:
            winner = "WINNER!"
            pos = 200
            if self.player1.score == self.player2.score:
                winner = "TIE!"
                pos = 400
            elif self.player2.score > self.player1.score:
                pos = 600
            self.playerText.draw(surface, winner, (pos, 400), True)

画屏幕。

    def onEnter(self):
        self.counter = 3

进入该状态时,将当前计数器设置为 3 秒。

if __name__ == '__main__':
    import sys
    class P(object):
        def __init__(self, s):
            self.score = s

    pygame.init()
    fpsClock = pygame.time.Clock()
    surface = pygame.display.set_mode((800, 600))

    score = ShowScore(None, P(55), P(10), True)

    background = (0, 0, 0) # Black

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

        deltaTime = fpsClock.tick(30) / 1000.0
        score.draw(surface)
        pygame.display.update()

测试存根。这将显示“游戏结束”状态。保存并运行文件以查看。如果您想看到“显示分数”屏幕,请更改以下行:

    score = ShowScore(None, P(55), P(10), True)

    score = ShowScore(None, P(55), P(10))

主文件

主文件实际上只有几行代码,其中大部分是设置有限状态机。创建一个名为“quiz.py”的新文件,并输入以下文本:

#!/usr/bin/python3

import pygame
from gamerunner import GameRunner
from questions import *
from headertextscreen import HeaderTextScreen
from choosequestion import ChooseQuestion
from playercontroller import PlayerController
from showquestion import ShowQuestion
from showscore import ShowScore
from currentquestion import CurrentQuestion

程序的所有导入。

pygame.init()

player1 = PlayerController([4, 17, 22])
player2 = PlayerController([5, 6, 13])
currentQuestion = CurrentQuestion()

初始化 PyGame 并设置存储在“PlayerController”实例和“CurrentQuestion”实例中的游戏状态数据。

questions = loadQuestions("questions.json")

从 JSON 文件中加载问题。

showQuestion = ShowQuestion(None, currentQuestion, player1, player2)
gameOver = ShowScore(None, player1, player2, True)
chooseQuestion = ChooseQuestion(showQuestion, gameOver, currentQuestion, questions)
showScore = ShowScore(chooseQuestion, player1, player2)
showQuestion.nextState = showScore

“ShowQuestion”、“ShowScore”和“ChooseQuestion”类用于构建游戏中使用的一些状态。由于状态的创建,无法为“ShowQuestion”设置初始状态,而是手动设置了“showQuestion”实例的“nextState”,并且没有将任何状态传递给“ShowQuestion”的构造函数

interstitial = HeaderTextScreen(chooseQuestion, player1, player2, 3)
interstitial.setHeader("Get Ready!")
interstitial.setSub("")
splashScreen = HeaderTextScreen(interstitial, player1, player2)
splashScreen.setHeader("QUIZ!")
splashScreen.setSub("Press any button to start")

“准备好!”的间隙(游戏间隙)屏幕还有闪屏。注意,我们没有为闪屏和“准备好!”创建单独的类,它只使用了两个独立的“HeaderTextScreen”实例

当我们从一种状态转换到另一种状态时,我们从一个类的一个实例转换到另一个。所以没有必要为每个状态编写完全独立的类。

game = GameRunner((800, 600), "Quiz", (0, 0, 0), splashScreen)

game runner 的实例被设置为 800×600 大小的窗口,背景为黑色(0,0,0),初始状态为闪屏实例“splash screen”

lastState = None
while game.state != None:
    nextState = game.update()
    if nextState != lastState:
        if game.state != None:
            game.state.onExit()
        if nextState != None:
            nextState.onEnter()
        lastState = nextState
    game.draw()

pygame.quit()

主程序循环包括调用游戏的“update()”和“draw()”方法。可以认为这个循环应该放在“GameRunner”的“run()”方法中,我的意思是它在名字中。我将把它作为读者的一个练习;在运行循环的“GameRunner”上创建一个名为“run()”的方法。

保存文件。

玩游戏

玩这个游戏你需要一个对手;这毕竟是一个基于沙发的问答游戏。请坐在沙发上,运行“quiz.py”文件。您需要为文件设置执行位:

$ chmod +x quiz.py

然后运行它:

$ ./quiz.py

一旦游戏开始,你们中的一个人按下试验板上的按钮开始测验。试着回答每个出现的问题。如果你在 30 秒内没有回答,你将失去这一分。获胜者是比赛结束时得分最多的人。祝你好运!

结论

这是一个有趣的游戏,展示了如何构建基于 PyGame 的游戏,与电子组件进行交互。您可以重写早期项目(如 Brick、Snake 和 Invaders)的输入例程,使用轻触开关代替计算机按键进行输入。

二十四、总结

到目前为止,您应该对 Python 语言以及 PyGame 库有了很好的理解。随着游戏包括在本文中,你应该有一个很好的理解如何创建一个视频游戏。的确,有了一个好主意,你应该有足够的知识来自己制作一个游戏!在本书中,我们介绍了播放器输入、显示图形、播放声音、在屏幕上移动字符,以及以读写 GPIO 引脚的形式输入和输出的替代形式。

除了游戏,我们还看了面向对象编程和一些相关的设计模式,如有限状态机(FSM)和模型视图控制器(MVC)。这些将有助于你构建自己的游戏,如果你想更进一步,可能会在游戏行业发展。

希望到了这个阶段,你应该对 Python 语言本身和 PyGame 有了很好的理解。由于本文中包含了三个游戏(砖块、蛇和入侵者),你已经理解了制作一个视频游戏需要什么。有了这里的一切,你应该有足够的时间来创造你自己的。只需要一个好主意!

现在去哪里?既然你有编程缺陷(原谅双关语),天空是极限。也许你想学 C++和做一些 3D 游戏。

如果你想了解更多关于 Python 的内容,你应该前往 https://docs.python.org/ 。PyGame 在 www.pygame.org/wiki/index 也有完整的文档。

即使你没有决定把编程作为一份全职工作,把制作游戏作为一种爱好仍然是一件非常有趣的事情。

可以考虑参加类似 Ludum Dare ( https://ldjam.com )或者其他在 https://itch.io/jams 上市的游戏 jam。在短时间内,通常是在一个周末,开发一个游戏是非常有趣的。你甚至可以带朋友来帮你。谁知道呢,你甚至可能会创造出下一个“核王座”、“超级肉仔”或“星谷”

我希望你喜欢这本书,无论你选择做什么,我都希望你能从中得到乐趣。

编码快乐!

  • 15
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值