🎮 第4章 Cocos2d Ⅰ
📄 一句话概括

Cocos2d 不是一层皮,是一套架子:导演、场景、层、精灵,位置都给你留好了。 它在这台机器上装不上,原因不是缺库,是 pyglet 的版本跟 Python 3.13 对不上。 Hello World 那三行里最要紧的是 do()——动作一旦挂上,每帧自己往前走。

📝 本章和前面几章的关系

前两章用 Pygame 从零搭了一只飞机大战,每一块都是自己写的。 这一章换一个思路:用一套「已经搭好架子」的引擎,看看同一个游戏少写多少东西。 教材第 4 章开头三节:【Cocos2d 简介】【Cocos2d 的安装】【Hello World】。 先说清楚:这本书的 Cocos2d 代码在本沙箱跑不起来,原因下面第三节会摊开。 跑不通的库,本课不糊过去——报错原文照贴,机制用 Pygame 重写一遍真跑。

一、一套架子,还是一层皮

Pygame 给自己的定位是「一层薄薄的皮」:给你窗口、给你 Surface、给你 Rect,剩下的全自己写。 循环怎么写、角色怎么管、场景怎么切,它一概不管。 Cocos2d 反过来,它把一整套结构都准备好了,你往里填内容。

要做的事 Pygame 里 Cocos2d 里
开窗口 自己 set_mode 加手写循环 引擎开,你只写场景
坐标系 左上原点、y 向下 左下原点、y 向上
动画 每帧自己改坐标 挂一个动作就不管了
切换画面 自己写状态机 导演 push/replace
组织对象 自己维护列表 节点树,父子连带

代价也明显:你得先学会它的规矩。这套规矩不是游戏本身的道理,是这一家引擎的设计选择。 换个引擎,这些名字全换。 所以这一章读的时候要分两层:哪一句在讲「游戏怎么组织」,哪一句只是「Cocos2d 这么叫」。 前者可以带走,后者只在用这个库的时候有用。

二、依赖链有三层

演示第一段把三层逐个试了一遍,结果是三个都导不进来。

层 是谁 管什么
最底 OpenGL 显卡的接口,画三角形和贴图
中间 pyglet 把 OpenGL 包成 Python 对象,管窗口和事件
最上 cocos2d 导演、场景、层、精灵、动作

这个三层结构跟 Pygame 那边一模一样:Pygame 底下也是 SDL,SDL 底下也是显卡。 越往上越好写,越往下越快。 三层全不通,说明不是「少装了一个包」,是这条链子从头到尾都没接上。 这时候一个个 pip install 是没用的,得先看最上面那个要什么。

三、真正的原因在版本号

教材 4.2 节让装的版本是 cocos2d 0.6.9。 它的 setup.py 里写着这么一行:install_requires=['pyglet >= 1.4.10, < 2']。 翻译过来:它只认 1.x 的 pyglet,2.x 一律不要。 那就装 1.x。装的是 1.5.20,然后在 Python 3.13 上,import pyglet 的第一秒就崩了。 报错原文在演示第二段里,核心是这一行:NameError: name 'COMPointer' is not defined。

这不是「你少装了什么」, 是 pyglet 1.5 里的那段代码在 Python 3.13 上不成立——它依赖的一个内部类型在新版本里改名字了。 往上升级到 pyglet 2.x 能解决这一行,但 cocos2d 0.6.9 又不认 2.x。两头堵死。 说句公道话:cocos2d 0.6.9 是 2018 年前后的东西,那个年代的教程默认 Python 3.6。 教材没有写错,是六年过去,这条依赖链断了。

四、三条出路

演示第四段列了三条,本课选的是中间那条。

办法 代价 适合谁
装 Python 3.8 或 3.9,单独跑 Cocos2d 多维护一个解释器 非要跑通教材的每个例子
用 Pygame 把机制手工做一遍 多写代码 想搞懂原理(本课走这条)
直接换 Pygame 或别的引擎做项目 学的不是 Cocos2d 只想把游戏做出来

选第二条的理由很简单: 这本书要教的是「一个游戏怎么组织」,不是「Cocos2d 的 API 怎么调」。导演、场景、节点树、 动作插值这些概念,用 Pygame 手工实现一遍,你才知道引擎替你做了什么。 顺手说一句第三条:现代做 2D 小游戏,Pygame 加几十行自己的场景管理就够了,不一定非得用引擎。 工具选小的,改起来才不疼。

五、Hello World 里三件事

教材这一节叫 Hello World,但代码里一个 Hello 都没有——它演示的是「让一个方块动起来」, 所以名字叫 Hello Actions 更贴。

import cocos
from cocos.sprite import Sprite
from cocos.actions import MoveTo

class Hello(cocos.layer.ColorLayer):
    def __init__(self):
        super().__init__(64, 64, 224, 255)
        block = Sprite('block.png', position=(100, 200))
        self.add(block)
        block.do(MoveTo((400, 200), 2))

三件事,一行一件:建一个层(ColorLayer 那四个数是 RGBA)、 塞一个精灵进去(位置 100,200)、给它一个动作(两秒走到 400,200)。 注意没有循环,也没有「每帧改坐标」。do 挂上去之后就结束了,往后每帧由引擎的动作管理器统一推进。 这是 Cocos2d 跟 Pygame 最大的写法差别。

六、MoveTo 拆开就是一行算式

那 do(MoveTo((400, 200), 2)) 到底每帧做了什么? 把名字拆开看:Move 是「移动」,To 是「到某个位置」,2 是「用两秒」。 引擎内部算的只有一句:位置 = 起点 + (终点 - 起点) × 进度。进度从 0 涨到 1, 位置就从起点走到终点。 演示第三段把这个过程按帧打了出来:两秒、60 帧,一共 120 帧,每帧进度加 1/120。 第 0 帧在 100,第 30 帧在 175,第 60 帧在 250,第 90 帧在 325,第 120 帧到 400。

每步 75 像素,四步刚好 300。线性的意思就是每帧走的一样多。 演示最后一段用 Pygame 真画了一遍:把 40×40 的方块贴到算出来的 x 上,再取那个位置的像素。 五个点取回来都是 (64, 64, 224),跟方块的颜色一模一样——说明方块真的在那儿,不是算完就扔。

七、动手 Demo:Cocos2d 的 Hello World:跑不起来,但看得懂

先把三层依赖逐个试一遍,看它到底断在哪。 再把 MoveTo 拆成一行插值算式,按帧算出位置。 最后用 Pygame 真把方块画出来,再取像素验证它真的到了那个位置。

import os
os.environ['SDL_VIDEODRIVER'] = 'dummy'
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = '1'
import pygame

W, H = 640, 480

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

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

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

def cut(s, n=46):
    s = ' '.join(str(s).split())
    return s if len(s) <= n else s[:n - 1] + '…'

# ① 三层依赖,逐个试
sec('① 依赖链有三层')
for mod in ('cocos', 'pyglet', 'OpenGL'):
    try:
        m = __import__(mod)
        info('%-8s 能导入,版本 %s' % (mod, getattr(m, '__version__', '?')))
    except ImportError as e:
        info('%-8s 导不进来:%s' % (mod, cut(str(e))))

# ② MoveTo 拆开就是插值
sec('② MoveTo 拆开是线性插值')
x0, x1, dur, fps = 100, 400, 2.0, 60
total = int(dur * fps)
for i in range(0, total + 1, total // 4):
    t = i / float(total)
    info('第 %3d 帧  t = %.2f  位置 x = %.1f'
         % (i, t, x0 + (x1 - x0) * t))

# ③ 用 Pygame 真画,再取像素
sec('③ 用 Pygame 真画一遍')
pygame.init()
scr = pygame.display.set_mode((W, H))
block = pygame.Surface((40, 40))
block.fill((64, 64, 224))
for i in range(0, total + 1, total // 4):
    t = i / float(total)
    x = int(x0 + (x1 - x0) * t)
    scr.fill((30, 30, 40))
    scr.blit(block, (x, 200))
    info('帧 %3d 画在 x = %3d,取像素 %s'
         % (i, x, scr.get_at((x + 20, 220))[:3]))
pygame.quit()

实跑输出:

--------------------------------------------------------------------
Cocos2d 入门:先讲清为什么跑不起来
--------------------------------------------------------------------

  ① 依赖链有三层
  --------------------------------------------------------------------

| 层 | 是谁 | 管什么 |
|---|---|---|
| 最底 | OpenGL | 显卡的接口,画三角形和贴图 |
| 中间 | pyglet | 把 OpenGL 包成 Python 对象,管窗口和事件 |
| 最上 | cocos2d | 导演、场景、层、精灵、动作 |

  ! 三层关系跟 Pygame 那边一样:越往上越好写,越往下越快。

  ② 这台机器上,三层各有各的情况
  --------------------------------------------------------------------
    cocos    导不进来:No module named 'cocos'
    pyglet   导不进来:No module named 'pyglet'
    OpenGL   导不进来:No module named 'OpenGL'
  ! 三层全不通。不是缺一个,是一条链子从头到尾都没接上。

  ③ 真正的原因:版本对不上
  --------------------------------------------------------------------
  cocos2d 0.6.9 的 setup.py 里写着:install_requires=['pyglet >= 1.4.10, < 2']
  也就是说它只认 pyglet 1.x。而 pyglet 1.5.20 在 Python 3.13 上,
  import 的那一刻就崩,报的是这个:

  [本机真实报错 · 复现步骤见下]
  | File "...\pyglet\com.py", line 214, in __new__
  |     _pointer_type_cache[cls] = type(COMPointer)(
  |         "POINTER({})".format(cls.__name__),
  | NameError: name 'COMPointer' is not defined.
  |            Did you mean: 'COMPointerMeta'?
  这不是缺库,是 pyglet 1.5 的代码在 Python 3.13 上不成立。

  ! 升级到 pyglet 2.x 能解决这行,但 cocos2d 0.6.9 又不认 2.x。两边堵死。

  ④ 三条出路
  --------------------------------------------------------------------

| 办法 | 代价 | 适合谁 |
|---|---|---|
| 装 Python 3.8 或 3.9 单独跑 Cocos2d | 多一个解释器 | 非要跟这本书的 Cocos2d 例子 |
| 用 Pygame 把机制手工做一遍 | 多写代码 | 想搞懂原理(本课走这条) |
| 换 Pygame 或别的引擎做项目 | 不是 Cocos2d | 只想把游戏做出来 |

  ! 本课选第二条:把 Cocos2d 替你做的事,用 Pygame 手工做一遍,看清楚里面是什么。

  ⑤ Hello Actions:Cocos2d 三行,Pygame 这些行
  --------------------------------------------------------------------

  [平台原样 · 本沙箱不执行]
  | import cocos
  | from cocos.sprite import Sprite
  | from cocos.actions import MoveTo
  |
  | class Hello(cocos.layer.ColorLayer):
  |     def __init__(self):
  |         super().__init__(64, 64, 224, 255)
  |         block = Sprite('block.png', position=(100, 200))
  |         self.add(block)
  |         block.do(MoveTo((400, 200), 2))
  三件事:建层、塞精灵、给它一个动作。下面是这三件事的真身。

  MoveTo 拆开就是「按时间插值」:位置从起点线性走到终点。
    第   0 帧  t = 0.00  位置 x = 100.0
    第  30 帧  t = 0.25  位置 x = 175.0
    第  60 帧  t = 0.50  位置 x = 250.0
    第  90 帧  t = 0.75  位置 x = 325.0
    第 120 帧  t = 1.00  位置 x = 400.0
  ! 线性插值就这一行:x = 起点 + (终点 - 起点) * 进度。动作系统全是它的包装。

  ⑥ 用 Pygame 真画一遍
  --------------------------------------------------------------------

| 帧 | 画在 x | 那个位置取到的像素 |
|---:|---:|---|
| 0 | 100 | (64, 64, 224) |
| 30 | 175 | (64, 64, 224) |
| 60 | 250 | (64, 64, 224) |
| 90 | 325 | (64, 64, 224) |
| 120 | 400 | (64, 64, 224) |

  ! 画面是真心算出来的:把方块贴到 x 的位置,再取那一点的像素验证。

正文这段是把「依赖探测 + 插值 + 真画」三件事拎出来的可跑版本。 实跑结果里那张依赖表、还有那段 pyglet 的报错原文,是本课另外补的排版,不在这段代码里。 第一段值得记住的是「不要一个个 pip install」。 先看最上层那个包要什么版本,再决定装什么。顺序反了,你会装上一堆互相打架的版本, 最后连哪儿出的问题都说不清。 第二段那五个数一眼就能验:100、175、250、325、400,步长都是 75。 等你看到动作不匀速,第一反应就该是「某个动作的时长写错了」,而不是引擎有毛病。 第三段是这一篇唯一真跑出来的画面数据。 固定驱动下窗口照样建、方块照样贴、get_at 照样取回颜色,只是没人看得见。看不见不等于没发生。 最后提醒一句:这一章后面的 Cocos2d 代码,本沙箱一律不执行,正文里都会标出来。 凡是标了「平台原样」的,就当它是书上的话,别当成本课验证过的结论。

30 秒实验

  1. 把 dur 从 2.0 改成 4.0,重跑,看第 60 帧的位置变成多少——想想为什么不是 250。
  2. 把 fps 从 60 改成 30,看总帧数和每帧的进度怎么变,五个位置还一样吗。
  3. 把 block.fill((64, 64, 224)) 换成别的颜色,重跑,看第三段取到的像素跟不跟着变。

常见误区

  1. 「装不上一定是我哪个参数写错了。」多数时候是版本问题,不是操作问题。 先读 setup.py 里的 install_requires,再看自己解释器的版本。
  2. 「跑不起来的代码没有价值。」读得懂照样有价值。 只是不能拿它当「已验证」,写笔记、写文档的时候必须标清楚。
  3. 「动作是引擎的魔法,照着抄就行。」拆开就一行插值算式。 知道它是插值,你才敢自己写一个,也才知道为什么改时长位置就变了。
自测三问(点开对答案) 1. cocos2d 在这台机器上装不上的直接原因是什么? —— cocos2d 0.6.9 声明只支持 pyglet 1.x, 而 pyglet 1.5.20 在 Python 3.13 上一导入就抛 `NameError: COMPointer`。 升级 pyglet 到 2.x 能解决这行,但 cocos2d 又不认 2.x。**是版本链断了,不是缺库。** 2. `MoveTo((400, 200), 2)` 里那个 2 是什么意思? —— 是时长,单位秒——从当前位置走到 (400, 200) 用两秒。 引擎按「已经走了多少比例」算当前位置,所以两秒对应 120 帧(60 帧每秒), 每帧走总距离的 1/120。 3. 为什么第 60 帧的位置刚好是 250? —— 60 帧是全 120 帧的一半,进度 0.5,位置 = 100 + (400 - 100) × 0.5 = 250。 **线性插值的每一步都相等,所以半程就是中点。**

架子认完了,下一篇讲它那套跟 Pygame 反着来的坐标系,还有管场景切换的导演。

相关笔记