聊聊一个游戏是怎么做出来的

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

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 资源一般怎么处理

对初学者来说,资源管理只需要先理解两种方式:

  1. 编辑器里拖拽引用或者添加资源组件

  2. 代码里动态加载

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:业务标记,便于在代码里区分对象类型

盒子落到地面

  1. 在 Canvas 下创建一个节点,命名为 Ground

  2. 给 Ground 挂 Sprite,让它在场景里看得见

  3. 给 Ground 挂 BoxCollider2D

  4. 给 Ground 挂 RigidBody2D,并把 Type 设为 Static

  5. 再创建一个节点,命名为 Box

  6. 给 Box 挂 Sprite

  7. 给 Box 挂 BoxCollider2D

  8. 给 Box 挂 RigidBody2D,并把 Type 设为 Dynamic

  9. 确认 Box 在上面,Ground 在下面

  10. 运行预览,就能看到盒子受重力下落并落到地面上

  • 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

可以按下面这组精简步骤来配:

  1. 在 Canvas 下创建 player,放到 DEFAULT 层,挂上 PlayerMove.ts

  2. 在 Canvas 下创建 bg-ocean,放到 DEFAULT 层

  3. 在 Canvas 下创建 PlayerCamera,放到 DEFAULT 层,挂上 CameraFollow.ts

  4. 把 CameraFollow.target 绑定到 player

  5. 在 Canvas 下创建 ProgressBar,并把它放到 UI_2D 层

  6. 在 Canvas 下创建 相机,命名为UICamera,并把它放到 UI_2D 层

  7. 把 PlayerCamera 的 Visibility 设为只看 DEFAULT层

  8. 把 UICamera 的 Visibility 设为只看 UI_2D层

  9. 把 PlayerCamera 的 Priority 设得比 UICamera 低,越低越早渲染

  10. 把 PlayerCamera 的 Clear Flags 设为 SOLID_COLOR 清屏渲染

  11. 把 UICamera 的 Clear Flags 设为 DEPTH_ONLY 只清深度不清屏

  12. 把 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. 序列帧动画

序列帧动画特别适合:

  • 角色跑步

  • 火焰闪烁

  • 爆炸效果

  • 金币旋转

它最常见的做法是:

  1. 准备多张连续图片

  2. 在编辑器里生成动画片段

  3. 按时间顺序播放这些图片

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 能帮我们做什么

安装视频:https://www.bilibili.com/video/BV1uzgVz8EyQ?spm_id_from=333.788.videopod.sections&vd_source=00040ae605a8b6f8a8cf53d5fb9f525a

GitHub:https://github.com/DaxianLee/cocos-mcp-server

如果只让 AI 直接改工程文件,会有两个问题:

  1. 它不知道编辑器当前打开的是哪个场景、哪个节点

  2. 它改的是文本文件,不是你眼前正在编辑的节点和组件

而 Cocos-MCP 的价值就在于:

让 AI 不只是改代码,而是能真正理解并操作 Cocos 编辑器。

它大概可以替我们做这些事情:

  • 读取当前场景的节点树

  • 查询某个节点挂了哪些组件

  • 创建节点、删除节点、重命名节点

  • 给节点挂脚本或挂内置组件

  • 修改组件属性

  • 触发预览、构建、刷新资源

  • 把控制台报错、当前上下文返回给 AI

如果要用一句最容易理解的话来讲,就是:

以前是我们手动点编辑器,现在是 AI 理解需求后,通过 MCP 去替我们点编辑器。

3.2 AI 生图能帮我们省掉哪些事

https://www.lovart.ai/zh

3.3 动画素材还能怎么借助 AI 来做

四、最后做个小结

既然AI能全流程协助我们开发游戏,那还要学习这些基础理论和技术吗?

相关推荐
全速向光2 小时前
盖世游戏PC模拟器:手机也能畅玩电脑平台游戏
游戏·智能手机
Minecraft红客3 小时前
以撒的结合
python·游戏·电脑·娱乐
the3clipse13 小时前
视频编码技术如何赋能游戏性能优化:从硬件隔离到AI驱动的带宽革命
游戏·性能优化·音视频·视频编码·硬件加速·ai编码·自适应编码
jeffsonfu17 小时前
生成对抗网络(GAN)原理:生成器与判别器的“猫鼠游戏”
人工智能·游戏·生成对抗网络
解决小子1 天前
【游戏作品设计解析】类魂游戏设计解析
游戏·业界资讯·游戏策划·游戏设计
k4m7v2pz1 天前
用中文编程语言写游戏:玄铁语言生命游戏的 macOS 适配实录
游戏·macos·玄铁·生命游戏·中文编程语言·玄铁中文编程
JQweryuiop8 天前
中小型游戏陪练工作室数字化管理实践:基于七彩屋电竞护航小程序订单、履约与绩效系统的落地复盘
大数据·游戏·小程序·架构
乌萨达9 天前
低配电脑玩手游怎么设置?4GB、8GB内存与模拟器选择指南
游戏·电脑·安卓模拟器·雷电模拟器
林川~0110 天前
Unity 万能物理检测工具:射线检测 / 范围检测 / 层级过滤 / 编辑器可视化(可直接拿去用)
游戏·unity·c#·射线检测·通用工具
一孤程11 天前
游戏测试专题第七篇:游戏自动化测试实战
游戏·测试