尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Cocos Creator回合制战斗框架CocoKnightManager:管理器分层与事件驱动设计
简介CocoKnightManager 是一套基于 Cocos Creator 的回合制游戏系统设计开源工程面向希望掌握回合制玩法架构的初中级游戏开发者与独立团队帮助其解决战斗流程抽象、角色状态管理与规则扩展等核心问题。资源包共 22 个文件约 216KB以 json 配置、meta 元数据、ts 脚本、png 贴图及 fire 场景文件为主配合 README 说明覆盖工程配置、场景资源与逻辑脚本等层面结构轻量便于快速导入与二次开发。目前已有 836 人学习下载。项目以模块化为核心将回合管理器、行动规则引擎与状态机拆分为独立组件并采用数据驱动方式通过 JSON 配置属性、效果与事件读者可据此理解回合顺序判定、指令执行与状态切换的完整链路也能借鉴其代码组织方式为自研回合制项目搭建可维护、可复用的系统骨架。1. 回合制战斗的骨架藏在一个叫 CocoKnightManager 的工程里第一次拿到 CocoKnightManager 这个工程时我本以为它只是个 Cocos Creator 的练手 Demo结果翻到战斗流程那一层才发现它把回合制游戏里最容易写乱的东西——行动顺序、状态结算、技能触发、战斗事件分发——拆成了几个互相解耦的管理器。换句话说这不是一个“能跑就行”的示例而是一套可以照着搭自己回合制项目的系统设计骨架。它适合两类人一类是刚接触 Cocos Creator、想搞明白一个完整战斗循环怎么落地的新手另一类是手里已经有半成品战斗逻辑、但越写越像一锅粥、想找一套可参考的分层结构的老手。系统开源这件事的价值就在这里——你能直接看到别人怎么把“回合”这件事拆开、再拼回去。2. 工程结构与技术选型先看清这套骨架由哪些层组成2.1 技术栈与运行环境这个工程建立在 Cocos Creator 之上语言是 TypeScript走的是组件化 管理器分层的路子。Cocos Creator 的版本建议对齐工程内package.json与project.json里声明的版本跨大版本打开经常会出现场景节点丢失、组件脚本挂载失败这类问题这是血泪经验别问我怎么知道的。运行环境上编辑器内预览和构建到 Web 端是最省事的验证方式回合制逻辑本身不依赖特定平台能力所以先在浏览器里把战斗跑通再考虑原生构建。工程的核心目录大致是这样组织的不同分支可能略有出入以实际下载的为准CocoKnightManager/ ├── assets/ │ ├── scripts/ │ │ ├── manager/ # 各类管理器战斗、回合、单位、事件 │ │ ├── entity/ # 单位、技能、状态的数据与行为 │ │ ├── ui/ # 战斗界面、血条、行动条 │ │ └── config/ # 数值配置、技能表 │ ├── prefabs/ # 单位预制体、特效预制体 │ └── scenes/ # 战斗场景、主菜单场景 ├── settings/ └── package.json这个分层的关键在于manager层负责流程调度entity层负责数据与行为ui层只负责表现config层提供静态数值。回合制最容易翻车的地方就是把“谁该行动”和“血条怎么动”写在一起一旦要加个新状态或者改行动规则牵一发动全身。这套结构把调度和表现隔开改规则时基本不用碰 UI。2.2 管理器分层谁在指挥这场战斗战斗的指挥权集中在几个管理器上职责划分大致如下管理器职责典型方法BattleManager战斗生命周期、胜负判定、事件分发startBattle、endBattle、checkResultTurnManager回合推进、行动顺序、回合计数nextTurn、getCurrentUnit、sortBySpeedUnitManager单位注册、查找、存活状态维护register、getAliveUnits、removeEventBus战斗事件订阅与广播on、emit、off这种拆法的好处是回合推进的逻辑只出现在 TurnManager 里战斗结束的判定只出现在 BattleManager 里。想加一个“回合开始时触发某状态”的机制只需要在 TurnManager 的回合切换点挂事件不用去改单位本身的代码。下面这段是回合推进的典型写法逻辑上对应工程里 TurnManager 的核心循环// TurnManager.ts —— 回合推进核心 export class TurnManager { private units: Unit[] []; private currentIndex: number 0; private roundCount: number 0; // 按速度排序速度相同则按注册顺序保证结果可复现 public sortBySpeed(): void { this.units.sort((a, b) { if (b.speed ! a.speed) return b.speed - a.speed; return a.registerOrder - b.registerOrder; }); } // 推进到下一个行动单位一轮结束则回合数 1 public nextTurn(): Unit { this.currentIndex; if (this.currentIndex this.units.length) { this.currentIndex 0; this.roundCount; EventBus.emit(roundStart, this.roundCount); // 回合开始事件 } const unit this.units[this.currentIndex]; EventBus.emit(turnStart, unit); // 单位行动开始事件 return unit; } public getCurrentUnit(): Unit { return this.units[this.currentIndex]; } }这段代码里有两个参数值得注意registerOrder是单位注册时分配的序号用来在速度相同时打破平局避免每次排序结果不稳定导致战斗不可复现roundCount是回合计数很多状态效果比如“持续三回合”都依赖它来倒计时。EventBus.emit的两个事件点是扩展的关键——roundStart适合挂回合级的 buff 结算turnStart适合挂单位行动前的状态检查。把扩展点做成事件而不是硬编码调用是这套设计里最值得抄的一点。2.3 单位与状态的数据建模单位的数据结构决定了后续能扩展出多少玩法。工程里单位通常包含基础属性、当前状态列表、技能列表三块。状态buff/debuff用独立对象表示带剩余回合数和结算时机标记这样状态结算就能统一在回合事件里处理而不是散落在各个技能逻辑里。常见做法是给状态加一个timing字段标记它在回合开始、回合结束还是行动前结算TurnManager 在对应事件点遍历所有单位的状态列表做统一处理。这样加一个新状态只需要写它的效果和时机不用改调度代码。3. 把战斗跑起来从打开工程到第一场对局3.1 环境准备与工程导入先把 Cocos Creator 装好版本对齐工程声明然后用 Dashboard 的“导入项目”指向工程根目录。导入后如果控制台报脚本编译错误优先检查 TypeScript 版本和tsconfig.json是否被改动过。导入完成后打开assets/scenes下的战斗场景编辑器里应该能看到单位预制体被实例化到场景中。这一步最容易出的问题是预制体引用丢失表现为场景里单位显示为空白节点原因通常是资源路径变更或 meta 文件冲突解决方式是重新拖拽预制体到对应挂载点或者清理library目录让编辑器重新生成资源索引。3.2 战斗流程的启动链路战斗的启动一般从场景脚本的onLoad或按钮回调进入链路是注册单位 → 初始化管理器 → 启动第一回合。下面这段是典型的启动代码对应工程里战斗入口的写法// BattleEntry.ts —— 战斗入口 import { BattleManager } from ./manager/BattleManager; import { TurnManager } from ./manager/TurnManager; export class BattleEntry extends Component { private battleManager: BattleManager; private turnManager: TurnManager; start() { // 1. 创建管理器实例 this.battleManager new BattleManager(); this.turnManager new TurnManager(); // 2. 注册双方单位数据来自配置表或场景挂载 const playerUnits this.loadUnits(player); const enemyUnits this.loadUnits(enemy); this.battleManager.registerUnits([...playerUnits, ...enemyUnits]); // 3. 按速度排序并启动首回合 this.turnManager.sortBySpeed(); this.battleManager.startBattle(); } private loadUnits(camp: string): Unit[] { // 从配置或预制体读取单位数据此处省略具体实现 return []; } }这段代码的要点在于顺序先注册再排序排序必须在启动前完成否则第一回合的行动顺序是错的。loadUnits的camp参数用来区分阵营实际工程里通常还会带上初始站位信息。启动后BattleManager 会监听turnStart事件驱动当前单位行动行动结束再调用nextTurn形成闭环。调试时可以在nextTurn里打日志观察行动顺序是否符合速度预期这是排查回合逻辑最直接的手段。3.3 用事件总线串起表现层表现层不直接调用战斗逻辑而是订阅事件。比如血条更新订阅unitHpChanged行动条高亮订阅turnStart。这种做法的好处是战斗逻辑可以在没有 UI 的情况下单独测试——写个纯逻辑的测试脚本手动触发事件看数据变化是否正确。工程里 EventBus 的实现通常很轻一个 Map 存事件名到回调数组的映射on注册、emit遍历调用、off移除。注意在场景销毁时把订阅清掉否则切场景后旧回调还在会出现“血条更新到了已销毁节点”的报错这是回合制项目里非常常见的坑。4. 核心模块拆解回合循环、技能结算与胜负判定4.1 回合循环的完整链路一个完整的回合循环包含这几个阶段回合开始事件 → 按顺序取行动单位 → 单位行动选技能、结算伤害、结算状态→ 行动结束 → 检查胜负 → 下一单位或下一回合。工程里这条链路是显式写出来的而不是靠回调层层嵌套好处是流程可读、断点好打。下面这段是循环驱动的核心// BattleManager.ts —— 战斗主循环 public startBattle(): void { this.isRunning true; EventBus.emit(battleStart); while (this.isRunning) { const unit this.turnManager.nextTurn(); // 跳过已阵亡单位 if (!unit.isAlive()) continue; // 单位行动选择目标、释放技能、结算 this.executeAction(unit); // 每次行动后检查胜负 if (this.checkResult()) { this.endBattle(); break; } } } private executeAction(unit: Unit): void { const action unit.decideAction(this.getBattleContext()); if (!action) return; // 无可用行动则跳过 const targets this.resolveTargets(action, unit); for (const target of targets) { const damage this.calculateDamage(unit, target, action); target.applyDamage(damage); EventBus.emit(unitHpChanged, target); } }decideAction把“这个单位这回合干什么”交给单位自己决定AI 单位和玩家单位可以有不同的实现这是策略扩展的入口。calculateDamage集中处理伤害公式改数值平衡只动这一处。checkResult在每次行动后调用而不是每回合结束才调避免出现“单位已全灭但还多打了一轮”的尴尬。注意while循环里对已阵亡单位的continue判断不能省否则死掉的单位还会被拉出来行动。4.2 技能与状态的结算顺序技能结算的顺序直接影响战斗结果的正确性。常见做法是先结算直接伤害再结算附加状态最后检查状态是否触发连锁效果比如“受到伤害时反弹”。工程里把结算拆成几个独立方法按固定顺序调用而不是在一个大函数里混着写。状态结算则统一挂在回合事件上按timing字段分组处理。这里有个容易忽略的点状态施加和状态结算要分开施加时记录剩余回合数结算时递减并在归零时移除两个动作混在一起会导致“刚加上的状态立刻被扣一回合”。4.3 胜负判定与战斗收尾胜负判定要覆盖三种情况一方全灭、回合数超限、主动撤退。工程里checkResult返回枚举而不是布尔值这样收尾时能区分是胜利、失败还是平局。战斗结束时通过battleEnd事件通知表现层同时清理事件订阅和单位引用避免内存泄漏。收尾阶段还要把战斗结果剩余血量、回合数、掉落整理成结果对象供结算界面使用。这一步别偷懒直接读单位对象单位在清理后可能已被回收读出来是空值。5. 避坑与排查回合制项目里最容易翻车的几处5.1 行动顺序不稳定同样的阵容每次结果不同现象是同一套阵容反复打胜负结果飘忽。原因多半是排序时没有处理速度相同的情况Array.sort在部分环境下对相等元素的顺序不保证稳定。解决办法是给单位加注册序号排序比较函数里速度相等时按序号兜底保证结果可复现。这个坑在调试数值平衡时特别折磨人因为你会以为是数值问题其实是顺序问题。5.2 状态结算重复触发buff 掉得比预期快现象是“持续三回合”的状态两回合就没了。原因通常是状态在回合开始和行动前各结算了一次或者事件被重复订阅。排查方式是给状态结算加日志打印每次递减的时机和剩余回合数看是不是同一帧内被扣了两次。解决上状态结算只在一个明确的时机做事件订阅在注册前先off再on避免重复挂载。5.3 切换场景后报“节点已销毁”现象是战斗结束返回主菜单再进战斗控制台报节点相关错误。原因是 EventBus 的订阅没有在场景销毁时清理旧回调持有已销毁节点的引用。解决方式是在场景脚本的onDestroy里统一off掉本场景注册的所有事件或者给 EventBus 加一个按上下文批量清理的接口。这个坑几乎每个回合制项目都会踩一次。5.4 伤害计算出现负数或溢出现象是血量变成负数还继续扣或者大数值下伤害算错。原因是伤害公式没有做下限保护以及数值类型用了浮点导致精度问题。解决上伤害结果做Math.max(0, ...)保护血量扣减后钳制在 0 到最大值之间涉及大数值时统一用整数运算。别小看这个数值一膨胀浮点误差能让战斗结果完全跑偏。6. 二次开发把这套骨架改成你自己的战斗系统拿到这套工程后最实际的改造路径是从配置层入手而不是先动管理器。先把config里的单位表、技能表换成你自己的数值跑一遍看流程是否还通这一步能验证你对数据结构的理解是否正确。然后改decideAction把 AI 行为换成你想要的策略比如优先攻击血量最低的目标或者按技能冷却轮转。再往后才是动 TurnManager比如加入“行动条”机制替代固定顺序这时候因为调度和表现已经解耦改动范围是可控的。验证改造是否成功我一般会写一个纯逻辑的测试脚本不挂 UI直接构造单位和技能跑若干回合断言血量、状态、回合数的变化。这套工程的分层让这种测试很容易写因为管理器不依赖场景节点。下面是一个最小的验证片段// battle.test.ts —— 纯逻辑验证 const turnManager new TurnManager(); const battleManager new BattleManager(); const attacker new Unit({ speed: 10, attack: 50 }); const defender new Unit({ speed: 5, hp: 100 }); battleManager.registerUnits([attacker, defender]); turnManager.sortBySpeed(); // 断言速度高的先行动 console.assert(turnManager.getCurrentUnit() attacker, 行动顺序错误); // 模拟一次攻击后断言血量 defender.applyDamage(50); console.assert(defender.hp 50, 伤害结算错误);改造时还有一点值得注意新增状态或技能时优先通过事件挂载而不是改主循环。主循环是骨架改一次影响全局事件是插槽加多少都不影响原有流程。我吃过一次亏为了加一个“回合结束回血”的效果直接改了nextTurn结果后面所有依赖回合切换的逻辑都跟着出问题排查了大半天。从那以后我每次动主循环前都先问自己这个需求能不能用事件解决能就不碰循环。希望这套骨架能帮到你少走我走过的弯路。本文还有配套的精品资源点击获取
RELATED

相关推荐

知识图谱在技术转移中的应用与架构解析

知识图谱在技术转移中的应用与架构解析

1. 项目概述:当知识图谱遇上技术转移去年参与某高校科技成果转化平台建设时,我们遇到一个典型困境:科研处的老师拿着厚达300页的专利汇编手册却找不到匹配企业,而企业技术总监抱怨"根本看不懂这些学术专利描述"。这种信…

📅 2026/10/27 4:31:29
Visual Studio旧版下载与多版本管理全攻略

Visual Studio旧版下载与多版本管理全攻略

1. 为什么需要旧版Visual Studio? 作为Windows平台最强大的集成开发环境,Visual Studio的版本迭代速度相当快。但实际开发中,我们经常会遇到必须使用旧版本的特殊场景: 企业遗留项目维护:很多老项目基于特定VS版本构…

📅 2026/10/27 4:26:29
X-Plane 11飞行数据监控:200+原生DataRef实时采集与边缘部署

X-Plane 11飞行数据监控:200+原生DataRef实时采集与边缘部署

简介:StableApproach 是一款专为 X-Plane 11 飞行模拟器设计的飞行数据监控(FDM)增强工具,面向飞行模拟爱好者、航校学员及进阶飞行员,聚焦稳定进近阶段的关键参数分析与操作评估,助力用户通过真实数据复盘…

📅 2026/10/27 4:26:29
MORE NEWS

更多资讯

📰

009、Camera 开源生态:AOSPQualcommMediaTek

009、Camera 开源生态:AOSPQualcommMediaTek 那次调试记录我到现在还记得。一台骁龙平台的设备,客户反馈“切换前后摄偶尔黑屏”,抓log看到一行关键报错:ConfigureStreams() returns UNKNOWN_ERROR。顺着AOSP的CameraService代码往…

📰

OpenMontage:规则驱动的自动化蒙太奇剪辑实践

1. 从“OpenMontage”这个名字说起:它到底想解决什么问题第一次看到“OpenMontage”这个词,我脑子里蹦出来的第一反应是“开放式的蒙太奇”。蒙太奇这个词在影视剪辑里太常见了,指的是把不同镜头拼接在一起,产生新的含义。但把“O…

📰

私有Docker Registry部署运维与镜像分发实践

1. 自建镜像仓库的动机与边界私有 Docker Registry 这个词,我在过去几年里被问过不下二十次。有人是被公共镜像托管服务的拉取频率限制卡住了脖子,流水线一跑就是几十个节点同时拉取;有人是内网环境根本出不去,构建机与外网完全隔…

📰

OpenMontage 程序化剪辑:配置驱动批量视频生产实战

1. 从零搭建 OpenMontage:一个把视频剪辑变成“搭积木”的开源方案第一次听到 OpenMontage 这个名字,我下意识以为是又一个套壳的在线剪辑工具。直到我把它的仓库拉下来跑通第一个自动成片流程,才意识到这东西的定位其实更接近“视频剪辑的乐…

📰

008、Camera HAL 与 Linux Kernel 的边界

008、Camera HAL 与 Linux Kernel 的边界 写这个题目前,我脑子里先蹦出来一件往事。去年调一块老平台,HAL层一开流,预览画面绿油油,像是蒙了一层滤镜。驱动同事说HAL传下去的buffer格式不对,HAL同事说驱动把pixel form…

📰

E2E测试实战:用Playwright打造稳定可靠的全链路自动化测试

1. 先把话说透:E2E 测试到底在解决什么问题做了这么多年测试,我越来越觉得,E2E(End-to-End,端到端)测试是大家口中"最重要又最不想碰"的那类东西。一说起来都知道它是质量保障的最后一关&#xf…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬