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:

評論