这次我们来看一个 Cocos Creator 回合制游戏项目。很多开发者一开始做游戏,都会选回合制,原因很简单:玩法边界清晰,不需要物理引擎的复杂模拟,也不需要实时对战的高频同步,核心就是“流程控制”和“表现管理”。这个自制项目的重点不是画面多华丽,而是能不能把战斗流程拆成一套干净、可扩展的框架:状态怎么切换、技能怎么结算、AI 怎么决策、UI 怎么刷新。
如果你正在学 Cocos Creator,或者需要一个课程设计、小游戏原型、商业项目的战斗框架雏形,这篇文章可以直接收藏。我会从环境准备、场景搭建、战斗脚本设计、状态机实现、AI 逻辑、技能数据、UI 绑定写到常用的排错方法,所有代码基于 Cocos Creator 3.x 的 TypeScript 环境,但思路在 Cocos Creator 2.x 和 JavaScript 下同样适用。
先给结论:这个项目适合做单人回合制战斗,比如 RPG 明雷遇敌、卡牌战斗、角色对战;需要的是基础场景编辑能力和组件化思维。不需要独立显卡,不需要复杂的 3D 资源,用 Cocos Creator 自带的预览功能就能跑起来。接下来我们按“先搭框架、再填内容、最后验证”的顺序把项目落地。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目类型 | Cocos Creator 自制回合制游戏项目 |
| 建议引擎版本 | Cocos Creator 3.x,使用 TypeScript 脚本 |
| 主要玩法 | 回合制战斗中普通攻击、技能、防御、逃跑,敌方 AI 自动决策 |
| 运行环境 | 浏览器 Web 预览、原生平台调试,不需要独立显卡 |
| 启动方式 | Cocos Creator 编辑器内打开项目,点击预览运行 |
| 脚本接口 | 提供战斗管理器、事件总线、技能配置接口,可扩展接入联机流程 |
| 数据配置 | 角色属性、技能效果、怪物波次通过 JSON 配置驱动 |
| 批量处理 | 支持多技能、多怪物配置批量生成战斗单位 |
| 适合场景 | 学习回合制流程设计、课程设计、RPG 原型、卡牌游戏前端基础 |
这个项目并不执着于做一个完整商业游戏,而是把回合制最常见的底层问题先解决掉:战斗状态如何流转、技能逻辑如何复用、角色和敌人如何组织、UI 如何监听战斗数据变化。后面接动画、新手引导、存档、联网对战都不会太费劲。
2. 适用场景与使用边界
先明确这个工具/框架适合谁。
第一类是刚开始学游戏开发的在校学生。很多人写回合制容易把逻辑全部塞进一个update里面,结果非常难维护。这个项目通过状态机把战斗拆成一个个环节,每个环节只做一件事,代码有什么问题很容易定位。
第二类是接外包或者做独立游戏找原型的开发者。回合制系统的战斗逻辑可以抽象出一套通用模块,后面换皮改数值就能快速出 Demo。比如换一套美术资源、改技能 JSON、调整怪物 AI 权重,战斗核心代码不需要大改。
第三类是想在 Cocos Creator 里验证事件系统、资源加载、组件通信等机制的开发者。这个项目会用到大量事件监听、节点查找和资源异步加载,刚好覆盖了 Cocos 开发中最高频的几个知识点。
同时要清楚它的使用边界。如果你要做的是《原神》那种即时战斗、只狼那种硬核动作,这个框架不合适,它的状态流是离散的,不是逐帧驱动的动作战斗。如果要做大型 MMORPG,单机回合制框架缺少服务器验证部分,也不能直接当作联网对战的核心逻辑,最多是前端表现和命令序列的一部分。
还有重要的合规边界:项目中使用的外部素材、UI 图片、音效、角色立绘,必须确认授权。尤其是从网上下载的角色图片、BGM、音效,不能直接用在商业项目里。自己绘制、购买正版素材、使用开源授权素材是最稳妥的三条路。
3. 环境准备与前置条件
这个项目对硬件几乎没有门槛。开发机和普通办公电脑即可,不需要独立显卡。Cocos Creator 的编辑器在启动时主要依赖 CPU 和内存,预览时浏览器渲染会用到一点 GPU 集成显卡就够了。更稳妥的配置建议是内存 8GB 以上,磁盘剩余空间留出至少 5GB 给引擎缓存和项目资源。
需要安装的软件如下:
- Cocos Creator 3.x 编辑器,到官网下载稳定版即可。
- Node.js 不是必须,但如果要用引擎外的脚本工具或 npm 包,建议装 14 以上版本。
- 浏览器推荐 Chrome 或者 Edge,用来调试预览。
- 代码编辑器推荐 VS Code,配合 TypeScript 语法检查更舒服。
操作系统方面,Windows、macOS 均支持。Linux 在绝大多数情况下不建议,因为 Cocos Creator 官方长期不提供 Linux 版,遇到问题难排查。
安装完成后,还需要确认编辑器使用的内部程序和脚本语言。Cocos Creator 3.x 默认使用 TypeScript,这是项目的主要语言。如果之前只写过 JavaScript,需要稍微看一下类、装饰器、类型标注这些基础内容。
磁盘方面,新建项目后会生成assets、library、temp等目录,其中library和temp是生成缓存,可以随时删除重新生成,不要手动改里面的文件。项目里需要避免存放超出必要大小的美术资源,这是性能优化的第一步。
4. 安装部署与启动方式
4.1 新建项目
打开 Cocos Creator 后选择“新建项目”,选择“空白项目”(3.x 版本)或适合 2D 的模板。项目名称可以取TurnBasedGameDemo,路径不要包含中文和空格,避免后续脚本编译和资源加载出现奇奇怪怪的报错。
创建后,工程会自动生成几个默认文件。重点关注assets目录下的Scene文件夹,里面有一个默认场景。直接双击打开默认场景。
4.2 搭建基础层级
在场景中创建以下节点结构:
Canvas ├── BattleLayer │ ├── PlayerNode │ └── EnemyNode ├── UILayer │ ├── PlayerInfoPanel │ ├── EnemyInfoPanel │ ├── BattleLogPanel │ └── CommandPanel └── EventBusNodeBattleLayer用来放战斗角色,UILayer用来放所有 UI 控件,EventBusNode挂载事件总线脚本。分层的意义是战斗表现和 UI 表现互不干扰,后面要调整角色动画、技能特效时,不会经常动到 UI 节点的位置。
创建完节点后,给相关节点挂载脚本。脚本放在assets/scripts目录下,建议按模块建子目录:
assets/scripts ├── core │ ├── EventBus.ts │ ├── BattleState.ts │ └── BattleManager.ts ├── data │ ├── RoleData.ts │ ├── SkillData.ts │ └── EnemyAI.ts ├── ui │ ├── UIManager.ts │ └── BattleLogView.ts └── config └── skillConfig.json4.3 启动预览
场景搭建完成、脚本挂载后,点击编辑器顶部“预览”按钮,浏览器会自动打开页面。如果页面显示空白,打开浏览器控制台看报错,最常见的问题是脚本没有挂载到任何启用的节点上,或者资源路径配置错误。
预览模式下,所有数据和场景都在内存中,刷新浏览器相当于重新加载。调试代码时,优先使用浏览器控制台的console.log,也可以直接在编辑器左下角的“控制台”面板查看 Cocos Script 输出。
5. 功能测试与效果验证
回合制游戏跑起来,第一件事不是检查技能动画,而是验证“回合流程能不能完整走一遍”。我们从状态机、技能、AI、UI 四个角度分别测试。
5.1 回合状态机测试
回合制战斗本质是一条状态流水线。常见的状态定义如下:
// BattleState.ts export enum BattleState { BeginBattle = "BeginBattle", PlayerTurn = "PlayerTurn", EnemyTurn = "EnemyTurn", CheckEnd = "CheckEnd", Victory = "Victory", Defeat = "Defeat" }战斗管理器使用一个状态变量和changeState方法驱动流程:
// BattleManager.ts import { _decorator, Component } from 'cc'; import { BattleState } from './BattleState'; const { ccclass, property } = _decorator; @ccclass('BattleManager') export class BattleManager extends Component { private currentState: BattleState = BattleState.BeginBattle; start() { this.changeState(BattleState.BeginBattle); } changeState(nextState: BattleState) { this.currentState = nextState; console.log(`[Battle] state -> ${nextState}`); this.handleState(nextState); } private handleState(state: BattleState) { switch (state) { case BattleState.BeginBattle: this.onBeginBattle(); break; case BattleState.PlayerTurn: this.onPlayerTurn(); break; case BattleState.EnemyTurn: this.onEnemyTurn(); break; case BattleState.CheckEnd: this.onCheckEnd(); break; case BattleState.Victory: case BattleState.Defeat: this.onGameEnd(state); break; } } private onBeginBattle() { // 播放战斗开始表现,2 秒后切换到玩家回合 this.scheduleOnce(() => this.changeState(BattleState.PlayerTurn), 2); } private onPlayerTurn() { // 打开操作面板,等待玩家输入 this.node.emit('player-turn-start'); } private onEnemyTurn() { this.node.emit('enemy-turn-start'); } private onCheckEnd() { // 双方血量判断 this.node.emit('check-end'); } private onGameEnd(state: BattleState) { this.node.emit(state === BattleState.Victory ? 'battle-victory' : 'battle-defeat'); } }测试方法:进入预览,看到日志依次输出BeginBattle -> PlayerTurn。如果卡住,看是否在onBeginBattle里没有正确调用scheduleOnce,或者changeState被重复调用导致状态覆盖。
5.2 角色数据和技能配置
角色数据用类来承载,可以序列化到编辑器属性面板,也可以从配置表读取。这里用类的方式演示:
// RoleData.ts export class RoleData { public name: string; public hp: number; public maxHp: number; public attack: number; public defense: number; public speed: number; public skills: number[]; // 技能 ID 列表 constructor(data: Partial<RoleData> = {}) { Object.assign(this, data); } public get isAlive(): boolean { return this.hp > 0; } public takeDamage(damage: number) { this.hp = Math.max(0, this.hp - damage); } }技能配置用 JSON 文件驱动,方便后期调平衡:
{ "skills": [ { "id": 1, "name": "猛击", "type": "attack", "power": 110, "target": "enemy", "description": "对单个敌人造成 110% 攻击力伤害" }, { "id": 2, "name": "破甲", "type": "debuff", "power": 60, "target": "enemy", "description": "造成伤害并降低目标防御" } ] }技能数据的加载可以放在战斗管理器初始化时完成。Cocos Creator 中建议用resources.load加载 JSON 文件,但前提是 JSON 文件放在assets/resources目录下。为了演示方便,上面这个 JSON 也可以直接用一个 TypeScript 数组对象替代,等后续有真实配置表需求时再改成资源加载。
5.3 玩家操作和技能释放
测试玩家回合时,点击“攻击”按钮,执行普通攻击;点击“技能”按钮,选择技能;点击“防御”,给角色加临时防御;点击“逃跑”,测试逃跑概率。
技能释放的核心逻辑是结算。攻击伤害不能直接用“攻击力 - 防御力”来做,否则数值波动太大。更常见的做法是带随机浮动:
// SkillExecutor.ts import { RoleData } from '../data/RoleData'; export function executeSkill(skill: any, attacker: RoleData, defender: RoleData): { damage: number; log: string } { // 按技能类型结算 if (skill.type === 'attack') { const baseDamage = attacker.attack * (skill.power / 100); const defenseReduction = defender.defense * 0.5; const variance = 0.9 + Math.random() * 0.2; const damage = Math.max(1, Math.floor((baseDamage - defenseReduction) * variance)); defender.takeDamage(damage); return { damage, log: `${attacker.name} 使用 ${skill.name},对 ${defender.name} 造成 ${damage} 点伤害` }; } // 其他类型略 return { damage: 0, log: `${attacker.name} 使用了 ${skill.name},但什么也没发生` }; }测试时注意几个边界:
- 攻击力低于防御力时,伤害能不能保证至少为 1。
- 角色血量降到 0 后,是否会被停止。
- 技能的效果是否只生效一次,不会重复结算。
5.4 敌人 AI 行为测试
敌方 AI 是回合制游戏的灵魂之一。AI 不需要设计得过于复杂,但需要有明确的决策逻辑。这里设计成:
- 50% 概率使用普攻。
- 30% 概率使用技能。
- 20% 概率防御。
把随机数种子保留,方便复现问题:
// EnemyAI.ts import { SkillData } from './SkillData'; import { RoleData } from './RoleData'; export function decisionEnemyAction(enemy: RoleData, player: RoleData): { type: string; skill?: any } { const rand = Math.random(); if (rand < 0.5) { return { type: 'attack' }; } else if (rand < 0.8 && enemy.skills.length > 0) { // 从技能列表里随机选一个 const skillId = enemy.skills[Math.floor(Math.random() * enemy.skills.length)]; return { type: 'skill', skill: SkillData.getSkill(skillId) }; } else { return { type: 'defense' }; } }测试时,在 AI 决策函数里打印日志,观察一段时间后 AI 行为是否均匀落在三种类型里。如果长时间不出技能,检查enemy.skills是否为空,或者技能 ID 是否能正确映射到SkillData。
5.5 UI 数据刷新测试
UI 面板需要监听战斗数据变化。Cocos Creator 中,最常见的做法是组件监听事件:
// UIManager.ts import { _decorator, Component, Label } from 'cc'; import { RoleData } from '../data/RoleData'; import { BattleState } from '../core/BattleState'; const { ccclass, property } = _decorator; @ccclass('UIManager') export class UIManager extends Component { @property(Label) public playerHpLabel: Label; @property(Label) public enemyHpLabel: Label; @property(Label) public battleLogLabel: Label; private player: RoleData; private enemy: RoleData; public init(player: RoleData, enemy: RoleData) { this.player = player; this.enemy = enemy; this.refreshAll(); } public refreshAll() { this.playerHpLabel.string = `玩家 HP ${this.player.hp}/${this.player.maxHp}`; this.enemyHpLabel.string = `敌人 HP ${this.enemy.hp}/${this.enemy.maxHp}`; } public appendLog(message: string) { this.battleLogLabel.string += message + '\n'; } public onStateChanged(state: BattleState) { if (state === BattleState.Victory) { this.battleLogLabel.string += '战斗胜利\n'; } else if (state === BattleState.Defeat) { this.battleLogLabel.string += '战斗失败\n'; } } }测试重点是 UI 是否在各阶段正确刷新。特别是血量变化、回合切换、胜负提示。如果 UI 没有更新,先检查组件是否在编辑器中正确拖入引用,再检查事件是否发到了正确的节点上。
6. 脚本接口与事件总线
回合制项目的“接口能力”体现在事件总线和管理器方法上。好的封装可以让战斗逻辑和 UI 完全解耦。
6.1 事件总线实现
Cocos Creator 的Node自带事件机制,但节点之间通信容易变成“层层找引用”。更干净的方式是单独封装一个事件总线:
// EventBus.ts import { EventTarget } from 'cc'; interface EventMap { [eventName: string]: Function[]; } export class EventBus { private static _instance: EventBus | null = null; private eventTarget: EventTarget = new EventTarget(); public static get instance(): EventBus { if (!this._instance) { this._instance = new EventBus(); } return this._instance; } public on(eventName: string, callback: (...args: any[]) => void, target?: any) { this.eventTarget.on(eventName, callback as any, target); } public off(eventName: string, callback: (...args: any[]) => void, target?: any) { this.eventTarget.off(eventName, callback as any, target); } public emit(eventName: string, ...args: any[]) { this.eventTarget.emit(eventName, ...args); } }使用方式:
// 监听战斗结果 EventBus.instance.on('battle-victory', this.onVictory, this); // 发送战斗结果 EventBus.instance.emit('battle-victory');这样的好处很大:战斗急促、动画播放、音效触发、UI 通知都通过事件解耦。比如你给角色加上受击闪白动画,只需要在伤害结算后发出role-hit事件,动画组件自己监听就行,不需要在战斗管理器里写死调用链。
6.2 战斗管理器接口设计
一个稳定的战斗管理器应该对外提供清晰的接口:
// 常见接口列表 export interface BattleAPI { startBattle(playerData: any, enemyData: any): void; playerAction(action: string, skillId?: number): void; enemyAction(): void; forceEndBattle(): void; getBattleState(): BattleState; }这些接口是后面接入 AI 自动战斗、多人对战、存档回放的基石。只要你遵守“外部只调用管理器方法,管理器内部自己走状态机”的原则,换 AI 逻辑立刻就能做到。
6.3 网络对战场景扩展
如果这个项目要扩展成联机回合制,需要把战斗管理器的方法改成命令序列化:
// 示例:把玩家操作转换为网络消息 const message = { type: 'player-action', action: 'skill', skillId: 1, targetId: 'enemy_01', requestId: 1001 };本地战斗管理器和联机战斗模式的差别在于:一个是“决定后立即执行”,另一个是“发送给服务器,等服务器广播结果后再执行”。先通过事件总线把战斗逻辑和 UI 解耦,后面接入 WebSocket 就简单得多。
7. 资源占用与性能观察
回合制游戏不是性能重灾区,但也需要注意几个问题。预览运行时,打开浏览器开发者工具的性能监控面板,重点观察三个指标:
- FPS:回合制 UI 切换、技能特效播放时,FPS 是否能稳定在 60。
- Draw Call:场景内节点和图片越多,Draw Call 越高。如果模型、UI、文字同时出现大量重绘,会掉帧。
- 内存占用:长时间反复战斗,资源释放不干净会导致内存越占越大。
回合制游戏中 Draw Call 的主要来源是 UI 节点过多。一个角色头像、一个血条、一个名字标签、一个按钮背景,随随便便就是十几个 Draw Call。减少的方法很简单:
- 使用动态合图,把多个零碎图片合到一张图集里。
- 减少不必要的 Shadow、多余透明节点。
- 战斗日志面板使用虚拟列表或定时清空,不要无限往 Label 里追加字符串。
显存方面,Cocos Creator 2D 项目占用很小,不需要独立显卡。真正吃资源的往往是 3D 场景和特效。如果项目后期加入大量 3D 角色、技能粒子,那就需要认真考虑目标真机的最低配置。按目前自制的 2D 回合制项目,浏览器预览和手机真机预览都能轻松跑起来,前提是美术资源不要无节制堆砌。
8. 常见问题与排查方法
回合制项目常见的坑集中在异步时序、节点生命周期、状态丢失、资源加载失败这几个方面。整理成排查表:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 点击攻击没反应 | 按钮事件没绑定到战斗管理器方法 | 检查事件是否注册,监听按钮点击日志 | 在按钮的clickEvents里正确绑定BattleManager.playerAction |
| 回合卡住,不切换 | 状态机中某个回调没有调用changeState | 在handleState打印当前状态 | 确保每个状态处理函数最终都会走到下一步 |
| UI 血量不刷新 | 血量 Label 引用丢失,或 UI 刷新逻辑没有调用 | 检查@property(Label)是否在编辑器里拖入 | 在refreshAll里加日志,确认刷新函数有执行 |
| 敌人 AI 一直不出技能 | skillConfig没加载成功,或技能 ID 不存在 | 打印skillConfig内容 | 检查 JSON 路径,确保资源加载成功 |
| 角色死亡后还能被攻击 | 结算时未检查isAlive | 在攻击结算前加存活判断 | 当前角色死亡时直接进入CheckEnd |
| 日志越堆越长,卡顿 | Label.string无限追加 | 打开性能面板检查 UI 更新频率 | 限制日志条数,超过 100 条清理旧内容 |
| 场景切换后事件重复监听 | EventBus.on没有对应off | 在节点销毁时检查事件监听 | 在onDestroy中使用off清除监听 |
| 预览刷新后数据没了 | 项目里没有存档系统 | 检查数据是否只在内存中初始化 | 后续接入localStorage或者存档脚本 |
| 异步加载资源后找不到对象 | 场景节点被提前销毁 | 在回调函数里判断节点isValid | 加载完成后先检查节点有效性再使用 |
排查问题的方法比问题本身重要。第一步永远要“看日志”。如果点击攻击没反应,先确认按钮的clickEvent里是否真的调用到了BattleManager.playerAction。如果调用了,在函数入口输出一行日志,再找为什么没有往下执行。第二步是把状态机的每个步骤都打日志,这样回合卡住时立刻能定位是哪个环节断了。
9. 最佳实践与使用建议
9.1 先走通最小流程
做回合制项目最容易犯的错误是“一步到位”。第一次做,建议只做三个按钮:攻击、防御、跳过回合。什么都不用加,先把PlayerTurn -> EnemyTurn -> CheckEnd这条线跑通。确认状态切换、双方血量变化、胜负判断没问题后,再加技能、AI 和动画。
9.2 数据驱动优先
所有角色属性、技能数值、怪物配置都应该放到独立的配置表里,而不是写在脚本的if-else里。这样调整平衡性只需要改配置,不需要改代码。后续甚至可以做策划配置表导出工具,把 Excel 转成 JSON。
9.3 组件通信走事件总线
尽量不要用getComponent查找组件再调方法,或者用node.parent一层层往上找。事件总线能让你在后续新增功能时少改很多旧代码。新增一个音效模块,只需要监听skill-hit、battle-victory等事件,不需要动战斗管理器。
9.4 状态机不要嵌套太深
回合制游戏很容易出现“如果 A 且 B,那么 C;否则 D”的逻辑。建议每个状态只处理一个入口条件,状态之间的跳转放在统一的方法里。不要在一个状态里直接写另一个状态的完整逻辑。
9.5 版权与合规提醒
从资源站、他人项目提取的代码、图片、音效,在使用前必须确认授权协议。CC0 资源可以直接商用,但需要留存证明;CC-BY 资源需要标明作者;禁止被商用的资源不能出现在正式项目里。如果项目涉及玩家上传的图片、昵称、语音等内容,还需要考虑内容审核和隐私保护。自制游戏用于学习没有问题,要上线发布就必须把素材版权和用户内容合规一起处理好。
10. 总结与下一步
这个自制回合制项目最值得尝试的点是“状态机 + 事件总线 + 数据配置”三件套。你可以用最简的形式先搭出完整战斗流程,再逐步填充内容。最先应该验证的功能是:玩家回合能不能发起攻击、敌人回合能不能自动回应、血量归零后能不能正确进入胜负判定。最容易踩的坑是状态切换卡死和事件重复监听,前者靠日志追踪,后者靠onDestroy里清理在线状态。
下一步的方向非常多:
- 在
ChangeState里接入一个“回合等待动画”表现,让队伍更直观。 - 把玩家技能列表做成可滚动 UI,支持多技能选择。
- 给角色加 buff 和 debuff 状态,比如中毒、眩晕、护盾。
- 引入存档机制,保存战斗进度和角色成长。
- 如果做联机,把战斗管理器中的
playerAction改为网络收发命令。
这套项目框架跑通后,你会发现自己对 Cocos Creator 的组件生命周期、事件机制、资源加载和数据结构都有了一层更实际的理解。后面不管做卡牌、回合制 RPG,还是模拟经营游戏里的“回合结算”模块,都可以复用这套思路。建议收藏备用,下次新开工程时直接拿来做底子。