《土豆荣耀》重构笔记(十九)使用单例模式实现游戏主逻辑管理器

前言

  到目前为止,我们已经能在场景中控制角色进行移动攻击怪物拾取道具,但我们还没有做出一个完整的游戏,我们还需要加入游戏的胜负条件

游戏的胜负条件

  1. 胜利条件:每击杀一个怪物获得100分,当已获得的分数达到预设的分数时,游戏胜利
  2. 失败条件:当角色死亡时,游戏失败

  知道了游戏胜负条件之后,我们还需要加入游戏主逻辑管理器来管理整个游戏的状态。通常来说,一个游戏场景里面,只有唯一一个游戏主逻辑管理器。因此,我们可以使用单例模式来实现游戏主逻辑管理器


什么是单例模式

  单例模式是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在,并提供一个访问这个唯一实例全局访问点

  单例模式的实现思路是:一个类定义一个静态的实例引用一个获得该实例的方法(必须是静态方法),当我们调用获取静态实例的方法时,如果类持有的静态实例引用不为空就返回这个引用,如果类保持的静态实例引用为空就创建该类的实例,并将新创建的实例引用赋予该类持有的静态实例引用。同时我们还应该将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例

单例模式的两种构建方式:

  1. 懒汉方式(Lazy initialization):指单例类的单例实例在第一次被使用时构建。
  2. 饿汉方式(Eager initialization):指单例类的单例实例在类装载时先主动构建。

&emsp;&emsp;由于Unity采用了组件化编程的方式,所以在Unity中,除了对象以外一切都是组件(Component),所有定义了继承自MonoBehaviour的类的C#脚本都需要先绑定到游戏对象(GameObject)上,Unity才会自动实例化该类,并在游戏运行时调用MonoBehaviour的各个事件函数。此外,我们不可以使用new关键字对继承自MonoBehaviour的类进行实例化,只能使用GetComponet<>函数来获取该类的实例对象,这就决定了如果我们使用饿汉方式(Eager initialization)来实现单例模式,那么我们只能在Awake函数中初始化单例对象

&emsp;&emsp;需要注意的是,根据Unity关于Event Functions的说明文档,Unity会按照随机的顺序执行所有继承自MonoBehaviour的类的Awake函数,因此如果我们使用饿汉方式(Eager initialization)来实现单例模式,那么我们需要保证单例类Awake函数在其他继承自MonoBehaviour的类的Awake函数之前执行,否则将有可能出现空引用的问题。考虑到保证脚本之间的执行顺序工作量较大,为了避免出现空引用的问题,我们采用懒汉方式(Lazy initialization)来实现单例模式


实现游戏主逻辑管理器框架

&emsp;&emsp;在了解了单例模式是什么,以及采用哪种方式实现单例模式之后,我们开始实现游戏主逻辑管理器。首先,我们在Assets\Scripts文件夹下创建一个名为Manager的文件夹,然后在Assets\Scripts\Manager文件夹下创建一个名为GameStateManager的C#脚本。接着,我们编辑GameStateManager.cs如下:

GameStateManager.cs
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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 游戏状态
public enum GameState {
Init,
Start,
Running,
End
}

[RequireComponent(typeof(AudioSource))]
public class GameStateManager : MonoBehaviour {
// 静态实例
private static GameStateManager m_Instance = null;
// 使用Property来访问静态实例
public static GameStateManager Instance {
get {
if (m_Instance == null) {
m_Instance = FindObjectOfType(typeof(GameStateManager)) as GameStateManager;

// 场景中没有添加了GameStateManager.cs脚本的GameObject,就自动创建一个
if (m_Instance == null) {
GameObject obj = new GameObject("GameStateManager");
m_Instance = obj.AddComponent<GameStateManager>();
}
}

// 返回静态实例的引用
return m_Instance;
}
}

[Tooltip("游戏运行时的背景音乐")]
public AudioClip BackgroundMusic;
[Tooltip("游戏胜利时的音效")]
public AudioClip GameWinClip;
[Tooltip("游戏失败时的音效")]
public AudioClip GameLoseClip;

// 游戏处于哪个状态
private GameState m_CurrentState;
// 游戏是否处于暂停状态
private bool m_IsPaused;
// 游戏结果,true为胜利,false为失败
private bool m_GameResult;

private AudioSource m_AudioSource;

#region MonoBehaviour的事件函数
private void Awake() {
// 初始化组件
m_AudioSource = GetComponent<AudioSource>();
m_AudioSource.playOnAwake = false;
}

private void Start() {
// 初始化成员变量
m_IsPaused = false;
m_CurrentState = GameState.Init;

// 开始游戏主循环
StartCoroutine(GameMainLoop());
}
#endregion

#region 自定义游戏状态函数
private IEnumerator GameMainLoop() {
GameInit();

while(m_CurrentState == GameState.Init) {
yield return null;
}

GameStart();

while(m_CurrentState == GameState.Running) {
GameRunning();

yield return null;
}

GameEnd();
}

// 游戏初始化
private void GameInit() {
// 执行一些游戏预操作,例如初始化其他Manager、播放过场动画和进行倒计时等
Debug.Log("Game Init");

// 进入游戏开始状态
m_CurrentState = GameState.Start;
}

// 游戏开始
private void GameStart() {
Debug.Log("Game Start");

// 开始播放背景音乐
if(BackgroundMusic != null) {
m_AudioSource.clip = BackgroundMusic;
m_AudioSource.loop = true;
m_AudioSource.Play();
} else {
Debug.LogError("请设置BackgroundMusic");
}

// 进入游戏运行状态
m_CurrentState = GameState.Running;
}

// 暂停游戏
private void GamePause() {
Debug.Log("Game Pause");

// 暂停背景音乐的播放
m_AudioSource.Pause();
// 暂停游戏
Time.timeScale = 0f;

m_IsPaused = true;
}

// 继续游戏
private void GameContinue() {
Debug.Log("Game Continue");

// 恢复背景音乐的播放
Time.timeScale = 1f;
// 恢复游戏
m_AudioSource.UnPause();

m_IsPaused = false;
}

// 游戏运行
private void GameRunning() {
Debug.Log("Game Running");

// 暂停或者恢复游戏
if(Input.GetKeyDown(KeyCode.P)) {
if(m_IsPaused) {
GameContinue();
} else {
GamePause();
}
}

if(Input.GetKeyDown(KeyCode.E)) {
SetGameResult(false);
}

if(Input.GetKeyDown(KeyCode.Q)) {
SetGameResult(true);
}
}

// 游戏结束
private void GameEnd() {
Debug.Log("Game End");

// 停止播放背景音乐
m_AudioSource.Stop();
m_AudioSource.loop = false;

float delay = 0f;

if(m_GameResult) {
if(GameWinClip != null) {
AudioSource.PlayClipAtPoint(GameWinClip, this.transform.position);
delay = GameWinClip.length;
} else {
Debug.LogError("请设置GameWinClip");
}
} else {
if(GameLoseClip != null) {
AudioSource.PlayClipAtPoint(GameLoseClip, this.transform.position);
delay = GameLoseClip.length;
} else {
Debug.LogError("请设置GameLoseClip");
}
}

// 播放完音效之后,删除场景中的所有Generator
Destroy(Generator, delay);
}
#endregion

#region 外部调用函数
// 设置游戏结果
public void SetGameResult(bool result) {
m_GameResult = result;
m_CurrentState = GameState.End;
}
#endregion
}

代码说明:

  1. 这里,我们自定义了GameInitGameStartGamePauseGameContinueGameRunningGameEnd这几个游戏状态函数,用于执行对应状态的代码。
  2. 其次,我们还使用了协程来实现GameMainLoop这个函数,用于切换和管理游戏状态
  3. 最后,我们使用键盘上的P暂停和恢复游戏,使用Q来切换至游戏胜利,使用E来切换至游戏失败

&emsp;&emsp;编辑完毕之后,我们在场景中新建一个名为GameStateManagerEmpty GameObject,然后为其添加GameStateManager.cs,可以看到Unity自动帮我们添加了AudioSource组件。

GameStateManager物体上各个组件的属性设置:

  1. Transform
    • Position:(0, 0, 0)
  2. GameStateManager.cs
    • Background Music: Assets\Audio\Music文件夹下的MainTheme
    • GameWinClip: Assets\Audio\Music文件夹下的GameWin
    • GameLoseClip: Assets\Audio\Music文件夹下的GameLose
  3. AudioSource:
    • Play On Awake: false
    • Vloume: 0.05

GameStateManager的组件

&emsp;&emsp;为GameStateManager添加完组件之后,我们可以听到游戏场景中出现了背景音乐,且Console窗口输出了对应的游戏状态。此外,当我们按键盘上的P键时,游戏可以正常暂停和恢复;当我们按Q或者E时,会停止播放背景音乐,开始播放游戏胜利或者游戏失败的音效,并在播放完之后删除场景中的Generator物体,不再产生新的东西。

&emsp;&emsp;但同时我们发现导弹击中物体时的爆炸音效音量过大,因此我们还需要将Assets\Prefabs\Weapons文件夹下的MissileExplosionAudioSource组件的Volume设置为0.2

MissileExplosion的组件


加入游戏胜负条件

&emsp;&emsp;有了游戏主逻辑管理器框架之后,我们开始加入游戏胜负条件。如果我们直接在GameStateManager加入分数管理的代码,那么势必会出现GameStateManager里的代码多且杂的问题。我们应该让其他Manager来执行分数管理的工作,然后让管理整个游戏状态的GameStateManager来管理其他的Manager

&emsp;&emsp;清楚了这一设计思路之后,我们先在Assets\Scripts\Manager文件夹下创建一个名为ScoreManager的C#脚本,然后编辑ScoreManager.cs如下:

ScoreManager.cs
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
73
74
75
76
77
using System;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public class ScoreManager {
[Tooltip("游戏胜利的目标分数")]
public int TargetScore = 5000;

[Tooltip("保存嘲讽音效")]
public AudioClip[] TauntClips;
[Tooltip("得分之后播放嘲讽音效的概率")]
public float TauntProbaility = 50f;
[Tooltip("嘲讽的间隔")]
public float TauntDelay = 1f;

// 当前的分数
private int m_CurrentScore;
// 上一次播放的嘲讽音效的下标
private int m_TauntIndex;
// 上一次播放嘲讽音效的时间
private float m_LastTauntTime;
// 当前管理器是否停止工作
private bool m_Stop;

private Transform m_Player;

public void Init(Transform player) {
m_CurrentScore = 0;
m_TauntIndex = 0;
m_LastTauntTime = Time.time;
m_Stop = false;

m_Player = player;
}

// 管理器停止工作
public void Stop() {
m_Stop = true;
}

public void AddScore(int score) {
// 管理器停止工作,不执行任何操作
if(m_Stop) {
return;
}

// 增加分数
m_CurrentScore += score;

// 达到目标分数,游戏胜利
if(m_CurrentScore >= TargetScore) {
GameStateManager.Instance.SetGameResult(true);
}

if(m_LastTauntTime <= Time.time + TauntDelay) {
float tauntChance = UnityEngine.Random.Range(0f, 100f);

if(tauntChance > TauntProbaility) {
// 播放嘲讽音效
m_TauntIndex = TauntRandom();
AudioSource.PlayClipAtPoint(TauntClips[m_TauntIndex], m_Player.position);
}
}
}


//确保相邻两次嘲讽音效不相同
private int TauntRandom() {
int i = UnityEngine.Random.Range(0, TauntClips.Length);

if (i == m_TauntIndex)
return TauntRandom();
else
return i;
}
}

代码说明:

  1. 因为我们使用GameStateManager来管理ScoreManager,所以ScoreManager不需要继承MonoBehaviour
  2. 因为ScoreManager没有继承MonoBehaviour,所以我们需要为ScoreManager添加Unity提供的Serializable这一Attribute,通过自定义序列化的方式使ScoreManager能被Unity序列化
  3. 因为ScoreManager没有继承MonoBehaviour,所以我们不能使用协程,我们需要使用Time.time来实现延时执行某段代码的功能

&emsp;&emsp;编辑完ScoreManager.cs之后,我们修改GameStateManager.cs,删除用于测试的代码,并加入管理ScoreManager的代码:

GameStateManager.cs
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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 游戏状态
public enum GameState {
Init,
Start,
Running,
End
}

[RequireComponent(typeof(AudioSource))]
public class GameStateManager : MonoBehaviour {
// 静态实例
private static GameStateManager m_Instance = null;
// 使用Property来访问静态实例
public static GameStateManager Instance {
get {
if (m_Instance == null) {
m_Instance = FindObjectOfType(typeof(GameStateManager)) as GameStateManager;

// 场景中没有添加了GameStateManager.cs脚本的GameObject,就自动创建一个
if (m_Instance == null) {
GameObject obj = new GameObject("GameStateManager");
m_Instance = obj.AddComponent<GameStateManager>();
}
}

// 返回静态实例的引用
return m_Instance;
}
}

[Tooltip("游戏运行时的背景音乐")]
public AudioClip BackgroundMusic;
[Tooltip("游戏胜利时的音效")]
public AudioClip GameWinClip;
[Tooltip("游戏失败时的音效")]
public AudioClip GameLoseClip;
[Tooltip("ScoreManager的实例")]
public ScoreManager ScoreManagerInstance = new ScoreManager();

// 游戏处于哪个状态
private GameState m_CurrentState;
// 游戏是否处于暂停状态
private bool m_IsPaused;
// 游戏结果,true为胜利,false为失败
private bool m_GameResult;

private AudioSource m_AudioSource;

#region MonoBehaviour的事件函数
private void Awake() {
// 初始化组件
m_AudioSource = GetComponent<AudioSource>();
m_AudioSource.playOnAwake = false;
}

private void Start() {
// 初始化成员变量
m_IsPaused = false;
m_CurrentState = GameState.Init;

// 开始游戏主循环
StartCoroutine(GameMainLoop());
}
#endregion

#region 自定义游戏状态函数
private IEnumerator GameMainLoop() {
GameInit();

while(m_CurrentState == GameState.Init) {
yield return null;
}

GameStart();

while(m_CurrentState == GameState.Running) {
GameRunning();

yield return null;
}

GameEnd();
}

// 游戏初始化
private void GameInit() {
// 执行一些游戏预操作,例如初始化其他Manager、播放过场动画和进行倒计时等
ScoreManagerInstance.Init();

// 进入游戏开始状态
m_CurrentState = GameState.Start;
}

// 游戏开始
private void GameStart() {
// 开始播放背景音乐
if(BackgroundMusic != null) {
m_AudioSource.clip = BackgroundMusic;
m_AudioSource.loop = true;
m_AudioSource.Play();
} else {
Debug.LogError("请设置BackgroundMusic");
}

// 进入游戏运行状态
m_CurrentState = GameState.Running;
}

// 暂停游戏
private void GamePause() {
// 暂停背景音乐的播放
m_AudioSource.Pause();
// 暂停游戏
Time.timeScale = 0f;

m_IsPaused = true;
}

// 继续游戏
private void GameContinue() {
// 恢复背景音乐的播放
Time.timeScale = 1f;
// 恢复游戏
m_AudioSource.UnPause();

m_IsPaused = false;
}

// 游戏运行
private void GameRunning() {
// 暂停或者恢复游戏
if(Input.GetKeyDown(KeyCode.P)) {
if(m_IsPaused) {
GameContinue();
} else {
GamePause();
}
}
}

// 游戏结束
private void GameEnd() {
// 停止播放背景音乐
m_AudioSource.Stop();
m_AudioSource.loop = false;

float delay = 0f;

if(m_GameResult) {
if(GameWinClip != null) {
AudioSource.PlayClipAtPoint(GameWinClip, this.transform.position);
delay = GameWinClip.length;
} else {
Debug.LogError("请设置GameWinClip");
}
} else {
if(GameLoseClip != null) {
AudioSource.PlayClipAtPoint(GameLoseClip, this.transform.position);
delay = GameLoseClip.length;
} else {
Debug.LogError("请设置GameLoseClip");
}
}

// 播放完音效之后,删除场景中的所有Generator
Destroy(Generator, delay);
}
#endregion

#region 外部调用函数
// 设置游戏结果
public void SetGameResult(bool result) {
m_GameResult = result;
m_CurrentState = GameState.End;
}
#endregion
}

&emsp;&emsp;接着,我们在Hierarchy窗口中选中GameStateManager物体,可以看到Inspector窗口多出了一个名为Score Manager Instance折叠框,且在Score Manager Instance折叠框下出现了ScoreManager类里定义的公共字段。

ScoreManager类定义的公共字段设置:

  • Target Score: 5000
  • Taunt Clips: Assets\Audio\Player\Taunts文件夹下的9个音频
  • Taunt Probaility: 50
  • Taunt Delay: 1

GameStateManager脚本的属性设置

&emsp;&emsp;设置好ScoreManager类定义的公共字段之后,我们还需要在Remover.csOnTriggerEnter2D加入设置游戏结果的代码:

Remover.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(BoxCollider2D))]
public class Remover : MonoBehaviour {
...

private void OnTriggerEnter2D(Collider2D collision) {
// 角色掉进河里,游戏失败
if(collision.CompareTag("Player")) {
GameStateManager.Instance.SetGameResult(false);
}

// 实例化水花对象,水花对象会自动播放声音和动画
Instantiate(SplashPrefab, collision.transform.position, transform.rotation);
// 销毁掉下去的物体
Destroy(collision.gameObject);
}
}

&emsp;&emsp;最后,我们还需要修改一下CameraFollow.csLateUpdate函数:

CameraFollow.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraFollow : MonoBehaviour {
...

private void LateUpdate() {
// 如果角色被销毁,不再进行跟随
if(m_Player != null) {
TrackPlayer();
}
}

...
}

&emsp;&emsp;修改完成之后,运行游戏,可以看到当我们打死怪物时,Console窗口会输出我们当前不断增加的分数,且一定概率会播放嘲讽音效。此外,当角色死亡或者直接掉进河里时,背景音乐停止播放并播放游戏失败的音效


添加击杀怪物得分特效

&emsp;&emsp;因为击杀怪物可以得分,所以我们需要添加击杀怪物得分的特效

得分特效的制作步骤:

  1. 在场景中新建一个名为ScoreEmpty GameObject,然后将Assets\Sprites\UI下的numeric-1numeric-0图片拖拽至Score物体下成为Score物体的子物体
  2. 复制Score物体下的numeric-0得到numeric-0 (1)物体
  3. 设置numeric-1Position(-0.4, 0, 0)Sprite Renderer组件的Sorting Layer属性为CharacterOrder In Layer属性为10
  4. 设置numeric-0Position(0, 0, 0)Sprite Renderer组件的Sorting Layer属性为CharacterOrder In Layer属性为10
  5. 设置numeric-0 (1)Position(0.5, 0, 0)Sprite Renderer组件的Sorting Layer属性为CharacterOrder In Layer属性为10
  6. 打开Animation窗口,选中Hierarchy窗口中的Score物体,然后点击Animation窗口中的Create按钮创建一个名为Score.anim的动画文件,并将其保存在Assets\Animation\Enemy文件夹下,最后将Score.controller文件移至Assets\Animator\Enemy文件夹下
  7. Score物体添加Destrpyer.cs脚本
  8. 点击Animation窗口的红点按钮,开始为Score.anim添加关键帧,添加的关键帧信息如下:

    Score.anim的关键帧:

    1. 第一帧
      • frame: 0
      • numeric-1:Position: (-0.4, 0, 0)
      • numeric-0:Position: (0, 0, 0)
      • numeric-0 (1):Position: (0.5, 0, 0)
    2. 第二帧
      • frame: 5
      • numeric-1:Position: (-0.4, 0.25, 0)
    3. 第三帧
      • frame: 10
      • numeric-0:Position: (0, 0.31, 0)
    4. 第四帧
      • frame: 30
      • numeric-1:Position: (-0.4, 0.78, 0)
      • numeric-0:Position: (0, 0.78, 0)
      • numeric-0 (1):Position: (0.5, 0.78, 0)
    5. 第五帧
      • frame: 40
      • numeric-1:Position: (-0.4, 1.1, 0)
      • numeric-0:Position: (0, 1.1, 0)
      • numeric-0 (1):Position: (0.5, 1.1, 0)
    6. 第六帧
      • frame: 60
      • numeric-1:Position: (-0.4, 1.25, 0)
      • numeric-0:Position: (0, 1.25, 0)
      • numeric-0 (1):Position: (0.5, 1.25, 0)
  9. Score.anim的最后一帧处添加一个Animation Event,选择调用的函数为DestroyGameObject
  10. 将场景中的Score物体拖拽至Assets\Prefabs\Character文件夹将其制作为Prefab,然后删除场景中的Score物体

&emsp;&emsp;至此,我们的得分特效就制作好了。接下来,我们改写Enemy.cs,在怪物死亡时生成得分特效:

Enemy.cs
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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Wander))]
[RequireComponent(typeof(Rigidbody2D))]
public class Enemy : MonoBehaviour {
[Tooltip("角色受伤时减少的血量")]
public float DamageAmount = 10f;
[Tooltip("角色被怪物伤害时受到的击退力大小")]
public float HurtForce = 500f;
[Tooltip("障碍物检测点")]
[SerializeField]
private Transform FrontCheck;
[Tooltip("怪物的血量")]
public float MaxHP = 10f;
[Tooltip("怪物受伤时用来展示的图片")]
public Sprite DamagedSprite;
[Tooltip("怪物死亡时用来展示的图片")]
public Sprite DeadSprite;
[Tooltip("怪物死亡时用来展示DeadSprite")]
public SpriteRenderer BodySpriteRenderer;

[Tooltip("怪物死亡时的音效")]
public AudioClip[] DeathClips;
[Tooltip("得分特效")]
public GameObject ScorePrefab;

private Wander m_Wander;
private Rigidbody2D m_Rigidbody2D;

private LayerMask m_LayerMask;
private float m_CurrentHP;
private bool m_Hurt;
private bool m_Dead;

private void Awake() {
// 获取引用
m_Wander = GetComponent<Wander>();
m_Rigidbody2D = GetComponent<Rigidbody2D>();
}

private void Start() {
// 初始化变量
m_LayerMask = LayerMask.GetMask("Obstacle");
m_CurrentHP = MaxHP;
m_Hurt = false;
m_Dead = false;
}

private void Update () {
// 死亡之后不执行任何操作
if(m_Dead) {
return;
}

Collider2D[] frontHits = Physics2D.OverlapPointAll(FrontCheck.position, m_LayerMask);

if(frontHits.Length > 0) {
m_Wander.Flip();
}
}

private void OnCollisionEnter2D(Collision2D collision) {
// 对角色造成伤害
if(collision.gameObject.CompareTag("Player")) {
collision.gameObject.GetComponent<PlayerHealth>().TakeDamage(this.transform, HurtForce, DamageAmount);
}
}

// 受伤函数
public void TakeDamage(Transform weapon, float hurtForce, float damage) {
// 减少当前的HP
m_CurrentHP -= damage;

// 制造击退效果
Vector3 hurtVector = transform.position - weapon.position;
m_Rigidbody2D.AddForce(hurtVector.normalized * hurtForce);

// 判断当前是否第一次受伤
if(!m_Hurt) {
m_Hurt = true;

if(DamagedSprite != null) {
// 禁用原有的Sprite
SpriteRenderer[] children = GetComponentsInChildren<SpriteRenderer>();
foreach(SpriteRenderer child in children) {
child.enabled = false;
}

// 显示怪物受伤图片
if(BodySpriteRenderer != null) {
BodySpriteRenderer.enabled = true;
BodySpriteRenderer.sprite = DamagedSprite;
} else {
Debug.LogError("请设置BodySpriteRenderer");
}
} else {
Debug.LogWarning("请设置DamagedSprite");
}
}

// 判断当前的是否死亡
if(m_CurrentHP <= 0 && !m_Dead) {
m_Dead = true;
Death();
}
}

private void Death() {
// 禁用Wander.cs
m_Wander.enabled = false;

if(DeadSprite != null) {
// 禁用原有的Sprite
SpriteRenderer[] children = GetComponentsInChildren<SpriteRenderer>();
foreach(SpriteRenderer child in children) {
child.enabled = false;
}

// 显示怪物死亡图片
if(BodySpriteRenderer != null) {
BodySpriteRenderer.enabled = true;
BodySpriteRenderer.sprite = DeadSprite;
} else {
Debug.LogError("请设置BodySpriteRenderer");
}
} else {
Debug.LogWarning("请设置DeadSprite");
}

// 将所有的Collider2D都设置为Trigger,避免和其他物体产生物理碰撞
Collider2D[] cols = GetComponents<Collider2D>();
foreach(Collider2D c in cols) {
c.isTrigger = true;
}

// 随机播放死亡的音效
if(DeathClips != null && DeathClips.Length > 0) {
int i = Random.Range(0, DeathClips.Length);
AudioSource.PlayClipAtPoint(DeathClips[i], transform.position);
} else {
Debug.LogWarning("请设置DeathClips");
}

// 生成得分特效
if(ScorePrefab != null) {
Vector3 scorePos = this.transform.position + Vector3.up * 1.5f;
Instantiate(ScorePrefab, scorePos, Quaternion.identity);
} else {
Debug.LogError("请设置ScorePrefab");
}

// 增加分数
GameStateManager.Instance.ScoreManagerInstance.AddScore(100);
}
}

&emsp;&emsp;最后,我们分别选中Assets\Prefabs\Character文件夹下的AlienShipAlienSlug这两个Prefab,将其Enemy.cs上的Score Prefab字段都设置为Assets\Prefabs\Character文件夹下的Score物体对应的Prefab。运行游戏,我们可以看到击杀怪物时,怪物上方会出现得分特效。


后言

&emsp;&emsp;至此,我们就已经完成了使用单例模式实现游戏游戏主逻辑管理器的所有功能。最后,本篇文章所做的修改,可以在PotatoGloryTutorial这个仓库的essay17分支下看到,读者可以clone这个仓库到本地进行查看。


参考链接

  1. 单例模式
  2. Unity-Initialization Events
  3. Unity-Custom serialization

《土豆荣耀》重构笔记(十九)使用单例模式实现游戏主逻辑管理器
https://asancai.github.io/posts/733f40fd/
作者
RainbowCyan
发布于
2019年1月29日
许可协议