ScripableObject概述

ScripableObject是Unity提供的一个数据配置存储基类。用来保存大量数据的数据容器。

它的主要作用:

数据复用(多个对象用同一个数据)

比如一个子弹对象,以前我们通过面向对象的思想去做的话,会写一个继承MonoBehavior脚本,相关的属性都会在这里声明,然后挂载到子弹模型预设体上,这样每次实例一个子弹,如果数据不变,那么对内存来说,是有有一定浪费的。因为每一个子弹预设体上都有该脚本,那么所有的属性都会分配一次内存。通过使用ScriptableObject可以有效避免内存的浪费。

配置文件(配置游戏中的数据)

编辑模式下的数据持久化

注意的是,在发布运行时ScriptableObject并不具备持久化特性(修改数据对象,并不会保存本地)

ScriptableObject数据文件的创建

如果自定义ScriptableObject数据容器模板

  1. 继承ScriptableObject类

  2. 在该类中声明成员(变量、方法等)

注意:声明后,我们就可以在Inspector窗口中看到变化

我们可以在其中进行设置,但是这些设置都是默认数据,并没有真正使用他们

这些关联信息都是通过脚本文本对应的Unity配置文件meta进行记录的

而这个数据只是一个数据容器模板,有了它我们之后才能根据它的信息创建对应的数据资源文件

using UnityEngine;

namespace Core
{
    public class MyData : ScriptableObject
    {
        public int _int;
        public float _float;
        public Vector2 _vector2;
        public Vector3 _vector3;
        public Vector4 _vector4;
        public Color _color;
        public Material _material;
        public GameObject _gameObject;
    }
}

根据数据容器模板创建数据文件

方法一:在挂载继承了ScriptableObject类上添加代码[CreateAssetMenu(fileName = "New Core Data", menuName = "Core Data")]

方法二:制作一个生成工具类

ScriptableObject数据文件的使用

使用ScriptableObject

方法一:通过Inspector中的public变量进行关联

创建一个数据文件,然后在继承MonoBehavior类中声明数据容器类型的成员,在Inspector窗口进行管理

using System;
using UnityEngine;

namespace Core
{
    public class Test : MonoBehaviour
    {
        // 在Inspector面板拖拽数据文件
        public MyData data;

        private void Start()
        {
            print(data._int);
            print(data._float);
            print(data._vector2);
            print(data._vector3);
            print(data._color);
        }
    }
}

方法二:通过资源加载获取

using UnityEngine;

namespace Core
{
    public class Test : MonoBehaviour
    {
        private MyData _data;

        private void Start()
        {
            _data = Resources.Load<MyData>("ScriptableObjects/my");
            
            print(_data._int);
            print(_data._float);
            print(_data._vector2);
            print(_data._vector3);
            print(_data._color);
        }
    }
}

注意:如果多个对象关联同一个数据容器文件,他们共享的是一个对象(因为是引用对象),在其他任何地方修改后,其他地方也会发生改变。

ScriptableObject的生命周期函数

using System;
using UnityEngine;

namespace Core
{
    [CreateAssetMenu(fileName = "New Core Data", menuName = "Core Data")]
    public class MyData : ScriptableObject
    {
        public int _int;
        public float _float;
        public Vector2 _vector2;
        public Vector3 _vector3;
        public Vector4 _vector4;
        public Color _color;
        public Material _material;
        public GameObject _gameObject;

        private void Awake()
        {
            Debug.Log("数据文件创建时调用");
        }

        private void OnDestroy()
        {
            Debug.Log("对象将被销毁时调用");
        }

        private void OnDisable()
        {
            Debug.Log("对象销毁时,即将重新加载及哦啊本程序集时调用");
        }

        private void OnValidate()
        {
            Debug.Log("编辑器才会调用的函数,Unity在加载脚本或者Inspector窗口中更改值时调用");
        }
    } 
}

ScriptableObject好处的体现

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

复用数据:如果多个对象关联一个数据文件,修改这个数据文件,所有对象获得的值也会改变。

ScriptableObject非持久化数据

如果我们想要不管在编辑器模式上还是发布后都不会持久化数据(相当于在内存中创建,程序结束后会被GC回收),我们可以利用代码动态生成数据。

using UnityEngine;
namespace Core
{
    public class Test : MonoBehaviour
    {
        private MyData _data;

        private void Start()
        {
            _data = ScriptableObject.CreateInstance<MyData>();
        }
    }
}

ScriptableObject让其真正意义的持久

使用Json结合ScriptableObject存储数据

在这里我使用LitJson来读取和存储数据,所以需要引入LitJson包(可以看我之前写的持久化数据)

using UnityEngine;

namespace Core
{
    public class Test : MonoBehaviour
    {
        private MyData _data;

        private void Start()
        {
            _data = Resources.Load<MyData>("ScriptableObjects/my");
            JsonManager.Instance.SaveData(_data, "mydata");
            Debug.Log(Application.persistentDataPath);
        }
    }
}

对应的JSON数据

{
    "name": "my",
    "hideFlags": 0,
    "_int": 40,
    "_float": 2,
    "_bool": true,
    "_string": "1321",
    "_floats": [
        0,
        1,
        2,
        3
    ]
}

使用Json结合ScriptableObject读取数据

using UnityEngine;

namespace Core
{
    public class Test : MonoBehaviour
    {
        private MyData _data;

        private void Start()
        {
            _data = JsonManager.Instance.LoadData<MyData>("mydata");
            print(_data._bool);
            print(_data._int);
            print(_data._float);
            print(_data._string);
            foreach (var item in _data._floats)
            {
                print(item);
            }
        }
    }
}

ScriptableObject的应用

配置文件

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

如何制作?接下来我们模拟物品的配置文件

using System;
using System.Collections.Generic;
using UnityEngine;

namespace Core
{
    [CreateAssetMenu(fileName = "Items", menuName = "ScriptableObject/创建物品", order = 1)]
    public class Items : ScriptableObject
    {
        [Serializable]
        public class Item
        {
            public int Id;
            public string Name;
            public string Description;
            public string IconRes;
            public bool CanUse;
            public bool CanFall;
        }

        public List<Item> items;
    }
}

创建完,我们就可以使用了

using UnityEngine;

namespace Core
{
    public class Test : MonoBehaviour
    {
        private Items _items;

        private void Start()
        {
            _items = Resources.Load<Items>("ScriptableObjects/Items");
            foreach (Items.Item item in _items.items)
            {
                print(item.Id);
                print(item.Name);
                print(item.Description);
                print(item.CanFall);
                print(item.CanUse);
                print(item.IconRes);
            }
            
        }
    }
}

注意:

配置数据是只用不改

数据复用

以一个子弹为例,通常我们会制作一个子弹预设体,子弹下面会挂载一个脚本,每个子弹都会有攻击力和移动速度,而且不会修改这些数据,就会造成内存占用。

using System;
using UnityEngine;

namespace Core
{
    public class Bullet : MonoBehaviour
    {
        public int atk;
        public float moveSpeed;

        private void Update()
        {
            this.transform.Translate(transform.forward * moveSpeed * Time.deltaTime);
        }
    }
}

为了让多个子弹共用一个攻击力和移动速度,就可以使用我们的ScriptableObject

首先我们定义一下共用的脚本逻辑

using UnityEngine;

[CreateAssetMenu(fileName = "bulletInfo", menuName = "ScriptableObject/Bullet Info")]
public class BulletInfo : ScriptableObject
{
    public int atk;
    public float moveSpeed;
}

接着子弹脚本

using System;
using UnityEngine;

namespace Core
{
    public class Bullet : MonoBehaviour
    {
        public BulletInfo info;

        private void Update()
        {
            this.transform.Translate(transform.forward * (info.moveSpeed * Time.deltaTime));
        }
    }
}

这样我们就可以是实现复用。如果修改BulletInfo 里面的值,那么所有的子弹的攻击力和移动速度都会跟着改变。

数据带来的多态行为

某些行为的变化时因为数据的不同带来的

我们可以利用面向对象的特性和原则,以及设计模式相关知识点结合ScriptableObject做出更加方便的功能

比如:物体拾取后给玩家带来不同的效果。

我们准备一个物品拾取效果的基类

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

然后制作一个加血的类和加攻击力的类

[CreateAssetMenu]
public class AddHPEffect : ItemEffect
{
    public int num;
    public override void AddEffect(GameObject player)
    {
        // 模拟玩家加血
    }
}

[CreateAssetMenu]
public class AddAtkEffect : ItemEffect
{
    public int atk;
    public override void AddEffect(GameObject player)
    {
        // 模拟玩家加攻击力
    }
}

接下来我们就需要手动创建资源文件,然后使用它。什么物品触发什么效果,直接拖拽就可以了。

public class ItemInfo : MonoBehaviour
{
    public ItemEffect itemEffect;

    private void OnTriggerEnter(Collider other)
    {
        itemEffect.AddEffect(other.gameObject);
    }
}

以上就是多态带来的好处。

单例模式化获取数据

我们之前获取资源文件是通过public关联(拖拽)或者动态加载,如果在多处使用,会存在很多重复代码,效率低下,如果我们将此类数据通过单例模式化去获取,可以提高效率,减少代码量。

所以我们可以实现一个ScriptableObject数据单例模式基类,让我们只需要让子类继承该基类,就可以直接获取到数据,而不再需要通过public关联和资源动态加载 了

using UnityEngine;

namespace Core.Utility
{
    public abstract class SingleScriptableObject<T> : ScriptableObject where T : ScriptableObject
    {
        private static T _instance;

        public static T Instance
        {
            get
            {
                if (_instance == null)
                {
                    // 自定义规则(如何获取)
                    // 1. ScriptableObject资源文件放在Resource/ScriptableObject文件下
                    // 2. 类名作为资源文件名
                    _instance = Resources.Load<T>("ScriptableObjects/" + typeof(T).Name);
                    if (_instance == null)
                    {
                        _instance = CreateInstance<T>();
                    }
                }

                return _instance;
            }
        }
    }
}

那么我们如何使用呢?我们可以修改上文中物品拾取效果类来作为例子

public abstract class ItemEffect : SingleScriptableObject0<ItemEffect>
{
    public abstract void AddEffect(GameObject player);
}

[CreateAssetMenu]
public class AddHPEffect : ItemEffect
{
    public int num;
    public override void AddEffect(GameObject player)
    {
        // 模拟玩家加血
    }
}

[CreateAssetMenu]
public class AddAtkEffect : ItemEffect
{
    public int atk;
    public override void AddEffect(GameObject player)
    {
        // 模拟玩家加攻击力
    }
}
public class ItemInfo : MonoBehaviour
{
    // 删除public关联的,直接通过单例模式去获取
    // public ItemEffect itemEffect; 
    private void OnTriggerEnter(Collider other)
    {
        AddHPEffect.Instance.AddEffect(other.gameObject);
    }
}