一、先搞懂基础:什么是序列化,为啥Unity要搞这东西
很多新手做Unity项目时,会遇到一个诡异的情况:自己写的脚本里,明明定义了一个公开的字段,比如用来存角色等级的public int roleLevel,但拖到Inspector面板里死活不显示;或者把一个自定义的类(比如专门存角色属性的类)当成字段写在MonoBehaviour里,Inspector里连个影子都没有。这背后的核心问题,就是没搞懂Unity的序列化规则——说白了,序列化就是Unity能“看懂”你写的C#代码里的字段,能把这些字段的值存下来、改一改、再读回去的能力。
举个最基础的例子,你在Unity里写一个普通的C#类,没加任何特殊标记:
// 普通C#类,用来存角色的基础属性
public class RoleBaseInfo
{
public int roleId; // 角色ID
public string roleName; // 角色名称
public float moveSpeed; // 移动速度
}
然后你在一个MonoBehaviour脚本里,把这个类当成字段:
// 控制角色的MonoBehaviour脚本
public class RoleController : MonoBehaviour
{
public RoleBaseInfo roleInfo; // 自定义类的字段
}
这时候你把RoleController拖到Unity的Inspector面板里,会发现roleInfo这个字段根本不显示——因为Unity默认只认几种类型的序列化,普通的自定义类不在里面。这时候就得用到[Serializable]这个标记了。
二、自定义Serializable类:让Unity“看懂”你的自定义类
2.1 给自定义类加[Serializable]标记
要让Unity能识别你的自定义类,最简单的方法就是给类加[Serializable]属性,这个属性是C#自带的,专门用来标记一个类可以被序列化。我们把刚才的RoleBaseInfo改一下:
// 给自定义类加[Serializable]标记,告诉Unity这个类可以被序列化
[Serializable]
public class RoleBaseInfo
{
public int roleId; // 角色ID
public string roleName; // 角色名称
public float moveSpeed; // 移动速度
}
这时候再看Inspector面板,roleInfo字段就出来了,还能展开编辑里面的三个属性。这里要注意一个坑:[Serializable]标记是加在自定义类上的,不是加在MonoBehaviour里的字段上。
2.2 自定义类里的嵌套:能不能继续自定义类?
很多时候我们的自定义类会有嵌套,比如角色属性里还要分“基础属性”和“战斗属性”,这时候只要嵌套的类也加了[Serializable]标记,Unity就能识别。比如:
// 战斗属性类,加[Serializable]
[Serializable]
public class BattleInfo
{
public int maxHp; // 最大生命值
public int attack; // 攻击力
public int defense; // 防御力
}
// 基础属性类,加[Serializable]
[Serializable]
public class RoleBaseInfo
{
public int roleId; // 角色ID
public string roleName; // 角色名称
public float moveSpeed; // 移动速度
public BattleInfo battleInfo; // 嵌套的战斗属性类,必须加[Serializable]
}
这时候在Inspector里,roleInfo展开后还能展开battleInfo,编辑里面的战斗属性,完全没问题。
2.3 自定义类里的集合:数组、List能不能序列化?
很多新手会遇到另一个问题:自定义类里的数组或者List,加了[Serializable]还是不显示?其实Unity对集合的序列化有要求:只有当集合里的元素是Unity能识别的类型时,集合本身才能被序列化。比如我们给RoleBaseInfo加一个技能列表:
[Serializable]
public class RoleBaseInfo
{
public int roleId; // 角色ID
public string roleName; // 角色名称
public float moveSpeed; // 移动速度
public BattleInfo battleInfo; // 嵌套的战斗属性类
public int[] skillIds; // 技能ID数组,int是Unity能识别的类型,所以数组能序列化
public List<string> skillNames; // 技能名称列表,string是Unity能识别的类型,所以List能序列化
}
这里的int[]和List<string>都能在Inspector里显示,因为int和string都是Unity原生支持的序列化类型。但如果我们加一个自定义类的集合,比如:
// 技能类,加[Serializable]
[Serializable]
public class Skill
{
public int skillId;
public string skillName;
}
[Serializable]
public class RoleBaseInfo
{
// ... 前面的属性不变
public List<Skill> skills; // 技能列表,Skill加了[Serializable],所以能序列化
}
这时候skills列表也能在Inspector里显示,因为Skill类加了[Serializable]。但如果Skill类没加[Serializable],这个列表就不会显示。
三、MonoBehaviour的字段序列化规则:哪些字段能在Inspector里显示?
很多新手会问:我写的MonoBehaviour里的字段,有的能在Inspector里显示,有的不能,到底有什么规律?这里我们把规则拆成“能显示的条件”和“不能显示的坑”两部分讲。
3.1 能在Inspector里显示的字段,必须满足这几个条件
条件1:字段必须是public的,或者加[SerializeField]标记
Unity默认只序列化public的字段,如果你写的是private或者protected的字段,不加任何标记的话,不会在Inspector里显示。比如:
public class RoleController : MonoBehaviour
{
public int publicLevel; // public字段,能显示
private int privateLevel; // private字段,不能显示
protected int protectedLevel; // protected字段,不能显示
}
但如果你想把private字段也在Inspector里显示,只要加[SerializeField]标记就行,这个标记是Unity专门用来让私有字段被序列化的:
public class RoleController : MonoBehaviour
{
public int publicLevel; // public字段,能显示
[SerializeField]
private int privateLevel; // 加了[SerializeField]的private字段,能显示
protected int protectedLevel; // 还是不能显示
}
这里要注意:[SerializeField]只能加在字段上,不能加在属性上(比如public int Level { get; set; }这种属性,加了也没用)。
条件2:字段的类型必须是Unity能识别的序列化类型
Unity能识别的序列化类型主要分几类:
- 基础值类型:int、float、bool、string、char、枚举(enum)
- Unity自带的类型:Vector2、Vector3、Color、GameObject、Transform、Texture2D等
- 加了
[Serializable]的自定义类、结构体 - 集合类型:数组、List(集合里的元素必须是上面三种类型)
比如你写一个枚举:
// 角色类型枚举,默认就能被序列化
public enum RoleType
{
Warrior, // 战士
Mage, // 法师
Archer // 弓箭手
}
public class RoleController : MonoBehaviour
{
public RoleType roleType; // 枚举类型,能在Inspector里显示
}
这时候roleType会在Inspector里显示成下拉框,非常方便。
条件3:字段不能是static、const或者readonly的
静态字段(static)、常量(const)、只读字段(readonly)都不能被序列化,因为这些字段的值是固定的,或者是属于类本身而不是属于实例的。比如:
public class RoleController : MonoBehaviour
{
public static int staticLevel; // static字段,不能显示
public const int constLevel = 1; // const字段,不能显示
public readonly int readonlyLevel = 1; // readonly字段,不能显示
}
这些字段即使是public的,也不会在Inspector里显示。
3.2 容易踩的坑:这些字段明明满足条件,却不显示
坑1:自定义类是结构体(struct),没加[Serializable]
很多人知道自定义类要加[Serializable],但自定义结构体的时候经常忘了加。结构体默认不能被序列化,所以必须加[Serializable]。比如:
// 自定义结构体,必须加[Serializable]才能被序列化
[Serializable]
public struct RoleInfoStruct
{
public int roleId;
public string roleName;
}
public class RoleController : MonoBehaviour
{
public RoleInfoStruct roleInfo; // 结构体加了[Serializable],能显示
}
如果结构体没加[Serializable],这个字段就不会显示。
坑2:集合里的元素是接口(interface)或者抽象类(abstract class)
接口和抽象类本身不能被实例化,所以Unity不能序列化它们。比如你写一个技能接口:
public interface ISkill
{
void Use();
}
public class FireSkill : ISkill
{
public int damage;
public void Use()
{
// 释放火焰技能
}
}
public class RoleController : MonoBehaviour
{
public List<ISkill> skills; // 接口类型的集合,不能被序列化
}
这个skills列表不会在Inspector里显示,因为Unity不知道要序列化哪个具体的实现类。
坑3:字段是属性(property),不是字段(field)
很多新手会把属性当成字段,比如:
public class RoleController : MonoBehaviour
{
public int RoleLevel { get; set; } // 属性,不是字段,不能被序列化
}
这个RoleLevel是属性,不是字段,所以不会在Inspector里显示。如果你想把属性的值在Inspector里显示,必须单独加一个对应的字段:
public class RoleController : MonoBehaviour
{
[SerializeField]
private int _roleLevel; // 对应的字段
public int RoleLevel // 属性
{
get => _roleLevel;
set => _roleLevel = value;
}
}
四、进阶技巧:自定义序列化的实际应用场景
说了这么多规则,很多人可能会问:这些东西到底有什么用?我们来举几个实际的应用场景。
场景1:配置游戏数据,不用写配置文件
很多游戏的配置数据,比如角色的属性、技能的效果,都可以用自定义的Serializable类来存,不用额外写JSON或者XML配置文件。比如我们做一个游戏的关卡配置:
// 关卡里的敌人配置
[Serializable]
public class EnemyConfig
{
public string enemyName; // 敌人名称
public int enemyHp; // 敌人生命值
public float spawnTime; // 生成时间(关卡开始后多久生成)
}
// 关卡配置
[Serializable]
public class LevelConfig
{
public int levelId; // 关卡ID
public string levelName; // 关卡名称
public List<EnemyConfig> enemies; // 关卡里的敌人列表
}
// 管理所有关卡的MonoBehaviour
public class LevelManager : MonoBehaviour
{
public List<LevelConfig> allLevels; // 所有关卡的配置
}
这时候我们可以直接在Inspector里编辑所有关卡的配置,不用写任何配置文件,非常方便,适合小型游戏或者原型开发。
场景2:保存游戏进度,不用写复杂的序列化代码
Unity自带的PlayerPrefs只能存简单的类型(int、float、string),如果要存复杂的游戏进度,比如角色的所有属性、背包里的物品,就可以用自定义的Serializable类,然后用Unity的JsonUtility来序列化和反序列化。比如:
// 游戏进度类,加[Serializable]
[Serializable]
public class GameSaveData
{
public int roleLevel; // 角色等级
public string roleName; // 角色名称
public List<int> inventory; // 背包里的物品ID
}
// 保存和读取游戏进度的工具类
public static class SaveLoadManager
{
// 保存游戏进度
public static void SaveGame(GameSaveData saveData)
{
// 把游戏进度序列化成JSON字符串
string json = JsonUtility.ToJson(saveData);
// 存到PlayerPrefs里
PlayerPrefs.SetString("GameSave", json);
PlayerPrefs.Save();
}
// 读取游戏进度
public static GameSaveData LoadGame()
{
// 从PlayerPrefs里读取JSON字符串
string json = PlayerPrefs.GetString("GameSave", "");
if (string.IsNullOrEmpty(json))
{
// 没有存档,返回默认值
return new GameSaveData
{
roleLevel = 1,
roleName = "新角色",
inventory = new List<int>()
};
}
// 把JSON字符串反序列化成GameSaveData对象
return JsonUtility.FromJson<GameSaveData>(json);
}
}
这个工具类可以很方便地保存和读取复杂的游戏进度,不用自己写复杂的序列化代码。
五、自定义序列化的优缺点和注意事项
5.1 优缺点
优点:
- 方便编辑:自定义的Serializable类可以直接在Inspector里编辑,不用额外的配置工具,适合小型项目和原型开发。
- 性能好:Unity的序列化是原生的,性能比自己写的序列化代码好很多。
- 兼容性好:Unity的序列化可以和Unity的其他功能(比如Prefab、Scene)很好地结合,比如你可以把一个带自定义Serializable类的MonoBehaviour做成Prefab,然后在不同的场景里复用。
缺点:
- 功能有限:Unity的序列化不能序列化接口、抽象类、静态字段等,对于复杂的项目来说可能不够用。
- 编辑效率低:如果配置数据很多,在Inspector里编辑会非常麻烦,比如一个有上百个关卡的游戏,每个关卡有几十个敌人,在Inspector里编辑会非常耗时,容易出错。
- 安全性差:自定义的Serializable类的字段可以在Inspector里随意修改,容易被玩家作弊,比如修改角色的等级、生命值等。
5.2 注意事项
- 不要过度依赖Inspector编辑配置:对于大型项目,建议用专门的配置工具(比如Excel、Google Sheets)来管理配置数据,然后导出成JSON或者XML,再用JsonUtility来读取。
- 不要序列化敏感数据:比如游戏的密码、付费信息等,不要用Unity的序列化来存,因为这些数据很容易被修改。
- 注意序列化的版本兼容:如果你的游戏更新后,自定义的Serializable类的结构发生了变化(比如加了一个字段),要注意旧的存档可能会不兼容,需要做版本兼容处理。
- 不要序列化太大的数据:比如一个有几万个元素的List,序列化的时候会占用很多内存和时间,建议把大的数据拆分或者用其他方式存储。
六、总结
Unity的序列化规则其实并不复杂,核心就是:自定义类(包括结构体)要加[Serializable]标记,MonoBehaviour里的字段要满足“类型能被识别、不是静态/常量/只读、是public或者加了[SerializeField]”的条件。掌握了这些规则,你就可以灵活地用自定义的Serializable类来做配置、存游戏进度等,让你的开发效率更高。
对于新手来说,一开始可能会遇到很多坑,比如自定义类没加[Serializable]、字段是属性不是字段、集合里的元素不能被序列化等,只要多试几次,慢慢就会熟悉这些规则。对于进阶开发者来说,还要注意序列化的性能、版本兼容、安全性等问题,根据项目的实际情况选择合适的序列化方案。
评论
围绕“Unity C#序列化进阶:自定义Serializable类与MonoBehaviour的字段序列化规则”参与讨论