一、先搞懂基础:什么是序列化,为啥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能识别的序列化类型主要分几类:

  1. 基础值类型:int、float、bool、string、char、枚举(enum)
  2. Unity自带的类型:Vector2、Vector3、Color、GameObject、Transform、Texture2D等
  3. 加了[Serializable]的自定义类、结构体
  4. 集合类型:数组、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 优缺点

优点:

  1. 方便编辑:自定义的Serializable类可以直接在Inspector里编辑,不用额外的配置工具,适合小型项目和原型开发。
  2. 性能好:Unity的序列化是原生的,性能比自己写的序列化代码好很多。
  3. 兼容性好:Unity的序列化可以和Unity的其他功能(比如Prefab、Scene)很好地结合,比如你可以把一个带自定义Serializable类的MonoBehaviour做成Prefab,然后在不同的场景里复用。

缺点:

  1. 功能有限:Unity的序列化不能序列化接口、抽象类、静态字段等,对于复杂的项目来说可能不够用。
  2. 编辑效率低:如果配置数据很多,在Inspector里编辑会非常麻烦,比如一个有上百个关卡的游戏,每个关卡有几十个敌人,在Inspector里编辑会非常耗时,容易出错。
  3. 安全性差:自定义的Serializable类的字段可以在Inspector里随意修改,容易被玩家作弊,比如修改角色的等级、生命值等。

5.2 注意事项

  1. 不要过度依赖Inspector编辑配置:对于大型项目,建议用专门的配置工具(比如Excel、Google Sheets)来管理配置数据,然后导出成JSON或者XML,再用JsonUtility来读取。
  2. 不要序列化敏感数据:比如游戏的密码、付费信息等,不要用Unity的序列化来存,因为这些数据很容易被修改。
  3. 注意序列化的版本兼容:如果你的游戏更新后,自定义的Serializable类的结构发生了变化(比如加了一个字段),要注意旧的存档可能会不兼容,需要做版本兼容处理。
  4. 不要序列化太大的数据:比如一个有几万个元素的List,序列化的时候会占用很多内存和时间,建议把大的数据拆分或者用其他方式存储。

六、总结

Unity的序列化规则其实并不复杂,核心就是:自定义类(包括结构体)要加[Serializable]标记,MonoBehaviour里的字段要满足“类型能被识别、不是静态/常量/只读、是public或者加了[SerializeField]”的条件。掌握了这些规则,你就可以灵活地用自定义的Serializable类来做配置、存游戏进度等,让你的开发效率更高。

对于新手来说,一开始可能会遇到很多坑,比如自定义类没加[Serializable]、字段是属性不是字段、集合里的元素不能被序列化等,只要多试几次,慢慢就会熟悉这些规则。对于进阶开发者来说,还要注意序列化的性能、版本兼容、安全性等问题,根据项目的实际情况选择合适的序列化方案。