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

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

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,负责播放动画
  • 物理组件 :比如 RigidBody2DBoxCollider2D,负责受力和碰撞
  • 脚本组件 :比如 PlayerController,通常用来写业务逻辑
  • UI 组件 :比如 LabelButton,负责界面显示和交互

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:告诉引擎这个节点是不是会经常变化,常见值有 StaticStationaryMovable

  • 层级 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. GroundSprite,让它在场景里看得见

  3. GroundBoxCollider2D

  4. GroundRigidBody2D,并把 Type 设为 Static

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

  6. BoxSprite

  7. BoxBoxCollider2D

  8. BoxRigidBody2D,并把 Type 设为 Dynamic

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

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

  • Box 会下落,是因为它是 Dynamic 刚体,且受重力影响

  • Ground 不会动,是因为它是 Static 刚体

  • 两者不会穿过去,是因为它们都挂了碰撞体

  • 如果把 GroundBox 的碰撞体去掉,效果马上就不对了

施加力(扔个小球)

物理世界里,物体不只是会掉下来,我们还可以主动给它一个力,让它朝某个方向动起来。

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)只触发回调,不产生物理反弹,适合"碰到了但不弹开"的场景,比如收集金币、进入区域

  • 碰撞不生效时,先检查节点上是否同时挂了 RigidBody2DCollider2D

  • 需要监听碰撞时,需要在刚体属性配置中打开 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 决定"这个节点属于哪一层"

  • CameraLayer 配合,决定"某一层内容会不会被渲染出来"

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. PlayerCameraVisibility 设为只看 DEFAULT

  8. UICameraVisibility 设为只看 UI_2D

  9. PlayerCameraPriority 设得比 UICamera 低,越低越早渲染

  10. PlayerCameraClear Flags 设为 SOLID_COLOR 清屏渲染

  11. UICameraClear Flags 设为 DEPTH_ONLY 只清深度不清屏

  12. UICameraOrtho 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 负责决定相机看哪层,PriorityClear 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 小时前
redis 的延时双删、双重检查锁定在游戏服务端的使用(伪代码为C#)
redis·后端·游戏·缓存·c#
IT从业者张某某4 小时前
基于DEVC++实现一个控制台的赛车游戏-01-背景知识
c++·游戏
毕设源码-赖学姐21 小时前
【开题答辩全过程】以 基于SSM的游戏商城系统为例,包含答辩的问题和答案
游戏
云边散步1 天前
godot2D游戏教程系列二(23)
笔记·学习·游戏·音视频·游戏开发
lxysbly1 天前
鸿蒙harmonyos端怀旧游戏模拟器,支持fc红白机 街机 gba psp ps1 nds n64世嘉md gbc gb sfc等主机
游戏·华为·harmonyos
迷海1 天前
力扣原题《有效的数独游戏》,纯手搓,已验证
算法·leetcode·游戏
The森1 天前
macOS 26(M芯片)部署 cocos2d-x(C++)全链路指南——Xcode + Rosetta
c++·经验分享·笔记·macos·xcode·cocos2d
仲舟1 天前
【Qt游戏】骰子街Machi_Koro_AI
c++·人工智能·qt·游戏
CDN3601 天前
游戏开发 + 运维:360CDN SDK 游戏盾 + 高防组合方案
运维·网络·游戏