Files
Continentis/docs/ModDevExperience_改进任务清单.md
SoulliesOfficial d09b58fd80 架构大更
2026-03-20 11:56:50 -04:00

21 KiB
Raw Permalink Blame History

Mod 开发体验改进 · 任务清单

文档类型:开发任务交接清单
生成日期2026-03-13
负责角色:首席游戏设计师 → 技术员
适用范围:仅针对开发者(第一方)制作 Mod 内容的体验提升,暂不涉及外部 Modder 工具链
阅读方式:标注 [设计] 的条目由设计师驱动决策,标注 [技术] 的条目由技术员实现,标注 [规范] 的是制度约定无需编程


一、编辑器界面重构IMGUI → Odin Inspector

背景:当前 CardDataEditor.csCharacterDataEditor.csEquipmentDataEditor.cs 均使用手动 IMGUI 代码。代码量大、维护难、功能弱。Odin Inspector 提供声明式 Attribute 方式定义界面,且无需使用 SerializedScriptableObject(不影响 UMod 序列化)。


1-A. 删除旧 IMGUI Editor 类,改用 Odin Attribute 装饰 Data 类 [技术]

涉及文件

  • Assets/Scripts/MainGame/Card/Editor/CardDataEditor.cs删除
  • Assets/Scripts/MainGame/Character/Editor/CharacterDataEditor.cs删除
  • Assets/Scripts/MainGame/Equipment/Editor/EquipmentDataEditor.cs删除
  • Assets/Scripts/MainGame/Card/CardData/CardData.cs改造
  • Assets/Scripts/MainGame/Character/CharacterData/CharacterData.cs改造
  • Assets/Scripts/MainGame/Equipment/EquipmentData.cs改造

具体要求

用以下 Odin Attribute 替代旧 IMGUI 的对应功能:

旧 IMGUI 功能 Odin 替代 Attribute
EditorGUILayout.LabelField("标题", boldLabel) [TitleGroup("标题")][FoldoutGroup("标题")]
EditorGUI.BeginDisabledGroup(true) [ReadOnly]
Show/Hide 条件逻辑(如 haveCustomClass [ShowIf("haveCustomClass")] / [HideIf]
手动绘制列表 [ListDrawerSettings]
字典SerializableDictionary [DictionaryDrawerSettings]
按钮([Button] 已在 NaughtyAttr 中使用) 统一改为 Odin [Button],功能相同但支持更多参数
EditorGUILayout.PropertyField(sprite) 图片预览 [PreviewField(80)]
多行文本 [MultiLineProperty(3)]

注意事项

  • 继续保留 CardData.PasteDefaultAttributes() 等编辑器专属方法,用 [Button("粘贴默认属性")] 暴露
  • 不得CardData / CharacterData 的基类改为 SerializedScriptableObject,必须保留 ScriptableObject 基类
  • 检查 NaughtyAttributes 与 Odin 的 Attribute 重叠,统一迁移到 Odin,逐步移除 NaughtyAttributes 依赖

1-B. CardData 编辑器特殊功能保留与增强 [技术]

当前 CardDataEditor 中有以下特殊功能须在 Odin 版本中等效实现:

  1. 可搜索类型选择器DrawSearchableTypeSelector):根据 CardLogicBase 的所有子类自动填充 modNamecategoryNameclassNamedisplayNamefunctionText

    • Odin 方案:使用 [ValueDropdown("GetCardLogicTypes")] + 在同类中写 private IEnumerable<ValueDropdownItem> GetCardLogicTypes() 方法,返回所有已注册 CardLogicBase 子类的列表
  2. 带引用选择器的列表DrawListWithEditRefSelector):关键词列表支持从 EditReference 资产中选取

    • Odin 方案[ValueDropdown("GetAvailableKeywords")] 装饰 keywords 字段
  3. 对象引用列表DrawCharacterListGUI<T>):通过资产名字符串引用其他 Data 资产

    • Odin 方案:保留字符串存储形式,但使用 [ValueDropdown]AssetDatabase 中查找并显示

1-C. 为 KeywordData 的编辑器添加更清晰的分组 [技术]

目前 KeywordData 依赖 NaughtyAttributes 的 [Button],界面简陋。 改造后:所有 InterpretedKeyword 在字典中的每个 entry 能展开显示 namedescription 的本地化预览文本(通过 [OnInspectorGUI] 实现一个 Preview() 方法)。


二、Mod 内容结构与命名规范

背景:目前 Basic Mod 的目录结构相对合理,但没有正式成文的规范文档,新增内容的开发者需要靠"看现有文件猜"。以下是需要成文并固化的规范。


2-A. 制定并存档《Mod 内容规范》文档 [规范] [设计]

在项目中创建文档 docs/ModContentStandard_内容规范.md,包含以下内容:

目录结构规范(以 "Basic" Mod 为蓝本)

Assets/Mods/{ModName}/
├── {ModName}_Manifest.asset            ← ModManifest必须存在
├── {ModName}_CombatOrganizer.asset     ← 战斗组织者配置(如有)
│
├── Cards/
│   ├── Data/                           ← 存放所有 CardData.asset
│   ├── Scripts/
│   │   ├── {ClassCategory}/            ← 按职业/分类分子文件夹,例如 Assassin/
│   │   ├── Enemies/                    ← 敌方专属卡牌
│   │   └── General/                    ← 通用卡牌
│   ├── LogicComponents/                ← 存放该 Mod 特有的 CardLogicComponent 子类
│   ├── DefaultCollections/             ← CardAttributesDefaultCollection.asset
│   └── Sprites/                        ← 卡牌图片
│
├── Characters/
│   ├── Data/                           ← 存放所有 CharacterData.asset
│   ├── Scripts/                        ← CharacterLogicBase 子类
│   ├── CombatBuffs/
│   │   ├── General/                    ← 通用 Buff跨职业可用
│   │   └── {ClassCategory}/            ← 职业专属 Buff
│   ├── CharacterViews/                 ← 角色视觉 Prefab
│   ├── DefaultCollections/             ← CharacterAttributesDefaultCollection.asset
│   └── Sprites&Animations/             ← 角色图片与动画资产
│
├── Equipments/
│   ├── Data/                           ← EquipmentData.asset
│   └── Scripts/                        ← EquipmentBase 子类(如有自定义逻辑)
│
├── Keywords/
│   └── KeywordData_{ModName}_*.asset   ← 关键词数据
│
├── Rules/
│   └── {ModName}_AttributeRulesCollection.cs ← AttributeRulesCollectionBase 子类
│
├── Localization/
│   ├── Localization_{ModName}_CN.asset
│   └── Localization_{ModName}_EN.asset
│
├── Audios/                             ← 音频资产
└── Prefabs/                            ← 其他 Prefab特效、HUD 等)

资产命名规范

格式:{AssetType}_{ModName}_{AssetName}

示例:
  CardData_Basic_Backstab.asset
  CharacterData_Basic_Assassin.asset
  EquipmentData_Basic_SteelBracer.asset
  KeywordData_Basic_Buff.asset
  HUDData_Basic_Default.asset

规则细则

  • AssetName 使用 PascalCase(大驼峰),禁止空格和下划线
  • ModName 必须与 Manifest 的 inEditorModFolder 字段完全一致
  • 所有脚本 .cs 文件名与类名保持一致Unity 默认要求)

C# 命名空间规范

格式Continentis.Mods.{ModName}.{Category}[.{SubCategory}]

示例:
  Continentis.Mods.Basic.Cards.Assassin    ← Assassin 职业卡牌逻辑
  Continentis.Mods.Basic.Cards.Enemies     ← 敌方卡牌逻辑
  Continentis.Mods.Basic.Cards.General     ← 通用卡牌逻辑
  Continentis.Mods.Basic.Buffs             ← 所有 Buff通用
  Continentis.Mods.Basic.Characters        ← CharacterLogicBase 子类
  Continentis.Mods.Basic.Rules             ← AttributeRulesCollection

2-B. 为每种 Data 类型创建「快速创建向导」菜单项 [技术](低优先级)

在 Unity 菜单栏添加 Continentis / Mod Tools / 新建卡牌...,弹出向导窗口:

  1. 输入Mod 名称、分类名、卡牌类名
  2. 自动:在正确的目录下创建 CardData_{Mod}_{Name}.asset,并预填 modNamecategoryNameclassName 字段
  3. 可选:同时创建配套的 .cs 脚本文件(从模板复制)

三、ModManifest 功能增强

背景ModManifest 当前已有「分类收集」按钮,功能基本具备,但仍有几处可以优化。


3-A. 增加「一键收集全部」按钮 [技术]

涉及文件Assets/Scripts/Mod/Manifests/ModManifest.cs

#if UNITY_EDITOR 块中添加:

[Button("★ 一键收集全部", ButtonSizes.Large)]
private void CollectAll()
{
    CollectAllKeywordData();
    CollectAllCardData();
    CollectAllCharacterData();
    CollectAllEquipmentData();
    CollectAllHUDData();
    CollectAllLocalizations();
    Debug.Log("[ModManifest] 全部资产已收集完毕。");
}

同时将此按钮放置在 Odin 的 [BoxGroup("Tools")] 中,与其他按钮分组显示。


3-B. 清理 cardDataIDList 中的空条目 [技术]BUG 修复)

问题Basic_Manifest.asset 第 45 行有一个空条目 "CardData"(未填写完整的资产名),会在运行时 host.Assets.Load<CardData>("CardData") 失败。

修复:直接在 Basic_Manifest.asset 中删除该空行;同时在 ModManifest.SaveToDatabase() 中增加防御性检查:

foreach (var assetName in idList)
{
    if (string.IsNullOrWhiteSpace(assetName)) continue; // 新增:跳过空条目
    T data = host.Assets.Load<T>(assetName);
    // ...
}

3-C. 增加 Mod 版本与元数据字段 [技术] [设计]

涉及文件Assets/Scripts/Mod/Manifests/ModManifest.cs

ModManifest 类中新增以下字段:

[FoldoutGroup("Mod Info")]
[LabelText("Mod 名称")] public string modDisplayName;
[LabelText("Mod 版本")] public string modVersion = "1.0.0";
[LabelText("作者")] public string modAuthor;
[LabelText("描述"), MultiLineProperty(3)] public string modDescription;
[LabelText("最低游戏版本")] public string minimumGameVersion = "0.1.0";

这些字段在 UMod 的 ModManifest 元数据中是标准字段,添加后可在 Mod 选择界面展示。


3-D. 增加「收集后预览」校验功能 [技术](低优先级)

收集完成后,在 Odin Inspector 中用 [InfoBox][OnInspectorGUI] 显示一个摘要表格:

✓ 卡牌数据81 项
✓ 角色数据6 项
✓ 装备数据1 项
✓ 关键词数据3 项
✓ 本地化文件13 项
⚠ 警告:发现 1 个不符合命名规范的资产CardData

四、属性名称安全化

背景:当前所有属性访问(GetAttribute("Block")SetAttribute("Stamina", 0) 等)均使用裸字符串,存在 typo 风险且无 IDE 提示。


4-A. 创建 GameAttributes 静态常量类 [技术]

新建文件Assets/Scripts/MainGame/Base/GameAttributes.cs

namespace Continentis.MainGame
{
    /// <summary>
    /// 游戏通用属性名称常量。
    /// 请始终使用此类中的常量而非裸字符串访问属性,以避免 typo 错误。
    /// </summary>
    public static class GameAttributes
    {
        // ── 生命值 ──────────────────────────────────
        public const string Health          = "Health";
        public const string MaximumHealth   = "MaximumHealth";

        // ── 资源 ────────────────────────────────────
        public const string Stamina              = "Stamina";
        public const string MaximumStamina       = "MaximumStamina";
        public const string StaminaRecoverPerAction = "StaminaRecoverPerAction";
        public const string Mana                 = "Mana";
        public const string MaximumMana          = "MaximumMana";
        public const string ManaRecoverPerAction = "ManaRecoverPerAction";

        // ── 防御 ────────────────────────────────────
        public const string Block   = "Block";
        public const string Shield  = "Shield";
        public const string Dodge   = "Dodge";

        public const string BlockGainOffset      = "BlockGainOffset";
        public const string BlockGainMultiplier  = "BlockGainMultiplier";
        public const string DodgeGainOffset      = "DodgeGainOffset";
        public const string DodgeGainMultiplier  = "DodgeGainMultiplier";
        public const string ShieldGainOffset     = "ShieldGainOffset";
        public const string ShieldGainMultiplier = "ShieldGainMultiplier";

        public const string KeepBlockOnActionStart = "KeepBlockOnActionStart";
        public const string KeepDodgeOnActionStart = "KeepDodgeOnActionStart";

        // ── 速度与行动 ──────────────────────────────
        public const string Speed                  = "Speed";
        public const string DrawCardAmountPerAction = "DrawCardAmountPerAction";
        public const string DeckCapacity           = "DeckCapacity";

        // ── 伤害调整(通用)──────────────────────────
        public const string PhysicsDamageDealtOffset  = "PhysicsDamageDealtOffset";
        public const string MagicDamageDealtOffset    = "MagicDamageDealtOffset";
        public const string FinalDamageDealtMultiplier = "FinalDamageDealtMultiplier";
        public const string FinalDamageGainMultiplier  = "FinalDamageGainMultiplier";
        public const string MagicDamageDealtMultiplier = "MagicDamageDealtMultiplier";
        public const string MagicDamageGainMultiplier  = "MagicDamageGainMultiplier";

        // ── 核心属性(角色创建时)──────────────────────
        public const string Strength     = "Strength";
        public const string Agility      = "Agility";
        public const string Intelligence = "Intelligence";
        public const string Physique     = "Physique";
        public const string Perception   = "Perception";
        public const string Charisma     = "Charisma";
        public const string Level        = "Level";

        // ── 感知与闪避检测 ──────────────────────────
        public const string Awareness                        = "Awareness";
        public const string DodgeCheckStartDamageMultiplier = "DodgeCheckStartDamageMultiplier";

        // ── 元素伤害(格式:{Element}DamageDealtMultiplier
        // 请按元素名在各 Mod 中自行定义扩展常量,或添加在此类的子类 / partial class 中
    }
}

迁移策略:不要在全项目做批量 Find & Replace风险高
建议:新代码强制要求使用常量,旧代码遇到则顺手替换,通过 Code Review 推进,不强制一次性迁移。


五、CardData 「默认属性模板」机制完善

背景CardAttributesDefaultCollectionCharacterAttributesDefaultCollection 已存在,但在 CardData 中缺少对应的 PasteDefaultAttributes() 方法(角色端有,卡牌端没有)。


5-A. 为 CardData 补充 PasteDefaultAttributes() 功能 [技术]

涉及文件Assets/Scripts/MainGame/Card/CardData/CardData.cs

参照 CharacterData.PasteDefaultAttributes() 的逻辑,在 #if UNITY_EDITOR 块中为 CardData 添加同名方法:

  • 扫描 Assets/Mods/{ModName}/Cards/DefaultCollections/ 目录下的 CardAttributesDefaultCollection 资产
  • 将模板中的 variableAttributesoriginalAttributesendowingCurrentAttributes 合并粘贴到当前 CardData
  • 注意:CardAttributesDefaultCollection 中的字段名是 endowingCurrentAttributes,但 CardData 中对应字段已改名为 runtimeCurrentAttributes(注意同步做字段名对齐,或检查是否存在 [FormerlySerializedAs] 标记)

补充:为 CardData Odin 编辑器 Inspector 中添加 [Button("粘贴默认属性")] 按钮暴露此功能。


六、BUG 修复与代码清理

以下是在代码审查中发现的已知问题,建议在本次迭代中一并修复。


6-A. 修复 ModifyMana() 中的 ClampAttribute 笔误 [技术]BUG

文件Assets/Scripts/MainGame/Character/CharacterMainFunctions.cs 第 70 行

// 当前(错误):
ClampAttribute("Stamina", 0, GetAttribute("MaximumStamina"));

// 修改为:
ClampAttribute("Mana", 0, GetAttribute("MaximumMana"));

6-B. 修复 NextAction() 中阵营判断的逻辑死区 [技术]BUG

文件Assets/Scripts/MainGame/Combat/CombatMainManager.cs 第 180-183 行

// 当前(第二个条件永远不会触发,与第一个条件完全相同):
else if (currentCharacter.fraction == Fraction.Enemy)
{
    text = "Ally Action";  // 永远不会显示
}

// 修改为:
else if (currentCharacter.fraction == Fraction.Ally)
{
    CombatUIManager.Instance.combatMainPage.endActionButton
        .GetComponentInChildren<TMP_Text>().text = "Ally Action";
}

6-C. 移除临时的 DrawCardAmount 偏移量 [技术](技术债)

文件Assets/Mods/Basic/Rules/Basic_AttributeRulesCollection.cs 第 42 行

// 当前(临时代码,需移除或改为正式配置):
general["DrawCardAmountPerAction"] += 10;

移除此行之前,需确认是否已通过其他途径(如 CharacterAttributesDefaultCollectionCharacterData.generalAttributes)为角色配置了合理的初始抽牌量,确保移除后游戏逻辑正常。


6-D. 清理 Basic_Manifest.asset 中的空 CardData 条目 [技术]BUG

见第三节 3-B,此处不再赘述。


七、额外优化建议(低优先级,可延后)


7-A. 建立「卡牌预览」工具窗口 [技术]

在 Unity 编辑器中实现一个 EditorWindowContinentis / Mod Tools / 卡牌预览),选中 CardData 后,在窗口中还原游戏内的卡牌 UI 外观(图片 + 类型色 + 描述文本)。这是提升开发效率的重要工具,能让设计师在不启动游戏的情况下确认卡牌视觉效果。


7-B. 本地化 Key 完整性检查工具 [技术]

写一个编辑器脚本,扫描所有 CardDatadisplayNamefunctionText 字段,检查它们是否在所有 Localization_*.asset 文件中都有对应条目,输出缺失的 Key 列表。避免出现"卡牌显示 Key 名称而非实际文字"的问题。


7-C. 为 CardLogicComponent_Attack 等内置组件添加 Tooltip 注释 [技术]

SetDamage_Default()SetBlock_Default() 等方法目前缺少 XML 注释Modder 无法通过 IDE 悬停了解参数含义。补充 <summary> 注释,提升 API 可发现性。


7-D. 升级系统:为 CardUpgradeNode 添加 Odin 预览 [技术]

CardUpgradeNode 中的 customUpgradeAttributesList<Dictionary<string, float>>在默认序列化下几乎无法编辑Unity 不序列化嵌套字典)。建议:

  • 改为 List<SerializableDictionary<string, float>>,并用 Odin [ListDrawerSettings(ShowIndexLabels = true)] 装饰
  • 这样每一级升级的属性增量都可以在 Inspector 中直观查看和编辑

八、任务优先级总表

优先级 编号 任务名称 类型 工作量估算
🔴 P0 6-A 修复 ModifyMana 笔误 BUG 5 分钟
🔴 P0 6-B 修复 NextAction 阵营判断死区 BUG 5 分钟
🔴 P0 3-B 清理 Manifest 空条目 + 防御检查 BUG 15 分钟
🟠 P1 1-A 删除旧 IMGUI Editor改用 Odin Attribute 编辑器重构 3-5 天
🟠 P1 1-B 保留 CardData 可搜索类型选择器Odin 版) 编辑器重构 1-2 天
🟠 P1 3-A ModManifest 一键收集全部按钮 功能增强 2 小时
🟠 P1 3-C ModManifest 增加版本/元数据字段 功能增强 1 小时
🟠 P1 4-A 创建 GameAttributes 常量类 规范化 2 小时
🟡 P2 2-A 撰写《Mod 内容规范》文档 规范文档 半天
🟡 P2 5-A CardData 补充 PasteDefaultAttributes 功能补全 2 小时
🟡 P2 6-C 移除临时 DrawCard 偏移量 技术债 30 分钟(需确认)
🟡 P2 7-D CardUpgradeNode 字典序列化修复 功能修复 3 小时
🟢 P3 1-C KeywordData 编辑器改善 编辑器优化 1 小时
🟢 P3 3-D Manifest 收集后预览校验 功能增强 2 小时
🟢 P3 7-A 卡牌预览工具窗口 工具开发 3-5 天
🟢 P3 7-B 本地化 Key 完整性检查工具 工具开发 1-2 天
🟢 P3 7-C CardLogicComponent 注释补充 文档 1 小时
P4 2-B 新建内容向导菜单项 工具开发 2-3 天

本文档由首席游戏设计师game-designer-generic生成技术员在实现时如遇到设计意图不明确的地方请向设计师确认后再动工。