☰
Cocos Creator回合制游戏实战:状态机与事件总线搭建战斗框架
2026/10/21 2:49:42 网站建设 项目流程

这次我们来看一个 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 └── EventBusNode

BattleLayer用来放战斗角色,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.json

4.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,还是模拟经营游戏里的“回合结算”模块,都可以复用这套思路。建议收藏备用,下次新开工程时直接拿来做底子。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询