前言   在上篇文章 中,我们已经实现了游戏主逻辑管理器,并加入了游戏的胜负条件。但在游戏中,玩家并不能清晰地知道当前自己获得了多少分数、游戏胜利的目标分数是多少以及当前还能释放多少颗炸弹,我们需要加入UI进行提示。
加入BombManager   在前面的文章中,因为我们还没有实现游戏主逻辑管理器,为了方便测试,我们直接在PlayerAttack.cs里面实现管理炸弹数量和释放炸弹的功能。因为PlayerAttack.cs的职责是释放炸弹和导弹,因此,我们需要将管理炸弹数量的代码从PlayerAttack.cs中抽取出来,并创建一个Manager来负责管理炸弹数量的工作。
  首先,我们在Assets\Scripts\Manager文件夹下创建一个名为BombManager的C#脚本,然后编辑BombManager.cs如下:
BombManager.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 using System;using System.Collections;using System.Collections.Generic;using UnityEngine; [Serializable ]public class BombManager { [Tooltip("炸弹的初始数量" ) ] public int InitBombNumber = 4 ; private int m_CurrentBombNumber; public void Init () { m_CurrentBombNumber = InitBombNumber; } public bool ReleaseBomb (int bombNum ) { int temp = m_CurrentBombNumber - bombNum; if (temp >= 0 ) { m_CurrentBombNumber = temp; return true ; } else { return false ; } } public void PickupBomb (int bombNum ) { m_CurrentBombNumber += bombNum; } }
  接着,还需要在GameStateManager.cs中添加对BombManager.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 [RequireComponent(typeof(AudioSource)) ]public class GameStateManager : MonoBehaviour { ... [Tooltip("ScoreManager的实例" ) ] public ScoreManager ScoreManagerInstance = new ScoreManager(); [Tooltip("BombManager的实例" ) ] public BombManager BombManagerInstance = new BombManager(); ... private void GameInit () { ScoreManagerInstance.Init(); BombManagerInstance.Init(); m_CurrentState = GameState.Start; } private void GameEnd () { m_AudioSource.Stop(); m_AudioSource.loop = false ; ScoreManagerInstance.Stop(); BombManagerInstance.Stop(); 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); } ... }
  修改完GameStateManager.cs之后,我们还需要删除PlayerAttack.cs中管理炸弹数量的代码,并使用BombManager提供的ReleaseBomb函数来释放炸弹:
PlayerAttack.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 using System.Collections;using System.Collections.Generic;using UnityEngine; [RequireComponent(typeof(PlayerController)) ]public class PlayerAttack : MonoBehaviour { [Tooltip("导弹Prefab" ) ] public Missile MissilePrefab; [Tooltip("导弹发射点" ) ] public Transform ShootingPoint; [Tooltip("发射导弹的音效" ) ] public AudioClip ShootEffect; [Tooltip("炸弹Prefab" ) ] public Rigidbody2D BombPrefab; [Tooltip("使用火箭筒抛射炸弹的力" ) ] public float ProjectileBombForce = 1000f ; private PlayerController m_PlayerCtrl; private void Awake () { m_PlayerCtrl = GetComponent<PlayerController>(); if (MissilePrefab == null ) { Debug.LogError("请设置MissilePrefab" ); } if (ShootingPoint == null ) { Debug.LogError("请设置ShootingPoint" ); } if (BombPrefab == null ) { Debug.LogError("请设置BombPrefab" ); } } private void Update () { if (Input.GetButtonDown("Fire1" )) { Fire(); } if (Input.GetButtonDown("Fire2" )) { LayBomb(); } if (Input.GetButtonDown("Fire3" )) { ProjectileBomb(); } } private void Fire () { AudioSource.PlayClipAtPoint(ShootEffect, ShootingPoint.position); Missile instance = Instantiate(MissilePrefab, ShootingPoint.position, Quaternion.identity) as Missile; if (m_PlayerCtrl.FacingRight ^ instance.FacingRight) { instance.Flip(); } } private void LayBomb () { if (GameStateManager.Instance.BombManagerInstance.ReleaseBomb(1 ) == false ) { return ; } Instantiate(BombPrefab, this .transform.position, Quaternion.identity); } private void ProjectileBomb () { if (GameStateManager.Instance.BombManagerInstance.ReleaseBomb(1 ) == false ) { return ; } Rigidbody2D body = Instantiate(BombPrefab, ShootingPoint.position, Quaternion.identity) as Rigidbody2D; if (m_PlayerCtrl.FacingRight) { body.AddForce(Vector2.right * ProjectileBombForce); } else { body.AddForce(Vector2.left * ProjectileBombForce); } } }
  最后,我们还需要在AmmunitionBoxPickup.cs中使用BombManager提供的PickupBomb函数来拾取炸弹:
AmmunitionBoxPickup.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 [RequireComponent(typeof(CircleCollider2D)) ] [RequireComponent(typeof(BoxCollider2D)) ]public class AmmunitionBoxPickup : MonoBehaviour { ... private void OnTriggerEnter2D (Collider2D collision ) { if (collision.tag == "Ground" && !m_Landed) { m_Landed = true ; transform.parent = null ; gameObject.AddComponent<Rigidbody2D>(); m_Animator.SetTrigger("Landing" ); return ; } if (collision.tag == "Player" ) { GameStateManager.Instance.BombManagerInstance.PickupBomb(BombAmount); AudioSource.PlayClipAtPoint(PickupEffect, transform.position); Destroy(transform.root.gameObject); } } }
  运行游戏,可以看到此时我们能正常释放和拾取炸弹。至此,代码重构完成,后面我们就可以在BombManager中对和炸弹有关的UI进行管理了。
创建Canvas   添加完BombManager之后,我们开始添加UI的工作。在Unity中,Canvas控制UI的绘制和缩放。只有成为Canvas的子物体,UI才能够被正常绘制,且Canvas会按照从上到下的顺序来绘制它的子物体,也就是上面的UI会被下面的UI覆盖。此外,根据Canvas的说明文档 ,我们知道Unity的Canvas有三种绘制的方式,它们的区别如下:
Canvas三种绘制的方式:
Screen Space - Overlay: 这是Canvas默认的绘制方式。使用Screen Space - Overlay的绘制方式,Canvas不受场景摄像机的影响,直接在屏幕上进行绘制,且在不同的屏幕分辨率下,Canvas会自动适配屏幕的分辨率大小。由于Canvas不受摄像机影响,所以整个过程Canvas都是保持静态的,也就不需要重新计算Canvas的位置和角度,是一种优化性较好的绘制方式。
Screen Space - Camera: 使用Screen Space - Camera的绘制方式,我们需要先选择用于绘制Canvas的摄像机,然后Canvas会被绘制在选定的摄像机最上方。因此,我们可以通过调整用于绘制Canvas的摄像机的属性来实现UI界面的三维翻转和让三维物体显示在UI界面之上等功能。此外,当用于渲染Canvas的摄像机的视口大小发生变化时,Canvas也会自动进行适配。
World Space:使用World Space的绘制方式,Unity会将Canvas当成普通三维物体进行处理。
  接着,根据CanvasScaler的说明文档 ,我们知道当Canvas进行缩放时,它在绘制UI时有三种对UI进行缩放的方式。在介绍这三种缩放方式之前,我们需要先清楚Unity的UI单位和像素的区别。当我们在Unity里编辑UI的时候,UI的Width和Height使用的是Unity的UI单位,当UI被绘制到屏幕上的时候,UI使用的是像素。了解了Unity的UI单位和像素的区别之后,我们接着了解一下Canvas对UI进行缩放的三种方式的区别:
Canvas对UI进行缩放的三种方式:
Constant Pixel Size:在不同尺寸的屏幕上,UI组件会占用相同数量的像素。当ScaleFactor的值为x时,1UI单位等于x个像素。此外,采用这种缩放方式,同样的UI在手机上会比在电脑上小很多,因为手机屏幕的DPI(Dots Per Inch,每英寸点数),也就是像素密度要远远大于电脑。
Scale With Screen Size:让UI根据屏幕尺寸的大小变化进行缩放,因此我们需要先设置一个用于参考的屏幕尺寸基准值
Constant Physical Size:无论屏幕尺寸多大,都让UI保持一样的物理尺寸大小(像素值会变化),如果我们希望在任意的设备上,自己手掌地方图片都可以和你的手掌完全重合,可以采用这种模式
  在了解了Canvas的绘制方式和对UI的缩放方式之后,我们开始为我们的游戏创建一个用于绘制UI的Canvas:
创建Canvas的步骤如下:
在Hierarchy窗口中右击鼠标,选择UI->Canvas在场景中新建一个Canvas,然后将其重命名为UICanvas
接着我们对UICanvas下Canvas Scaler组件的设置进行修改
选择UI Scale Mode为Scale With Screen Size,让我们的UI根据屏幕尺寸的变化进行缩放,保证我们的UI在不同尺寸的手机上看起来都差不多
因为我们预设的屏幕分辨率为1920 X 1080,所以我们设置Reference Resolution为X(1920), Y(1080)
设置Screen Match Mode为Match Width or Height,让Unity根据分辨率的宽和高的权重来缩放Canvas
我们希望Canvas能同时考虑分辨率的宽和高的变化来缩放UI,因此我们设置Match为0.5(0表示只考虑宽,1表示只考虑高)
添加提示UI   添加完BombManager之后,我们在场景里面添加提示UI,添加提示UI的步骤如下:
添加提示UI的步骤:
在UICanvas物体下新建一个Image,然后将其重名为BombUI
BombUI物体需要修改的组件属性:
Rect Transform:
Anchors: 点击Rect Transform组件左上角的方框,然后选择top-left
PosX: 160
PosY: -100
Width: 150
Height: 150
Image:
Source Image: Assets\Sprites\Props文件夹下的prop_crate_ammo图片
Color: (255, 255, 255, 200)
在BombUI物体下新建一个Text,然后将其重命名为BombNumberText
BombNumberText物体需要修改的组件属性:
Rect Transform:
PosX: -3.5
PosY: -10
Width: 200
Height: 160
Text:
Text: 0
Font: Assets\Fonts下的BradBunR字体文件
Font Size: 80
Alignment: 水平居中,垂直居中
在UICanvas物体下新建一个Text,然后将其重名为TargetScoreLabel
TargetScoreLabel物体需要修改的组件属性:
Rect Transform:
Anchors: 点击Rect Transform组件左上角的方框,然后选择top-center
PosX: -460
PosY: -35
Width: 320
Height: 80
Text:
Text: Target Score:
Font: Assets\Fonts下的BradBunR字体文件
Font Size: 60
Alignment: 水平居中,垂直居中
Color: (119, 119, 119, 180)
在TargetScoreLabel物体下新建一个Text,然后将其重命名为TargetScoreText
TargetScoreText物体需要修改的组件属性:
Rect Transform:
PosX: 0
PosY: -50
Width: 320
Height: 80
Text:
Text: 50000
Font: Assets\Fonts下的BradBunR字体文件
Font Size: 60
Alignment: 水平居中,垂直居中
Color: (119, 119, 119, 180)
在UICanvas物体下新建一个Text,然后将其重名为ScoreLabel
ScoreLabel物体需要修改的组件属性:
Rect Transform:
Anchors: 点击Rect Transform组件左上角的方框,然后选择top-center
PosX: -80
PosY: -70
Width: 240
Height: 120
Text:
Text: Score:
Font: Assets\Fonts下的BradBunR字体文件
Font Size: 100
Alignment: 水平居中,垂直居中
Color: (0, 0, 0, 255)
在ScoreLabel物体下新建一个Text,然后将其重名为ScoreLabelForground
ScoreLabelForground物体需要修改的组件属性:
Rect Transform:
PosX: 0
PosY: -49
Width: 240
Height: 120
Text:
Text: Score:
Font: Assets\Fonts下的BradBunR字体文件
Font Size: 100
Alignment: 水平居中,垂直居中
在UICanvas物体下新建一个Text,然后将其重名为ScoreText
ScoreText物体需要修改的组件属性:
Rect Transform:
Anchors: 点击Rect Transform组件左上角的方框,然后选择top-center
PosX: 225
PosY: -70
Width: 320
Height: 120
Text:
Text: 50000
Font: Assets\Fonts下的BradBunR字体文件
Font Size: 80
Alignment: 向左对齐,垂直居中
在UICanvas物体下新建一个Button,然后将其重名为BackButton,并删除BackButton的Text子物体
BackButton物体需要修改的组件属性:
Rect Transform:
Anchors: 点击Rect Transform组件左上角的方框,然后选择top-right
PosX: -140
PosY: -75
Width: 115
Height: 100
Image:
Source Image: Assets\Sprites\UI文件夹下的BackButton图片
Color: (119, 119, 119, 255)
在UICanvas物体下新建一个Button,然后将其重名为BackButton,并删除BackButton的Text子物体
BackButton物体需要修改的组件属性:
Rect Transform:
Anchors: 点击Rect Transform组件左上角的方框,然后选择top-right
PosX: -140
PosY: -75
Width: 115
Height: 100
Image:
Source Image: Assets\Sprites\UI文件夹下的BackButton图片
Color: (119, 119, 119, 255)
  添加完提示UI之后,我们Game窗口中选择其它尺寸的视口,可以看到,游戏场景中的UI会随着视口尺寸的改变而缩放。
更新提示UI的内容   添加完提示UI之后,我们还需要选择使用代码在游戏运行时更新提示UI的内容。首先,我们为BombManager.cs加入更新提示UI的代码:
BombManager.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 using System;using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.UI; [Serializable ]public class BombManager { [Tooltip("炸弹的初始数量" ) ] public int InitBombNumber = 4 ; [Tooltip("炸弹UI" ) ] public Image BombUI; [Tooltip("显示炸弹的数量" ) ] public Text BombNumberText; private int m_CurrentBombNumber; private bool m_Stop; public void Init () { m_CurrentBombNumber = InitBombNumber; m_Stop = false ; UpdateUI(); } public void Stop () { m_Stop = true ; } public bool ReleaseBomb (int bombNum ) { if (m_Stop) { return false ; } int temp = m_CurrentBombNumber - bombNum; if (temp >= 0 ) { m_CurrentBombNumber = temp; UpdateUI(); return true ; } else { return false ; } } public void PickupBomb (int bombNum ) { if (m_Stop) { return ; } m_CurrentBombNumber += bombNum; UpdateUI(); } private void UpdateUI () { BombNumberText.text = "" + m_CurrentBombNumber; if (m_CurrentBombNumber <= 0 ) { BombUI.color = new Color(255 , 0 , 0 , BombUI.color.a / 2 ); } else { BombUI.color = new Color(255 , 255 , 255 , BombUI.color.a); } } }
代码说明:   因为我们使用了Text类和Image类来操作UI,所以我们需要加上using UnityEngine.UI;
  修改完BombManager.cs之后,我们继续为ScoreManager.cs加入更新提示UI的代码:
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 78 79 80 81 82 83 84 85 86 87 88 89 using System;using System.Collections.Generic;using UnityEngine;using UnityEngine.UI; [Serializable ]public class ScoreManager { [Tooltip("游戏胜利的目标分数" ) ] public int TargetScore = 5000 ; [Tooltip("保存嘲讽音效" ) ] public AudioClip[] TauntClips; [Tooltip("得分之后播放嘲讽音效的概率" ) ] public float TauntProbaility = 50f ; [Tooltip("嘲讽的间隔" ) ] public float TauntDelay = 1f ; [Tooltip("显示目标分数" ) ] public Text TargetScoreText; [Tooltip("显示当前的分数" ) ] public Text ScoreText; private int m_CurrentScore; private int m_TauntIndex; private float m_LastTauntTime; private bool m_Stop; private Transform m_Player; public void Init () { m_CurrentScore = 0 ; m_TauntIndex = 0 ; m_LastTauntTime = Time.time; m_Stop = false ; TargetScoreText.text = "" + TargetScore; ScoreText.text = "" + m_CurrentScore; m_Player = GameObject.FindGameObjectWithTag("Player" ).transform;; } public void Stop () { m_Stop = true ; } public void AddScore (int score ) { if (m_Stop) { return ; } m_CurrentScore += score; ScoreText.text = "" + m_CurrentScore; 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; } }
代码说明:   因为我们使用了Text类来操作UI,所以我们需要加上using UnityEngine.UI;
  脚本编辑完成之后,我们选择Hierarchy窗口中的GameStateManager,然后将UICanvas物体下的TargetScoreText和ScoreText子物体分别拖拽到Score Manager Instance折叠框下的Target Score Text和Score Text属性赋值框,接着将UICanvas物体下的BombUI和BombNumberText子物体分别拖拽到Bomb Manager Instance折叠框下的Bomb UI和Bomb Number Text属性赋值框。最后,运行游戏,可以看到此时提示UI能正确显示目标分数、当前的分数以及当前可释放的炸弹数。
添加游戏暂停UI和游戏结束UI   添加完提示UI之后,我们继续在场景里面添加游戏暂停UI和游戏结束UI。首先,我们先为场景添加游戏暂停UI:
添加游戏暂停UI的步骤:
在UICanvas物体下新建一个Panel,然后将其重名为PausedPanel
PausedPanel物体需要修改的组件属性:
Image:
Color: (119, 119, 119, 100)
在PausedPanel物体下新建一个Image,然后将其重命名为Background
Background物体需要修改的组件属性:
Rect Transform:
Image:
Source Image: Assets\Sprites\UI文件夹下的SF Window图片
Color: (119, 119, 119, 100)
在Background物体下新建一个Text,然后将其重名为PausedText
PausedText物体需要修改的组件属性:
Rect Transform:
PosX: 0
PosY: 100
Width: 560
Height: 480
Text:
Text: Whether to quit the game?
Font: Assets\Fonts下的BradBunR字体文件
Font Size: 80
Alignment: 水平居中,垂直居中
在PausedPanel物体下新建一个Button,然后将其重名为ConfirmButton
ConfirmButton物体需要修改的组件属性:
Rect Transform:
PosX: -220
PosY: -175
Width: 300
Height: 120
Image:
Source Image: Assets\Sprites\UI文件夹下的SF Button图片
修改ConfirmButton物体下的子物体Text
Text物体需要修改的组件属性:
Text:
Text: Yes
Font: Assets\Fonts下的BradBunR字体文件
Font Size: 80
Alignment: 水平居中,垂直居中
在PausedPanel物体下新建一个Button,然后将其重名为CancelButton
CancelButton物体需要修改的组件属性:
Rect Transform:
PosX: 220
PosY: -175
Width: 300
Height: 120
Image:
Source Image: Assets\Sprites\UI文件夹下的SF Button图片
修改CancelButton物体下的子物体Text
Text物体需要修改的组件属性:
Text:
Text: No
Font: Assets\Fonts下的BradBunR字体文件
Font Size: 80
Alignment: 水平居中,垂直居中
  添加完游戏暂停UI之后,我们继续添加游戏结束UI:
添加游戏结束UI的步骤:
复制PausedPanel得到PausedPanel (1),并将PausedPanel (1)物体重命名为GameResultPanel
将PausedPanel物体设置为在游戏场景中不可见
将PausedText物体重名为GameResultText
GameResultText物体需要修改的组件属性:
Text:
Width: 600
Height: 480
Text: You Win!!!
Font Size: 150
Color: (103, 103, 103, 255)
将ConfirmButton物体重命名为RestartButton
修改RestartButton物体下的子物体Text
Text物体需要修改的组件属性:
将CancelButton重命名为QuitButton
修改QuitButton物体下的子物体Text
Text物体需要修改的组件属性:
  添加完游戏结束UI之后,我们将GameResultPanel设置为在游戏场景中不可见。
管理游戏暂停UI和游戏结束UI   添加完游戏暂停UI和游戏结束UI之后,我们还需要在GameStateManager.cs中加入管理游戏暂停UI和游戏结束UI的代码:
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 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.UI;using UnityEngine.SceneManagement;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("场景中所有Generator的父物体" ) ] public GameObject Generator; [Tooltip("ScoreManager的实例" ) ] public ScoreManager ScoreManagerInstance = new ScoreManager(); [Tooltip("BombManager的实例" ) ] public BombManager BombManagerInstance = new BombManager(); [Tooltip("游戏暂停界面" ) ] public GameObject PausedPanel; [Tooltip("游戏结束界面" ) ] public GameObject GameResultPanel; [Tooltip("游戏结果" ) ] public Text GameResultText; 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(); BombManagerInstance.Init(); PausedPanel.SetActive(false ); GameResultPanel.SetActive(false ); 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 GameRunning () {#if UNITY_STANDALONE || UNITY_EDITOR if (Input.GetKeyDown(KeyCode.P)) { if (m_IsPaused) { GameContinue(); } else { GamePause(); } }#endif } private void GameEnd () { m_AudioSource.Stop(); m_AudioSource.loop = false ; ScoreManagerInstance.Stop(); BombManagerInstance.Stop(); float delay = 0f ; if (m_GameResult) { if (GameWinClip != null ) { AudioSource.PlayClipAtPoint(GameWinClip, this .transform.position); delay = GameWinClip.length; } else { Debug.LogError("请设置GameWinClip" ); } GameResultText.text = "You Win!!!" ; } else { if (GameLoseClip != null ) { AudioSource.PlayClipAtPoint(GameLoseClip, this .transform.position); delay = GameLoseClip.length; } else { Debug.LogError("请设置GameLoseClip" ); } GameResultText.text = "You Lose!!!" ; } GameResultPanel.SetActive(true ); Destroy(Generator, delay); }#endregion #region 外部调用函数 public void SetGameResult (bool result ) { m_GameResult = result; m_CurrentState = GameState.End; } public void GamePause () { m_AudioSource.Pause(); Time.timeScale = 0f ; m_IsPaused = true ; PausedPanel.SetActive(true ); } public void GameContinue () { Time.timeScale = 1f ; m_AudioSource.UnPause(); m_IsPaused = false ; PausedPanel.SetActive(false ); } public void Restart () { SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex); } public void Back () { }#endregion }
代码说明:
因为我们使用了Text来操作UI,所以我们需要加上using UnityEngine.UI;
因为我们使用了SceneManager来加载场景,所以我们需要加上using UnityEngine.SceneManagement;
为了方便测试,我们在GameRunning方法中使用了Unity提供的平台宏定义 来进行平台隔离
为了将GamePause函数和GameContinue函数设置为按钮点击事件的回调函数,我们将GamePause函数和GameContinue函数的可见性设置为public
  编辑完GameStateManager.cs之后,我们在Hierarchy窗口选中GameStateManager物体,然后将GameStateManager物体上GameStateManager.cs组件的Paused Panel和Game Result Panel字段分别设置UICanvas下的子物体PausedPanel和子物体GameResultPanel,将Game Result Text字段设置为GameResultPanel物体下的子物体Game Result Text。
  设置完游戏暂停UI和游戏结束UI之后,我们还需要设置按钮的点击事件,我们先来设置BackButton的点击事件。
BackButton点击事件的设置步骤:
选中BackButton物体,然后点击其Button组件上On Click()下的+号增加一个空点击事件
将场景中的GameStateManager拖拽至On Click()下的GameObject赋值框处
点击No Function下拉菜单,选择GameStateManager下的GamePause函数
  最后,我们按照相同的步骤,将PausedPanel物体的子物体ConfirmButton的点击事件设置为GameStateManager下的Back函数,将PausedPanel物体的子物体CancelButton的点击事件设置为GameStateManager下的GameContinue函数,将GameResultPanel物体的子物体RestartButton的点击事件设置为GameStateManager下的Restart函数,将GameResultPanel物体的子物体QuitButton的点击事件设置为GameStateManager下的Back函数。
  运行游戏,可以看到当游戏胜利或者失败时,会弹出游戏结束界面,此时若点击Restart按钮,游戏将重新开始。此外,当我们点击BackButton时,游戏会暂停并弹出游戏暂停界面,若我们点击游戏暂停界面的No按钮时,游戏恢复且游戏暂停界面消失。
后言   需要注意的是,目前我们还没有实现菜单场景,因此我们的Back函数是一个空函数。最后,本篇文章所做的修改,可以在PotatoGloryTutorial 这个仓库的essay18分支下看到,读者可以clone这个仓库到本地进行查看。
参考链接
Unity-Canvas
Unity-Canvas Scaler
Unity-Designing UI for Multiple Resolutions
Unity-Platform dependent compilation