Unity大作业报告

        本次大作业中,我选择的主题是制作一款简单的坦克大战小游戏,此处借鉴了学长的博客,使用了KawaiiTank作为坦克的基本模型,利用Unity自带的3D导航技术实现敌人坦克的自动导航。

        演示视频:Unity 坦克大战_哔哩哔哩bilibili

        源代码:UnityGame/TankWar/Assets at master · ShirohaBili/UnityGame (github.com)


3D自动寻路的实现

第一步:建立地图

        首先,先导入KawaiiTank包中自带的Test_Field场景,然后在它的基础上,对整张地图进行美化,加入一些后续会实现的道具和地方的坦克,最后实现的地图如下所示

将上述的场景另存为TankWar,保存在Assert文件夹目录下。

第二步:建立NavMesh导航图

        先点击页面上方的Window按钮,在下拉框中选择AI一栏,选中AI一栏内的Navigation选项,引出Navigation页面,再点击Terrain,在右侧刚引出的Navigation页面中,如下设置bake选项:

         

        设置完成后点击bake按钮,生成如下的NavMesh导航图:

第三步:编写脚本实现导航

        为所有的敌方坦克加入NavMeshAgent插件,然后编写如下的脚本,实现自动巡航:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

namespace ChobiAssets.KTP{
    public class PlaceTarget : MonoBehaviour
    {
        public GameObject target; 
        NavMeshAgent mr;  
        Damage_Control_CS DamageScript;
        private int destPoint = 0;
        private bool found = false;
        private static Vector3[] points = { new Vector3(12.7f,-3.52f,140.4f),  new Vector3(91.72f, -2.427363f, -3.54f)};  //设置的巡逻点
                        
        // Use this for initialization
        void Start()
        {
            mr = GetComponent<NavMeshAgent>();  //获取到自身的NavMeshAgent组件
            DamageScript = transform.root.GetComponent<Damage_Control_CS>(); //获取到自身的Damage_Control_CS脚本,用于判断该坦克是否已经爆炸
        }

        // Update is called once per frame
        void Update()
        {
            if (!DamageScript.getStatus() && (found || Vector3.Distance(transform.position, target.transform.position) <= 30)){  //玩家没有被敌人发现时,巡逻
                mr.SetDestination(target.transform.position);
                mr.autoBraking = true;
                found = true;
            }
            else if (!DamageScript.getStatus() && Vector3.Distance(transform.position, target.transform.position) > 30){  //玩家被敌人发现时,追捕
                if (this.gameObject.tag == "guard")  return;   //如果该坦克是负责守护物资的,则不巡航。
                patrol();
            }
        }

        private void patrol()
        {
            if(!mr.pathPending && mr.remainingDistance < 10f){
                GotoNextPoint();
                Debug.Log(mr.pathStatus);
            }
            mr.SetDestination(points[destPoint]);
        }

        private void GotoNextPoint()
        {
            Debug.Log(mr.SetDestination(points[destPoint]));
            destPoint = (destPoint + 1) % points.Length;    //在两个点之间来回循环
        }
    }
}

本段代码中需要注意的地方:

        实现导航是利用NavMeshAgent组件实现的,所以首先需要获取本游戏对象上的组件。 

        这里设置了一个简单的“感知系统”,当坦克和玩家距离大于30f时,坦克会根据预先设置好的点位进行巡逻,若是发现了玩家,即距离小于30f时,会对玩家展开追捕。若系统判定本坦克已经爆炸,则会自动停止追捕,停留在原地。

        关于代码中出现的Damage_Control_CS中的getStatus方法,这是我自己加的,需要在其中增加一个方法,代码如下:

        public bool getStatus(){
            return isDead;
        }

        其实就是简单地返回一个是否死亡的标记符而已。

        通过上述的过程,已经可以实现敌方坦克的自动寻路功能了,但现在它还不能射击攻击玩家,因此还需要进一步完善。


敌方坦克的攻击实现

        在原本的Fire_Control_CS中,是没有设置敌方坦克的攻击的,所以需要对其进行修改,在其Update函数中,简单增加一些判断:

void Update()
{
    if (isLoaded == false)
    {
        return;
    }

    if (isSelected)
    {
        inputScript.Get_Input();
    }

    counter = counter + Time.deltaTime;
    if (gameObject.tag != "Player" && counter > 3.0f && Vector3.Distance(transform.position, target.transform.position) <= 30){
        Fire();
        counter = 0;
    }
}

        代码中有一个if条件判定语句,它判断了三个条件:

        ①:判断本对象是否是玩家控制的对象(为了与玩家区别开来,玩家通过点击鼠标左键来开火,不会自动开火)。

        ②:当前计数器超过了3.0f(具体的值需要根据ReLoad时间来设置)

        ③:为了增加命中率,我设置了靠近到一定距离才开枪,否则不开枪。

        将上述的脚本修改完成后,由于KawaiiTank中已经将这一脚本设置好了,所以直接运行游戏即可实现敌方坦克开炮的功能。


血条/装填条的实现

总览:

        UML设计图如下:

 

        为了让玩家更好地了解当前坦克的状况,我分别设计了一个血条和装填条,血条显示的是当前的坦克的血量,装填条显示的是当前的装弹情况,实现的效果如下所示:

        这两个条都不是静止不动的,在受到伤害或者发射导弹后,会发生相对应的变化,接下来作详细的说明:

血条

        先使用UGUI设计一个简单的血条,创建一个Canvas对象作为载体,并在其中加入一个Slider,调整合适的参数后设置为Camera_Manager的子对象,使其能够跟随画面移动,一直显示在玩家控制的坦克的正上方。

        然后再编写代码,将其和玩家控制的坦克联系到一起:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

namespace ChobiAssets.KTP
{
    public class HealthBar : MonoBehaviour
    {
        public Slider slider;
        float factor = 0.1f;
        public bool active = true;
        public Damage_Control_CS DamageScript;
        GameObject gameObject;
        float initialDurability;
        float currentDurability;

        private void Start() {
            gameObject = GameObject.Find("SD_Tiger-I_2.0");     //找到玩家的操作的坦克
            DamageScript = gameObject.GetComponent<Damage_Control_CS>();    //找到其上的Damage_Control_CS脚本
            initialDurability = 300f;
            slider.value = DamageScript.getHealth() / initialDurability * 100;  //初始化slider.value为100
        }
        
        void change() {
            if (DamageScript == null) slider.value = Mathf.Lerp(slider.value,0,0.1f);   //当玩家被摧毁时,为了避免出现空指针访问,这里直接判断值为0
            else slider.value = Mathf.Lerp(slider.value,DamageScript.getHealth() / initialDurability * 100,0.1f);   //当玩家没有被摧毁时,根据当前的生命值计算slider的值
        }
        void Update () {
            this.transform.LookAt (Camera.main.transform.position);
            change();

            Color current = slider.fillRect.transform.GetComponent<Image>().color;  //设置不同生命值下的血条颜色
            if (slider.value <= 30) {   //生命值在30%以下时,血条为红色
                slider.fillRect.transform.GetComponent<Image>().color = Color.Lerp(current, Color.red, factor);
            }
            else if (slider.value <=60){   //生命值在30%-60%之间时,血条为黄色
                slider.fillRect.transform.GetComponent<Image>().color = Color.Lerp(current, Color.yellow, factor);
            }    
            else{       //生命值大于60%时,血条为绿色
                slider.fillRect.transform.GetComponent<Image>().color = Color.Lerp(current, Color.green, factor);
            }
        }
    }
}

        代码大致的功能已经在注释中给出,这里做一点小小的补充:

        使用Mathf.Learp()方法的目的是通过使用插值,实现血条平滑减少和增加的功能,不会显得特别的突兀。

        DamageScript内的getHealth()方法也是我自己增加的,它实现的内容其实和前文的getStatus差不多,返回玩家坦克当前的生命值。

装填条

        在KawaiiTank中,作者设置了一个ReLoad时间,在发射一枚炮弹之后,都会需要有一定的时间来重新装填,装填完成之后才可以发射炮弹,这样的设计更加符合真实的情况,增加了游戏的趣味性。

        为了让玩家更好地了解到目前装填的情况,我设计了一个装填条,它位于血条的正下方,外观和血条是一模一样的,在发射后会清零,之后会随着时间缓慢恢复。代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

namespace ChobiAssets.KTP
{
    public class ReloadBar : MonoBehaviour
    {
        public Slider slider;
        float factor = 0.1f;
        public bool active = true;
        public Fire_Control_CS FireScript;  //获取发射状态

        private void Start() {
            slider.value = 100;
        }
        
        void change() {
            if (!FireScript.Loading() && active){   //发射完之后,清零
                while (slider.value != 0f) slider.value = slider.value - Time.deltaTime*1000;
                active = false;
            }
        }
        void Update () {
            this.transform.LookAt (Camera.main.transform.position);
            change();

            if (slider.value != 100f) {     //根据时间缓慢恢复
                slider.value = slider.value + Time.deltaTime*100;
                active = true;
            }
            Color current = slider.fillRect.transform.GetComponent<Image>().color;  //和血条一样
            if (slider.value <= 30) {
                slider.fillRect.transform.GetComponent<Image>().color = Color.Lerp(current, Color.red, factor);
            }
            else if (slider.value <=60){
                slider.fillRect.transform.GetComponent<Image>().color = Color.Lerp(current, Color.yellow, factor);
            }    
            else{
                slider.fillRect.transform.GetComponent<Image>().color = Color.Lerp(current, Color.green, factor);
            }
        }
    }
}

        从上面的代码可以看出,和血条不一样的地方在于,缓慢恢复的过程没有采用插值法,而是直接利用Time.deltaTime,方便控制时间。


子弹系统设计

        为了使游戏更加完整,我设置了子弹系统,初始状态下,玩家具有五发炮弹可以发射,若是子弹耗尽了,则无法发射炮弹,为了实现这一功能,我对Fire_Control_CS进行了更改了配置,加入了curAmmo变量,用于记录目前的子弹数量,并对Fire方法进行了修改,如下所示:

public void Fire()
{
    if (gameObject.tag == "Player"){    //只有玩家有这个限制,敌方坦克不受子弹数量限制
        if (curAmmo > 0){
            curAmmo--;
        }
        else return;
    }
    // Call the "Fire_Spawn_CS".
    fireSpawnScript.Fire_Linkage();

    // Call the "Barrel_Control_CS".
    barrelScript.Fire_Linkage();

    // Add recoil shock force to the MainBody.
    bodyRigidbody.AddForceAtPosition(-thisTransform.forward * recoilForce, thisTransform.position, ForceMode.Impulse);

    // Reload.
    StartCoroutine("Reload");
}

        通过上述的更改,当玩家开火时,会自动减少当前的子弹数量,若子弹数量为0时,则直接返回,此时就无法发射炮弹了。

        为了让玩家能够实时了解当前的子弹数量,我为它设计了UI,在屏幕右上角自动显示当前的子弹数量,当子弹为0时,会出现弹药不足的提示,代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace ChobiAssets.KTP
{
    public class UserGUI : MonoBehaviour {
        private GUIStyle score_style = new GUIStyle();
        private GUIStyle text_style = new GUIStyle();

        public Fire_Control_CS fireScript;
        void Start () {
            text_style.normal.textColor = new Color(0, 0, 0, 1);
            text_style.fontSize = 16;
            score_style.normal.textColor = new Color(1,0.92f,0.016f,1);
            score_style.fontSize = 16;
        }

        private void OnGUI() {
            GUI.Label(new Rect(Screen.width - 250, 10, 200, 50), "子弹数量:", text_style);

            if (fireScript.getAmmo() !=0) GUI.Label(new Rect(Screen.width - 170, 10, 200, 50), "" + fireScript.getAmmo(), text_style);
            else GUI.Label(new Rect(Screen.width - 170, 10, 200, 50), "弹药不足!!" , score_style);
        }
    }
}


        这里沿用了之前作业中实现的代码,因此变量名什么的可能有点奇怪,不用太在意。


物资设计

        为了和之前设计的子弹系统和血条形成配套,我这里设计了两种物资,分别是子弹包和医疗包,分布在地图上的两个地方,可供玩家收集。

        碰到医疗包时,会自动将其拾取,当玩家血量没满的时候,会为玩家增加30的血量,否则就会变无效,碰到弹药包时,也会自动拾取,为玩家增加5发子弹。

        在编写代码之前,我先创建了两个Cube游戏对象,在网络上找了两张贴图,将贴图贴上去,并更改了两个Cube的标签,分别为medkit和ammo,便于后续的使用。最后设计出来的医疗包和弹药包如下图所示:

        上面的是弹药包,下面的是医疗包。

        完成建模之后,编写代码来判断是否出现碰撞,然后决定是否删除该游戏对象,并奖励玩家对应的物资,UML设计图如下:

         

        详细的代码如下所示:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace ChobiAssets.KTP{
    public class Collide_Detect : MonoBehaviour
    {
        // Start is called before the first frame update
        private GameObject obj;
        Damage_Control_CS DamageScript;
        public Fire_Control_CS fireScript;

        void Start()
        {
            obj = this.gameObject;
        }

        // Update is called once per frame
        void Update()
        {
            
        }

        void OnCollisionEnter(Collision e) {
            if (e.gameObject.tag == "Player" && obj.tag == "medkit"){   //碰到医疗包的时候
                DamageScript = e.transform.root.GetComponent<Damage_Control_CS>();
                DamageScript.addHealth();
                Destroy(this.gameObject);
            }
            else if (e.gameObject.tag == "Player" && obj.tag =="Ammo"){     //碰到弹药包的时候
                fireScript.addAmmo();
                Destroy(this.gameObject);   //碰到了都需要删除
            }
        }
    }
}

        其中加入了一个碰撞体tag的判断,只有玩家碰到的时候会有奖励并且摧毁该物资,若是敌方坦克碰到了,不会有奖励物资也不会消失。

        代码中使用了DamageScript.addHealth() 方法,这是我自行添加的,代码如下:

public void addHealth(){
    if (currentDurability < 270f) currentDurability = currentDurability + 30;  //判断,保证玩家的生命值不超过上限
    else currentDurability = 300f;
}

        同时也用到了fireScript内的addAmmo() 方法,这也是我添加的,代码如下:

public void addAmmo(){
    curAmmo = curAmmo + 5;    //添加五发子弹
}

        通过上述的代码和脚本,即可以实现碰撞检测,并根据是否是玩家,决定是否添加物资的操作了。


其他

        本栏的创建是为了补充说明一些前文没有提到的内容

       

        巡逻点的设置:我设置了两个巡逻点,光是看代码很难理解是什么东西,其中一个点其实就是主道路的尽头,另一个点是放置医疗包的房间门口,敌方坦克会在这两个点之间来回巡逻。

        守卫角色的设置:在我的设计中,弹药包是稀缺物品,所以很容易就能获取的话不符合基本的常识,毕竟在很多游戏中,顶级武器都是由大Boss守着的,所以我为弹药创建了一个类似军火库的地形,并且配置了两台坦克作为护卫守着弹药库,它们的tag为guard,在前文findTarget中配置好了,它们不需要巡逻,只需要在原地守着包等着玩家过来。


参考博客

        (126条消息) 3D游戏制作——AI坦克对战_Jenny_Shirunhao的博客-CSDN博客icon-default.png?t=MBR7https://blog.csdn.net/Jenny_Shirunhao/article/details/103337423

  • 2
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值