网站搜索

构建 Python 海龟游戏:太空侵略者克隆


在本教程中,您将使用 Python 的 turtle 模块构建 Space Invaders 克隆。 《太空入侵者》这款游戏不需要任何介绍。原版游戏于 1978 年发布,是有史以来最受认可的视频游戏之一。无可否认,它定义了自己的视频游戏类型。在本教程中,您将创建该游戏的基本克隆。

您将用于构建游戏的 turtle 模块是 Python 标准库的一部分,它使您能够在屏幕上绘制和移动精灵。 turtle 模块不是游戏开发包,但它提供了有关创建 turtle 游戏的说明,这将帮助您了解视频游戏如何已建成。

在本教程中,您将学习如何:

  • 设计和构建经典视频游戏
  • 使用turtle模块创建动画精灵
  • 在基于图形的程序中添加用户交互
  • 创建游戏循环来控制游戏的每一帧
  • 使用函数来表示游戏中的关键操作

本教程非常适合熟悉 Python 核心主题并希望使用它们从头开始构建经典视频游戏的任何人。您无需熟悉 turtle 模块即可完成本教程。您可以通过单击下面的链接下载每个步骤的代码:

在下一部分中,您可以按照本教程中概述的步骤查看您将构建的游戏版本。

演示:Python Turtle Space Invaders 游戏

您将构建经典太空入侵者游戏的简化版本,并使用键盘上的按键控制激光炮。你可以通过按空格键从大炮发射激光,外星人会定期出现在屏幕顶部并向下移动。你的任务是在外星人到达屏幕底部之前将其射杀。当一名外星人到达底部时游戏结束。

当您完成本教程后,您的海龟游戏将如下所示:

在这里你可以看到这个游戏的主要玩法,激光炮来回移动并射击坠落的外星人。游戏还会在屏幕上显示经过的时间和被击落的外星人数量。

项目概况

在此项目中,您将首先创建包含游戏的屏幕。在每个步骤中,您将创建游戏组件,例如激光炮、激光器和外星人,并且您将添加制作正常游戏所需的功能。

要创建这个海龟游戏,您将完成以下步骤:

  1. 创建游戏屏幕激光炮
  2. 使用按键左右移动大炮
  3. 使用空格键发射激光
  4. 创建外星人并将其移至屏幕底部
  5. 确定激光何时击中外星人
  6. 当外星人到达底部时结束游戏
  7. 添加计时器得分
  8. 改进大炮的移动,使游戏更加流畅
  9. 设置游戏的帧速率

您将从空白屏幕开始,然后在完成本教程中的每个步骤时看到游戏一次一个地显示出来。

先决条件

要完成本教程,您应该熟悉以下概念:

  • 使用 for 循环和 while 循环重复代码
  • 使用 if 语句来控制不同条件下发生的情况
  • 定义函数来封装代码
  • 使用列表来存储多个项目

您无需熟悉 Python 的 turtle 即可开始本教程。不过,您可以阅读 turtle 模块的概述,以了解有关基础知识的更多信息。

如果您在开始之前不具备所有必备知识,也没关系!事实上,您可以通过继续并开始了解更多信息!如果遇到困难,您可以随时停下来查看此处链接的资源。

第 1 步:设置带有屏幕和激光炮的海龟游戏

如果没有发生所有动作的屏幕,你就不可能拥有一款游戏。因此,第一步是创建一个空白屏幕。然后,您可以添加精灵来代表游戏中的项目。在此项目中,您可以随时运行代码以查看游戏的当前状态。

您可以从以下链接中名为 source_code_step_1/ 的文件夹中下载代码,因为它将在此步骤结束时查看:

现在是使用 turtle 模块设置游戏并创建屏幕的第一步了。

创建屏幕

首先,创建一个新文件并导入 turtle 模块:

import turtle

turtle.done()

您调用turtle.done(),它会显示窗口并使其保持打开状态。此行应始终是使用 turtle 的任何程序中的最后一行。 这将防止程序在到达最后一行时终止。要停止该程序,您可以通过单击窗口右上角的关闭窗口图标来关闭该窗口,就像操作操作系统中的任何其他窗口一样。

尽管此代码创建并显示屏幕,但您可以在代码中显式创建屏幕并将其分配给变量名称。这种方法可以让您自定义屏幕的大小和颜色:

import turtle

window = turtle.Screen()
window.setup(0.5, 0.75)
window.bgcolor(0.2, 0.2, 0.2)
window.title("The Real Python Space Invaders")

turtle.done()

您使用浮点数作为 .setup() 的参数,它将屏幕尺寸设置为计算机屏幕的一部分。窗口的宽度是屏幕宽度的 50%,高度是屏幕高度的 75%。您可以尝试使用像素值来设置屏幕尺寸,方法是使用整数而不是浮点数作为 .setup() 中的参数。

turtle 模块中的默认颜色模式要求红色、绿色和蓝色分量在 [0, 1] 范围内浮动。您还应该更新标题栏中的文本。当您运行此代码时,您将看到以下屏幕:

此版本具有深色背景,但您可以使用自己喜欢的颜色自定义游戏。现在,您已准备好创建激光炮。

创建激光炮

就像没有屏幕就不可能有游戏一样,没有激光炮就不可能有太空侵略者克隆游戏。要设置大炮,您需要使用 turtle.Turtle() 创建一个 Turtle 对象。游戏中的每个独立项目都需要自己的 Turtle 对象。

当您创建一个 Turtle 对象时,它位于屏幕的中心并且面向右侧。默认图像是显示对象标题的箭头。您可以使用 Turtle 方法更改这些值:

import turtle

window = turtle.Screen()
window.setup(0.5, 0.75)
window.bgcolor(0.2, 0.2, 0.2)
window.title("The Real Python Space Invaders")

# Create laser cannon
cannon = turtle.Turtle()
cannon.penup()
cannon.color(1, 1, 1)
cannon.shape("square")

turtle.done()

大炮现在是一个白色的方块。您调用 cannon.penup() 以便 Turtle 对象在您移动时不会画线。运行代码时可以看到大炮:

但是,您希望大炮位于屏幕底部。以下是设置大炮新位置的方法:

import turtle

window = turtle.Screen()
window.setup(0.5, 0.75)
window.bgcolor(0.2, 0.2, 0.2)
window.title("The Real Python Space Invaders")

LEFT = -window.window_width() / 2
RIGHT = window.window_width() / 2
TOP = window.window_height() / 2
BOTTOM = -window.window_height() / 2
FLOOR_LEVEL = 0.9 * BOTTOM

# Create laser cannon
cannon = turtle.Turtle()
cannon.penup()
cannon.color(1, 1, 1)
cannon.shape("square")
cannon.setposition(0, FLOOR_LEVEL)

turtle.done()

您可以使用 window.window_height()window.window_width() 定义屏幕边缘,因为这些值在不同的计算机设置上会有所不同。屏幕中心的坐标为(0, 0)。因此,您将宽度和高度除以二,以获得四个边的x-y-坐标。

您还可以将 FLOOR_LEVEL 定义为底部边缘 y- 坐标的 90%。然后,使用 .setposition() 将大炮放置在该高度,使其从屏幕底部稍微升起。

请记住,这是 20 世纪 70 年代游戏的克隆,因此您所需要的只是简单的图形!您可以通过使用 .turtlesize() 将海龟的形状拉伸为不同大小的矩形来绘制大炮,并调用 .stamp() 将此形状的副本保留在屏幕。下面的代码仅显示了已更改的代码部分:

# ...

# Draw cannon
cannon.turtlesize(1, 4)  # Base
cannon.stamp()
cannon.sety(FLOOR_LEVEL + 10)
cannon.turtlesize(1, 1.5)  # Next tier
cannon.stamp()
cannon.sety(FLOOR_LEVEL + 20)
cannon.turtlesize(0.8, 0.3)  # Tip of cannon
cannon.stamp()
cannon.sety(FLOOR_LEVEL)

turtle.done()

当您运行此代码时,您将在屏幕底部看到大炮:

您的下一个任务是引入一些交互性,以便您可以左右移动大炮。

第 2 步:使用按键移动大炮

在上一步中,您编写了在屏幕上绘制大炮的代码。但游戏需要一个可以与之互动的玩家。现在是时候学习按键绑定了,这样你就可以使用键盘来移动大炮了。

您可以从以下链接中名为 source_code_step_2/ 的文件夹中下载代码,因为它将在此步骤结束时查看:

首先,您将熟悉动画中使用的关键技术。

移动海龟对象

您可以使用 window.onkeypress() 将函数绑定到按键。按键调用绑定到该键的函数。您可以定义两个函数来左右移动大炮,并将它们绑定到 LeftRight< /kbd> 键盘上的箭头键:

import turtle

CANNON_STEP = 10

# ...

def move_left():
    cannon.setx(cannon.xcor() - CANNON_STEP)

def move_right():
    cannon.setx(cannon.xcor() + CANNON_STEP)

window.onkeypress(move_left, "Left")
window.onkeypress(move_right, "Right")
window.onkeypress(turtle.bye, "q")
window.listen()

turtle.done()

您是否注意到您使用了不带括号的函数名称 move_leftmove_right 作为 .onkeypress() 的参数?如果添加括号,程序在执行带有 .onkeypress() 的行时调用该函数,并将该函数的返回值绑定到该键。函数 move_left()move_right() 返回 None,因此如果包含括号,箭头键将保持不活动状态。

移动大炮的函数使用 CANNON_STEP,您可以在代码顶部定义它。该名称使用大写字母书写,表明这是您用于配置游戏的常量值。尝试更改此值以查看这将如何影响游戏。

您还可以使用 window.listen() 将焦点移至屏幕,以便程序收集按键事件。默认情况下,turtle 模块在程序运行时不使用计算资源来侦听按键。调用 window.listen() 会覆盖此默认值。如果运行 turtle 游戏时按键不起作用,请确保包含 .listen(),因为使用 .onkeypress 时很容易忘记()

现在您已经熟悉了键绑定,您将添加一项功能,通过将 Q 键绑定到 turtle.bye 来退出程序。正如您对前面的函数所做的那样,即使它是一个函数,您也不会将括号添加到 turtle.bye 中。确保使用小写 q,因为如果在代码中使用大写字母,则需要按 Shift<span>+ Q 退出程序。

现在,当您运行程序时,您可以按左右箭头键来移动 Turtle 对象。

这是此阶段的输出:

这不完全是您想要的行为。请注意,当您按箭头键时,绘图中只有一部分会移动。这是代表 Turtle 对象的精灵。绘图的其余部分是绘制激光炮的代码部分中 .stamp() 调用的结果。

移动整个激光炮

每次移动激光炮时,您都可以清除之前的绘图并在新位置重新绘制激光炮。为了避免重复,您可以将绘制大炮的代码移至函数 draw_cannon() 中。在以下代码中,draw_cannon() 中的大部分行与之前版本中的行相同,但现在它们缩进了:

# ...

def draw_cannon():
    cannon.clear()
    cannon.turtlesize(1, 4)  # Base
    cannon.stamp()
    cannon.sety(FLOOR_LEVEL + 10)
    cannon.turtlesize(1, 1.5)  # Next tier
    cannon.stamp()
    cannon.sety(FLOOR_LEVEL + 20)
    cannon.turtlesize(0.8, 0.3)  # Tip of cannon
    cannon.stamp()
    cannon.sety(FLOOR_LEVEL)

def move_left():
    cannon.setx(cannon.xcor() - CANNON_STEP)
    draw_cannon()

def move_right():
    cannon.setx(cannon.xcor() + CANNON_STEP)
    draw_cannon()

window.onkeypress(move_left, "Left")
window.onkeypress(move_right, "Right")
window.onkeypress(turtle.bye, "q")
window.listen()

draw_cannon()

turtle.done()

当您调用 cannon.clear() 时,所有由 cannon 绘制的绘图都会从屏幕上删除。每次移动大炮时,您都可以在 move_left()move_right() 中调用 draw_cannon() 来重新绘制大炮。您还可以在程序的主要部分中调用 draw_cannon() 来在游戏开始时绘制大炮。

游戏现在的样子是这样的:

当您按下向左或向右箭头键时,整个激光炮就会移动。函数draw_cannon()删除并重新绘制组成大炮的三个矩形。

然而,大炮的移动并不平稳,如果你试图移动得太快,可能会出现故障,正如你在视频的第二部分中看到的那样。您可以通过控制 turtle 模块中何时在屏幕上绘制项目来解决此问题。

控制项目的显示时间

Turtle 对象移动时,turtle 模块会显示几个小步骤。当您将cannon从其初始中心位置移动到屏幕底部时,您可以看到方块向下移动到新位置。当您重画大炮时,Turtle 对象需要移动到多个位置、改变形状并创建该形状的印章。

这些操作需要时间,如果您在此绘制过程中按箭头键,Turtle 对象会在完成绘制之前移动。

当游戏中发生更多事件时,这个问题变得更加明显。现在是解决这个问题的好时机。您可以通过设置 window.tracer(0) 来防止屏幕上显示任何更改。

程序仍然会更改 Turtle 对象的坐标及其标题,但不会在屏幕上显示更改。您可以通过调用window.update()来控制屏幕何时更新。这使您可以控制何时更新屏幕。

您可以在创建屏幕后立即调用 window.tracer(0) 并在 draw_cannon() 中调用 window.update()

import turtle

CANNON_STEP = 10

window = turtle.Screen()
window.tracer(0)

# ...

def draw_cannon():
    # ...
    window.update()

# ...

现在,移动大炮时不会出现任何故障,并且从中心到底部的大炮初始位置不再可见。这是目前为止的海龟游戏:

在本教程的后面部分,您将改进大炮移动的流畅程度。在下一步中,您将解决另一个问题。请注意,在视频末尾,当按左箭头键多次时,您可以看到激光炮移出屏幕。你需要阻止大炮离开屏幕,这样它才能占据中心位置,保卫地球免受外星人入侵!

防止激光炮离开屏幕

每次玩家按下方向键之一向左或向右移动激光炮时,都会调用函数 move_left()move_right()。要检查激光炮是否到达屏幕边缘,可以添加 if 语句。您将使用一个排水沟来在大炮到达边缘之前停止它,而不是使用屏幕边缘的LEFTRIGHT

# ...

LEFT = -window.window_width() / 2
RIGHT = window.window_width() / 2
TOP = window.window_height() / 2
BOTTOM = -window.window_height() / 2
FLOOR_LEVEL = 0.9 * BOTTOM
GUTTER = 0.025 * window.window_width()

# ...

def move_left():
    new_x = cannon.xcor() - CANNON_STEP
    if new_x >= LEFT + GUTTER:
        cannon.setx(new_x)
        draw_cannon()

def move_right():
    new_x = cannon.xcor() + CANNON_STEP
    if new_x <= RIGHT - GUTTER:
        cannon.setx(new_x)
        draw_cannon()

# ...

现在,激光炮无法离开屏幕。当您将激光和外星人添加到您的海龟游戏中时,您将在以下步骤中使用类似的技术。

第三步:用空格键发射激光

如果激光炮不能发射激光,那么它就没任何用处。在此步骤中,您将创建可以通过按空格键发射的激光。您将学习如何将所有活动激光存储在列表中,以及如何在游戏的每一帧中向前移动每个激光。您还将开始研究控制每一帧中发生的情况的游戏循环。

您可以从以下链接中名为 source_code_step_3/ 的文件夹中下载代码,因为它将在此步骤结束时查看:

首先,您将使用 Turtle 对象来创建和存储激光。

创建激光器

您希望每次按 Space 时都能发射激光。由于您的代码需要存储所有存在的激光器,因此您可以为每个激光器创建一个 Turtle 并将其存储在列表中。您还需要一个创建新激光器的函数:

# ...

lasers = []

def draw_cannon():
    # ...

def move_left():
    # ...

def move_right():
    # ...

def create_laser():
    laser = turtle.Turtle()
    laser.penup()
    laser.color(1, 0, 0)
    laser.hideturtle()
    laser.setposition(cannon.xcor(), cannon.ycor())
    laser.setheading(90)
    # Move laser to just above cannon tip
    laser.forward(20)
    # Prepare to draw the laser
    laser.pendown()
    laser.pensize(5)

    lasers.append(laser)

# Key bindings
window.onkeypress(move_left, "Left")
window.onkeypress(move_right, "Right")
window.onkeypress(create_laser, "space")
window.onkeypress(turtle.bye, "q")
window.listen()

draw_cannon()

turtle.done()

空格键绑定到 create_laser(),它创建一个新的 Turtle 对象来表示激光,设置其初始属性,并将其附加到所有激光的列表中。为了避免使代码复杂化,本教程进行了一些简化,您可以在以下部分中阅读:

有两个简化可以防止代码变得过于复杂:

  1. 大炮的高度被硬编码为 20 像素。该值在 draw_cannon() 中用于放置组成大炮的矩形,并在 create_laser() 中用于将激光的起点放置在大炮的尖端。一般来说,您应该尽量避免对值进行硬编码。该值可以定义为屏幕高度的百分比,并对 draw_cannon() 进行适当的更改以设置用于绘制大炮的三个矩形的宽度和高度。
  2. 函数create_laser()修改程序全局范围内的列表。一般来说,函数最好返回值而不是更改现有全局变量的状态。但是,您将通过按键调用 create_laser(),这会阻止您访问返回的对象。

当您按 Space 时,程序会为每个激光创建新的 Turtle 对象。您可以在create_laser()中添加print(len(lasers)),以确保程序在您按空格键时创建新的激光。

在下一部分中,您将在游戏的每一帧中移动激光。然后,您将创建一个游戏循环来解释每一帧中发生的所有事情。

创建游戏循环来移动激光

游戏的结构可以分为三个部分:

  • 初始化:您可以在此处创建所需的对象及其初始状态以设置游戏环境。您还可以在此处包含游戏中需要发生的操作的函数定义。
  • 游戏循环:这是游戏的核心部分。它检查和处理用户输入,更改游戏中对象的状态和其他值(例如分数),并更新显示以反映更改。游戏循环之所以是循环,是因为它需要不断运行才能运行游戏。游戏循环的每次迭代代表游戏的一个帧。
  • 结束:一旦游戏循环结束,无论是玩家赢还是输,程序都可以立即结束。通常,游戏会显示结束屏幕,但它们也可以将任何输出保存到文件并为玩家提供选项,例如再次玩。

您已经处理了这个海龟游戏初始化的重要部分。现在,是时候处理游戏循环了。

游戏循环的结构取决于您使用的平台。在 turtle 模块中,您需要编写自己的 while 循环来更新游戏中对象的状态并检查事件,例如对象之间的碰撞。但是,其他事件是通过 turtle 方法(例如 .onkeypress())进行管理的。 turtle 程序运行一个事件循环,该循环在您调用 turtle.done() 时启动,并处理按键等事件。

您可以将使用 turtle 时所需的游戏循环与其他 Python 游戏包(例如 pygamearcade)中的游戏循环结构进行比较>。

到目前为止,在这个海龟游戏中,唯一移动的物品是大炮,您可以使用箭头键移动它。其他时间大炮不会移动。

然而,在大多数游戏中,有些物品需要连续移动或执行其他动作。在这里,当您按下空格键时创建的激光需要在游戏的每一帧中向上移动固定的量。因此,现在您将创建一个 while 循环来进行每个帧中发生的这些更改:

import turtle

CANNON_STEP = 10
LASER_LENGTH = 20
LASER_SPEED = 10

# ...

def create_laser():
    laser = turtle.Turtle()
    laser.penup()
    laser.color(1, 0, 0)
    laser.hideturtle()
    laser.setposition(cannon.xcor(), cannon.ycor())
    laser.setheading(90)
    # Move laser to just above cannon tip
    laser.forward(20)
    # Prepare to draw the laser
    laser.pendown()
    laser.pensize(5)

    lasers.append(laser)

def move_laser(laser):
    laser.clear()
    laser.forward(LASER_SPEED)
    # Draw the laser
    laser.forward(LASER_LENGTH)
    laser.forward(-LASER_LENGTH)

# ...

# Game loop
while True:
    # Move all lasers
    for laser in lasers:
        move_laser(laser)
    window.update()

turtle.done()

您定义函数 move_laser(),它接受 Turtle 对象。该函数清除之前的激光绘图,并将 Turtle 对象向前移动代表激光速度的量。这是对 laser.forward() 的三个调用中的第一个。

要在屏幕上绘制激光,您需要使用不同的技术。创建大炮时,您使用了 Turtle 对象的形状。要绘制激光,请使用 Turtle 对象绘制一条线。

回头看看 create_laser() 并注意您调用了 laser.hideturtle() 来隐藏精灵,以及 laser.pendown()laser.pensize(5) 使 Turtle 在移动时能够绘制 5 个像素宽的线条。现在,您需要将 Turtle 对象带回到 move_laser() 中的起始位置,以便为下一帧做好准备。

while 循环包含一个 for 循环,该循环遍历所有激光器并将它们向前移动。您还可以在游戏循环中添加 window.update() 以在每帧中更新一次显示。此时,您还可以从 draw_cannon() 中删除 window.update(),因为您不再需要它了。

当您运行此代码并按空格键时,您将看到大炮发射激光:

接下来,您将处理当激光离开屏幕顶部时如何处理它们。

移除离开屏幕的激光

当激光离开屏幕时,您将不再看到它。但是,Turtle 对象仍然在激光列表中,程序仍然需要处理该激光。即使激光在视线之外,程序仍然会向上移动激光,一直移动到无穷远!随着时间的推移,激光列表将包含大量不再在游戏中发挥任何作用的物体。

当程序移动激光时,性能瓶颈是在屏幕上绘制激光的过程。更新 Turtle 对象的 y- 坐标是一个快速修复方法。当激光离开屏幕时,程序不需要再绘制它。因此,您需要发射许多激光才能看到游戏明显变慢。但是,删除不再需要的对象始终是一个好习惯。

要在这些对象离开屏幕时将其删除,您可以将它们从激光列表中删除,这样就会阻止游戏循环向前移动它们。但由于 turtle 模块保留所有 Turtle 对象的内部列表,因此如果您使用 turtle.turtles() 从该内部列表中删除激光,对象也将从内存中删除:

# ...

# Game loop
while True:
    # Move all lasers
    for laser in lasers.copy():
        move_laser(laser)
        # Remove laser if it goes off screen
        if laser.ycor() > TOP:
            laser.clear()
            laser.hideturtle()
            lasers.remove(laser)
            turtle.turtles().remove(laser)
    window.update()

turtle.done()

请注意 for 循环如何迭代 lasers 的副本,因为最好不要循环遍历同一 for 循环中正在更改的列表。

此时,您可以将 print(len(turtle.turtles()))print(len(lasers)) 添加到游戏循环中,以确认激光已被移除当他们离开屏幕时从这些列表中删除。

好工作!你已经准备好创造外星人并将他们移到屏幕上。

第四步:创建并移动外星人

这是暴风雨前的宁静。由于没有可供射击的入侵外星人,激光炮目前处于闲置状态。是时候将外星人添加到您的游戏中了。

你已经完成了创造和移动外星人的大部分艰苦工作,因为你可以像激光一样处理外星人。但有一些细微的差别:

  • 每隔几秒就会产生新的外星人,而不是当玩家按下某个键时。
  • 外星人出现在屏幕顶部的随机x-位置,而不是出现在大炮的尖端。
  • 外星人的形状和颜色与激光不同。

除了这些细微的差异之外,其余代码将反映您为创建和移动激光器而编写的代码。

您可以从以下链接中名为 source_code_step_4/ 的文件夹中下载代码,因为它将在此步骤结束时查看:

要开始项目的这一部分,您将创建定期出现的外星人。一旦到位,您就可以专注于移动它们。

产生新的外星人

首先,定义新函数 create_alien() 和一个用于存储外星人的列表:

import random
import turtle

# ...

lasers = []
aliens = []

# ...

def create_alien():
    alien = turtle.Turtle()
    alien.penup()
    alien.turtlesize(1.5)
    alien.setposition(
        random.randint(
            int(LEFT + GUTTER),
            int(RIGHT - GUTTER),
        ),
        TOP,
    )
    alien.shape("turtle")
    alien.setheading(-90)
    alien.color(random.random(), random.random(), random.random())
    aliens.append(alien)

#  ...

您将外星人放置在屏幕顶部的随机 x- 位置。外星人面朝下。您还可以为红色、绿色和蓝色分量分配随机值,以便每个外星人都有随机颜色。

在这个海龟游戏中,外星人会定期出现。您可以导入 time 模块并设置计时器来生成新的外星人:

import random
import time
import turtle

CANNON_STEP = 10
LASER_LENGTH = 20
LASER_SPEED = 10
ALIEN_SPAWN_INTERVAL = 1.2  # Seconds

# ...

# Game loop
alien_timer = 0
while True:
    # ...

    # Spawn new aliens when time interval elapsed
    if time.time() - alien_timer > ALIEN_SPAWN_INTERVAL:
        create_alien()
        alien_timer = time.time()
    window.update()

turtle.done()

该代码现在每 1.2 秒生成一个新的外星人。您可以调整该值以使游戏变得更难或更容易。这是迄今为止的游戏:

外星人会定期生成,但它们会卡在屏幕顶部。接下来,您将学习如何在屏幕上移动外星人。

移动外星人

您可以在此处移动游戏循环的外星人列表中的所有外星人:

import random
import time
import turtle

CANNON_STEP = 10
LASER_LENGTH = 20
LASER_SPEED = 10
ALIEN_SPAWN_INTERVAL = 1.2  # Seconds
ALIEN_SPEED = 2

# ...

# Game loop
alien_timer = 0
while True:
    # ...

    # Move all aliens
    for alien in aliens:
        alien.forward(ALIEN_SPEED)
    window.update()

turtle.done()

外星人现在正在不断地向下移动:

如果外星人没有以正确的速度移动,您可以通过使用不同的 ALIEN_SPEED 值来调整它们的速度。请记住,游戏速度会根据您所使用的系统而有所不同。您将在本教程的最后一步中设置游戏的帧速率,因此您现在无需担心速度。

这款海龟游戏看起来已经接近完成,但在这款游戏正式上线之前,您还需要执行一些关键步骤。这些步骤的第一步是检测激光何时击中外星人。

第 5 步:确定激光何时击中外星人

您可能在之前的视频中注意到,激光直接穿过外星人而没有影响他们。这种行为并不能阻止外星人的入侵!目前,该程序并未检查激光是否击中了外星人。因此,您需要更新代码来处理这一重要的游戏功能。

您可以从以下链接中名为 source_code_step_5/ 的文件夹中下载代码,因为它将在此步骤结束时查看:

在游戏的每一帧中,你可能会有几道激光向上飞,还有几个外星人向下坠落。所以你需要检查是否有任何激光击中了任何外星人。在教程的这一部分中,您将应用一种简单的技术来处理这个问题 - 您将检查每个激光与每个外星人的距离以检测任何命中。

当然这不是最有效的选择,但对于这个简化版的太空入侵者来说已经足够了。

在游戏循环中,您已经有一个循环遍历激光列表。您可以添加另一个循环来迭代每个激光的每个外星人:

# ...

while True:
    # Move all lasers
    for laser in lasers.copy():
        move_laser(laser)
        # Remove laser if it goes off screen
        if laser.ycor() > TOP:
            laser.clear()
            laser.hideturtle()
            lasers.remove(laser)
            turtle.turtles().remove(laser)
        # Check for collision with aliens
        for alien in aliens.copy():
            if laser.distance(alien) < 20:
                # TODO Remove alien and laser
                ...

# ...

新的 for 循环迭代 aliens 的副本,并且嵌套在迭代 lasersfor 循环中。您使用 aliens.copy() 因为您将在某些帧中从该列表中删除外星人。确保避免更改用于迭代的列表。

上面的代码显示了 # TODO 注释,而不是所需的代码块。您需要在此处编写的代码应执行以下操作:

  1. 从屏幕和内存中移除激光
  2. 从屏幕和内存中删除外星人

您已经编写了删除激光的代码。当激光离开屏幕时,这些线会清除绘图并从两个列表中删除 Turtle

# ...
laser.clear()
laser.hideturtle()
lasers.remove(laser)
turtle.turtles().remove(laser)

您应该避免重复相同的代码行。因此,您可以将这些行放入一个函数中并调用它两次。然而,这些与将外星人从游戏中移除所需的步骤相同。因此,您可以编写一个函数来删除任何精灵并将其用于激光和外星人:

# ...

def remove_sprite(sprite, sprite_list):
    sprite.clear()
    sprite.hideturtle()
    window.update()
    sprite_list.remove(sprite)
    turtle.turtles().remove(sprite)

# ...

# Game loop
alien_timer = 0
while True:
    # Move all lasers
    for laser in lasers.copy():
        move_laser(laser)
        # Remove laser if it goes off screen
        if laser.ycor() > TOP:
            remove_sprite(laser, lasers)
            break
        # Check for collision with aliens
        for alien in aliens.copy():
            if laser.distance(alien) < 20:
                remove_sprite(laser, lasers)
                remove_sprite(alien, aliens)
                break

    # ...

函数remove_sprite()接受一个Turtle对象和一个列表,它会清除Turtle以及链接到它的任何绘图。您为激光调用 remove_sprite() 两次:一次是当激光离开屏幕时,另一次是当激光击中外星人时。您还可以在游戏循环中为外星人调用一次remove_sprite()

还有两个 break 语句。第一个是当激光离开屏幕时,因为你不需要再检查激光是否与外星人相撞。第二个是当激光击中外星人时的内部 for 循环,因为该激光无法击中另一个外星人。

您的游戏现在应该如下所示:

正如你所看到的,当发生碰撞时,激光和外星人都会消失。在玩这个游戏之前,还需要执行一个步骤。如果外星人到达楼层,游戏就会结束

第六步:结束游戏

当其中一个外星人到达地面时游戏结束。外星人入侵成功了!现在,您将添加一个启动屏幕以在屏幕上显示游戏结束

您可以从以下链接中名为 source_code_step_6/ 的文件夹中下载代码,因为它将在此步骤结束时查看:

游戏循环已经遍历了外星人列表,您可以检查每个外星人是否已到达地面。发生这种情况时,有多种方法可以停止游戏循环。在下一部分中,您将创建一个布尔标志来控制 while 循环何时停止:

# ...

# Game loop
alien_timer = 0
game_running = True
while game_running:
    # ...

    # Move all aliens
    for alien in aliens:
        alien.forward(ALIEN_SPEED)
        # Check for game over
        if alien.ycor() < FLOOR_LEVEL:
            game_running = False
            break
    window.update()

turtle.done()

当外星人的 y- 坐标低于地面水平时,您将布尔标志 game_running 设置为 False 并中断 for循环。该标志指示 while 循环停止重复。

在进行最后一项更改之前,您有一个无限的 while 循环。现在,循环不再是无限的,您可以在循环后面添加代码,该代码将在游戏结束后执行:

# ...

# Game loop
alien_timer = 0
game_running = True
while game_running:
    # ...

    window.update()

splash_text = turtle.Turtle()
splash_text.hideturtle()
splash_text.color(1, 1, 1)
splash_text.write("GAME OVER", font=("Courier", 40, "bold"), align="center")

turtle.done()

您创建一个新的 Turtle 来在屏幕上书写文本。游戏现已接近完成:

恭喜你,你已经有了一款可以运行的游戏!如果您愿意,可以到此为止,但如果您想进一步完善这个海龟游戏,还可以采取三个额外的步骤。

第 7 步:添加计时器和分数

在此步骤中,您将添加一个计时器并显示玩家击中的外星人数量。

您可以从以下链接中名为 source_code_step_7/ 的文件夹中下载代码,因为它将在此步骤结束时查看:

首先,您将创建并显示一个计时器。

创建一个计时器

您在生成外星人时已经导入了 time 模块。您可以使用此模块来存储游戏循环开始时的时间,并计算出每帧已经过去了多少时间。您还可以创建一个新的 Turtle 来在屏幕上显示文本:

# ...

# Create laser cannon
cannon = turtle.Turtle()
cannon.penup()
cannon.color(1, 1, 1)
cannon.shape("square")
cannon.setposition(0, FLOOR_LEVEL)

# Create turtle for writing text
text = turtle.Turtle()
text.penup()
text.hideturtle()
text.setposition(LEFT * 0.8, TOP * 0.8)
text.color(1, 1, 1)

# ...

# Game loop
alien_timer = 0
game_timer = time.time()
game_running = True
while game_running:
    time_elapsed = time.time() - game_timer
    text.clear()
    text.write(
        f"Time: {time_elapsed:5.1f}s",
        font=("Courier", 20, "bold"),
    )

    # ...

您可以在每帧开始时在屏幕右上角写下经过的时间。在 .write() 的第一个参数中,使用格式说明符 :5.1f 来显示经过的时间,总共五个字符和小数点后一位数字。

添加分数

创建另一个变量来保存分数。每次玩家击中外星人时都会增加分数,并在经过的时间下方显示分数:

# ...

# Game loop
alien_timer = 0
game_timer = time.time()
score = 0
game_running = True
while game_running:
    time_elapsed = time.time() - game_timer
    text.clear()
    text.write(
        f"Time: {time_elapsed:5.1f}s\nScore: {score:5}",
        font=("Courier", 20, "bold"),
    )

    # ...
        # Check for collision with aliens
        for alien in aliens.copy():
            if laser.distance(alien) < 20:
                remove_sprite(laser, lasers)
                remove_sprite(alien, aliens)
                score += 1
                break

    # ...

这个turtle 游戏现在显示击中的外星人数量以及游戏每一帧所经过的时间:

游戏功能齐全,但您是否注意到激光炮从一个位置跳到另一个位置?而且,如果你想更快地移动大炮,跳跃会更加明显。在下一步中,您将更改激光炮的移动方式以使其更加平滑。

第8步:改进大炮的运动

在当前版本的游戏中,每次玩家按下向左或向右箭头键时,激光炮都会移动固定数量的像素。如果你想在屏幕上更快地移动大炮,你需要使这些跳跃更大。在教程的这一步中,您将更改大炮的移动方式,使其移动更加平滑。它甚至可能增加您拯救地球免遭外星人入侵的机会!

您可以从以下链接中名为 source_code_step_8/ 的文件夹中下载代码,因为它将在本步骤结束时查看:

激光炮的移动方式与激光和外星人的移动方式不同。在函数 move_left()move_right() 中,大炮移动一大步。当您按下箭头键时,将调用这些函数。如果你想让大炮有一个更平滑的运动,你需要以较小的增量连续移动它。为了实现这一点,你需要像激光和外星人一样在游戏循环中移动激光炮。

但是,您需要控制激光炮是向左、向右移动还是静止。为此,您可以创建一个新变量来存储游戏过程中任何时刻大炮移动的方向。以下是这个新变量可以具有的三个值:

  • 1代表向右移动
  • -1代表向左移动
  • 0表示大炮没有移动

然后,您可以在 move_left()move_right() 中更改此值,并用它来控制游戏循环中大炮的移动:

import random
import time
import turtle

CANNON_STEP = 3

# ...

# Create laser cannon
cannon = turtle.Turtle()
cannon.penup()
cannon.color(1, 1, 1)
cannon.shape("square")
cannon.setposition(0, FLOOR_LEVEL)
cannon.cannon_movement = 0  # -1, 0 or 1 for left, stationary, right

# ...

def move_left():
    cannon.cannon_movement = -1

def move_right():
    cannon.cannon_movement = 1

# ...

# Game loop
alien_timer = 0
game_timer = time.time()
score = 0
game_running = True
while game_running:
    time_elapsed = time.time() - game_timer
    text.clear()
    text.write(
        f"Time: {time_elapsed:5.1f}s\nScore: {score:5}",
        font=("Courier", 20, "bold"),
    )

    # Move cannon
    new_x = cannon.xcor() + CANNON_STEP * cannon.cannon_movement
    if LEFT + GUTTER <= new_x <= RIGHT - GUTTER:
        cannon.setx(new_x)
        draw_cannon()

    #  ...

左右箭头键仍然绑定到相同的函数,move_left()move_right()。但是,这些函数会更改数据属性 cannon.cannon_movement 的值。

您将其设置为数据属性而不是标准变量。数据属性类似于变量,但它附加到对象。 Turtle 对象已在 Turtle 类中定义了其他数据属性,但 .cannon_movement 是您在代码中添加的自定义数据属性。

您需要使用数据属性而不是标准变量的原因是这样您可以在函数中修改它的值。如果您为此值定义标准变量,则函数 move_left()move_right() 将需要返回该值。但这些函数绑定到按键,并且是从 .onkeypress() 内部调用的,因此您无法访问它们返回的任何数据。

您还可以在上次修改中减小 CANNON_STEP 的值,以减慢大炮移动的速度。您可以选择适合您系统的值。

当您运行此代码时,您会注意到您可以将大炮设置为向左或向右移动,并且可以更改其方向。但大炮一旦开始移动,就永远不会停止。您永远不会在代码中将 cannon.cannon_movement 设置回 0。相反,您希望在释放箭头键时该值恢复为 0。使用 .onkeyrelease() 来实现这一点:

# ...

def move_left():
    cannon.cannon_movement = -1

def move_right():
    cannon.cannon_movement = 1

def stop_cannon_movement():
    cannon.cannon_movement = 0

# ...

# Key bindings
window.onkeypress(move_left, "Left")
window.onkeypress(move_right, "Right")
window.onkeyrelease(stop_cannon_movement, "Left")
window.onkeyrelease(stop_cannon_movement, "Right")
window.onkeypress(create_laser, "space")
window.onkeypress(turtle.bye, "q")
window.listen()

# ...

现在您可以按住箭头键来移动大炮。当您释放箭头键时,大炮停止移动:

大炮的移动更加平稳。游戏运行完美。但是,如果您尝试在不同的计算机上运行此游戏,您会发现游戏的速度有所不同。为了解决这个问题,您可能需要根据运行游戏的计算机类型来调整大炮、激光和外星人的速度值。

在本教程的最后一步中,您将设置游戏的帧速率,以便您可以控制游戏在任何计算机上的运行速度。

第9步:设置游戏的帧速率

while 循环的每次迭代代表游戏的一帧。在循环的每次迭代中需要执行几行代码。程序完成循环中所有操作所需的时间取决于计算机的速度以及操作系统是否忙于其他任务。

在此步骤中,您将通过选择帧速率来固定帧的持续时间。

您可以从以下链接中名为 source_code_final/ 的文件夹中下载代码,因为它将在此步骤结束时查看:

在这里,您将设置帧速率(以每秒帧数为单位),并计算出该帧速率下一帧的时间:

import random
import time
import turtle

FRAME_RATE = 30  # Frames per second
TIME_FOR_1_FRAME = 1 / FRAME_RATE  # Seconds

# ...

接下来,测量运行一次 while 循环迭代所需的时间。如果这个时间短于一帧所需的时间,则暂停游戏,直到达到一帧所需的时间。然后循环可以继续进行下一次迭代。只要运行 while 循环所需的时间短于一帧所需的时间,一旦暂停完成,每一帧将花费相同的时间:

import random
import time
import turtle

FRAME_RATE = 30  # Frames per second
TIME_FOR_1_FRAME = 1 / FRAME_RATE  # Seconds

CANNON_STEP = 10
LASER_LENGTH = 20
LASER_SPEED = 20
ALIEN_SPAWN_INTERVAL = 1.2  # Seconds
ALIEN_SPEED = 3.5

# ...

# Game loop
alien_timer = 0
game_timer = time.time()
score = 0
game_running = True
while game_running:
    timer_this_frame = time.time()

    # ...

    time_for_this_frame = time.time() - timer_this_frame
    if time_for_this_frame < TIME_FOR_1_FRAME:
        time.sleep(TIME_FOR_1_FRAME - time_for_this_frame)
    window.update()

# ...

现在是调整游戏参数以满足您的喜好的最佳时机,例如决定大炮、激光和外星人速度的值。

这是游戏的最终版本:

您可以在下面的可折叠部分找到完整的游戏代码:

import random
import time
import turtle

FRAME_RATE = 30  # Frames per second
TIME_FOR_1_FRAME = 1 / FRAME_RATE  # Seconds

CANNON_STEP = 10
LASER_LENGTH = 20
LASER_SPEED = 20
ALIEN_SPAWN_INTERVAL = 1.2  # Seconds
ALIEN_SPEED = 3.5

window = turtle.Screen()
window.tracer(0)
window.setup(0.5, 0.75)
window.bgcolor(0.2, 0.2, 0.2)
window.title("The Real Python Space Invaders")

LEFT = -window.window_width() / 2
RIGHT = window.window_width() / 2
TOP = window.window_height() / 2
BOTTOM = -window.window_height() / 2
FLOOR_LEVEL = 0.9 * BOTTOM
GUTTER = 0.025 * window.window_width()

# Create laser cannon
cannon = turtle.Turtle()
cannon.penup()
cannon.color(1, 1, 1)
cannon.shape("square")
cannon.setposition(0, FLOOR_LEVEL)
cannon.cannon_movement = 0  # -1, 0 or 1 for left, stationary, right

# Create turtle for writing text
text = turtle.Turtle()
text.penup()
text.hideturtle()
text.setposition(LEFT * 0.8, TOP * 0.8)
text.color(1, 1, 1)

lasers = []
aliens = []

def draw_cannon():
    cannon.clear()
    cannon.turtlesize(1, 4)  # Base
    cannon.stamp()
    cannon.sety(FLOOR_LEVEL + 10)
    cannon.turtlesize(1, 1.5)  # Next tier
    cannon.stamp()
    cannon.sety(FLOOR_LEVEL + 20)
    cannon.turtlesize(0.8, 0.3)  # Tip of cannon
    cannon.stamp()
    cannon.sety(FLOOR_LEVEL)

def move_left():
    cannon.cannon_movement = -1

def move_right():
    cannon.cannon_movement = 1

def stop_cannon_movement():
    cannon.cannon_movement = 0

def create_laser():
    laser = turtle.Turtle()
    laser.penup()
    laser.color(1, 0, 0)
    laser.hideturtle()
    laser.setposition(cannon.xcor(), cannon.ycor())
    laser.setheading(90)
    # Move laser to just above cannon tip
    laser.forward(20)
    # Prepare to draw the laser
    laser.pendown()
    laser.pensize(5)

    lasers.append(laser)

def move_laser(laser):
    laser.clear()
    laser.forward(LASER_SPEED)
    # Draw the laser
    laser.forward(LASER_LENGTH)
    laser.forward(-LASER_LENGTH)

def create_alien():
    alien = turtle.Turtle()
    alien.penup()
    alien.turtlesize(1.5)
    alien.setposition(
        random.randint(
            int(LEFT + GUTTER),
            int(RIGHT - GUTTER),
        ),
        TOP,
    )
    alien.shape("turtle")
    alien.setheading(-90)
    alien.color(random.random(), random.random(), random.random())
    aliens.append(alien)

def remove_sprite(sprite, sprite_list):
    sprite.clear()
    sprite.hideturtle()
    window.update()
    sprite_list.remove(sprite)
    turtle.turtles().remove(sprite)

# Key bindings
window.onkeypress(move_left, "Left")
window.onkeypress(move_right, "Right")
window.onkeyrelease(stop_cannon_movement, "Left")
window.onkeyrelease(stop_cannon_movement, "Right")
window.onkeypress(create_laser, "space")
window.onkeypress(turtle.bye, "q")
window.listen()

draw_cannon()

# Game loop
alien_timer = 0
game_timer = time.time()
score = 0
game_running = True
while game_running:
    timer_this_frame = time.time()

    time_elapsed = time.time() - game_timer
    text.clear()
    text.write(
        f"Time: {time_elapsed:5.1f}s\nScore: {score:5}",
        font=("Courier", 20, "bold"),
    )
    # Move cannon
    new_x = cannon.xcor() + CANNON_STEP * cannon.cannon_movement
    if LEFT + GUTTER <= new_x <= RIGHT - GUTTER:
        cannon.setx(new_x)
        draw_cannon()
    # Move all lasers
    for laser in lasers.copy():
        move_laser(laser)
        # Remove laser if it goes off screen
        if laser.ycor() > TOP:
            remove_sprite(laser, lasers)
            break
        # Check for collision with aliens
        for alien in aliens.copy():
            if laser.distance(alien) < 20:
                remove_sprite(laser, lasers)
                remove_sprite(alien, aliens)
                score += 1
                break
    # Spawn new aliens when time interval elapsed
    if time.time() - alien_timer > ALIEN_SPAWN_INTERVAL:
        create_alien()
        alien_timer = time.time()

    # Move all aliens
    for alien in aliens:
        alien.forward(ALIEN_SPEED)
        # Check for game over
        if alien.ycor() < FLOOR_LEVEL:
            game_running = False
            break

    time_for_this_frame = time.time() - timer_this_frame
    if time_for_this_frame < TIME_FOR_1_FRAME:
        time.sleep(TIME_FOR_1_FRAME - time_for_this_frame)
    window.update()

splash_text = turtle.Turtle()
splash_text.hideturtle()
splash_text.color(1, 1, 1)
splash_text.write("GAME OVER", font=("Courier", 40, "bold"), align="center")

turtle.done()