mcpField
U
unity-data-driven

unity-data-driven

github.com/nice-wolf-studio开发4073 次安装6 周前更新
代码仓库

关于 unity-data-driven

unity-data-driven 是面向 Unity 游戏开发的决策指南,帮助开发者选择游戏配置的数据存储、组织和版本迁移方式。内容涵盖 ScriptableObject、JSON/CSV、设计师编辑流程及数据兼容注意事项。

主要功能

  1. 01选择配置数据存储方式
  2. 02比较继承与组合结构
  3. 03规划 JSON 数据加载管线
  4. 04设计师数据编辑流程
  5. 05处理数据版本与迁移

适用场景

  1. 01配置敌人属性和关卡数据
  2. 02从表格导入游戏配置
  3. 03选择批量编辑工具
  4. 04升级已有配置数据格式

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/nice-wolf-studio/unity-claude-skills --skill unity-data-driven

安装后在 Claude Code 中输入/unity-data-driven确认已加载。

附带文件references/data-pipeline-patterns.md

详细介绍

数据驱动设计——决策模式

前置技能: unity-scripting/references/scriptableobjects.md(SO 创建、事件通道、运行时集合、变量引用)、unity-editor-tools(自定义检查器、EditorWindow)

这些模式旨在解决最常见的数据管理问题:Claude 在 MonoBehaviour 中硬编码可调值或使用魔法数字,导致迭代和调优变得困难。所有游戏数据都应支持设计师在不修改代码的情况下编辑。


模式:配置数据存储方式选择

适用场景:选择游戏配置(敌人属性、关卡布局、战利品表)的存储位置

决策:

  • ScriptableObject 资源(默认)——设计师可在 Inspector 中编辑,类型安全,便于重构,并以 YAML 格式进行版本控制。最适合大多数游戏配置。
  • JSON/CSV 文件——需要由外部工具(电子表格、Web 工具)生成数据、支持 Mod,或进行批量编辑。运行时加载。
  • 嵌入式常量——真正固定的值(物理常量、数学、协议版本)。使用 static readonly 或 const。

脚手架(ScriptableObject 配置):

[CreateAssetMenu(fileName = "New Enemy Config", menuName = "Game/Enemy Config")]
public class EnemyConfig : ScriptableObject
{
    [Header("Stats")]
    [Min(1)] public int maxHealth = 100;
    [Range(0f, 20f)] public float moveSpeed = 5f;
    [Range(0f, 50f)] public float attackDamage = 10f;

    [Header("AI")]
    [Range(1f, 50f)] public float detectionRange = 15f;
    [Range(0.5f, 5f)] public float attackCooldown = 1.5f;

    [Header("Rewards")]
    [Min(0)] public int xpReward = 25;
    [Tooltip("Loot dropped on death. Leave empty for no loot.")]
    public LootTable lootTable;
}

// Usage in MonoBehaviour:
public class Enemy : MonoBehaviour
{
    [SerializeField] private EnemyConfig config; // Assign in Inspector

    void Awake()
    {
        // Read from config -- never hardcode values
        _health = new HealthSystem(config.maxHealth);
        _moveSpeed = config.moveSpeed;
    }
}

注意:在 Editor 中运行时修改 ScriptableObject 资源,会将这些更改持久化到磁盘(保存到 .asset 文件)。在构建版本中,应用程序退出时,运行时修改会丢失。切勿依赖运行时修改 SO 来保存数据——请使用独立的存档系统。有关持久化,请参阅 unity-save-system。


模式:ScriptableObject 继承与组合

适用场景:多种配置类型共享基础字段(所有敌人都有 HP/速度,子类型还包含飞行/远程/首领字段)

决策:

  • SO 继承——EnemyConfig : ScriptableObject、FlyingEnemyConfig : EnemyConfig。每个子类型添加自己的字段。设计师在每个资源中只会看到相关字段。适用于 2 到 3 层的继承层级。
  • 嵌套的 [Serializable] 结构体(组合)——EnemyConfig 包含 [SerializeField] MovementConfig movement; [SerializeField] AttackConfig attack;。支持灵活组合。面对多样化需求时,通常比继承更简洁。

脚手架(组合):

[System.Serializable]
public struct MovementConfig
{
    [Range(0f, 20f)] public float speed;
    public bool canFly;
    [Tooltip("Only used if canFly is true")]
    [Range(0f, 50f)] public float flyHeight;
}

[System.Serializable]
public struct AttackConfig
{
    public AttackType type;
    [Range(0f, 100f)] public float damage;
    [Range(0.1f, 10f)] public float cooldown;
    [Range(0f, 30f)] public float range;
}

[CreateAssetMenu(fileName = "New Enemy", menuName = "Game/Enemy Config")]
public class EnemyConfig : ScriptableObject
{
    [Header("Identity")]
    public string displayName;
    [TextArea(2, 4)] public string description;

    [Header("Movement")]
    public MovementConfig movement;

    [Header("Combat")]
    public AttackConfig attack;

    [Header("Stats")]
    [Min(1)] public int maxHealth = 100;
    [Min(0)] public int xpReward = 25;
}

脚手架(继承):

public abstract class EnemyConfig : ScriptableObject
{
    [Min(1)] public int maxHealth = 100;
    [Range(0f, 20f)] public float moveSpeed = 5f;
}

[CreateAssetMenu(fileName = "New Melee Enemy", menuName = "Game/Enemies/Melee")]
public class MeleeEnemyConfig : EnemyConfig
{
    [Range(0f, 50f)] public float meleeDamage = 15f;
    [Range(0.5f, 3f)] public float attackRadius = 1.5f;
}

[CreateAssetMenu(fileName = "New Ranged Enemy", menuName = "Game/Enemies/Ranged")]
public class RangedEnemyConfig : EnemyConfig
{
    [Range(0f, 50f)] public float projectileDamage = 10f;
    [Range(5f, 30f)] public float fireRange = 15f;
    public GameObject projectilePrefab;
}

注意:SO 继承会在 Inspector 中显示所有基类字段(无法隐藏)。如果基类字段使 Inspector 显得杂乱,可使用 [HideInInspector] 或自定义编辑器。使用 [Serializable] 结构体进行组合通常更简洁,因为每个结构体都可以在无关的配置类型之间复用。[CreateAssetMenu] 仅适用于具体的(非抽象)ScriptableObject 子类。


模式:JSON 数据流水线

适用场景:从外部来源(电子表格、Web API、Mod)加载游戏数据

决策:

  • 构建时流水线——使用 Editor 脚本将 JSON/CSV 转换为 ScriptableObject 资源。数据在构建时打包。设计师编辑电子表格、运行导入流程,数据便会转换为 SO 资源。
  • 运行时加载——在运行时从 StreamingAssets 或远程 URL 加载 JSON。支持 Mod、热重载和服务器驱动的配置。

脚手架(运行时 JSON 加载):

// Data class (plain C#, not ScriptableObject)
[System.Serializable]
public class WaveData
{
    public int waveNumber;
    public string[] enemyTypes;
    public int[] enemyCounts;
    public float spawnInterval;
}

[System.Serializable]
public class WaveDataList
{
    public WaveData[] waves; // JsonUtility requires a wrapper for arrays
}

// Loading
public class WaveLoader : MonoBehaviour
{
    async Awaitable<WaveData[]> LoadWaves()
    {
        string path = Path.Combine(Application.streamingAssetsPath, "waves.json");

        #if UNITY_ANDROID && !UNITY_EDITOR
        // Android: StreamingAssets is inside APK, must use UnityWebRequest
        using var request = UnityWebRequest.Get(path);
        await request.SendWebRequest();
        string json = request.downloadHandler.text;
        #else
        string json = await File.ReadAllTextAsync(path);
        #endif

        var wrapper = JsonUtility.FromJson<WaveDataList>(json);
        return wrapper.waves;
    }
}

注意:JsonUtility 无法序列化:字典、顶层数组(需要包装类)、多态类型、空值(会改用默认值)或属性(只支持字段)。如需处理这些情况,请使用 Newtonsoft JSON(com.unity.nuget.newtonsoft-json 包)。所有平台上的 Application.streamingAssetsPath 均为只读,在 Android 上需要使用 UnityWebRequest。有关 Editor 导入脚本,请参阅 references/data-pipeline-patterns.md。


模式:设计师交接工作流

适用场景:设计师需要创建和编辑游戏数据,而无需接触代码

决策:

  • SO + CreateAssetMenu -- 设计师在 Project 窗口中右键创建配置资源,并在 Inspector 中编辑。最适合小到中型数据集(< 100 项)。
  • SO + Custom EditorWindow -- 适合批量编辑、验证、搜索和筛选。对于大型数据集(100+ 项、战利品表、对话)而言,值得投入。
  • 外部电子表格 + 导入 -- Google Sheets/Excel -> CSV -> Editor 导入脚本。最适合偏好使用电子表格的设计师,或数据来自外部工具的情况。

脚手架(自说明式 Inspector):

[CreateAssetMenu(fileName = "New Weapon", menuName = "Game/Weapons/Weapon Config")]
public class WeaponConfig : ScriptableObject
{
    [Header("Identity")]
    [Tooltip("Display name shown in UI and inventory")]
    public string displayName;

    [TextArea(2, 5)]
    [Tooltip("Flavor text shown in the item tooltip")]
    public string description;

    public Sprite icon;

    [Header("Combat Stats")]
    [Tooltip("Base damage before modifiers. Actual damage = base * level multiplier")]
    [Range(1f, 200f)] public float baseDamage = 10f;

    [Tooltip("Seconds between attacks")]
    [Range(0.1f, 5f)] public float attackSpeed = 1f;

    [Tooltip("Maximum range in world units")]
    [Range(0.5f, 30f)] public float range = 2f;

    [Header("VFX/SFX")]
    [Tooltip("Played on hit. Leave null for no effect")]
    public GameObject hitEffect;

    [Tooltip("Played on attack")]
    public AudioClip attackSound;

    [Space(10)]
    [Header("Advanced")]
    [Tooltip("Damage falloff over distance. X = normalized distance (0-1), Y = damage multiplier")]
    public AnimationCurve damageFalloff = AnimationCurve.Linear(0, 1, 1, 0.5f);
}

注意事项:设计师看不懂代码注释——请为每个字段使用 [Tooltip("...")]。使用 [Range(min, max)] 防止输入无效数据。使用 [Header("Section")] 对相关字段分组。使用 [Space(10)] 进行视觉分隔。AnimationCurve 字段适用于可由设计师调整的衰减、缓动和响应曲线。为可选引用添加提示,例如“留空表示无效果”。


模式:数据版本控制与迁移

适用场景:已发布的数据格式在更新之间发生变化(添加、重命名或移除字段)

决策:

  • SO 弹性序列化(增量变更) -- 添加新字段时会为其赋予默认值。移除字段时会静默丢弃数据。适用于不破坏兼容性的变更。Unity 会自动处理。
  • 显式版本字段 + 迁移器 -- 适用于破坏性变更(字段重命名、数据结构重组)。在数据中存储 int version,并在加载时运行迁移。

脚手架(重命名时使用 FormerlySerializedAs):

using UnityEngine.Serialization;

public class EnemyConfig : ScriptableObject
{
    // Renamed from "hp" to "maxHealth" -- existing assets preserved
    [FormerlySerializedAs("hp")]
    [Min(1)] public int maxHealth = 100;

    // Renamed from "speed" to "moveSpeed"
    [FormerlySerializedAs("speed")]
    [Range(0f, 20f)] public float moveSpeed = 5f;

    // New field -- existing assets get the default value (25)
    [Min(0)] public int xpReward = 25;

    // Removed field: just delete it. Existing assets silently drop the old data.
}

脚手架(带迁移的版本化运行时数据):

[System.Serializable]
public class PlayerData
{
    public int version = 2; // Current schema version
    public string playerName;
    public int level;
    public float[] position; // v2: changed from Vector3 to float[] for JSON compat

    // Migration from v1 to v2
    public static PlayerData MigrateFromV1(string json)
    {
        // v1 had "pos_x", "pos_y", "pos_z" as separate fields
        var jObj = Newtonsoft.Json.Linq.JObject.Parse(json);
        if ((int)jObj["version"] == 1)
        {
            float x = (float)jObj["pos_x"];
            float y = (float)jObj["pos_y"];
            float z = (float)jObj["pos_z"];
            jObj.Remove("pos_x"); jObj.Remove("pos_y"); jObj.Remove("pos_z");
            jObj["position"] = new Newtonsoft.Json.Linq.JArray(x, y, z);
            jObj["version"] = 2;
        }
        return jObj.ToObject<PlayerData>();
    }
}

注意事项:[FormerlySerializedAs] 仅适用于 Unity 序列化(Inspector、.asset 文件、预制件),不适用于 JSON 或自定义序列化。迁移 JSON 时,必须先解析原始 JSON,再反序列化为新类型。SO 数据版本控制和存档数据版本控制是不同的问题——有关存档文件迁移,请参阅 unity-save-system。


Inspector 属性速查表

属性用途示例
[Header("Section")]分区标签将相关字段分组
[Tooltip("Help text")]悬停帮助文本说明不明显的字段
[Range(min, max)]数值字段滑块[Range(0, 100)]
[Min(value)]最小值限制[Min(0)],用于确保值非负
[TextArea(min, max)]多行文本字段[TextArea(2, 5)]
[Space(pixels)]垂直间距[Space(10)]
[HideInInspector]在 Inspector 中隐藏内部字段
[FormerlySerializedAs]重命名时保留数据确保迁移安全
[ColorUsage(alpha, hdr)]颜色选择器配置[ColorUsage(true, true)]
[GradientUsage(hdr)]渐变 HDR 支持VFX 颜色渐变
[Delayed]按 Enter 或失去焦点时应用防止每次按键都更新
[SerializeReference]多态序列化接口字段

相关技能

  • unity-scripting/references/scriptableobjects.md -- SO 创建、事件通道、运行时集合、变量引用
  • unity-editor-tools -- 用于批量编辑的 Custom EditorWindow、用于自定义显示的 PropertyDrawer
  • unity-save-system -- 运行时数据持久化(区别于配置数据)
  • unity-game-architecture -- ScriptableObject 架构决策

其他资源

评论

支持 Markdown