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、首先,我们先从游戏的整个流程来梳理

游戏逻辑1

从官方的教程中,我们可以知道,Game Manager充当一个管理全局的角色,首先它初始化的过程中,会在出生点生成两个坦克供玩家控制,并且把摄像机的目标设置为该两辆坦克,那么这样就完成了初始化。接着就是正常的游戏流程,那么这里就涉及到了游戏的输赢判定,这里使用的是分回合的形式,每一回合获胜则获得一分,经过若干回合后,总分最高者获胜。每一回合结束之后,会回到初始化过程,重新生成坦克。具体到每一个回合上,坦克的控制就交给Tank Manager来控制。
游戏逻辑2

从上图可以看出,Tank Manager控制了坦克的移动和射击的脚本以及UI的展示。

2、我们从游戏者的角度来梳理:

游戏逻辑3

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游戏的开发流程到此完毕。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,402评论 6 499
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,377评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,483评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,165评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,176评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,146评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,032评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,896评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,311评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,536评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,696评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,413评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,008评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,815评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,698评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,592评论 2 353

推荐阅读更多精彩内容