关于 unity-data-driven
unity-data-driven 是面向 Unity 游戏开发的决策指南,帮助开发者选择游戏配置的数据存储、组织和版本迁移方式。内容涵盖 ScriptableObject、JSON/CSV、设计师编辑流程及数据兼容注意事项。
主要功能
- 01选择配置数据存储方式
- 02比较继承与组合结构
- 03规划 JSON 数据加载管线
- 04设计师数据编辑流程
- 05处理数据版本与迁移
适用场景
- 01配置敌人属性和关卡数据
- 02从表格导入游戏配置
- 03选择批量编辑工具
- 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 架构决策