ScriptableObject as delegate object

ScriptableObject 除了作為資料的容器外,還可在其內部定義方法,讓 MonoBehaviour 可以將自己傳入 ScriptableObject 的方法中,根據不同的 ScriptableObject 實體去執行不同的動作。

例如:要做增益效果,定義一個抽象的 PowerupEffect 類,它繼承 ScriptableObject ,並且只有一個 ApplyTo 方法,這個方法將增益效果應用到傳入的 GameObject 中。

1
2
3
4
public abstract class PowerupEffect : ScriptableObject 
{
public abstract void ApplyTo(GameObject object);
}

建立一個子類 HpBooster ,並 overrideApplyTo ,將增加遊戲物件的生命值。

1
2
3
4
5
6
7
8
public class HpBooster : PowerupEffect
{
public int Amount;
public override void ApplyTo(GameObject object)
{
object.GetComponent<HP>().currentValue += Amount;
}
}

在建立一個 增益物件 的腳本,這個增益物件將持有 PowerupEffect 的參考,當在觸發器中觸發時,就會呼叫它持有的 PowerupEffect 來將增益效果應用到碰到的遊戲物件。

1
2
3
4
5
6
7
8
9
public class Powerup : MonoBehaviour 
{
public PowerupEffect effect;

public void OnTriggerEnter(Collider other)
{
effect.ApplyTo(other.gameObject);
}
}

例子,使用音效

1
2
3
4
5
6
7
// 這個 struct 為最大最小值的結構
[Serializable]
public struct RangedFloat
{
public float minValue;
public float maxValue;
}

定義一個抽象的 AudioDelegateSO 並繼承 ScriptableObject。在其中含有一個 Play 抽象方法,讓傳入的 AudioSource 播放音效。

1
2
3
4
public abstract class AudioDelegateSO: ScriptableObject
{
public abstract void Play(AudioSource source);
}

定義一個 SimpleAudioDelegateSO 類,它繼承 AudioDelegateSO
這個 SimpleAudioDelegateSO 可以管理一個音效列表clips,在 Play 方法中,將隨機播放 clips 列表中的聲音。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[CreateAssetMenu(fileName ="AudioDelegate")]
public class SimpleAudioDelegateSO : AudioDelegateSO
{
public AudioClip[] clips;
public RangedFloat volume;
public RangedFloat pitch;
public override void Play(AudioSource source)
{
if (clips.Length == 0 || source == null)
{
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();
}
}

我們可以使用這個 SimpleAudioDelegateSO 讓同一個遊戲物件隨機播放不同的聲音,例如可以儲存狗的叫聲,每當觸碰狗遊戲物件時,就隨機播放一種叫聲。


例子: 在遊戲中,會分為玩家控制,或是電腦控制的遊戲物件,如坦克遊戲中的坦克,可以由不同 AI 控制,也可以為玩家控制。這種不同的行為,我們也可以用 Delegate Object 來做。

定義一個抽象的 TankBrain 繼承 ScriptableObject。其中

  • 含有 Initialize 虛方法,不一定會實作這個方法,但是當需要做初始化的動作時可以 override 它。
  • 含有一個 Think 抽象方法,決定傳入的 TankThinker 要有什麼行為。
    1
    2
    3
    4
    public abstract class TankBrain : ScrptableObject
    {
    public abstract void Think(TankThinker tank);
    }

定義一個 PlayerControlledTank 類,它繼承 TankBrain

  • 它為玩家操控的坦克。
  • override Think 方法,在裡面取得坦克的位置,並判斷輸入來移動坦克或是發射砲彈。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    [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)
    {
    // 取得坦克移動 component
    var movement = tank.GetComponent<TankMovement>();
    // 透過 Steer 與 Input 輸入來移動坦克
    movement.Steer(Input.GetAxis(m_MovementAxisName), Input.GetAxis(m_TurnAxisName));
    // 取得坦克發射砲彈 component
    var shooting = tank.GetComponent<TankShooting>();
    // 判斷 Input 輸入是否為開火按鈕
    if (Input.GetButton(m_FireButton))
    shooting.BeginChargingShot();
    else
    shooting.FireChargedShot();
    }
    }

定義一個 SimpleSniper 類,它也繼承 TankBrain ,它為電腦操控的坦克。

  • override Think 方法,在裡面判斷是否移動與開火
    • 如取得坦克的位置,並根據它的記憶(Remeber)中判斷是否看過玩家坦克
      • 如果記憶(Remeber)中沒有玩家坦克,則找最近的物件,並判斷是否為玩家坦克
      • 如果還是沒找到,則移動自己
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
[CreateAssetMenu(menuName="Brains/Simple sniper")]
public class SimpleSniper : TankBrain
{

public float aimAngleThreshold = 2f;
[MinMaxRange(0, 0.05f)]
public RangedFloat chargeTimePerDistance;
[MinMaxRange(0, 10)]
public RangedFloat timeBetweenShots;

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

if (!target)
{
// 如果記憶(`Remeber`)中沒有玩家坦克,則找最近的物件,並判斷是否為玩家坦克
target =
GameObject
.FindGameObjectsWithTag("Player")
.OrderBy(go => Vector3.Distance(go.transform.position, tank.transform.position))
.FirstOrDefault(go => go != tank.gameObject);
// 記住找到的目標
tank.Remember<GameObject>("target", target);
}

if (!target)
{
// 如果還是沒找到,則移動自己
movement.Steer(0.5f, 1f);
return;
}

// 瞄準目標
Vector3 desiredForward = (target.transform.position - tank.transform.position).normalized;
if (Vector3.Angle(desiredForward, tank.transform.forward) > aimAngleThreshold)
{
// 判斷轉動方向
bool clockwise = Vector3.Cross(desiredForward, tank.transform.forward).y > 0;
movement.Steer(0f, clockwise ? -1 : 1);
}
else
{
// 停止轉向
movement.Steer(0f, 0f);
}

// 獲取坦克的射擊 component
var shooting = tank.GetComponent<TankShooting>();
// 檢查是否可以開始充能射擊
if (!shooting.IsCharging)
{
if (Time.time > tank.Remember<float>("nextShotAllowedAfter"))
{
float distanceToTarget = Vector3.Distance(target.transform.position, tank.transform.position);
float timeToCharge = distanceToTarget*Random.Range(chargeTimePerDistance.minValue, chargeTimePerDistance.maxValue);
tank.Remember("fireAt", Time.time + timeToCharge);
shooting.BeginChargingShot();
}
}
else
{
float fireAt = tank.Remember<float>("fireAt");
if (Time.time > fireAt)
{
shooting.FireChargedShot();
tank.Remember("nextShotAllowedAfter", Time.time + Random.Range(timeBetweenShots.minValue, timeBetweenShots.maxValue));
}
}
}
}

返回 ScriptableObject 系列

Reference:

ScriptableObject as Enum

ScriptableObject 作為 Enum 可以有很多好處,像是可含有自己的屬性和方法等。另外還不具 Enum 的一些缺陷

Enum 的缺陷有

  1. 它會隱含(implicitly)的為所有元素賦予一個整數值,這造成刪改 Enum 中的值很不方便,若隨意刪除一個元素,那它之後元素的值都會變

    • 下面例子,沒有明確指定值,因此會自動從0開始為每個元素賦值
    1
    2
    3
    4
    5
    6
    enum Skill 
    {
    SpeedUp,
    PowerUp,
    DefenseUp
    }

    預設會自動幫你的賦值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    enum Skill 
    {
    SpeedUp = 0,
    PowerUp = 1,
    DefenseUp = 2
    }

    // 假設之後要刪除 `PowerUp` ,那麼在它之後元素的值都會變
    enum Skill
    {
    SpeedUp,
    DefenseUp // DefenseUp 的值會變為 1
    }
  2. 除非使用 Nullable 否則它不可以為 NULL ,因此一般常會自訂一個元素來代表空值

    • 自訂一個元素來代表空值
      1
      2
      3
      4
      5
      6
      7
      enum Skill 
      {
      NONE = 0,
      SpeedUp = 1,
      PowerUp = 2,
      DefenseUp = 3
      }
  3. C#enum 中不允許有方法或是屬性,需要使用擴展方法來完成

    • 不可以有方法或屬性
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      enum Skill 
      {
      NONE = 0,
      SpeedUp = 1,
      PowerUp = 2,
      DefenseUp = 3
      // 不可以
      // private float bonusRatio;
      // 不可以
      // public float GetBonusRatio(){ return bonusRatio; }
      }
      需要使用擴展方法來完成
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      public static class SkillExtensions
      {
      public static double GetBonusRatio(this Skill skill)
      {
      switch (skill)
      {
      case Skill.SpeedUp:
      return 1.2;
      case Skill.PowerUp:
      return 1.5;
      case Skill.DefenseUp:
      return 1.3;
      default:
      return 1.0;
      }
      }
      }

因此將 ScriptableObject 作為 Enum 來使用也是一種好選擇,使用的方式為

1
2
3
4
5
[CreateAssetMenu(menuName = "ScriptableObjects/SkillSO")]
public class SkillSO : ScriptableObject
{

}

然後在透過 Unity 編輯器建立需要的元素即可

使用上也和 Enum 類似

返回 ScriptableObject 系列

Reference:

ScriptableObject as Runtime Set

RuntimeSet 的做法是將 ScriptableObject 作為一個分享集合容器,在執行期間,將物件放入,讓其他要使用它的物件可以透過這個分享容器拿到。

以下的例子中,將建立一個 GameObjectRuntimeSet ,這個 GameObjectRuntimeSet 將會在執行時持有 Circle 物件集合,而 Circle 物件將由 CreateObject 建立,而 ObjectMonitor 負責查看 GameObjectRuntimeSet 中有多少 Circle 物件

以下建立一個通用抽象類 RuntimeSet 它繼承 ScriptableObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public abstract class RuntimeSet<T> : ScriptableObject
{
protected readonly List<T> Items = new();

public int Count => Items.Count;

public void Add(T thing)
{
if (!Items.Contains(thing))
Items.Add(thing);
}

public bool Remove(T thing)
{
return Items.Remove(thing);
}

public T this[int index]
{
get => Items[index];
set => Items[index] = value;
}

public void Clear()
{
Items.Clear();
}
}

接著繼承 RuntimeSet 建立一個 GameObjectRuntimeSet , 這個 GameObjectRuntimeSet 將為 GameObject 的分享集合容器

1
2
3
4
[CreateAssetMenu(menuName = "ScriptableObjects/GameObjectRuntimeSet")]
public class GameObjectRuntimeSet : RuntimeSet<GameObject>
{
}

在 Unity 編輯器中建立一個 ScriptableObject 實體 CircleRuntimeSet

接著建立要放到這個集合的物件 Circle ,在 OnEnable 時將自己加入集合, OnDisable 時將自己從集合中移除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Circle : MonoBehaviour
{
public GameObjectRuntimeSet GameObjectRuntimeSet;
private void OnEnable()
{
GameObjectRuntimeSet.Add(this.gameObject);
}

private void OnDisable()
{
GameObjectRuntimeSet.Remove(this.gameObject);
}

}

ScriptableObject 實體 CircleRuntimeSet 設定到 CircleGameObjectRuntimeSet 中,讓 Circle 知道在 OnEnable 時要放入哪個集合。之後,把 Circle 轉為 Prefab

再建立一個製造CircleCreateObject,這邊建立一個按鈕,當按下時,會觸發 CreateObject.Create()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CreateObject : MonoBehaviour
{

public GameObject circle;

public void Create()
{
float randomX = Random.Range(-6f, 6f);
float randomY = Random.Range(-6f, 6f);
Vector3 randomPosition = new Vector3(randomX, randomY, 0f);

Instantiate(circle, randomPosition, Quaternion.identity);
}

}

最後建立一個觀看這個集合的物件

1
2
3
4
5
6
7
8
9
10
11
public class ObjectMonitor : MonoBehaviour
{

public GameObjectRuntimeSet GameObjectRuntimeSet;

public void Update()
{
Debug.Log("Current objects in the set " + GameObjectRuntimeSet.Count);
}

}

觀察結果
每按下按鈕一次,所產生的 Circle 物件都會加入 GameObjectRuntimeSet ,而需要知道有多少個 Circle 的物件,它只要從 GameObjectRuntimeSet 裏面就可以找到目前有多少 Circle 物件。

總結

  • RuntimeSetScriptableObject 作為分享的集合容器。
  • 它可以避免使用 Singleton 來管理物件群
  • 它避免了使用 Object.FindObjectOfTypeGameObject.FindWithTag 等開銷可能會比較大的方法來搜尋物件

返回 ScriptableObject 系列

Reference:

ScriptableObject as Event

ScriptableObject 可以用來作為事件系統的一部分。這種方法可以幫助我們在不同的 MonoBehaviour 之間進行通信,而不需要它們彼此知道對方的存在,從而實現鬆耦合。

首先要建立 GameEventGameEventListener 。它們兩個互相依賴

GameEvent

  • 含有一個 GameEventListener 列表用來保存要聽取該事件的聽眾,
  • 使用 RegisterListener 來註冊聽眾
  • 使用 UnregisterListener 將聽眾移出
  • 使用 Raise 觸發聽眾註冊的事件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    [CreateAssetMenu(menuName = "ScriptableObjects/GameEvent", order = 1)]
    public class GameEvent : ScriptableObject
    {
    private readonly List<GameEventListener> eventListeners =
    new List<GameEventListener>();

    public void Raise()
    {
    for (int i = eventListeners.Count - 1; i >= 0; i--)
    eventListeners[i].OnEventRaised();
    }

    public void RegisterListener(GameEventListener listener)
    {
    if (!eventListeners.Contains(listener))
    eventListeners.Add(listener);
    }

    public void UnregisterListener(GameEventListener listener)
    {
    if (eventListeners.Contains(listener))
    eventListeners.Remove(listener);
    }
    }

GameEventListener

  • 含有一個要監聽的事件參考 Event
  • 含有一個當監聽到後,要做出的回應 Response
  • OnEnable 中將自己註冊到 Event
  • OnDisable 中將自己從 Event 中移出
  • OnEventRaised 中呼叫回應的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class GameEventListener : MonoBehaviour
{
[Tooltip("Event to register with.")]
public GameEvent Event;

[Tooltip("Response to invoke when Event is raised.")]
public UnityEvent Response;

private void OnEnable()
{
Event.RegisterListener(this);
}

private void OnDisable()
{
Event.UnregisterListener(this);
}

public void OnEventRaised()
{
Response.Invoke();
}
}

接著在 Unity 編輯器中建立一個 ScriptableObject 實體,並命名為 ClickEvent

建立一個 DoSomethingWhenClick 的遊戲物件(GameObject) ,並添加以下腳本,這個遊戲物件有一個公開方法 JustDoIt,當事件發生時,要被呼叫。

1
2
3
4
5
6
7
public class DoSomethingWhenClick : MonoBehaviour
{
public void JustDoIt()
{
Debug.Log("Just Do It");
}
}

建立一個 EventListenerGameObject,將 GameEventListener 腳本添加到這個 EventListener GameObject 中。

  • 接下來將前面建立的 ScriptableObject 實體 ClickEvent ,拖放到,EventListener 中的 Event 欄位,表示這個 EventListener 要監聽的事件是 ClickEvent
  • 然後設定聽到之後要做的事。將 DoSomethingWhenClick 遊戲物件放到 Response 欄位,並設定要執行該物件的公開方法 JustDoIt

接著建立事件發起的物件,這邊建立兩個, RaiseClickableCircleRaiseClickableButWithUnityEvent

RaiseClickableCircle 的腳本如下,它主要是直接使用 GameEvent 作為參考,其缺點是只能用一個 Event

1
2
3
4
5
6
7
8
9
10
public class RaiseClickableCircle : MonoBehaviour
{

public GameEvent ClickGameEvent;

void OnMouseDown()
{
ClickGameEvent.Raise();
}
}

RaiseClickableButWithUnityEvent 幾乎與 RaiseClickableCircle 一模一樣,差別在於是使用 UnityEvent ,這樣可以放置多個 Event,當觸發 Raise 時在裡面的 Event 都會觸發。

1
2
3
4
5
6
7
8
9
public class RaiseClickableButWithUnityEvent : MonoBehaviour
{
public UnityEvent ClickEvent;

void OnMouseDown()
{
ClickEvent.Invoke();
}
}

最後執行,並點擊畫面上的圓形或是方形,可以發現都會觸發 DoSomethingWhenClickJustDoIt 方法,而 RaiseClickableButWithUnityEventRaiseClickableCircleDoSomethingWhenClick 完全不知曉對方的存在。

使用基於事件架構(event-based architecture)的好處是,它不會每一幀都在執行,只有在當事件發生時才會執行,因此效率會比在 MonoBehaviourUpdate 方法中執行好。

返回 ScriptableObject 系列

Reference:

ScriptableObject as shared variable

ScriptableObject 可以作為遊戲變數在各個 MonoBehaviour 之間分享資料,此外,透過這種方式還可以減少 MonoBehaviour 物件之間互相參考的耦合度。

下面將使用 ScriptableObject 建立玩家生命數值,並在不同的 MonoBehaviour之間使用

首先建立一個繼承 ScriptableObject 的類 FloatVariable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[CreateAssetMenu(menuName = "ScriptableObjects/FloatVariable", order = 1)]
public class FloatVariable : ScriptableObject
{
#if UNITY_EDITOR
[Multiline]
public string DeveloperDescription = "";
#endif
public float Value;

public void SetValue(float value)
{
Value = value;
}

public void SetValue(FloatVariable value)
{
Value = value.Value;
}

public void ApplyChange(float amount)
{
Value += amount;
}

public void ApplyChange(FloatVariable amount)
{
Value += amount.Value;
}
}

接下來建立兩個 FloatVariable 實體, HPMaxHP

然後建立繼承 MonoBehaviourPlayer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Player : MonoBehaviour
{
public FloatVariable CurrentHP;
public FloatVariable MaxHP;
public float StartingHP;
public bool ResetHP;

private void Start()
{
if (ResetHP)
{
CurrentHP.SetValue(StartingHP);
}
}

// 暴露給外部,這邊會綁定按鈕的 On Click 事件
public void Hurt()
{
CurrentHP.ApplyChange(-10);
}
}

在 Unity 編輯器中,將剛剛建立的 HPMaxHP 設定到 Player

接著建立一個 HPView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class HPView : MonoBehaviour
{
public TextMeshProUGUI CurrentHPText;
public TextMeshProUGUI MaxHPText;

public FloatVariable CurrentHP;
public FloatVariable MaxHP;

private void Update()
{
CurrentHPText.text = CurrentHP.Value.ToString();
MaxHPText.text = MaxHP.Value.ToString();
}
}

在 Unity 編輯器中,將 HPMaxHP 設定到 HPView 上,

Player.Hurt() 設定給按鈕的 On Click 。

最後執行,在畫面上可以按下按鈕,觸發 Player.Hurt() ,可以觀察到 CurrentHPText 的變化

並且可以發現這兩個 MonoBehaviourPlayerHPView 互相不知道彼此的存在,實現了鬆耦合(loosely coupled)。

返回 ScriptableObject 系列

Reference:

ScriptableObject 基礎

ScriptableObject 是一個資料容器(Data container),它透過共享通用資料的方式來減少對重複資料的記憶體佔用,進而降低你APP的記憶體使用量。

如果你的預製物件(Prefab)中儲存了一些不會改變的資料,那麼很適合把它們改為使用 ScriptableObject ,因為每當你實體化(instantiate)一個預製物件(Prefab)時,這個實體會保存自己的一份資料,當實體化很多個實體時,會有很多重複的資料佔用很多記憶體,因此你可以使用 ScriptableObject 可以把資料儲存起來,然後讓所有的預製物件(Prefab)存取這個 ScriptableObject

ScriptableObjectMonoBehaviour 一樣,繼承了 UnityEngine.Object ,但是與 MonoBehaviour 不同在於你不能把 ScriptableObject 附加(attach)到一個 GameObject 上,也不能進行 GatComponent 等對 GameObject 的操作,相反你需要把它儲存為資源(Asset)。

在使用Unity編輯器時,你可以把資料放在 ScriptableObject ,在執行時也可以調整其中的資料,但是你不可以把它當作遊戲存檔的工具。

使用 ScriptableObject 主要情況是在編輯時期修改 ScriptableObject 的資料,在執行時這些資料會被當作資源(Asset)使用。

使用方式

建立一個類並繼承 ScriptableObject,你可以使用 CreateAssetMenu 屬性(attribute),讓你方便在編輯器中使用,以下是一個範例:

1
2
3
4
5
6
7
8
[CreateAssetMenu(fileName = "Data", menuName = "ScriptableObjects/SpawnManagerScriptableObject", order = 1)]
public class SpawnManagerScriptableObject : ScriptableObject
{
public string prefabName;

public int numberOfPrefabsToCreate;
public Vector3[] spawnPoints;
}

之後,你可以在 Assets -> Create -> ScriptableObjects -> SpawnManagerScriptableObject 中建立新的 ScriptableObject 實體。

你可以更改該實體的名稱與它的屬性。

接下來你可以在 MonoBehaviour 中使用這個 ScriptableObject 實體,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using UnityEngine;

public class Spawner : MonoBehaviour
{
// 要實體化 Prefab 的參考
public GameObject entityToSpawn;

// 一個 ScriptableObject 的參考
public SpawnManagerScriptableObject spawnManagerValues;

// 記錄了實體化了多少個實體
int instanceNumber = 1;

void Start()
{
SpawnEntities();
}

void SpawnEntities()
{
int currentSpawnPointIndex = 0;

for (int i = 0; i < spawnManagerValues.numberOfPrefabsToCreate; i++)
{
// 使用 ScriptableObject 中的資料
GameObject currentEntity = Instantiate(entityToSpawn, spawnManagerValues.spawnPoints[currentSpawnPointIndex], Quaternion.identity);

currentEntity.name = spawnManagerValues.prefabName + instanceNumber;

currentSpawnPointIndex = (currentSpawnPointIndex + 1) % spawnManagerValues.spawnPoints.Length;

instanceNumber++;
}
}
}

在Unity編輯器中,使用拖拉的方式將剛剛建立好的 ScriptableObject 實體拖拉到對應的位置上,另外 Box Prefab 是一個簡單的Square

最後在執行Unity,就可以看到Unity使用剛剛建立出來 ScriptableObject 資料去產生 Box 物件。

此外除了在編輯器中建立 實體,在執行期間也可以透過 CreateInstance 來建立。

1
ScriptableObject.CreateInstance<MyScriptableObjectClass>();

MonoBehaviourScriptableObject 的比較

MonoBehaviour ScriptableObject
MonoBehaviour 接收所有來自 Unity 的 callback,像是 StartAwakeUpdateOnEnable ,OnDisablOnCollisonEnter ScriptableObject 只接收一些: AwakeOnEnableOnDestroyOnDisable 。 在 Editor 中的話還有 OnValidateReset
MonoBehaviour 必須要 附加(attach)到 GameObject ScriptableObject 不能附加到 GameObject 上,需要在專案(Project Level)中將它們儲存為 asset 檔案,在其他腳本中參考這些 ScriptableObject asset
當儲存 MonoBehaviour 時,會將它們的資料儲存到 Scenes 與 Prefabs 中 每個 ScriptableObject 實體都會被保存為專案層級(Project level)的獨立檔案中
一般在 Play Mode 中修改 MonoBehaviour 中的值之後離開 Play Mode 的話,這些值會被重新設定(Reset) 當離開 Play Mode 後, ScriptableObject 則不會重新設定。此外當編譯發布之後,執行期間 ScriptableObject 修改的值不會被保存,而是為發布時的值

ScriptableObject 中只有以下的 callback 會被呼叫

  • Awake : 與 MonoBehaviour 的類似,只有在 ScriptableObject 開始時會被呼叫。當遊戲執行(launched)或是有參考到這個 ScriptableObject asset 的 Scene 被載入時會呼叫。
  • OnEnable : 在 Awake 之後呼叫,當 ScriptableObject 被載入(Loaded)或是實體化(Instantiated)時會被呼叫。當 ScriptableObject.CreateInstance 或是 script 被重新編譯(recompilation)時 OnEnable 會被呼叫。
  • OnDisable : 當載入的 Scene 不再參考這個 ScriptableObject asset 時,就會呼叫 OnDisable , 它會在 OnDestory 之前被呼叫。此外在 script 重新編譯進入 Play Mode 時,也會呼叫,此時會出現 OnDisable 出現在 OnEnable 的情況。
  • OnDestory : 當 ScriptableObject 在 編輯器中刪除,或是重程式碼中刪除時,會呼叫 OnDestory 。如果是在執行期間建立的 ScriptableObject 在APP離開或是離開 Play Mode 時也會呼叫。

以下 callback 只在編輯器中呼叫

  • OnValidate : 當值在 Inspector 中改變值,才會呼叫。可以在這邊確保你的編輯器輸入的資料是合適的範圍(如 0 ~ 1)。
  • Reset : 當點擊 Inspector 中的 Reset 按鈕時,會呼叫。

返回 ScriptableObject 系列

Reference: