欢迎光临散文网 会员登陆 & 注册

Unity快速上手系列之4:《塔防》续

2019-05-17 17:42 作者:皮皮关做游戏  | 我要投稿

作者:四五二十


大家好。

最近状态比较佳,趁这个机会继续丰富咱们的塔防游戏。

对了,如大家觉得哪里没能理解,欢迎在评论区给我留言。


在上一篇中,我们已经能够通过生成器产生敌人,这些敌人能自动寻路到达主城所在位置进行攻击。主城被攻破后游戏结束。攻击方已经具备。

接下来是防御方了。这里,咱们建立防御塔阻止敌人的进攻。首先说说本例中三种防御塔的攻击方式:

弓箭手:远程攻击,对敌人射出弓箭造成伤害,弓箭可以插在敌人身上;

锤兵:群体攻击,锤击地面,原地击飞敌人,减慢敌人移动速度;

剑士:近程攻击。

那么从弓箭手开始,来做他的攻击功能:

弓箭手攻击时会从“枪口”处发出弓箭,所以先在弓箭手模型上创建一个枪口Muzzle:

枪口一Z轴指向发射方向

枪口要随弓箭移动,在Hierarchy面板中大概在这里:

为了获取这个未知层级的子物体,也为了让其它类也方便调用,我们先写一个工具类,里面创建一个查找未知层级子物体的方法:

public class ToolsMethod

{

    private static ToolsMethod _Instance;

    public static ToolsMethod Instance //单例

    {

        get

        {

            if (_Instance == null)

                _Instance = new ToolsMethod();

            return _Instance;

        }

    }

    //根据名称获取未知层级子物体

    public Transform FindChildByName(Transform currentTF, string childName)

    {

        Transform childTF = currentTF.Find(childName);

        if (childTF != null) return childTF;

        for (int i = 0; i < currentTF.childCount; i++)

        {

            childTF = FindChildByName(currentTF.GetChild(i), childName);

            if (childTF != null) return childTF;

        }

        return null;

    }

}

 


弓箭手的脚本中还需要拿到箭矢的预制体,先把箭矢预制体放入路径中:

为箭矢预制体创建一个脚本Bullet挂上去:


public class Bullet : MonoBehaviour

{

}

 

为了让箭矢和其它物体可以使用对象池,我们先创建一个空物体,作为所有对象池的管理器。取名“PoolManager”,创建对象池类:


public class GameObjectPool

{

    private static GameObjectPool _Instance;

    public static GameObjectPool Instance //单例模式

    {

        get

        {

            if (_Instance == null)

                _Instance = new GameObjectPool();

            return _Instance;

        }

    }

    //用于保存所有对象池

    public Dictionary<string, Transform> poolDict = new Dictionary<string, Transform>();

    //获取对象池

    public Transform GetPool(string poolName)

    {

        if (poolDict.ContainsKey(poolName))

            return poolDict[poolName];

        //字典中没有重新创建

        Transform poolObj = new GameObject(poolName + "_Pool").transform;

        //创建的对象池放入对象池管理器的子物体中

        poolObj.SetParent(GameObject.Find("PoolManager").transform);

        poolObj.gameObject.SetActive(false);

        poolDict.Add(poolName, poolObj);

        return poolObj;

    }

}

 


其它物体需要对象池也可以调用该类的方法。

弓箭手需要有一个攻击范围。敌人进入该范围后才会被认定为目标并展开攻击。

首先将敌人都放在Enemy层,创建弓箭手的脚本:

public class Pagoda : MonoBehaviour

{

    protected Animator anim;

    protected Transform muzzle;

    Bullet Arrow;

    Transform arrowPool; //箭矢对象池

    //初始化

    public void initPagoda()

     {

        enabled = true; //启用脚本

        anim = GetComponentInChildren<Animator>();

        muzzle = ToolsMethod.Instance.FindChildByName(transform, "Muzzle");

        Arrow = Resources.Load<Bullet>("Prefab/Bullet/Arrow");

        //为箭矢创建一个以它命名的对象池

        arrowPool = GameObjectPool.Instance.GetPool(Arrow.name);

     }

    private void Update()

     {

        //游戏结束,停止攻击

        if (GameMain.instance.gameOver)

        {

            anim.SetBool("Attack", false);

            return;

        }

        GetTarget();

    }

    public float attactRange; //攻击范围

    public float damage; //伤害值

    protected Enemy target; //攻击目标

    //获取攻击目标

    void GetTarget()

    {

        if (target == null) //攻击目标为空时,用球形射线检测Enemy层找寻攻击目标

        {

            Collider[] enemys = Physics.OverlapSphere(transform.position, attactRange, LayerMask.GetMask("Enemy"));

            if (enemys.Length == 0)

                anim.SetBool("Attack", false);

            //发现敌人,设为目标,进行攻击(播放攻击动画)

            for (int i = 0; i < enemys.Length;)

            {

                target = enemys[i].GetComponent<Enemy>();

                anim.SetBool("Attack", true);

                break;

            }

        }

        else

        {

            //面向攻击目标

            Vector3 pos = target.transform.position;

            Quaternion dir = Quaternion.LookRotation(new Vector3(pos.x, transform.position.y, pos.z) - transform.position);

            transform.rotation = Quaternion.Lerp(transform.rotation, dir, 0.1f);

            //攻击目标离开攻击范围或死亡,重新获取攻击目标

            if (Vector3.Distance(target.transform.position, transform.position) >= attactRange || target.state == EnemyState.death)

                target = null;

        }

    }

    //攻击方法(放在攻击动画事件中)

    public virtual void PagodaAttack()

    {

        //在枪口位置创建箭矢

    }

}

 

弓箭手的初始化在弓箭手创建时调用。

如果弓箭手已经能检测敌人并发射箭矢,接下来就是箭矢的功能了。主要如下:

1. 飞向目标(始终面向目标,并向前飞);

2. 打到目标,调用目标受伤方法(用距离判断是否打到);

3. 插在目标身上(认目标做父物体,停止移动)

要让箭矢能插在敌人身上,需要在敌人模型上创建一个空物体做打击点,取名HitPos,为了效果逼真,HitPos最好放在模型骨骼上,并可以在敌人脚本中声明一个hitPos,使用查找未知层级子物体的方法来获取:

Transform hitPos = ToolsMethod.Instance.FindChildByName(transform, "HitPos");

 

当弓箭手检测到敌人创建箭矢时,同时将敌人信息、伤害值、箭矢对象池赋给箭矢,由箭矢去做接下来的工作,如伤害敌人,它的脚本可以这样写:

public class Bullet : MonoBehaviour

{

    public float speed;

 

    Enemy target; //攻击目标

    float damage; //伤害值

    Transform pool; //对象池

    Vector3 initPos; //初始位置

    //初始化

    public void InitBullet(Vector3 position, Quaternion rotation, Enemy _target, float _damage, Transform _pool)

    {

        transform.SetParent(null);

        transform.position = position;

        transform.rotation = rotation;

        target = _target;

        damage = _damage;

        pool = _pool;

        initPos = transform.position;

    }

    private void Update()

    {

        if (transform.parent == null) //没有射中目标,继续飞

        {

            transform.Translate(0, 0, speed * Time.deltaTime);

            if (Vector3.Distance(initPos, transform.position) > 500) //飞出一定范围自动销毁

                DestroySelf();

 

            if (target != null && target.state != EnemyState.death) //如果目标活着朝向目标

            {

                transform.LookAt(target.hitPos);

                //到达有效范围,调用目标受伤方法,成为目标子物体(插在目标身上)

                if (Vector3.Distance(target.hitPos.position, transform.position) <= 1)

                {

                    target.Damage(damage);

                    transform.SetParent(target.hitPos);

                }

            }

        }

        else if (target.state == EnemyState.death) //射中后,只要目标一死就销毁

            DestroySelf();

    }

    //销毁自身(进入对象池)

    private void DestroySelf()

    {

        transform.SetParent(pool);

    }

}

 

箭矢的移动速度在编辑器界面自行设定,在弓箭手的攻击方法中,就可以在创建箭矢的同时把相关信息赋给它:

    //攻击方法(放在攻击动画事件中)

    public virtual void PagodaAttack()

    {

        //如果对象池有,则从对象池取子弹,否则重新实例化

        //设定位置,方向,攻击目标,伤害值,所在对象池

        if (arrowPool.childCount > 0)

            arrowPool.GetChild(0).GetComponent<Bullet>().InitBullet(muzzle.position, muzzle.rotation, target, damage, arrowPool);

        else

            Instantiate(Arrow).InitBullet(muzzle.position, muzzle.rotation, target, damage, arrowPool);

    }

 

弓箭手做完,接下来是锤子兵的功能:

锤子兵的功能和弓箭手非常相似。除了攻击方式不同,其它都一样。所以我们创建锤子兵的脚本可以继承自弓箭手的脚本:

public class Pagoda2 : Pagoda

{

    public float force; //击飞力度

    public ParticleSystem effect; //击飞特效

    //重写攻击方法(在攻击动画事件中调用)

    public override void PagodaAttack()

    {

        //群体攻击,作用范围始攻击范围的一半

        Collider[] enemys = Physics.OverlapSphere(muzzle.position, attactRange / 2, LayerMask.GetMask("Enemy"));

        for (int i = 0; i < enemys.Length; i++)

        {

            //伤害作用范围内的每个敌人

            Enemy enemy = enemys[i].GetComponent<Enemy>();

            enemy.Damage(damage);       

            //播放特效

            effect.transform.position = muzzle.position;

            effect.Play();

            //击飞方法

        }

    }

}

 

除了对敌人造成伤害之外,需要专门写一个击飞的方法,击飞方法可以写在锤子兵的脚本里,也可以在敌人脚本(Enemy)中写一个被击飞方法:

    //被击飞方法

    bool isFly; //是否被击飞(处于击飞状态时不能再被击飞)

    public void StrikeFly(float force)

    {

        if (isFly == false) //未被击飞状态下才可以被击飞

        {

            isFly = true;

            rigid.AddForce(Vector3.up * force, ForceMode.Impulse);

            float initSpeed = speed; //初始速度

            speed = 0;

            //0.5秒后恢复

            Util.Instance.AddTimeTask(() =>

            {

                speed = initSpeed;

                isFly = false;

            }, 0.5f);

        }

    }

 

该方法是公开属性,在锤子兵那边调用。

然后是剑士的功能:

剑士功能最简单,但增加了一个暴击的属性,将暴击率代入攻击力的计算就好,脚本也继承自弓箭手:

public class Pagoda3 : Pagoda

{

    public float critChance = 0.2f; //暴击率

    //重新攻击方法

    public override void PagodaAttack()

    {

        if (target != null)

        {

            //代入暴击率,计算最终伤害(暴击是双倍伤害)

            int crit = (int)(critChance * 100);

            target.Damage(damage * (Random.Range(0, 100) < crit ? 2 : 1), this);

        }

    }

}

 

是不是很简单?

好了,三个人形防御塔的功能都做完了。现在正式做安放防御塔的功能,用Image搭建一个防御塔菜单UI界面,放入精灵图片,取名“PagodaMenu”:

在PagodaMenu下创建三个Image做头像:

我是直接把模型放在红色背景板前截图

在道路旁摆上若干的防御塔地形,将层设为Pagoda:

我们先来看下放置的过程:

通过演示,我们大概可以理清创建的逻辑:

1. 点击头像实例化一个防御塔,并显示攻击范围;

2. 按住鼠标不放防御塔会跟随鼠标移动;

3. 攻击范围的颜色在可放置位置显示为绿色,其余地方为红色;

4. 在可放置位置弹起鼠标时,会将防御塔放在地形上,且同时为防御塔初始化。

根据以上的逻辑顺序,我们首先要让图片具有可点击事件与弹起事件。为头像Image创建一个脚本,引入相应接口:

public class IconElement : MonoBehaviour, IPointerDownHandler, IPointerUpHandler

{

    //点击事件

    public void OnPointerDown(PointerEventData eventData)

    {

    }

    //弹起事件

    public void OnPointerUp(PointerEventData eventData)

    {

    }

}

 


然后可以从主摄像打出射线,将射线检测点的坐标赋给防御塔,可以放在在Update中调用。攻击范围的显示可以通过创建一个普通球形物体来实现,后面在代码中调整颜色就行了。位置也跟随射线检测点移动,平时处于禁用状态,只有在摆放防御塔过程中调用:

现在将所有防御塔预制体放入路径中:

预制体名字和头像图片的名字相同,且一一对应:

为Icon的脚本创建初始化方法:

    string pagodaName; //防御塔名,用来加载防御塔

    Camera mainCamera; //主摄像

    Transform attRange; //攻击范围显示器

    Material ria; //范围显示器的材质球

    LayerMask layer; //射线可照射层

    //初始化

    public void Init(Camera _mainCamera, Transform _attRange)

    {

        pagodaName = GetComponent<Image>().sprite.name; //自身图片的名字就是对应防御塔名字

        mainCamera = _mainCamera;

        attRange = _attRange;

        ria = attRange.GetComponent<MeshRenderer>().material;

        layer = LayerMask.GetMask("Ground") | LayerMask.GetMask("Way") | LayerMask.GetMask("Pagoda");

    }

 

然后在点击事件中写入点击时要执行的功能:


    Pagoda pagodaObj; //防御塔实例   

//点击头像实例化防御塔

    public void OnPointerDown(PointerEventData eventData)

    {

        //加载防御塔模型

        pagodaObj = Instantiate(Resources.Load<Pagoda>("Prefab/Chara/PagodaChara/" + pagodaName));

        //启用攻击范围显示器并将防御塔攻击方位反映在尺寸上

        attRange.gameObject.SetActive(true);

        attRange.localScale = new Vector3(pagodaObj.attactRange * 2, 10, pagodaObj.attactRange * 2);

        GetComponent<Image>().color = new Color(0, 1, 0); //头像变色

    }

 

弹起事件中根据条件判断当前是否可放置防御塔,判断逻辑放在Update中:


    bool isPlace; //是否可放置

    Transform terrain; //可放置地形

    //抬起鼠标放置或删除防御塔

    public void OnPointerUp(PointerEventData eventData)

    {

        if (isPlace) //可放置时

        {

            //放置在该地形并成为地形子物体,然后初始化

            pagodaObj.transform.position = terrain.position;

            pagodaObj.transform.SetParent(terrain);

            pagodaObj.initPagoda();

        }

        else //不可放置则销毁

            Destroy(pagodaObj.gameObject);

 

        attRange.gameObject.SetActive(false); //禁用范围显示器

        pagodaObj = null;

        GetComponent<Image>().color = new Color(1, 1, 1); //头像变色

    }

    void Update()

    {

        //如果防御塔实例化,则找寻可以放置的位置

        if (pagodaObj != null)

        {

            //摄像机向鼠标位置发射线

            Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition);

            RaycastHit hit;

            if (Physics.Raycast(ray, out hit, 500, layer))

            {

                pagodaObj.transform.position = hit.point; //防御塔模型根据鼠标移动

                attRange.position = hit.point; //范围显示器根据鼠标移动

                int index = hit.collider.gameObject.layer; //获取照射到物体的层

                //如果是可以放置的地形,并且该地形上没有其它防御塔,就可以放置

                if (LayerMask.LayerToName(index) == "Pagoda" && hit.collider.transform.childCount == 0)

                {

                    isPlace = true;

                    terrain = hit.collider.transform;

                    ria.color = new Color(0, 1, 0, 0.3f);

                }

                else

                {

                    isPlace = false;

                    ria.color = new Color(1, 0, 0, 0.3f);

                }

            }

        }

    }

 

好的,头像功能的脚本就做完了。头像的数量可以根据防御塔具体数量增减。我们注意到每个头像的初始化方法没地方调用,可以创建一个管理类来对它们统一初始化,将它挂在PagodaMenu上:

public class PagodaMenu : MonoBehaviour

{

    public Camera mainCamera; //主摄像机

    public Transform attRange; //攻击范围显示器

    public void Init()

    {

        IconElement[] icons = GetComponentsInChildren<IconElement>();

        for (int i = 0; i < icons.Length; i++)

        {

            icons[i].Init(mainCamera, attRange);

        }

    }

}

 

主摄像机和范围显示器在编辑器界面直接拖入。

到这里,我们之前在第一篇文章演示视频里的功能就做完了。之后可能会做一些经济系统方面的功能,如消灭敌人可以获得金钱、使用金钱购买和升级防御塔等等。

工程链接:https://pan.baidu.com/share/init?surl=e8UOSkOtG7hr93t2kl3xnA

提取码:oshk


有意向参与线下游戏开发学习的童鞋,欢迎访问:http://levelpp.com/

皮皮关的游戏开发QQ群也欢迎各位强势插入:869551769

Unity快速上手系列之4:《塔防》续的评论 (共 条)

分享到微博请遵守国家法律