记录下关于打斗类(如:ARPG类,并非格斗类)游戏的攻击判定问题,通常这类游戏都会有普通攻击和技能攻击等攻击方式(可参考王者荣耀),下面将分别介绍下。


一、普通攻击

    普通攻击的流程就是主角靠近敌人,播放攻击动画,调用敌人受伤害计算方法(+被击动画、特效等);这一过程有几点需要注意的,

    (1)调用受伤害计算方法时机:因为播放攻击动画会有1s左右的时间,可以在播放动画同时启动一个协程来帮助调用受伤害计算方法;

    (2)攻击成功触发条件:通过点击攻击按钮可以触发攻击技能,但不代表能打出攻击伤害,主角和敌人必须满足一些条件才行;通常我们的做法是判断距离和方向。通俗来说,比如主角要攻击前面的敌人,从这就可以提取两点信息了,就是两者需满足一定的距离和角度了。具体方法如下:
 

// 方式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类中的任意公开数据(变量、方法等);但这种方式也有个小问题,举个例子:故事背景是军队打仗,双方各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.y<0 目标点在主角左方,主角逆时针转身;t.y=0,两者平行(夹角0或180)。PS:为方便记忆,可使用左手定则,如果y>0,左手拇指朝上,四指为转身方向,否则相反。


    当然也可以使用下面方法自带区分顺、逆方向旋转
 

Quaternion rotation = Quaternion.LookRotation(TargetPoint - mGameObject.transform.position);
mGameObject.transform.rotation = Quaternion.Slerp(mGameObject.transform.rotation, rotation, Time.deltaTime * 10f);