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

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

2019-05-15 12:07 作者:皮皮关做游戏  | 我要投稿

作者:四五二十


大家好。偶尔想起了这个手把手教学的、但现已长满杂草的坑,还是来挖几铲子。

这一期的游戏是最常见的类型之一——塔防。

塔防游戏相信大家并不陌生,几个主要元素如下:

1、敌方士兵

2、我方防御塔

3、我方主城

emmmmmmm好像就没了。

玩法就是建立防御塔阻击前往我方主城的敌兵,可以通过视频直观感受下:


人越狠,话越不多。不多说,接下来我们一步步把这几个功能做完。

素材准备:

网上随便找一些资源就行,不一定要和我一样。这里再次强调:

网上获取的资源一定不能用作商业用途!!!!!!

就本工程而言,资源有一下几种:

敌人2个,分别拥有移动,攻击,待机,死亡四种动画

防御塔3个,拥有待机,攻击两种动画

人形防御塔可还行

主城1个,主地形 1组(内含各种杂草乱石 )

敌人地形(敌人能用来走的路)1种,防御塔地形(防御塔能放置的地方)1种

箭矢1个

弓兵模型中自带

场景搭建:

先从简单的功能做起:让敌人从生成点走到主城,看见主城就攻击。

搭建一个简单场景:

为了检测敌人寻路,最好是能转弯的道路

敌人和主城有一个都有血量的属性,都会被攻击,这里为它们做能显示在头上的血条。

以主城为例,在主城的子节点层创建一个Sprite做黄血条,设为黄色,取名“BloodStrip”,调整好大小:

然后在BloodStrip的子节点层创建一个空物体,取名“Hp”,在Hp的子节点层再创建一个Sprite做红血条,名字“Red”,设为红色,大小和黄血条一样,把黄血色覆盖:

接下来就移动红血条位置,让它左边边缘与父物体Hp的Y轴重合:

然后再将Hp往右移动,让Y轴与黄血条左边缘重合(红血条刚好覆盖黄血条):

这样我们只需要设置Hp的X轴大小,就可以控制红血条长度了:

***这里请初学者注意,如果你选取的红血条图片资源不是纯色的、是有其他花纹的,则不能用这个方法。原因很简单,这种方法会把花纹拉长或压扁。大家可以下来想一下:这种情况下应该怎样来设置?

后面在代码中只需要将当前血量与总血量的比值赋给Hp的X轴,就可以将血量信息显示在界面上了。敌人血条做法一样。

做好后让BloodStrip处于禁用状态,受伤后才显示(这是游戏UI显示的一个约定俗成的规则)。

代码编写:

为主城与敌人创建一个基类脚本Character:

public class Character : MonoBehaviour

{

    public float totalHp = 100; //总血量

    float surHp; //剩余血量

    protected Transform hpObj; //黄血条

    protected Transform redHp; //血条红条

    protected Transform mainCamera; //主摄像机

 

    public virtual void Init() //初始化

    {

        surHp = totalHp;

        hpObj = transform.Find("BloodStrip");

        redHp = hpObj.Find("Hp");

        mainCamera = GameObject.Find("Main Camera").transform;

    }

    public void Damage(float damage) //受伤方法,参数为受到的伤害值

    {

        if (surHp > damage) //当前血量大于受伤血量,正常扣血

        {

            surHp -= damage;

            //受伤后开始显示血条

            if (surHp < totalHp)

                hpObj.gameObject.SetActive(true);

            Vector3 hpScale = redHp.localScale;

            hpScale.x = surHp / totalHp;

            redHp.localScale = hpScale;

        }

        else //当前血量不够,调用死亡方法         

            Death();

    }

    public virtual void Death() //死亡方法

    {

        surHp = 0;

        hpObj.gameObject.SetActive(false); //血条不再显示

    }

}

 

创建主调脚本:用于游戏初始化和记录游戏死亡,挂在一个场景物体上:

public class GameMain : MonoBehaviour

{

    public static GameMain instance;

    public bool gameOver;

    void Start()

    {

        InitGame();

    }

    //初始化游戏

    void InitGame()

    {

        instance = this; //单例

        gameOver = false;

    }

}

 

创建主城脚本,继承自Character脚本:


public class MainCity : Character

{

    void Start()

    {

        Init();

    }

    private void Update()

    {

        hpObj.rotation = mainCamera.rotation; //血条始终面向镜头

    }

    public override void Death() //重新死亡方法

    {

        base.Death();

        GameMain.instance.gameOver = true; //游戏结束

    }

}

 

敌人的脚本也继承自Charater,除了受伤和死亡之外还能攻击与移动:


public class Enemy : Character

{

    Animator anim;

    public float damage; //伤害

    public float speed; //移动速度

    MainCity target; //主城

    public override void Init()

    {

        base.Init();

        anim = GetComponent<Animator>();

    }

    private void Update()

    {

        hpObj.rotation = mainCamera.rotation; //血条始终面向镜头

    }

    //前进方法

    private void EnemyForward()

    {

    }

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

    private void EnemyAttack()

    {

        if (target != null)

            target.Damage(damage);

    }

    //死亡方法

    public override void Death()

    {

        base.Death();

        anim.Play("death");

    }

    //尸体消失

    private void DestroySelf()

    {

        Destroy(gameObject);

    }

}

 

重点在移动方法上。因为敌人的移动带有寻路功能,这里没有采取Unity自带的NavMeshAgent,而是用脚本来实现,主要思路仿照盲人的行进方式,利用射线充当导盲棍,发现前方道路中断再从两边找新的行进路线:

拐杖就是射线

要利用好这个思路,场景中道路的搭建也有一定要求,道路都要挂上MeshCollider组件,方便射线检测。

所有道路的Z轴指向路线前进方向

道路的物体层设置为“Way”,主城也挂上碰撞器,物体层设为“City”。

在敌人模型身上创建一个空物体为眼睛,取名为“Eye”,主要作用是从此为射线起始点,位置合适即可,注意,因为所有敌人都用的相同脚本,所以所有敌人的眼睛高度距离地面相同:

正面看这些模型真特么惊悚

当然每个敌人也请挂上碰撞器和刚体以及Animator组件:

创建一个敌人状态机:

public enum EnemyState //状态机

{

    forward,

    attack,

    death

}

 

重写初始化方法:


    Animator anim;

    Rigidbody rigid;

    public EnemyState state;

    Transform eye; //眼睛:用于观测道路和攻击目标

    List<Collider> ways; //记录走过的路(不走回头路)

    //重新初始化方法

    public override void Init()

    {

        base.Init();

   

        anim = GetComponent<Animator>();

        rigid = GetComponent<Rigidbody>();

        gameObject.layer = LayerMask.NameToLayer("Enemy"); //敌人层设置为"Enemy"

        state = EnemyState.forward;

        eye = transform.Find("Eye");

        ways = new List<Collider>();

    }

 

编写移动方法,并在Update中调用:


private void Update()

    {

        hpObj.rotation = mainCamera.rotation; //血条始终面向镜头

        if (GameMain.instance.gameOver) //游戏结束播放待机动画

            anim.Play("idle");

        else if (state == EnemyState.forward)

            EnemyForward();

    }

    public int view; //视野

    Quaternion wayDir; //前进方向

    MainCity target; //主城

    Transform way; //正在走的路

    public float speed;

    //前进方法

    private void EnemyForward()

    {

        RaycastHit hit;

        //看见攻击目标则攻击

        if (Physics.Raycast(eye.position, transform.forward, out hit, view, LayerMask.GetMask("City")))

        {

            state = EnemyState.attack;

            anim.Play("attack");

            target = hit.collider.GetComponent<MainCity>();

        }

 

        //斜下方30°打射线检测前方道路

        if (Physics.Raycast(eye.position, Quaternion.AngleAxis(30, transform.right)

            * transform.forward, out hit, 50, LayerMask.GetMask("Way")))

        {

            Debug.DrawLine(eye.position, hit.point, Color.blue);

            //发现未走过的道路,获取该道路,朝向该路通往的方向

            if (!ways.Contains(hit.collider))

            {

                ways.Add(hit.collider);

                way = hit.transform;

                wayDir = Quaternion.LookRotation(way.forward);

            }

        }

        else //前方没路了发射球形射线检测周围是否有路

        {

            Collider[] colliders = Physics.OverlapSphere(transform.position, 8, LayerMask.GetMask("Way"));

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

            {

                //发现未走过的道路,获取该道路,朝向该路通往的方向

                if (!ways.Contains(colliders[i]))

                {

                    way = colliders[i].transform;

                    wayDir = Quaternion.LookRotation(way.forward);

                    break;

                }

            }

        }

        //获取与脚下道路x轴上偏差值,好让自身走在路中间

        float offset = 0;

        if (way != null)

        {

            Vector3 distance = transform.position - way.position;

            offset = Vector3.Dot(distance, way.right.normalized);

        }

        //面向该路指向的方向前进

        transform.rotation = Quaternion.RotateTowards(transform.rotation, wayDir, speed * 20 * Time.deltaTime);

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

    }

 

暂时把初始化方法放在Start中调用(后面我们会在创建的时候初始化),然后设置好血量、视野、速度、伤害,主城也设置好血量:

先来看下寻路运行效果:


寻路没有问题了,将攻击动画设为循环播放,然后将攻击方法放入攻击动画事件中,敌人看到主城就会自动攻击了:

敌人主要功能就已经完成。现在我们来做敌人生成器。

塔防游戏的敌人生成方式一般都是比较有规律的,比如先生成一组a敌人,跟着生成一组b敌人,每组敌人的生成间隔也恒定(当然,读者也可以自己尝试更丰富的出兵方法,比如让“某些特定敌人的血量减到某个阈值”作为触发条件等等):

为了生成方便,我们来做一个定时器,可以重复并规律地调用一个生成敌人方法:

public class Util : MonoBehaviour

{

    private static Util _Instance = null;

    public static Util Instance //单例模式,依附GameObject

    {

        get

        {

            if (_Instance == null)

            {

                GameObject obj = new GameObject("Util");

                _Instance = obj.AddComponent<Util>();

            }

            return _Instance;

        }

    }

    public class TimeTask //定时事件类

    {

        public Action callback; //回调函数

        public float delayTime; //延迟长度

        public float destTime; //延迟后的目标时间

        public int count; //重复次数

    }               

    List<TimeTask> timeTaskList = new List<TimeTask>(); //保存所有的定时事件  

    //增加定时回调的方法

    public void AddTimeTask(Action _callback, float _delayTime, int _count = 1)    

    {

        timeTaskList.Add(new TimeTask()

        {

            callback = _callback,

            delayTime = _delayTime,

            destTime = Time.realtimeSinceStartup + _delayTime,

            count = _count

        });

    }

    private void Update()

    {

        for (int i = 0; i < timeTaskList.Count; i++) //实时监测所有定时事件

        {

            TimeTask task = timeTaskList[i];

            if (Time.realtimeSinceStartup >= task.destTime) //时间到了,则执行

            {

                task.callback?.Invoke();

                if (task.count == 1) //当次数为1,执行完移除该定时事件

                    timeTaskList.RemoveAt(i);

                else if (task.count > 1) //当次数大于1,执行完次数减1

                    task.count--;

                task.destTime += task.delayTime; //执行完一次后,重新定出下次执行时间

            }

        }

    }

}

 


把所有敌人放入一个路径中:

创建一个空物体做敌人生成器,放在敌人生成点,创建脚本挂上去:

public class EnemySystem : MonoBehaviour

{

    //根据名称保存所有敌人

Dictionary<string, Enemy> enemyDict = new Dictionary<string, Enemy>();

//初始化,放在主调脚本GameMain中执行

    public void Init()

    {

        //保存所有种类敌人,可以根据名字获取

        Enemy[] enemys = Resources.LoadAll<Enemy>("Prefab/Chara/EnemyChara");

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

        {

            if (!enemyDict.ContainsKey(enemys[i].name))

                enemyDict.Add(enemys[i].name, enemys[i]);

        }

    }

    //生成敌人,参数中设置敌人种类,生成间隔,生成数量(默认为1)

    public void CreateEnemy(string name, float delay, int count = 1)

    {

        if (GameMain.instance.gameOver == false)

            //使用定时器,生成敌人

            Util.Instance.AddTimeTask(() => Instantiate(

            enemyDict[name], transform.position, transform.rotation).Init(),

            delay, count);

}

    //点击按钮生成敌人(挂在按钮事件中)

    public void ClickButtonDispatchTroops()

    {

        //每秒生成一个敌人,生成5次,第一次生成在1秒后执行

        CreateEnemy("Zombie1", 1, 5);

        //没0.5秒生成一个敌人,生成10次,第一次生成在5.5秒后执行

        Util.Instance.AddTimeTask(() => CreateEnemy("Zombie2", 0.5f, 10), 5);

    }

}

 

做到这一步就可以像演示视频中那样点击按钮出兵了。

放上工程链接:https://pan.baidu.com/share/init?surl=T2nZ_FrIk9DaTvem-YH8nQ

提取码:n61s


下一篇文章我们将做UI界面点击头像在场景中生成防御塔,以及不同的防御塔与敌人的交互。


想系统学习游戏开发的童鞋,欢迎访问:http://levelpp.com/    

另有专业开发交(gao)流(ji)群等待大家强势插入:869551769

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

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