夜雨聆风学习资料网

ARTICLE · 1335682

【附源码】用 Python 复刻经典游戏《愤怒的小鸟》

【附源码】用 Python 复刻经典游戏《愤怒的小鸟》

别死磕枯燥练习题了!!!

做小游戏,才是学 Python 最划算的练手方式。

循环、状态、物理、碰撞、输入…… 编程最核心的知识点,一个项目全部练到。做完直接能玩,成就感拉满,正反馈超快~

今天这个《愤怒的小鸟》,不依赖任何图片和音频素材。

所有元素,全部用pygame.draw画出来。

你复制粘贴到一个.py文件里,装个 pygame 就能玩。(完整代码文末领!)


一、先看它长什么样

窗口 1000×570,左边是弹弓,右边是小猪的木头城堡:三只小猪,一块横梁、两根立柱

关注
重播 分享 赞

二、准备环境

bash

pip install pygame

装不上、或者在新版本 Python 上报编译错误,就换成社区版:

bash

pip install pygame-ce

两者的 import pygame 写法完全一样,本文代码通用。

字体说明: 中文文字使用系统字体渲染,程序会尝试匹配设备上已安装的中文字体。若出现中文乱码或方框,可安装相应的中文字体。

【这一步搞不定,问AI】


三、第一步:窗口和背景

几乎所有 pygame 项目的前 20 行都是同一套:

python

pygame.init()

screen = pygame.display.set_mode((WIDTH, HEIGHT))

pygame.display.set_caption("愤怒的小鸟 · pygame 版")

clock = pygame.time.Clock()

然后是背景。这里有个新手常踩的坑:把渐变天空写在主循环里。

python

# 反例:每帧画 570 条线

for y in range(HEIGHT):

    pygame.draw.line(screen, color, (0, y), (WIDTH, y))

570 条线本身不算慢,但它每帧都在做完全重复的事。正确做法是先把静态背景画到一张 Surface 上,之后每帧只 blit 一次:

python

def make_bg(self):

    bg = pygame.Surface((WIDTH, HEIGHT))

    for y in range(HEIGHT):                       # 天空渐变

        ...

    return bg

同理,云、远山、草地也一次性画好。这个「预渲染」(pre-render)思路能救很多项目:凡是不变的东西,都别每帧重画。


四、第二步:小鸟为什么会飞

不需要物理引擎,两行就够:

python

self.vel.y += GRAVITY      # 速度被重力改变

self.pos += self.vel       # 位置被速度改变

这就是最朴素的欧拉积分:每一帧给纵向速度加一点加速度,再把速度加到位置上。

GRAVITY = 0.45 的单位是「像素 / 帧²」,在 60 帧下相当于每秒加速 27 像素/帧——一开始不明显,飞出去 20 帧之后就明显往下掉了,正好是抛物线的手感。想要「月球重力」就调小,想要「铅球」就调大,其他代码一行都不用改。

配套的还有两件事:

一是边界。撞地要弹一下,但不能无限弹。所以落地时把纵向速度乘 -0.42(反向 + 衰减),横向速度乘 0.72(摩擦),抖两下就趴下了。

二是判断「这一箭结束了没有」。这里我用了一个计数器,而不是「落地就算结束」:

python

# 半秒都没怎么动,就认为这一箭结束了

if self.vel.length() < 0.9:

    self.still += 1

else:

    self.still = 0

if self.still >= 30:

    self.settled = True

为什么不用「落地」判断?因为小鸟可能正好停在横梁上,或者卡在两根柱子中间,用落地判断会卡死在那儿,这一局再也进行不下去。用「连续 30 帧几乎没动」来判定,不管它停在哪,都能正常进入下一只鸟。


五、第三步:弹弓的手感

拖拽的本质是一个向量减法。鼠标在哪、皮兜在哪,两者一减就是拉伸向量:

python

def pull_vector(self):

    """皮兜 → 拉伸点,长度限制在 MAX_PULL 之内。"""

    v = SLING - self.drag_pos

    if v.length() > MAX_PULL:

        v.scale_to_length(MAX_PULL)

    return v

scale_to_length() 是 pygame.Vector2 自带的限位方法,比手写 math.hypot 再乘系数干净得多。限位很重要:不限制的话,鼠标拖到屏幕外就能拉出天文数字的初速度,一发射小鸟直接飞出银河系。

松手时,这个向量乘一个系数,就成了初速度:

python

self.vel = pull * POWER

POWER = 0.15,拉满 110 像素对应初速 16.5 像素/帧(约 990 像素/秒),45° 出手大约能飞 700 像素——刚好够打到右边城堡。这个系数就是「弹弓的力道」,想让它更暴力就调大。

预测弹道是这套代码里我最喜欢的一段:把发射后要做的事,用另一份变量先算一遍。

因为位移逻辑只有两行(加速度改变速度,速度改变位置),所以可以直接在一个循环里空跑,每 4 帧落一个白点,不落地不停。同一个物理,算两遍——一遍给真正的小鸟,一遍给瞄准线。做瞄准辅助、抛物线预览、AI 预判,思路都是这个。


六、第四步:碰撞检测(核心就 10 行)

这是全文唯一需要动点脑子的地方。

小鸟是圆,木箱是矩形。圆和矩形怎么判碰撞?把圆心「夹」到矩形上,得到矩形上离圆心最近的点,再看这个点到圆心的距离是否小于半径:

python

cx = max(rect.left, min(pos.x, rect.right))

cy = max(rect.top, min(pos.y, rect.bottom))

max(左边界, min(圆心, 右边界)) 这个写法,等于「如果圆心在矩形左边就取左边界,在右边就取右边界,在中间就取圆心自己」。两次 clamp,一个最近点就到手了。

有了最近点,就有法线方向,也就能做反弹:

python

vn = vel.x * nx + vel.y * ny

if vn < 0:                         # 只有朝里冲才反弹

    vel.x -= (1 + bounce) * vn * nx

    vel.y -= (1 + bounce) * vn * ny

vn 是速度在法线上的投影。如果它是正的,说明物体正在离开,这时候还去反弹就会把它又推回去,出现抽搐——所以只处理 vn < 0。这一行判断能让碰撞从「一碰就抖」变成「一碰就弹」。

那为什么不用 pygame 自带的 sprite.collide_rect?因为它只告诉你「碰了没有」,不告诉你从哪个方向碰的。没有法线,就没有反弹,小鸟只会黏在木箱上。


七、第五步:关卡与状态机

整局游戏只有四个状态:

python

self.state = "aim"      # 瞄准 / 飞行 / 赢了 / 输了

  • aim:可以拖拽,显示弹弓皮筋和瞄准线;

  • fly:小鸟在飞,每帧跑物理 + 碰撞;

  • win / over:画面压一层半透明黑,显示结算文字,等着按 R。

用状态机而不是一堆 if 堆在一起,好处是每一帧该做什么只有一个入口:update() 开头一句 if self.state != "fly": return,飞行之外的所有逻辑就都被挡在门外了,不用担心「结算了还在跑物理」。

关卡本身也只是几行数据:

python

self.pigs = [

    Pig(760, GROUND_Y - 22),        # 城堡里

    Pig(760, GROUND_Y - 132),       # 城堡顶

    Pig(880, GROUND_Y - 22),        # 右边站岗

]

self.blocks = [

    Block(700, GROUND_Y - 110, 16, 110),

    Block(800, GROUND_Y - 110, 16, 110),

    Block(690, GROUND_Y - 126, 136, 16),

]

想改关卡,改这几行的坐标就行,不用碰任何逻辑。


八、完整代码

不想自己敲代码?可以直接领取完整项目!

关注公众号,后台回复【游戏】即可获取:

① Python 完整源码:复制运行,方便学习和二次修改。

② 一键运行 exe 版:不用装 Python 环境,解压双击直接打开游戏

(🔺点击上方名片关注🔺)

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料