《土豆荣耀》重构笔记(二十)为游戏场景添加UI

前言

  在上篇文章中,我们已经实现了游戏主逻辑管理器,并加入了游戏的胜负条件。但在游戏中,玩家并不能清晰地知道当前自己获得了多少分数游戏胜利的目标分数是多少以及当前还能释放多少颗炸弹,我们需要加入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() {
// 执行一些游戏预操作,例如初始化其他Manager、播放过场动画和进行倒计时等
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");
}
}

// 播放完音效之后,删除场景中的所有Generator
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(Animator))]
[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 Animator m_Animator;
private PlayerController m_PlayerCtrl;

private void Awake() {
// 获取引用
// m_Animator = GetComponent<Animator>();
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() {
// // 播放射击动画
// m_Animator.SetTrigger("Shoot");

// 播放射击音效
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);
}
}
}

&emsp;&emsp;最后,我们还需要在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);
}
}
}

&emsp;&emsp;运行游戏,可以看到此时我们能正常释放和拾取炸弹。至此,代码重构完成,后面我们就可以在BombManager中对和炸弹有关的UI进行管理了。


创建Canvas

&emsp;&emsp;添加完BombManager之后,我们开始添加UI的工作。在Unity中,Canvas控制UI的绘制和缩放。只有成为Canvas的子物体,UI才能够被正常绘制,且Canvas会按照从上到下的顺序来绘制它的子物体,也就是上面的UI会被下面的UI覆盖。此外,根据Canvas的说明文档,我们知道Unity的Canvas有三种绘制的方式,它们的区别如下:

Canvas三种绘制的方式:

  1. Screen Space - Overlay: 这是Canvas默认的绘制方式。使用Screen Space - Overlay的绘制方式,Canvas不受场景摄像机的影响,直接在屏幕上进行绘制,且在不同的屏幕分辨率下,Canvas会自动适配屏幕的分辨率大小。由于Canvas不受摄像机影响,所以整个过程Canvas都是保持静态的,也就不需要重新计算Canvas的位置和角度,是一种优化性较好的绘制方式。
  2. Screen Space - Camera: 使用Screen Space - Camera的绘制方式,我们需要先选择用于绘制Canvas的摄像机,然后Canvas会被绘制在选定的摄像机最上方。因此,我们可以通过调整用于绘制Canvas的摄像机的属性来实现UI界面的三维翻转让三维物体显示在UI界面之上等功能。此外,当用于渲染Canvas的摄像机的视口大小发生变化时,Canvas也会自动进行适配
  3. World Space:使用World Space的绘制方式,Unity会将Canvas当成普通三维物体进行处理。

&emsp;&emsp;接着,根据CanvasScaler的说明文档,我们知道当Canvas进行缩放时,它在绘制UI时有三种对UI进行缩放的方式。在介绍这三种缩放方式之前,我们需要先清楚Unity的UI单位和像素的区别。当我们在Unity里编辑UI的时候,UI的WidthHeight使用的是Unity的UI单位,当UI被绘制到屏幕上的时候,UI使用的是像素。了解了Unity的UI单位和像素的区别之后,我们接着了解一下Canvas对UI进行缩放的三种方式的区别:

Canvas对UI进行缩放的三种方式:

  1. Constant Pixel Size:在不同尺寸的屏幕上,UI组件会占用相同数量的像素。当ScaleFactor的值为x时,1UI单位等于x个像素。此外,采用这种缩放方式,同样的UI在手机上会比在电脑上小很多,因为手机屏幕的DPI(Dots Per Inch,每英寸点数),也就是像素密度要远远大于电脑。
  2. Scale With Screen Size:让UI根据屏幕尺寸的大小变化进行缩放,因此我们需要先设置一个用于参考的屏幕尺寸基准值
  3. Constant Physical Size:无论屏幕尺寸多大,都让UI保持一样的物理尺寸大小(像素值会变化),如果我们希望在任意的设备上,自己手掌地方图片都可以和你的手掌完全重合,可以采用这种模式

&emsp;&emsp;在了解了Canvas的绘制方式和对UI的缩放方式之后,我们开始为我们的游戏创建一个用于绘制UI的Canvas:

创建Canvas的步骤如下:

  1. Hierarchy窗口中右击鼠标,选择UI->Canvas在场景中新建一个Canvas,然后将其重命名为UICanvas
  2. 接着我们对UICanvasCanvas Scaler组件的设置进行修改
    1. 选择UI Scale ModeScale With Screen Size,让我们的UI根据屏幕尺寸的变化进行缩放,保证我们的UI在不同尺寸的手机上看起来都差不多
    2. 因为我们预设的屏幕分辨率为1920 X 1080,所以我们设置Reference ResolutionX(1920), Y(1080)
    3. 设置Screen Match ModeMatch Width or Height,让Unity根据分辨率的宽和高权重来缩放Canvas
    4. 我们希望Canvas能同时考虑分辨率的宽和高的变化来缩放UI,因此我们设置Match0.50表示只考虑宽,1表示只考虑高

添加提示UI

&emsp;&emsp;添加完BombManager之后,我们在场景里面添加提示UI,添加提示UI的步骤如下:

添加提示UI的步骤:

  1. 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)
  2. 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: 水平居中,垂直居中
  3. 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)
  4. 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)
  5. 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)
  6. 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: 水平居中,垂直居中
  7. 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: 向左对齐,垂直居中
  8. UICanvas物体下新建一个Button,然后将其重名为BackButton,并删除BackButtonText子物体

    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)
  9. UICanvas物体下新建一个Button,然后将其重名为BackButton,并删除BackButtonText子物体

    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)

&emsp;&emsp;添加完提示UI之后,我们Game窗口中选择其它尺寸的视口,可以看到,游戏场景中的UI会随着视口尺寸的改变而缩放

添加提示UI之后的游戏场景


更新提示UI的内容

&emsp;&emsp;添加完提示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;

// 更新UI
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;
// 更新UI
UpdateUI();

return true;
} else {
return false;
}
}

// 拾取炸弹
public void PickupBomb(int bombNum) {
// 管理器停止工作,不执行任何操作
if(m_Stop) {
return;
}

m_CurrentBombNumber += bombNum;
// 更新UI
UpdateUI();
}

// 更新UI
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);
}
}
}

代码说明:
&emsp;&emsp;因为我们使用了Text类和Image类来操作UI,所以我们需要加上using UnityEngine.UI;

&emsp;&emsp;修改完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;
}
}

代码说明:
&emsp;&emsp;因为我们使用了Text类来操作UI,所以我们需要加上using UnityEngine.UI;

&emsp;&emsp;脚本编辑完成之后,我们选择Hierarchy窗口中的GameStateManager,然后将UICanvas物体下的TargetScoreTextScoreText子物体分别拖拽到Score Manager Instance折叠框下的Target Score TextScore Text属性赋值框,接着将UICanvas物体下的BombUIBombNumberText子物体分别拖拽到Bomb Manager Instance折叠框下的Bomb UIBomb Number Text属性赋值框。最后,运行游戏,可以看到此时提示UI能正确显示目标分数当前的分数以及当前可释放的炸弹数

提示UI的设置


添加游戏暂停UI和游戏结束UI

&emsp;&emsp;添加完提示UI之后,我们继续在场景里面添加游戏暂停UI游戏结束UI。首先,我们先为场景添加游戏暂停UI

添加游戏暂停UI的步骤:

  1. UICanvas物体下新建一个Panel,然后将其重名为PausedPanel

    PausedPanel物体需要修改的组件属性:

    • Image:
      • Color: (119, 119, 119, 100)
  2. PausedPanel物体下新建一个Image,然后将其重命名为Background

    Background物体需要修改的组件属性:

    • Rect Transform:
      • Width: 1200
      • Height: 900
    • Image:
      • Source Image: Assets\Sprites\UI文件夹下的SF Window图片
      • Color: (119, 119, 119, 100)
  3. 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: 水平居中,垂直居中
  4. PausedPanel物体下新建一个Button,然后将其重名为ConfirmButton

    ConfirmButton物体需要修改的组件属性:

    • Rect Transform:
      • PosX: -220
      • PosY: -175
      • Width: 300
      • Height: 120
    • Image:
      • Source Image: Assets\Sprites\UI文件夹下的SF Button图片
  5. 修改ConfirmButton物体下的子物体Text

    Text物体需要修改的组件属性:

    • Text:
      • Text: Yes
      • Font: Assets\Fonts下的BradBunR字体文件
      • Font Size: 80
      • Alignment: 水平居中,垂直居中
  6. PausedPanel物体下新建一个Button,然后将其重名为CancelButton

    CancelButton物体需要修改的组件属性:

    • Rect Transform:
      • PosX: 220
      • PosY: -175
      • Width: 300
      • Height: 120
    • Image:
      • Source Image: Assets\Sprites\UI文件夹下的SF Button图片
  7. 修改CancelButton物体下的子物体Text

    Text物体需要修改的组件属性:

    • Text:
      • Text: No
      • Font: Assets\Fonts下的BradBunR字体文件
      • Font Size: 80
      • Alignment: 水平居中,垂直居中

添加游戏暂停UI之后的游戏场景

&emsp;&emsp;添加完游戏暂停UI之后,我们继续添加游戏结束UI

添加游戏结束UI的步骤:

  1. 复制PausedPanel得到PausedPanel (1),并将PausedPanel (1)物体重命名为GameResultPanel
  2. PausedPanel物体设置为在游戏场景中不可见
  3. PausedText物体重名为GameResultText

    GameResultText物体需要修改的组件属性:

    • Text:
      • Width: 600
      • Height: 480
      • Text: You Win!!!
      • Font Size: 150
      • Color: (103, 103, 103, 255)
  4. ConfirmButton物体重命名为RestartButton
  5. 修改RestartButton物体下的子物体Text

    Text物体需要修改的组件属性:

    • Text:
      • Text: Restart
  6. CancelButton重命名为QuitButton
  7. 修改QuitButton物体下的子物体Text

    Text物体需要修改的组件属性:

    • Text:
      • Text: Quit

添加游戏结束UI之后的游戏场景

&emsp;&emsp;添加完游戏结束UI之后,我们将GameResultPanel设置为在游戏场景中不可见。


管理游戏暂停UI和游戏结束UI

&emsp;&emsp;添加完游戏暂停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;
// 使用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("场景中所有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;
// 游戏结果,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();
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);
// 播放完音效之后,删除场景中的所有Generator
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
}

代码说明:

  1. 因为我们使用了Text来操作UI,所以我们需要加上using UnityEngine.UI;
  2. 因为我们使用了SceneManager来加载场景,所以我们需要加上using UnityEngine.SceneManagement;
  3. 为了方便测试,我们在GameRunning方法中使用了Unity提供的平台宏定义来进行平台隔离
  4. 为了将GamePause函数GameContinue函数设置为按钮点击事件的回调函数,我们将GamePause函数GameContinue函数的可见性设置为public

&emsp;&emsp;编辑完GameStateManager.cs之后,我们在Hierarchy窗口选中GameStateManager物体,然后将GameStateManager物体上GameStateManager.cs组件的Paused PanelGame Result Panel字段分别设置UICanvas下的子物体PausedPanel和子物体GameResultPanel,将Game Result Text字段设置为GameResultPanel物体下的子物体Game Result Text

游戏暂停UI和游戏结束UI的设置

&emsp;&emsp;设置完游戏暂停UI游戏结束UI之后,我们还需要设置按钮的点击事件,我们先来设置BackButton的点击事件。

BackButton点击事件的设置步骤:

  1. 选中BackButton物体,然后点击其Button组件上On Click()下的+号增加一个空点击事件
  2. 将场景中的GameStateManager拖拽至On Click()下的GameObject赋值框处
  3. 点击No Function下拉菜单,选择GameStateManager下的GamePause函数

设置BackButton的点击事件

&emsp;&emsp;最后,我们按照相同的步骤,将PausedPanel物体的子物体ConfirmButton的点击事件设置为GameStateManager下的Back函数,将PausedPanel物体的子物体CancelButton的点击事件设置为GameStateManager下的GameContinue函数,将GameResultPanel物体的子物体RestartButton的点击事件设置为GameStateManager下的Restart函数,将GameResultPanel物体的子物体QuitButton的点击事件设置为GameStateManager下的Back函数

&emsp;&emsp;运行游戏,可以看到当游戏胜利或者失败时,会弹出游戏结束界面,此时若点击Restart按钮,游戏将重新开始。此外,当我们点击BackButton时,游戏会暂停并弹出游戏暂停界面,若我们点击游戏暂停界面的No按钮时,游戏恢复且游戏暂停界面消失。


后言

&emsp;&emsp;需要注意的是,目前我们还没有实现菜单场景,因此我们的Back函数是一个空函数。最后,本篇文章所做的修改,可以在PotatoGloryTutorial这个仓库的essay18分支下看到,读者可以clone这个仓库到本地进行查看。


参考链接

  1. Unity-Canvas
  2. Unity-Canvas Scaler
  3. Unity-Designing UI for Multiple Resolutions
  4. Unity-Platform dependent compilation

《土豆荣耀》重构笔记(二十)为游戏场景添加UI
https://asancai.github.io/posts/908f0d6b/
作者
RainbowCyan
发布于
2019年1月30日
许可协议