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 或其他引擎时,输入、更新、碰撞和渲染分层的思路仍然适用。