unity中关于游戏中数据的流动总结-1
一、普通脚本 int hp = 10 的完整生命周期
1. 编写代码阶段(Visual Studio)
csharp
public class Enemy : MonoBehaviour
{
public int hp = 10; // 代码中写死的初始值
}
此时 10 只是一个源代码中的字面量,还没有任何“固化”的形式。
2. 编译阶段(Unity 自动编译)
- Unity 将 C# 脚本编译成 程序集(Assembly-CSharp.dll)。
- 编译器会把
hp = 10这个初始化操作转换成 IL 指令(例如ldc.i4.s 10),并将这个10作为常量数据嵌入到 DLL 文件的元数据中。 - 此时,
10已经以二进制形式存在于磁盘上的 DLL 文件里,但它属于代码逻辑的一部分,而非独立的数据资源。
3. 编辑器中的实例化与序列化(Unity Editor)
情况 A:从未在 Inspector 中修改过 hp
- 当你把
Enemy脚本挂载到场景中的 GameObject 上,Unity 会在内存中创建该脚本的实例。 - 实例化过程:
- 分配内存,所有字段初始化为默认值(hp = 0)。调用构造函数(如果有)和字段初始化器(int hp = 10),将 hp 设为 10。Unity 的序列化系统会检查该对象是否有保存的序列化数据(比如来自场景文件 .unity)。因为没有修改过,所以没有额外数据,最终 hp 保持 10。
- 当你保存场景(
Ctrl+S),Unity 会将当前场景中所有对象的当前值序列化到.unity文件中。由于hp没改过,场景文件里其实不会显式存储**hp=10**,而是依赖代码初始化。
情况 B:在 Inspector 中修改了 hp(比如改成 20)
- 在 Inspector 中把
hp改为20,Unity 立即将这个新值存入内存中的对象实例。 - 保存场景时,Unity 会检测到
hp的值与代码默认值(10)不一致,于是将**20**序列化到场景文件(或预制体文件)中。 - 此时,
20就作为序列化数据持久化到了磁盘上的资源文件中。
4. 打包阶段(Build)
- Unity 将所有的程序集(DLL)、场景文件、预制体、其他资源打包成一个完整的游戏包(如 PC 版的
.exe及附带数据文件)。 - 注意:DLL 中包含的是代码指令(包括
hp=10这个常量),而场景/预制体文件中包含的是经过修改的序列化数据(如果有的话)。
5. 玩家运行游戏(Runtime)
- 游戏启动,操作系统将游戏包中的 DLL 和数据文件加载到内存。
- 当需要创建一个
Enemy实例时(例如加载场景或实例化预制体): - 先从 DLL 的元数据中获取类的定义。分配内存,字段先清零(hp = 0)。执行字段初始化器(int hp = 10),将 hp 暂时设为 10。Unity 反序列化系统介入:检查该实例是否有对应的序列化数据(来自场景文件或预制体文件)。如果有(比如之前在 Inspector 里改成了 20),则用文件中的 20覆盖hp。如果没有(从未修改过),则 hp 保持 10。
- 最终,
Enemy实例的hp值要么是代码默认值10,要么是被序列化数据覆盖后的值。
关键点:普通脚本的字段默认值 10 确实也是从磁盘(DLL)读入内存的,但它属于代码指令的一部分,无法在编辑器里像改资源那样直接修改并独立保存。如果要在编辑器中修改并持久化,必须通过 Inspector 修改并保存到场景/预制体文件中——这种方式本质上是在用资源文件覆盖代码默认值。
二、ScriptableObject 版 int hp = 10 的完整生命周期
1. 编写代码
csharp
[CreateAssetMenu]
public class EnemySO : ScriptableObject
{
public int hp = 10;
}
同样,代码中写死了 10。
2. 编译阶段
- 和普通脚本一样,
hp = 10被编译进 DLL 的 IL 指令中。
3. 编辑器中的创建与修改
首次创建 .asset 文件
- 你在 Project 窗口右键创建
EnemySO资产,Unity 会在内存中实例化一个EnemySO对象。 - 实例化过程:
- 调用 ScriptableObject.CreateInstance<EnemySO>(),内部会分配内存并执行字段初始化器,将 hp 设为 10。Unity 立即将这个内存中的对象序列化到磁盘,生成 .asset 文件。此时文件中存储的 hp 值就是 10。
- 注意:
.asset文件一旦生成,hp的值就被固化到了这个独立的数据文件中,与代码中的10脱离关系。
修改 .asset 文件
- 在 Inspector 中选中该
.asset文件,把hp改为20,然后保存。 - Unity 将新的
20重新序列化到同一个.asset文件中,覆盖原来的10。 - 整个过程不需要重新编译代码,因为改的是数据文件,而非代码。
4. 打包阶段
.asset文件作为常规资源被打包进游戏包,和场景、预制体等并列。
5. 玩家运行游戏
- 当游戏需要用到这个
EnemySO数据时(比如通过Resources.Load或直接引用),Unity 从包中加载对应的.asset文件。 - 反序列化过程:
- 读取 .asset 文件的二进制数据。直接在内存中重建对象,将 hp 字段赋值为文件中存储的值(比如 20)。
- 整个过程中不会执行字段初始化器,因为对象是通过反序列化创建的,其所有数据都来自文件。如果文件损坏或不存在,可能才会回退到代码默认值,但正常流程中文件是主数据源。
三、核心对比总结
编译后 | 10 固化在 DLL 的 IL 指令中 | 10 同样固化在 DLL 中,但仅作为创建资产时的初始值 |
编辑器修改 | 需通过 Inspector 修改,新值保存在场景/预制体文件中 | 直接在 Inspector 修改 .asset 文件,新值保存在资产文件中 |
运行时数据源 | 优先从场景/预制体文件反序列化,若无则用 DLL 中的默认值 | 直接从 .asset 文件反序列化,DLL 默认值被完全覆盖 |
能否独立配置多份 | 每个预制体/场景实例可独立修改值(通过序列化覆盖) | 可创建多个 .asset 文件,每个文件持有不同的值,被多个对象共享 |
修改门槛 | 改代码默认值需重新编译;改序列化值只需在 Inspector 操作 | 改默认值也无需重新编译,直接改 .asset 即可 |
四、你的总结非常到位
“其实说白了 这两种方式的数据流向是一样的 最主要的区别是写到正常脚本里int EnemyHp=10 不能修改 而写到ScriptableObject里面的是可以在unity编辑时随时修改并保存到磁盘 然后被调用就像其它unity资源是一样的对吧 最后游戏运行起来 这俩都是从磁盘读二进制到内存”
没错!两者的数据最终都是从磁盘二进制文件加载到内存,但:
- 普通脚本的默认值
**10**是“代码”的一部分,它存在于 DLL 这个“代码文件”中,修改它必须重新编译代码(除非用场景/预制体覆盖)。 - ScriptableObject 的
**10**只是创建资产时的“种子”,一旦资产文件生成,实际数据就独立存在于资产文件(.asset)中,可以在编辑器中像改贴图、模型一样随时修改并保存,完全无需碰代码。
这就是为什么 ScriptableObject 非常适合做游戏配置数据——它实现了真正的数据与代码分离,让策划、美术等非程序员也能自由调整数值,而不需要每次改个血量就麻烦程序员重新编译。
如果你还有任何疑问,或者想深入了解序列化在底层的具体实现,欢迎继续探讨
查看9道真题和解析