10.总结
10.1 知识点

学习的主要内容

总结讲什么

优点

何时使用ScriptableObject

注意

10.2 核心要点速览
ScriptableObject 是什么
一种用于存储大量数据的数据容器类,继承自 ScriptableObject 基类。
主要特点:
- 数据复用:多个对象可以共享同一份数据,节约内存空间
- 配置文件:可以直接在 Unity 的 Inspector 窗口进行配置,不需要第三方软件(如 Excel、XML 编辑器)
- 编辑器持久化:在编辑模式下修改数据会保存到 .asset 文件中,但打包运行后不会自动持久化
适用场景:
- 只用不改的数据(如角色配置、技能数据)
- 需要在多处复用的数据
- 需要频繁配置的数据
自定义 ScriptableObject 数据容器
基本步骤
- 继承
ScriptableObject类 - 在类中声明成员变量和方法
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 文件更合适。
应用场景一:配置数据
为什么非常适合做配置文件
- 配置文件的数据在游戏发布之前定规则
- 配置文件的数据在游戏运行时只会读出来使用,不会改变内容
- 在 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 具备自己的优点:
- 更方便的配置
- 共享数据节约内存
应用场景四:单例模式化的获取数据
为什么要单例模式化
对于只用不变并且要复用的数据(比如配置文件中的数据),往往需要在很多地方获取它们。
问题:
- 如果直接通过
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 本身不支持 |
注意事项总结
编辑器持久化 vs 运行时持久化:ScriptableObject 在编辑模式下修改会保存,但打包运行后不会自动持久化
多引用共享:多个对象关联同一个 .asset 文件时共享同一实例,任何地方修改都会影响所有引用
非持久化数据特点:通过
CreateInstance创建的对象不会继承脚本中设置的默认关联不建议做数据持久化:ScriptableObject 的设计初衷是配置和复用数据,用 Json/PlayerPrefs 做持久化更合适
单例基类规则:数据文件放在
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,不是原值
应用场景:
- 全局配置:修改一处,所有使用该配置的对象同步生效
- 内存优化:避免每个对象存储重复数据
- 热更新配置:运行时加载新配置,所有对象自动使用新值
注意事项:编辑模式下修改会保存到 .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;
}
}
}
使用规则:
- 数据文件必须放在
Resources/ScriptableObject/目录下 - 文件名必须与类名完全一致
- 子类继承时泛型参数填自己:
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