☀️ 爆肝整整一个周末写一款类似 皇室战争 的 即时战斗类 游戏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!两万多字游戏制作过程+解析!建议收藏学习的主要内容,如果未能解决你的问题,请参考以下文章