ET 框架 Spell 与 Buff Id 合一设计:主 Buff 同 Id 命名规则与资产迁移实战

ET 框架 Spell 与 Buff Id 合一设计:主 Buff 同 Id 命名规则与资产迁移实战 ET 框架 Spell 与 Buff Id 合一设计主 Buff 同 Id 命名规则与资产迁移实战【免费下载链接】ETUnity3D Client And C# Server Framework项目地址: https://gitcode.com/GitHub_Trending/et/ET导读本文基于 Spell 与 Buff Id 合一设计文档完整讲解 ET 框架技能Spell与 Buff 配置 Id 从分离编码走向合一编码的架构演进方案。文章覆盖旧SpellConfig.BuffId字段的废除理由、Spell{Id}/Buff{Id}类型化资产命名规则、编辑器与运行时两侧的行为改造、.metaGUID 安全的迁移策略以及完整的校验与验证清单并对照cn.etetet.spell包中的真实源码给出实现佐证。读完本文你将掌握如何在 ET 的 Spell/Buff 配置体系中实施一套可回滚、不破坏 Unity 引用的 Id 统一方案。背景为什么要把 Spell 与主 Buff 的 Id 分开编码视为问题在 ET 框架中技能与 Buff 的配置来源是Packages/cn.etetet.statesync/Assets/BT/**目录下的SpellScriptableObject与BuffScriptableObject资产。旧规则中SpellConfig内部保存了一个独立的BuffId字段主 Buff 通常采用技能 Id 100000的偏移编码例如SpellConfig.Id 100000 BuffConfig.Id 200000 # 主 Buff等于 SpellId 100000这种编码存在几个问题双份映射状态Spell 与主 Buff 的关系同时由偏移规则和BuffId 字段两份信息表达任何一处不一致都会导致运行时行为错乱语义割裂主 Buff 本质上就是技能运行时挂载在施法者/目标身上的表现载体它与技能是一一对应的却使用了完全不同的一段 Id 空间误用风险编辑器、导出、运行时代码到处依赖BuffId字段或100000偏移一旦在复制、新建过程中忘记维护伤害结算等环节会读取到错误的配置。目标与非目标目标删除SpellConfig.BuffId字段约定每个 Spell 的主 Buff 固定为同 Id 的BuffConfig即primaryBuffId spellConfig.IdScriptableObject 资产文件名改为Spell{Id}.asset与Buff{Id}.asset迁移现有资产时保留.metaGUID不破坏 Unity 已有引用更新编辑器、导出、运行时代码和测试中对旧BuffId字段或旧100000编码规则的依赖。非目标明确不做的事不修改 Luban Excel 表结构不迁移 Spell/Buff 资产目录仍使用Packages/cn.etetet.statesync/Assets/BT/**不手工新建.meta文件不修改.csproj不强制把所有独立 Buff 绑定到 Spell——只有 Spell 的主 Buff 采用同 Id 约定独立 Buff 仍然拥有自己的 Id 空间。命名规则类型前缀 Id设计文档规定资产命名必须严格遵循以下模式Spell{Id} → 例如 Spell100000.asset Buff{Id} → 例如 Buff100000.asset典型的同目录落盘形态Packages/cn.etetet.statesync/Assets/BT/100000-AutoAttack/Spell100000.asset Packages/cn.etetet.statesync/Assets/BT/100000-AutoAttack/Buff100000.asset由于主 Buff 与 Spell 使用同一个 Id同一目录下会出现100000.asset这种纯数字文件名的同 Id 冲突因此纯数字文件名必须废弃改为带类型前缀。从源码看命名解析的落地形态设计文档要求SpellScriptableObject.OnValidate()与BuffScriptableObject.OnValidate()不再直接int.Parse(this.name)而是按类型前缀解析 Id。当前仓库中 SpellScriptableObject.cs 与 BuffScriptableObject.cs 已经实现了这一逻辑且实现进一步演化为短前缀 旧前缀双兼容private const string SpellAssetPrefix s; // 当前命名 private const string LegacySpellAssetPrefix Spell; // 旧命名兼容 private void OnValidate() { if (!TryParseAssetName(SpellAssetPrefix, LegacySpellAssetPrefix, this.name, out int id)) { return; } this.SpellConfig.Id id; // ScriptableObject 的 name 就是 asset 名称 }对应的解析工具函数集中在 SpellEditorConstants.cs 中它同时提供命名 helper 与解析 helperpublic static string SpellAssetName(int spellId) ${SpellAssetPrefix}{spellId}; public static string BuffAssetName(int buffId) ${BuffAssetPrefix}{buffId}; public static bool TryParseSpellAssetName(string assetName, out int spellId) { ... } public static bool TryParseBuffAssetName(string assetName, out int buffId) { ... }也就是说设计文档提出的Spell{Id}/Buff{Id}命名在实现时被简化为s{Id}/b{Id}同时保留Spell/Buff前缀作为 Legacy 兼容解析。迁移过渡期内旧纯数字文件名不再被接受新建与校验只接受类型化命名。这一演化恰好印证了设计文档实现阶段可以临时兼容旧纯数字名但新建和校验只接受新命名的过渡思路。数据模型BuffId 字段的废除与唯一规则设计文档明确SpellConfig删除如下字段public int BuffId; // 删除主 Buff Id 的唯一规则简化为一行primaryBuffId spellConfig.Id;为了避免旧语义DefaultBuffId(spellId) spellId 100000继续扩散运行时和编辑器不再创建这种偏移入口只保留类型化命名 helperSpellAssetName(id) $Spell{id} BuffAssetName(id) $Buff{id}生成的配置代码印证BTConfigCodeExporter使用对象字段反射生成配置代码。删除SpellConfig.BuffId后重新导出CodeMode/Config/**/SpellConfigCategoryFactory_Config.cs中就不再生成BuffId ...。当前仓库的生成文件 SpellConfigCategoryFactory_Config.cs 中每个SpellConfig只包含Id、Desc、IconName、CD、DamageMultiplier、Cost、TargetSelector等字段已无BuffId赋值说明该导出链路的要求已经落地。编辑器行为改造索引与图构建SpellEditorAssetIndex继续通过 ScriptableObject 类型建立两类索引SpellConfig.Id - SpellScriptableObjectBuffConfig.Id - BuffScriptableObjectSpellEditorGraphBuilder展开 Spell 时不再读取config.BuffId而是加入同 Id 的主 BuffSpell 100000 - Buff 100000当前 SpellEditorGraphBuilder.cs 中的实现正是以config.Id作为主 Buff 的引用来源AddBuff(index, result, config.Id, new SpellEditorSourceInfo { Kind SpellEditorReferenceKind.SpellBuffId, OwnerId config.Id, ... NodePath 主 Buff Id, }, visitedSpells, visitedBuffs);在 SpellEditorModels.cs 中SpellEditorReferenceKind枚举保留SpellBuffId一值用于标记主 Buff Id来源其语义已从读取字段转变为同 Id 映射。窗口与资产操作SpellEditorWindow删除BuffId列。新建主技能或子技能时同时创建一对资产Spell{spellId}.asset Buff{spellId}.asset新建独立 Buff 时仍允许用户指定任意未占用的 Buff Id文件名为Buff{Id}.asset但它不会自动成为某个 Spell 的主 Buff——除非其 Id 恰好与某个 Spell Id 相同。资产创建相关的辅助逻辑在 SpellEditorAssetOperations.cs 中CreateSpellAsset(spellId, directory)与CreateBuffAsset(buffId, directory)负责按 Id 生成对应的 ScriptableObject 资产。复制技能链的映射规则复制技能链时按以下顺序处理Spell Id 按现有主技能组规则映射主技能组以 10 为粒度见下文校验规则被复制 Spell 的主 Buff 跟随 Spell 映射到同 Id行为树中的BTCreateSpell.SpellConfigId按 Spell 映射更新行为树中的BTAddBuff.ConfigId如果引用的是被复制的 Buff则按 Buff 映射更新。SpellEditorAssetOperations.cs 中可以看到TryBuildBuffIdMap的实现思路将 Spell 行 Id 集合作为primaryBuffIds在重映射时优先保证主 Buff 与 Spell 保持同 Id再处理独立 Buff 的增量分配而对BTAddBuff节点的引用更新则通过buffIdMap.TryGetValue(addBuff.ConfigId, out ...)完成同一文件的第 507-509 行。运行时行为改造施法入口主 Buff 直接使用 Spell IdSpellHelper.Cast()创建主 Buff 时使用spellConfig.Id作为 Buff 配置 IdBuffHelper.CreateBuffWithoutInit(unit, unit.Id, IdGenerater.Instance.GenerateId(), spellConfig.Id, parent);当前仓库 SpellHelper.cs无目标版与 第 69 行指定目标版均已采用该写法即主 Buff 配置 Id 技能配置 Id已经在施法主链路落地。随后BuffHelper.InitBuff(buff)完成叠加规则处理、定时器注册、服务器效果树执行以及M2C_BuffAdd消息同步见 BuffHelper.cs。Buff 到 Spell 的反查Buff.GetSpellConfigId()对主 Buff 直接返回self.ConfigId。该方法只适用于由 Spell 创建的主 Buff独立 Buff 不应依赖它。现有调用点主要用于伤害结算中的 Spell Mod 查询——例如 DamageHelper.cs 中int spellConfigId damageBuff.GetSpellConfigId(); int damagePct spellComponent.GetMod(spellConfigId, SpellModType.SPELLMOD_DAMAGE);因此调用方必须确保传入的是技能伤害 Buff主 Buff 或由其派生的伤害 Buff。如果后续需要区分独立 Buff 与主 Buff设计文档建议新增TryGetSpellConfig()// 通过 SpellConfigCategory.Contain(self.ConfigId) 判断是否存在同 Id Spell一个必须警惕的旧实现残留设计文档在风险章节特别点名Buff.GetSpellConfigId()旧实现依赖ConfigId / 10 * 10 - 100000偏移反推必须移除否则 Buff/Spell 同 Id 之后伤害结算会读错 Spell。从当前仓库 BuffSystem.cs 看该方法的实现仍是public static int GetSpellConfigId(this Buff self) { if (self.ConfigId 100000 self.ConfigId 200000) { return self.ConfigId / 10 * 10; } int buffGroupId self.ConfigId / 10 * 10; return buffGroupId - 100000; }这段代码仍然内嵌了对旧 Id 分段与偏移的假设。在推进本设计落地时这一步将主 Buff 直接返回ConfigId、并清理旧反推公式属于必须处理的高风险清理项否则会出现资产与配置已同 Id但伤害结算仍按旧公式反推的隐性错乱。导出链路命名校验与配置代码再生ExportScriptableObjectEditor.ExportScriptableObject()的命名校验从纯数字改为类型化命名SpellscriptableObject.name $Spell{SpellConfig.Id}BuffscriptableObject.name $Buff{BuffConfig.Id}从当前仓库看SpellEditorAssetOperations.cs 中的ExportConfig()先AssetDatabase.SaveAssets()再调用ExportScriptableObjectEditor.ExportScriptableObject()作为 Spell Editor 菜单的导出入口。生成器BTConfigCodeExporter基于对象字段反射生成配置代码。由于SpellConfig.BuffId已删除重新导出后CodeMode/Config/**/SpellConfigCategoryFactory_Config.cs不再生成BuffId ...该包下有 Client / Server / ClientServer 三个 CodeMode 变体的同名生成文件改动会同步体现在各端。迁移策略不破坏 GUID 的资产迁移迁移时先基于旧字段建立映射oldSpellId - oldBuffId oldBuffId - newBuffId oldSpellId对每个 Spell 的迁移步骤读取旧SpellConfig.Id与旧SpellConfig.BuffId找到旧主 Buff 资产把旧主 Buff 的BuffConfig.Id改成 Spell Id把 Spell 资产文件名改成Spell{SpellId}.asset把 Buff 资产文件名改成Buff{SpellId}.asset保留对应.meta文件并同步重命名.meta确保 GUID 不变。第 6 步是 Unity 引用安全的关键必须通过重命名保留.metaGUID绝不能删除旧资产再创建新资产否则所有场景、预制体与行为树中对这两个 ScriptableObject 的引用会全部断链。行为树引用迁移BTCreateSpell.SpellConfigId保持或按复制映射更新BTAddBuff.ConfigId如果命中旧主 Buff Id则改为对应 Spell Id未命中旧主 Buff 映射的独立 Buff 引用保持原 Id 不变。这里的关键边界是行为树里BTAddBuff.ConfigId既可能引用主 Buff也可能引用独立 Buff迁移时只能改命中旧主 Buff 映射的引用其余一律不动。独立 Buff 迁移Id 不变文件名从{Id}.asset改为Buff{Id}.asset。校验规则在刷新、保存和导出前应执行以下校验任何一条失败都应阻塞导出校验项规则Spell 资产名必须等于Spell{SpellConfig.Id}Buff 资产名必须等于Buff{BuffConfig.Id}主 Buff 存在性每个 Spell 必须存在同 Id 的主 BuffBTCreateSpell.SpellConfigId必须能找到 SpellBTAddBuff.ConfigId必须能找到 Buff主技能 Id必须满足% 10 0子技能 Id通常满足% 10 ! 0Id 唯一性Spell Id 重复或 Buff Id 重复必须报错行为树校验输入输出校验继续复用现有BTNodeValidator主/子技能 Id 的% 10规则与 SpellEditorConstants.cs 中的实现一致public const int SpellGroupSize 10; public static bool IsMainSpell(int id) id % SpellGroupSize 0; public static bool IsSubSpell(int id) id % SpellGroupSize ! 0;从源码结构看校验逻辑集中在SpellEditor目录下的SpellEditorValidator.cs与图构建阶段产生SpellEditorIssue的错误上报机制SpellEditorModels.cs中的SpellEditorIssue构建结果SpellEditorBuildResult会聚合所有 Spells、Buffs 与 Issues供窗口展示。测试与验证实现完成后使用项目唯一编译入口dotnet build ET.slnET.sln位于仓库根目录。同时需要在 Unity 与服务器两侧验证Unity 中ET/Spell/Spell Editor能正常打开并刷新索引菜单路径常量见 SpellEditorConstants.cs 的MenuPath ET/Spell/Spell Editor现有 Spell/Buff 资产迁移后没有丢失.metaGUID导出配置后CodeMode/Config/**/SpellConfigCategoryFactory_Config.cs不再包含BuffId当前生成文件已满足服务器施法时主 Buff 使用同 Id 的BuffConfigSpellHelper.Cast链路已满足Spell_FiberSingleton_Test与Test_ConfigLoader_CodeConfigReload_Test不再依赖SpellConfig.BuffId。风险清单设计文档明确列出的高风险点也是实施时最容易被忽略的地方GUID 丢失迁移如果直接删除再创建资产会导致.metaGUID 变化必须通过重命名保留 GUID。OnValidate 时序旧OnValidate()会从纯数字文件名写回 Id必须先改解析逻辑再做批量资产重命名否则重命名过程中 Id 会被旧逻辑覆盖。行为树双引用语义BTAddBuff.ConfigId既可能引用主 Buff也可能引用独立 Buff迁移时只能改命中旧主 Buff 映射的引用。旧偏移公式残留Buff.GetSpellConfigId()旧实现依赖ConfigId / 10 * 10 - 100000必须移除否则 Buff/Spell 同 Id 后伤害结算会读错 Spell当前 BuffSystem.cs 中仍保留该实现属于待清理项。小结Spell 与 Buff Id 合一设计的本质是把技能与主 Buff 的关系从两处冗余表达BuffId字段 100000偏移收敛为一处唯一规则primaryBuffId spellConfig.Id并以类型化资产命名Spell{Id}/Buff{Id}当前仓库实现为s{Id}/b{Id}并兼容旧前缀消除同目录同 Id 冲突。它的落地涉及数据模型、ScriptableObject 校验、编辑器索引与图构建、施法运行时、配置导出、资产迁移与校验规则七个环节任何一环遗漏都会破坏 Unity 引用或导致伤害结算读错配置。仓库中的cn.etetet.spell包已经完成了其中大部分改造OnValidate 前缀解析、SpellHelper.Cast同 Id 建 Buff、生成配置无 BuffId而BuffSystem.GetSpellConfigId()的旧偏移逻辑仍作为明确的风险残留存在是继续推进该设计时首要的清理目标。【免费下载链接】ETUnity3D Client And C# Server Framework项目地址: https://gitcode.com/GitHub_Trending/et/ET创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考