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
| [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) { 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(); } }
|
定義一個 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) { 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); }
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: