zoukankan      html  css  js  c++  java
  • ScriptableObject

    我竟然最近两天才知道Unity中ScriptableObject的存在……

    看了下ScriptableObject的一些介绍,最大的优点感觉有三点:

    • 把数据真正存储在了资源文件中,可以像其他资源那样管理它,例如退出运行也一样会保持修改
    • 可以在项目之间很好的复用,不用再制作Prefab那样导入导出
    • 在概念上有很好的fit,强迫症患者的福音

    看了下感觉有很多东西都可以用它。之前的做法一般都是[Serializable]一个class,然后在面板里配数据,做成prefab,但这种方法没有上面的三个优点。感觉今后如果有类似通过Serializable + Class + Prefab实现存储数据的想法的时候,都应该先考虑下能不能用ScriptableObject做成一个真正的资源文件。

    当然了,ScriptableObject还有很多可以应用的地方,例如跟多态结合起来可以做各种特效、音效、技能、对话的资源配置文件。当然不用ScriptableObject也是可以完成这些需求的,感觉ScriptableObject提供了一种更优雅的实现方法。

    下面的内容主要参考以下资料:

    MonoBehaviour Tyranny

    这里写图片描述

    为什么某些情况下使用MonoBehaviour很不好:

    • 运行时刻修改了数据一退出就全部丢失了。 
      • 这个深有感触,目前都是靠Copy Component Values来解决,很麻烦。其实有这样的需求的时候大部分就说明这个脚本存储的是很多数据,就应该考虑使用ScriptableObject,而不是MonoBehaviour。说到底是因为这些对象不是Assets
    • 当实例化新的对象的时候,这个MonoBehaviour也在内存中多了一份实例,浪费空间
    • 在场景和项目之间很难共享
    • 在概念上就很难定义这种对象,明明是为了存储一些数据和设置等,但却要作为一个Component附着在Gameobject或Prefab上,不能独立存在

    为什么使用C#的statics也无法解决这个问题:

    • 一旦退出运行仍然会重置所有数据
    • 需要自己进行serialsation,而且不容易引用其他Unity对象(因为有Static限制)
    • 显示面板也需要我们自己实现,很麻烦

    为什么Prefabs也不行:

    • Prefab的确可以在项目和场景之间贡献,但很容易被搞得乱七八糟,我们只需要实例化一个prefab,然后就可以随意更改数据了
    • 会有额外的一些Component,但其实我们只是想要存储数据而已,这些没有任何意义
    • 仍然在概念上不能更好的fit

    ScriptableObject是我们的rescue!

    • 在内部实现上它仍然继承自MonoBehaviour,但它不必附着在某个对象上作为一个Component
    • 我们也不能(当然初衷就是不愿意)把它赋给Gameobject或Prefab
    • 可以被serialised,而且可以自动有类似MonoBehavior的面板,很方便
    • 可以被放到.asset文件中,也就是说我们可以自定义asset的类型。Unity内置的asset资源有材质、贴图、音频等等,现在依靠ScriptableObject我们可以自定义新的资源类型,来存储我们自己的数据
    • 可以解决某些多态问题
    这里写图片描述

    ScriptableObject是如何解决我们的问题的:

    • ScriptableObject的数据是存储在asset里的,因此它不会在退出时被重置数据,这类似Unity里面的材质和纹理资源数据,我们在运行时刻改变它们的数值就是真的改变了
    • 这些资源在实例化的时候是可以被引用,而非复制
    • 类似其他资源,它可以被任何场景引用,即场景间共享
    • 在项目之间共享
    • 没有其他多余的东西,例如多余的Component

    当然ScriptableObject也有一些缺点:

    • 很少的回调函数 
      • OnEnable
      • OnDisable
      • OnDestroy
    • 真正意义上的共享,因此一旦修改数据都真的修改了

    总结:其实说明白点,ScriptableObject的优点和缺点都是因为它表现起来就像一个类似材质、纹理等类型的资源,存在于Assets文件夹下,只有唯一实例

    如何使用

    非常简单,只需要把平时的继承自MonoBehaviour改成ScriptableObject即可:

    基本使用

    using UnityEngine;
    
    [CreateAssetMenu(menuName="MySubMenue/Create MyScriptableObject ")]
    public class MyScriptableObject : ScriptableObject
    {
        public int someVariable;
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    其中,CreateAssetMenu可以让我们在资源创建菜单中添加创建这个ScriptableObject的选项,类似创建脚本、材质等其他资源。

    我们也可以在脚本中动态创建一个ScriptableObject:

    ScriptableObject.CreateInstance<MyScriptableObject >();
    • 1

    这会在内存中创建一个新的实例,用作临时修改等用途,然后在不使用的时候可以让GC回收。

    回调函数

    这里写图片描述

    create可以是从脚本中被创建,当有其他对象引用该ScriptableObject时它会被load。

    生命周期

    这里写图片描述

    其实ScriptableObject的生命周期和其他资源都是类似的:

    • 当它是被绑定到.asset文件或者AssetBundle等资源文件中的时候,它就是persistent的,这意味着 
      • 它可以通过Resources.UnloadUnusedAssets来被unload出内存
      • 可以通过脚本引用或其他需要的时候被再次load到内存
    • 如果是通过CreateInstance<>来创建的,它就是非persistent的,这意味着 
      • 它可以通过GC被直接destroy掉(如果没有任何引用的话)
      • 如果不想被GC的话,可以使用HideFlags.HideAndDontSave

    什么时候使用

    下面介绍一些常见的应用场景。

    Data Objects和Tables

    第一种最常见的就是数据对象和表格数据,我们可以在Assets下创建一个.asset文件,并在编辑器面板中编辑修改它,再提交这个唯一的一份资源给版本控制器。例如,本地化数据、清单目录、表格、敌人配置等(这些真的非常常见,目前我接触过的大部分都是通过json、xml文件或是Monobehaviour来实现的,json和xml文件对策划并不友好,Monobehaviour的问题前面就说过了)。

    一个例子:

    class EnemyInfo : ScriptableObject {
        public int MaximumHealth;
        public int DamagePerMeleeHit;
    }
    • 1
    • 2
    • 3
    • 4

    记住,ScriptableObject的目的是只有一份,因此这里面不应该包括一些会根据实例不同而变化的数值。例如,我们在这个例子里没有声明敌人的生命值等变量,这是因为不同的敌人的生命值可能是不同的,这些属性应该在相应的MonoBehaviour里定义。

    然后,我们就可以在真正的MonoBehaviour脚本中声明对ScriptableObject的引用:

    class Enemy : MonoBehaviour {
        public EnemyInfo info;
    }
    • 1
    • 2
    • 3

    这保证所有的Enemy都会引用到同一个ScriptableObject对象。

    Dual Serialisation

    使用ScriptableObject的一个好处是你不需要考虑序列化的问题,但是我们也可以和Json这些进行配合(使用JsonUtility),既支持直接在编辑器里创建ScriptableObject,也支持在运行时刻通过读取Json文件来创建。例子是,内置 + 用户自定义的场景文件,我们可以在编辑器里设计一些场景存储成.asset文件,而在运行时刻玩家可以自己设计关卡存储在Json文件里,然后可以据此生成相应的ScriptableObject。

    一个例子:

    [CreateAssetMenu]
    class LevelData : ScriptableObject { ... }
    
    LevelData LoadLevelFromFile(string path) {
        string json = File.ReadAllText(path);
        LevelData result = CreateInstance<LevelData>();
        JsonUtility.FromJsonOverwrite(result, json);
        return result;
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    JsonUtility.FromJsonOverwrite会使用Json文件中的数据来更新LevelData。

    Reload-proof Singleton

    我们经常会需要一个可以在场景间共享的Singleton对象,有时候我们就可以使用ScriptableObject + static instance variable的方法来解决,当场景变换的时候,我们可以使用Resources.FindObjectsOfTypeAll<>来找到已有的instance(当然这需要在实例化第一个instance的时候把它标识为instance.hideFlags = HideFlags.HideAndDontSave)。一个例子就是游戏状态和游戏设置。

    一个例子:

    class GameState : ScriptableObject {
        public int lives, score;
        private static GameState _instance;
        public static GameState Instance {
            get {
                if (!_instance) {
                    // 如果为空,先试着从Resource中找到该对象
                    _instance = Resources.FindObjectOfType<GameState>();
                }
                if (!_instance) {
                    // 如果仍然没有,就从默认状态中创建一个新的
                    // CreateDefaultGameState函数可以是从JSON文件中读取,并且在实例化完后指明_instance.hideFlags = HideFlags.HideAndDontSave
                    _instance = CreateDefaultGameState();
                }
                return _instance;
            }
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    Delegate Objects

    ScriptableObject除了可以存储数据外,我们还可以在ScriptableObject中定义一些方法,MonoBehaviour会把自身传递给ScriptableObject中的方法,然后ScriptableObject再进行一些工作。这类似于插槽设计模式,ScriptableObject提供一些槽,MonoBehaviour可以把自己插进去。适用于AI类型、加能量的buff或debuffs等

    这种用法大概是最常见的。首先看一个加能量的例子(来源Unite 2016 Europe)。

    一个例子:

    abstract class PowerupEffect : ScriptableObject {
        public abstract void ApplyTo(GameObject go);
    }
    
    [CreateAssetMenu]
    class HealthBooster : PowerupEffect {
        public int Amount;
        public override void ApplyTo(GameObject go) {
            go.GetComponent<Health>().currentValue += Amount;
        }
    }
    
    class Powerup : MonoBehaviour {
        public PowerupEffect effect;
        public void OnTriggerEnter(Collider other) {
            effect.ApplyTo(other.gameObject);
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    关于我

    我叫乐乐,程序媛一枚,就读于上海交通大学软件学院,研究生,数字媒体方向。喜欢用计算机来绘制各种五彩缤纷的画面~欢迎访问我的独立博客作品集 :)

    邮件:lelefeng1992 # gmail DOT com
    PS:为防止垃圾邮件,请自行转换为正确格式哦~


     
  • 相关阅读:
    eclipse svn插件
    eclipse 图片预览插件
    eclipse properties文件插件
    eclipse Failed to load the JNIshared library
    Spark MLlib之线性回归源代码分析
    ul,li不能左右居中的问题
    【日常学习】【搜索/递归】codevs2802 二的幂次方题解
    UVa 112
    查询一个月最后一天的总用户数,数据库中没有保存最好一天的数据,就查询本月数据库已存有的最后一天的数据
    uva:10763
  • 原文地址:https://www.cnblogs.com/cyct/p/10681092.html
Copyright © 2011-2022 走看看