在 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文件,
该类一般要实现以下功能

  1. 文件读取模块 : 负责打开和关闭CSV文件,读取文件流。
  2. 数据解析模块 : 将读取到的文本数据解析成结构化对象或数据集合。
  3. 错误处理模块 : 捕获并处理在读取和解析过程中可能发生的异常。
  4. 接口暴露模块 :定义外部可见的接口,提供给用户访问类功能的方式。
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);
}
}