能做。前提是别指望它去扛渲染——渲染交给显卡,它只管「这一帧谁在哪」。 游戏真正贵的不是算得多聪明,是每帧重复做多少件事。 同一件事换个算法,能从五十万次比较降到几千次。 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 秒实验
- 把演示里的
N从一百万改成五百万,重跑,看那个「快几倍」变不变。变了说明什么?没变说明什么? - 把方块数从 1000 改成 3000,看贴图耗时是不是差不多变成三倍。如果不是,想想为什么。
- 在你自己有显示器的机器上,把
SDL_VIDEODRIVER那一行删掉再跑,看窗口是不是真弹出来了, 以及 flip 的耗时变了多少。
常见误区
- 「Python 慢,所以做不了游戏。 」慢的是逐条解释,不是每帧的总量。先把「这一帧要做多少次真正的计算」数清楚,再判断够不够。 数不清楚就说「不行」,是猜,不是结论。
- 「Pygame 和 Cocos2d 选一个学。」它们是两层东西:一个是积木,一个是拿积木搭好的房子。 不是二选一,是先学哪个。 本书的顺序是先积木后房子。
- 「帧率越高越好。」显示器一秒刷 60 次,你算 200 帧它也只显示 60 帧。 多算的那 140 帧是白烧电。 先把 60 帧稳住,再谈别的。
自测三问(点开对答案)
1. 同屏 1000 颗子弹两两判断碰撞,要比较将近五十万次。这是不是说明 Python 做不了弹幕游戏? —— 不是。这说明「两两比较」这个做法不行。 真实工程里会按位置先分格,或者建四叉树把远处的整批排除掉,比较次数能降到几千。 判断一门语言够不够用,要看「优化之后每帧还剩多少次计算」,不是看最笨的写法。 2. `sum(range(1000000))` 比 for 循环快好几倍,是算法更好吗? —— 不是算法,是**这段循环跑在 C 里**。 `sum` 是内置函数,它内部那个循环由 C 编译成机器码, 没有「每条 Python 指令都过一遍解释器」的开销。算法复杂度两者一样,都是线性。 差别全在常数因子。 3. 为什么本课不给你「真机上 flip 要多少毫秒」这个数? —— 因为这台机器是离屏软件渲染,没有显示器可等,`flip` 几乎不花时间。 真机上它会阻塞到下一帧刷新,但这个等待取决于显示器刷新率和驱动,本机测不出来。 **测不出来的数写进去就是编,编一个数比写「测不到」危险得多。**下一篇先别急着写游戏代码,把环境弄干净——这一步偷懒,后面每一次装包都会回来找你。
- ☐ (本篇完,下一篇 1.2 搭建开发环境)