场景搭建

功能说明和实现步骤

把场景模型文件拖拽到层级视图中,创建游戏所需要的场景,同时把场景中所需要的元素补充完整。具 体实现步骤如下:

把Assets/MOBA and Tower Defense/Demo文件夹里面的Demo2_towers拖拽到层级视图中,transform组件属性设置如下:

m2_2_1

效果展示图

m2_1_4

怪物生成

功能说明和实现步骤

1、首先创建怪物数据脚本,并通过游戏控制脚本在指定地点生成怪物:

在场景中创建出生位置游戏对象startPositon,transform组件属性设置如下:

m2_2_1-1

2、在场景中创建目标点游戏对象endPositon,transform组件属性设置如下:

m2_2_1-2

3、场景中创建GameController游戏对象,并添加CreateMonster和MonsterData脚本组件。

重点代码

/// <summary>
/// 生成怪物
/// </summary>
public class CreateMonster : MonoBehaviour
{
    //怪物预制件
    private GameObject monsterPrefab;
    //怪物出生地
    [SerializeField]
    private Transform startPosition;
    //怪物每一波生成计时器
    private float time;
    //定义一个变量存储正在生成第几波
    private int index = 0;
    //定义一个bool类型变量来存储是否正在生成怪物
    private bool isBorning = false;
    //定义一个结构体类型变量来缓存当前需要生成的波次怪物信息
    private MonsterDataStruct dataStruct;

    //定义一个小计时器用来记录每一个小怪的生成
    private float smallTime;
    //定义每一个小怪的生成时间间隔
    private float smallTimeInterval = 1;
    //定义一个变量记录当前波生成到第几个小怪
    private int monsterCount = 0;

    // Use this for initialization
    void Start()
    {
        //得到第一波敌人数据
        dataStruct = MonsterData.Instance.Monsters[index];
        //加载怪物预制件
        monsterPrefab = Resources.Load<GameObject>("mon_orcWarrior");
    }

    // Update is called once per frame
    void Update()
    {
        //判断计数变量的值是否和怪物链表长度相等,如果相等证明所有波次的怪物生成完毕,直接返回
        if (index == MonsterData.Instance.Monsters.Count)
        {
            return;
        }

        //isBorning=false
        if (isBorning==false)
        {
            //如果正在生成怪物标志位为false
            //开始启动计时器
            //计时器不断累加时间
            time += Time.deltaTime;
            //如果累加的时间大于等于当前波次的等待时间
            if (time >= dataStruct.waitTime)
            {
                //将正在生成怪物标志位设置为true
                isBorning = true;
                //将计时器清0;
                time = 0;
            }
        }
        else
        {
            //如果正在生成怪物标志位为true
            //调用生成怪物方法即可
            CreatMethod();
        }
    }

    //生成一波怪物的方法
    private void CreatMethod()
    {
        //开始生成第一波怪物
        //重新定义一个小的定时器来控制每一个小怪的生成间隔
        smallTime += Time.deltaTime;
        if (smallTime >= smallTimeInterval)
        {
            smallTime = 0;
            //满足时间生成一个怪物
            GameObject monster = Instantiate(monsterPrefab, startPosition.position,Quaternion.identity);
            monster.name = "monster"+index+ monsterCount;
            //每生成一个怪物记录一次,
            monsterCount++;
            //判断记录的个数和当前波次最大怪物数量相等,
            if (monsterCount == dataStruct.monsterCount)
            {
                //将小怪物计数也清零
                monsterCount = 0;
                //如果相等那么当前波怪物生成完毕
                //正在生成第几波计数变量自增
                index++;
                //将正在生成标志位设置为false
                isBorning = false;
                //更新下一波波敌人数据
                if (index < MonsterData.Instance.Monsters.Count)
                {
                    dataStruct = MonsterData.Instance.Monsters[index];
                }
            }
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//定义一个结构体,来存储每一波怪物信息
public struct MonsterDataStruct
{
    //当前波怪物的移动速度
    public float speed;
    //当前波怪物的数量
    public int monsterCount;
    //上一波生成结束之后的等待时间
    public float waitTime;
}

/// <summary>
/// 怪物数据管理类
/// </summary>
public class MonsterData : MonoBehaviour {
    //将脚本组件设置为单例的方式
    //首先定义一个静态变量
    private static MonsterData instance;
    //定义一个公有的静态属性值
    public static MonsterData Instance
    {
        get { return instance; }
    }
    //第一波怪物的移动速度初始值
    private float initSpeed=4;
    //第一波怪物的数量初始值
    private int initCount = 6;
    //第一波怪物生成等待时间
    private float initWaitTime = 2;
    //定义一个链表存储每一波的数据
    private List<MonsterDataStruct> monsters;

    public List<MonsterDataStruct> Monsters
    {
        get { return monsters; }
    }

    private void Awake()
    {
        //由于Awake总是被执行而且只执行一次,所以开始就对instance赋值
        instance = this;
        //初始化链表对象
        monsters = new List<MonsterDataStruct>();
        //定义一个循环来生成5波怪物数据
        for (int i = 0; i < 5; i++)
        {
            //创建一个结构体对象
            MonsterDataStruct data = new MonsterDataStruct();
            //设置速度值
            data.speed = initSpeed + i * 0.5f;
            //设置数量
            data.monsterCount = initCount + i;
            //设置等待时间
            data.waitTime = initWaitTime + i * 3f;
            //将数据添加到链表中
            monsters.Add(data);
        }
    }

    // Use this for initialization
    void Start () {
        
    }
}

脚本组件属性设置截图

m2_2_3

效果展示图

m2_2_4

怪物移动

功能说明和实现步骤

Navgation烘焙场景导航路径:

m2_3_1-1

怪物添加如下组件:

m2_3_1-2

重点代码

public class MonsterControl : MonoBehaviour {
    //定义一个委托来广播怪物死亡事件
    public delegate void DeadDelegate(GameObject monster);
    //用事件修饰委托变量
    public event DeadDelegate deadEvent;

    //获取导航组件
    private NavMeshAgent agent;
    //获取寻路目标点
    private Transform endPosition;
    //怪物血量
    private float hp = 100;
    //得到Animation组件
    private Animation ani;

	// Use this for initialization
	void Start () {
        ani = GetComponent<Animation>();
        endPosition = GameObject.Find("endPosition").transform;
        agent = GetComponent<NavMeshAgent>();
        //调用导航方法设置目标点
        agent.SetDestination(endPosition.position);
    }
	
	// Update is called once per frame
	void Update () {
		
	}

    /// <summary>
    /// 受到伤害逻辑处理
    /// </summary>
    /// <param name="damage"></param>
    public void Damage(float damage)
    {
        if (hp <= 0)
        {
            return;
        }

        hp -= damage;
        if (hp <= 0)
        {
            //怪物死亡
            //禁用导航组件
            //setActive方法是隐藏和显示游戏对象
            //enabled属性是禁用和启用游戏对象身上的组件,false禁用,true启用
            agent.enabled = false;
            //播放死亡动作
            ani.CrossFade("Dead");
            //死亡之后禁用碰撞器组件
            GetComponent<CapsuleCollider>().enabled = false;
            //广播自己死亡事件
            if (deadEvent != null)
            {
                Debug.Log("dead:"+transform.name);
                deadEvent(gameObject);
            }

            //等待死亡动作播完延迟销毁当前怪物对象
            Destroy(gameObject,1);
        }
    }
}

 

脚本组件属性设置截图

m2_3_3-1

m2_3_3-2

生成炮塔

功能说明和实现步骤

GameController游戏对象添加InitTowerFire组件即可,如下:

m2_4_1

重点代码

public class InitTowerFire : MonoBehaviour {
    //定义字段存储大炮预制件
    private GameObject firePrefab;
    private int index;

	// Use this for initialization
	void Start () {
        //通过Resources加载大炮预制件
        firePrefab = Resources.Load<GameObject>("Tow_Machinegun1");

    }
	
	// Update is called once per frame
	void Update () {
        //实时校验是否摁下鼠标左键
        if (Input.GetMouseButtonDown(0))
        {
            //在鼠标点击位置生成一条射线
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            //获取拦截射线的所有碰撞体信息
            RaycastHit[] hits = Physics.RaycastAll(ray);
            //遍历数组
            foreach (RaycastHit hit in hits)
            {
                //校验拦截对象的标签是否是tower并且该炮塔基座下没有子物体时生成一门大炮
                if (hit.transform.tag == "tower" && hit.transform.childCount == 0)
                {
                    //创建一门大炮
                    //实例化需要大炮预制件,父物体
                    GameObject fire = Instantiate(firePrefab, hit.transform);
                    index++;
                    fire.name = "tower" + index;
                    //得到基座的碰撞体组件
                    BoxCollider collider = hit.transform.GetComponent<BoxCollider>();
                    //通过碰撞体组件得到y轴的值也就是对应大炮基座的高度
                    float height = collider.size.y - 0.2f;
                    //设置大炮的本地坐标为:沿着y轴上移以上得到的高度值
                    fire.transform.localPosition = Vector3.up * height;
                    //结束循环
                    break;
                }
            }
        }
    }
}

效果展示图

m2_4_4

炮塔攻击

功能说明和实现步骤

Tow_Machinegun1预制件展开在Base游戏对象添加以下组件TowerFireControl:

m2_5_1

重点代码

/// <summary>
/// 主要控制炮塔转向,开火,处理进入攻击范围的怪物等逻辑
/// </summary>
public class TowerFireControl : MonoBehaviour {
    //炮管对象
    [SerializeField]
    private GameObject turret;
    //子弹预制件
    private GameObject firePrefab;
    //炮塔转向目标的速度
    private float turnSpeed=5;
    //设置一个变量存储正在攻击的目标
    private GameObject currentAttackTarget;

    //定义生成炮弹的计时器
    private float time;
    //定义发射间隔
    private float timeInterval = 0.5f;

    //开火点
    [SerializeField]
    private Transform firePoint;

    //定义一个链表存储正在攻击范围内的所有怪物
    private List<GameObject> attackList = new List<GameObject>();

    // Use this for initialization
    void Start () {

        //加载子弹预设体
        firePrefab = Resources.Load<GameObject>("Fire");

    }

    // Update is called once per frame
    void Update() {
        //如果攻击目标为空
        if (currentAttackTarget == null)
        {
            //查询链表是否为空,如果链表不为空,那么获取第一个怪物作为攻击目标
            if (attackList.Count != 0)
            {
                currentAttackTarget = attackList[0];
            }
        }
        else
        {
            //如果攻击目标不为空那么开火
            //开炮
            Fire();
        }
    }

    private void OnTriggerEnter(Collider other)
    {
        //怪物进入攻击范围时将其加入攻击链表
        if (other.tag == "monster")
        {
            attackList.Add(other.gameObject);
            //订阅怪物死亡事件
            other.GetComponent<MonsterControl>().deadEvent += MosterDead;
        }
    }

    private void OnTriggerExit(Collider other)
    {
        //怪物离开攻击范围时将其从攻击链表中移除
        if (other.tag == "monster")
        {
            attackList.Remove(other.gameObject);
            if (other.gameObject == currentAttackTarget)
            {
                //将攻击目标清空
                currentAttackTarget = null;
            }
            //怪物走出攻击范围取消订阅死亡事件
            other.GetComponent<MonsterControl>().deadEvent -= MosterDead;
        }
    }

    /// <summary>
    /// 当怪物死亡之后的逻辑处理
    /// </summary>
    private void MosterDead(GameObject monster)
    {
        //判断死亡的怪物是否在攻击链表中,如果在的话移除
        if (attackList.Contains(monster))
        {
            attackList.Remove(monster);
        }

        //如果死亡的怪物是正在攻击的目标
        if (monster == currentAttackTarget)
        {
            //清空正在攻击的目标对象
            currentAttackTarget = null;
        }
            
    }

    /// <summary>
    /// 当攻击目标不为空时调用该方法开炮
    /// </summary>
    private void Fire()
    {
        //首先得到由大炮指向攻击目标的方向向量
        Vector3 dir = currentAttackTarget.transform.position - turret.transform.position;
        //将该方向转为四元数
        Quaternion dirQua = Quaternion.LookRotation(dir);
        //使用四元数插值控制炮管由快到慢看向攻击目标
        turret.transform.rotation = Quaternion.Lerp(turret.transform.rotation,dirQua,Time.deltaTime*turnSpeed);
        //计算炮管的朝向和目标方向的夹角,如果小于5度,那么直接将炮管的方向设置为目标方向
        float angle = Vector3.Angle(turret.transform.forward,dir);
        if (angle <= 5)
        {
            turret.transform.rotation = dirQua;
            //生成子弹
            time += Time.deltaTime;
            if (time >= timeInterval)
            {
                time = 0;
                //间隔一段时间生成一发子弹
                GameObject fire = Instantiate(firePrefab, firePoint.position, firePoint.rotation);
                //设置子弹目标点为当前正在攻击的目标
                fire.GetComponent<FireControl>().Target = currentAttackTarget;
            }
        }
    }
}

相机移动

功能说明和实现步骤

_MainCamer游戏对象添加CameraControl脚本组件:

m2_6_1

重点代码

/// <summary>
/// 相机控制脚本,相机根据水平和垂直轴的输入沿着世界坐标进行移动
/// </summary>
public class CameraControl : MonoBehaviour {

	// Use this for initialization
	void Start () {
		
	}
	
	// Update is called once per frame
	void Update () {
        //得到水平轴输入
        float h = Input.GetAxis("Horizontal");
        //得到垂直虚拟轴输入
        float v = Input.GetAxis("Vertical");
        //h的值来控制相机沿着z轴进行移动,v的值控制相机沿着x轴前后移动
        //摁下D=(0,0,1),摁下A=(0,0,-1)
        //摁下W =(-1,0,0),摁下s=(1,0,0)
        transform.Translate(new Vector3(h, 0, v) * Time.deltaTime * 6,Space.World);
	}
}