ScriptableObject

什么是 ScriptableObject

  • ScriptableObject 是 Unity 提供的一个数据配置存储基类,它是一个可以用来保存大量数据的数据容器,我们可以将它保存为自定义的数据资源文件。
  • ScriptableObject 是一个类似 MonoBehaviour 的基类,继承自 UnityEngine.Object 。要想使用它,需要我们写个脚本去继承 ScriptableObject 。需要注意的是,继承自 SctiptableObject 的脚本无法挂载到游戏物体上,毕竟它不是继承自 MonoBehaviour。
  • ScriptableObject 类的实例会被保存成资源文件(.asset文件),和预制体,材质球,音频文件等类似,都是一种资源文件,存放在 Assets 文件夹下,创建出来的实例也是唯一存在的。

ScriptableObject 的主要作用

大体上可以分成三点:
1) 编辑模式下的数据持久化
2) 配置文件 (配置游戏中的数据)
3) 数据复用 (多个对象共用一套数据)

编辑模式下的数据持久化

当我们在编辑模式下修改了继承自 ScriptableObject 对象的数据文件内容时,修改的数据将被保存到磁盘上。

但是在发布运行后,即使在游戏中修改了 ScriptableObject 的数据,改后的数据并不会保存在本地,重新打开运行时数据并还是配置的初始数据

因此 ScriptableObject 适合在编辑模式下调试数据,但不适合存储在游戏打包发布后的运行期间会改变的数据

配置文件

ScriptableObject非常适合用来做配置文件。因为:
1)配置文件的数据在游戏发布之前就定好了规则
2)配置文件的数据在游戏运行时只会读出来使用,不会修改数据的内容
3)传统的配置文件一般会通过xml、json、excel等方式来配置游戏数据
,相对来说都是在 Unity 外部通过其它格式的文件对数据进行配置。
而通过 ScriptableObject 我们可以直接在 Unity内部的 Inspector 面板中进行数据的配置,有时候会更加方便。

数据复用

对于只用不变的数据,通过使用 ScriptableObject 可以有效避免内存的浪费,因为它将共用的数据单独抽离出来,供相同的一类对象使用。

对于生成子弹问题
1) 每生成一个子弹就会对原来的预制体进行拷贝,其挂载的 Bullet 数据脚本同样会被拷贝,因此同样的数据会被拷贝多次。而我们规定同一种子弹的数据是相同的,也就是同一种子弹,不管生成了多少个,它们都共用一套数据。因此,这种方法会创建多余的数据脚本,造成内存浪费。
2) 如果预制体上的脚本丢失,之前在 Inspector 面板中配置的数据也会消失。

不过解决方法有很多,比如:

  • 我们可以创建一个全局的数据管理中心脚本,通过静态变量去调用每种子弹的数据。不过这种数据配置方式不能实现数据的持久化,而且必须要打开代码文件进行修改,面对茫茫代码可能不是那么直观。
  • 我们还可以用上像 excel,Json,xml 等持久化数据存储的方法,结合 Unity 对准备好的数据文件进行数据读写。这么做的好处是可以实现数据的持久化,比如在游戏过程中修改了数据,退出游戏后下一次打开游戏使用的就是之前修改过的数据。
  • 还是利用前言中举的例子,比如一个子弹对象,通过面向对象的思想,会写一个继承自 MonoBehaviour 的脚本,声明相关的属性,然后挂载到子弹预设体上,把子弹需要的数据赋给子弹对象。如果我们要求子弹的数据是不会改变的,那么这样每次实例化一个子弹,对内存来说会造成一定的浪费,因为每次生成一个子弹都会复制 Assets 下子弹预制体的值,也就是多次复制了相同的数据。

这里每个子弹预制体中的子弹脚本都只是持有 Bullet Data 这一个 ScriptableObject 实例的引用,真正在内存当中分配空间的只有红线所指向的 ScriptableObject 实例,也就是我们的数据资源文件。

创建sb

创建资源文件

既然要创建资源文件,大家其实可以类比创建材质、预制体,它们也是一种资源文件。要想创建它们,我们只要在编辑器中的 Asset 目录或者它的子目录中按下鼠标右键(或者直接点击编辑器最上方菜单栏的 Assets),然后点击 Create,找到我们想要创建的资源就行了。

1
2
3
4
5
6
7
8
9
10
//fileName 表示数据资源文件创建出来的文件名。
//menuName 表示在 Assets/Create 下的名字。
//order 表示在 Assets/Create 下的位置顺序。

[CreateAssetMenu(fileName = "BulletData", menuName = "ScriptableObject/子弹数据", order = 0)]
public class BulletData : ScriptableObject
{
public float speed;
public float damage;
}

静态方法创建数据对象

利用ScriptableObject的静态方法创建数据对象,然后将数据对象保存在工程目录下

可以新建一个脚本(可以不用继承自 MonoBehavoiur,这个脚本不用挂载到游戏物体上),引入 UnityEditor 命名空间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using UnityEngine;
using UnityEditor;
public class ScriptableObjectTool
{
[MenuItem("ScritableObject/CreateMyData")]
public static void CreateMyData()
{
//创建数据资源文件
//泛型是继承自ScriptableObject的类
BulletData asset = ScriptableObject.CreateInstance<BulletData>();
//前一步创建的资源只是存在内存中,现在要把它保存到本地
//通过编辑器API,创建一个数据资源文件,第二个参数为资源文件在Assets目录下的路径
AssetDatabase.CreateAsset(asset, "Assets/Resources/ScriptableObject/BulletData.asset");
//保存创建的资源
AssetDatabase.SaveAssets();
//刷新界面
AssetDatabase.Refresh();
}
}

注意:

  • 使用这种方法无需在继承自 ScriptableObject 的类上增加 CreateAssetMenu 特性。
  • 刚刚创建的 ScriptableObjectTool 脚本需要放在 Assets 文件夹下任一位置的 Editor 文件夹下(这个文件夹放哪都行,看自己需求,只要在 Assets 文件夹或其子文件夹下就好)。因为我们引入了 UnityEditor 命名空间,这意味着这个脚本只在编辑模式下会用到,实际打包发布后是不会用到的。如果没放在 Editor 文件夹下,Unity 打包时会认为此脚本是会被一起打包,作用于游戏运行期间,与 Editor 命名空间的性质相矛盾,所以会报错。(这里就涉及到一些扩展编辑器的知识)

如何使用 ScriptableObject

数据文件的使用

  • 通过 Inspector 面板中的 public 变量进行关联

    • 步骤一:创建一个数据文件

    • 步骤二:在继承自 MonoBehaviour 类中声明数据容器类型的成员,在 Inspector 面板中进行关联(拖拽的是数据文件而不是继承自ScriptableObject 类的脚本)

  • 直接加载数据资源文件
    • 可以用 Resources,AddressBundle,Addressables 等方式加载数据资源文件。

生命周期函数

ScriptableObject 和 MonoBehaviour 类似,也存在生命周期函数,但是数量会少很多。

1
2
3
4
5
Awake 数据文件创建时调用
OnDestroy 对象将被销毁时调用
OnEnable 创建或加载对象时调用
OnDisable 对象销毁时,即将加载脚本程序集时调用
OnValidate 编辑器才会调用的函数,Unity在加载脚本或者Inspector面板中更改值时调用

实现非持久化数据

其实,对于某些数据资源,我们不一定要将数据保存为磁盘中的资源文件占据空间,而只希望运行期间在内存中临时生成一组共用的数据给对象使用就够了,退出游戏后就释放掉生成的数据资源。这个时候不论是在编辑模式还是打包发布后,数据都是非持久化的,也就是改动的数据不会被保存到磁盘中而是内存中。退出游戏后重新打开,读取的还是初始配置的数据。

  • 如何生成非持久化数据?

利用 ScriptableObject 类中的静态方法 CreateInstance<>() 。该方法可以在运行时创建出指定继承自 ScriptableObject 的对象,该对象只存在于内存中,可以被GC垃圾回收,调用一次就创建一次。

1
2
3
4
5
public BulletData bulletData;
void Start(){
//通过这种方式创建的数据对象,它里面的默认值不会受到脚本中设置的影响
bulletData=ScriptableObject.CreateInstance<BulletData>();
}

单例模块获取数据

之前介绍使用数据文件的时候,要么是通过声明 public 变量在 Inspector 面板中进行拖拽关联,要么是使用资源加载的方法。

如果用拖拽的方式,物体之间的拖拽关系可能会随着项目量的增长而变复杂,不利用后续的维护。😟
如果用资源加载的方式,以 Resources 为例,可能就要写大量的 Resources.Load 方法,其实是有一点重复工作的。😟

因此可以将 ScriptableObject 实例通过单例模式化去获取,减少重复代码,提高编码效率:

SingleScriptableObject

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
public class SingleScriptableObject<T> : ScriptableObject where T :ScriptableObject
{
//所有数据资源文件都放在Resources文件夹下加载对应的数据资源文件
//对需要复用的唯一的数据资源文件名定一个规则:文件名和类名一致
private static string scriptableObjectPath = "ScriptableObject/"+typeof(T).Name;
private static T instance;
public static T Instance
{
get
{
if (instance == null)
{
//如果为空,首先应该去资源路径下加载对应的数据资源文件
instance = Resources.Load<T>(scriptableObjectPath);
}
//如果没有这个文件,直接创建一个数据
if (instance == null)
{
instance = CreateInstance<T>();
}
return instance;
}
}
}


TestData

1
2
3
4
5
6
7
//这个TestData,去创建出一个名为TestData的asset,然后可以直接在其他类里点出来TestData.GetInstance().i
[CreateAssetMenu]
public class TestData : SingleScriptableObject<TestData>
{
public int i;
public bool b;
}

调用

1
2
3
4
5
6
7
8
void Start()
{
print(RoleInfo.Instance.roleList[0].id);
print(RoleInfo.Instance.roleList[1].tips);

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

参考链接

https://blog.csdn.net/qq_32175379/article/details/125704476?spm=1001.2014.3001.5502

Unity进阶:ScriptableObject使用指南-CSDN博客