前提

阿茶最近在做unity的項目時,學習了很多的框架使用,在實現這些框架的時候,遇到了生命周期函數相關的繼承問題。

需要實現的內容

舉一個最簡單的例子,我製作了一個Enemy的基類,遊戲中所有的Enemies都需要繼承自該類。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Enemy : MonoBehaviour
{
private Rigidbody2D rigidbody;

void Start()
{
rigidbody = this.GetComponent<Rigidbody2D>();
}

void Update()
{
// ...
}
void Awake()
{
// ...
}
}

如上面的類,如果我每一個敵人的身體上都會掛在一個Rigitbody2D組件,那麽將其放在基類中進行查找和初始化是很方便的。

那麽問題來了,如果我的遊戲中有兩類敵人,一類是史萊姆,史萊姆身上要有一個Material組件來分別掛在不同屬性的材質球;第二類敵人是丘丘人,丘丘人的身上要有一個Sprite組件,用以標識丘丘人的狀態。

PS: 上面這個例子取自於生活中的例子並且加以改造,不保證實際情況亦是如此。

那麽史萊姆的類便可以這樣去寫:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Slime: Enemy
{
private Material material;

void Start()
{
material = MaterialManager.Instance.GetMaterial("Slime");
}

void Update()
{
// ...
}
void Awake()
{
// ...
}
}

而丘丘人的類如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Hilichurl: Enemy
{
private Sprite stateSprite;

void Start()
{
stateSprite = HilichrulState.Get("Hydro");
}

void Update()
{
// ...
}
void Awake()
{
// ...
}
}

遇到問題

這樣看似非常完美的能夠將父類的rigidbody繼承,在本類的實例化中調用,而且還可以初始化本類中的變量。

但是,在實際運行之後,便出現了rigidbody報錯為null的錯誤。

究其原因,是其父類(Enemy)的Start方法沒有被正確調用。期初,阿茶想的是,那麽將基類的方法直接繼承,然後調用基類的方法。因為C#是支持繼承和多態的。

解決問題

方案一

不過阿茶想到了一個問題,由於不太了解unity中這套生命周期的調用方法,VisualStudio自動生成的代碼和unity自動生成的代碼中,start、awake、update等生命周期函數都是私有化函數,而private標識是無法被繼承和重載的,因此阿茶思考是否能夠將其變為protected,不過考慮到自動生成時候都是private狀態的,遂在vs中嘗試使用cmd+click進入方法,發覺並非是virtual函數相關的內容,因此在嘗試檢索相關結果的時候看到了一個解決方法,那便是在基類中自己寫可以被重寫的方法,如OnStartOnUpdate等,然後再基類的生命周期中調用這些虛方法,子類中直接重寫覆蓋即可達成目的。

基類Enemy便可以修改成如下方法。

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
using UnityEngine;
using System.Collections;

public class Enemy : MonoBehaviour
{
private Rigidbody2D rigidbody;

void Start()
{
rigidbody = this.GetComponent<Rigidbody2D>();
OnStart();
}

void Update()
{
OnUpdate();
}

void Awake()
{
OnAwake();
}
protected virtual void OnAwake(){ }
protected virtual void OnStart(){ }
protected virtual void OnUpdate(){ }
}

而兩個類可以這樣去寫

那麽史萊姆的類便可以這樣去寫:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Slime: Enemy
{
private Material material;

protected override void OnStart()
{
material = MaterialManager.Instance.GetMaterial("Slime");
}

protected override void OnUpdate()
{
// ...
}
protected override void OnAwake()
{
// ...
}
}

而丘丘人的類如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Hilichurl: Enemy
{
private Sprite stateSprite;

protected override void OnStart()
{
stateSprite = HilichrulState.Get("Hydro");
}
protected override void OnUpdate()
{
// ...
}
protected override void OnAwake()
{
// ...
}
}

方案二

在阿茶接受並且記住使用方案一這種方式過去了20+天的某一天,阿茶無意間在看某個unity教程的時候,卻發現教程中對於這個問題的解決方式便是直接重寫Start等生命周期函數,而且還可以將標識符改變為protected,於是阿茶又去用英文嘗試查找了關鍵詞,這個時候才了解到,unity在調用生命周期函數的時候似乎用的是reflection方式,也就是說只要是名稱相同,甚至返回值不同都可以被unity進行調用。

因此該基類便可以修改生命周期函數的標識符從private變為protected,並且在繼承該基類的子類中重寫相關生命周期函數,然後執行相應的base.xx()方法即可。

因此三個類可以按照下述方式進行編寫:

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
using UnityEngine;
using System.Collections;

public class Enemy : MonoBehaviour
{
private Rigidbody2D rigidbody;

protected virtual void Start()
{
rigidbody = this.GetComponent<Rigidbody2D>();
}

protected virtual void Update()
{
// ...
}

protected virtual void Awake()
{
// ...
}
}

// 那麽史萊姆的類便可以這樣去寫:

public class Slime: Enemy
{
private Material material;

protected override void Start()
{
base.Start();
material = MaterialManager.Instance.GetMaterial("Slime");
}

protected override void Update()
{
base.Update();
// ...
}
protected override void Awake()
{
base.Awake();
// ...
}
}

// 而丘丘人的類如下

public class Hilichurl: Enemy
{
private Sprite stateSprite;

protected override void OnStart()
{
base.Start();
stateSprite = HilichrulState.Get("Hydro"); // 水元素のヒルチャール
}
protected override void Update()
{
base.Update();
// ...
}
protected override void Awake()
{
base.Awake();
// ...
}
}

雖然每一種方式都需要使用overrideprotected兩個關鍵詞對生命周期函數進行一定的改寫,但是在熟悉兩種方式之後阿茶更加傾向於第二種方式,理由是在基類中如果沒有使用到的如FixedUpdate等方法,可以直接在子類中調用而不用修改基類,雖然第一種方式也可以不用修改基類而直接調用,但是第一種方法並沒有在子類中執行對應的如Start等方法,會另阿茶感到不安。

因此覺得方案二優於方案一只是為了不讓阿茶感覺到不安的一種取舍,由於沒有經過控製變量的嚴格測試,這裏也無法判斷究竟是哪種方法更加好。

不過用師傅的話來說,能夠實現功能的代碼就是好代碼,所以關於孰優孰略這個問題就暫時交給時間去判別吧。

參考內容

关于继承MonoBehaviour的一些记录: https://www.cnblogs.com/hammerc/p/4492434.html

MonoBehaviour生命周期函数调用机制分析: https://www.cnblogs.com/syzhang/p/8994359.html