Unity学习_ScriptableObject

一、概述

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 作业练习

  1. 首先利用UGUI组合一个设置界面
  1. 其次创建脚本管理器,进行关联及管理

    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;
        }
    }
}
相关推荐
MartinYeung52 小时前
[论文学习]LLM 与其他 AI 模型的隐私考量:输入与输出隐私框架方法
人工智能·学习
Thomas_YXQ2 小时前
Unity无GC读取图片与网格完整方案
大数据·人工智能·unity·微信·产品运营
(●—●)橘子……3 小时前
力扣第503场周赛练习理解
python·学习·算法·leetcode·职场和发展·周赛
AOwhisky4 小时前
MySQL 学习笔记(第一期):数据库基础与 MySQL 初探
运维·数据库·笔记·学习·mysql·云计算
try2find5 小时前
Agent学习之补充my_plan_solve_agent
学习
想你依然心痛6 小时前
HarmonyOS 6(API 23)实战:打造“光码智学舱“——AI编程学习新范式
学习·ar·ai编程·harmonyos·智能体
郝学胜-神的一滴6 小时前
中级OpenGL教程 008:精准控制高光光斑大小与强度
c++·unity·godot·three.js·图形学·opengl·unreal
一口吃俩胖子7 小时前
【脉宽调制DCDC功率变换学习笔记023】渐进分析法
笔记·学习
m0_377108147 小时前
pid学习
学习