☀️ 爆肝整整一个周末写一款类似 皇室战争 的 即时战斗类 游戏Demo!两万多字游戏制作过程+解析!建议收藏学习

Posted 呆呆敲代码的小Y

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了☀️ 爆肝整整一个周末写一款类似 皇室战争 的 即时战斗类 游戏Demo!两万多字游戏制作过程+解析!建议收藏学习相关的知识,希望对你有一定的参考价值。

  • 📢博客主页:https://blog.csdn.net/zhangay1998
  • 📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!
  • 📢本文由 呆呆敲代码的小Y 原创,首发于 CSDN🙉
  • 📢未来很长,值得我们全力奔赴更美好的生活✨


📢前言

  • ❄️苍茫大地一剑尽挽破,何处繁华笙歌落。斜倚云端千壶掩寂寞,纵使他人空笑我。
  • 🐵回头望去云卷云舒,又是一个令人惬意的周末~
  • 👻正准备卷起袖角去王者峡谷酣畅淋漓的战斗一番!
  • 🔔叮咚~ 原来是 憨憨的小云儿 发来了消息~
  • 😐只好停下了去峡谷征战的脚步,熟练地点开微信聊天框。
  • 小云儿👩:小Y哥哥,你这些天在干嘛呢~,怎么也不见你制作游戏了呀!
  • 小Y(博主):害,工作太忙了呗,忙着赚钱买皮肤、充游戏呢~
  • 小云儿👩:(呆滞)…小YY你变了,说好带我学做更多的游戏呢!!!
  • 小Y:Emma…既然你学习的心情那么迫切,那我就满足你的要求!
  • 小云儿👩:好呀好呀~小Y哥哥,我最近又对皇室战争这款游戏玩法非常感兴趣,你能不能…
  • 小Y:皇室战争呀~ 好说,我之前也很喜欢玩!那我就满足你,来制作一款复刻皇室战争玩法的游戏!
  • 小云儿👩:小Y哥哥 你真好~ 那我就期待你做完,再好好学习一波了!
  • 小Y:没得问题,你瞧好了,我这就开始动手做!
  • 小云儿👩:好咧小Y哥哥,老规矩!动手可以,不阔以动脚哦!

在这里插入图片描述


✨正文

  • 由于最近工作挺忙的,已经很长时间没有写过制作游戏的文章了

  • 趁这个周末有时间,那就现在开始动手复刻一款前几年很火爆【皇室战争】玩法的游戏Demo

在这里插入图片描述


💫制作思路(游戏策划)

  • 既然要开始开发一个项目,按照老规矩,那就是先来理一下思路

  • 既然我们是照着皇室战争的玩法来做一个类似的,那自然要去熟悉一下皇室战争啦

  • 先来想一下皇室战争的游戏玩法,皇室战争的核心玩法就是战斗模块了

  • 两个人有自己的卡牌组,有圣水限制,满足圣水的数量才能召唤出相应的卡牌人物去场景中战斗!

  • 在圣水足够的时候,可以将对应的卡牌拖到场景中,只可以拖动到自己的地盘~

  • 这里要注意的是要在没张卡牌身上赋予一个怪物模型的信息,并在需要的时候进行调用!

  • 然后就是卡牌对应的游戏对象在中间对拼战斗,直到把对方的国王塔给干爆就算完成胜利了!

  • 这样说起来好像挺简单的哈,但是还有抽卡、存卡、联网和数据库等操作…

  • 有些功能其实无关紧要,对我们简单复刻一款游戏并没有很大的用处

  • 所以肯定会阉割一部分功能,只把核心玩法开发出来就好了,毕竟完整开发一个皇室战争这样的游戏还是很麻烦的!

  • 那我们搞一个单机模式的游戏,奔着战斗模块的核心玩法!

  • 先来看一下游戏步骤思维导图,然后就开始动手操作吧!
    在这里插入图片描述


🎉开始制作

  • 首先打开Unity新建一个项目,博主开发这个使用的Unity2018.4.24

  • 如果有小伙伴下载本文中的游戏资源,酌情使用Unity版本哦~

  • 每个细节都写出来肯定是不现实哒,由于制作过程是在是又臭又长

  • 所以文章中只把关键性操作和配置介绍出来,一起加油~


🏳️‍🌈第一步:寻找合适的模型+动画配置

  • 制作之前在网上查了一下皇室战争是用H5开发的,那我们就没办法了,没有合适的资源包可以直接利用

  • 那就正好我们自己适配 模型+动画 吧。这可是一个苦力活呀~

  • 那我们就利用手头上的资源来自己制作合适的模型吧

  • 我从自己的资源中整理出来一些个模型,大家来看一下吧~ 鱼龙混杂的模型!
    在这里插入图片描述

  • 从左到右,我分别称他们为:米娅公主、火焰武士、暗黑巫师、骷髅兵、一拳超人、弓箭手、绿巨人、美国队长和钢铁侠!

  • 哈哈,这名字都是我起的,尤其是最后的"漫威三巨头",要是被斯坦李老爷子看到估计会气的揍我~
    在这里插入图片描述

  • 模型有了,动画就要自己配置了

  • 每一个模型都要写一个动画控制器,用于控制他们的动画,比如行走攻击死亡动画

  • 我们这里就需要简单设置一下就好了~没必要整一些不必要的麻烦了

模型动画配置

这里对动画系统不熟悉的可以去看我之前介绍动画系统的一篇文章,介绍的挺详细!
Unity零基础到进阶 ☀️| 近万字教程 对 Unity 中的 动画系统基础 全面解析+实战演练

下面我们来进行一下动画配置,我们拿 “漫威三巨头” 的动画配置举例
先看钢铁侠的动画配置

  • 先在工程界面右键 Create—>Animator Controller,创建动画控制器
    在这里插入图片描述
  • 再给钢铁侠添加上Animator动画组件,并把刚才创建的动画控制器添加到Animator动画组件上
    在这里插入图片描述

下面看一下我自己给制作的动画吧

🎈钢铁侠攻击动画和死亡动画

🎈绿巨人攻击动画和死亡动画

这动画效果~ 个人制作,纯属娱乐哈哈哈隔~ 大家将就看就行😂

然后点开我们创建的动画控制器Animator Controller进行配置

  • 将创建的动画拖上去,然后如下图所示配置

  • 创建三个动画参数,分别是Trigger类型的移动、Bool类型的死亡、Trigger类型的攻击

  • 因为移动和攻击都是持续性的,所以使用Bool值控制比较好

  • 而死亡只会执行一次,使用Trriger类型参数正合适!
    在这里插入图片描述

  • 这三个参数我们点击动画控制器中的箭头进行配置,分别是:

  • 移动指向攻击时添加上对应的条件:Move变为false,Attacking变为true
    在这里插入图片描述
    攻击指向移动时添加上对应的条件:Move变为true,Attacking变为false
    在这里插入图片描述
    指向死亡时,执行Death
    在这里插入图片描述

  • 拿绿巨人的动画控制器配置举例了,其他的模型动画都是一样的参数配置

  • 这里就不挨个介绍了,直接配置好就好啦!

  • 最后把所有模型设置成预制体,用来备用生成时候使用!
    在这里插入图片描述


🏳️‍🌈第二步:战斗场景配置

想要游戏可以顺利的运行,那肯定是需要一个游戏场景

  • 由于皇室战争是一个用2d画面实现3d效果的制作方式

  • 那我自己肯定实现不了这么高级的操作呀,这其中的灯光配置相机处理还不得把我头皮搞发麻呀

  • 所以这里就简单搭建一个游戏场景,让我们可以运行游戏就好啦!

  • 新建两个游戏对象,分别是敌人的场景区域和我们的场景区域

  • 因为我们在战斗的时候,只能把卡牌拖到自己的区域

  • 后边还要设置不同的场景层级来做区别,所以要使用两个游戏对象

  • 然后去网上搜几张皇室战争的场景图片,来添加到材质上,然后拖到场景上面就好了!

  • 有条件的还可以加一些点缀,这里就不添加了,时间有点不够用了…
    在这里插入图片描述

  • 给我们的playerScene场景区域添加上层级layout为:Ground
    在这里插入图片描述

  • 一起来看看的搭建的场景吧,还加上了前面分享过的特效资源包,正好用上!
    在这里插入图片描述

  • 说实话场景确实很lou,但是搭建成这样已经尽力了,对相机视角的把控还是差很多😨

  • 这也不得不让我更加佩服皇室战争的制作了😊

  • 能以一个2d UI制作出这么好的3d效果,对视角和画面渲染的把控确实很强🤪!


🏳️‍🌈第三步:自动寻路设计

  • 在菜单栏点击windows->AI->Navigation

  • 不同的版本可能所在的位置有些出入,但是在Windows菜单下应该都能找到!
    在这里插入图片描述
    然后会跳转到这个页面,这是设置Unity的寻路系统Navigation的面板
    在这里插入图片描述

  • 我们先暂时不去仔细深究这些个Navigation面板属性值到底是干嘛的

  • 其实就是设置寻路的时候可以走的路径配置,有些时候路面高低不平,很复杂的时候会用到

  • 那我们这里就只是一个平面,所以就不用过多设置,直接默认属性值就好!

  • 然后直接选中Bake界面,点击Bake就是自动设置可执行的寻路环境配置!

在这里插入图片描述

  • 当我们点击Bake了之后,可能发现场景中并没有什么变化 ~

  • 正常设置完之后,选中Navigation面板后,场景中会出现一个蓝色的一层可以寻路的图层!

  • 那我们这里可能就是没有设置静态场景 ~

  • 我们选中场景中的场景中如下所示的游戏对象,然后点击右边的Static将选中的物体设置成静态对象

  • 这样点击Bake的时候才能正确的设置导航路线

在这里插入图片描述

  • 设置完静态对象之后,再来点击Bake!就成了以下的模样,就说明导航可行路线设置成功啦:

  • 蓝色区域代表我们寻路可以行走的路线,正好包含了场景中的两个桥!
    在这里插入图片描述

  • 那接下来就给我们所有的模型添加上这个Nav Mesh Agent组件

  • 这样的话就可以使用代码调用nav.SetDestination(target.position),确认寻路目标位置并启用自动寻路

  • 然后使用nav.isStopped = true 就可以暂时停止自动寻路~
    在这里插入图片描述

  • 这样的话自动寻路就设置好了,在什么时候需要启动自动寻路的时候

  • 在代码中设置启动就好了!


🏳️‍🌈第四步:怪物逻辑脚本编写(关键+重要)

  • 模型加动画还有场景都很随意的简单搭配好了,我们只是快速开发一个游戏玩法的游戏Demo
  • 并不是完整的开发一个游戏,实在是时间不允许呀
  • 所以我们就轻装上阵,下面开始进行怪物逻脚本辑编写
  • 由于代码部分内容有很多,所以文章中只介绍关键代码
  • 毕竟大多数人看一下开发过程就好啦,需要完整代码工程的在下面下载源码工程就好啦~

先来看一下写的怪物基本属性
在这里插入图片描述

  • 有一个唯一编号,这个是后面用来生成不同怪物时候用来做标志的
  • 还有怪物名称、血量、生成需要的费用、移动速度、攻击距离、攻击速度、攻击力大小、怪物的卡牌图片、怪物描述、怪物放到场景的生成音效、攻击音效、死亡音效。。。
  • 这样就差不多怪物的属性写完了,下面根据这些属性来编写方法实现!

其中怪物的类型分为两种,分别是近程远程攻击类型,这里用一个枚举表示

/// <summary>
/// 怪物的类型,近战还是投掷
/// </summary>
public enum MonsterType
{
    Infighting,
    Throw
}
  • 其中,怪物自身还有几种状态分别是:寻路状态,攻击状态、静止状态和死亡状态
  • 定义这几种状态是用来进行动画的播放逻辑处理
/// <summary>
/// 怪物的状态
/// </summary>
public enum MonsterState
{
    Finding,
    Attacking,
    Stopping,
    Death
}

还会用到以下属性,怪物状态、攻击范围,导航目标等等,后面都会用到!

    public bool isHome;

    //怪物的当前状态
    public MonsterState monsterCrtstate;

    //怪物的上一个状态
    private MonsterState monsterLastState;

    [HideInInspector]
    public NavMeshAgent nav;

    private Animator ani;

    //攻击范围检测器
    private SphereCollider attackCol;

    //导航目标
    public Transform target;

    [HideInInspector]
    //怪物攻击队列
    public List<Monster> monsterAttackList;

    [HideInInspector]
    //怪物被攻击队列
    public List<Monster> monsterBeAttackList;

    //是否被放到了场景中
    private bool isEndDrag = false;

    //获取当前技能
    [HideInInspector]
    public Skills crtSkill;

    //血条
    private Slider haemalStrand;

    private Audiosource aud;

Awake中找到寻路、动画、声音和一个控制攻击范围的SphereCollider触发器

    private void Awake()
    {     
            nav = GetComponent<NavMeshAgent>();
            ani = GetComponent<Animator>();
            aud = GetComponent<AudioSource>();
            attackCol = GetComponent<SphereCollider>();

        attackCol.isTrigger = true;
        MonsterInit();
    }

还有一个怪物初始化处理MonsterInit方法,将怪物的各种初始属性设置好:

    /// <summary>
    /// 怪物初始化初始化
    /// </summary>
    private void MonsterInit()
    {
        crtSkill = GetComponent<Skills>();

        if (crtSkill == null)
        {
            Debug.Log("Null");
        }
        if(nav != null)
        {
            nav.stoppingDistance = monsterAttackRange;
        }
        monsterCrtstate = MonsterState.Stopping;
        monsterLastState = MonsterState.Stopping;
        attackCol.radius = monsterAttackRange;
        monsterAttackList = new List<Monster>();
        monsterBeAttackList = new List<Monster>();
        monsterCrtHP = monsterHP;
        haemalStrand = transform.Find("Canvas").GetChild(0).GetComponent<Slider>();
        //根据怪物所属阵营选择导航目标

    }
  • 然后在Update中进行判断,当怪物被放到场景中后

  • 如果当前状态和最后的状态不一致,就进行状态判断的方法

  • 如果攻击范围内有敌人就将当前的状态切换到攻击状态,不然就切换成寻路状态

  • 然后下面的 MonsterStateSwitch()方法就是进行不同状态切换的具体方法,下面会讲到。

  • 至于怎样寻路,上一步中已经讲到啦~

    void Update()
    {

        if(!isEndDrag)
            return;

        //持续性技能触发
        if(crtSkill.skilltype == SkillType.Update)
        {
            crtSkill.use();
        }

        //攻击列表中有对象就进入攻击状态
        if(monsterAttackList.Count > 0)
        {
            monsterCrtstate = MonsterState.Attacking;

            if(monsterAttackList[0] != null)
            {
                //转向目标
                MonsterLookAtTarget(monsterAttackList[0].transform);
            }
        }
        else if(monsterAttackList.Count == 0)
        {
            monsterCrtstate = MonsterState.Finding;
        }

        //进行状态判断
        if(monsterCrtstate != monsterLastState)
        {
            MonsterStateSwitch();
            monsterLastState = monsterCrtstate;
        }
        haemalStrand.transform.parent.LookAt(-Camera.main.transform.position);

    }
  • 然后对怪物身上的触发器进行代码编写

  • 触发器范围内检测到敌方怪物或者敌方水晶,就加入到攻击列表中进行攻击

  • 当这个游戏对象从触发器范围消失(被消灭)后,就移除该游戏对象

    private void OnTriggerEnter(Collider other)
    {
        if (other.isTrigger)
            return;

        if(other.CompareTag(GameConst.GAME_TAG_MONSTER) || other.CompareTag(GameConst.GAME_TAG_NAV_END) || other.CompareTag(GameConst.GAME_TAG_NAV_START))
        {
            Monster otherMonster = other.GetComponent<Monster>();

            if (otherMonster == null)
                return;

            if (otherMonster.monsterOwer != this.monsterOwer)
            {
                //设为攻击目标
                monsterAttackList.Add(otherMonster);

                otherMonster.monsterBeAttackList.Add(this);
            }
        }    
    }

    private void OnTriggerExit(Collider other)
    {
        if (other.isTrigger)
            return;

        if (other.CompareTag(GameConst.GAME_TAG_MONSTER) || other.CompareTag(GameConst.GAME_TAG_NAV_END) || other.CompareTag(GameConst.GAME_TAG_NAV_START))
        {
            Monster otherMonster = other.GetComponent<Monster>();

            if (otherMonster != null && otherMonster.monsterOwer != this.monsterOwer)
            {
                //移除攻击队列中的该对象
                monsterAttackList.Remove(otherMonster);
                otherMonster.monsterBeAttackList.Remove(this);
            }
        }
    }
  • 还定义了一个方法,是在拖拽卡牌到场景中后调用的
  • 拖拽到场景中后,对声音和技能进行加载和释放
  • 还对自身的标签tag进行判断,如果是我方的怪物,就把寻路目标改为敌方水晶
  • 如果是敌方的怪物,就将寻路目标改为我方水晶进行寻路
    /// <summary>
    /// 结束拖拽,意思是怪物进入场景作战
    /// </summary>
    public void IsEndDrag()
    {
        this.gameObject.tag = "Monster";

        if(goClip == null)
        {
            goClip = Resources.Load<AudioClip>(GameConst.AUDIO_GO);
        }
        aud.clip = goClip;

        aud.Play();

        //AudioSource.PlayClipAtPoint(goClip, transform.position + Vector3.up * 48 - Vector3.right *40, 10f);

        isEndDrag = true;

        //战吼类技能触发
        if (crtSkill.skilltype == SkillType.Start && crtSkill != null)
        {
            crtSkill.use();
        }
        if (monsterOwer == MonsterOwer.Player)
        {
            target = GameObject.FindGameObjectWithTag(GameConst.GAME_TAG_NAV_END).transform;
        }
        else
        {
            if (GameObject.FindGameObjectWithTag(GameConst.GAME_TAG_NAV_START).transform)
            {
                target = GameObject.FindGameObjectWithTag(GameConst.GAME_TAG_NAV_START).transform;
            }
        }      
    }
  • 还有一个是动画帧中会调用的方法
  • 如果是近战,就执行近战攻击,是远程就进行远程攻击
    /// <summary>
    /// 动画帧事件中调用该方法
    /// </summary>
    public void MonsterAttackOne()
    {
        if (attackingClip == null)
        {
            attackingClip = Resources.Load<AudioClip>(GameConst.AUDIO_ATTACK);
        }
        aud.clip = attackingClip;
        aud.Play();
        //AudioSource.PlayClipAtPoint(attackingClip, transform.position + Vector3.up * 48 - Vector3.right * 40 * 48,10f);      

        if (monsterType == MonsterType.Infighting)
        {
            //近战
            Inflict();
            //通过动画帧事件调用受伤方法
        }
        else
        {
            if (Missile == null)
                return;
           
            if(monsterAttackList.Count >0 && monsterAttackList[0] != null)
            {
                //远程射击,生成 Missile

                GameObject btn = Instantiate(Missile,transform.position +  transform.up, Quaternion.LookRotation(transform.forward));
                btn.GetComponent<Bullet>().target = monsterAttackList[0].transform;
                btn.GetComponent<Bullet>().attackHarm = monsterAttackPower;
            }
        }
    }
  • 最后就是具体 控制怪物动画状态切换 并执行 相应状态操作 的方法。

  • 这个方法在Update中当前状态和最后状态不同的时候就会被调用!

  • 方法中对怪物不同的状态做出不同的相应来实现我们的需求~

  #region 怪物状态
    /// <summary>
    /// 不同的转态切换
    /// </summary>
    private void MonsterStateSwitch()
    {
        switch (monsterCrtstate)
        {
            case MonsterState.Attacking:
                MonsterStateAttacking();
                break;
            case MonsterState.Finding:
                MonsterStateFinding();
                break;
            case MonsterState.Stopping:
                MonsterStateStopping();
                break;
            case MonsterState.Death:
                MonsterStateDeath();
                break;
        }
    }
    /// <summary>
    /// 怪物在寻路状态
    /// </summary>
    private void MonsterStateFinding()
    {
        nav.SetDestination(target.position);
        ani.SetBool(GameConst.MONSTER_ANIPARAM_MOVE, true);
        ani.SetBool(GameConst.MONSTER_ANIPARAM_ATTACK, false);
        nav.isStopped = false;
        FindWay();
    }
    //设置导航目标
    public void FindWay()
    {
        nav.SetDestination(target.position);
    }
    /// <summary>
    /// 怪物在攻击状态
    /// </summary>
    private void MonsterStateAttacking()
    {
        ani.SetBool(GameConst.MONSTER_ANIPARAM_ATTACK, true);
        ani.SetBool(GameConst.MONSTER_ANIPARAM_MOVE, false);
        nav.isStopped = true;
    }
    /// <summary>
    /// 怪物被控制
    /// </summary>
    private void MonsterStateStopping()
    {
        nav.isStopped = true;
    }
    /// <summary>
    /// 怪物死亡
    /// </summary>
    private void MonsterStateDeath()
    {
        if (deathClip == null)
        {
            deathClip = Resources.Load<AudioClip>(GameConst.AUDIO_DEATH);
        }以上是关于☀️ 爆肝整整一个周末写一款类似 皇室战争 的 即时战斗类 游戏Demo!两万多字游戏制作过程+解析!建议收藏学习的主要内容,如果未能解决你的问题,请参考以下文章

Clash Royale皇室战争新手需要知道的一些小知识

clash royale怎么换中文 部落冲突皇室战争中文设置

皇室战争在对战时对话框里的三句英文各怎么翻译?

《皇室战争》克隆法术卡组分享

皇室战争 路线行走

安卓模拟器的使用--皇室战争免费快速成长之路