一、概述
1.ScriptableObject是什么
Unity提供的数据配置存储基类,可以实现道具的可视化,他是一个可以保存大量数据的数据容器,此外还支持编辑模式下的数据持久化,但游戏打包后修改数据对象,不会将其保存到本地
2.ScriptableObject的优点
- 可以直接在Inspector窗口编辑配置数据,因此可以用来做配置文件
- 可以处理重复数据,减少数据拷贝时造成的内存占用,因此可以用来做公共数据
- 可以处理数据带来的多态行为
二、Scriptableobject 数据文件
2.1 自定义Scriptableobject数据文件的创建
2.1.1 使用前提
- 自定的数据容器类需要继承Scriptableobject基类
- 在类中申明成员:这里可以声明任意类型的成员变量,但是否会在Inspector窗口编辑,需要遵循以下规则
- public修饰的基础类型、Unity内置类型、枚举、一维数组、List可以直接在Inspector窗口显示
- 非公有字段,可以通过添加 SerializeField 特性,在Inspector窗口显示
- 对于自定义结构体/类,可以通过添加 System.Serializable 特性,在Inspector窗口显示
- 对于static静态变量、const常量、stack栈、queue队列等,不允许在Inspector窗口显示
- 自定义容器中也可以存在一些方法
2.1.2 创建方法
1.为自定义数据容器类添加 CreateAssetMenu 特性,通过菜单创建资源

语法:CreateAssetMenu(fileName="默认文件名",menuName="在菜单中显示的名字",orden = 在菜单中的顺序
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 这是道具资源的创建脚本
/// </summary>
[CreateAssetMenu(fileName = "Item",menuName ="New Item")]
public class ItemSo : ScriptableObject
{
//道具的基本属性
public Image itemSprite; //道具的精灵图像
public Object itemPrefab; //道具关联的预设体
public string itemName; //道具名
public string itemInfo; //道具介绍
public int maxStackAcomunt; //最大堆叠数量
}
选中该脚本你可以看到左图所示内容,其可以配置道具的默认数据。而右图是通过该脚本创建的资源文件实例,这里可以进行配置道具信息

2.利用的静态方法创建数据对象

该方法将通过菜单栏进行创建,并保存到工程目录下
using UnityEditor;
using UnityEngine;
/// <summary>
/// 该脚本用来实现再菜单栏创建自定义资源文件
/// </summary>
public class ScriptableObjectTool
{
[MenuItem("ScriptableObject/CreateNewItem")]
public static void CreateItemSo()
{
ItemSo itemSo = ScriptableObject.CreateInstance<ItemSo>();
AssetDatabase.CreateAsset(itemSo, "Assets/ItemSos/ItemSo.asset");
AssetDatabase.SaveAssets();
//刷新页面
AssetDatabase.Refresh();
}
}
这里只会重复创建ItemSo,修改方式
using System.IO;
using UnityEditor;
using UnityEngine;
/// <summary>
/// 该脚本用来实现再菜单栏创建自定义资源文件
/// </summary>
public class ScriptableObjectTool
{
private static int i = 0;
[MenuItem("ScriptableObject/CreateNewItem")]
public static void CreateItemSo()
{
ItemSo itemSo = ScriptableObject.CreateInstance<ItemSo>();
if(File.Exists($"Assets/ItemSos/ItemSo_{i}.asset"))
{
i++;
}
AssetDatabase.CreateAsset(itemSo, $"Assets/ItemSos/ItemSo_{i}.asset");
AssetDatabase.SaveAssets();
//刷新页面
AssetDatabase.Refresh();
}
}
2.1.3 作业练习

using UnityEngine;
[CreateAssetMenu(fileName = "SettingData", menuName = "New SettingData")]
public class SettingData : ScriptableObject
{
//音乐和音效的开关
public bool musicIsOpen;
public bool soundIsOpen;
//音乐和音效的 大小
public float musicValue;
public float soundValue;
}
2.2 自定义ScriptableObject 数据文件的使用
2.2.1 ScriptableObject的使用
法一:通过Inspector中的public变量进行关联,首先要有一个数据文件实例,然后再继承Mono的类中申明变量,最后在Inspector窗口关联。
法二:通过资源加载的信息关联,Resources、AB包、Addressables都支持加载继承ScriptableObject的数据文件
注意:如果多个对象关联同一个数据文件时,他们共享的时同一个引用对象,所以修改任意一处,其他地方也会修改
2.2.2 ScriptableObject的生命周期函数
- Awake 生命周期函数,在数据文件创建时调用
- OnDestroy 生命周期函数,在ScriptableObject对象被销毁时调用
- OnDisable 生命周期函数,在ScriptableObject对象销毁或失活时调用
- OnEnable 生命周期函数,在ScriptableObject对象激活后创建时调用
- OnValidate 生命周期函数,在编辑器修改数据时调用
2.2.3 ScriptableObject的优点
1.编辑器模式下,数据持久化,即通过脚本修改数据对象中的内容,会影响数据文件的本身
2.复用数据,多个对象关联同一个数据文件时,他们会复用同一组数据,节约内存空间
2.2.4 作业练习

- 首先利用UGUI组合一个设置界面


-
其次创建脚本管理器,进行关联及管理
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;public class SettingPanel : MonoBehaviour
{
public Toggle musicToggle; //音乐开口
public Toggle soundToggle; //音效开关
public Slider musicSlider; //调正音乐的大小
public Slider soundSlider; //调整音效的大小public SettingData settingData; //设置面板关联的数据信息 void Start() { //进行初始化面板 musicToggle.isOn = settingData.musicIsOpen; soundToggle.isOn = settingData.soundIsOpen; musicSlider.value = settingData.musicValue; soundSlider.value = settingData.soundValue; //当UI面板上 控件变化时 记录数据 musicToggle.onValueChanged.AddListener((value) => { settingData.musicIsOpen = value; }); soundToggle.onValueChanged.AddListener((value) => { settingData.soundIsOpen = value; }); musicSlider.onValueChanged.AddListener((value) => { settingData.musicValue = value; }); soundSlider.onValueChanged.AddListener((value) => { settingData.soundValue = value; }); }}

2.3 非持久化数据
非持久化数据是指不管是在编辑模式还是发布后,都不会进行持久化的数据,可以根据需求自由创建数据对象使用,在使用时,会被GC
2.3.1如何生成非持久化数据:
方法:ScritptableObject.CreateInstance<数据对象类>(),该数据对象不受脚本在编辑器上的默认设置影响
该方法可以在远行时创建继承 ScriptableObject的数据对象,该对象被保存在内存中会被GC,但可以通过数据持久化相关知识进行持久化保存
2.3.2 非特久化数据对象的意义
希望在运行时只存在唯一一个数据对象,且这个数据又不太希望保存为数据资源文件浪费硬盘空间,只在运行时使用,在编辑器模式下也不会保存在本地
2.4 让数据对象持久化、
可以使用XML、Json等持久化方式实现ScriptableObject的数据持久化
2.5 Scriptableobject的应用
2.5.1 配置数据
1.ScriptableObject充当配置文件的优点:
- 配置文件的数据在游戏发布之前定规则
- 配置文件的数据在游戏运行时只会读出来使用,不会改变内容
- 可以在Unity中Inspector窗口进行配置更加的方便
2.使用举例:
配置三种道具,包含蘑菇、骨头、南瓜

2.5.2复用数据
1.使用预设体对象可能存在的内存浪费问题,如场景中存在三个Cube,这三个Cube的各属性都一样,但因为是三个实例,因此需要三个空间来分别存储这些信息,如图所示,只改变其中一个,并不影响另外两个实例

2.因此为了避免空间的浪费,可以通过关联ScriptableObject的数据对象,令这三个Cube实例的信息指向同一个空间(这里只给其中一个Cube挂上了T实例)

using System;
using System.Runtime.InteropServices;
using UnityEngine;
public class CubeCs : MonoBehaviour
{
public ItemSo itemSo;
void Start()
{
if (gameObject.name == "Cube1")
{
itemSo.maxStackAmount = 64;
}
Debug.Log("第一次运行时,itemName为" + itemSo.itemName);
Debug.Log("第一次运行时,maxStackAmount为" + itemSo.maxStackAmount);
}
}
这里我发现虽然成功复用了,但是我在第一次运行时,还是可能出现没有来的及修改的情况,如图,这是由于执行顺序导致的,这里将修改逻辑放到Awake中就好了

using System;
using System.Runtime.InteropServices;
using UnityEngine;
public class CubeCs : MonoBehaviour
{
public ItemSo itemSo;
private void Awake()
{
if (gameObject.name == "Cube1")
{
itemSo.maxStackAmount = 64;
}
}
void Start()
{
Debug.Log("第一次运行时,itemName为" + itemSo.itemName);
Debug.Log("第一次运行时,maxStackAmount为" + itemSo.maxStackAmount);
}
}
2.5.3 数据带来的多态行为
某些行为的变化是因为数据的不同带来的,因此我们可以利用面向对象的特性和原则,以及设计模式相关知识点,结合ScriptableObject是实现不同的功能,如:随机音效,物品使用,AI等等
1.使用举例:物品的随机掉落
这里正常情况下RandomItemDrop_Base应该是灌木丛的变量,首先检测玩家是否进入可交互范围,然后通过判断是否按下对应按键来进行创建道具掉落
using UnityEngine;
public abstract class RandomItemDrop_Base : ScriptableObject
{
//这是是是实现物品掉落的基类,因此他需要创建一个道具实例在地面上
public abstract void CreateItemOnGround(int index,Transform transform);
}
using UnityEngine;
/// <summary>
/// 搜索灌木丛
/// </summary>
[CreateAssetMenu(fileName = "SearchTheBushes", menuName = "RandomItenDrop/RandomItemDrop_SearchTheBushes")]
public class RandomItemDrop_SearchTheBushes : RandomItemDrop_Base
{
//搜索该灌木丛可能获得的道具
public ItemSo[] items;
public override void CreateItemOnGround(int index,Transform createPos)
{
//外界传入一个索引,当索引失败时不创建道具
if(index >= items.Length || index<0 || items[index] == null)
{
Debug.Log("索引失败,物品没有成功创建");
return;
}
GameObject.Instantiate(items[index].itemPrefab, createPos.position, Quaternion.identity);
}
}
using UnityEngine;
public class Player : MonoBehaviour
{
public RandomItemDrop_Base dropItem;
private void Update()
{
if(Input.GetKeyDown(KeyCode.Space))
{
//如果按下空格,这里就模拟搜索灌木丛
int index = Random.Range(0, 10);
Debug.Log($"按下空格,创建索引为 {index} 的物品");
dropItem.CreateItemOnGround(index,this.transform);
}
}
}

如图所示,这里第四次才创建出一个随机的道具蘑菇,配置信息如下,这里创建成功的概率为60%

2.使用举例:物品的不同使用效果
using UnityEngine;
public abstract class ItemUsageEffect_Base : ScriptableObject
{
//道具的使用效果,改变玩家状态
public abstract void ChangePlayerState(int value);
}
首先,针对不同道具,会有不同的使用效果,如增加经验或改变生命
using UnityEngine;
[CreateAssetMenu(fileName = "AddExp", menuName = "ItemUsageEffect/ItemUsageEffect_AddExp")]
public class ItemUsageEffect_AddExp : ItemUsageEffect_Base
{
public override void ChangePlayerState(int value)
{
Debug.Log("增加经验逻辑" + value);
}
}
using UnityEngine;
[CreateAssetMenu(fileName = "ChangeHp", menuName = "ItemUsageEffect/ItemUsageEffect_ChangeHp")]
public class ItemUsageEffect_ChangeHp : ItemUsageEffect_Base
{
public override void ChangePlayerState(int value)
{
Debug.Log("改变血量的逻辑" + value);
}
}
其次,给道具配置不同的效果(这里需要修改ItemSo脚本)
using UnityEngine;
/// <summary>
/// 这是道具资源的创建脚本
/// </summary>
[CreateAssetMenu(fileName = "Item",menuName ="New Item")]
public class ItemSo : ScriptableObject
{
//道具的基本属性
public Sprite itemSprite; //道具的精灵图像
public Object itemPrefab; //道具关联的预设体
public string itemName; //道具名
public string itemInfo; //道具介绍
public int maxStackAmount; //最大堆叠数量
public bool isUsable; //该道具是否可以使用
public int effectValue= 1; //使用效果值
public ItemUsageEffect_Base useEffect; //使用效果
}
其中根据道具是否可以使用,来设置对应的效果,不可以使用的道具没有效果,道具的配置如下

最后,为玩家添加测试功能,根据玩家持有的道具不同,从而实现不同的效果
using UnityEngine;
public class Player : MonoBehaviour
{
public RandomItemDrop_Base dropItem;
public ItemSo itemSo;
private void Update()
{
if(Input.GetKeyDown(KeyCode.Space))
{
//如果按下空格,这里就模拟搜索灌木丛
int index = Random.Range(0, 10);
Debug.Log($"按下空格,创建索引为 {index} 的物品");
dropItem.CreateItemOnGround(index,this.transform);
}
if(Input.GetKeyDown(KeyCode.F))
{
//按下F模拟使用道具
if(itemSo.isUsable&& itemSo.useEffect != null)
{
//可以正常使用道具
itemSo.useEffect.ChangePlayerState(itemSo.effectValue);
}
else
{
Debug.Log("道具使用失败");
}
}
}
}

2.5.4 单例模式化的数据获取
1.思考:
这里我想,这种单例不应该会占用空间,然后一直保存着吗,所以我感觉单例模式不会经常使用,比如我上次的物品道具及物品栏的功能实现中,我的物品是ItenSo,我只是关联了ItenSo,而且这些使用相同的数据信息就会使用同一个空间,也不会有空间的浪费,然后对于引用public 关联,也只会出现几次。
后面我的代码复杂了,然后我就经常分不清命名了,就比如我的任务系统,里面涉及到奖励发放,逻辑就是从任务中拿出所有奖励如图,这里reward.reward就是ItemSo类型的数据信息,而reward.reward.realItem1是该信息关联的预设体,所以我想单例模式在一定程度应该可以减少这种情况,比如一些常用的信息数据,但对于一些使用次数少的就会很浪费

2.回答:
这里我找了AI,他是这样给我解释的

3.实现单例的方法
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SingleScriptableObject<T> :ScriptableObject where T:ScriptableObject
{
//这里是基类,真正使用的是继承了SingleScriptableObject<T> 类的子类
private static T instance;
public static T Instance
{
get
{
if (instance == null)
{
//第一次使用时,为null,去指定的路径下加载资源文件
instance = Resources.Load<T>("ScriptableObject/" + typeof(T).Name);
}
//没有找到,则创建一个默认的返回
if(instance==null)
{
instance = CreateInstance<T>();
}
return instance;
}
}
}