前言   到目前为止,我们已经能在场景中控制角色进行移动、攻击怪物并拾取道具,但我们还没有做出一个完整的游戏,我们还需要加入游戏的胜负条件。
游戏的胜负条件
胜利条件:每击杀一个怪物获得100分,当已获得的分数达到预设的分数时,游戏胜利
失败条件:当角色死亡时,游戏失败
  知道了游戏胜负条件之后,我们还需要加入游戏主逻辑管理器来管理整个游戏的状态。通常来说,一个游戏场景里面,只有唯一一个游戏主逻辑管理器。因此,我们可以使用单例模式来实现游戏主逻辑管理器。
什么是单例模式   单例模式 是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在,并提供一个访问这个唯一实例的全局访问点。
  单例模式的实现思路是:一个类定义一个静态的实例引用和一个获得该实例的方法(必须是静态方法),当我们调用获取静态实例的方法时,如果类持有的静态实例引用不为空就返回这个引用,如果类保持的静态实例引用为空就创建该类的实例,并将新创建的实例引用赋予该类持有的静态实例引用。同时我们还应该将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。
单例模式的两种构建方式:
懒汉方式(Lazy initialization):指单例类的单例实例在第一次被使用时构建。
饿汉方式(Eager initialization):指单例类的单例实例在类装载时先主动构建。
  由于Unity采用了组件化编程的方式,所以在Unity中,除了对象以外一切都是组件(Component),所有定义了继承自MonoBehaviour的类的C#脚本都需要先绑定到游戏对象(GameObject)上,Unity才会自动实例化该类,并在游戏运行时调用MonoBehaviour的各个事件函数。此外,我们不可以使用new关键字对继承自MonoBehaviour的类进行实例化,只能使用GetComponet<>函数来获取该类的实例对象,这就决定了如果我们使用饿汉方式(Eager initialization)来实现单例模式,那么我们只能在Awake函数中初始化单例对象。
  需要注意的是,根据Unity关于Event Functions的说明文档 ,Unity会按照随机的顺序执行所有继承自MonoBehaviour的类的Awake函数,因此如果我们使用饿汉方式(Eager initialization)来实现单例模式,那么我们需要保证单例类的Awake函数在其他继承自MonoBehaviour的类的Awake函数之前执行,否则将有可能出现空引用的问题。考虑到保证脚本之间的执行顺序工作量较大,为了避免出现空引用的问题,我们采用懒汉方式(Lazy initialization)来实现单例模式。
实现游戏主逻辑管理器框架   在了解了单例模式是什么,以及采用哪种方式实现单例模式之后,我们开始实现游戏主逻辑管理器。首先,我们在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 ; public static GameStateManager Instance { get { if (m_Instance == null ) { m_Instance = FindObjectOfType(typeof (GameStateManager)) as GameStateManager; 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; 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 () { 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" ); } } Destroy(Generator, delay); }#endregion #region 外部调用函数 public void SetGameResult (bool result ) { m_GameResult = result; m_CurrentState = GameState.End; }#endregion }
代码说明:
这里,我们自定义了GameInit、GameStart、GamePause、GameContinue、GameRunning和GameEnd这几个游戏状态函数,用于执行对应状态的代码。
其次,我们还使用了协程来实现GameMainLoop这个函数,用于切换和管理游戏状态
最后,我们使用键盘上的P来暂停和恢复游戏,使用Q来切换至游戏胜利,使用E来切换至游戏失败
  编辑完毕之后,我们在场景中新建一个名为GameStateManager的Empty GameObject,然后为其添加GameStateManager.cs,可以看到Unity自动帮我们添加了AudioSource组件。
GameStateManager物体上各个组件的属性设置:
Transform:
GameStateManager.cs
Background Music: Assets\Audio\Music文件夹下的MainTheme
GameWinClip: Assets\Audio\Music文件夹下的GameWin
GameLoseClip: Assets\Audio\Music文件夹下的GameLose
AudioSource:
Play On Awake: false
Vloume: 0.05
  为GameStateManager添加完组件之后,我们可以听到游戏场景中出现了背景音乐,且Console窗口输出了对应的游戏状态。此外,当我们按键盘上的P键时,游戏可以正常暂停和恢复;当我们按Q或者E时,会停止播放背景音乐,开始播放游戏胜利或者游戏失败的音效,并在播放完之后删除场景中的Generator物体,不再产生新的东西。
  但同时我们发现导弹击中物体时的爆炸音效音量过大,因此我们还需要将Assets\Prefabs\Weapons文件夹下的MissileExplosion上AudioSource组件的Volume设置为0.2。
加入游戏胜负条件   有了游戏主逻辑管理器框架之后,我们开始加入游戏胜负条件。如果我们直接在GameStateManager加入分数管理的代码,那么势必会出现GameStateManager里的代码多且杂的问题。我们应该让其他Manager来执行分数管理的工作,然后让管理整个游戏状态的GameStateManager来管理其他的Manager。
  清楚了这一设计思路之后,我们先在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; } }
代码说明:
因为我们使用GameStateManager来管理ScoreManager,所以ScoreManager不需要继承MonoBehaviour
因为ScoreManager没有继承MonoBehaviour,所以我们需要为ScoreManager添加Unity提供的Serializable这一Attribute,通过自定义序列化的方式 使ScoreManager能被Unity序列化
因为ScoreManager没有继承MonoBehaviour,所以我们不能使用协程,我们需要使用Time.time来实现延时执行某段代码的功能
  编辑完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 ; public static GameStateManager Instance { get { if (m_Instance == null ) { m_Instance = FindObjectOfType(typeof (GameStateManager)) as GameStateManager; 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; 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 () { 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" ); } } Destroy(Generator, delay); }#endregion #region 外部调用函数 public void SetGameResult (bool result ) { m_GameResult = result; m_CurrentState = GameState.End; }#endregion }
  接着,我们在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
  设置好ScoreManager类定义的公共字段之后,我们还需要在Remover.cs的OnTriggerEnter2D加入设置游戏结果的代码:
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); } }
  最后,我们还需要修改一下CameraFollow.cs的LateUpdate函数:
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(); } } ... }
  修改完成之后,运行游戏,可以看到当我们打死怪物时,Console窗口会输出我们当前不断增加的分数,且一定概率会播放嘲讽音效。此外,当角色死亡或者直接掉进河里时,背景音乐停止播放并播放游戏失败的音效。
添加击杀怪物得分特效   因为击杀怪物可以得分,所以我们需要添加击杀怪物得分的特效。
得分特效的制作步骤:
在场景中新建一个名为Score的Empty GameObject,然后将Assets\Sprites\UI下的numeric-1和numeric-0图片拖拽至Score物体下成为Score物体的子物体
复制Score物体下的numeric-0得到numeric-0 (1)物体
设置numeric-1的Position为(-0.4, 0, 0),Sprite Renderer组件的Sorting Layer属性为Character、Order In Layer属性为10
设置numeric-0的Position为(0, 0, 0),Sprite Renderer组件的Sorting Layer属性为Character、Order In Layer属性为10
设置numeric-0 (1)的Position为(0.5, 0, 0),Sprite Renderer组件的Sorting Layer属性为Character、Order In Layer属性为10
打开Animation窗口,选中Hierarchy窗口中的Score物体,然后点击Animation窗口中的Create按钮创建一个名为Score.anim的动画文件,并将其保存在Assets\Animation\Enemy文件夹下,最后将Score.controller文件移至Assets\Animator\Enemy文件夹下
为Score物体添加Destrpyer.cs脚本
点击Animation窗口的红点按钮,开始为Score.anim添加关键帧,添加的关键帧信息如下:
Score.anim的关键帧:
第一帧:
frame: 0
numeric-1:Position: (-0.4, 0, 0)
numeric-0:Position: (0, 0, 0)
numeric-0 (1):Position: (0.5, 0, 0)
第二帧:
frame: 5
numeric-1:Position: (-0.4, 0.25, 0)
第三帧:
frame: 10
numeric-0:Position: (0, 0.31, 0)
第四帧:
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)
第五帧:
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)
第六帧:
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)
在Score.anim的最后一帧处添加一个Animation Event,选择调用的函数为DestroyGameObject
将场景中的Score物体拖拽至Assets\Prefabs\Character文件夹将其制作为Prefab,然后删除场景中的Score物体
  至此,我们的得分特效就制作好了。接下来,我们改写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 ) { m_CurrentHP -= damage; Vector3 hurtVector = transform.position - weapon.position; m_Rigidbody2D.AddForce(hurtVector.normalized * hurtForce); if (!m_Hurt) { m_Hurt = true ; if (DamagedSprite != null ) { 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 () { m_Wander.enabled = false ; if (DeadSprite != null ) { 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[] 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 ); } }
  最后,我们分别选中Assets\Prefabs\Character文件夹下的AlienShip和AlienSlug这两个Prefab,将其Enemy.cs上的Score Prefab字段都设置为Assets\Prefabs\Character文件夹下的Score物体对应的Prefab。运行游戏,我们可以看到击杀怪物时,怪物上方会出现得分特效。
后言   至此,我们就已经完成了使用单例模式实现游戏游戏主逻辑管理器的所有功能。最后,本篇文章所做的修改,可以在PotatoGloryTutorial 这个仓库的essay17分支下看到,读者可以clone这个仓库到本地进行查看。
参考链接
单例模式
Unity-Initialization Events
Unity-Custom serialization