之前有读者问过一个很实际的问题:用 Cocos Creator 能不能自己写一个回合制游戏的战斗框架?其实不用一上来就奔着《宝可梦》《火焰纹章》这种体量去,先做一个经典的“英雄打怪物”回合制战斗 Demo,就能把节点管理、组件通信、状态机、UI 刷新这些核心能力完整练一遍。
本文就以 Cocos Creator 3.x 为例,从零搭建一个可运行的回合制战斗项目。代码结构采用数据和表现分离的思路,后续可以直接改造成卡牌、RPG、战棋等玩法的基础战斗框架。无论是刚接触 Cocos 的新手,还是想快速搭战斗原型做验证的开发者,这篇文章都适用。
1. 回合制游戏的核心概念与项目定位
1.1 回合制游戏到底是什么
回合制游戏的核心规则是“你行动一次,我行动一次”,双方轮流操作,直到某一方所有角色倒下。和即时战斗(如动作游戏、MOBA)相比,回合制不依赖玩家的操作速度和反应时间,而更依赖策略、数值和资源管理。
这种玩法的关键在于“回合(Turn)”——整个战斗过程被拆成很多个时间片段,每个片段里某一方拥有行动权。行动权可以是简单的“玩家->敌人->玩家”交替,也可以带速度值排序、技能释放顺序、Buff 持续时间等复杂规则。
从实现角度看,回合制战斗至少需要四个模块:
- 战斗实体:英雄、怪物,以及它们的属性数据。
- 战斗状态机:控制当前处于哪个阶段,比如玩家回合、敌人回合、胜利、失败。
- 战斗结算:攻击伤害计算、血量扣减、胜负判定。
- 界面表现:血条/数值刷新、战斗日志、按钮可用状态。
1.2 为什么适合用 Cocos Creator 来做
Cocos Creator 是成熟的 2D/3D 游戏开发引擎,编辑器内置场景、节点、UI、动画系统,非常适合快速验证玩法逻辑。回合制战斗特别适合用 Cocos Creator 来实现,原因主要有几点:
- 组件化开发:一个角色就是一个节点,角色身上的数据和行为挂在对应的脚本组件上,天然契合回合制里“一个单位一个对象”的模型。
- UI 系统完善:血量标签、按钮、日志文本都可以直接用 UITransform、Label、Button 等组件搭建,不需要额外引入 UI 框架。
- 跨平台发布方便:开发完成后可以一键发布到 Web、Android、iOS、微信小游戏等平台,适合课程设计、毕业设计和个人作品展示。
- 脚本语言是 TypeScript:写战斗逻辑时类型约束清晰,后期扩展技能系统、道具系统更安全。
1.3 本文项目的目标
我们不做复杂的 RPG 地图探索,也不做装备背包系统,只聚焦一个最小可玩的回合制战斗闭环:
- 玩家控制一个英雄,敌人是一个怪物。
- 玩家回合可以选择普通攻击或释放技能。
- 敌人回合由简单的 AI 控制,自动攻击英雄。
- 每次攻击后刷新血量 UI,并打印战斗日志。
- 任意一方血量降为 0 时,战斗结束并显示胜负结果。
项目跑通之后,你可以在这个框架上继续加技能冷却、暴击、属性克制、多角色队伍、掉落奖励等玩法。
2. 开发环境准备与项目初始化
2.1 环境版本说明
本文示例基于 Cocos Creator 3.x 的 API 编写,编辑器操作路径也以 3.x 版本为例。由于引擎版本更新较快,不同 3.x 小版本之间 API 基本稳定,但个别方法和参数可能有细微差异,建议使用较新的 3.8 版本。
如果你的项目是 Cocos Creator 2.x,TypeScript 写法差别较大,不建议直接照搬本文代码,但设计思路依然可以参考。
环境准备清单:
| 项目 | 要求 |
|---|---|
| 操作系统 | Windows / macOS 均可 |
| 引擎 | Cocos Creator 3.x |
| 开发语言 | TypeScript |
| Node.js | 引擎自带或独立安装均可 |
| 浏览器 | 建议 Chrome,用于预览调试 |
版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示配置思路。
2.2 创建项目
打开 Cocos Dashboard,新建一个空项目,选择“Empty(2D)”模板即可。项目名称可以命名为TurnBasedDemo。
创建完成之后,等待编辑器加载完毕,你会看到默认的assets和settings目录。我们接下来的所有脚本和场景都放在assets下面。
2.3 场景与节点结构设计
在assets下新建一个scenes目录,然后创建名为Battle的场景。双击打开场景,按照下图结构手动创建节点树:
Battle └── Canvas ├── Hero // 英雄节点,挂 Sprite 显示占位图 ├── Monster // 怪物节点,挂 Sprite 显示占位图 └── UIRoot // UI 根节点 ├── HeroHpLabel ├── MonsterHpLabel ├── StateLabel ├── LogLabel ├── AttackButton └── SkillButton节点的具体职责:
Hero和Monster:战斗立绘位置。没有美术资源时,可以先给节点添加 Sprite 组件,使用内置的默认白色图片,或自己画一个简单的色块当占位图。UIRoot:承载所有战斗 UI。HeroHpLabel/MonsterHpLabel:显示双方血量。StateLabel:显示当前回合状态,比如“玩家回合”“敌人回合”“战斗胜利”。LogLabel:显示战斗日志,比如“英雄对怪物造成了 15 点伤害”。AttackButton和SkillButton:玩家的操作按钮,在敌人回合需要置灰禁用。
挂载脚本的分工如下:
| 脚本 | 挂载节点 | 职责 |
|---|---|---|
Hero.ts | Hero | 维护英雄属性数据 |
Monster.ts | Monster | 维护怪物属性数据 |
BattleManager.ts | Canvas | 战斗流程控制、回合切换、胜负判定 |
3. 核心战斗机制设计
3.1 战斗实体数据模型
战斗实体的最小数据模型如下:
interface BattleUnit { name: string; // 单位名称 maxHp: number; // 最大血量 hp: number; // 当前血量 attack: number; // 攻击力 alive: boolean; // 是否存活 }alive这个字段很重要。在战斗结算过程中,我们不直接用hp === 0来判断存活,因为伤害计算、扣血、刷新 UI 这几个步骤是分散在不同函数里的,统一用alive可以避免某个环节遗漏判断。
伤害计算采用最经典的减法公式:
实际伤害 = 攻击方攻击力 - 防御方防御力 最低伤害 = 1当然实际项目还会加入暴击、属性克制、浮动值等规则,我们这里先保持最简。
3.2 回合状态机设计
回合制战斗的难点是状态切换。同一个函数里同时处理“玩家回合”和“敌人回合”会让代码越来越难维护,所以引入一个简单的状态机:
enum BattlePhase { Start = 'Start', // 战斗开始 PlayerTurn = 'PlayerTurn', // 玩家回合 EnemyTurn = 'EnemyTurn', // 敌人回合 Win = 'Win', // 玩家胜利 Lose = 'Lose', // 玩家失败 }状态流转方向如下:
Start -> PlayerTurn -> EnemyTurn -> PlayerTurn -> ... 玩家回合结束 -> 切换到 EnemyTurn 敌人回合结束 -> 切换到 PlayerTurn 任意一方敌人全部倒下 -> Win 任意一方英雄全部倒下 -> Lose在代码里,BattleManager持有currentPhase,所有操作入口都先判断当前状态。比如玩家执行攻击时,首先要确认currentPhase是PlayerTurn,否则直接返回,防止玩家在敌人回合通过按钮疯狂操作导致数据错乱。
3.3 战斗流程拆解
我们把一次完整的战斗循环拆成以下步骤:
- 玩家回合开始,刷新按钮状态,日志提示“请选择行动”。
- 玩家点击“攻击”按钮。
- 英雄对怪物造成伤害。
- 刷新怪物血量 UI,追加日志。
- 判断怪物是否死亡,死亡则进入胜利结算。
- 未死亡则切换到敌人回合,禁用玩家按钮。
- 延迟 1 秒后怪物执行 AI 攻击。
- 刷新英雄血量 UI,追加日志。
- 判断英雄是否死亡,死亡则进入失败结算。
- 未死亡则回到玩家回合,重新启用按钮。
这个流程包含了回合制战斗的核心循环:状态切换、伤害结算、UI 刷新、胜负判定。
4. 完整实战:实现一个回合制战斗 Demo
4.1 配置场景节点与 UI
先准备好场景节点。为了让代码可以直接复制运行,建议节点命名和下面保持一致。
在Canvas下创建节点后,分别给Hero和Monster添加 Sprite 组件,设置一个简单的颜色和尺寸,方便在场景里看到位置。不需要额外美术资源,运行时我们主要看 UI 日志和血量数字。
再给HeroHpLabel、MonsterHpLabel、StateLabel、LogLabel都添加 Label 组件,并设置合适的字号。LogLabel需要设置Overflow为Resize Height,并勾选Enable Wrap Text,这样长日志会自动换行,不会撑出屏幕。
AttackButton和SkillButton添加 Button 组件,子节点添加 Label 组件,文字分别写“攻击”和“技能”。
4.2 英雄脚本 Hero.ts
在assets/scripts目录下新建Hero.ts,直接控制英雄的基础属性。
// 文件路径:assets/scripts/Hero.ts import { _decorator, Component } from 'cc'; const { ccclass } = _decorator; @ccclass('Hero') export class Hero extends Component { /** 英雄名称 */ public unitName: string = '勇者'; /** 最大血量 */ public maxHp: number = 100; /** 当前血量 */ public hp: number = 100; /** 攻击力 */ public attack: number = 20; /** 技能攻击倍率 */ public skillMultiplier: number = 1.5; /** 防御力 */ public defense: number = 5; /** 是否存活 */ public alive: boolean = true; /** 对目标造成普通攻击伤害 */ public normalAttack(target: Monster): number { return this.calculateDamage(this.attack, target.defense); } /** 对目标造成技能伤害 */ public skillAttack(target: Monster): number { const skillPower = Math.floor(this.attack * this.skillMultiplier); return this.calculateDamage(skillPower, target.defense); } private calculateDamage(attackValue: number, defenseValue: number): number { const damage = attackValue - defenseValue; return Math.max(damage, 1); } }这里把伤害计算写在Hero内部,Monster也写一份对应逻辑。实际的游戏项目中,伤害公式通常会抽成独立的 BoultScript 或者数值模块,这里为了演示保持简单。
注意一个细节:技能攻击力Math.floor(this.attack * this.skillMultiplier)这里先用向下取整,避免出现小数伤害。
4.3 怪物实体脚本 Monster.ts
新建Monster.ts,结构和英雄类似。
// 文件路径:assets/scripts/Monster.ts import { _decorator, Component } from 'cc'; const { ccclass } = _decorator; @ccclass('Monster') export class Monster extends Component { /** 怪物名称 */ public unitName: string = '史莱姆'; /** 最大血量 */ public maxHp: number = 80; /** 当前血量 */ public hp: number = 80; /** 攻击力 */ public attack: number = 12; /** 防御力 */ public defense: number = 3; /** 是否存活 */ public alive: boolean = true; /** 对英雄造成伤害 */ public attackHero(target: Hero): number { const damage = this.attack - target.defense; return Math.max(damage, 1); } }这两个脚本不需要在编辑器里配置任何属性,直接用默认值即可。如果你希望后续在编辑器面板上能修改数值,可以把公共属性改成@property装饰器修饰的属性,比如:
@property public maxHp: number = 100;这样属性面板上会出现输入框,改数值后无需修改代码。
4.4 战斗管理器 BattleManager.ts
这是整个战斗的核心脚本。挂载在Canvas节点上,负责连接英雄、怪物和所有 UI。
先导入需要使用的模块:
// 文件路径:assets/scripts/BattleManager.ts import { _decorator, Component, Node, Label, Button, tween, Vec3 } from 'cc';注意我们这里引入了tween和Vec3,是为了让战斗时的位置变化有简单动画效果,让 Demo 看起来更有“战斗中”的感觉。
定义战斗阶段枚举:
enum BattlePhase { Start = 'Start', PlayerTurn = 'PlayerTurn', EnemyTurn = 'EnemyTurn', Win = 'Win', Lose = 'Lose', }然后编写BattleManager组件:
const { ccclass, property } = _decorator; @ccclass('BattleManager') export class BattleManager extends Component { @property({ type: Node }) public heroNode: Node = null; @property({ type: Node }) public monsterNode: Node = null; @property({ type: Label }) public heroHpLabel: Label = null; @property({ type: Label }) public monsterHpLabel: Label = null; @property({ type: Label }) public stateLabel: Label = null; @property({ type: Label }) public logLabel: Label = null; @property({ type: Button }) public attackButton: Button = null; @property({ type: Button }) public skillButton: Button = null; private hero: Hero = null; private monster: Monster = null; private currentPhase: BattlePhase = BattlePhase.Start; protected onLoad() { this.hero = this.heroNode.getComponent(Hero); this.monster = this.monsterNode.getComponent(Monster); this.attackButton.node.on(Button.EventType.CLICK, this.onAttackClick, this); this.skillButton.node.on(Button.EventType.CLICK, this.onSkillClick, this); } protected start() { this.setPhase(BattlePhase.PlayerTurn); this.refreshUI(); this.appendLog(`战斗开始!${this.hero.unitName} VS ${this.monster.unitName}`); } }onLoad里获取组件和注册按钮监听事件。start里将战斗切换到玩家回合,刷新 UI,并输出战斗开始的日志。
4.5 玩家攻击逻辑
当玩家点击攻击按钮或技能按钮时,先判断当前是不是玩家回合。
private onAttackClick() { if (this.currentPhase !== BattlePhase.PlayerTurn) { return; } this.heroAttack(false); } private onSkillClick() { if (this.currentPhase !== BattlePhase.PlayerTurn) { return; } this.heroAttack(true); }heroAttack方法接收一个布尔值,表示是否为技能攻击:
private heroAttack(isSkill: boolean) { if (!this.monster.alive) { return; } let damage = 0; let actionName = '普攻'; if (isSkill) { damage = this.hero.skillAttack(this.monster); actionName = '技能'; } else { damage = this.hero.normalAttack(this.monster); } this.monster.hp -= damage; this.appendLog(`${this.hero.unitName} 使用${actionName},对 ${this.monster.unitName} 造成 ${damage} 点伤害。`); // 攻击动画 this.playAttackAnimation(this.heroNode, this.monsterNode); if (this.monster.hp <= 0) { this.monster.hp = 0; this.monster.alive = false; this.monsterHpLabel.string = `${this.monster.unitName} HP:0/${this.monster.maxHp}`; this.appendLog(`${this.monster.unitName} 被打败了!`); this.setPhase(BattlePhase.Win); return; } this.refreshUI(); this.setPhase(BattlePhase.EnemyTurn); }核心逻辑是:先计算伤害,再扣减怪物血量,刷新日志,播放攻击动画,最后统一判断怪物是否死亡。如果没死,就切换到敌人回合。
这里要特别强调一个顺序问题:先改数据,再刷新 UI,最后切换状态。在实际项目中,如果把refreshUI放在状态切换之后,可能会因为状态已经变成 EnemyTurn 而导致 UI 刷新时读取到不一致的状态。
4.6 攻击动画辅助方法
为了让战斗过程不显得死板,我们给攻击方一个向前突进的动画效果。
private playAttackAnimation(attacker: Node, target: Node) { const attackerPos = attacker.position.clone(); const targetPos = target.position.clone(); const dir = targetPos.subtract(attackerPos).normalize(); const offset = dir.multiplyScalar(40); tween(attacker) .by(0.15, { position: offset }) .by(0.15, { position: offset.multiplyScalar(-1) }) .start(); }动画思路是:计算攻击方到目标的方向,向前移动 40 像素,再回到原位。tween的链式调用让突进和回退先后执行,视觉上就有“冲上去打一下”的效果。
注意multiplyScalar(-1)是在原来的offset基础上反向,不会影响 attacker 最终坐标。如果用tween(attacker).by(0.15, { position: offset })再配合by(0.15, { position: offset.negative() })效果相同,但要注意 Vec3 是不可变对象,每次计算都要用返回值。
4.7 敌人 AI 与回合切换
敌人回合需要在延迟后自动攻击。这里使用组件自带的scheduleOnce定时器。
private setPhase(phase: BattlePhase) { this.currentPhase = phase; switch (phase) { case BattlePhase.PlayerTurn: this.attackButton.interactable = true; this.skillButton.interactable = true; this.stateLabel.string = '玩家回合'; break; case BattlePhase.EnemyTurn: this.attackButton.interactable = false; this.skillButton.interactable = false; this.stateLabel.string = '敌人回合'; this.scheduleOnce(() => this.enemyTurn(), 1.0); break; case BattlePhase.Win: this.attackButton.interactable = false; this.skillButton.interactable = false; this.stateLabel.string = '战斗胜利'; break; case BattlePhase.Lose: this.attackButton.interactable = false; this.skillButton.interactable = false; this.stateLabel.string = '战斗失败'; break; } }按钮的interactable属性可以直接控制是否可点击,同时影响灰显状态。在玩家回合两个按钮可用,敌人回合和战斗结束后都禁用,可以避免误操作。
敌人回合的 AI 逻辑:
private enemyTurn() { if (!this.hero.alive) { return; } const damage = this.monster.attackHero(this.hero); this.hero.hp -= damage; this.appendLog(`${this.monster.unitName} 攻击 ${this.hero.unitName},造成 ${damage} 点伤害。`); this.playAttackAnimation(this.monsterNode, this.heroNode); if (this.hero.hp <= 0) { this.hero.hp = 0; this.hero.alive = false; this.heroHpLabel.string = `${this.hero.unitName} HP:0/${this.hero.maxHp}`; this.appendLog(`${this.hero.unitName} 倒下了……`); this.setPhase(BattlePhase.Lose); return; } this.refreshUI(); this.appendLog(`轮到你了,请选择行动。`); this.setPhase(BattlePhase.PlayerTurn); }敌人的 AI 其实非常简单:如果英雄存活,就执行一次普通攻击,然后判断英雄是否死亡。由于我们之前已经禁用了按钮,所以enemyTurn执行期间玩家无法干扰。
4.8 UI 刷新与日志
refreshUI统一刷新双方血量和按钮状态:
private refreshUI() { this.heroHpLabel.string = `${this.hero.unitName} HP:${this.hero.hp}/${this.hero.maxHp}`; this.monsterHpLabel.string = `${this.monster.unitName} HP:${this.monster.hp}/${this.monster.maxHp}`; }日志追加:
private appendLog(message: string) { const current = this.logLabel.string; const newLog = current ? current + '\n' + message : message; this.logLabel.string = newLog; }如果需要保持日志不超出屏幕高度,可以自定一个最大行数逻辑,比如只保留最后 20 行。这个 Demo 里为了简单直接拼接,实际项目建议用数组存储日志,再渲染到列表组件上。
4.9 在编辑器中完成节点绑定
代码写完之后,回到 Cocos Creator 编辑器:
- 把
Hero.ts挂到Hero节点上。 - 把
Monster.ts挂到Monster节点上。 - 把
BattleManager.ts挂到Canvas节点上。 - 点击
Canvas节点,在 Inspector 面板中把Hero、Monster节点拖拽到Hero Node和Monster Node属性槽中。 - 依次把各个 Label 和 Button 拖拽到对应的属性槽中。
绑定完成后点击编辑器上方的预览按钮,打开浏览器运行。
4.10 运行与验证
预期看到的效果:
- 页面初始化时显示“战斗开始!勇者 VS 史莱姆”。
- 两个按钮都处于可用状态,顶部的状态文字显示“玩家回合”。
- 点击“攻击”,英雄向前突进并攻击怪物,怪物血量下降,日志增加一条伤害记录。
- 状态切换到“敌人回合”,按钮变成灰色不可点击。
- 1 秒后怪物突进攻击英雄,英雄血量下降,按钮恢复可用。
- 任意一方血量为 0 时,状态文字显示“战斗胜利”或“战斗失败”。
如果出现这个流程,说明你的回合制战斗闭环已经跑通了。
5. 常见问题与排查思路
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 编译报错“Hero is not defined” | 脚本之间循环引用,或没有正确导入类名 | 检查 import 语句,尽量只在 BattleManager 中引用具体类 |
| 点击攻击按钮没有任何反应 | 按钮点击事件没有绑定成功,或当前不是玩家回合 | 检查 onLoad 中是否注册了监听事件,检查 currentPhase 值 |
| 属性面板拖不到节点 | 属性未使用 @property 装饰器导出 | 给 HeroNode、MonsterNode 等属性添加 @property 装饰器 |
| 怪物没有在 1 秒后攻击 | scheduleOnce 回调没有被触发 | 确认敌人回合确实被设置,并且组件处于激活状态 |
| 血量显示异常 | 刷新顺序错误或计算伤害时未取整 | 先修改数据再 refreshUI,伤害使用 Math.max 保证最低为 1 |
| 按钮在敌人回合仍然可以点击 | interactable 没有设置为 false | 在 setPhase 的 EnemyTurn 分支里禁用按钮 |
如果遇到按钮不可点击的问题,先检查Button组件是否存在,并且节点上是否有遮挡。Cocos 的 UI 点击会受节点层级和 Block Input Events 影响,如果 UI 节点被其他透明节点挡住,按钮也可能点不到。排查时可以把 UIRoot 节点分布错开,或临时隐藏其他节点验证。
另一个常见问题是this.hero或this.monster为 null。这通常是因为在BattleManager.onLoad里执行getComponent(Hero)时,Hero 组件还没有附加到节点上。解决方法是确认挂载脚本后再运行,或者把获取组件的操作挪到start中执行。因为start会在所有onLoad之后才统一调用,组件挂载顺序问题少很多。
6. 从 Demo 到工程:最佳实践与扩展建议
6.1 数据与表现分离
上面的 Demo 为了直观,把hp、attack这些数据直接放在Hero和Monster组件里。但在项目变大之后,更推荐把战斗单位和表现节点分开。
比如可以设计一个BattleUnitData纯数据类,角色节点只负责显示动画和接收刷新指令,战斗逻辑全部在管理器里操作数据对象。这样好处是:
- 把游戏逻辑和场景表现解耦,以后做存档、联网同步,直接序列化数据对象即可。
- 方便写单元测试,不需要创建场景节点就能测试伤害计算和回合流程。
- 多单位战斗时,不会因为某个节点被销毁导致数据丢失。
6.2 用事件机制代替直接引用
BattleManager目前直接引用了英雄、怪物和 UI 节点,算是耦合度很高的写法。当 UI 越来越多时,可以把“血量变化”“战斗状态变化”“日志输出”都改成事件分发。
在 Cocos Creator 中,可以用节点的emit和on实现简单的事件通信:
this.node.emit('onHeroHpChange', this.hero.hp, this.hero.maxHp);UI 组件只需要在初始化时监听这个事件:
this.node.on('onHeroHpChange', (hp: number, maxHp: number) => { this.hpLabel.string = `${hp}/${maxHp}`; });这样做之后,战斗管理器不用再持有具体的 Label 节点,新增 UI 面板不会影响战斗逻辑代码。
6.3 技能系统扩展方向
目前的技能只是一个倍率攻击,真实项目的技能系统会更复杂。你可以按下面的思路继续扩展:
- 技能配置表:用 JSON 或 Excel 配置技能的伤害倍率、消耗、冷却、目标类型。
- 技能效果列表:一个技能可以包含多个 Effect,比如伤害、回血、加 Buff、眩晕。
- Buff 系统:攻击力增强、防御削弱、中毒、灼烧等持续效果。
- 技能目标选择:单体、群体、随机目标、死亡后重新选择。
建议先把一篇技能做成接口,比如:
interface ISkill { name: string; execute(user: BattleUnit, targets: BattleUnit[]): void; }后续新增技能时,只需要实现ISkill接口,不需要修改回合控制器。
6.4 动画与音效接入
Demo 里的突进动画用tween实现,真正的项目里更推荐直接播放骨骼动画或序列帧动画,并在关键帧回调中结算伤害。Cocos Creator 提供 Animation 组件和 Animator 组件,可以给英雄做一个“攻击前摇”“攻击挥击”“攻击后摇”的三段动画,把伤害结算放在挥击动画的中间时间点。
音效方面,攻击命中、伤害飘血、胜利结算都可以挂 AudioSource 组件播放短音效。注意音效文件要提前导入项目并进行压缩设置,避免包体过大。
6.5 可维护性建议
- 所有公共属性尽量用 @property 导出到编辑器面板,方便策划和技术美术调节数值。
- 战斗阶段枚举统一维护,不要使用魔法字符串。
- 日志系统建议封装成一个独立模块,支持多行限制、颜色分级,方便战斗调 BUG。
- 可以用
console.log在开发期打印战斗状态变化,发布时统一关闭。 - 如果英雄或怪物有多个,建议用 Node 的子节点列表动态生成战斗单位,而不是在编辑器里手动拖好几个角色节点。
7. 总结
回到开头的那个问题:用 Cocos Creator 做回合制游戏战斗到底难不难?通过这个 Demo 可以看到,核心难点并不在于某个复杂的 API,而在于回合状态的流转和战斗数据的同步。只要把状态机梳理清楚,把“谁行动、谁结算、谁刷新 UI”这三个环节拆分好,回合制战斗就能稳稳地跑起来。
本文实现的战斗循环已经具备了一个回合制战斗系统的最小骨架:战场双方、回合交替、伤害公式、UI 刷新、胜负判定。代码量不大,但每一块都覆盖了后续扩展的关键位置。接下来你可以试着增加防御指令、治疗技能、暴击概率,甚至加入第二个英雄做队伍战斗,这些都是很好的练习方向。
如果身边有同样在学 Cocos 的同学或同事,可以把这个 Demo 作为团队内部的基础战斗框架,互相看看彼此的代码组织方式,然后一起完善技能和 Buff 系统。动手改一版属于自己的战斗规则,比单纯复制代码收获会大得多。