在 Unity 游戏开发中,数据与代码分离是提升开发效率、降低维护成本的核心原则,特别是团队开发时,通常由策划来进行数据的导入与设计。本文将以怪兽数据配置为例,分享三种不同类型的Unity 数据配置方案,适配不同规模项目的需求。
核心设计原则 可视化编辑 ,低耦合 ,易扩展
ScriptableObject ScriptableObject是 Unity 官方推荐的轻量级数据配置方案,以资源文件形式存储数据,原生支持 Inspector 可视化编辑,无需编写复杂的解析逻辑。推荐个人开发者使用。 使用ScriptableObject就像是定义一个可以直接在Unity内创建数据对象的数据结构。
实现步骤 创建数据配置类 核心用法为在创建类时继承ScriptableObject。 通常还会使用[CreateAssetMenu(fileName = "文件名", menuName = "父文件夹/子文件夹")]来让Unity能够在菜单直接创建。 代码演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 using UnityEngine; // 允许在Unity中右键创建该类型的配置文件 [CreateAssetMenu(fileName = "NewMonsterData", menuName = "GameData/MonsterData")] public class MonsterDataSO : ScriptableObject { [Header("基础信息")] public string monsterName; // 怪物名称 public int id; // 唯一标识ID [Header("战斗属性")] [Range(10, 1000)] // 限制数值范围,防止不合理配置 public int maxHp; // 最大血量 public int attackDamage; // 攻击伤害 public float attackSpeed; // 攻击间隔(秒) public int defense; // 防御值 [Header("其他属性")] public float moveSpeed; // 移动速度 public int expReward; // 击杀经验奖励 public Sprite icon; // 怪物图标(可选) }
创建具体的配置文件 在 Project 面板右键 → GameData → MonsterData,生成配置文件,在 Inspector 中直接填写数值。
在业务代码中使用 在需要配置数据的对象中编写好对应的逻辑脚本,挂在,通过读取数据对象来获得数据 代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 using UnityEngine; public class Monster : MonoBehaviour { // 在Inspector中拖入对应的MonsterDataSO文件 public MonsterDataSO monsterData; // 运行时动态数值(如当前血量) private int currentHp; void Start() { // 从配置文件初始化数据 currentHp = monsterData.maxHp; Debug.Log($"怪物{monsterData.monsterName}初始化,血量:{currentHp},伤害:{monsterData.attackDamage}"); } // 受击逻辑示例 public void TakeDamage(int damage) { int realDamage = Mathf.Max(0, damage - monsterData.defense); currentHp -= realDamage; if (currentHp <= 0) Die(); } // 死亡逻辑示例 private void Die() { Debug.Log($"怪物{monsterData.monsterName}死亡,奖励经验:{monsterData.expReward}"); Destroy(gameObject); } }
JSON 配置文件 JSON全称为(JavaScript Object Notation),是一种文件的类型,在开发中经常使用到,没有基础的直接理解为一套按照固定规则编写的txt文件,通过拆解这个规则能够获得文件所描述的对象。
JSON学习链接
特点 :相较于Scriptable对象,JSON配置的好处就在于数据脱离了引擎存在,这个数据拿到哪里都可以别识别为对象,并且文件便于传输,适用于策划 / 开发分工明确的团队。
实现 JSON文件创建 在Resources文件夹下创建 MonsterDatas.json,按结构编写配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 { "monsters": [ { "id": 101, "name": "哥布林", "maxHp": 200, "attackDamage": 30, "attackSpeed": 1.5, "defense": 5, "moveSpeed": 2.0, "expReward": 50 }, { "id": 102, "name": "兽人", "maxHp": 500, "attackDamage": 80, "attackSpeed": 2.0, "defense": 20, "moveSpeed": 1.5, "expReward": 100 } ] }
定义JSON数据模型 定义JSON数据模型的作用是在Unity中创建一个类,用来存储JSON文件中的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 using System.Collections.Generic; using UnityEngine; // 单条怪物数据模型 [System.Serializable] public class MonsterData { public int id; public string name; public int maxHp; public int attackDamage; public float attackSpeed; public int defense; public float moveSpeed; public int expReward; } // 所有怪物数据的容器 [System.Serializable] public class MonsterDataList { public List<MonsterData> monsters; }
实现数据管理器 数据管理器用于各个管理数据的加载和解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 using System.Collections.Generic; using UnityEngine; public class GameDataManager : MonoBehaviour { // 全局单例实例 public static GameDataManager Instance; // 字典存储数据,按ID快速查找 private Dictionary<int, MonsterData> monsterDataDict; void Awake() { // 单例初始化(确保全局唯一) if (Instance == null) { Instance = this; DontDestroyOnLoad(gameObject); // 跨场景不销毁 LoadMonsterDatas(); } else { Destroy(gameObject); } } // 加载并解析JSON配置 private void LoadMonsterDatas() { monsterDataDict = new Dictionary<int, MonsterData>(); // 从Resources加载JSON文件 TextAsset jsonFile = Resources.Load<TextAsset>("MonsterDatas"); if (jsonFile == null) { Debug.LogError("找不到MonsterDatas.json配置文件!"); return; } // 解析JSON到对象 MonsterDataList dataList = JsonUtility.FromJson<MonsterDataList>(jsonFile.text); // 存入字典,便于快速查询 foreach (MonsterData data in dataList.monsters) { monsterDataDict.Add(data.id, data); } Debug.Log($"成功加载{monsterDataDict.Count}种怪物配置"); } // 对外提供数据查询接口 public MonsterData GetMonsterData(int monsterId) { if (monsterDataDict.ContainsKey(monsterId)) { return monsterDataDict[monsterId]; } else { Debug.LogError($"未找到ID为{monsterId}的怪物配置!"); return null; } } }
在业务代码中访问数据管理器获取数据配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 using UnityEngine; public class Monster : MonoBehaviour { public int monsterId; // 在Inspector填写怪物ID(如101) private MonsterData monsterData; private int currentHp; void Start() { // 从全局管理器获取配置 monsterData = GameDataManager.Instance.GetMonsterData(monsterId); if (monsterData == null) return; // 初始化数值 currentHp = monsterData.maxHp; Debug.Log($"怪物{monsterData.name}初始化,血量:{currentHp},伤害:{monsterData.attackDamage}"); } // 受击、死亡逻辑与方案一一致 public void TakeDamage(int damage) { int realDamage = Mathf.Max(0, damage - monsterData.defense); currentHp -= realDamage; if (currentHp <= 0) Die(); } private void Die() { Debug.Log($"怪物{monsterData.name}死亡,奖励经验:{monsterData.expReward}"); Destroy(gameObject); } }
Excel配置 其实核心与JSON配置的思路差不多,只是Excel有更好的软件进行数据管理和操作,都是通过外置文件,文件转换为数据,实例获取数据。核心流程 :通过 Excel 管理数据,导出为 CSV/JSON 后在 Unity 中解析
1 2 3 4 5 graph LR A[Excel编写数据] --> B[导出为CSV/JSON] B --> C[Unity解析文件] C --> D[数据管理器封装] D --> E[业务代码调用]
实现(这里以使用CsvHelper库实现) 具体如何导库可以查看导入外部库 文章
新建表格填写数据 新建 MonsterDatas.xlsx,列名对应属性(ID、name、maxHp、attackDamage 等),填写所有怪物数据:
ID
name
maxHp
attackDamage
attackSpeed
defense
moveSpeed
expReward
101
哥布林
200
30
1.5
5
2.0
50
102
兽人
500
80
2.0
20
1.5
—
导出为csv文件 将 Excel 另存为 MonsterDatas.csv(必须选择 UTF-8 编码,避免中文乱码)。
定义数据模型(与Json方法一致) 创建MonsterCsvData.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 using System.ComponentModel; using CsvHelper.Configuration.Attributes; /// 怪物CSV数据模型(字段名需与CSV列名一致,大小写不敏感) public class MonsterCsvData { [Name("ID")] public int Id { get; set; } [Name("Name")] public string MonsterName { get; set; } [Name("MaxHp")] public int MaxHp { get; set; } [Name("AttackDamage")] public int AttackDamage { get; set; } [Name("AttackSpeed")] public float AttackSpeed { get; set; } [Name("Defense")] public int Defense { get; set; } [Name("MoveSpeed")] public float MoveSpeed { get; set; } [Name("ExpReward")] public int ExpReward { get; set; } }
实现CSV解析工具类 实现该类一般需要一个外部库,作用是能够让C#读懂CSV文件, 该类一般要实现以下功能
文件读取模块 : 负责打开和关闭CSV文件,读取文件流。
数据解析模块 : 将读取到的文本数据解析成结构化对象或数据集合。
错误处理模块 : 捕获并处理在读取和解析过程中可能发生的异常。
接口暴露模块 :定义外部可见的接口,提供给用户访问类功能的方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 using System; using System.Collections.Generic; using System.IO; using System.Text; using CsvHelper; using CsvHelper.Configuration; using UnityEngine; /// <summary> /// CSV通用解析工具类 /// </summary> public static class CsvParser { /// <summary> /// 从Resources加载CSV并解析为指定类型的列表 /// </summary> /// <typeparam name="T">数据模型类型</typeparam> /// <param name="csvPath">Resources下的CSV路径(不含.csv后缀)</param> /// <returns>解析后的数据集</returns> public static List<T> ParseCsvFromResources<T>(string csvPath) where T : class, new() { try { // 1. 加载CSV文件 TextAsset csvAsset = Resources.Load<TextAsset>(csvPath); if (csvAsset == null) { Debug.LogError($"CSV文件加载失败:Resources/{csvPath}.csv 不存在!"); return null; } // 2. 处理编码(解决中文乱码,强制UTF-8) var config = new CsvConfiguration(System.Globalization.CultureInfo.InvariantCulture) { Encoding = Encoding.UTF8, HasHeaderRecord = true, // CSV第一行是列名 IgnoreBlankLines = true, // 忽略空行 TrimOptions = TrimOptions.Trim, // 自动去除字段前后空格 MissingFieldFound = null // 忽略缺失字段(避免解析报错) }; // 3. 解析CSV using (var reader = new StringReader(csvAsset.text)) using (var csv = new CsvReader(reader, config)) { var records = csv.GetRecords<T>().ToList(); Debug.Log($"CSV解析成功:{csvPath}.csv,共{records.Count}条数据"); return records; } } catch (Exception e) { Debug.LogError($"CSV解析失败:{e.Message}\n{e.StackTrace}"); return null; } } }
全局数据管理器 创建Manager并挂载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 using System.Collections.Generic; using UnityEngine; /// <summary> /// CSV数据全局管理器(单例模式) /// </summary> public class CsvDataManager : MonoBehaviour { // 全局单例实例 public static CsvDataManager Instance { get; private set; } // 缓存怪物数据(Key=怪物ID,Value=怪物数据) private Dictionary<int, MonsterCsvData> _monsterDataDict; #region 单例初始化 private void Awake() { // 单例逻辑(确保全局唯一) if (Instance == null) { Instance = this; DontDestroyOnLoad(gameObject); // 跨场景不销毁 InitAllCsvData(); // 初始化所有CSV数据 } else { Destroy(gameObject); } } #endregion #region 数据初始化 /// <summary> /// 初始化所有CSV配置数据 /// </summary> private void InitAllCsvData() { // 1. 解析怪物CSV数据 var monsterDataList = CsvParser.ParseCsvFromResources<MonsterCsvData>("Configs/MonsterDatas"); if (monsterDataList == null || monsterDataList.Count == 0) { Debug.LogError("怪物CSV数据解析为空!"); return; } // 2. 转换为字典(按ID索引,提升查询效率) _monsterDataDict = new Dictionary<int, MonsterCsvData>(); foreach (var data in monsterDataList) { if (_monsterDataDict.ContainsKey(data.Id)) { Debug.LogWarning($"怪物ID重复:{data.Id},已跳过重复数据"); continue; } _monsterDataDict.Add(data.Id, data); } } #endregion #region 对外接口 /// <summary> /// 根据ID获取怪物数据 /// </summary> /// <param name="monsterId">怪物ID</param> /// <returns>怪物数据(无则返回null)</returns> public MonsterCsvData GetMonsterDataById(int monsterId) { if (_monsterDataDict.TryGetValue(monsterId, out var data)) { return data; } else { Debug.LogError($"未找到ID为{monsterId}的怪物数据!"); return null; } } /// <summary> /// 获取所有怪物数据 /// </summary> /// <returns>所有怪物数据列表</returns> public List<MonsterCsvData> GetAllMonsterData() { return new List<MonsterCsvData>(_monsterDataDict.Values); } #endregion }
业务代码使用 挂载到怪物预制体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 using UnityEngine; public class Monster : MonoBehaviour { [Header("怪物配置")] public int monsterId; // 在Inspector填写怪物ID(如101) private MonsterCsvData _monsterData; private int _currentHp; // 运行时当前血量 private void Start() { // 从数据管理器获取配置 _monsterData = CsvDataManager.Instance.GetMonsterDataById(monsterId); if (_monsterData == null) { enabled = false; // 数据不存在时禁用脚本 return; } // 初始化怪物属性 _currentHp = _monsterData.MaxHp; Debug.Log($"怪物[{_monsterData.MonsterName}]初始化完成:\n" + $"血量:{_currentHp}/{_monsterData.MaxHp}\n" + $"攻击伤害:{_monsterData.AttackDamage}\n" + $"移动速度:{_monsterData.MoveSpeed}"); } /// <summary> /// 受击逻辑 /// </summary> /// <param name="damage">受到的伤害</param> public void TakeDamage(int damage) { if (_monsterData == null) return; // 计算实际伤害(扣除防御) int realDamage = Mathf.Max(0, damage - _monsterData.Defense); _currentHp -= realDamage; Debug.Log($"[{_monsterData.MonsterName}]受到{realDamage}点伤害,剩余血量:{_currentHp}"); if (_currentHp <= 0) { Die(); } } /// <summary> /// 死亡逻辑 /// </summary> private void Die() { Debug.Log($"[{_monsterData.MonsterName}]死亡,奖励经验:{_monsterData.ExpReward}"); Destroy(gameObject); } }