Cocos Creator 回合制战斗框架开发实战:从零搭建状态机与数据驱动战斗系统

之前有读者问过一个很实际的问题:用 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.tsHero维护英雄属性数据
Monster.tsMonster维护怪物属性数据
BattleManager.tsCanvas战斗流程控制、回合切换、胜负判定

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 战斗流程拆解

我们把一次完整的战斗循环拆成以下步骤:

  1. 玩家回合开始,刷新按钮状态,日志提示“请选择行动”。
  2. 玩家点击“攻击”按钮。
  3. 英雄对怪物造成伤害。
  4. 刷新怪物血量 UI,追加日志。
  5. 判断怪物是否死亡,死亡则进入胜利结算。
  6. 未死亡则切换到敌人回合,禁用玩家按钮。
  7. 延迟 1 秒后怪物执行 AI 攻击。
  8. 刷新英雄血量 UI,追加日志。
  9. 判断英雄是否死亡,死亡则进入失败结算。
  10. 未死亡则回到玩家回合,重新启用按钮。

这个流程包含了回合制战斗的核心循环:状态切换、伤害结算、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 编辑器:

  1. 把Hero.ts挂到Hero节点上。
  2. 把Monster.ts挂到Monster节点上。
  3. 把BattleManager.ts挂到Canvas节点上。
  4. 点击Canvas节点,在 Inspector 面板中把Hero、Monster节点拖拽到Hero Node和Monster Node属性槽中。
  5. 依次把各个 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 系统。动手改一版属于自己的战斗规则,比单纯复制代码收获会大得多。

相关新闻

杰理692N开发入门:从SDK源码到音频功能调试全流程

杰理692N开发入门:从SDK源码到音频功能调试全流程

简介&#xff1a;这份杰理692N开发入门教程SDK源码面向蓝牙、Wi-Fi等无线通信方向的嵌入式开发者与初学者&#xff0c;帮助其快速搭建692N芯片的开发环境并理解SDK整体架构。压缩包共647个文件&#xff0c;约40.08MB&#xff0c;以241个h头文件与127个c源文件为核心&#xff0c…

2026/10/21 8:04:16 阅读更多 →
Windows下用VS2019从源码编译MySQL 8.0.25的完整实操指南

Windows下用VS2019从源码编译MySQL 8.0.25的完整实操指南

拿到这个项目题目的时候&#xff0c;说实话我愣了一下。“数据库系统设计与实现”这门夏季小学期课程&#xff0c;任务居然是&#xff1a;用VS2019编译一份MySQL 8.0.25源码。这可不是装个安装包点下一步那么简单&#xff0c;而是要把一个数百万行C代码的大型数据库系统从头到尾…

2026/10/22 10:39:05 阅读更多 →
AI编程教程从零到两万Star:内容体系、提示词工程与持续迭代实践

AI编程教程从零到两万Star:内容体系、提示词工程与持续迭代实践

那篇教程项目拿到两万 Star 的时候&#xff0c;我正在改它第 43 个版本的目录结构。说实话&#xff0c;第一眼看到消息推送&#xff0c;我以为是数据统计的缓存出了问题&#xff0c;刷新了三遍才确认那个数字是真的。更让我意外的是&#xff0c;几天后有人在群里告诉我&#xf…

2026/10/21 8:04:16 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年&#xff0c;我大部分时间都在跟浏览器标签页打交道&#xff0c;直到第一次把桌面应用跑起来&#xff0c;才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词&#xff0c;最开始是在某段时间频繁出现在同事的讨论里&#xff0c;一个用HTML…

2026/10/22 10:18:38 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介&#xff1a;亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档&#xff0c;主要面向智能锁安装人员、终端用户及售后维护者&#xff0c;帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件&#xff0c;大小4.37MB&#xff0c;内容结构清晰&#xff…

2026/10/21 9:32:40 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统&#xff0c;整车要求空载跑到8m/s&#xff0c;电机转速已经拉到额定点&#xff0c;驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令&#xff0c;转速纹丝不动&#xff0c;三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/10/22 9:44:47 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年&#xff0c;用它写过嵌入式驱动、做过游戏服务端&#xff0c;也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词&#xff0c;我其实挺感慨的——这门语言的难点从来不是语法多&#xff0c;而是知识点之间的耦合太深…

2026/10/22 10:42:00 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介&#xff1a;亚太天能T109滑盖智能锁产品说明书以PDF格式发布&#xff0c;适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开&#xff0c;先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能&#xff0c;再给出完整安装步骤&…

2026/10/22 10:40:08 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历&#xff1a;翻开源码注释、论文摘要或者一份专业报告&#xff0c;每个字都认识&#xff0c;连在一起却像天书。想找人聊聊&#xff0c;身边要么没人懂&#xff0c;要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型&#xff0c;几秒钟之后&a…

2026/10/22 10:56:37 阅读更多 →

日新闻

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/22 1:47:45 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/21 21:32:28 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/22 5:12:39 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/21 21:50:56 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/21 21:51:55 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/21 16:24:48 阅读更多 →