Cocos Creator 2.x 实战笔记:动画、事件、向量、资源与避坑指南
- 约 10 分钟读完 - 全文 3685 字Cocos Creator 2.x 实战笔记:动画、事件、向量、资源与避坑指南
本文基于 Cocos Creator 2.x(
cc.*API)整理,覆盖从动作动画、输入事件、位置向量、场景管理、音效、资源加载到摄像机跟随等高频知识点。虽然 3.x 已全面转向 TypeScript 与新的组件系统,但 2.x 的核心思想——节点树 + 组件 + 生命周期 + Action 系统——仍然是理解引擎设计的最佳入口。
一、高层次思想:Cocos Creator 在组织什么?
Cocos Creator 的核心理念可以概括为一句话:
“节点(Node)负责层级与变换,组件(Component)负责行为与表现。”
graph TD
A[Scene 场景] --> B[Canvas]
B --> C[Node 节点]
C --> D[Transform 变换]
C --> E[Sprite 渲染]
C --> F[Script 脚本]
C --> G[AudioSource 音效]
C --> H[PhysicsCollider 物理]
D --> I[position / rotation / scale]
F --> J[onLoad / start / update]
1.1 节点树 = 变换层级
每个 cc.Node 都有父节点,子节点的变换会继承父节点。理解这一点才能避免“为什么我改了坐标没效果?”这类问题。
World Space(世界坐标系)
│
├── Canvas (Design Resolution: 720x1280)
│ ├── Player (position: 0, -400)
│ │ ├── Head (position: 0, 80) ← 子节点相对父节点
│ │ └── Gun (position: 60, 0)
│ └── Enemy
1.2 组件生命周期
stateDiagram-v2
[*] --> onLoad: 场景初始化
onLoad --> onEnable: 节点激活
onEnable --> start: 第一次 update 前
start --> update: 每帧调用
update --> lateUpdate: update 之后
lateUpdate --> update: 下一帧
update --> onDisable: 节点禁用
onDisable --> onEnable: 重新激活
onDisable --> onDestroy: 销毁节点
避坑指南 #1:
onLoad中适合获取节点引用和初始化数据;start中适合访问其他组件(此时场景中所有节点的onLoad都已执行完毕)。不要把耗时的初始化放在update里。
二、动作系统(Action):让节点“动起来”
Action 是 Cocos Creator 2.x 中非常优雅的动画封装。它基于组合模式:简单动作可以拼接成复杂序列。
2.1 跳跃动画:位移 + 缓动曲线
// 跳跃上升
var jumpUp = cc.moveBy(this.jumpDuration, cc.v2(0, this.jumpHeight))
.easing(cc.easeCubicActionOut());
// 下落
var jumpDown = cc.moveBy(this.jumpDuration, cc.v2(0, -this.jumpHeight))
.easing(cc.easeCubicActionIn());
// 不断重复
return cc.repeatForever(cc.sequence(jumpUp, jumpDown));xychart-beta
title "跳跃高度随时间变化"
x-axis [0, 0.25, 0.5, 0.75, 1.0]
y-axis "高度" 0 --> 100
line [0, 80, 100, 80, 0]
避坑指南 #2:
moveBy是相对移动,moveTo是绝对移动。跳跃一般用moveBy,因为落地位置取决于起跳点。如果误用moveTo,角色会跳到固定世界坐标。
2.2 缩放与回调:序列动作
cc.sequence(
cc.scaleTo(dua, 0.8, 0.8),
cc.scaleTo(dua, 1, 1),
cc.callFunc(() => {
paopaoNode.destroy();
})
);2.3 旋转
var rotationTo = cc.rotateTo(1, 0, 180);
this.paiNode.runAction(rotationTo);2.4 左右晃动(以锚点为原点)
var seq = cc.sequence(
cc.rotateTo(0.2, 15),
cc.rotateTo(0.4, -15),
cc.rotateTo(0.4, 15),
cc.rotateTo(0.4, -15),
cc.rotateTo(0.4, 15),
cc.rotateTo(0.2, 0),
cc.delayTime(2)
);
this.node.runAction(cc.repeatForever(seq));graph LR
A[初始 0°] --> B[右偏 +15°]
B --> C[左偏 -15°]
C --> D[右偏 +15°]
D --> E[左偏 -15°]
E --> F[回正 0°]
F --> G[停顿 2s]
G --> A
避坑指南 #3:旋转方向遵循引擎约定,正数为顺时针,负数为逆时针。
rotateTo会尽量选择最短路径到达目标角度;如果需要连续多圈旋转,请用rotateBy。
2.5 点击时按钮放大
// 在按钮的触摸开始/结束时分别放大与恢复
buttonNode.on(cc.Node.EventType.TOUCH_START, () => {
buttonNode.runAction(cc.scaleTo(0.1, 1.1, 1.1));
});
buttonNode.on(cc.Node.EventType.TOUCH_END, () => {
buttonNode.runAction(cc.scaleTo(0.1, 1.0, 1.0));
});三、输入事件:键盘、触摸与鼠标
3.1 键盘事件
注册:
cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this);实现监听函数:
onKeyDown(event) {
switch(event.keyCode) {
case cc.macro.KEY.a:
this.accLeft = true;
break;
case cc.macro.KEY.d:
this.accRight = true;
break;
}
}避坑指南 #4:第三个参数
this不要漏!否则回调里的this会指向cc.systemEvent,导致this.accLeft为undefined。建议在onDestroy中移除监听,避免场景切换后事件泄漏。
3.2 触摸 / 鼠标事件
this.node.on('touchstart', this.touchStart, this);
this.node.on('touchend', this.touchEnd, this);枚举与事件名对照:
| 枚举 | 字符串事件名 | 说明 |
|---|---|---|
cc.Node.EventType.TOUCH_START |
touchstart |
手指按下 |
cc.Node.EventType.TOUCH_MOVE |
touchmove |
手指移动 |
cc.Node.EventType.TOUCH_END |
touchend |
手指抬起(在节点内) |
cc.Node.EventType.TOUCH_CANCEL |
touchcancel |
系统中断(如来电) |
cc.Node.EventType.MOUSE_DOWN |
mousedown |
鼠标按下 |
cc.Node.EventType.MOUSE_ENTER |
mouseenter |
鼠标进入节点 |
cc.Node.EventType.MOUSE_MOVE |
mousemove |
鼠标移动 |
cc.Node.EventType.MOUSE_LEAVE |
mouseleave |
鼠标离开节点 |
cc.Node.EventType.MOUSE_UP |
mouseup |
鼠标抬起 |
cc.Node.EventType.MOUSE_WHEEL |
mousewheel |
滚轮滚动 |
四、位置向量 Vec2:2D 游戏的数学基础
4.1 生成与设置位置
var pos = cc.v2(randX, randY);
newStar.setPosition(pos);4.2 距离、长度与运算
// 计算当前节点与玩家之间的距离
var dist = this.node.position.sub(playerPos).mag();
// 返回向量长度
var len = dir.mag();
// 自加(修改原向量)
v.addSelf(cc.v2(5, 5));
// 返回新向量(不修改原向量)
var vnew = v.add(cc.v2(5, 5));
// 缩放
var v = cc.v2(10, 10);
v.mul(5); // 返回 Vec2 {x: 50, y: 50}
console.log(v); // 仍打印 Vec2 {x: 10, y: 10}
// 判断相等
var res = v.equals(cc.v2(10, 10)); // true
避坑指南 #5:
addSelf、mulSelf、subSelf等方法会修改原向量;不带Self的版本会返回新向量。在update中计算方向时,若不想意外修改this.node.position,请使用非Self版本。
4.3 线性插值(Lerp)
var v = cc.v2(10, 10);
var v1 = cc.v2(5, 5);
var v3 = cc.v2(0, 0); // 存储结果
v.lerp(v1, 0.5, v3); // 在 v 与 v1 之间取 50%
console.log(v3); // Vec2 {x: 7.5, y: 7.5}
4.4 坐标转换
| 方法 | 原点 | 用途 |
|---|---|---|
convertToWorldSpaceAR(point) |
节点锚点 | 局部坐标 → 世界坐标 |
convertToNodeSpaceAR(point) |
节点锚点 | 世界坐标 → 局部坐标 |
convertToWorldSpace(point) |
节点左下角 | 不建议,除非 UI 特殊需求 |
convertToNodeSpace(point) |
节点左下角 | 不建议 |
graph LR
subgraph 世界坐标系
direction TB
A[World Origin] --> B[Player]
end
subgraph 局部坐标系
direction TB
C[Anchor Point] --> D[(100, 100)]
end
B -->|convertToWorldSpaceAR| D
D -->|convertToNodeSpaceAR| B
避坑指南 #6:Cocos 编辑器默认以锚点(Anchor)为原点,因此优先使用带 AR 的方法。左下角为原点的方法主要用于兼容旧项目或特殊 UI 对齐。
五、场景与生命周期
5.1 重新加载场景
cc.director.loadScene('game');避坑指南 #7:
loadScene会销毁当前场景所有节点。如果需要在切换前保存数据(如分数、关卡进度),请先写入本地存储,再调用加载。
5.2 暂停与恢复
// 暂停游戏逻辑,但渲染和 UI 响应仍在
cc.director.pause();
// 恢复
cc.director.resume();常用于弹出结算面板或暂停菜单。
六、透明度、音效与组件
6.1 透明度渐变(实现缓慢消失)
update(dt) {
this.node.opacity -= 100 * dt;
if (this.node.opacity <= 0) {
this.node.destroy();
}
}6.2 音效
properties: {
scoreAudio: {
default: null,
type: cc.AudioClip
}
}
// 播放音效(false 表示不循环)
cc.audioEngine.playEffect(this.scoreAudio, false);避坑指南 #8:背景音乐请用
playMusic,音效用playEffect。微信小游戏对同时播放的音频数量有限制,过量的playEffect可能导致后续音效被丢弃。
6.3 getComponent:获取组件
newStar.getComponent('Star').game = this;避坑指南 #9:
getComponent是Node的方法,JS 中的方法和属性都挂在组件上。不要把脚本逻辑直接写在场景或节点上,而要写在组件脚本里。
七、屏幕、包围盒与资源加载
7.1 获取视图大小
var winSize = cc.director.getWinSize();7.2 包围盒
var first_xMax = bgList[0].getBoundingBox().xMax;getBoundingBox() 返回父节坐标系下的轴向对齐包围盒(AABB),包含 xMin、xMax、yMin、yMax、width、height 等属性。
7.3 加载图片
cc.loader.load(
{ url: "res/raw-assets/resource/texture/gold.png", type: 'png' },
(err, texture) => {
var fra = this.spr2.getComponent(cc.Sprite);
var sframe = new cc.SpriteFrame(
texture,
new cc.rect(20, 20, 100, 120)
);
fra.spriteFrame = sframe;
}
);cc.rect(20, 20, 100, 120) 表示从图片的 (20, 20) 开始,取宽 100、高 120 的矩形区域,即图片中 (20, 20) 到 (120, 140) 的内容。
避坑指南 #10:微信小游戏对图片尺寸有限制,长宽都不能超过 2048。超出此限制会导致加载失败或显示异常,导入资源前请先检查尺寸。
7.4 加载 Prefab
cc.loader.loadRes(`prefab/game08/${randIdx}`, cc.Prefab, (err, res) => {
this.drawing = cc.instantiate(res);
this.drawing.group = "drawing";
});八、本地存储
// 写入
cc.sys.localStorage.setItem("finishLevel", this.curLevel);
// 读取(注意转换类型,localStorage 只存字符串)
var level = parseInt(cc.sys.localStorage.getItem("finishLevel") || 0);避坑指南 #11:
localStorage只能存储字符串。存储对象时请先JSON.stringify,读取后再JSON.parse。微信小游戏有本地存储上限,重要数据建议结合云端存档。
九、摄像机跟随
9.1 主角简单跟随
// this.node 为 Camera 节点
update(dt) {
var targetPos = this.player.position;
this.node.position = targetPos;
}9.2 主角自然跟随(平滑插值)
update(dt) {
var targetPos = this.player.position;
var currentPos = this.node.position;
currentPos.lerp(targetPos, 0.1, currentPos);
this.node.position = currentPos;
}graph LR
A[Camera 当前位置] -->|lerp 0.1| B[Player 当前位置]
B --> C[下一帧继续插值]
C --> D[最终重合]
十、调试、绘制与更新策略
10.1 cc.log
cc.log("只在发布时勾选调试模式才起效");10.2 Graphics 画图
var g = this.node.getComponent(cc.Graphics);
g.moveTo(0, 0);
g.lineTo(100, 100);
g.stroke();10.3 打包更新
- 发布时勾选 MD5 Cache,避免浏览器/微信缓存旧资源。
- 热更新需配合
cc.assetManager.loadBundle或官方热更新方案。
cc.assetManager.loadBundle('level2', (err, bundle) => {
bundle.loadDir('prefabs', (err, assets) => {
// ...
});
});十一、物理关节:旋转关节 RevoluteJoint
// cc.RevoluteJoint 可约束两个刚体围绕一个点旋转
var joint = nodeA.addComponent(cc.RevoluteJoint);
joint.connectedBody = nodeB.getComponent(cc.RigidBody);
joint.anchor = cc.v2(0, 0);
joint.connectedAnchor = cc.v2(0, 50);常用于门铰链、钟摆、车轮等效果。
十二、帧率与屏幕适配
12.1 锁定帧率
let fps = (cc.sys.os == cc.sys.OS_IOS) ? 40 : 40;
cc.game.setFrameRate(fps);
if (cc.sys.platform == cc.sys.WECHAT_GAME) {
wx.setPreferredFramesPerSecond(fps);
}12.2 分辨率适配
let frameSize = cc.view.getFrameSize();
let scrall = frameSize.width / frameSize.height;
cc.view.setResolutionPolicy(cc.ResolutionPolicy.EXACT_FIT);
cc.view.setOrientation(cc.macro.ORIENTATION_LANDSCAPE);
cc.Canvas.instance.fitWidth = false;
cc.Canvas.instance.fitHeight = false;
let isFitWidth = scrall < 1.77;
cc.Canvas.instance.fitWidth = isFitWidth;
cc.Canvas.instance.fitHeight = !isFitWidth;12.3 常用设计分辨率
| 分辨率 | 比例 | 适用场景 |
|---|---|---|
| 720 × 1280 | 9:16 | 竖屏手游最常见 |
| 640 × 960 | 2:3 | 早期 iPhone 经典 |
| 1080 × 1920 | 9:16 | 高清竖屏 |
| 1280 × 720 | 16:9 | 横屏手游 |
graph TD
A[设计分辨率 720x1280] --> B[实际屏幕分辨率]
B --> C{选择适配策略}
C -->|FIT_WIDTH| D[宽度铺满,高度可能裁剪/留黑]
C -->|FIT_HEIGHT| E[高度铺满,宽度可能裁剪/留黑]
C -->|EXACT_FIT| F[拉伸铺满,可能变形]
C -->|SHOW_ALL| G[等比缩放,全部可见,可能留黑]
C -->|NO_BORDER| H[等比缩放,铺满屏幕,可能裁剪]
十三、网络同步:Colyseus
对于多人实时游戏,Colyseus 是 Cocos Creator 常用的 Node.js 状态同步框架。
| 同步方式 | 核心思想 | 适用场景 |
|---|---|---|
| 状态同步 | 服务端权威,客户端插值预测 | MMO、卡牌、休闲对战 |
| 帧同步 | 只同步输入,客户端各自演算 | RTS、MOBA、格斗 |
避坑指南 #12:帧同步对浮点数一致性和随机种子要求极高。Cocos 2.x 默认使用 JavaScript 浮点运算,跨平台(iOS/Android/微信)可能出现不一致,需统一数学库与随机算法。
十四、避坑清单速查表
| 编号 | 避坑点 |
|---|---|
| 1 | 初始化放 onLoad,跨组件访问放 start |
| 2 | moveBy 相对移动,moveTo 绝对移动,别混用 |
| 3 | 旋转方向:正顺负逆;多圈旋转用 rotateBy |
| 4 | 注册事件必须传 this,切场景时记得 off |
| 5 | 向量运算区分 Self 与非 Self 版本 |
| 6 | 坐标转换优先用带 AR 的方法 |
| 7 | loadScene 前保存必要数据 |
| 8 | 音乐用 playMusic,音效用 playEffect |
| 9 | 逻辑写在组件脚本里,不要直接挂在节点上 |
| 10 | 微信小游戏图片尺寸不能超过 2048×2048 |
| 11 | localStorage 只存字符串,对象需 JSON 序列化 |
| 12 | 帧同步注意浮点一致性与随机种子 |
十五、总结
Cocos Creator 2.x 的开发可以归纳为以下流程:
graph LR
A[搭建场景节点树] --> B[挂载脚本组件]
B --> C[处理输入事件]
C --> D[在 update 中更新逻辑]
D --> E[使用 Action 或逐帧修改属性实现动画]
E --> F[加载资源 / 切换场景 / 保存数据]
掌握 节点-组件模型、生命周期、Action 组合、Vec2 运算、坐标转换、屏幕适配 这六大核心后,2D 游戏开发中大部分问题都能迎刃而解。希望这份笔记能成为你项目中的速查手册。
参考
- Cocos Creator 官方文档 - 快速上手
- Cocos Creator 官方论坛 - Colyseus 状态同步
- CSDN - Cocos Creator 帧同步 + TypeScript 定义