用 Python Turtle 做一个可玩的太空侵略者:游戏循环、碰撞与帧率

2026-09-28 22 预计阅读时间: 1 分钟
来源: realpython.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:10 分钟

Python 自带的 turtle 不只是画几何图形的教学工具。把海龟对象当作精灵,再配合定时器、键盘事件和距离检测,就能搭出一个小而完整的街机游戏。这个项目尤其适合练习游戏开发中最基础的几块拼图:绘制对象、持续更新状态、处理输入、检测碰撞,以及控制刷新频率。

下面实现一个不依赖图片素材的太空侵略者简化版:玩家可以左右移动和发射子弹,敌人会横向移动、触边下沉,被击中后消失;敌人全部消灭时获胜,敌人抵达底线时失败。

Turtle 如何承担游戏引擎的职责

一个最小游戏通常需要三类对象:

  • 场景:窗口大小、背景色、键盘监听和刷新机制。
  • 精灵:玩家、敌人、子弹,以及它们的位置与显示状态。
  • 游戏状态:分数、按键状态、子弹是否激活、游戏是否结束。

turtle.Screen() 负责场景,多个 turtle.Turtle() 实例充当精灵。关键设置是:

screen.tracer(0)

这会关闭 Turtle 的自动逐步绘制。程序需要在每一帧末尾主动调用 screen.update(),从而让玩家、子弹和敌人的移动在同一次刷新中显示,避免各个对象依次闪动。

游戏循环则可以通过定时器调度:

FPS = 60
FRAME_MS = 1000 // FPS
screen.ontimer(update, FRAME_MS)

ontimer 不保证硬实时精度,但对于这个规模的教学游戏已经足够。它还有一个重要优势:不会用无限 while 循环阻塞 Turtle 所依赖的 Tk 事件系统。

一个可以直接运行的完整版本

下面的示例只使用 Python 标准库。将代码保存为 space_invaders.py,确保本机有支持 Tk 的桌面版 Python,然后运行:

python space_invaders.py

方向键控制移动,空格键发射子弹。

import turtle

WIDTH = 800
HEIGHT = 600
FPS = 60
FRAME_MS = 1000 // FPS

PLAYER_SPEED = 7
BULLET_SPEED = 12
ENEMY_STEP = 2
ENEMY_DROP = 24

screen = turtle.Screen()
screen.title('Turtle Space Invaders')
screen.setup(WIDTH, HEIGHT)
screen.bgcolor('black')
screen.tracer(0)


def make_sprite(shape, color, x, y, heading=0):
    sprite = turtle.Turtle()
    sprite.shape(shape)
    sprite.color(color)
    sprite.penup()
    sprite.setheading(heading)
    sprite.goto(x, y)
    return sprite


player = make_sprite('triangle', 'cyan', 0, -250, 90)
player.shapesize(1.2, 1.2)

bullet = make_sprite('square', 'yellow', 0, -220)
bullet.shapesize(0.25, 0.6)
bullet.hideturtle()
bullet_active = False

enemies = []
for row in range(3):
    for column in range(7):
        x = -240 + column * 80
        y = 210 - row * 55
        enemy = make_sprite('square', 'lime', x, y)
        enemy.shapesize(1.0, 1.4)
        enemies.append(enemy)

hud = turtle.Turtle()
hud.hideturtle()
hud.color('white')
hud.penup()
hud.goto(-380, 265)

message = turtle.Turtle()
message.hideturtle()
message.color('white')
message.penup()

score = 0
enemy_dx = ENEMY_STEP
game_over = False
pressed_keys = set()


def draw_score():
    hud.clear()
    hud.write(f'Score: {score}', font=('Arial', 16, 'normal'))


def press_left():
    pressed_keys.add('Left')


def release_left():
    pressed_keys.discard('Left')


def press_right():
    pressed_keys.add('Right')


def release_right():
    pressed_keys.discard('Right')


def fire():
    global bullet_active

    if game_over or bullet_active:
        return

    bullet.goto(player.xcor(), player.ycor() + 20)
    bullet.showturtle()
    bullet_active = True


def finish(text):
    global game_over

    game_over = True
    message.goto(0, 0)
    message.write(text, align='center', font=('Arial', 30, 'bold'))


def update_player():
    x = player.xcor()

    if 'Left' in pressed_keys:
        x -= PLAYER_SPEED
    if 'Right' in pressed_keys:
        x += PLAYER_SPEED

    limit = WIDTH / 2 - 30
    x = max(-limit, min(limit, x))
    player.setx(x)


def update_bullet():
    global bullet_active, score

    if not bullet_active:
        return

    bullet.sety(bullet.ycor() + BULLET_SPEED)

    if bullet.ycor() > HEIGHT / 2:
        bullet.hideturtle()
        bullet_active = False
        return

    for enemy in enemies[:]:
        if bullet.distance(enemy) < 24:
            enemy.hideturtle()
            enemies.remove(enemy)
            bullet.hideturtle()
            bullet_active = False
            score += 100
            draw_score()
            break


def update_enemies():
    global enemy_dx

    if not enemies:
        return

    edge = WIDTH / 2 - 35
    will_hit_edge = any(
        abs(enemy.xcor() + enemy_dx) >= edge
        for enemy in enemies
    )

    if will_hit_edge:
        enemy_dx *= -1
        for enemy in enemies:
            enemy.sety(enemy.ycor() - ENEMY_DROP)
    else:
        for enemy in enemies:
            enemy.setx(enemy.xcor() + enemy_dx)


def check_end_conditions():
    if not enemies:
        finish('YOU WIN')
        return

    danger_line = player.ycor() + 25
    for enemy in enemies:
        if enemy.ycor() <= danger_line or enemy.distance(player) < 28:
            finish('GAME OVER')
            return


def update():
    if game_over:
        screen.update()
        return

    update_player()
    update_bullet()
    update_enemies()
    check_end_conditions()

    screen.update()
    screen.ontimer(update, FRAME_MS)


screen.listen()
screen.onkeypress(press_left, 'Left')
screen.onkeyrelease(release_left, 'Left')
screen.onkeypress(press_right, 'Right')
screen.onkeyrelease(release_right, 'Right')
screen.onkeypress(fire, 'space')

draw_score()
update()
screen.mainloop()

代码里最值得观察的四个机制

1. 按键事件只修改状态

左右键按下时,程序把键名加入 pressed_keys;松开时再移除。真正的位移发生在 update_player() 中,而不是键盘回调中。

这种写法比在每次按键事件里直接移动更稳定,因为操作系统的键盘重复频率并不等于游戏帧率。输入负责表达意图,游戏循环负责改变世界状态,两者由此解耦。

2. 子弹是可复用的精灵

示例没有在每次开火时创建新对象,而是反复显示、移动和隐藏同一个子弹对象。bullet_active 明确记录当前是否已有子弹飞行,因此这也是一种游戏规则:屏幕上一次只能存在一发玩家子弹。

如果要支持连发,可以把单个 bullet 改成列表或对象池,并为每颗子弹分别更新位置和检测碰撞。

3. 碰撞使用距离近似

这一版通过下面的条件判断命中:

if bullet.distance(enemy) < 24:
    ...

距离检测简单直观,适合尺寸接近的圆形或方形精灵。不过它并不是精确的矩形碰撞。若精灵尺寸差异很大,可以改用轴对齐包围盒:

def overlaps(a, b, half_width=20, half_height=20):
    return (
        abs(a.xcor() - b.xcor()) < half_width * 2
        and abs(a.ycor() - b.ycor()) < half_height * 2
    )

这里的宽高阈值需要根据精灵缩放比例调整。

4. 敌人触边后整体下沉

程序先预测敌人的下一步位置。只要任意敌人即将碰到边界,就翻转整个队伍的水平速度,并让所有敌人下沉一次。这比让每个敌人自行反弹更接近经典编队移动的效果。

遍历碰撞时使用 enemies[:] 创建浅拷贝,是因为命中后需要从原列表删除敌人。直接在正在遍历的列表中删除元素,容易跳过下一个对象。

帧率固定不等于移动速度绝对稳定

示例把帧间隔设为约 16 毫秒,并使用每帧移动多少像素的速度模型。代码简单,但当机器繁忙、窗口被拖动,或者定时器回调延迟时,实际移动速度会发生变化。

如果想继续升级,可以记录相邻两帧的真实时间差,并让位移变成每秒速度乘以 delta_time。同时应限制过大的时间差,避免窗口暂停后敌人突然跨越半个屏幕。

对于 Turtle 教学项目,固定帧步长通常足够;对于节奏严格、对象很多或需要物理模拟的游戏,则应考虑 Pygame 等更专业的框架。

继续改造时的检查清单

可以按下面的顺序扩展,避免一次引入过多状态:

  • 增加敌人的发射逻辑,并为玩家设置生命值。
  • 随着敌人减少,提高编队移动速度。
  • 使用多个关卡调整敌人行列、下沉距离和射击概率。
  • 加入重新开始按键,集中重置分数、位置和对象列表。
  • 将玩家、子弹和敌人封装成类,减少全局变量。
  • 为碰撞、计分和胜负条件编写独立函数,方便测试。

Turtle 的价值不在于渲染性能,而在于让游戏循环中的每一个动作都清晰可见。完成这个版本后,再迁移到 Pygame 或其他引擎时,输入、更新、碰撞和渲染分层的思路仍然适用。


相关推荐