ScriptableObject
是一個資料容器(Data container
),用於儲存資料並作為資源(Asset
)在執行時使用。除了基本的資料儲存功能外,它還有多種特殊用法。
ScriptableObject
是一個資料容器(Data container
),用於儲存資料並作為資源(Asset
)在執行時使用。除了基本的資料儲存功能外,它還有多種特殊用法。
ScriptableObject as delegate object
ScriptableObject
除了作為資料的容器外,還可在其內部定義方法,讓 MonoBehaviour
可以將自己傳入 ScriptableObject
的方法中,根據不同的 ScriptableObject
實體去執行不同的動作。
例如:要做增益效果,定義一個抽象的 PowerupEffect
類,它繼承 ScriptableObject
,並且只有一個 ApplyTo
方法,這個方法將增益效果應用到傳入的 GameObject
中。
1 | public abstract class PowerupEffect : ScriptableObject |
建立一個子類 HpBooster
,並 override
了 ApplyTo
,將增加遊戲物件的生命值。
1 | public class HpBooster : PowerupEffect |
在建立一個 增益物件 的腳本,這個增益物件將持有 PowerupEffect
的參考,當在觸發器中觸發時,就會呼叫它持有的 PowerupEffect
來將增益效果應用到碰到的遊戲物件。
1 | public class Powerup : MonoBehaviour |
例子,使用音效
1 | // 這個 struct 為最大最小值的結構 |
定義一個抽象的 AudioDelegateSO
並繼承 ScriptableObject
。在其中含有一個 Play
抽象方法,讓傳入的 AudioSource
播放音效。
1 | public abstract class AudioDelegateSO: ScriptableObject |
定義一個 SimpleAudioDelegateSO
類,它繼承 AudioDelegateSO
,
這個 SimpleAudioDelegateSO
可以管理一個音效列表clips
,在 Play
方法中,將隨機播放 clips
列表中的聲音。
1 | [ ] |
我們可以使用這個 SimpleAudioDelegateSO
讓同一個遊戲物件隨機播放不同的聲音,例如可以儲存狗的叫聲,每當觸碰狗遊戲物件時,就隨機播放一種叫聲。
例子: 在遊戲中,會分為玩家控制,或是電腦控制的遊戲物件,如坦克遊戲中的坦克,可以由不同 AI 控制,也可以為玩家控制。這種不同的行為,我們也可以用 Delegate Object
來做。
定義一個抽象的 TankBrain
繼承 ScriptableObject
。其中
Initialize
虛方法,不一定會實作這個方法,但是當需要做初始化的動作時可以 override
它。Think
抽象方法,決定傳入的 TankThinker
要有什麼行為。1 | public abstract class TankBrain : ScrptableObject |
定義一個 PlayerControlledTank
類,它繼承 TankBrain
override
Think
方法,在裡面取得坦克的位置,並判斷輸入來移動坦克或是發射砲彈。1 | [ ] |
定義一個 SimpleSniper
類,它也繼承 TankBrain
,它為電腦操控的坦克。
override
Think
方法,在裡面判斷是否移動與開火Remeber
)中判斷是否看過玩家坦克Remeber
)中沒有玩家坦克,則找最近的物件,並判斷是否為玩家坦克1 | [ ] |
Reference:
將 ScriptableObject
作為 Enum
可以有很多好處,像是可含有自己的屬性和方法等。另外還不具 Enum
的一些缺陷
Enum
的缺陷有
它會隱含(implicitly
)的為所有元素賦予一個整數值,這造成刪改 Enum
中的值很不方便,若隨意刪除一個元素,那它之後元素的值都會變
1 | enum Skill |
預設會自動幫你的賦值
1 | enum Skill |
除非使用 Nullable
否則它不可以為 NULL
,因此一般常會自訂一個元素來代表空值
1 | enum Skill |
在 C#
, enum
中不允許有方法或是屬性,需要使用擴展方法來完成
1 | enum Skill |
1 | public static class SkillExtensions |
因此將 ScriptableObject
作為 Enum
來使用也是一種好選擇,使用的方式為
1 | [ ] |
然後在透過 Unity 編輯器建立需要的元素即可
使用上也和 Enum
類似
Reference:
ScriptableObject as Runtime Set
RuntimeSet
的做法是將 ScriptableObject
作為一個分享集合容器,在執行期間,將物件放入,讓其他要使用它的物件可以透過這個分享容器拿到。
以下的例子中,將建立一個 GameObjectRuntimeSet
,這個 GameObjectRuntimeSet
將會在執行時持有 Circle
物件集合,而 Circle
物件將由 CreateObject
建立,而 ObjectMonitor
負責查看 GameObjectRuntimeSet
中有多少 Circle
物件
以下建立一個通用抽象類 RuntimeSet
它繼承 ScriptableObject
1 | public abstract class RuntimeSet<T> : ScriptableObject |
接著繼承 RuntimeSet
建立一個 GameObjectRuntimeSet
, 這個 GameObjectRuntimeSet
將為 GameObject
的分享集合容器
1 | [ ] |
在 Unity 編輯器中建立一個 ScriptableObject
實體 CircleRuntimeSet
接著建立要放到這個集合的物件 Circle
,在 OnEnable
時將自己加入集合, OnDisable
時將自己從集合中移除
1 | public class Circle : MonoBehaviour |
把 ScriptableObject
實體 CircleRuntimeSet
設定到 Circle
的 GameObjectRuntimeSet
中,讓 Circle
知道在 OnEnable
時要放入哪個集合。之後,把 Circle
轉為 Prefab
再建立一個製造Circle
的 CreateObject
,這邊建立一個按鈕,當按下時,會觸發 CreateObject.Create()
1 | public class CreateObject : MonoBehaviour |
最後建立一個觀看這個集合的物件
1 | public class ObjectMonitor : MonoBehaviour |
觀察結果
每按下按鈕一次,所產生的 Circle
物件都會加入 GameObjectRuntimeSet
,而需要知道有多少個 Circle
的物件,它只要從 GameObjectRuntimeSet
裏面就可以找到目前有多少 Circle
物件。
總結
RuntimeSet
將 ScriptableObject
作為分享的集合容器。Object.FindObjectOfType
或 GameObject.FindWithTag
等開銷可能會比較大的方法來搜尋物件Reference:
ScriptableObject
可以用來作為事件系統的一部分。這種方法可以幫助我們在不同的 MonoBehaviour
之間進行通信,而不需要它們彼此知道對方的存在,從而實現鬆耦合。
首先要建立 GameEvent
與 GameEventListener
。它們兩個互相依賴
GameEvent
GameEventListener
列表用來保存要聽取該事件的聽眾,RegisterListener
來註冊聽眾UnregisterListener
將聽眾移出Raise
觸發聽眾註冊的事件1 | [ ] |
GameEventListener
Event
Response
OnEnable
中將自己註冊到 Event
中OnDisable
中將自己從 Event
中移出OnEventRaised
中呼叫回應的方法1 | public class GameEventListener : MonoBehaviour |
接著在 Unity 編輯器中建立一個 ScriptableObject
實體,並命名為 ClickEvent
建立一個 DoSomethingWhenClick
的遊戲物件(GameObject
) ,並添加以下腳本,這個遊戲物件有一個公開方法 JustDoIt
,當事件發生時,要被呼叫。
1 | public class DoSomethingWhenClick : MonoBehaviour |
建立一個 EventListener
的 GameObject
,將 GameEventListener
腳本添加到這個 EventListener
GameObject
中。
ScriptableObject
實體 ClickEvent
,拖放到,EventListener
中的 Event 欄位,表示這個 EventListener
要監聽的事件是 ClickEvent
DoSomethingWhenClick
遊戲物件放到 Response 欄位,並設定要執行該物件的公開方法 JustDoIt
接著建立事件發起的物件,這邊建立兩個, RaiseClickableCircle
與 RaiseClickableButWithUnityEvent
RaiseClickableCircle
的腳本如下,它主要是直接使用 GameEvent
作為參考,其缺點是只能用一個 Event
1 | public class RaiseClickableCircle : MonoBehaviour |
RaiseClickableButWithUnityEvent
幾乎與 RaiseClickableCircle
一模一樣,差別在於是使用 UnityEvent
,這樣可以放置多個 Event,當觸發 Raise
時在裡面的 Event 都會觸發。
1 | public class RaiseClickableButWithUnityEvent : MonoBehaviour |
最後執行,並點擊畫面上的圓形或是方形,可以發現都會觸發 DoSomethingWhenClick
的 JustDoIt
方法,而 RaiseClickableButWithUnityEvent
, RaiseClickableCircle
與 DoSomethingWhenClick
完全不知曉對方的存在。
使用基於事件架構(event-based architecture
)的好處是,它不會每一幀都在執行,只有在當事件發生時才會執行,因此效率會比在 MonoBehaviour
的 Update
方法中執行好。
Reference:
ScriptableObject as shared variable
ScriptableObject
可以作為遊戲變數在各個 MonoBehaviour
之間分享資料,此外,透過這種方式還可以減少 MonoBehaviour
物件之間互相參考的耦合度。
下面將使用 ScriptableObject
建立玩家生命數值,並在不同的 MonoBehaviour
之間使用
首先建立一個繼承 ScriptableObject
的類 FloatVariable
1 | [ ] |
接下來建立兩個 FloatVariable
實體, HP
與 MaxHP
然後建立繼承 MonoBehaviour
的 Player
類
1 | public class Player : MonoBehaviour |
在 Unity 編輯器中,將剛剛建立的 HP
與 MaxHP
設定到 Player
上
接著建立一個 HPView
1 | public class HPView : MonoBehaviour |
在 Unity 編輯器中,將 HP
與 MaxHP
設定到 HPView
上,
將 Player.Hurt()
設定給按鈕的 On Click 。
最後執行,在畫面上可以按下按鈕,觸發 Player.Hurt()
,可以觀察到 CurrentHPText
的變化
並且可以發現這兩個 MonoBehaviour
: Player
和 HPView
互相不知道彼此的存在,實現了鬆耦合(loosely coupled
)。
Reference:
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 | [ ] |
之後,你可以在 Assets
-> Create
-> ScriptableObjects
-> SpawnManagerScriptableObject
中建立新的 ScriptableObject
實體。
你可以更改該實體的名稱與它的屬性。
接下來你可以在 MonoBehaviour
中使用這個 ScriptableObject
實體,如下
1 | using UnityEngine; |
在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 按鈕時,會呼叫。Reference: