自主3DRPG游戏开发--关于自动寻路与怪物攻击前后摇的实现
概要
本篇文章主要介绍的有:
1.在RTS,RPG类游戏中如何划分地图:格子(Grid)、路点(Waypoint)、导航网格(Navmesh)
2.各类寻路算法的优劣:A星,IDA星,DIJKSTRA等
3.业界如何实现AI攻击前后摇与攻击效果和攻击动画的匹配
4.在我的项目中我是如何实现攻击前后摇并且使它与攻击动画匹配的
参考资料:
如何划分地图:寻路建模的三种方式比较
A星,IDA星寻路算法
NavMesh插件教程
攻击前后摇
1.自动寻路
1.1 如何划分地图
划分地图共有3种方法:格子(Grid)、路点(Waypoint)、导航网格(Navmesh)
三种方法各有优劣 总体来说:
- 实现复杂度:导航网格 > 格子 > 路点
- 内存和计算开销3:格子 > 导航网格 > 路点
- 表达精确性:导航网格 > 格子 > 路点
格子与路点实现起来都比较简单.
格子开销大,路点开销小.
格子实际走起路来非常不平滑,而路点既需要人工参与,同时其局限性比较大.
而对比起来,虽然NavMesh导航网格实现起来比较复杂,但是由于其优秀的内存开销能力以及路径的精确性、灵活性和平滑性,再加上如今已经有现成的NavMesh可以用,业界普遍使用NavMesh导航网格来划分地图.
1.2 各类寻路算法优劣
这里着重讲一下A星和IDA星:
- 这两个算法的效率都与估价函数的准确度挂钩.
- A星是是一种静态路网中求解最短路径最有效的直接搜索方法,也是解决许多搜索问题的有效算法。算法中的距离估算值与实际值越接近,最终搜索速度越快。
- A星是基于BFS的启发式搜索,而IDA星是基于迭代加深搜索的启发式搜索.
- 当A星的空间需求非常大时,则考虑用IDA星,因为IDA星基于的迭代加深搜索本质是DFS搜索,所以对空间的需求不大,但对时间方面的需求较大
2.攻击前后摇的实现
2.1业界如何实现攻击前后摇
1.为目标位置添加碰撞体(格斗类,动作类游戏)
- 之前风靡全国的拳皇系列,它的人物攻击判定就是使用的矩形碰撞盒。而且就Unity而言,我想到的是根据动画,每帧更新碰撞框的位置。
- 现在的怪物猎人,他会根据你攻击巨型怪兽的不同部位来计算伤害和特效。
2.利用动画帧事件来进行伤害判定和特效生成(MOBA游戏中的攻击无弹道类英雄)
这种方式适用于对击打判定精度要求不太高的游戏。
- 流行的MOBA游戏,英魂之刃,混沌与秩序之英雄战歌,虚荣
- LOL,DOTA2.
这些游戏都有一个特点,那就是近战英雄的攻击,只会判定打没打到,而不是打到了哪个部位。(远程英雄是发射子弹的,这里不谈),如果还应用第一种方式来判定伤害的话,无疑会造成性能上的浪费,因为我们没必要判定打到了哪里。
所以我们不需要再多余的给人物某个部位添加碰撞体,这将毫无意义。
我们需要做的只是将计算伤害的时间点把握好,添加帧事件,当这个动画播放到这里的时候,就执行这个事件。
这也同时解决了攻击前摇,后摇,硬直等一系列的问题。只要自己在代码层面上安排好,这些都不是问题。
当然,像英雄联盟中,那些有弹道的英雄,如大多数ADC,都不是采用这样的方法来实现,他们应该也是通过碰撞体来实现的.
2.2自己如何实现攻击前后摇
2.2.1思路
- 这里,我用到了两个函数:AnimatorStateInfo的.IsName()和.normalizedtime().
- 第一个是检查当前播放的动画是不是怪物攻击的动画,如果是,才能说明怪物在攻击,实现了怪物不在攻击时必定不会触发这个脚本的功能.
- 第二个是检查当前播放的动画播放到什么时间了,有了这个我就可以判断在当前时间怪物的攻击是否落下,如果落下,我就在这个时间段的一小段范围内,触发这个脚本,并且设置人物的无敌时间为这一小段时间,大概是0.1s,很短,所以实际玩起来是基本感觉不到的.
- 这样的优点是怪物的攻击落下时间非常严格,也非常直接,不会有延迟的情况.而且人物也不会多次受击,各方面都比较完美.
2.2.2代码
using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif namespace Gamekit3D { public class MyApplyDamager : MonoBehaviour { public int amount; public LayerMask damagedLayers; public Collider MyCollider; public float BeforetheAttackRoll = 0.7f; //攻击前摇时间 private void OnTriggerStay(Collider other) //碰撞体判断是否与其他碰撞体发生碰撞 { if ((damagedLayers.value & 1 << other.gameObject.layer) == 0) return; Animator MyAni = GetComponentInParent<Animator>();// 得到AI当前的动画组件 AnimatorStateInfo stateinfo = MyAni.GetCurrentAnimatorStateInfo(0); //得到AI当前的动画信息 bool OK = stateinfo.IsName("ChomperAttack"); //判断当前动画是否为AI攻击动画 float time=stateinfo.normalizedTime; //表示当前动画播放进度 //如果该动画是攻击动画且动画进度在攻击落下瞬间左右 if (OK && stateinfo.normalizedTime>=BeforetheAttackRoll-0.05f && stateinfo.normalizedTime<=BeforetheAttackRoll+0.05f) { MyCollider = other; applyDamage();//对另一碰撞体发送受击信息 } } public void applyDamage() { Damageable d = MyCollider.GetComponentInChildren<Damageable>(); if (d != null && !d.isInvulnerable) { //发送受击信息 Damageable.DamageMessage message = new Damageable.DamageMessage { damageSource = transform.position, damager = this, amount = amount, direction = (MyCollider.transform.position - transform.position).normalized, throwing = false }; d.ApplyDamage(message); } } } public class MyHelpBoxAttribute : PropertyAttribute { } #if UNITY_EDITOR [CustomPropertyDrawer(typeof(HelpBoxAttribute))] public class MyHelpBoxDrawer : PropertyDrawer { public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { return Mathf.Max(EditorGUIUtility.singleLineHeight * 2, EditorStyles.helpBox.CalcHeight(new GUIContent(property.stringValue), Screen.width) + EditorGUIUtility.singleLineHeight); } public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { EditorGUI.HelpBox(position, property.stringValue, MessageType.Info); } } #endif }