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 ,並 override 了 ApplyTo ,將增加遊戲物件的生命值。

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 設定到 Circle 的 GameObjectRuntimeSet 中,讓 Circle 知道在 OnEnable 時要放入哪個集合。之後,把 Circle 轉為 Prefab

再建立一個製造Circle的 CreateObject,這邊建立一個按鈕,當按下時,會觸發 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 物件。

總結

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

返回 ScriptableObject 系列

Reference:

ScriptableObject as Event

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

首先要建立 GameEvent 與 GameEventListener 。它們兩個互相依賴

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

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

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

接著建立事件發起的物件,這邊建立兩個, RaiseClickableCircle 與 RaiseClickableButWithUnityEvent

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

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

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

返回 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 實體, HP 與 MaxHP

然後建立繼承 MonoBehaviour 的 Player 類

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 編輯器中,將剛剛建立的 HP 與 MaxHP 設定到 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 編輯器中,將 HP 與 MaxHP 設定到 HPView 上,

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

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

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

返回 ScriptableObject 系列

Reference:

ScriptableObject 基礎

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

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

ScriptableObject 和 MonoBehaviour 一樣,繼承了 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>();

MonoBehaviour 與 ScriptableObject 的比較

MonoBehaviour ScriptableObject
MonoBehaviour 接收所有來自 Unity 的 callback,像是 Start , Awake , Update , OnEnable ,OnDisabl , OnCollisonEnter 等 ScriptableObject 只接收一些: Awake , OnEnable , OnDestroy 與 OnDisable 。 在 Editor 中的話還有 OnValidate 與 Reset
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: