记录下关于打斗类(如:ARPG类unity 近战攻击判定,并非格斗类)游戏的攻击判定问题,通常这类游戏都会有普通攻击和技能攻击等攻击方式(可参考王者荣耀),下面将分别介绍下。
一、普通攻击
普通攻击的流程就是主角靠近敌人,播放攻击动画,调用敌人受伤害计算方法(+被击动画、特效等);这一过程有几点需要注意的,
(1)调用受伤害计算方法时机:因为播放攻击动画会有1s左右的时间,可以在播放动画同时启动一个协程来帮助调用受伤害计算方法;
(2)攻击成功触发条件:通过点击攻击按钮可以触发攻击技能,但不代表能打出攻击伤害,主角和敌人必须满足一些条件才行;通常我们的做法是判断距离和方向。通俗来说3D场景,比如主角要攻击前面的敌人,从这就可以提取两点信息了,就是两者需满足一定的距离和角度了。具体方法如下:
// 方式1:通过主角和场景中的所有敌人比较
private void AtkCondition1(float _range,float _angle)
{
// 搜索所有敌人列表(在动态创建敌人时生成的)
// 列表存储的并非敌人的GameObject而是自定义的Enemy类
// Enemy类的一个变量mGameObject则用来存储实例出来的敌人实例
foreach (var go in GameManager.GetInstance.gMonsterDict)
{
// 敌人的坐标向量减去Player的坐标向量的长度(使用magnitude)
float tempDis1 = (go.Value.mGameObject.transform.position - mGameObject.transform.position).magnitude;
// 敌人向量减去Player向量就能得到Player指向敌人的一个向量
Vector3 v3 = go.Value.mGameObject.transform.position - mGameObject.transform.position;
// 求出Player指向敌人和Player指向正前方两向量的夹角,其实就是Player和敌人的夹角(不分左右)
float angle = Vector3.Angle( v3, mGameObject.transform.forward);
if (tempDis1 < _range && angle < _angle)
{
// 距离和角度条件都满足了
}
}
}
// 方式2:通过主角和射线检测到的敌人比较
private void AtkCondition2(float _range,float _angle)
{
// 球形射线检测周围怪物,不用循环所有怪物类列表,无法获取“Enemy”类
Collider[] colliderArr = Physics.OverlapSphere(mGameObject.transform.position, _range, LayerMask.GetMask("Enemy"));
for (int i = 0; i < colliderArr.Length; i++)
{
Vector3 v3 = colliderArr[i].gameObject.transform.position - mGameObject.transform.position;
float angle = Vector3.Angle(v3, mGameObject.transform.forward);
if (angle < _angle)
{
// 距离和角度条件都满足了
}
}
}
上面两种方式主要针对两种不同方式,第一种方式是因为我的主角、敌人等对象是不挂任何脚本的,所有的模型对象都是动态生成,模型对象只是对应类(Player类和Enemy类等)中的一个变量而已,所以需要循环查找Enemy类列表来获取对应的其中一个类实例,这样就不单能获取GameObject了,而是可以获取Enemy类中的任意公开数据(变量、方法等);但这种方式也有个小问题,举个例子:故事背景是军队打仗unity 近战攻击判定,双方各100人;那么使用方式1就是双方各有100个类实例,而每个类实例都包含这个判断方法,是循环对方类实例列表(大小100),那么双方加起来就是(2*100*100)的计算量了,当然手游的话不应该这么极端同时出现这么多模型的。即使是一个主角加100的情况下,主角类作这样的判断也是浪费的,因为一般主角旁边最多几个敌人的,不应该每次都查找所有敌人啊。
所以第二种方式就是这种情况,只判断身旁的敌人,通过主角发射一定长度的环形射线检测周围敌人(类似球形触发器检测敌人是否进入触发器),直接获取射线检测到的敌人数组列表,再将其和主角作夹角对比地图场景,从而得到判断结果。因为碰撞检测都是直接得到碰撞对象GameObject的,比较适合对象上挂载脚本的方式(获取数据方便),但是对于我那种方式来说,我如果要通过一个GameObject获取其所属的类实例,只能循环查找类实例列表一个个判断了,那么就又变回第一种方式了,所以说这两种方式应该按实际情况去使用。
技术扩展
(1)归一化、点乘、叉乘
1.1 上面我们用了方法Vector3.Angle来求两向量的夹角,其实也是可以用其他方法来计算的。
// 计算目标是否在指定扇形攻击区域内
private bool CalculateDistance()
{
float distance = (mGameObject.transform.position - Target.transform.position).magnitude;
Vector3 mfrd = mGameObject.transform.forward;
Vector3 tV3 = Target.transform.position - mGameObject.transform.position;
// mfrd.normalized(归一化):方向不变,长度归一,用在只关心方向忽略大小情况下(毕竟以单位1计算比使用float类型数计算方便快速嘛)
// Vector3.Dot(点乘):余弦值;Mathf.Acos()反余弦值(弧度形式表现)
// Mathf.Rad2Deg:弧度转度;Mathf.Deg2Rad:度转弧度
float deg = Mathf.Acos(Vector3.Dot(mfrd.normalized, tV3.normalized)) * Mathf.Rad2Deg;
// 一半扇形区域
if (distance < 2f && deg < 120 * 0.5){
return true;
}
return false;
}
1.2 点乘虽然可以判断两坐标点的夹角,但范围是[0,180],也就是说不分左右的,那么在实现移动转身时就无法区分顺时针转身或者逆时针转身了(只能指定一个转身方向)。
// 借用上面变量作叉乘计算
Vector3 t = Vector3.Cross(mfrd.normalized, tV3.normalized); // 叉乘结果为一个Vector3向量
关于叉乘结果t,有三种结果:如果t.y>0 目标点在主角右方,主角顺时针转身;如果t.y0,左手拇指朝上,四指为转身方向,否则相反。
当然也可以使用下面方法自带区分顺、逆方向旋转
Quaternion rotation = Quaternion.LookRotation(TargetPoint - mGameObject.transform.position);
mGameObject.transform.rotation = Quaternion.Slerp(mGameObject.transform.rotation, rotation, Time.deltaTime * 10f);