Unity官方教程《Tanks》学习笔记(五)
本系列文章是根据官方视频教程而写下的学习笔记,原官方视频教程网址:https://unity3d.com/cn/learn/tutorials/s/tanks-tutorial
系列其他笔记传送门
Unity官方教程《Tanks》学习笔记(一)
Unity官方教程《Tanks》学习笔记(二)
Unity官方教程《Tanks》学习笔记(三)
Unity官方教程《Tanks》学习笔记(四)
管理
本小节的目标是创建一个管理脚本,同一管理该游戏场景中的两辆坦克,并且添加输赢的游戏逻辑,让游戏有始有终。
在上一节中,我们把根目录下的Tank删除了,我们需要在游戏的过程中动态生成两个Tank,而不是一开始就设置好。因此我们需要两个Tank的出生点。在Hierarchy下新建两个空对象,分别命名为SpawnPoint1和SpawnPoion2。
选中SpawnPoint1,作以下修改:
SpawnPoint1
选中SpawnPoint2,作以下修改:
SpawnPoint2
接着,在Hierarchy层级下,新建一个Canvas(GameObject——>UI——>Canvas),重命名为MessageCanvas。接着,在Scene View中点击2D模式,如下图所示:
视图2D模式
选中MessageCanvas,右键新建一个Text,让其成为MessageCanvas的子对象,选中Text对象,我们来修改它的数据如下:
Text
下一步,在Text内,新建一个组件:Shadow,为Text添加阴影效果:
Shadow
接着,取消刚才设置的2D视图模式。
选中CameraRig,点击Edit——>Frame Selected,在CameraRig的脚本组件那里,我们之前设置了m_Targets为已经被删除的Tank,所以我们要把该数组的长度设置为0,并按回车确认。再打开CameraControl脚本来编辑:这里只需要把之前提及的[HideInInspector]的注释去掉即可,也就是说隐藏掉该公共变量。
下面就来创建我们的游戏管理者,在Hierarchy层级创建一个空对象,命名为GameManager,在/Scripts/Managers文件夹内找到GameManager脚本,把它拖拽到GameManager对象内。我们先初始化它的几个公共变量:
接下来先整理一下我们的游戏逻辑。
1、首先,我们先从游戏的整个流程来梳理:
从官方的教程中,我们可以知道,Game Manager充当一个管理全局的角色,首先它初始化的过程中,会在出生点生成两个坦克供玩家控制,并且把摄像机的目标设置为该两辆坦克,那么这样就完成了初始化。接着就是正常的游戏流程,那么这里就涉及到了游戏的输赢判定,这里使用的是分回合的形式,每一回合获胜则获得一分,经过若干回合后,总分最高者获胜。每一回合结束之后,会回到初始化过程,重新生成坦克。具体到每一个回合上,坦克的控制就交给Tank Manager来控制。
游戏逻辑2
从上图可以看出,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文件夹,新建一个Audio Mixer,命名为MainMix。双击打开该文件。
MainMix
确保左上角选中的是MainMix,然后在Groups选项下点击“+”来创建三个子对象,并分别命名为Music、SFX、Driving。(如果无法重命名,则点击开始游戏再结束游戏)。接着对三个子对象的属性进行更改:
1、选中Music,把Attenuation选择为-12,并且通过“Add..”按钮新建一个Duck Volume
2、选中SFX,新建一个Send,设置Receive为Music\Duck Volume
3、选中Driving,把Attenuation选择为-25。
4、重新选择Music,在Inspectior界面做更改如下:
image.png
然后,在Prefabs文件夹内找到Tank,展开第一个Audio Source把Output选择为Driving。
Driving
展开第二个Audio Source,把Output选择为SFX
SFX
在Prefabs文件夹内找到Shell,展开,选中ShellExplosion,把Audio Source的Output选择为SFX。
在Prefabs文件夹找到TankExplosion,把Audio Source的Output选择为SFX。
在Hierarchy选择GameManager,新建Audio Source组件,音效选择为BackgroundMusic,Output选择为Music。勾选Loop。
最后,保存场景,运行游戏。整个Tanks游戏的开发流程到此完毕。