zl程序教程

您现在的位置是:首页 >  其他

当前栏目

Unity笔记-29-ARPG游戏项目-05-简易的战斗系统

游戏项目笔记系统 Unity 简易 05 29
2023-09-11 14:22:30 时间

Unity笔记-29-ARPG游戏项目-05-简易的战斗系统

功能优化与BUG修复

先对之前的功能做一些优化

攀爬优化-检测约束

在测试中,我发现,当攀爬到顶的动作中,再次检测到墙壁会导致上墙BUG,卡到墙体里,例如以下场景,由于两面墙的墙面距离差距较小,就会导致以上BUG

在这里插入图片描述

因此每次攀登动作完成后的再次检测上墙需要进行约束,不能在攀登动作未完成时就上墙,思路比较简单,只需要在墙体检测最开头加入以下代码即可

if (ani.GetCurrentAnimatorStateInfo(1).shortNameHash == GameConst.CLIMBTOUP_STATE) return;

攀爬BUG修复

之前忘记做攀爬到地面的检测了,导致如果一直向下攀爬会穿越地面的BUG,这里修复一下,再攀爬脚本里增加一个方法,并在Update里调用,放在攀爬到墙顶检测方法的后面;

    /// <summary>
    /// 攀爬到地面检测
    /// </summary>
    public void ClimbDownToLand()
    {
        if (inputDelta.y > 0) return;//如果是向上移动则退出,向下移动才进入判断
        Vector3 origin = roleTransform.position + GameConst.GROUND_CHECK_ORIGION_OFFSET * Vector3.up;
        RaycastHit hit;
        if (Physics.Raycast(origin, -roleTransform.up, out hit, climbDownToLand, environmentLayerMask)) 
        {
            float distance = Vector3.Distance(roleTransform.position,hit.point);
            float angle = Vector3.Angle(hit.normal, upAxis);
            if (angle > 60) return;
            if (distance < 0.2f)
            {
                ExitClimb();
            }
        }
    }

首先判断是否为向下移动,如果是才进行攀爬到墙底检测,通过向人物的下方进行射线检测,并计算检测点和人物的距离以及检测面向量与世界Y轴的角度,如果这个角度大于60度,说明检测到的不是地面而是弯曲的墙壁,不退出攀爬,如果角度小于60度,那么则判断距离是否足够小以接近地面,如果是,则退出攀爬,注意这里的射线检测方向应当是朝着以人物坐标系的负Y轴方向,而非世界坐标的负Y轴方向

移动优化-台阶修复

之前的移动还存在bug,例如遇到台阶需要通过跳跃才能越过,而不能直接越过,因此需要加入台阶修复

    /// <summary>
    /// 台阶跨越修正
    /// </summary>
    private void StepProcess()
    {
        Vector3 origin = roleTranform.position + GameConst.STEP_CHECK_OFFSET * Vector3.up;
        RaycastHit hit;
        if(Physics.Raycast(origin, moveDirection, out hit, stepCheckLength, environmentLayerMask))
        {
            Vector3 upNormal = Quaternion.AngleAxis(90,roleTranform.right)*hit.normal;
            Vector3 raydir = Quaternion.AngleAxis(45,roleTranform.right) * roleTranform.forward;
            RaycastHit stepHit;
            Physics.Raycast(hit.point + upNormal * stepFixedhHeight, raydir, out stepHit, environmentLayerMask);
            if (Vector3.Angle(hit.normal, stepHit.normal) < 5f)//如果台阶太高,两次检测的都是一个墙面那么就直接返回
                return;
            Vector3 targetPos = new Vector3(hit.point.x, stepHit.point.y, hit.point.z);
            roleTranform.position = Vector3.Lerp(roleTranform.position, targetPos, stepFixedSpeed);
        }
    }

从脚底射出一条朝向移动方向的射线,检测台阶,如果检测到台阶,该检测点作为旧检测点,再在检测点往上移动设定的能够跨越的台阶距离作为新的射线起点,在新的起点斜向下再次发出射线,检测台阶,这次检测到的点作为新检测点,如果检测到的新检测点的法线和旧检测点法线夹角几乎相同,说明台阶过高无法跨越;否则,则为可跨越台阶,新建一个坐标点,使用旧检测点的x,z,新检测点的y坐标,即可获得从角色当前位置提高台阶高度的新位置,将角色的位置lerp到这个位置即可

移动功能新增-潜行

当玩家进入潜行状态时,敌人无法通过听觉察觉到玩家,逻辑比较简单,动画部分阅者自行完成,这里只展示添加了潜行逻辑的新增部分代码,但按住C键时,进入潜行。

            if (!ani.GetBool(GameConst.ISWALK_PARAM)&&!ani.GetBool(GameConst.ISSNEAK_PARAM))
            {
                rigidbody.velocity = Vector3.ProjectOnPlane(moveDirection,upAxis) * runSpeed * Time.fixedDeltaTime+Vector3.up*rigidbody.velocity.y;
                ani.SetFloat(GameConst.SPEED_PARAM, 1f);
                IsMove = true;
            }
            else if(!ani.GetBool(GameConst.ISSNEAK_PARAM))
            {
                rigidbody.velocity = Vector3.ProjectOnPlane(moveDirection, upAxis) * walkSpeed * Time.fixedDeltaTime + Vector3.up * rigidbody.velocity.y;
                ani.SetFloat(GameConst.SPEED_PARAM, 1f);
                IsMove = true;
            }
            else
            {
                rigidbody.velocity = Vector3.ProjectOnPlane(moveDirection, upAxis) * sneakSpeed * Time.fixedDeltaTime + Vector3.up * rigidbody.velocity.y;
                ani.SetFloat(GameConst.SPEED_PARAM, 1f);
                IsMove = true;
            }
        if (Input.GetButton(GameConst.SNEAK_BUTTON))
        {
            ani.SetBool(GameConst.ISSNEAK_PARAM, true);
        }
        else
        {
            ani.SetBool(GameConst.ISSNEAK_PARAM, false);
        }

战斗系统

属性与伤害

角色&敌人基础属性

普通属性:基础生命值上限;基础攻击力;基础防御力;基础体力值;

进阶属性:暴击率;暴击伤害;冷却缩减;护盾强效;额外伤害加成;伤害减免;魔法抗性;物理抗性;治疗加成;

韧性值;韧性削减力量;霸体值;

这里主要说明一下韧性与霸体值,角色和敌人都拥有随着时间不断恢复的韧性值,当角色或敌人受到攻击时,韧性值会减少,减少的量遵从以下计算公式:韧性削减值 = 韧性削减力量*(1-霸体值)

霸体值为0-1,所有单位本身的基础霸体值均为0,也就是说在受到攻击的时候会减少相当于100%的韧性削减力量的韧性值,霸体值为1时,受到攻击时韧性不会削减;

当角色韧性值小于0时,此时再受到攻击,则会造成硬直,在硬直期间播放受伤动画,无法攻击,移动,跳跃,攀爬,如果在墙上则会立刻坠落,以其其他任何动作;

武器属性

普通属性:基础攻击力;额外生命值上限加成;额外攻击力加成;额外防御力加成;

进阶属性:暴击率;暴击伤害;护盾强效;额外伤害加成;治疗加成;韧性削减力量;

装备属性

普通属性:固定生命值上限加成;额外生命值上限加成;固定攻击力加成;额外攻击力加成;固定防御力加成;额外防御力加成;固定体力值加成;

进阶属性:暴击率;暴击伤害;冷却缩减;护盾强效;额外伤害加成;伤害减免;魔法抗性;物理抗性;治疗加成;固定韧性值加成;韧性削减力量;霸体值;

角色面板计算公式

生命值 = 角色基础生命值上限*(1+武器额外生命值上限加成+装备额外生命值上限加成)+装备固定生命值上限加成

攻击力 = (角色基础攻击力+武器基础攻击力)*(1+武器额外攻击力加成+装备额外攻击力加成)+装备固定攻击力加成

防御力 = 角色基础防御力*(1+武器额外防御力加成+装备额外防御力加成)+装备固定防御力加成

体力值 = 角色基础体力值+装备固定体力值加成

暴击率 = 角色暴击率+武器暴击率+装备暴击率

暴击伤害 = 角色暴击伤害+武器暴击伤害+装备暴击伤害

冷却缩减 = 角色冷却缩减+装备冷却缩减

护盾强效 = 角色护盾强效+武器护盾强效+装备护盾强效

额外伤害加成 = 角色额外伤害加成+武器额外伤害加成+装备额外伤害加成

伤害减免 = 角色伤害减免+装备伤害减免

魔法抗性 = 角色魔法抗性+装备魔法抗性

物理抗性 = 角色物理抗性+装备物理抗性

治疗加成 = 角色治疗加成+武器治疗加成+装备治疗加成

韧性值 = 角色韧性值 + 装备韧性值

韧性削减力量 = 角色韧性削减力量+武器韧性削减力量+装备韧性削减力量

霸体值 = 角色霸体+装备霸体值

BUFF

普通属性:额外攻击力加成;额外防御力加成;

进阶属性:暴击率;暴击伤害;冷却缩减;护盾强效;伤害加成;伤害减免;魔法抗性;物理抗性;治疗加成;韧性削减力量;霸体值

DEBUFF

普通属性:攻击弱化;防御弱化

进阶属性:护盾弱化;伤害减免弱化;魔法抗性弱化;物理抗性弱化;治疗加成弱化;韧性削减力量弱化;霸体弱化

BUFF&DEBUFF计算公式

角色叠加BUFF&DEBUFF后的对应面板属性——A

A = 角色面板攻击力+(角色基础攻击力+武器基础攻击力)*(BUFF攻击力加成-DEBUFF攻击弱化)

A = 角色面板防御力+(角色基础防御力)*(BUFF防御力加成-DEBUFF防御弱化)

A = 角色面板暴击率+BUFF暴击率

A = 角色面板暴击伤害+BUFF暴击伤害

A = 角色面板冷却缩减+BUFF冷却缩减

A = 角色面板护盾强效+BUFF护盾强效-DEBUFF护盾弱化

A = 角色面板伤害加成+BUFF伤害加成

A = 角色面板伤害减免+BUFF伤害减免-DEBUFF伤害减免弱化

A = 角色面板魔法抗性+BUFF魔法抗性-DEBUFF魔法抗性弱化

A = 角色面板物理抗性+BUFF物理抗性-DEBUFF物理抗性弱化

A = 角色面板治疗加成+BUFF治疗加成-DEBUFF治疗弱化

A = 角色面板韧性削减力量+BUFF韧性削减力量-DEBUFF韧性削减力量弱化

A = 角色面板霸体值+BUFF霸体值-DEBUFF霸体弱化

注意:霸体值经过BUFF与DEBUFF的叠加,仍然不会越过0-1的区间范围,如果叠加数值过大,则固定在1,反之同理;

伤害计算公式

角色造成的实际伤害根据伤害类型(物理伤害,魔法伤害,真实伤害)

以下属性均为已经叠加BUFF后的面板属性,不再赘述

依据一下公式:

物理伤害:造成的伤害=攻击力*暴击伤害(如果没有暴击则没有这部分乘区)*(1+额外伤害加成)*(1-敌人伤害减免)*(1-敌人物理抗性)*(角色防御力/(敌人防御力+角色防御力))*本次招式倍率

魔法伤害,同理

真实伤害,则无视伤害减免,抗性,防御力这三个乘区,注意真实伤害会忽略护盾值直接对本体造成伤害

角色如果此时拥有护盾值,则优先扣除护盾值,当扣除护盾值时,会根据以下公式扣除:

扣除的护盾值=造成的伤害*(1-护盾强效)

注意:如果护盾强效为负值,则造成的伤害会被放大,护盾强效大于100%,那么造成的伤害会被吸收转化为护盾值

因此在强度控制中,要注意护盾强效的值的控制

如果受到伤害造成护盾值归零,则溢出的伤害会被直接抵消;

如果受到伤害护盾值已经为0,则扣除角色当前生命值,注意,这里不是面板生命值,而是另外一个变量存储当前生命值

并且,如果扣除的是护盾值,则不会造成韧性值削减;

韧性削减公式在上文中已经说过,不在赘述;

攻击逻辑

这里需要配合动画帧事件,在对应的动画攻击时机添加Hit事件

通过脚本调用,在动画播放到那一刻时,会调用Hit方法,思路就是检测武器伤害判定范围内所有敌人碰撞体,并对其造成伤害。

    public void Hit(int index)
    {
        ani.speed = 0;
        Collider[] enemies = Physics.OverlapBox(weapon.transform.position,new Vector3(0.21f,0.37f,1.4f),weapon.transform.rotation,enemyLayerMask);
        for(int i = 0; i < enemies.Length; i++)
        {
            float damage;
            Enemy enemy = enemies[i].GetComponent<Enemy>();
            RoleSynthesizedAttribute enemyAttr = enemy.addBuff_Attribute;
            switch (weapon.damageType)
            {
                case DamageType.物理伤害:
                    damage = DamageCalculations(enemyAttr) * (1 - enemyAttr.physicalResistance) * commomHit[index];
                    enemy.Damage(damage, roleAttribute.attackToughness);
                    break;
                case DamageType.魔法伤害:
                    damage = DamageCalculations(enemyAttr) * (1 - enemyAttr.magicResistance) * commomHit[index];
                    enemy.Damage(damage, roleAttribute.attackToughness);
                    break;
                case DamageType.真实伤害:
                    damage = DamageCalculations(enemyAttr, true) * commomHit[index];
                    enemy.Damage(damage, roleAttribute.attackToughness, true);
                    break;
                default:
                    damage = 0;
                    break;
            }
            //Debug.Log(damage);
        }
    }

受击逻辑

角色状态脚本需要有受伤方法Damage,敌人也同理

当角色被调用受伤方法时,首先判断当前动画是否在播放有无敌帧设定的动画,如果有,那么直接返回不造成伤害;

再判断当前角色生命值是否已经小于等于0,如果是,则直接返回,防止角色已经死亡而继续造成伤害;

判断当前护盾值是否大于0并且不为真实伤害,如果是,那么按照之前讲过的逻辑扣除护盾值;

否则,按照之前的讲过的逻辑扣除当前生命值,并扣除韧性值,如果韧性值归零,则播放受击动画;

如果生命值归零则执行死亡方法,代码如下

        public void Damage(float damage, float attackToughness,bool real=false)
        {
            if (ani.GetCurrentAnimatorStateInfo(2).shortNameHash == GameConst.ROLL_STATE) return;//翻滚无敌帧
            if (health <= 0) return;
            if (shieldValue > 0 && !real) 
            {
                shieldValue -= damage * (1 - addBuff_Attribute.shieldPotent);
                if (shieldValue <= 0)
                {
                    shieldValue = 0;
                }
            }
            else
            {
                health -= damage;
                toughness -= attackToughness * Mathf.Clamp((1 - addBuff_Attribute.damValue), 0, 1);
                if (toughness <= 0)
                {
                    ani.CrossFade(GameConst.GETHIT_STATE, 0f);
                }
            }
            if (health <= 0)
            {
                die = StartCoroutine(Death());
            }
        }

特别说明:敌人和玩家的攻击与受击逻辑是一样的,敌人没有翻滚设定,所以在敌人的受击逻辑里把翻滚无敌帧那行去掉即可

敌人AI

需求说明

敌人拥有视觉和听觉,当玩家处于潜行状态时,敌人只能通过视觉发现玩家;一旦某个敌人发现玩家,那么一定范围内的所有敌人都会发现玩家;当敌人发现玩家后,如果没有进入可攻击范围,则会逐步靠近玩家,但不会径直走向玩家,会左右或者后退前进移动试探,当玩家进入可攻击范围时,敌人会攻击,如果攻击落空,那么不施展接下来的连招,如果攻击到了,那么施展接下来的连招;如果玩家走出敌人的攻击状态检测范围,那么敌人会进入追击状态,注意,敌人拥有耐心值,如果追击过程中追击超过一定时间,则不再追击返回原位;

另外说一下听觉,如果敌人通过听觉察觉到玩家,不会立刻发现玩家,而是单人前往目标点查看,如果没有玩家则返回,如果视觉看到玩家,则发现玩家;

如果敌人被打出硬直,则无法进行任何动作,这一点和玩家相同;

如果玩家攀爬墙壁,导致敌人无法攻击到,那么则直接放弃追击回到原位

听觉&视觉

感官脚本有一个变量:findPlayer用于标记是否发现玩家,当敌人通过视觉发现玩家则将其标记为true

还有一个Vector3变量:checkPos,当敌人通过听觉察觉玩家时,会将听到的最后位置赋值给改变量

视听的原理比较简单,这里不做赘述,听觉中如果玩家处于潜行状态则直接范围,这里直接上代码

    public void Sighting()
    {
        Vector3 dir = roleTransform.position - transform.position;
        float angle = Vector3.Angle(dir,transform.forward);
        if (angle > sightAngle / 2) return;
        RaycastHit hit;
        if (Physics.Raycast(transform.position + Vector3.up * GameConst.ENEMY_EYE_OFFSET, dir, out hit, sphereCollider.radius)) 
        {
            if (hit.collider.CompareTag(GameConst.PLAYER))
            {
                findPlayer = true;
            }
        }
    }
    public void Hearing()
    {
        if (roleAni.GetCurrentAnimatorStateInfo(1).shortNameHash == GameConst.SNEAK_STATE) return;
        float distance = Vector3.Distance(roleTransform.position, transform.position);
        if (distance < hearDistance)
        {
            chechPos = roleTransform.position;
        }
    }

拉扯能力

之前说过,当敌人发现玩家,不会径直走向玩家,而是会不断左右前后拉扯距离,当距离小于攻击范围时,则进行攻击

这里使用协程,每隔一段时间就随机改变拉扯方向,并通过attackDesire这个值控制敌人的攻击欲望,如果随机到的值小于攻击欲望,返回true,如果大于攻击欲望,返回false

协程根据返回值,如果为true则想玩家方向移动,否则随机移动,也就是随机改变拉扯方向

并通过变量canStrafe来判断当前是否能够进行拉扯

    public bool ForwardRate()
    {
        if (Random.Range(0,100) < attackDesire)//说明当前想攻击就前进
        {
            return true;
        }
        else//当前想拉扯,不前进,随机
        {
            return false;
        }
    }
    IEnumerator Strafe()
    {
        while (true)
        {
            if (ForwardRate())
            {
                randomDelta = new Vector2(0, 1);
            }
            else
            {
                randomDelta = new Vector2(Random.Range(-1, 1), Random.Range(-1, 1));
            }
            ani.SetFloat(GameConst.HOR_PARAM_ENEMY, randomDelta.x);
            ani.SetFloat(GameConst.VER_PARAM_ENEMY, randomDelta.y);
            yield return new WaitForSeconds(strafeInterval);
        }
    }

移动/追击

通过canMove变量表示当前能否移动/追击,逻辑十分简单,直接上代码,通过根运动控制

    private void Update()
    {
        if (!canMove)
        {
            ani.SetBool(GameConst.ISRUN_PARAM_ENEMY, false);
            return;
        }
        if (nav.isStopped)
        {
            ani.SetBool(GameConst.ISRUN_PARAM_ENEMY,false);
        }
        else
        {
            ani.SetBool(GameConst.ISRUN_PARAM_ENEMY, true);
        }
    }

攻击

通过canAttack变量控制当前能否攻击,通过根运动控制

    public void Update()
    {
        if (!canAttack)
        {
            ani.SetBool(GameConst.ISATTACK_PARAM_ENEMY,canAttack);
            return;
        }
        else
        {
            ani.SetBool(GameConst.ISATTACK_PARAM_ENEMY, canAttack);
        }
    }

AI

首先需要有一个方法检测敌人是否通过听觉察觉到玩家,那么就要检测感官脚本中的checkPos是否发生改变

    private bool HearPos()
    {
        if (preCheckPos != enemySense.chechPos)
        {
            preCheckPos = enemySense.chechPos;
            return true;
        }
        return false;
    }

由于玩家可能会通过攀爬等行为到达敌人无法通过导航到达的位置,因此需要一个变量canReach来表示本次导航能否到达目的地;

在Update中进行判断

首先使用布尔变量接受听觉检测

        hearing = HearPos();

其次判断当前canReach&findPlayerfindPlayer在感官脚本中)

如果是,则进入下一步判断,首先将导航启用,设置目的地为角色位置,并判断能否到达目的地,如果不能则执行放弃方法,如果能,那么计算敌人与玩家的距离,并根据距离做出:拉扯/攻击/追击 的不同应对策略。在进行拉扯与攻击时,需要将导航关闭。

如果没有发现玩家,并且canReachtrue,那么首先判断hearing,如果听觉检测位置变化,则开启导航,并设置导航目的地为玩家位置,否则检查敌人是否已经到达导航位置,如果是,则判断当前位置是否为原位,如果是,则返回;如果不是,则表示当前为通过听觉到达了玩家最后发出声音的位置,通过计时器延迟几秒,在返回原位

如果canReachfalse表示无法到达,则即可返回原位,同样判断hearing

另外,如果敌人处于追击状态,则会不断消耗耐心值,归零则返回原位;回到原位时,耐心值才会回升

总之,逻辑比较复杂,一两句话不能说清楚,需要阅者自己加以思考,这里提供参考代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class EnemyAI : MonoBehaviour
{
    [Header("攻击距离")]
    public float attackDistance;
    [Header("耐心值")]
    public float patienceValue;
    [Header("耐心变化速率")]
    public float patienceSpeed;
    [Header("旋转速度")]
    public float rotateToPlayerSpeed;
    [Header("视察停滞时间")]
    public float waitTime;
    public Vector3 originPos;

    private Vector3 preCheckPos;
    private NavMeshAgent nav;
    private EnemyStrafe enemyStrafe;
    private EnemyAttack enemyAttack;
    private EnemySense enemySense;
    private EnemyMove enemyMove;
    private Transform roleTransform;

    private SphereCollider sphereCollider;
    //private Rigidbody rigidbody;
    public bool canReach;
    private bool hearing;
    public float waitCounter;

    private void Awake()
    {
        sphereCollider = GetComponent<SphereCollider>();
        roleTransform = GameObject.FindGameObjectWithTag(GameConst.PLAYER).transform;
        nav = GetComponent<NavMeshAgent>();
        enemyStrafe = GetComponent<EnemyStrafe>();
        enemyAttack = GetComponent<EnemyAttack>();
        enemySense = GetComponent<EnemySense>();
        enemyMove = GetComponent<EnemyMove>();
        preCheckPos = enemySense.chechPos;
        patienceValue = 100;
        //rigidbody = GetComponent<Rigidbody>();
    }
    private void Start()
    {
        originPos = transform.position;
        canReach = true;
        waitCounter = waitTime;
    }
    /// <summary>
    /// 通过听觉检测玩家位置
    /// </summary>
    /// <returns></returns>
    private bool HearPos()
    {
        if (preCheckPos != enemySense.chechPos)
        {
            preCheckPos = enemySense.chechPos;
            return true;
        }
        return false;
    }
    private void Update()
    {
        hearing = HearPos();
        if (canReach && enemySense.findPlayer)
        {
            nav.isStopped = false;
            nav.SetDestination(roleTransform.position);
            if (nav.pathStatus == NavMeshPathStatus.PathPartial)
            {
                GiveUpChase(false);
                return;
            }
            float distance = Vector3.Distance(transform.position, roleTransform.position);
            if (distance < attackDistance)
            {
                Attack();
            } else if (distance < sphereCollider.radius)
            {
                Strafe();
            }
            else
            {
                Chase();
            }
        }
        else if (!enemySense.findPlayer && canReach) 
        {
            if (hearing)
            {
                CheckPos();
                return;
            }
            if (Mathf.Abs(nav.remainingDistance - nav.stoppingDistance) < 0.1f)
            {
                if (Vector3.Distance(transform.position, originPos) < 0.1f)
                {
                    if(waitCounter<waitTime)
                    waitCounter = waitTime;
                    return;
                }
                enemyMove.canMove = false;
                if (patienceValue < 100)
                {
                    patienceValue += patienceSpeed;
                }
                else
                {
                    patienceValue = 100;
                }
                if (waitCounter <= 0)
                {
                    waitCounter = waitTime;
                    ReturnPos();
                }
                else
                {
                    waitCounter -= Time.deltaTime;
                }
            }
            else
            {
                if (Vector3.Distance(transform.position, originPos) < 0.1f) return;
                enemyMove.canMove = true;
            }
        }
        else if (!canReach)
        {
            ReturnPos();
            if (hearing)
            {
                CheckPos();
            }
            if (Mathf.Abs(nav.remainingDistance - nav.stoppingDistance) > 1f)
            {
                enemyMove.canMove = true;
            }
        }
    }
    /// <summary>
    /// 拉扯
    /// </summary>
    public void Strafe()
    {
        nav.isStopped = true;
        patienceValue = 100;
        enemyMove.canMove = false;
        enemyAttack.canAttack = false;
        enemyStrafe.canStrafe = true;
        RotateToPlayer();
    }
    /// <summary>
    /// 攻击
    /// </summary>
    public void Attack()
    {
        nav.isStopped = true;
        patienceValue = 100;
        enemyMove.canMove = false;
        enemyAttack.canAttack = true;
        enemyStrafe.canStrafe = false;
        RotateToPlayer();
    }
    /// <summary>
    /// 追赶
    /// </summary>
    public void Chase()
    {
        nav.isStopped = false;
        enemyMove.canMove = true;
        enemyAttack.canAttack = false;
        enemyStrafe.canStrafe = false;
        patienceValue -= patienceSpeed * Time.deltaTime;
        if (patienceValue <= 0)
        {
            patienceValue = 0;
            GiveUpChase();
        }
    }
    /// <summary>
    /// 放弃追赶
    /// </summary>
    /// <param name="r"></param>
    public void GiveUpChase(bool r = true)
    {
        if (r)
        {
            enemySense.findPlayer = false;
            ReturnPos();
        }
        else
        {
            enemyAttack.canAttack = false;
            enemyStrafe.canStrafe = false;
            canReach = false;
        }
    }
    /// <summary>
    /// 视察位置
    /// </summary>
    public void CheckPos()
    {
        nav.isStopped = false;
        nav.SetDestination(roleTransform.position);
        if (nav.pathStatus == NavMeshPathStatus.PathComplete)
        {
            enemyMove.canMove = true;
            canReach = true;
        }
    }
    /// <summary>
    /// 返回原位
    /// </summary>
    public void ReturnPos()
    {
        nav.isStopped = false;
        nav.SetDestination(originPos);
        enemySense.findPlayer = false;
        canReach = true;
    }
    /// <summary>
    /// 转向玩家
    /// </summary>
    public void RotateToPlayer()
    {
        Vector3 relative = roleTransform.position - transform.position;
        Vector3 dir = Vector3.ProjectOnPlane(relative,-Physics.gravity.normalized);
        transform.rotation = Quaternion.Lerp(transform.rotation,Quaternion.LookRotation(dir),rotateToPlayerSpeed*Time.deltaTime);
    }
}

当前战斗系统仍然比较简单,后续会加入更多复杂的功能