【Unity】ScriptableObject的介绍

【Unity】ScriptableObject的介绍

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

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

看了下感受有不少东西均可以用它。以前的作法通常都是[Serializable]一个class,而后在面板里配数据,作成prefab,但这种方法没有上面的三个优势。感受从此若是有相似经过Serializable + Class + Prefab实现存储数据的想法的时候,都应该先考虑下能不能用ScriptableObject作成一个真正的资源文件。设计模式

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

下面的内容主要参考如下资料:编辑器

MonoBehaviour Tyranny

这里写图片描述

为何某些状况下使用MonoBehaviour很很差:ide

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

为何使用C#的statics也没法解决这个问题:svg

  • 一旦退出运行仍然会重置全部数据
  • 须要本身进行serialsation,并且不容易引用其余Unity对象(由于有Static限制)
  • 显示面板也须要咱们本身实现,很麻烦

为何Prefabs也不行:函数

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

ScriptableObject是咱们的rescue!

  • 在内部实现上它仍然继承自MonoBehaviour,但它没必要附着在某个对象上做为一个Component
  • 咱们也不能(固然初衷就是不肯意)把它赋给Gameobject或Prefab
  • 能够被serialised,并且能够自动有相似MonoBehavior的面板,很方便
  • 能够被放到.asset文件中,也就是说咱们能够自定义asset的类型。Unity内置的asset资源有材质、贴图、音频等等,如今依靠ScriptableObject咱们能够自定义新的资源类型,来存储咱们本身的数据
  • 能够解决某些多态问题

这里写图片描述

ScriptableObject是如何解决咱们的问题的:this

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

固然ScriptableObject也有一些缺点:spa

  • 不多的回调函数
    • OnEnable
    • OnDisable
    • OnDestroy
  • 真正意义上的共享,所以一旦修改数据都真的修改了

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

如何使用

很是简单,只须要把平时的继承自MonoBehaviour改为ScriptableObject便可:

基本使用

using UnityEngine;

[CreateAssetMenu(menuName="MySubMenue/Create MyScriptableObject ")]
public class MyScriptableObject : ScriptableObject
{
    public int someVariable;
}

其中,CreateAssetMenu可让咱们在资源建立菜单中添加建立这个ScriptableObject的选项,相似建立脚本、材质等其余资源。

咱们也能够在脚本中动态建立一个ScriptableObject:

ScriptableObject.CreateInstance<MyScriptableObject >();

这会在内存中建立一个新的实例,用做临时修改等用途,而后在不使用的时候可让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;
}

记住,ScriptableObject的目的是只有一份,所以这里面不该该包括一些会根据实例不一样而变化的数值。例如,咱们在这个例子里没有声明敌人的生命值等变量,这是由于不一样的敌人的生命值多是不一样的,这些属性应该在相应的MonoBehaviour里定义。

而后,咱们就能够在真正的MonoBehaviour脚本中声明对ScriptableObject的引用:

class Enemy : MonoBehaviour {
    public EnemyInfo info;
}

这保证全部的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;
}

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;
        }
    }
}

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);
    }
}

咱们先声明了一个PowerupEffect抽象类,来规定全部的加能量技能都须要定义一个ApplyTo函数做用于玩家。而后,咱们定义一个HealthBooster类来管理那些专门加血的技能,咱们能够经过建立资源的方式建立多个加血技能的资源实例,它们每一个均可以有不一样的加血量(Amount),当传进来一个GameObject的时候,就能够给它加血。咱们又定义了Powerup的MonoBehaviour类,把它做为Component赋给各个能够触发加血技能的物体,它们能够接受一个PowerupEffect类型的加能量技能,而后靠碰撞体触发加血行为。

Tank Demo

在参考资料中的Tank Demo中有更多的例子。

Delegate Objects

这种是很是常见的一种ScriptableObject应用模式。

例子:音效事件资源

首先是播放音效被定义成一个ScriptableObject资源对象。

public abstract class AudioEvent : ScriptableObject
{
    public abstract void Play(AudioSource source);
}

上面的AudioEvent能够用于定义一个播放音效的事件资源。全部继承它的类都须要定义播放Play函数,以便其余MonoBehaviour在运行时刻能够传递一个AudioSource文件给它进行播放。

一个简单的例子是:

[CreateAssetMenu(menuName="Audio Events/Simple")]
public class SimpleAudioEvent : AudioEvent
{
    public AudioClip[] clips;

    public RangedFloat volume;

    [MinMaxRange(0, 2)]
    public RangedFloat pitch;

    public override void Play(AudioSource source)
    {
        if (clips.Length == 0) return;

        source.clip = clips[Random.Range(0, clips.Length)];
        source.volume = Random.Range(volume.minValue, volume.maxValue);
        source.pitch = Random.Range(pitch.minValue, pitch.maxValue);
        source.Play();
    }
}

SimpleAudioEvent能够管理一个音效列表,而后在播放时随机选取一个进行播放。RangedFloat和MinMaxRanges也是自定义的类型,同时咱们也为SimpleAudioEvent和RangedFloat定义了面板显示编辑器类AudioEventEditor.cs和RangedFloatDrawer.cs,再也不赘述。最终,咱们能够经过建立资源菜单来建立一些真正的音效事件资源:

这里写图片描述

上面一共显示了4个音效播放资源,咱们选中的是一个庆祝时会播放的音频事件Celebration,它会随机播放一个大笑的音效。咱们能够点击Preview按钮来预览播放效果,这个也是在编辑器类AudioEventEditor.cs中定义的。

最后,咱们能够把这些资源拖拽给须要播放音效的MonoBehaviour,例如子弹爆炸的脚本:

这里写图片描述

例子:AI

Tank游戏里面的坦克能够是由不一样的AI控制的,一种是由玩家本身控制,一种是由电脑扮演,这种也能够有不一样的行为。咱们能够把控制坦克行为的brain也定义成一种ScriptableObject资源:

public abstract class TankBrain : ScriptableObject
{
    public virtual void Initialize(TankThinker tank) { }
    public abstract void Think(TankThinker tank);
}

TankBrain必须实现两个方法,一个是根据输入的坦克实体TankThinker(是一个MonoBehaviour类) 初始化本身,一个是根据TankThinker进行Think行为的函数。

而后,咱们能够定义玩家控制类PlayerControlledTank:

[CreateAssetMenu(menuName="Brains/Player Controlled")]
public class PlayerControlledTank : TankBrain
{

    public int PlayerNumber;
    private string m_MovementAxisName;
    private string m_TurnAxisName;
    private string m_FireButton;


    public void OnEnable()
    {
        m_MovementAxisName = "Vertical" + PlayerNumber;
        m_TurnAxisName = "Horizontal" + PlayerNumber;
        m_FireButton = "Fire" + PlayerNumber;
    }

    public override void Think(TankThinker tank)
    {
        var movement = tank.GetComponent<TankMovement>();

        movement.Steer(Input.GetAxis(m_MovementAxisName), Input.GetAxis(m_TurnAxisName));

        var shooting = tank.GetComponent<TankShooting>();

        if (Input.GetButton(m_FireButton))
            shooting.BeginChargingShot();
        else
            shooting.FireChargedShot();
    }
}

相似的还能够定义直接由电脑控制的其余AI。在Think函数里咱们能够进行和实现MonoBehaviour方法相似的功能,例如经过GetComponent、FindGameobject等函数来获取游戏对象。

而后,咱们就能够在建立真正的Brain资源:

这里写图片描述

上面显示了一个Idiot类型AI的Brain资源。

而后,咱们能够在游戏管理类GameManager里面定义两个玩家,并把须要的Tank Brain资源拖拽进去:

这里写图片描述

Reload-proof Singleton

例子:游戏设置

这个例子是说用户能够在开始菜单里定义Tank Brain的个数和类型,而后这个设置(GameSettings)会做为一个Singleton传递给后面的关卡中。GameSettings类咱们不少时候其实都是直接用普通的Singleton类(值得说明的是Unity里面实现的Singleton类一般都是靠DonotDestroyOnLoad等“费劲的方法”来实现)来作的,它会在开始的时候读取Json文件,在必要的时候再写回Json进行保存。这里选择在ScriptableObject + Singleton的方法来实现的一个好处是咱们不须要什么其余繁冗的步骤,就能够保证惟一性和在场景之间共享的目的,由于ScriptableObject自己能够认为是一种资源:

[CreateAssetMenu]
public class GameSettings : ScriptableObject
{
    [Serializable]
    public class PlayerInfo
    {
        public string Name;
        public Color Color;

        ...
    }

    public List<PlayerInfo> players;

    private static GameSettings _instance;
    public static GameSettings Instance
    {
        get
        {
            if (!_instance)
                _instance = Resources.FindObjectsOfTypeAll<GameSettings>().FirstOrDefault();
#if UNITY_EDITOR
            if (!_instance)
                InitializeFromDefault(UnityEditor.AssetDatabase.LoadAssetAtPath<GameSettings>("Assets/Test game settings.asset"));
#endif
            return _instance;
        }
    }

    public int NumberOfRounds;

    public static void LoadFromJSON(string path)
    {
        if (!_instance) DestroyImmediate(_instance);
        _instance = ScriptableObject.CreateInstance<GameSettings>();
        JsonUtility.FromJsonOverwrite(System.IO.File.ReadAllText(path), _instance);
        _instance.hideFlags = HideFlags.HideAndDontSave;
    }

    public void SaveToJSON(string path)
    {
        Debug.LogFormat("Saving game settings to {0}", path);
        System.IO.File.WriteAllText(path, JsonUtility.ToJson(this, true));
    }

    public static void InitializeFromDefault(GameSettings settings)
    {
        if (_instance) DestroyImmediate(_instance);
        _instance = Instantiate(settings);
        _instance.hideFlags = HideFlags.HideAndDontSave;
    }

#if UNITY_EDITOR
    [UnityEditor.MenuItem("Window/Game Settings")]
    public static void ShowGameSettings()
    {
        UnityEditor.Selection.activeObject = Instance;
    }
#endif

    ...
}

GameSettings继承了ScriptableObject,并支持咱们在资源文件夹中建立一个资源文件,这个资源文件能够是在游戏第一次运行时候的一个默认的玩家配置。而后,它实现了LoadFromJSON和SaveToJSON函数来加载和存储硬盘上的数据。一个有趣的地方是上面的最后一个函数,这个函数容许咱们在菜单栏上打开并显示当前的GameSettings资源,很是方便,不须要再本身写窗口类了。

在欢迎界面上,咱们能够在控制类MainMenuController中获取GameSettings,并在进入下一关前保存数据到硬盘:

public class MainMenuController : MonoBehaviour
{
    public GameSettings GameSettingsTemplate;

    ...

    public string SavedSettingsPath {
        get {
            return System.IO.Path.Combine(Application.persistentDataPath, "tanks-settings.json");
        }
    }

    void Start () {
        if (System.IO.File.Exists(SavedSettingsPath))
            GameSettings.LoadFromJSON(SavedSettingsPath);
        else
            GameSettings.InitializeFromDefault(GameSettingsTemplate);

        foreach(var info in GetComponentsInChildren<PlayerInfoController>())
            info.Refresh();

        NumberOfRoundsSlider.value = GameSettings.Instance.NumberOfRounds;
    }

    public void Play()
    {
        GameSettings.Instance.SaveToJSON(SavedSettingsPath);
        GameState.CreateFromSettings(GameSettings.Instance);
        SceneManager.LoadScene(1, LoadSceneMode.Single);
    }

    ...
}

在以后的场景中,咱们只须要GameSettings.Instance来访问就能够了。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值