Unity官方教程《Tanks》学习笔记(五)
这些学习笔记基于官方视频教程编写而成。原始官方视频教程的链接为:https://unity3d.com/cn/learn/tutorials/s/tanks-tutorial
系列其他学习笔记入口
官方教程·Tanks系列 第一课
官方教程·Tanks系列 第二课
官方教程·Tanks系列 第三课
官方教程·Tanks系列 第四课
管理
本小节旨在构建一个游戏场景中的两辆坦克管理脚本,并在动态运行中实现这两台坦克之间的胜负游戏逻辑以确保游戏流程完整。
在上一节操作后我们已成功删除了根目录下的Tank对象现需要在此基础上进行扩展以支持动态生成两个独立的坦克在游戏中进行循环战斗为此我们在游戏执行过程中需为每个Tank设定独特的出生点位置。
具体操作如下:首先在Hierarchy菜单下新增两个空对象分别命名为SpawnPoint1和SpawnPoint2这两个出生点将作为各个Tank的起始位置。
随后选中SpawnPoint1执行以下设置:

选中SpawnPoint2,作以下修改:

随后,在层级结构中创建一个新的Canvas,并命名为MessageCanvas。随后,在Scene View中切换至2D模式。

选定MessageCanvas后,通过右键操作创建一个新的Text,并将其设为MessageCanvas的一个子对象。选择该Text对象后,我们对其中的数据进行如下修改:

下一步,在Text内,新建一个组件:Shadow,为Text添加阴影效果:

接着,取消刚才设置的2D视图模式。
选择并打开CameraRig后,请依次执行以下操作:首先单击进入Edit菜单中的Frame Selected选项,在其脚本部分中我们曾将m TARGETs赋值给已被删除的Tank对象,请注意此时需要将该数组归零并按回车键确认。接着打开CameraControl组件进行编辑:在这里只需要去除之前提到过的[Uncomment]注释即可。换句话说就是隐藏那个公共变量。
接下来将建立我们的游戏管理者,在HIERARCHY层级上生成一个空对象并定名为GameManager。在位于/Scripts/Managers文件夹内的GameManager脚本中进行操作,并将该脚本拖入到ManagerInterface对象中进行配置。接下来我们需要为ManagerInterface对象初始化几个公共变量:

接下来先整理一下我们的游戏逻辑。
1、首先,我们先从游戏的整个流程来梳理 :

按照官方教程介绍,《Game Manager》扮演了一个全局管理者的角色。在初始化阶段,在指定位置生成了两辆 tanks 供玩家操作,并将摄像机焦点定向为这两辆 tanks 的中点位置;从而完成了初始化流程。接下来是游戏的标准流程:其中包含了游戏结果判定机制——采用分轮制进行判断:在每一轮比赛中获胜的一方将获得一分积分;经过多轮比赛后——最终累计积分最高的一方胜出;而每一轮结束后都会返回到初始化阶段——重新生成新的 tank 队伍;具体到每一个比赛循环——则由 Tank Manager 负责 tank 的自主行动与控制。

通过图表可以看出 Tank Manager 负责管理坦克的移动、射击程序以及UI的呈现。
2、我们从游戏者的角度来梳理:

GameManager能够划分为多个独立的Tank Manager单元体,并且Game Manager主要承担每个Tank Manager的管理职责。每一个 Tank Manager则专注于处理各自游戏坦克的具体行为操作,并通过这种方式实现了系统间的解耦机制。这种设计使得系统的可扩展性得以显著提升:例如,在未来需要增加玩家数量或其他功能时,则只需对 Game Manager进行相应的功能拓展即可完成全部功能升级
接着,我们打开GameManager脚本,对它进行完善与编辑:
using UnityEngine;
using System.Collections;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class GameManager : MonoBehaviour
{
public int m_NumRoundsToWin = 5; //5回合获胜则游戏获胜
public float m_StartDelay = 3f; //每回合开始的等待时间
public float m_EndDelay = 3f; //每回合结束之后的等待时间
public CameraControl m_CameraControl;
public Text m_MessageText;
public GameObject m_TankPrefab;
public TankManager[] m_Tanks; //两个坦克管理者
private int m_RoundNumber;
private WaitForSeconds m_StartWait;
private WaitForSeconds m_EndWait;
private TankManager m_RoundWinner;
private TankManager m_GameWinner;
private void Start()
{
m_StartWait = new WaitForSeconds(m_StartDelay); //用来协同yield指令,等待若干秒
m_EndWait = new WaitForSeconds(m_EndDelay);
SpawnAllTanks(); //生成坦克
SetCameraTargets(); //设置摄像机
StartCoroutine(GameLoop()); //
}
/** * 在出生点生成坦克
*/
private void SpawnAllTanks()
{
for (int i = 0; i < m_Tanks.Length; i++)
{
m_Tanks[i].m_Instance =
Instantiate(m_TankPrefab, m_Tanks[i].m_SpawnPoint.position, m_Tanks[i].m_SpawnPoint.rotation) as GameObject;
m_Tanks[i].m_PlayerNumber = i + 1; //为坦克标号
m_Tanks[i].Setup(); //调用TankManager的setup方法
}
}
/** * 设置摄像头的初始位置
*/
private void SetCameraTargets()
{
Transform[] targets = new Transform[m_Tanks.Length];
for (int i = 0; i < targets.Length; i++)
{
targets[i] = m_Tanks[i].m_Instance.transform;
}
m_CameraControl.m_Targets = targets;
}
//游戏循环
private IEnumerator GameLoop()
{
yield return StartCoroutine(RoundStarting()); //等待一段时间后执行
yield return StartCoroutine(RoundPlaying());
yield return StartCoroutine(RoundEnding());
//如果有胜者,则重新加载游戏场景
if (m_GameWinner != null)
{
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
else
{
StartCoroutine(GameLoop()); //如果没有胜者,则继续循环
}
}
/** * 每一回合的开始
*/
private IEnumerator RoundStarting()
{
ResetAllTanks(); //重置坦克位置
DisableTankControl(); //取消对坦克的控制
m_CameraControl.SetStartPositionAndSize(); //摄像机聚焦位置重置
m_RoundNumber++; //回合数增加
m_MessageText.text = "ROUND" + m_RoundNumber; //更改UI的显示
yield return m_StartWait;
}
/** * 每一回合的游戏过程
*/
private IEnumerator RoundPlaying()
{
EnableTankControl(); //激活对坦克的控制
m_MessageText.text = string.Empty; //UI不显示
//如果只剩下一个玩家,则跳出循环
while(!OneTankLeft()){
yield return null;
}
}
/** * 每一回合的结束
*/
private IEnumerator RoundEnding()
{
//取消对坦克的控制
DisableTankControl();
m_RoundWinner = null;
//判断当前回合获胜的玩家
m_RoundWinner = GetRoundWinner();
//累积胜利次数
if(m_RoundWinner != null){
m_RoundWinner.m_Wins++;
}
//判断是否有玩家达到了游戏胜利的条件
m_GameWinner = GetGameWinner();
string message = EndMessage();
m_MessageText.text = message;
yield return m_EndWait;
}
/** * 该方法用于判断是否只剩下一个玩家在场景中
*/
private bool OneTankLeft()
{
int numTanksLeft = 0;
for (int i = 0; i < m_Tanks.Length; i++)
{
if (m_Tanks[i].m_Instance.activeSelf)
numTanksLeft++;
}
return numTanksLeft <= 1;
}
/** * 该方法用于判断回合胜者
*/
private TankManager GetRoundWinner()
{
for (int i = 0; i < m_Tanks.Length; i++)
{
if (m_Tanks[i].m_Instance.activeSelf)
return m_Tanks[i];
}
return null;
}
/** * 该方法用于判断游戏获胜者
*/
private TankManager GetGameWinner()
{
for (int i = 0; i < m_Tanks.Length; i++)
{
if (m_Tanks[i].m_Wins == m_NumRoundsToWin)
return m_Tanks[i];
}
return null;
}
private string EndMessage()
{
string message = "DRAW!";
if (m_RoundWinner != null)
message = m_RoundWinner.m_ColoredPlayerText + " WINS THE ROUND!";
message += "\n\n\n\n";
for (int i = 0; i < m_Tanks.Length; i++)
{
message += m_Tanks[i].m_ColoredPlayerText + ": " + m_Tanks[i].m_Wins + " WINS\n";
}
if (m_GameWinner != null)
message = m_GameWinner.m_ColoredPlayerText + " WINS THE GAME!";
return message;
}
private void ResetAllTanks()
{
for (int i = 0; i < m_Tanks.Length; i++)
{
m_Tanks[i].Reset(); //调用TankManager的Reset()方法
}
}
private void EnableTankControl()
{
for (int i = 0; i < m_Tanks.Length; i++)
{
m_Tanks[i].EnableControl(); //调用TankManager的EnableControl()方法
}
}
private void DisableTankControl()
{
for (int i = 0; i < m_Tanks.Length; i++)
{
m_Tanks[i].DisableControl(); //调用TankManager的DisableControl()方法
}
}
}
编写完成后值得进一步了解的是TankManager这个脚本。位于Manager文件夹中但需要特别注意的是,并非将其复制粘贴到任何游戏对象上即可。因为由GameManager负责管理。
using System;
using UnityEngine;
[Serializable] //为了在Inspector显示公共变量,需要使用序列化标识符
public class TankManager
{
public Color m_PlayerColor; //下面两个变量在GameManager(Script)Inspector初始化
public Transform m_SpawnPoint;
[HideInInspector] public int m_PlayerNumber;
[HideInInspector] public string m_ColoredPlayerText;
[HideInInspector] public GameObject m_Instance;
[HideInInspector] public int m_Wins;
private TankMovement m_Movement;
private TankShooting m_Shooting;
private GameObject m_CanvasGameObject;
public void Setup()
{
m_Movement = m_Instance.GetComponent<TankMovement>(); //获取移动和射击的脚本
m_Shooting = m_Instance.GetComponent<TankShooting>();
m_CanvasGameObject = m_Instance.GetComponentInChildren<Canvas>().gameObject;
m_Movement.m_PlayerNumber = m_PlayerNumber; //设置玩家编号
m_Shooting.m_PlayerNumber = m_PlayerNumber;
m_ColoredPlayerText = "<color=#" + ColorUtility.ToHtmlStringRGB(m_PlayerColor) + ">PLAYER " + m_PlayerNumber + "</color>";
MeshRenderer[] renderers = m_Instance.GetComponentsInChildren<MeshRenderer>(); //用特定颜色渲染坦克
for (int i = 0; i < renderers.Length; i++)
{
renderers[i].material.color = m_PlayerColor;
}
}
public void DisableControl()
{
m_Movement.enabled = false;
m_Shooting.enabled = false;
m_CanvasGameObject.SetActive(false);
}
public void EnableControl()
{
m_Movement.enabled = true;
m_Shooting.enabled = true;
m_CanvasGameObject.SetActive(true);
}
public void Reset()
{
m_Instance.transform.position = m_SpawnPoint.position;
m_Instance.transform.rotation = m_SpawnPoint.rotation;
m_Instance.SetActive(false);
m_Instance.SetActive(true);
}
}
到这一步之后,就可以保存场景,并测试一下了。
音效
可以说游戏已经基本完成了。经过上一小节的测试后,在最后一小节还需要进一步优化音效表现。
第一步操作是在AudioMixer文件夹上执行右键点击,在其弹出的菜单中选择'新建'选项,并命名为MainMix。完成之后,请您双击启动该文件。

请确保主菜单中的选项是MainMix。随后,在Groups选项栏中单击“+”按钮以创建三个子对象,并分别命名为Music、SFX和Driving(如果无法直接重命名,请先启动游戏并重新结束游戏)。接下来,请对这三个子对象的属性进行设置:对于每个子对象,请选择其父级类型并相应地调整其属性参数设置以达到预期效果)。
- 在选中的Music选项中, 将Attenuation参数设置为-12分贝, 并新增一个Volume参数用于Duck效果.
- 在SFX效果中新建一个Send效果, 并将Receive设置为来自Music源的Duck Volume效果.
- 在选中的Driving效果中, 将Attenuation参数设定为-25分贝.
- 重新选择Music, 在Inspector界面进行如下更改:

随后,在Prefab文件夹中定位到Tank对象后,请依次打开第一个音频源实例,并将输出设置为Driving模式。

展开第二个Audio Source,把Output选择为SFX

打开位于Prefabs文件夹中的Shell,并将其进行展开操作。随后,在展开后的界面中,请您将焦点对准位于此处的ShellExplosion选项,并将其设为目标。接着,在同样位置处,请您将音频源的输出设置为SFX效果。对于位于Prefabs文件夹中的TankExplosion选项,请您重复上述操作以实现同样的效果设置。最后,在游戏层级结构(Hierarchy)中前往GameManager节点处并新增一个Audio Source组件,在其音效类型字段中请设置成BackgroundMusic,并将输出设置为主力音乐(Main Music)。为了让该音频源具备循环功能,请确保其Loop属性被勾选。
最后,保存场景,运行游戏。整个Tanks游戏的开发流程到此完毕。
