精灵 = 一张图 + 一个位置 + 一个锚点,剩下的属性都是在它上面加料。 动作分三类:一瞬完成的、给定时间的、还有把前两类拼起来的。 动作按秒算不按帧算——这是它跟「每帧改坐标」最根本的差别。
上一篇把树搭起来了,这一篇给树上的东西挂内容:精灵、菜单、动作。 教材 4.5 节下面四小节:【Sprite】【Menu】【Action and Effect】【小结】。 这四节基本就是 Cocos2d 的「操作手册」:拿什么显示、怎么排按钮、怎么让它动。 动作时间轴、菜单排布、两种帧率下的位置,全是真算的。
一、精灵的六个属性
精灵就是「一张图 + 一个位置 + 一个锚点」,其余全是附加。
| 属性 | 意思 |
|---|---|
| image | 用哪张图,可以是文件,也可以是纯色块 |
| position | 位置,相对父节点 |
| anchor | 拿自身的哪个点去对 position |
| scale | 缩放,1.0 是原大 |
| rotation | 旋转角度,正数是逆时针 |
| opacity | 不透明度,0 全透明、255 全实 |
锚点默认是 (0.5, 0.5),图的正中对着 position。这一条跟 Pygame 正好相反——
Pygame 贴图给的是左上角。
从 Pygame 转过来的第一个坑就在这儿:同样写 (300, 200),Pygame 里图在右边,Cocos2d 里图在正中间。
还有个写法上的差别:Cocos2d 的 scale 和 opacity 都是「属性」,挂个动作就能变;
Pygame 里透明度得在贴之前 set_alpha,是一次性的动作,不是能持续变化的量。
二、菜单就是算出来的排布
Menu 不是一个神奇的控件,它就是「一堆按钮 + 一条排布规则」。 演示算了一遍竖排:四项目,从 y = 200 起,每项隔 45,位置依次是 200、155、110、65。 排布是算出来的,不是手填的。加一项,后面的位置自己往下让——手填的话你每次加按钮都要重算一遍。
横排也一样:定一个中心 320,间距 140,两项就落在 250 和 390。 公式是「中心减去半个间距,再按序号加整数个间距」。 自己写这套排布,十行代码就够。 知道它是算出来的,你就不怕「按钮位置不对」这种问题了——去看那两个数(起点、间距)就行。
三、动作有三类
| 类别 | 特点 | 有哪些 |
|---|---|---|
| 瞬时动作 | 当帧就完成,没有时长 | Place、Hide、Show、CallFunc |
| 间隔动作 | 给一个时长,慢慢变过去 | MoveTo、RotateBy、ScaleTo、FadeIn |
| 组合动作 | 把上面两类拼起来 | Sequence、Spawn、Repeat、RepeatForever |
名字里的 To 是「到某个值」,By 是「在这个基础上加多少」。 MoveTo((400, 200), 2) 是「走到 (400, 200)」;MoveBy((40, 0), 2) 是「往右挪 40」。 差一个字母,意思完全不同。同一个位置连挂两次 MoveTo 没有叠加效果,连挂两次 MoveBy 会走两倍距离。 瞬时动作里最有用的是 CallFunc——它让你在动作序列里插一段自己的函数。 「走到位置之后播一声响」就靠它,不用自己算时间。
四、Sequence 是一条时间轴
演示把四个动作接成一条 Sequence,并把每个的起止时间算了出来:MoveTo 占 0 到 2.0 秒, DelayTime 占 2.0 到 2.5 秒,RotateBy 占 2.5 到 3.5 秒,FadeOut 占 3.5 到 5.0 秒。 整条时间轴 5.0 秒。 排队的规则只有一条:上一个走完,下一个才开始,时间自动往后接。你不用写任何时间偏移, 引擎按顺序累加。 DelayTime 是个特别有用的动作——它什么都不做,就占一段时间。 「先亮两秒再灭」这种效果,没有它就得多写一个空动作。
自己实现 Sequence 也就十几行:记一个当前时长,每帧减 delta,减到 0 就切下一个。 引擎替你做的,是把边界情况(正好切换的那一帧、重复播放、被中途 stop)都处理干净了。
五、排着走还是同时走
Sequence 是排队,Spawn 是一起。同样两个 2 秒的动作:Sequence 一共 4 秒,Spawn 一共 2 秒。 要「边移动边转」用 Spawn,要「先移动再转」用 Sequence。这两个词就能描述 90% 的游戏动画。 更进一步是 Repeat(重复固定次数)和 RepeatForever(一直重复)。 背景的云一直飘、火把一直在闪,都是 RepeatForever。
组合是可以嵌套的:Spawn 里放两个 Sequence,Sequence 里放 Repeat。 但别嵌套超过三层,超过三层就该拆成几个命名好的动作,不然半年后你自己都读不懂。
六、动作按秒算,不按帧算
这一段是整章最该记住的一条。 演示拿一个「2 秒从 0 走到 300」的动作,在 30、60、120 三种帧率下各算了一遍。 总帧数分别是 60、120、240,但 0.5 秒时的位置都是 75.0,2.0 秒时的位置都是 300.0。 因为动作里存的是「时长」和「起点终点」,每帧只用「已经过去多少比例」算一次位置。 帧率变的是采样密度,不是轨迹。
对比一下手写帧计数:if frame % 6 == 0: x += 5,这种写法把速度和帧率绑死了。
换台机器跑 120 帧,游戏就快一倍。这就是为什么老游戏换电脑会「跑得太快」。
Pygame 里要避免这个问题,就得用 clock.tick() 的返回值按真实时间算,
或者直接按「每秒多少像素 × delta 秒」来更新。动作系统只是把这件事做进了引擎。
七、动作挂在节点上,一个节点能挂好几个
挂动作就一句 do(...),挂上去就不管了,每帧由动作管理器统一推进。
一个节点可以同时挂好几个互不相干的动作:一个管移动、一个管旋转、一个管闪烁。
它们各算各的,最后叠加到同一个节点上。
要停有 stop_all_actions(),要停某一个就把动作对象存下来单独停。
做「被打断的动画」时这个很关键——不 stop 掉旧动作,新动作会和旧动作打架。
这也是新手最常踩的坑:连续点两次「跳」,角色会跳两次高度。正确做法是挂新动作之前先 stop 掉旧的。
八、Effect 是另一回事
Effect 长得像动作(也是 do(...)),但它干的事完全不同:它不是改节点属性,
而是拿显卡去扭整块画面。
Waves3D 让画面像水面一样起伏,Twirl 让画面打旋,Lens3D 做出放大镜效果。
这些都要 OpenGL 的网格变形,纯软件绘图做不出来。
特效在本沙箱验证不了:它要真实的 OpenGL 上下文,而这里的 Cocos2d 连 import 都过不去。
所以这一节只有源码,没有实跑——这一条本课如实标出来。
实用建议:特效很吃性能,手机上尤其明显。先保证玩法跑满帧,再加特效。
九、动手 Demo:菜单排布、动作时间轴与帧率无关性
先算一遍竖排菜单和横排菜单的位置,看「排布是算出来的」是什么意思。 再把四个动作接成 Sequence,算出每个的起止秒。 最后用三种帧率跑同一个动作,验证同一时刻的位置一模一样。
W, H = 640, 480
def info(s):
print(' ' + s)
def sec(t):
print(); print(' ' + t); print(' ' + '-' * 68)
# ① 竖排菜单
sec('① 竖排菜单:位置是算出来的')
y0, gap = 200, 45
for i, lab in enumerate(('开始游戏', '操作说明', '排行榜', '退出')):
info('第 %d 项 %-4s y = %d' % (i + 1, lab, y0 - i * gap))
# ② 横排菜单
sec('② 横排菜单也是一条式子')
cx, gapx = W // 2, 140
for i, lab in enumerate(('继续', '重来')):
info('%-4s x = %d' % (lab, cx - gapx // 2 + i * gapx))
# ③ Sequence 时间轴
sec('③ Sequence 是一条时间轴')
acts = [('MoveTo', 2.0), ('DelayTime', 0.5),
('RotateBy', 1.0), ('FadeOut', 1.5)]
t = 0.0
for name, dur in acts:
info('%-9s 第 %.1f 秒 到 第 %.1f 秒,占 %.1f 秒'
% (name, t, t + dur, dur))
t += dur
info('整条时间轴一共 %.1f 秒' % t)
# ④ 帧率无关
sec('④ 动作按秒算,不按帧算')
x0, x1, dur = 0, 300, 2.0
def at(sec, fps):
p = min(1.0, max(0.0, sec / dur))
return x0 + (x1 - x0) * p
for fps in (30, 60, 120):
info('%3d 帧率 共 %3d 帧 0.5 秒 x = %.1f 2.0 秒 x = %.1f'
% (fps, int(dur * fps), at(0.5, fps), at(2.0, fps)))
实跑输出:
--------------------------------------------------------------------
精灵、菜单与动作:让它动起来
--------------------------------------------------------------------
① 精灵是「一张图 + 一个位置 + 一个锚点」
--------------------------------------------------------------------
| 属性 | 意思 |
|---|---|
| image | 用哪张图,可以是文件,也可以是纯色块 |
| position | 位置,相对父节点 |
| anchor | 拿自身的哪个点去对 position |
| scale | 缩放,1.0 是原大 |
| rotation | 旋转角度,正数是逆时针 |
| opacity | 不透明度,0 全透明、255 全实 |
锚点默认是 (0.5, 0.5),也就是图的正中对着 position。
! 跟 Pygame 不一样:Pygame 贴图给的是左上角,Cocos2d 默认给的是中心。
② 菜单就是一堆按钮,加一条排布规则
--------------------------------------------------------------------
Cocos2d 的 Menu 竖着排,每项之间隔一个固定距离。
四项目,从 y = 200 起,每项隔 45:
| 第几项 | 文字 | y 坐标 |
|---:|---|---|
| 1 | 开始游戏 | 200 |
| 2 | 操作说明 | 155 |
| 3 | 排行榜 | 110 |
| 4 | 退出 | 65 |
! 排布是算出来的,不是手填的。加一项,后面的位置自己往下让。
横排也一样:定一个中心,两项各偏半个间距。
| 文字 | x 坐标 |
|---|---:|
| 继续 | 250 |
| 重来 | 390 |
中心 320,间距 140,所以两项落在 250 和 390。
③ 动作有一瞬的,也有要时间的
--------------------------------------------------------------------
| 类别 | 特点 | 有哪些 |
|---|---|---|
| 瞬时动作 | 当帧就完成,没有时长 | Place、Hide、Show、CallFunc |
| 间隔动作 | 给一个时长,慢慢变过去 | MoveTo、RotateBy、ScaleTo、FadeIn |
| 组合动作 | 把上面两类拼起来 | Sequence、Spawn、Repeat、RepeatForever |
! 名字里的 To 是「到某个值」,By 是「在这个基础上加多少」。差一个字母,意思反。
④ Sequence 是一条时间轴
--------------------------------------------------------------------
把四个动作接成一个 Sequence:
MoveTo 第 0.0 秒 到 第 2.0 秒,占 2.0 秒
DelayTime 第 2.0 秒 到 第 2.5 秒,占 0.5 秒
RotateBy 第 2.5 秒 到 第 3.5 秒,占 1.0 秒
FadeOut 第 3.5 秒 到 第 5.0 秒,占 1.5 秒
整条时间轴一共 5.0 秒
! Sequence 就是排队:上一个走完,下一个才开始,时间自动往后接。
⑤ 排着走还是同时走
--------------------------------------------------------------------
Sequence 是排队走,Spawn 是一起走。同样两个 2 秒的动作:
Sequence(MoveTo, RotateBy) 一共 4.0 秒
Spawn(MoveTo, RotateBy) 一共 2.0 秒
! 要「边移动边转」用 Spawn,要「先移动再转」用 Sequence。
⑥ 动作按秒算,不按帧算
--------------------------------------------------------------------
MoveTo 从 x = 0 走到 x = 300,耗时 2.0 秒。同一时刻,不同帧率:
| 帧率 | 总帧数 | 0.5 秒时 x | 2.0 秒时 x |
|---:|---:|---:|---:|
| 30 | 60 | 75.0 | 300.0 |
| 60 | 120 | 75.0 | 300.0 |
| 120 | 240 | 75.0 | 300.0 |
帧率差四倍,同一时刻的位置一模一样——都是 75.0 和 300.0。
! 帧率高低只影响动作顺不顺,不影响动作走多久。
手写「每 6 帧挪一次」那种计数法就做不到这点,改帧率动作就变快变慢。
⑦ 动作是挂在节点上的,一个节点可以同时挂好几个
--------------------------------------------------------------------
sprite.do(MoveTo((400, 300), 2)) 走过去
sprite.do(RotateBy(360, 2)) 同时转一圈
sprite.do(RepeatForever(blink)) 一直闪
! 挂上去就不用管了,每帧由动作管理器统一推进。要停就 stop_all_actions()。
⑧ Effect 是另一回事
--------------------------------------------------------------------
[平台原样 · 本沙箱不执行]
| from cocos.effect import Waves3D
| self.do(Waves3D(waves=3, amplitude=20, duration=3))
特效是拿显卡去扭整块画面,靠的是 OpenGL 的网格变形。
! 特效在本沙箱验证不了:它要真实的 OpenGL 上下文,而这里的 Cocos2d 连 import 都过不去。
这段代码把「排布、时间轴、帧率无关」三件事各拆成一小截,没有依赖 Cocos2d,纯 Python 就能跑。
数是对的,因为算的就是那几条式子。
第一段那两个数(200 和 45)就是全部的秘密。
按钮位置不对,永远是这两个数之一写错了,或者加了项忘了重算。
第三段那个 t 变量越加越大,这就是 Sequence 的全部实现。
真引擎里还多一层:每帧算「当前该由第几个动作负责」,以及切换那一帧的收尾。
第四段是全篇最值钱的一小段。三种帧率、同样两个时刻、位置一个像素都不差。
记住这个结论,你以后就不会写出「跳帧游戏就变快」的代码。
最后说清楚边界:Effect 那一节本课没跑,只贴了源码。
没跑过的就得标成没跑过,这是笔记可信度的底线。
30 秒实验
- 把竖排菜单的
gap从 45 改成 60,重跑,看四项目的位置变成多少。 - 在 Sequence 里删掉那个 DelayTime,重算整条时间轴该多长,再跑对答案。
- 把
dur改成 1.0,重跑,看 0.5 秒时的位置变成多少——想想为什么是一半的一半。
常见误区
- 「To 和 By 差不多,都是移动。」差远了。To 是到绝对值,By 是加相对量。 两个 MoveTo 连挂位置不变,两个 MoveBy 连挂距离翻倍。
- 「动作挂上去之后想改就再挂一个。」新旧动作会同时跑,互相抢同一个属性。要先 stop 掉旧的。
- 「帧率越高动作越快。」不会。动作按秒算,帧率只影响平不平滑。 会变快的只有「每帧固定加多少」那种手写计数。
自测三问(点开对答案)
1. `MoveTo((400, 200), 2)` 和 `MoveBy((40, 0), 2)` 有什么区别? —— MoveTo 是走到绝对位置 (400, 200);MoveBy 是在当前位置上再挪 40 像素。 连挂两次 MoveTo 还是停在 (400, 200),连挂两次 MoveBy 会一共挪 80 像素。 2. Sequence 里放 MoveTo(2 秒) 和 RotateBy(1 秒),整条多长?换成 Spawn 呢? —— Sequence 是排队,2 + 1 = 3 秒。Spawn 是同时,取长的那个,2 秒。 **一个加起来,一个取最大值,这是两者的本质区别。** 3. 为什么帧率从 30 提到 120,同一个动作的终点位置和时间都不变? —— 因为动作内部存的是时长和起止值,每帧只用「已过时间 / 总时长」算进度。 帧率提高只是采样点变密,轨迹一模一样。**变化的是平滑度,不是速度。**到这儿 Cocos2d 的基础件都认完了。 下一篇换一本教材里的第二个项目:俄罗斯方块,看这些件怎么拼成一个能玩的游戏。
- ☐ (本篇完,下一篇 5.1 俄罗斯方块:规划与菜单场景)