🎮 第1章 起步
📄 一句话概括

能做。前提是别指望它去扛渲染——渲染交给显卡,它只管「这一帧谁在哪」。 游戏真正贵的不是算得多聪明,是每帧重复做多少件事。 同一件事换个算法,能从五十万次比较降到几千次。 Pygame 是底层积木,Cocos2d 是现成框架,两者差着一层,不是两个选项。

📝 本章和前面几章的关系

这是全书第一篇,不假设你写过游戏,也不假设你写过多少代码。 教材第 1 章第一节是【用 Python 编写游戏】,下面挂着五个小标题: 能不能写、能写哪类、性能怎么样、怎么用它写、Pygame 和 Cocos2d 各是什么。 本篇就把这五问挨个答一遍,顺序不乱。 本课 30 个 Demo 全部在这台机器上真跑,包括 Pygame 的离屏渲染。 第 4 章起讲 Cocos2d 时会交代一个例外,以及为什么。

一、「脚本语言做不了游戏」这句话的来历

二十年前这句话是对的。 那时候游戏机内存几兆、CPU 几十兆赫兹,渲染、物理、AI 全挤在一个循环里,每一帧的预算要抠到指令级。 解释型语言在那种环境下确实没有位置。 现在反过来了。一台普通电脑,CPU 几个核跑在几吉赫兹,显卡每秒能刷上亿个像素。 算力过剩出来的那一块,正好够养活一层解释器。 更关键的是,现代游戏的代码不是一整块。 渲染归显卡,物理归引擎,剩下那一大堆——关卡怎么摆、任务什么时候触发、存档怎么写、 界面怎么跳转——全是可以慢慢算的部分。

这部分现在大量用脚本写。Unity 用 C#,虚幻引擎有蓝图,很多手游内嵌 Lua。 Python 在这条线上位置更靠外一点:它常常不是写引擎,是拿现成的引擎做游戏。 Pygame 就是那个「现成的引擎」。 它把 SDL 包了一层,给你三样东西:一块能画的像素画布、一个能收键盘鼠标的事件队列、一套出声的接口。 剩下的全归你。 所以准确的说法是:Python 能做游戏,但它做的是「游戏本身」,不是「引擎」。

二、游戏里真正贵的是什么

给个数就清楚了。假设同屏 1000 颗子弹,要判断有没有撞上。 最笨的做法是比较每一对:1000 × 999 / 2,差不多五十万次比较。 一帧只有 16.67 毫秒,纯 Python 做不完。 但这不是「Python 不行」,是「这个做法不行」。真实工程里没人两两比:按位置先分格,只比同一格里的; 或者建四叉树,把远处的整批排除掉。同一件事,换个算法就从五十万降到几千。

所以判断标准不是「Python 快不快」,是【每帧要做多少次真正的计算】。这句话是本篇最该带走的一条。

类型 每帧规模 结论
2D 休闲、解谜、卡牌 同屏几十个对象 绰绰有余
像素风 2D 动作 一两百个精灵 够用
弹幕射击 同屏上千个物体 必须上算法优化
3D 大型场景 顶点数上十万 不行,换语言或换引擎

最后一行要说明白:这不是「优化一下就行」。 3D 的顶点变换是每个顶点都要算一次矩阵乘法,十万个顶点乘六十帧, 就是每秒六百万次矩阵乘法——这块只能交给显卡,任何 CPU 上的脚本语言都没戏。

三、一帧十六毫秒怎么花

演示里跑出来一个数:纯 Python 的 for 循环做一百万次加法,大约一百毫秒出头。 除一下就知道了——一帧的时间只够跑十几万次这种加法。 而同样一百万次相加,换成内置的 sum(),快五倍上下。 快的原因不是算法更聪明,是那段循环写在 C 里。 第一条规矩就出来了:能用内置函数和现成库做的事,别自己写循环。

第二条来自演示的另一组对比:同样摆 1000 个方块,图在循环里现造一遍,比贴现成的慢一倍多。 图要在初始化时建好,游戏循环里只管贴。 第三条演示里测不出来,只能讲道理:真机上 display.flip() 那一句要等显示器刷完一轮, 在 60 帧的节奏里,那一等就是十几毫秒,比画 1000 个方块本身贵得多。 这个数本课给不出来,因为沙箱是离屏软件渲染,没有垂直同步可等。 测不到就不编,这是本课从第 57 课沿用下来的规矩。

四、Pygame 和 Cocos2d 差着一层

三个名字,三层东西,从上往下是这样的: 最底下是 SDL,一个 C 写的库,直接跟显卡、声卡、手柄打交道。 它不关心你在做游戏还是做播放器,只负责「把这块像素摆到屏幕上」「把这段声音送出去」。 中间是 Pygame,SDL 的 Python 外套。 它把 SDL 那些 C 函数包成 Python 对象,给你三件趁手的东西:Surface(一块像素画布)、 Rect(一个只管位置大小的矩形盒)、事件队列(键盘鼠标发生了什么)。

最上面是 Cocos2d,一个框架。 它把「游戏该怎么组织」也一起给了:导演管场景切换,场景管这一屏有谁,层管谁盖在谁上面, 动作管「在多少秒内把某个属性变成多少」。 越往下越接近硬件、越快、越难写;越往上越好写、越慢、越难改。这句话在软件里通用, 游戏只是把它摆得特别清楚。 为什么先学 Pygame?因为它把游戏拆得最碎。 你亲手写循环、亲手算碰撞、亲手决定先画谁,才知道框架替你省掉的是哪些活。 一上来就用框架,出了问题只会猜。

五、这本书的路线

教材 7 章加一个附录,本课按主题合成 30 篇,分五块:

块 章 篇数 做什么
一 起步 第 1 章 2 能做什么、环境怎么搭
二 Pygame 打基础 第 2、3 章 8 摸零件,然后做一个飞机大战
三 Cocos2d 打基础 第 4、5 章 9 换框架,做一个俄罗斯方块
四 Cocos2d 进阶 第 6 章 5 碰撞、地图、自定义动作、动画、粒子
五 一个完整项目 第 7 章、附录 6 做一个超级马里奥

顺序是有意的:先在低层把每个像素怎么来的搞清楚,再上框架看一个项目怎么组织。 反过来也能学,但踩坑时你会分不清是框架的问题还是自己的问题。

六、本课的三条口径

第一条:能真跑的全真跑。 这台机器没有显示器,所以 Pygame 走离屏渲染——窗口真建出来、 图真画上去、像素真取回来、PNG 真存进磁盘。看不见它,但它确实存在。 第二条:测不出来的不编。 比如刚才那个垂直同步的等待,本机测不到,那就只讲原理,不给毫秒数。 第三条:Cocos2d 那条线只讲机制。 原因不在没有显示器, 在版本冲突——它依赖的 pyglet 在 Python 3.13 上直接崩,第 4 章会把报错原文贴出来。

凡是能从框架里抽出来的算法(锚点换算、动作插值、瓦片换算、切图坐标、粒子生命曲线), 一律用纯 Python 复刻并真跑真算,正文里逐处注明【复刻的是语义,不是原库】。 这三条先摆在这儿,后面每一篇都照这个来。

七、动手 Demo:Python 到底能不能做游戏

先看这台机器的家底,再实测同一件事两种写法的差距。 然后用 Pygame 真摆 1000 个方块,看时间到底花在哪儿。 最后两张表:Python 适合做哪类游戏,Pygame 与 Cocos2d 各是什么定位。

import os

# 这台机器没有显示器,用 SDL 的离屏驱动;声音同理
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = '1'
os.environ['SDL_VIDEODRIVER'] = 'dummy'
os.environ['SDL_AUDIODRIVER'] = 'dummy'

import platform, sys, time, pygame

def sec(t):
    print(); print('  ' + t); print('  ' + '-' * 68)

def info(s):
    print('  ' + s)

def note(s):
    print('  ! ' + s)

def clock(fn, repeat=5):
    fn()                                  # 先空跑一次,摊掉预热
    best = 1e9
    for _ in range(repeat):
        t = time.perf_counter(); fn()
        best = min(best, (time.perf_counter() - t) * 1000)
    return best

# ① 家底
sec('① 先看家底')
info('解释器 %s,%s %s,%s' % (sys.version.split()[0], platform.system(),
                               platform.release(), platform.machine()))
info('pygame %s,底下真正的多媒体库 SDL %s'
     % (pygame.version.ver, pygame.version.SDL))
note('SDL 才是直接跟显卡、声卡、手柄打交道的那个库;pygame 是它的 Python 外套。')

# ② 同一件事,两种写法
sec('② 同一件事,两种写法的差距')
N = 1000000
def loop_sum(n):
    s = 0
    for i in range(n):
        s += i
    return s
t_loop = clock(lambda: loop_sum(N))
t_builtin = clock(lambda: sum(range(N)))
info('for 循环 %.1f 毫秒,内置 sum %.1f 毫秒,快 %.1f 倍'
     % (t_loop, t_builtin, t_loop / t_builtin))

# ③ 一帧的预算是多少
sec('③ 一帧到底有多少时间')
info('60 帧每秒,每帧预算 1000 / 60 = %.2f 毫秒' % (1000.0 / 60))
per = 1000.0 / 60 / t_loop * N
info('这点时间里纯 Python 只能跑 %s 次加法' % format(per, ',.0f'))

# ④ 画 1000 个方块:贴现成的 vs 每帧现造
sec('④ 真正吃时间的不是算,是每帧重新造一遍')
pygame.init()
scr = pygame.display.set_mode((800, 600))
tile = pygame.Surface((32, 32)); tile.fill((214, 148, 74))
blocks = [tile] * 1000
pos = [((i % 25) * 32, (i // 25) * 24) for i in range(1000)]

def blit_all():
    scr.fill((18, 20, 28))
    for xy, b in zip(pos, blocks):
        scr.blit(b, xy)

def remake_all():
    scr.fill((18, 20, 28))
    for xy in pos:
        b = pygame.Surface((32, 32))   # 每帧新建一张小图,这是错的写法
        b.fill((214, 148, 74))
        scr.blit(b, xy)

t_blit = clock(blit_all, repeat=9)
t_new = clock(remake_all, repeat=5)
t_flip = clock(pygame.display.flip, repeat=9)
info('贴现成的 %.2f 毫秒;每帧现造 %.2f 毫秒,慢 %.1f 倍;flip %.2f 毫秒'
     % (t_blit, t_new, t_new / t_blit, t_flip))
pygame.quit()

实跑输出:

--------------------------------------------------------------------
Python 到底能不能做游戏
--------------------------------------------------------------------

  ① 先看家底
  --------------------------------------------------------------------
  解释器 3.13.14,Windows 10,AMD64
  pygame 2.6.1,底下真正的多媒体库 SDL 2.28.4
  ! SDL 才是直接跟显卡、声卡、手柄打交道的那个库;pygame 是它的 Python 外套。
  这本书的第二个主角 Cocos2d 是架在 pyglet 上的,pyglet 又架在 OpenGL 上——
  三层,越往上越好写,越往下越快。

  ② 同一件事,两种写法的差距
  --------------------------------------------------------------------

| 写法 | 一百万个整数相加 | 相对倍数 |
|---|---:|---:|
| for 循环里一个个加 | 103.0 毫秒 | 1.0 倍(基准) |
| 直接用内置 sum() | 19.7 毫秒 | 快 5.2 倍 |

  ! 内置函数快,不是因为算法更好,是因为那段循环写在 C 里。

  ③ 一帧到底有多少时间
  --------------------------------------------------------------------
  想跑 60 帧每秒,每帧的预算就是 1000 / 60 = 16.67 毫秒。
  这点时间里,纯 Python 的 for 循环能跑 161,828 次加法。
  听着不少,可一次加法只是最便宜的操作。
  算两个圆有没有撞、路径怎么走、每颗子弹在哪——一次都要几十上百条语句。

  ④ 真正吃时间的不是「算」,是「每帧重新造一遍」
  --------------------------------------------------------------------

| 每帧要做的事 | 耗时 | 说明 |
|---|---:|---|
| 贴 1000 张早就建好的小图 | 1.12 毫秒 | 只是内存拷贝 |
| 新建 1000 张再贴上去 | 2.69 毫秒 | 慢了 2.4 倍 |
| 最后 flip 一次 | 0.01 毫秒 | 离屏模式下不等垂直同步 |

  同样是 1000 个方块,图在循环里现造一遍就慢 2.4 倍。
  ! 第一条优化规则:图在初始化时建好,游戏循环里只管贴。
  至于 flip,本沙箱是离屏软件渲染,它几乎不花时间。
  真机上这一句要等显示器刷完一轮,那才是 16.67 毫秒里最大的一笔。
  ! 这一项本沙箱测不出来,所以这里不给数字,只把原理说清。

  ⑤ Python 到底适合做哪类游戏
  --------------------------------------------------------------------

| 类型 | 合适度 | 原因 |
|---|---|---|
| 2D 休闲、解谜、卡牌、文字冒险 | 很合适 | 逻辑为主,同屏几十个对象 |
| 像素风 2D 动作、平台跳跃 | 合适 | 精灵数可控,热点在循环里 |
| 弹幕射击 | 勉强 | 同屏上千颗子弹,每帧都要算碰撞 |
| 3D 大型场景 | 不合适 | 顶点、光照、阴影都是 CPU 密集活 |
| 联网小游戏 | 合适 | 网络本来就在等,慢一点感觉不出来 |


  ⑥ Pygame 和 Cocos2d 不是一个层级的东西
  --------------------------------------------------------------------

| 对比项 | Pygame | Cocos2d |
|---|---|---|
| 定位 | 底层积木 | 现成的框架 |
| 谁管游戏循环 | 你自己写 | 导演替你转 |
| 场景切换 | 自己实现 | 内置场景与过渡 |
| 动作与动画 | 自己算每帧 | 内置 MoveTo、RotateBy 等 |
| 上手速度 | 几行就出窗口 | 要先懂节点树和锚点 |
| 适合 | 小游戏、教学、原型 | 中等规模、多场景 |
| 本沙箱 | 真装真跑 | 不执行,第 4 章交代原因 |

  ! 先学 Pygame 不是因为它更好,是因为它把游戏拆得最碎,看得最清。

第一段那两行版本号不是凑数:pygame 2.6.1 底下是 SDL 2.28.4,这个 SDL 才是真正跟硬件打交道的那层。 你在游戏里遇到「有画面没声音」「手柄不认」,往上查的常常是 SDL 而不是 Python。 第二段是本篇的立论基础。 同一件事,换一个写法差五倍——不是因为 sum() 用了个更聪明的算法,而是它整段循环跑在 C 里, 没有 Python 每条指令的解释开销。Python 的慢,慢在「每一句都要过一遍解释器」,不在「算不快」。 第三段那个数别只看大小,要看它对一帧意味着什么:一帧只够跑十几万次最便宜的加法。 而真实的游戏逻辑,一次判断要几十条语句。这就是为什么游戏代码里到处都是「别在循环里做重活」。 第四段是这一篇最实用的一条。 两种写法贴的是同样的 1000 个方块、同样的位置,唯一区别是图是现成的还是每帧新造的。 差的那一倍多,全花在「造图」上——分配内存、初始化 Surface 结构、填色。而这些事做一次就够了。 最后一行那个 flip 只有零点零几毫秒,别当成「真机上也就这么快」。离屏驱动没有显示器可等,自然不等。 真机上它会阻塞到下一帧刷新,那是 16.67 毫秒里最大的一笔开销。本机测不到,正文里就不给数字。

30 秒实验

  1. 把演示里的 N 从一百万改成五百万,重跑,看那个「快几倍」变不变。变了说明什么?没变说明什么?
  2. 把方块数从 1000 改成 3000,看贴图耗时是不是差不多变成三倍。如果不是,想想为什么。
  3. 在你自己有显示器的机器上,把 SDL_VIDEODRIVER 那一行删掉再跑,看窗口是不是真弹出来了, 以及 flip 的耗时变了多少。

常见误区

  1. 「Python 慢,所以做不了游戏。 」慢的是逐条解释,不是每帧的总量。先把「这一帧要做多少次真正的计算」数清楚,再判断够不够。 数不清楚就说「不行」,是猜,不是结论。
  2. 「Pygame 和 Cocos2d 选一个学。」它们是两层东西:一个是积木,一个是拿积木搭好的房子。 不是二选一,是先学哪个。 本书的顺序是先积木后房子。
  3. 「帧率越高越好。」显示器一秒刷 60 次,你算 200 帧它也只显示 60 帧。 多算的那 140 帧是白烧电。 先把 60 帧稳住,再谈别的。
自测三问(点开对答案) 1. 同屏 1000 颗子弹两两判断碰撞,要比较将近五十万次。这是不是说明 Python 做不了弹幕游戏? —— 不是。这说明「两两比较」这个做法不行。 真实工程里会按位置先分格,或者建四叉树把远处的整批排除掉,比较次数能降到几千。 判断一门语言够不够用,要看「优化之后每帧还剩多少次计算」,不是看最笨的写法。 2. `sum(range(1000000))` 比 for 循环快好几倍,是算法更好吗? —— 不是算法,是**这段循环跑在 C 里**。 `sum` 是内置函数,它内部那个循环由 C 编译成机器码, 没有「每条 Python 指令都过一遍解释器」的开销。算法复杂度两者一样,都是线性。 差别全在常数因子。 3. 为什么本课不给你「真机上 flip 要多少毫秒」这个数? —— 因为这台机器是离屏软件渲染,没有显示器可等,`flip` 几乎不花时间。 真机上它会阻塞到下一帧刷新,但这个等待取决于显示器刷新率和驱动,本机测不出来。 **测不出来的数写进去就是编,编一个数比写「测不到」危险得多。**

下一篇先别急着写游戏代码,把环境弄干净——这一步偷懒,后面每一次装包都会回来找你。

相关笔记