一、先聊聊游戏开发这件事

1.1 游戏引擎到底是干什么的
游戏引擎可以理解成一套专门为游戏开发准备的基础设施。
如果没有游戏引擎,开发者需要自己从零处理很多底层问题,比如:
-
画面怎么渲染到屏幕上
-
动画怎么播放
-
碰撞怎么检测
-
玩家视角怎么跟随
有了游戏引擎之后,开发者就不用每次都重复造这些底层轮子,而是把更多精力放在真正的业务逻辑上,比如:
-
角色怎么移动
-
关卡怎么玩
-
UI 怎么交互
-
分数、血量、奖励这些规则怎么设计
从前端的角度类比,可以把游戏引擎理解成:
它不是某一个组件库,而更像是把渲染、事件、动画、物理、资源管理这些能力都集成在一起的一套"运行时 + 编辑器 + 开发框架"。
所以游戏引擎解决的核心问题,不是"帮你写业务",而是:帮你把游戏运行所依赖的底层能力准备好,让你专注在玩法和交互本身。
1.2 几个常见游戏引擎简单对比一下
| 引擎 | 定位 | 语言 | 平台支持 | 适用场景 | 代表作品 | 收费模式 | GitHub Stars |
|---|---|---|---|---|---|---|---|
| Unity | 全能型,2D/3D | C# | 全平台 | 独立游戏、手游、VR/AR | 《原神》《炉石传说》《空洞骑士》 | 免费 + 订阅 | 不开源 |
| Godot | 开源全能 | GDScript / C# | 全平台 | 独立游戏 | 《Brotato》《穹顶守护者》《Cassette Beasts》 | 完全开源免费 | 107k |
| Unreal Engine | 高品质 3D | C++ / Blueprint | 全平台 | 3A 大作、影视、虚拟制片 | 《黑神话·悟空》《绝地求生》《最终幻想 VII 重制版》 | 免费(收入超 100 万抽成 5%) | 私有仓库 |
| Cocos Creator | 轻量 2D/3D | TypeScript | H5、小游戏、原生 | 休闲手游、小游戏、教育 | 《开心消消乐》《捕鱼达人》《跳一跳》 | 完全开源免费 | 9.4k |
| Egret | H5 轻量 2D | TypeScript | H5、小游戏 | H5 营销游戏 | 《传奇来了》《热血战歌》 | 免费(部分服务收费) | 4k |
| Laya Air | 高性能 H5 | TypeScript / AS3 | H5、小游戏、原生 | 重度 H5 游戏 | 《神仙道 3》《传奇世界之仗剑天涯》 | 免费 | 2k |
从前端视角看,Cocos Creator 的几个优势很直接:
-
使用 TypeScript
-
编辑器所见即所得,适合快速搭 UI 和场景
-
对 H5、微信小游戏、小游戏平台 支持比较友好
-
开源,且文档友好、社区完善
二、先把 Cocos 的基本认知搭起来
2.1 先认识一下 Cocos 编辑器

- 层级管理器(Hierarchy):用来看当前场景里的节点树,谁是谁的父子节点,一眼就能看出来
- 场景编辑器(Scene):用来直接摆放节点、调整位置、缩放、旋转,是最直观的可视化编辑区域
- 资源管理器(Assets):查看项目里的图片、脚本、动画、预制资源等内容
- 属性检查器(Inspector):选中某个节点后,在这里看它挂了哪些组件、改哪些属性
- 控制台(Console):查看运行日志、报错信息,排查问题时非常常用
- 预览 / Game 视图:查看游戏真正运行起来之后的效果
层级管理器看结构,场景编辑器负责摆东西,属性检查器负责改属性,资源管理器管资源,控制台用来看问题。
2.2 场景、节点、组件,先把这几个词搞明白
可以把 Cocos 先理解成一句话:
场景里放节点,节点上挂组件。组件里写逻辑
一个典型场景长这样:

Scene
├── Main Camera(Node)
│ └── Camera(Component)
└── Canvas(Node)
├── Background(Node)
│ └── Sprite(Component)
├── Player(Node)
│ ├── Sprite(Component)
│ ├── Animation(Component)
│ ├── BoxCollider2D(Component)
│ └── PlayerController(Script Component)
└── UI(Node)
├── ScoreLabel(Node)
│ └── Label(Component)
└── StartButton(Node)
└── Button(Component)
- Scene:一个完整页面或一个关卡
- Node:场景树里真正组织层级的是节点,节点负责承载位置、旋转、缩放等信息
- Component:组件是挂在节点上的能力,节点本身不直接负责"显示图片"或"处理碰撞"
- Camera 组件 :比如
Camera,通常挂在Main Camera这类节点上,负责决定场景内容如何被渲染出来 - 渲染组件 :比如
Sprite,负责把图片显示出来 - 动画组件 :比如
Animation,负责播放动画 - 物理组件 :比如
RigidBody2D、BoxCollider2D,负责受力和碰撞 - 脚本组件 :比如
PlayerController,通常用来写业务逻辑 - UI 组件 :比如
Label、Button,负责界面显示和交互
2.3 生命周期是怎么一回事
typescript
import { _decorator, Component } from "cc";
const { ccclass } = _decorator;
@ccclass("LifeCircle")
export class LifeCircle extends Component {
onLoad() {
console.log("onLoad: 组件初始化完成");
}
onEnable() {
console.log("onEnable: 组件变为启用状态");
}
start() {
console.log("start: 第一帧渲染前执行一次");
}
update(deltaTime: number) {
console.log("update: 每一帧都会执行", deltaTime);
}
onDisable() {
console.log("onDisable: 组件被禁用");
}
}
-
onLoad:组件刚初始化时执行,适合拿引用、设默认值 -
onEnable:组件或节点变为启用状态时执行,适合注册事件 -
start:在第一帧渲染前执行一次,适合放依赖初始化完成后的逻辑 -
update:每一帧都会执行,适合持续逻辑,比如移动、倒计时、状态刷新 -
onDisable:组件或节点被禁用时执行,适合解绑事件、停止监听
注意事项:
-
不要把大量初始化工作塞进
update -
注册了事件就要记得在
onDisable里解绑 -
update会高频执行,分享时一定要提醒大家不要在里面乱写重逻辑
2.4 节点上最常改的几个属性
-
位置
position:节点在父节点坐标系里的位置 -
旋转
angle:节点旋转了多少度 -
缩放
scale:节点放大或缩小了多少 -
移动性
Mobility:告诉引擎这个节点是不是会经常变化,常见值有Static、Stationary、Movable -
层级
Layer:决定这个节点属于哪一层,常用来配合Camera控制"哪些内容会被渲染出来" -
尺寸
contentSize:UI 节点有多宽多高,通常通过UITransform查看 -
锚点
anchorPoint:UI 节点以哪个点作为定位和旋转基准,通常也在UITransform上
typescript
import { _decorator, Component, Quat, UITransform, Vec3 } from "cc";
const { ccclass } = _decorator;
@ccclass("NodePropertyDemo")
export class NodePropertyDemo extends Component {
start() {
const uiTransform = this.node.getComponent(UITransform);
console.log("当前节点名称:", this.node.name);
// 1. 位置
// this.node.setPosition(new Vec3(100, 50, 0));
console.log("相对于父节点的坐标 position:", this.node.position);
console.log("世界坐标 worldPosition:", this.node.worldPosition);
// 2. 旋转
// this.node.angle = 30;
console.log("相对于父节点的旋转 angle:", this.node.angle);
console.log(
"世界旋转 worldRotation:",
Quat.toEuler(new Vec3(), this.node.worldRotation)
);
// 3. 缩放
// this.node.setScale(new Vec3(1.2, 1.2, 1));
console.log("相对于父节点的缩放 scale:", this.node.scale);
console.log("世界缩放 worldScale:", this.node.worldScale);
// 4. UI 尺寸和锚点
console.log("尺寸 contentSize:", uiTransform?.contentSize);
console.log("锚点 anchorPoint:", uiTransform?.anchorPoint);
if (this.node.parent) {
console.log("父节点名称:", this.node.parent.name);
}
}
}
-
Mobility: -
Static:静态节点,运行时基本不动,比如背景、UI
-
Stationary:介于静态和动态之间,位置通常不怎么变,但某些属性可能会变化,比如路灯、炮台
-
Movable:可移动节点,运行时可能频繁移动、旋转、缩放,比如角色、子弹
-
Layer:分层,对节点进行分层,比如 UI 在 UI 层,角色在角色层,背景在背景层,通过 Camera 组件来控制哪些层级会被渲染出来,也可以自定义 -
子节点自己的
position没变,但父节点一动,它的worldPosition就会跟着变 -
如果节点明明在场景树里,但显示位置不对,优先检查父节点、锚点、缩放和 Canvas 适配方式
-
位置、旋转、缩放这些属性都挂在 node 上,但它们的表现会受到父节点层级关系影响,比如子节点自己的
position没变,但父节点一动,它的worldPosition就会跟着变 -
尺寸和锚点这类 UI 信息,很多时候要通过
UITransform来看
2.5 资源一般怎么处理
对初学者来说,资源管理只需要先理解两种方式:
-
编辑器里拖拽引用或者添加资源组件
-
代码里动态加载
typescript
import {
_decorator,
AudioClip,
AudioSource,
Button,
Component,
Label,
Node,
director,
resources,
} from "cc";
const { ccclass } = _decorator;
@ccclass("PlayBgm")
export class PlayBgm extends Component {
private audioSource: AudioSource | null = null;
private audioClip: AudioClip | null = null;
private buttonLabel: Label | null = null;
private isLoading = false;
private isPaused = false;
onLoad() {
const scene = director.getScene();
if (!scene) {
console.warn("当前场景不存在");
return;
}
const audioNode = new Node("RuntimeAudio");
scene.addChild(audioNode);
this.audioSource = audioNode.addComponent(AudioSource);
this.audioSource.loop = true;
this.buttonLabel = this.node.getComponentInChildren(Label);
this.updateButtonText("开始播放");
}
onEnable() {
this.node.on(Button.EventType.CLICK, this.onClickPlaySound, this);
}
onDisable() {
this.node.off(Button.EventType.CLICK, this.onClickPlaySound, this);
}
private onClickPlaySound() {
if (!this.audioSource) {
console.warn("AudioSource 未创建");
return;
}
if (this.isLoading) {
return;
}
// 如果音频未加载,则加载并播放音频
if (!this.audioClip) {
this.isLoading = true;
this.updateButtonText("加载中...");
console.log("开始加载音频");
resources.load("audio/bgm", AudioClip, (err, clip) => {
this.isLoading = false;
if (err || !clip) {
this.updateButtonText("开始播放");
console.error("加载音频失败", err);
return;
}
this.audioClip = clip;
this.audioSource!.clip = clip;
this.audioSource!.play();
this.isPaused = false;
this.updateButtonText("暂停播放");
});
return;
}
// 如果音频已加载,则播放或暂停音频
if (this.isPaused) {
this.audioSource.play();
this.isPaused = false;
this.updateButtonText("暂停播放");
return;
}
// 如果音频正在播放,则暂停音频
this.audioSource.pause();
this.isPaused = true;
this.updateButtonText("继续播放");
}
private updateButtonText(text: string) {
if (!this.buttonLabel) {
return;
}
this.buttonLabel.string = text;
}
}
-
需动态加载的内容,必须要放到
resources目录下,否则无法加载 -
加载资源不需要写文件类型后缀,也不需要写
resources前缀
2.6 物理和碰撞,先抓最常用的部分
物理系统只需要先掌握 3 个概念:刚体、碰撞体、传感器。
最容易理解的说法是:
-
刚体决定这个物体怎么"动" -
碰撞体决定这个物体怎么"撞" -
传感器只检测碰撞触发回调,不产生实际的碰撞效果
在属性检查器里,刚体和碰撞体最常见、最值得先认识的属性有这些:
刚体(RigidBody2D) 常用属性
-
Type:刚体类型,决定这个物体怎么参与物理模拟。常见选项有: -
Dynamic:会受重力、会受力、会碰撞,适合玩家、箱子、球、会掉落的道具 -
Static:自己不动,但别人可以撞到它,适合地面、墙、平台、边界 -
Kinematic:可以按代码移动,但不受重力和物理力驱动,适合移动平台、轨道机关、巡逻障碍物 -
Gravity Scale:重力缩放,值越大,下落越快;设为0就不受重力影响 -
Linear Velocity:线速度,控制它当前往哪个方向移动 -
Linear Damping:线性阻尼,可以理解成"空气阻力",值越大越容易慢下来 -
Angular Velocity:角速度,控制旋转速度 -
Angular Damping:角阻尼,控制旋转时衰减得有多快 -
Fixed Rotation:是否锁定旋转。常见选项有: -
开启:只允许平移,不允许碰撞后翻滚,适合平台跳跃角色、人物、可控主角 -
关闭:允许物体自然旋转,适合箱子、木桶、球、被撞后会翻滚的物体 -
Allow Sleep:是否允许物体静止后进入休眠状态。常见选项有: -
开启:物体静止后减少物理计算,适合大多数普通场景物体 -
关闭:即使静止也持续参与计算,适合需要持续精确检测或特殊逻辑控制的对象
3 种刚体类型:
-
Dynamic:会受力、会碰撞,常用于玩家、箱子 -
Static:自己不动,常用于地面、墙 -
Kinematic:自己按代码移动,但不受物理力驱动
碰撞体(Collider2D)常用属性
-
碰撞体类型:2D 里常见的碰撞体主要有: -
BoxCollider2D:盒子碰撞体,最常用,适合地面、墙、平台、箱子、按钮区域 -
CircleCollider2D:圆形碰撞体,适合球、金币、圆形检测范围 -
PolygonCollider2D:多边形碰撞体,适合形状不规则的地形、障碍物、特殊区域 -
Size / Radius / Points:不同碰撞体类型会有不同的尺寸字段,比如盒子看Size,圆形看Radius,多边形看Points -
Offset:碰撞体相对节点中心的偏移 -
Density:密度,会影响质量计算 -
Friction:摩擦力,值越大越不容易滑动 -
Restitution:弹性,值越大越容易反弹 -
Sensor:是否是传感器。常见选项有: -
开启:只检测接触,不产生碰撞反弹,适合金币收集区、触发区域、检测范围 -
关闭:既检测接触,也产生正常碰撞,适合地面、墙、箱子、角色实体 -
Tag:业务标记,便于在代码里区分对象类型
盒子落到地面
-
在
Canvas下创建一个节点,命名为Ground -
给
Ground挂Sprite,让它在场景里看得见 -
给
Ground挂BoxCollider2D -
给
Ground挂RigidBody2D,并把Type设为Static -
再创建一个节点,命名为
Box -
给
Box挂Sprite -
给
Box挂BoxCollider2D -
给
Box挂RigidBody2D,并把Type设为Dynamic -
确认
Box在上面,Ground在下面 -
运行预览,就能看到盒子受重力下落并落到地面上
-
Box会下落,是因为它是Dynamic刚体,且受重力影响 -
Ground不会动,是因为它是Static刚体 -
两者不会穿过去,是因为它们都挂了碰撞体
-
如果把
Ground或Box的碰撞体去掉,效果马上就不对了
施加力(扔个小球)
物理世界里,物体不只是会掉下来,我们还可以主动给它一个力,让它朝某个方向动起来。
typescript
import { _decorator, Component, RigidBody2D, Vec2 } from "cc";
const { ccclass } = _decorator;
@ccclass("BallThrow")
export class BallThrow extends Component {
start() {
const rb = this.getComponent(RigidBody2D);
if (!rb) return;
// 给当前节点一个向右上方45度角的冲量
const center = new Vec2();
rb.getWorldCenter(center);
rb.applyLinearImpulse(new Vec2(50, 50), center, true);
}
}
在物理系统里,施加力 和 直接改位置 是两种完全不同的思路:
1. 施加力 / 冲量
-
交给物理引擎计算运动结果
-
会自然受到重力、摩擦、碰撞影响
-
轨迹更真实
-
适合做跳跃、抛射、滚动、撞击这类效果
2. 直接改节点位置
-
相当于你每一帧手动把物体"拎到某个位置"
-
不一定遵循真实物理过程
-
容易和碰撞、刚体系统打架
-
更适合纯 UI 动画、非物理对象移动、脚本控制的简单位移
碰撞回调
两个带碰撞体的对象接触时,会触发回调,业务逻辑通常写在这里:
typescript
import {
_decorator,
Collider2D,
Component,
Contact2DType,
} from "cc";
const { ccclass } = _decorator;
@ccclass("ColliderListener")
export class ColliderListener extends Component {
private collider: Collider2D | null = null;
onEnable() {
this.collider = this.getComponent(Collider2D);
this.collider?.on(Contact2DType.BEGIN_CONTACT, this.onPickup, this);
}
onDisable() {
this.collider?.off(Contact2DType.BEGIN_CONTACT, this.onPickup, this);
}
private onPickup(self: Collider2D, other: Collider2D) {
console.log("检测到碰撞", other.node.name);
}
}
注意事项:
-
传感器(Sensor)只触发回调,不产生物理反弹,适合"碰到了但不弹开"的场景,比如收集金币、进入区域
-
碰撞不生效时,先检查节点上是否同时挂了
RigidBody2D和Collider2D -
需要监听碰撞时,需要在刚体属性配置中打开
enable contact listener
2.7 相机怎么理解,怎么用
相机的作用: 场景里的哪些内容会被看到,以及是从什么位置看到。
在 Cocos 里,相机通常也是一个节点,只不过这个节点上挂了 Camera 组件。
最常见的两种相机用法
-
场景相机:拍角色、地图、障碍物这些游戏内容
-
UI 相机:拍按钮、分数、血条、弹窗这些界面内容
如果项目比较简单,也可以只有一个相机;但一旦场景和 UI 需要独立控制,就很适合拆成不同层和不同相机。
一个更适合演示的例子:WASD 控制玩家,相机跟着走
第一步:给玩家挂一个移动脚本
typescript
import {
_decorator,
Component,
EventKeyboard,
input,
Input,
KeyCode,
Vec3,
} from "cc";
const { ccclass } = _decorator;
@ccclass("PlayerMove")
export class PlayerMove extends Component {
private moveX = 0;
private moveY = 0;
private speed = 200;
onEnable() {
input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this);
input.on(Input.EventType.KEY_UP, this.onKeyUp, this);
}
onDisable() {
input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this);
input.off(Input.EventType.KEY_UP, this.onKeyUp, this);
}
update(deltaTime: number) {
if (this.moveX === 0 && this.moveY === 0) return;
const pos = this.node.position.clone();
pos.x += this.moveX * this.speed * deltaTime;
pos.y += this.moveY * this.speed * deltaTime;
this.node.setPosition(pos);
}
private onKeyDown(event: EventKeyboard) {
if (event.keyCode === KeyCode.KEY_A) this.moveX = -1;
if (event.keyCode === KeyCode.KEY_D) this.moveX = 1;
if (event.keyCode === KeyCode.KEY_W) this.moveY = 1;
if (event.keyCode === KeyCode.KEY_S) this.moveY = -1;
}
private onKeyUp(event: EventKeyboard) {
if (event.keyCode === KeyCode.KEY_A || event.keyCode === KeyCode.KEY_D) {
this.moveX = 0;
}
if (event.keyCode === KeyCode.KEY_W || event.keyCode === KeyCode.KEY_S) {
this.moveY = 0;
}
}
}
第二步:给相机挂一个跟随脚本
typescript
import { _decorator, Component, Node } from "cc";
const { ccclass, property } = _decorator;
@ccclass("CameraFollow")
export class CameraFollow extends Component {
@property(Node)
target: Node | null = null;
update() {
if (!this.target) return;
const targetPos = this.target.worldPosition;
this.node.setWorldPosition(
targetPos.x,
targetPos.y,
this.node.worldPosition.z
);
}
}
如何做一个固定视角的血条?
先记住 3 件事:
-
相机决定"画面拍到什么"
-
Layer决定"这个节点属于哪一层" -
Camera和Layer配合,决定"某一层内容会不会被渲染出来"
plain
Canvas
├── bg-ocean
├── player
├── PlayerCamera
├── ProgressBar
└── UICamera
可以按下面这组精简步骤来配:
-
在
Canvas下创建player,放到DEFAULT层,挂上PlayerMove.ts -
在
Canvas下创建bg-ocean,放到DEFAULT层 -
在
Canvas下创建PlayerCamera,放到DEFAULT层,挂上CameraFollow.ts -
把
CameraFollow.target绑定到player -
在
Canvas下创建ProgressBar,并把它放到UI_2D层 -
在
Canvas下创建 相机,命名为UICamera,并把它放到UI_2D层 -
把
PlayerCamera的Visibility设为只看DEFAULT层 -
把
UICamera的Visibility设为只看UI_2D层 -
把
PlayerCamera的Priority设得比UICamera低,越低越早渲染 -
把
PlayerCamera的Clear Flags设为SOLID_COLOR清屏渲染 -
把
UICamera的Clear Flags设为DEPTH_ONLY只清深度不清屏 -
把
UICamera的Ortho Height设得和场景相机一样,保证两个相机处于同一观察高度,不会有放大缩小的问题
这几个相机参数怎么理解
-
Priority:相机的渲染顺序,值越大越晚渲染 -
场景相机一般先渲染
-
UI 相机一般后渲染,这样 UI 会盖在场景上面
-
Visibility:这个相机能看到哪些层 -
PlayerCamera只看DEFAULT -
UICamera只看UI_2D -
这个参数和节点的
Layer是配套使用的 -
Clear Flags:这个相机开始渲染前,先清掉什么内容,常见选项有: -
SOLID_COLOR:清屏并填充背景色,适合场景主相机。2D 项目里最常用,能保证每一帧先把背景清干净再开始画 -
DEPTH_ONLY:只清深度,不清颜色,适合 UI 相机叠加在场景上面。这样前一个相机画好的场景还能保留下来,UI 直接盖在上面 -
SKYBOX:先清屏,再用天空盒作为背景,更多用于 3D 场景 -
DONT_CLEAR:什么都不清,直接在上一帧或前一个相机的结果上继续画。这个选项更偏高级用法,配置不当很容易出现残影或画面叠加问题 -
Ortho Height:正交相机的可视高度 -
值越大,看到的范围越大,看起来物体越小
-
值越小,看到的范围越小,看起来物体越大
Layer负责给节点分层,Visibility负责决定相机看哪层,Priority和Clear Flags负责决定多个相机怎么叠加出最终画面。
2.8 动画在 Cocos 里怎么做
先区分两个概念

1. 序列帧动画
序列帧动画特别适合:
-
角色跑步
-
火焰闪烁
-
爆炸效果
-
金币旋转
它最常见的做法是:
-
准备多张连续图片
-
在编辑器里生成动画片段
-
按时间顺序播放这些图片
2. 关键帧动画
关键帧动画更适合做节点属性变化,比如:
-
位置移动
-
缩放变化
-
透明度变化
-
旋转变化
这类动画不一定要切换图片,而是直接让节点属性发生变化。
3. 在 Cocos 里怎么播放
不管是序列帧动画还是关键帧动画,通常都先在编辑器里配置成 AnimationClip,代码里再触发播放:
typescript
import { _decorator, Animation, Component, Node } from "cc";
const { ccclass } = _decorator;
@ccclass("PlayAnimaion")
export class PlayAnimaion extends Component {
private animation: Animation | null = null;
onLoad() {
this.animation = this.getComponent(Animation);
}
onEnable() {
this.node.on(Node.EventType.TOUCH_END, this.onPlayAnimation, this);
}
onDisable() {
this.node.off(Node.EventType.TOUCH_END, this.onPlayAnimation, this);
}
private onPlayAnimation() {
if (!this.animation) {
console.warn("当前节点没有 Animation 组件");
return;
}
const firstClip = this.animation.clips[0] ?? this.animation.defaultClip;
if (!firstClip) {
console.warn("Animation 组件上没有可播放的动画片段");
return;
}
this.animation.play(firstClip.name);
}
}
2.9 多个场景情况
当项目不再只有一个页面时,就一定会遇到场景切换。
最常见的几个场景比如:
-
启动页切到主界面
-
主界面切到游戏场景
-
游戏结束后切到结算页
在 Cocos 里,切换方式有两种,一种是直接切换,一种是先预加载,再切换:
typescript
import { _decorator, Button, Component, director, Enum } from "cc";
const { ccclass, property } = _decorator;
enum SceneSwitchMode {
Direct = 0, // 直接切换场景
Preload = 1, // 预加载场景
}
@ccclass("SceneSwitcher")
export class SceneSwitcher extends Component {
@property
targetScene = "play";
@property({ type: Enum(SceneSwitchMode) })
switchMode = SceneSwitchMode.Direct;
onEnable() {
this.node.on(Button.EventType.CLICK, this.onClickSwitchScene, this);
}
onDisable() {
this.node.off(Button.EventType.CLICK, this.onClickSwitchScene, this);
}
private onClickSwitchScene() {
if (!this.targetScene) {
console.warn("未设置目标场景");
return;
}
// 如果切换模式为预加载,则预加载场景
if (this.switchMode === SceneSwitchMode.Preload) {
console.log(`开始预加载场景: ${this.targetScene}`);
director.preloadScene(this.targetScene, (err) => {
if (err) {
console.error(`预加载场景失败: ${this.targetScene}`, err);
return;
}
console.log(`预加载完成,开始切换场景: ${this.targetScene}`);
director.loadScene(this.targetScene);
});
return;
}
// 如果切换模式为直接切换,则直接切换场景
director.loadScene(this.targetScene);
}
}
2.10 切场景之后,数据怎么留住
切场景之后,分数、用户信息、音频播放器这些东西怎么办?
在 Cocos 里,常见有三种思路:
1. 用单例或全局管理器保存数据
适合保存这类内容:
-
玩家分数
-
当前关卡
-
用户配置
-
登录信息
typescript
export class GameData {
private static _instance: GameData;
static get instance() {
if (!this._instance) {
this._instance = new GameData();
}
return this._instance;
}
score = 0;
currentLevel = 1;
}
使用时:
typescript
GameData.instance.score += 10;
console.log(GameData.instance.score);
2. 用常驻节点保留对象
如果你不仅想保留一份数据,还想保留一个真正的节点对象,比如:
-
背景音乐播放器
-
全局弹窗管理器
-
常驻网络管理器
可以把这个节点设成常驻节点:
typescript
import { _decorator, Component, director } from "cc";
const { ccclass } = _decorator;
@ccclass("GlobalNode")
export class GlobalNode extends Component {
onLoad() {
director.addPersistRootNode(this.node);
}
}
需要注意的是:常驻节点必须放在场景的根节点下,不能是任何的子节点
3. 用本地存储或网络存储做持久化
前两种方案更适合"运行中的临时保留"。
如果你希望数据在重进游戏后还在 ,或者需要多端同步,就要用持久化存储。
适合这类内容:
-
玩家设置项,比如音量、语言、画质
-
已解锁关卡
-
最高分记录
-
用户账号信息
-
需要和服务端同步的进度数据
本地存储
适合保存轻量、本机即可使用的数据,比如音量、开关状态、最高分:
typescript
sys.localStorage.setItem('bgm-volume', '0.8');
const volume = sys.localStorage.getItem('bgm-volume');
console.log(volume);
网络存储
适合保存需要服务端统一管理的数据,比如账号资料、在线进度、排行榜:
typescript
async function savePlayerData(data: { score: number; level: number }) {
await fetch('/api/player/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
}
可以这样理解:
-
本地存储:数据保存在当前设备上,读取快,实现简单
-
网络存储:数据保存在服务端,更适合登录态、跨设备同步和正式项目
怎么选更合适
-
只想保留一份简单数据:优先用单例或管理器
-
想保留一个带组件、能持续运行的节点对象:用常驻节点
-
想让数据在退出游戏后还能保留,或者需要多端同步:用本地存储或网络存储
注意事项:
-
不要什么都做成全局数据,否则后面会越来越难维护
-
常驻节点要控制数量,通常留给音频、网络、全局 UI 管理器这类真正需要跨场景存在的对象
-
本地存储适合轻量配置,不适合保存敏感信息
-
网络存储要考虑登录状态、接口失败、重试和数据一致性
三、AI 现在能怎么帮我们做游戏
AI 不只是"帮你写代码",它可以从素材生成、编辑器操作、动画制作,一路参与到游戏开发流程里。
-
用
Cocos-MCP帮我们操作编辑器 -
用 AI 生图生成美术素材
-
用 AI 生成视频,再转成序列帧动画
3.1 先说说 Cocos-MCP 能帮我们做什么
GitHub:https://github.com/DaxianLee/cocos-mcp-server
如果只让 AI 直接改工程文件,会有两个问题:
-
它不知道编辑器当前打开的是哪个场景、哪个节点
-
它改的是文本文件,不是你眼前正在编辑的节点和组件
而 Cocos-MCP 的价值就在于:
让 AI 不只是改代码,而是能真正理解并操作 Cocos 编辑器。
它大概可以替我们做这些事情:
-
读取当前场景的节点树
-
查询某个节点挂了哪些组件
-
创建节点、删除节点、重命名节点
-
给节点挂脚本或挂内置组件
-
修改组件属性
-
触发预览、构建、刷新资源
-
把控制台报错、当前上下文返回给 AI
如果要用一句最容易理解的话来讲,就是:
以前是我们手动点编辑器,现在是 AI 理解需求后,通过 MCP 去替我们点编辑器。
3.2 AI 生图能帮我们省掉哪些事
3.3 动画素材还能怎么借助 AI 来做
-
先用图片生成视频:https://www.liblib.art/
-
再用视频转为序列帧:从网上找点工具
-
再把导出来的序列帧图片,批量抠图:https://www.koukoutu.com/
四、最后做个小结
既然AI能全流程协助我们开发游戏,那还要学习这些基础理论和技术吗?