10.Unity进阶ScriptableObject总结

  1. 10.总结
    1. 10.1 知识点
      1. 学习的主要内容
      2. 总结讲什么
      3. 优点
      4. 何时使用ScriptableObject
      5. 注意
    2. 10.2 核心要点速览
      1. ScriptableObject 是什么
      2. 自定义 ScriptableObject 数据容器
        1. 基本步骤
      3. 创建数据文件的两种方式
        1. 方式一:CreateAssetMenu 特性(推荐)
        2. 方式二:MenuItem + AssetDatabase API
      4. 数据文件的使用方式
        1. 方式一:Inspector 拖拽关联
        2. 方式二:资源动态加载
      5. ScriptableObject 的生命周期函数
      6. ScriptableObject 的好处体现
        1. 1. 更方便的配置数据
        2. 2. 项目之间的复用
        3. 3. 编辑器中的数据持久化
        4. 4. 复用数据节约内存
      7. 非持久化数据
        1. 什么是非持久化数据
        2. 如何创建非持久化数据
        3. 非持久化数据存在的意义
      8. 真正意义上的持久化
        1. 核心结论
        2. 结合 Json 实现持久化
        3. 完整示例:设置信息的持久化
      9. 应用场景一:配置数据
        1. 为什么非常适合做配置文件
        2. 示例:角色信息配置
        3. 适用条件
      10. 应用场景二:复用数据
        1. 预设体可能存在的内存浪费问题
        2. 解决方案:使用 ScriptableObject 共享数据
        3. 总结
      11. 应用场景三:数据带来的多态行为
        1. 什么是数据带来的多态行为
        2. 示例一:随机音效系统
        3. 示例二:物品拾取系统
        4. 总结
      12. 应用场景四:单例模式化的获取数据
        1. 为什么要单例模式化
        2. 单例基类实现
        3. 使用方式
        4. 总结
      13. 要点速查表
      14. 注意事项总结
    3. 10.3 面试题精选
      1. 基础题
        1. 1. ScriptableObject 和 MonoBehaviour 有什么区别?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. 如何创建 ScriptableObject 数据文件?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
      2. 进阶题
        1. 3. ScriptableObject 的数据在运行时能持久化吗?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 4. 多个对象引用同一个 ScriptableObject 时,修改数据会有什么影响?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
      3. 深度题
        1. 5. 如何设计一个 ScriptableObject 单例基类?有什么使用规则?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 6. 如何利用 ScriptableObject 实现数据驱动的多态行为?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章

10.总结


10.1 知识点

学习的主要内容

总结讲什么

优点

何时使用ScriptableObject

注意


10.2 核心要点速览

ScriptableObject 是什么

一种用于存储大量数据的数据容器类,继承自 ScriptableObject 基类。

主要特点

  • 数据复用:多个对象可以共享同一份数据,节约内存空间
  • 配置文件:可以直接在 Unity 的 Inspector 窗口进行配置,不需要第三方软件(如 Excel、XML 编辑器)
  • 编辑器持久化:在编辑模式下修改数据会保存到 .asset 文件中,但打包运行后不会自动持久化

适用场景

  • 只用不改的数据(如角色配置、技能数据)
  • 需要在多处复用的数据
  • 需要频繁配置的数据

自定义 ScriptableObject 数据容器

基本步骤

  1. 继承 ScriptableObject
  2. 在类中声明成员变量和方法
public class MyData : ScriptableObject
{
    // 声明成员时需要注意:
    // 我们可以声明任何类型的成员变量
    // 但是如果希望在 Inspector 窗口中能够编辑它
    // 声明的变量规则要和 MonoBehaviour 中 public 变量的规则是一样的
    
    public int i;
    public float f;
    public bool b;
    
    public GameObject obj;
    public Material m;
    public AudioClip audioClip;
    public VideoClip videoClip;
}

注意:声明后可以在 Inspector 窗口中看到变化,可以在其中进行设置。但这些设置都是默认数据,并没有真正使用它们。这些关联信息都是通过脚本文件对应的 Unity 配置文件(.meta)进行记录的。目前该数据只是一个数据容器模板,有了它之后才能根据它的信息创建对应的数据资源文件。

创建数据文件的两种方式

方式一:CreateAssetMenu 特性(推荐)

// fileName:默认文件名
// menuName:在 Asset/Create 菜单中显示的名字
// order:在 Asset/Create 菜单中的位置(多个时可以通过它来调整顺序)
[CreateAssetMenu(fileName = "MrTangData", menuName = "ScriptableObject/我的数据", order = 0)]
public class MyData : ScriptableObject
{
    public int i;
    public float f;
    public bool b;
}

添加特性后,在 Project 窗口右键 → Create → ScriptableObject/我的数据 即可创建 .asset 文件。创建的文件名和配置的相同,其他公共变量也会显示出来,有默认值的变量会默认关联配置的默认值。

方式二:MenuItem + AssetDatabase API

创建一个脚本,在静态方法中添加 [MenuItem] 特性:

using UnityEditor;  // AssetDatabase 在 UnityEditor 命名空间下

public class ScriptableObjectTool
{
    [MenuItem("ScriptableObject/CreateMyData")]
    public static void CreateMyData()
    {
        // 书写创建数据资源文件的代码
        // <T> 中一定要继承 ScriptableObject
        MyData myDataAsset = ScriptableObject.CreateInstance<MyData>();
        
        // 通过编辑器 API 根据数据创建一个数据资源文件
        AssetDatabase.CreateAsset(myDataAsset, "Assets/Resources/MyDataTest.asset");
        
        // 保存创建的资源
        AssetDatabase.SaveAssets();
        
        // 刷新界面
        AssetDatabase.Refresh();
    }
}

注意AssetDatabase 是编辑器 API,相关代码需要放在 Editor 文件夹中,否则打包时会报错。

两种方式对比

  • CreateAssetMenu:简单直观,适合大多数场景
  • MenuItem + AssetDatabase:可自定义创建逻辑、批量创建、设置默认值,适合编辑器工具开发

数据文件的使用方式

方式一:Inspector 拖拽关联

在继承 MonoBehaviour 的类中声明数据容器类型的成员,然后在 Inspector 窗口进行关联:

public class TestScript : MonoBehaviour
{
    public MyData myData;  // 在 Inspector 中拖入 .asset 文件
    
    void Start()
    {
        myData.PrintInfo();  // 调用数据对象的方法
    }
}

方式二:资源动态加载

public class TestScript : MonoBehaviour
{
    public MyData myData;
    
    void Start()
    {
        // Resources、AB包、Addressables 都支持加载
        myData = Resources.Load<MyData>("MyDataTest");
        myData.PrintInfo();
    }
}

重要特性:如果多个对象关联同一个数据容器文件,它们共享的是同一个对象。因为是引用类型,所以在其中任何地方修改后,其他地方也会发生改变。

理解方式:可以把创建出来的数据资源文件理解成一种记录数据的资源,使用方式和 Unity 中其他资源(预设体、音频文件、视频文件、动画控制器、材质球等)的规则是一样的,只不过通过继承 ScriptableObject 生成的数据资源文件主要是和数据相关的。

ScriptableObject 的生命周期函数

ScriptableObject 和 MonoBehaviour 类似,也有生命周期函数,但数量较少,一般使用不多。

函数 触发时机 说明
Awake 数据文件创建时调用 初始化逻辑
OnEnable ScriptableObject 创建或加载对象时调用
OnDisable ScriptableObject 对象销毁时、即将重新加载脚本程序集时调用
OnDestroy ScriptableObject 对象将被销毁时调用
OnValidate 仅编辑器下调用的函数,Unity 加载脚本或 Inspector 窗口中更改值时调用 调试和验证用
public class MyData : ScriptableObject
{
    private void Awake()
    {
        Debug.Log("数据文件创建时会调用");
    }
    
    private void OnValidate()
    {
        Debug.Log("值改变");  // Inspector 中修改值时触发
    }
}

ScriptableObject 的好处体现

1. 更方便的配置数据

可以直接在 Inspector 当中配置数据,不需要第三方软件。

2. 项目之间的复用

可以拷贝继承 ScriptableObject 的脚本到任何工程中,数据文件也可以直接复制使用。

3. 编辑器中的数据持久化

通过代码修改数据对象中的内容,会影响数据文件,实现了在编辑器中数据的持久化。

myData.i = 6666;
myData.f = 54250.66f;
myData.b = false;

注意:这种数据持久化只在编辑模式下有效,在发布和运行时并不会保存数据。

4. 复用数据节约内存

当多个对象关联同一个数据文件时,它们实际上是共享一组数据的,这样可以更加节约内存空间。

非持久化数据

什么是非持久化数据

指的是不管在编辑器模式还是在发布后都不会持久化的数据。可以根据自己的需求随时创建对应数据对象进行使用,就好像直接 new 一个数据结构类对象。

如何创建非持久化数据

// 利用 ScriptableObject 中的静态方法 CreateInstance<>()
// 可以理解为这是 ScriptableObject 的 new 操作
// 该方法可以在运行时创建出指定继承 ScriptableObject 的对象
// 该对象只存在于内存当中,可以被 GC 回收
// 调用一次就创建一次

MyData myData = ScriptableObject.CreateInstance<MyData>();
myData.PrintInfo();

重要区别:通过这种方式创建出来的数据对象,它里面的默认值不会受到脚本中设置的影响

比如 MyData 脚本有关联默认的音乐切片文件和材质球:

  • 直接在 Inspector 窗口创建的 MyData 数据文件 → 会关联默认音乐切片文件和材质球
  • CreateInstance 方法创建的数据对象 → 不会关联,这些引用都是 null
Debug.Log(myData.audioClip);  // Null
Debug.Log(myData.m);          // Null

非持久化数据存在的意义

  • 只是希望在运行时能有一组唯一的数据可以使用
  • 但是这个数据又不太希望保存为数据资源文件浪费硬盘空间
  • 它的特点是只在运行时使用,在编辑器模式下也不会保存在本地

真正意义上的持久化

核心结论

ScriptableObject 并不适合用来做数据持久化功能。在游戏发布运行过程中,ScriptableObject 的数据无法被持久化。

结合 Json 实现持久化

可以利用已学过的数据持久化方案(PlayerPrefs、XML、Json、二进制)让其持久化。

存储数据

using System.IO;  // File 类需要此命名空间

MyData myData = ScriptableObject.CreateInstance<MyData>();
myData.i = 9999;
myData.f = 6.6f;
myData.b = true;

// 将数据对象序列化为 json 字符串
string str = JsonUtility.ToJson(myData);
print(str);

// 把数据序列化后的结果存入指定路径当中
File.WriteAllText(Application.persistentDataPath + "/testJson.json", str);
print(Application.persistentDataPath);

读取数据

// 从本地读取 Json 字符串
string str = File.ReadAllText(Application.persistentDataPath + "/testJson.json");

// 根据 json 字符串反序列化出数据,将内容覆盖到数据对象中
JsonUtility.FromJsonOverwrite(str, myData);

myData.PrintInfo();

FromJson vs FromJsonOverwrite 区别

方法 作用 说明
FromJson 将 JSON 数据转换为新的对象实例 用一个新对象来接,就算用老对象来接也是引用新的实例
FromJsonOverwrite 将 JSON 数据覆盖到现有对象 保持原对象引用,只覆盖数据内容

完整示例:设置信息的持久化

[CreateAssetMenu(fileName = "SettingInfo", menuName = "ScriptableObject/音乐音效设置信息")]
public class SettingInfo : ScriptableObject
{
    // 音乐和音效的开关
    public bool musicIsOpen;
    public bool soundIsOpen;
    
    // 音乐和音效的大小
    public float musicValue;
    public float soundValue;
    
    private void Awake()
    {
        // 判断是否存在持久化的数据文件
        if (File.Exists(Application.persistentDataPath + "/SettingInfo.json"))
        {
            string str = File.ReadAllText(Application.persistentDataPath + "/SettingInfo.json");
            JsonUtility.FromJsonOverwrite(str, this);
        }
    }
    
    /// <summary>
    /// 保存到本地进行持久化
    /// </summary>
    public void Save()
    {
        string str = JsonUtility.ToJson(this);
        File.WriteAllText(Application.persistentDataPath + "/SettingInfo.json", str);
    }
}

建议:不建议利用 ScriptableObject 来做数据持久化,有点画蛇添足的意思了。直接用 PlayerPrefs 或 Json 文件更合适。

应用场景一:配置数据

为什么非常适合做配置文件

  1. 配置文件的数据在游戏发布之前定规则
  2. 配置文件的数据在游戏运行时只会读出来使用,不会改变内容
  3. 在 Unity 的 Inspector 窗口进行配置更加方便

以前常规的配置方式是利用 XML、Json、Excel 等进行配置,需要第三方软件。而 ScriptableObject 不需要第三方软件,可以直接在 Unity 中配置。

示例:角色信息配置

[CreateAssetMenu(fileName = "RoleInfo", menuName = "ScriptableObject/角色信息")]
public class RoleInfo : ScriptableObject
{
    // 定义内部类 RoleData,用于存储角色的各项信息
    // [System.Serializable] 让这个类能在 Inspector 窗口上编辑
    [System.Serializable]
    public class RoleData
    {
        public int id;         // 角色ID
        public string res;     // 角色模型资源路径
        public int atk;        // 角色攻击力
        public string tips;    // 角色提示信息
        public int lockMoney;  // 解锁所需金币数
        public int type;       // 角色类型
        public string hitEff;  // 攻击特效路径
        
        public void Print()
        {
            Debug.Log(id);
            Debug.Log(res);
            Debug.Log(atk);
            // ...
        }
    }
    
    public List<RoleData> roleList;  // 存储角色数据的列表
}

使用

public class GameManager : MonoBehaviour
{
    public RoleInfo roleInfo;
    
    void Start()
    {
        for (int i = 0; i < roleInfo.roleList.Count; i++)
        {
            roleInfo.roleList[i].Print();
        }
    }
}

适用条件

  • 只用不改
  • 并且经常会进行配置的数据

扩展应用:可以利用 ScriptableObject 数据文件来制作编辑器相关功能,比如 Unity 内置的技能编辑器、关卡编辑器等。不需要把编辑器生成的数据生成别的数据文件,而是直接通过 ScriptableObject 进行存储。因为内置编辑器只会在编辑模式下运行,编辑模式下 ScriptableObject 具备数据持久化的特性。

应用场景二:复用数据

预设体可能存在的内存浪费问题

对于只用不变的数据,以面向对象的思想去声明对象类可能存在内存浪费的问题。

以子弹对象为例:每个创建出来的子弹预设体都挂了子弹脚本,每个子弹脚本的成员变量都会占内存空间。像速度、攻击力这种配置变量,所有子弹都是一样的,完全可以共享同一份内存空间的数据。

解决方案:使用 ScriptableObject 共享数据

创建子弹信息类

[CreateAssetMenu()]
public class BulletInfo : ScriptableObject
{
    public float speed;  // 子弹速度
    public int atk;      // 子弹攻击力
}

子弹脚本关联数据文件

public class Bullet : MonoBehaviour
{
    public BulletInfo bulletInfo;  // 所有子弹预制体关联同一个 .asset 文件
    
    void Update()
    {
        // 所有子弹共享同一份速度配置
        this.transform.Translate(Vector3.forward * bulletInfo.speed * Time.deltaTime);
    }
}

全局控制

public class GameManager : MonoBehaviour
{
    public BulletInfo bulletInfo;  // 关联同一个子弹信息数据文件
    
    void Update()
    {
        // 按下空格键增加所有子弹的速度
        // 因为所有子弹共享同一个数据文件,修改一处全局生效
        if (Input.GetKeyDown(KeyCode.Space))
            bulletInfo.speed += 1;
    }
}

优点

  • 即使发布也是共享同一份内存空间
  • 通过控制子弹信息数据文件上的配置可以整体控制所有子弹

总结

对于不同对象使用相同数据时,可以使用 ScriptableObject 来节约内存。

应用场景三:数据带来的多态行为

什么是数据带来的多态行为

某些行为的变化是因为数据的不同带来的。可以利用面向对象的特性和原则(里氏替换原则、依赖倒转原则),以及设计模式相关知识点,结合 ScriptableObject 做出更加方便的功能。

典型应用

  • 随机音效:播放音乐时随机播放多个音效中的一种
  • 物品拾取:拾取一个物品,物品给玩家带来不同的效果
  • AI:不同数据带来的不同行为模式

示例一:随机音效系统

抽象基类

// 声明一个抽象类 AudioPlayBase,继承自 ScriptableObject
public abstract class AudioPlayBase : ScriptableObject
{
    // 声明一个抽象方法 Play,接收一个 AudioSource 参数
    public abstract void Play(AudioSource source);
}

随机播放实现

[CreateAssetMenu()]
public class RandomPlayAudio : AudioPlayBase
{
    // 存储希望随机播放的音频文件列表
    public List<AudioClip> clips;
    
    public override void Play(AudioSource source)
    {
        // 如果音频文件列表为空,则直接返回
        if (clips.Count == 0)
            return;
        
        // 从音频文件列表中随机选择一个
        int randomIndex = Random.Range(0, clips.Count);
        AudioClip randomClip = clips[randomIndex];
        
        // 设置并播放
        source.clip = randomClip;
        source.Play();
    }
}

单曲播放实现

[CreateAssetMenu()]
public class PlayerAudio : AudioPlayBase
{
    // 存储要播放的音频文件
    public AudioClip clip;
    
    public override void Play(AudioSource source)
    {
        source.clip = clip;
        source.Play();
    }
}

使用(里氏替换原则)

public class AudioManager : MonoBehaviour
{
    // 声明基类变量,可以关联 RandomPlayAudio 或 PlayerAudio 的 .asset 文件
    public AudioPlayBase audioPlay;
    
    void Start()
    {
        // 调用时不需要知道具体是哪种实现
        audioPlay.Play(this.GetComponent<AudioSource>());
    }
}

示例二:物品拾取系统

抽象基类

public abstract class ItemEffect : ScriptableObject
{
    public abstract void AddEffect(GameObject obj);
}

加攻击力效果

[CreateAssetMenu]
public class AddAtkItemEffect : ItemEffect
{
    public int atk;
    
    public override void AddEffect(GameObject obj)
    {
        // 具体加多少攻击力的逻辑
    }
}

加血效果

[CreateAssetMenu]
public class AddHealthItemEffect : ItemEffect
{
    public int num;
    
    public override void AddEffect(GameObject obj)
    {
        // 通过获取到的对象让其加血,加 num 的值
    }
}

物品对象

public class ItemObj : MonoBehaviour
{
    public ItemEffect eff;  // 可以关联任意子类的 .asset 文件
    
    private void OnTriggerEnter(Collider other)
    {
        eff.AddEffect(other.gameObject);
    }
}

总结

这些功能就算不用 ScriptableObject 也能用面向对象思想结合配置文件来完成,但 ScriptableObject 具备自己的优点:

  1. 更方便的配置
  2. 共享数据节约内存

应用场景四:单例模式化的获取数据

为什么要单例模式化

对于只用不变并且要复用的数据(比如配置文件中的数据),往往需要在很多地方获取它们。

问题

  • 如果直接通过 public 关联或动态加载,在多处使用会存在很多重复代码
  • 每次使用要不就得拖拽,要不就得 Resources.Load,比较麻烦

解决:将此类数据通过单例模式化的方式获取,可以提升效率,减少代码量。

单例基类实现

public class SingleScriptableObject<T> : ScriptableObject where T : ScriptableObject
{
    private static T instance;
    
    public static T Instance
    {
        get
        {
            // 如果为空,首先去资源路径下加载对应的数据资源文件
            if (instance == null)
            {
                // 我们定两个规则:
                // 1. 所有的数据资源文件都放在 Resources 文件夹下的 ScriptableObject 中
                // 2. 需要复用的唯一的数据资源文件名:和类名是一样的
                instance = Resources.Load<T>("ScriptableObject/" + typeof(T).Name);
            }
            
            // 如果没有这个文件,为了安全起见可以直接创建一个数据对象
            if (instance == null)
            {
                instance = CreateInstance<T>();
            }
            // 甚至可以在这里从 json 中读取数据
            // 但是不建议用 ScriptableObject 来做数据持久化
            
            return instance;
        }
    }
}

使用方式

继承基类

// 让 RoleInfo 继承 SingleScriptableObject<RoleInfo>
public class RoleInfo : SingleScriptableObject<RoleInfo>
{
    [System.Serializable]
    public class RoleData
    {
        public int id;
        public string tips;
        // ...
    }
    
    public List<RoleData> roleList;
}

// 或者创建新的数据类
[CreateAssetMenu]
public class TestData : SingleScriptableObject<TestData>
{
    public int i;
    public bool b;
}

直接使用 Instance 获取

// Resources/ScriptableObject 下有没有数据都行
// 没有无非就是动态得到默认的
print(RoleInfo.Instance.roleList[0].id);
print(RoleInfo.Instance.roleList[1].tips);

print(TestData.Instance.i);
print(TestData.Instance.b);

总结

这种基类比较适合配置数据的管理和获取。当数据是只用不变,并且是唯一的时候,可以使用该方式提高开发效率。

在此基础上也可以根据自己的需求进行变形,比如添加数据持久化的功能,将数据从 json 中读取,并提供保存数据的方法。但不建议用 ScriptableObject 来制作数据持久化功能。

要点速查表

场景 推荐方案 说明
配置数据 创建 .asset 文件 只读不改,Inspector 直接配置
复用数据 多对象关联同一 .asset 共享内存,修改全局生效
运行时临时数据 CreateInstance<T>() 不创建文件,可被 GC 回收
多处获取同一数据 单例基类 减少重复代码,统一入口
运行时持久化 Json + File ScriptableObject 本身不支持

注意事项总结

  1. 编辑器持久化 vs 运行时持久化:ScriptableObject 在编辑模式下修改会保存,但打包运行后不会自动持久化

  2. 多引用共享:多个对象关联同一个 .asset 文件时共享同一实例,任何地方修改都会影响所有引用

  3. 非持久化数据特点:通过 CreateInstance 创建的对象不会继承脚本中设置的默认关联

  4. 不建议做数据持久化:ScriptableObject 的设计初衷是配置和复用数据,用 Json/PlayerPrefs 做持久化更合适

  5. 单例基类规则:数据文件放在 Resources/ScriptableObject/ 下,文件名与类名一致

10.3 面试题精选

基础题

1. ScriptableObject 和 MonoBehaviour 有什么区别?

题目

ScriptableObject 和 MonoBehaviour 有什么区别?各自适用于什么场景?

深入解析

两者都是 Unity 的基类,但定位完全不同:

对比项 MonoBehaviour ScriptableObject
挂载对象 必须挂载到 GameObject 不能挂载,是独立资源文件
生命周期 与 GameObject 绑定 独立存在,生命周期更长
数据共享 每个实例独立数据 多引用共享同一实例
序列化 随场景/预制体保存 保存为 .asset 文件
适用场景 行为逻辑、游戏对象 数据存储、配置管理

核心差异:MonoBehaviour 是”行为+数据”的载体,每个挂载实例都有独立的数据副本;ScriptableObject 是纯数据容器,多个引用共享同一份数据。

答题示例

MonoBehaviour 必须挂载到 GameObject 上,适合处理游戏对象的行为逻辑,每个实例有独立数据。

ScriptableObject 是独立的数据资源文件,不能挂载到 GameObject,多个对象引用同一 .asset 时共享同一实例,适合存储配置数据、复用数据。

简单说:MonoBehaviour 管行为,ScriptableObject 管数据。

参考文章
  • 1.概述
  • 2.ScriptableObject数据文件的创建

2. 如何创建 ScriptableObject 数据文件?

题目

创建 ScriptableObject 数据文件有哪些方式?各有什么优缺点?

深入解析

方式一:CreateAssetMenu 特性

[CreateAssetMenu(fileName = "MyData", menuName = "ScriptableObject/我的数据")]
public class MyData : ScriptableObject { }

优点:简单直观,右键菜单即可创建;适合大多数场景。

方式二:MenuItem + AssetDatabase

[MenuItem("Tools/CreateMyData")]
public static void Create()
{
    var data = ScriptableObject.CreateInstance<MyData>();
    AssetDatabase.CreateAsset(data, "Assets/MyData.asset");
    AssetDatabase.SaveAssets();
}

优点:可自定义创建逻辑、批量创建、设置默认值;适合编辑器工具开发。

答题示例

主要有两种方式:

一是用 CreateAssetMenu 特性,右键菜单直接创建,简单方便,最常用。

二是用 MenuItem 配合 AssetDatabase API,适合需要自定义创建逻辑或批量生成的场景,比如编辑器工具。

注意 AssetDatabase 是编辑器 API,相关代码要放 Editor 文件夹。

参考文章
  • 2.ScriptableObject数据文件的创建

进阶题

3. ScriptableObject 的数据在运行时能持久化吗?

题目

ScriptableObject 的数据在游戏运行时能持久化吗?如果需要持久化该怎么做?

深入解析

核心结论:ScriptableObject 在编辑模式下修改会保存到 .asset 文件,但打包运行后不会自动持久化,退出游戏数据重置。

原因:.asset 文件是只读资源,打包后被打包进 AssetBundle 或 StreamingAssets,运行时修改只在内存中生效。

解决方案:结合 Json/XML/PlayerPrefs 等持久化方案:

// 保存
string json = JsonUtility.ToJson(myData);
File.WriteAllText(Application.persistentDataPath + "/save.json", json);

// 读取
string json = File.ReadAllText(Application.persistentDataPath + "/save.json");
JsonUtility.FromJsonOverwrite(json, myData);

注意FromJsonOverwrite 覆盖现有对象,FromJson 创建新实例。

答题示例

不能。ScriptableObject 在编辑模式下修改会保存,但打包运行后只是内存中的修改,退出游戏就丢失。

如果需要持久化,可以结合 Json 或其他方案:用 JsonUtility.ToJson 序列化后写入文件,读取时用 FromJsonOverwrite 覆盖回对象。

但不建议用 ScriptableObject 做持久化,直接用 PlayerPrefs 或 Json 文件更合适。

参考文章
  • 4.ScriptableObject非持久数据
  • 5.ScriptableObject让其真正意义上的持久

4. 多个对象引用同一个 ScriptableObject 时,修改数据会有什么影响?

题目

多个对象引用同一个 ScriptableObject 数据文件时,在一个地方修改数据会有什么影响?这个特性有什么应用场景?

深入解析

行为:所有引用共享同一个实例,任何地方修改都会影响所有引用者。

// 对象A和B都引用同一个 bulletInfo.asset
// 在A中修改
bulletInfo.speed = 20;
// B中读取
Debug.Log(bulletInfo.speed);  // 输出 20,不是原值

应用场景

  1. 全局配置:修改一处,所有使用该配置的对象同步生效
  2. 内存优化:避免每个对象存储重复数据
  3. 热更新配置:运行时加载新配置,所有对象自动使用新值

注意事项:编辑模式下修改会保存到 .asset 文件,可能影响下次运行;运行时修改只在当前会话生效。

答题示例

所有引用共享同一个实例,任何地方修改都会影响所有引用者。

这个特性可以用来做全局配置,比如所有子弹的速度配置,修改一处全部生效;也能节约内存,避免每个对象存一份重复数据。

但要注意编辑模式下修改会保存到文件,可能影响下次运行。

参考文章
  • 3.ScriptableObject数据文件的使用
  • 7.ScriptableObject应用-复用数据

深度题

5. 如何设计一个 ScriptableObject 单例基类?有什么使用规则?

题目

如何设计一个 ScriptableObject 单例基类?使用时需要遵守什么规则?

深入解析

设计思路:利用泛型约束 + 静态属性 + 懒加载实现。

public class SingleScriptableObject<T> : ScriptableObject where T : ScriptableObject
{
    private static T instance;
    
    public static T Instance
    {
        get
        {
            if (instance == null)
            {
                instance = Resources.Load<T>("ScriptableObject/" + typeof(T).Name);
            }
            if (instance == null)
            {
                instance = CreateInstance<T>();
            }
            return instance;
        }
    }
}

使用规则

  1. 数据文件必须放在 Resources/ScriptableObject/ 目录下
  2. 文件名必须与类名完全一致
  3. 子类继承时泛型参数填自己:class RoleInfo : SingleScriptableObject<RoleInfo>

优点

  • 避免到处拖拽或 Resources.Load
  • 统一的获取入口,代码更简洁
  • 不存在文件时自动创建默认实例

适用场景:只用不变且全局唯一的配置数据,如角色配置、技能配置等。

答题示例

设计一个泛型基类,静态属性中懒加载:先从 Resources 加载,加载不到就 CreateInstance 创建。

使用时要遵守两个规则:数据文件放 Resources/ScriptableObject/ 目录,文件名和类名一致。

子类继承时泛型参数填自己,比如 class RoleInfo : SingleScriptableObject

这样适合管理全局唯一的配置数据,避免到处拖拽或加载。

参考文章
  • 9.ScriptableObject应用-单例模式化的获取数据

6. 如何利用 ScriptableObject 实现数据驱动的多态行为?

题目

如何利用 ScriptableObject 实现数据驱动的多态行为?举例说明应用场景。

深入解析

核心思想:定义抽象基类继承 ScriptableObject,子类实现不同行为,使用时声明基类引用,关联不同子类 .asset 实现多态。

示例:随机音效系统

// 抽象基类
public abstract class AudioPlayBase : ScriptableObject
{
    public abstract void Play(AudioSource source);
}

// 随机播放实现
[CreateAssetMenu]
public class RandomPlayAudio : AudioPlayBase
{
    public List<AudioClip> clips;
    public override void Play(AudioSource source)
    {
        source.clip = clips[Random.Range(0, clips.Count)];
        source.Play();
    }
}

// 单曲播放实现
[CreateAssetMenu]
public class SinglePlayAudio : AudioPlayBase
{
    public AudioClip clip;
    public override void Play(AudioSource source)
    {
        source.clip = clip;
        source.Play();
    }
}

// 使用:声明基类变量,关联不同子类 .asset
public class Player : MonoBehaviour
{
    public AudioPlayBase audioPlay;  // 可关联 RandomPlayAudio 或 SinglePlayAudio
    
    void PlaySound()
    {
        audioPlay.Play(GetComponent<AudioSource>());
    }
}

应用场景

  • 音效系统:随机播放、顺序播放、单曲播放
  • 物品效果:加血、加攻击、加经验等不同效果
  • AI 行为:不同数据驱动不同行为模式

优点

  • 行为与数据解耦,配置灵活
  • 新增行为只需新建子类和 .asset,无需修改使用方代码
  • 符合开闭原则
答题示例

定义抽象基类继承 ScriptableObject,声明抽象方法;子类实现具体行为并添加 CreateAssetMenu 特性。使用时声明基类变量,在 Inspector 中关联不同子类的 .asset 文件,就能实现多态。

比如音效系统:基类 AudioPlayBase 声明 Play 方法,子类 RandomPlayAudio 实现随机播放,SinglePlayAudio 实现单曲播放。使用者只管调用 audioPlay.Play(),具体行为由关联的 .asset 决定。

这种方式行为与数据解耦,新增行为不用改使用方代码。

参考文章
  • 8.ScriptableObject应用-数据带来的多态行为

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏