尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Cocos Creator回合制战斗系统设计:分层架构与核心模块实现
简介CocoKnightManager 是一套基于 Cocos Creator 的回合制游戏系统设计开源工程面向希望掌握回合制玩法架构的初中级游戏开发者与独立团队帮助其解决战斗流程抽象、角色状态管理与规则扩展等核心问题。压缩包共 22 个文件约 216KB以 json 配置、meta 资源描述、png 贴图、ts 脚本及 fire 场景文件为主配合 README 说明构成可直接导入引擎的轻量项目骨架。资源围绕回合管理器、行动规则引擎与状态机三大组件展开采用模块化拆分与数据驱动设计属性、效果和事件均可通过 JSON 配置调整便于非程序人员参与内容迭代。目前已有 838 人学习下载读者可从中获取完整的回合流程组织思路、可复用的脚本结构与场景配置范例作为自研回合制项目的参考起点。1. 回合制战斗的“总调度室”CocoKnightManager 到底管什么如果你用 Cocos Creator 做过回合制战斗大概率经历过这种场面角色行动条走到头了技能动画播到一半Buff 结算和伤害飘字同时触发结果血量先扣后加、回合数错乱、UI 和逻辑各说各话。CocoKnightManager 就是冲着这类问题来的——它把回合制游戏里最容易写成一锅粥的部分拆成一套可复用的系统设计回合调度、行动队列、技能与 Buff 结算、战斗状态机、数据配置驱动。它适合两类人一类是刚接触 Cocos Creator、想照着搭一个能跑的战斗原型的开发者另一类是被“战斗逻辑和表现层纠缠不清”折磨过、想看看别人怎么分层的熟手。系统开源代码结构本身就是最好的说明书。2. 架构拆解回合制战斗的分层与数据流2.1 为什么回合制最怕“逻辑表现不分家”回合制游戏的本质是“状态推进”谁先动、动了之后产生什么结果、结果如何反馈到界面。很多新手项目翻车不是因为算法难而是因为把“播放动画”和“计算伤害”写在了同一个函数里。动画一卡、回调一乱战斗状态就不可信了。CocoKnightManager 的设计思路很明确逻辑层只负责算出“发生了什么”表现层只负责“把发生的事演出来”中间用事件或队列解耦。常见做法是三层结构层级职责典型模块数据层配置表、角色属性、技能/Buff 定义配置加载、数据模型逻辑层回合推进、行动排序、结算规则战斗管理器、回合控制器表现层动画、飘字、血条、按钮状态UI 控制器、动画回调这样拆的好处是逻辑层可以脱离渲染单独跑测试。你甚至可以在控制台里把一场战斗从头模拟到尾不用打开场景。对于回合制这种“结果确定、过程可复现”的类型这一点非常关键。2.2 目录结构先认清每个文件夹的职责拿到源码后别急着点运行先花十分钟看目录。一个典型的 Cocos Creator 回合制项目结构大致如下具体命名以源码为准这里按这类项目的通用组织方式说明assets/ ├── scripts/ │ ├── core/ # 战斗核心回合调度、状态机、事件总线 │ │ ├── BattleManager.ts │ │ ├── TurnController.ts │ │ └── EventBus.ts │ ├── data/ # 数据模型与配置解析 │ │ ├── UnitData.ts │ │ ├── SkillConfig.ts │ │ └── BuffConfig.ts │ ├── logic/ # 结算规则伤害、治疗、Buff 生效 │ │ ├── DamageCalculator.ts │ │ └── BuffResolver.ts │ └── ui/ # 表现层血条、行动条、技能面板 │ ├── BattleHUD.ts │ └── UnitView.ts ├── resources/ # 动态加载的配置与预制体 │ ├── config/ │ └── prefabs/ └── scenes/ └── Battle.scene看目录就能判断作者的意图core里放的是不依赖具体 UI 的调度逻辑logic里是纯计算ui只做展示。如果你二次开发时把伤害公式写进了UnitView那基本就破坏了这套分层后面加新技能会越来越难受。2.3 核心数据流一次行动从触发到落地把一次角色行动拆开看数据流是这样的回合控制器根据速度或行动值决定“轮到谁”战斗管理器向该单位发出“行动开始”事件单位选择技能逻辑层计算目标、伤害、Buff 变化结算结果打包成事件抛给表现层表现层播放动画动画结束后回调通知逻辑层“可以推进下一步”。这个链路里最关键的是第 4 步逻辑层不要直接调用 UI 的播放函数而是发事件。下面是一段简化的事件总线写法用来承载这种解耦// EventBus.ts —— 极简事件总线逻辑层与表现层的唯一通道 type Handler (...args: any[]) void; export class EventBus { private static handlers: Mapstring, Handler[] new Map(); // 注册监听表现层订阅自己关心的事件 static on(event: string, handler: Handler) { if (!this.handlers.has(event)) { this.handlers.set(event, []); } this.handlers.get(event)!.push(handler); } // 派发事件逻辑层只负责“说发生了什么” static emit(event: string, ...args: any[]) { const list this.handlers.get(event); if (!list) return; list.forEach(fn fn(...args)); } // 清理切场景时务必调用否则旧监听会残留 static off(event: string, handler?: Handler) { if (!handler) { this.handlers.delete(event); return; } const list this.handlers.get(event) || []; this.handlers.set(event, list.filter(fn fn ! handler)); } }这段代码没有花哨的东西但它是整个分层的地基。on负责订阅emit负责广播off负责清理。参数event建议用常量管理比如BattleEvent.UNIT_DAMAGED避免字符串拼错。很多项目后期出现“动画播了但血没掉”或者“血掉了两次”追根溯源都是事件重复注册或没清理。切场景时调用off是血泪经验别省。2.4 回合调度器行动顺序是怎么定出来的回合制的“回合”有两种常见实现一种是固定顺序轮转一种是行动值/速度条。CocoKnightManager 这类系统设计通常会留出扩展点。下面是一个基于行动值的调度器骨架// TurnController.ts —— 按行动值决定下一个行动者 interface Unit { id: string; speed: number; // 速度越高行动值增长越快 actionValue: number; // 当前行动值达到阈值即可行动 } export class TurnController { private units: Unit[] []; private threshold 100; // 行动值阈值可配置 constructor(units: Unit[]) { this.units units; } // 推进行动值返回本轮可以行动的单位 public tick(): Unit | null { // 所有单位按速度累加行动值 this.units.forEach(u { u.actionValue u.speed; }); // 找出行动值最高且达到阈值的单位 const ready this.units .filter(u u.actionValue this.threshold) .sort((a, b) b.actionValue - a.actionValue); if (ready.length 0) return null; const actor ready[0]; actor.actionValue - this.threshold; // 行动后扣除保留余量 return actor; } }tick每次被调用时推进所有单位的行动值threshold是行动门槛speed决定推进速度。行动后减去阈值而不是清零是为了让高速单位在长线战斗中体现优势。这个设计的好处是你不需要维护一个固定的“回合队列”加人、减人、加速减速都只是改数值。参数怎么调threshold调大战斗节奏变慢speed差距调大先手优势更明显。如果发现某个单位永远轮不到先检查它的speed是不是被配成了 0。3. 跑起来环境准备与第一场战斗3.1 环境与项目导入这类 Cocos Creator 项目第一步永远是版本对齐。用错编辑器版本轻则预制体丢引用重则脚本编译不过。操作顺序如下确认源码根目录下的project.json或package.json里记录的 Creator 版本用对应大版本的 Cocos Dashboard 导入项目文件夹等待资源导入完成观察控制台是否有脚本报错打开Battle.scene检查场景里的节点引用是否完整。# 如果项目带 package.json可以先看依赖和脚本入口 cat package.json # 确认 Creator 版本线索不同版本字段名不同以实际文件为准 cat project.json这里没有复杂的构建命令Cocos Creator 是编辑器驱动的。导入后如果出现大量“Missing Script”八成是版本不匹配或者脚本没编译完先别改代码等编辑器把temp和library目录重建完再看。第一次导入慢是正常的别急着强退。3.2 配置表战斗数值的入口回合制项目的可玩性很大程度靠配置。CocoKnightManager 这类设计通常会把角色、技能、Buff 放在resources/config下用 JSON 或表格导出。启动战斗前先确认配置能被正确加载// ConfigLoader.ts —— 启动时加载战斗配置 import { resources, JsonAsset } from cc; export class ConfigLoader { public static async loadAll(): Promisevoid { // 并行加载减少启动等待 const [units, skills, buffs] await Promise.all([ this.loadJson(config/units), this.loadJson(config/skills), this.loadJson(config/buffs), ]); // 挂到全局或注入战斗管理器具体看项目约定 console.log(单位配置条数:, units.length); console.log(技能配置条数:, skills.length); console.log(Buff 配置条数:, buffs.length); } private static loadJson(path: string): Promiseany[] { return new Promise((resolve, reject) { resources.load(path, JsonAsset, (err, asset) { if (err) { reject(err); return; } resolve(asset.json as any[]); }); }); } }resources.load的路径不带扩展名JsonAsset是 Cocos 内置的 JSON 资源类型。Promise.all让三张表并行加载减少黑屏时间。加载完打印条数是个很实用的自检手段如果某个表是 0 条说明路径错了或者文件没放在resources下。注意resources目录下的资源才会被打进包放在别处动态加载会失败这是新手最常踩的坑之一。3.3 启动一场战斗从入口脚本到回合循环配置就绪后战斗的启动通常由一个入口脚本完成创建单位、初始化调度器、进入回合循环。下面是一个最小可跑的启动流程// BattleEntry.ts —— 战斗入口挂在场景根节点上 import { _decorator, Component } from cc; import { ConfigLoader } from ./ConfigLoader; import { TurnController } from ./TurnController; import { EventBus } from ./EventBus; const { ccclass } _decorator; ccclass(BattleEntry) export class BattleEntry extends Component { private turnController: TurnController | null null; async start() { await ConfigLoader.loadAll(); // 1. 加载配置 const units this.buildUnits(); // 2. 构建战斗单位 this.turnController new TurnController(units); // 3. 初始化调度 this.runLoop(); // 4. 进入回合循环 } private buildUnits() { // 这里按配置生成单位示例只给两个 return [ { id: hero, speed: 12, actionValue: 0 }, { id: enemy, speed: 9, actionValue: 0 }, ]; } private runLoop() { const actor this.turnController!.tick(); if (!actor) { // 没有单位达到行动阈值下一帧继续推进 this.scheduleOnce(() this.runLoop(), 0.1); return; } // 通知表现层轮到谁了 EventBus.emit(TURN_START, actor.id); // 行动结束后继续下一轮实际项目里由动画回调驱动 this.scheduleOnce(() this.runLoop(), 0.5); } }start里四步走加载配置、构建单位、初始化调度、进入循环。runLoop用scheduleOnce模拟时间推进真实项目里应该由动画结束回调或玩家输入来驱动而不是固定延时。EventBus.emit(TURN_START, actor.id)是逻辑层向表现层喊话的地方UI 收到后高亮当前行动单位。参数0.5只是演示节奏实际应替换为动画时长。跑通这一步你就能在控制台看到回合交替的日志说明骨架是通的。4. 核心模块拆解伤害结算与 Buff 系统怎么写才不乱4.1 伤害计算把公式关进一个类里伤害结算是回合制的核心也是最容易被写散的地方。合格的做法是单独一个计算器输入输出都是纯数据不碰任何节点// DamageCalculator.ts —— 纯函数式伤害计算 export interface DamageInput { attack: number; // 攻击方攻击力 defense: number; // 防守方防御力 skillRate: number; // 技能倍率比如 1.2 表示 120% critRate: number; // 暴击率 0~1 critMultiplier: number; // 暴击倍率比如 1.5 } export interface DamageResult { damage: number; isCrit: boolean; } export class DamageCalculator { public static calculate(input: DamageInput): DamageResult { // 基础伤害攻击减防御保底为 1避免出现 0 或负数 const base Math.max(input.attack - input.defense, 1); const isCrit Math.random() input.critRate; const multiplier isCrit ? input.critMultiplier : 1; const damage Math.floor(base * input.skillRate * multiplier); return { damage, isCrit }; } }base用Math.max保底防止高防御单位完全免伤导致战斗卡死。skillRate是技能倍率critRate和critMultiplier控制暴击。返回isCrit是为了让表现层决定要不要播暴击特效逻辑层不关心特效长什么样。这个类没有任何 Cocos 依赖你可以直接拿去做单元测试。二次开发时想加属性克制就在这里加一个elementRate参数别去改 UI 代码。4.2 Buff 结算时机比效果更重要Buff 系统翻车的重灾区不是“效果算错”而是“结算时机错”。中毒是在回合开始扣血还是回合结束扣血护盾是先吸收还是后吸收这些必须在设计上定死。下面是一个按时机分发的 Buff 解析器骨架// BuffResolver.ts —— 按触发时机结算 Buff export type BuffTiming TURN_START | TURN_END | ON_HIT; export interface Buff { id: string; timing: BuffTiming; duration: number; // 剩余回合数 value: number; // 效果数值比如每回合扣血量 } export class BuffResolver { // 在指定时机结算某个单位身上的 Buff public static resolve(unit: { hp: number; buffs: Buff[] }, timing: BuffTiming) { unit.buffs .filter(b b.timing timing) .forEach(b { if (b.id poison) { unit.hp Math.max(unit.hp - b.value, 0); } // 其他 Buff 按 id 分发保持可扩展 }); // 回合结束时统一扣持续时间避免同一回合多次递减 if (timing TURN_END) { unit.buffs.forEach(b b.duration - 1); unit.buffs unit.buffs.filter(b b.duration 0); } } }timing把 Buff 分成回合开始、回合结束、受击时三类resolve只处理匹配时机的 Buff。持续时间统一在TURN_END递减这个细节很关键如果每个 Buff 自己管自己的倒计时很容易出现“刚加上就少一回合”的玄学问题。filter清理过期 Buff防止数组无限增长。想加新 Buff就在forEach里加一个id分支或者改成策略表别把逻辑散到各处。4.3 状态机战斗阶段要有明确边界一场战斗至少有“准备、进行、结算”几个阶段用状态机管住边界能避免“战斗结束了还能点技能”这类问题// BattleState.ts —— 战斗阶段状态机 export enum BattleState { READY READY, RUNNING RUNNING, FINISHED FINISHED, } export class BattleStateMachine { private state: BattleState BattleState.READY; public canAcceptInput(): boolean { return this.state BattleState.RUNNING; } public start() { if (this.state ! BattleState.READY) return; this.state BattleState.RUNNING; } public finish() { if (this.state ! BattleState.RUNNING) return; this.state BattleState.FINISHED; } public get current(): BattleState { return this.state; } }canAcceptInput是 UI 层每次点击前要问的一句话start和finish都做了前置判断防止重复调用。状态机本身很简单价值在于“所有阶段切换只有一个入口”。二次开发加“暂停”或“结算演出”阶段时在这里扩展枚举和判断比在十几个脚本里加if靠谱得多。5. 避坑与排查那些让战斗跑不起来的细节5.1 现象动画播完回合不推进原因通常是表现层的动画回调没有通知逻辑层或者事件名对不上。解决在动画结束回调里统一EventBus.emit(ACTION_END)逻辑层监听后调用runLoop。排查时先在emit和on两侧各打一条日志确认事件是否真的发出和收到。5.2 现象血量变成负数或飘字对不上原因是伤害计算和 UI 更新用了两份数据或者扣血没有做下限保护。解决所有血量变更只走一个入口函数内部用Math.max(hp - damage, 0)保底UI 只读结果不自己算。飘字数值直接取结算结果里的damage别在 UI 里重新算一遍。5.3 现象切场景后旧战斗还在跑原因是EventBus的监听没清理scheduleOnce的定时器没取消。解决在组件的onDestroy里调用EventBus.off并unscheduleAllCallbacks。这类问题很隐蔽表现是“新战斗里偶尔冒出旧单位的日志”本质是内存里的旧对象还活着。5.4 现象配置改了但游戏里没生效原因是资源被缓存或者改的不是resources下的那份。解决确认修改路径在resources内必要时在编辑器里刷新资源构建后测试要重新打包别拿旧包验证新配置。另外 JSON 字段名大小写敏感attack写成Attack就是读不到。5.5 现象高速单位连续行动低速单位没机会原因是行动值阈值和速度差设置不合理或者行动后没有正确扣除行动值。解决检查tick里是否执行了actionValue - threshold并适当调大threshold或压缩速度区间。回合制的节奏感很依赖这组参数建议做成可配置项方便调平衡。6. 二次开发从能跑到好改的几个改造点把项目跑通只是开始真正决定它能不能为你所用的是改造路径。我的习惯是先动数据、再动规则、最后动表现顺序反了容易把自己绕进去。第一个改造点是加新技能。不要直接改DamageCalculator的公式而是在技能配置里加字段比如effectType然后在结算入口按类型分发。这样老技能不受影响新技能可以独立验证。第二个改造点是属性克制在DamageInput里加elementRate由配置表决定倍率计算器只负责乘上去。第三个改造点是战斗日志逻辑层每次结算后往一个数组里 push 一条结构化记录战斗结束可以回放排查数值问题时比看动画靠谱得多。验证方法上我一般会写一个脱离场景的模拟脚本把配置喂进去跑一百场统计胜率和平均回合数。回合制的好处就是结果可复现同样的随机种子应该得到同样的战斗过程。如果两次跑出来不一样说明有地方用了没受控的随机数或者依赖了帧率。改造目标建议改动位置验证方式新增技能类型技能配置 结算分发单场战斗对比伤害属性克制DamageInput 增加倍率字段模拟脚本统计胜率战斗回放逻辑层记录结构化日志重放日志比对状态多队伍支持TurnController 管理单位集合增减单位后跑循环有个教训我印象很深早期我图省事把 Buff 的持续时间递减写在了 UI 的回合提示回调里结果有一次动画被跳过Buff 就永远不消失战斗直接卡死。从那以后我每次加结算逻辑都强制走一遍“逻辑层算完再发事件”的流程绝不让表现层碰状态。希望这套拆法能帮到你少走一点我走过的弯路。本文还有配套的精品资源点击获取
RELATED

相关推荐

基于Python+Django+jieba的文本相似度计算系统设计与实现

基于Python+Django+jieba的文本相似度计算系统设计与实现

简介:这份DOCX文档面向计算机相关专业毕业设计学生与自然语言处理入门者,围绕基于Python的文本相似度计算系统给出论文式方案参考,可帮助解决选题立意、系统设计和论文结构搭建等问题。压缩包共1个DOCX文件,约749KB,主…

📅 2026/10/31 7:12:46
EEGLab脑电分析全流程:从数据导入到ICA伪迹剔除的实操指南

EEGLab脑电分析全流程:从数据导入到ICA伪迹剔除的实操指南

简介:EEGLab V10.0.0.0b 压缩包面向神经科学、心理学与生物医学工程领域的研究者及学生,提供一套完整的脑电(EEG)分析工具环境,可用于信号质量检查、预处理、事件相关电位分析、频率分析与源定位等任务,也便…

📅 2026/10/31 7:12:46
Python+MySQL网络舆情分析系统源码部署与双模型实践

Python+MySQL网络舆情分析系统源码部署与双模型实践

简介:这份文档是一篇围绕网络舆情分析系统设计与实现的毕业论文,面向计算机相关专业学生、课程设计或毕业设计写作者,以及希望了解舆情监测系统技术路线的开发者。内容以 Python 与 MySQL 为主线,串联舆情分析、言论管理、用户管理…

📅 2026/10/31 7:12:46
MORE NEWS

更多资讯

📰

ICU4C 54.1 源码包实战:从编译集成到裁剪优化

简介:这份资源是 ICU4C 54.1 版本的完整 C 源码包,面向需要在 Windows 平台使用 Visual C 编译国际化与 Unicode 支持库的开发者,尤其适合从事跨语言、跨区域应用开发的中高级 C 工程师。压缩包共 4519 个文件,约 25.7MB&#xff…

📰

OpenShell 环境描述与执行逻辑分离:实现命令执行的可移植与可复现

1. 从“OpenShell”这个名字说起:它到底想解决什么问题 第一次看到“OpenShell”这个词,很多人会下意识地把它和“命令行外壳”“终端环境”联系起来。这个直觉不算错,但如果只停留在“又一个终端工具”的层面,就很容易低估它真正…

📰

模型PMF实战:从返工成本到评测集与留存度量

先说一个我反复见到的场面:团队花了两三个月把一个模型能力调到很不错的状态,内部 demo 演示时全场鼓掌,上线两周后日活掉了一半,运营开始怀疑是不是模型不够强,于是继续加数据、换更大的底座、加更长上下文——然后数…

📰

插件系统设计指南:从plugin.json到TypeScript SDK的加载与激活机制

1. 从“plugins”这个标题说起:插件系统到底在解决什么问题 “plugins”这个词看起来简单,但它背后牵扯的东西一点都不少。我做了十多年开发,几乎每隔一段时间就要跟各种插件系统打交道——从编辑器插件、构建工具插件,到 CLI 的扩…

📰

基于语音识别与蓝牙通信的智能家居控制原型设计与实现

简介:这份资源面向嵌入式与物联网方向的毕业设计学习者及智能家居入门开发者,提供一套以语音指令控制灯光、温度、窗帘等家居设备的完整实现方案,核心融合32位单片机、语音识别模块与蓝牙通信技术。压缩包共2358个文件、约33.89MB&#xff0c…

📰

AI时代开源社区的信任机制:Vouch解决方案解析

1. 开源社区的信任危机:AI代码泛滥的现状与挑战2024年以来的AI浪潮正在深刻改变软件开发的面貌,特别是像Claude Code这样的AI编程助手的普及,使得代码生成变得前所未有的简单。但硬币的另一面是,开源项目维护者们突然发现自己被淹…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

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

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

📞 💬