Unity3D RPG实现 3

目录

数值伤害

守卫状态

死亡状态

泛型单例模式

接口实现观察者模式的订阅和广播

制作更多的敌人 

扩展方法

设置石头人boss

为石头人boss添加丢石头

使用石头进行反击

代码



数值伤害

在人物动画里attack2作为暴击的动画,条件的设置和之前一样。

玩家的冷却时间更新:

在character stats里设置伤害数值

判断攻击的方式使用帧事件的方式实现

判断是否暴击的话在攻击之前就要产生判断 所以在attackevent里面实现

hit函数 真是件调用的函数

hit 

在特定的时间添加关键帧,并设定函数:

此时即可实现攻击时实现暴击的效果。

接下来实现怪物的暴击,

当我们想在slime的动画里添加关键帧时发是只读

将文件夹里的动画使用ctrl+d复制一份,然后在动画机里替换即可。

之后就可以被编辑了,添加关键帧:

接下来即可实现怪物攻击调用的代码。

守卫状态

接下来补充守卫状态的代码:

用sqr也可以计算距离

接下来即可实现让怪物脱战时回到原来的状态,但是出问题在于回去时脸朝外:

记录下初始的旋转四元数

然后这个可以实现缓慢转动然后 

接下来即可实现回去后仍然朝向原来的地方了。

            case EnemyStates.GUARD:

                isChase = false;
                if (transform.position != guardPos)
                {
                    isWalk = true;
                    agent.destination = guardPos;
                    if (Vector3.SqrMagnitude(guardPos - transform.position) < agent.stoppingDistance)
                    {
                        isWalk = false;
                        transform.rotation = Quaternion.Lerp(transform.rotation, guardRotation, 0.01f);
                    }
                }

                break;

死亡状态

接下来添加受击:

添加一个死亡层级,并把权重设置为1,并建立基础空状态

导入动画并设定条件

(死亡动画由anystate进入)

 

在死亡动画勾选中为防止其重复播放动画,取消勾选:

给玩家添加一样的死亡层级:

在代码中添加一些设定

 

 (注意此处加else 否则会再次进入chase状态)

受击的动画触发条件则在此处实现:

 之后即可实现怪物被杀并播放死亡动画,但是此处存在一个问题,怪物被杀到消失之前仍然可以点击攻击,这是因为我们是通过鼠标事件点击是否有collider,因此怪物死亡时需要立马关掉collider即可。

人物的死亡与此同理设定即可。

注意到每次加载时需要重新设置血量。

            case EnemyStates.DEAD:
                collider.enabled = false;
                //agent.enabled = false;
                agent.radius = 0;
                Destroy(gameObject, 2f);
                break;

泛型单例模式

创建一个gamemanager,会变成齿轮的样子

此处public是为了此后所有的数据都通过gamemanager进行访问,这样可以方便集中管理数据。

那我们什么时候进行赋值呢,我们这里用观察者模式,反向注册的方法,让player在生成的时候告诉gamemanager我是playerstats。

写完了这个方法,我们希望在player中进行调用。

如果我们希望在player生成的时候就使用,那我们应该将gamemanager像mousemanager一样使用单例模式。这样可以直接使用,就不需要创建并赋值。

但是,在这个游戏中会有很多的manager,我们都希望它是单例模式然后进行访问,每一个都像mousemanager一样去写的话会很麻烦,因此此处使用泛型的单例模式。

如果将所有的manager都继承自这个泛型单例,就可以省下很多的步骤。

举个例子,此处使用单例,并且在后面加约束,约束是T就是那样的单例。

(此处涉及到的相关 知识是泛型约束的概念,所谓的泛型约束,实际上就是约束的类型T。使T必须遵循一定的规则。比如T必须继承自某个类,或者T必须实现某个接口等等。那么怎么给泛型指定约束?其实也很简单,只需要where关键字,加上约束的条件。)

具体可以查看这个文章的第五点:

C#泛型详解 - .NET开发菜鸟 - 博客园 (cnblogs.com)

用这个返回当前的单例模式是否已生成。

可以用这个在之后的订阅注册中判断是否已经生成。这个bool值的变量是通常的泛型模式中会用到的。

注意到上面有一个destroy函数,有一个函数在destroy调用时也会一起调用,就是onDestroy,如果一个场景中有多个单例模式,我们需要将它销毁。

 如果当前这个事例被销毁就将它设置为空。

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

public class Singleton<T> : MonoBehaviour where T : Singleton<T>
{
    private static T instance;
    public static T Instance
    {
        get { return instance; }
    }
    protected virtual void Awake()
    {
        if (instance != null) Destroy(gameObject);
        else instance = (T)this;
    }

    public static bool IsInitialized
    {
        get { return instance != null; }
    }

    protected virtual void OnDestroy()
    {
        if (instance == this)
        {
            instance = null;
        }
    }
}

于是此处就可以在此将之前的mouseManager继承为单例:

然后这两个就可以不需要了

并且此时也会警告:

此处有一点,通常来说,我们不希望在场景转换的时候manager被消除掉,因此我们需要在awake中写一些东西。

 (base.Awake是算数)

这个方法可以实现跨场景保留。

同理这里gamemanager也可以继承

然后就可以在playercontroller中直接使用并注册,将信息传递进去。

然后接下来就可以实现一个广播的效果,如果player死了,就执行一个广播,通知所有敌人玩家死了。

接口实现观察者模式的订阅和广播

 有关观察者的详解在此:

(172条消息) 学过的设计模式_晴夏。的博客-CSDN博客

定义一个观察者接口:

所有的观察者都需要继承该接口,并实现该接口,这样才可以作为观察者。

接下来在enemy controller中继承:

继承之后需要实现接口 

以后会有多个敌人,都会实现接口。

我们创建一个列表,收集所有实现加载了这个接口的函数方法,那么就代表它是一个敌人,需要订阅游戏结束。

加入列表和移出列表:

在enemycontroller中的启用和销毁调用该代码:

这里有一点需要注意,当人物消失时,

所以结束游戏时会出现这样的报错:

解决方法:

怎么实现广播呢?

让接口执行end notify的函数,

然后当死亡时执行该方法:

在怪物的win条件中增加

 但是光这样不行,因为即使此时敌人的动画被设定为了win,但此时enemy的update函数仍然会继续进行,切换状态到攻击状态。

因此需要设定一个变量:

 因此此处加入判断条件:

但是这样子仍然不行,因为此时处于attack layer中

而attack layer的动画会覆盖掉上面的动画:

所以此处还得是这个动画:

制作更多的敌人 

本节内容:

-解决复制出来的敌人公用同一个数据的问题

复制模板数据然后复制即可

在代码中补充这个, 

-解决 Player 死亡后仍然可以移动的问题

-使用 Animator override controller 制作 乌龟 敌人

对于动画器使用override创建的controller,可以直接覆盖动画即可。

对乌龟的数据设定如下:

 

-添加 兽人 和 石头人 的素材简单摆放

下载兽人和石头人素材

对于兽人的动画,创建动画器时不使用override而是直接复制,因为使用override,如果修改会对原版也产生修改,所以使用复制的版本。

可以修改兽人和石头人材质,修改其金属质感和光滑程度(光滑会决定反光程度)

将attack02的动画的条件设置为true

创建脚本和一些数据

让它继承enemycontroller动画 

 

 然后添加帧事件

在attack01中添加这个帧事件:

attack02同理:

 将enemycontroller中的attacktarget改成protected,以便于继承。

attck02有两个函数需要执行,除了上面的攻击,此处还实现一个推开的函数: 

击飞:

修改一下数据

 attakforce改为15

接下来实现玩家被击飞时实现眩晕的效果:

在玩家这里添加dizzy眩晕动画:

 在kick off里添加眩晕的代码

实测发现动画播放速度太慢,则调整播放动画速度:

此处还有个问题,处于眩晕状态时,人物依然可以移动。我们希望被攻击的时候不会发生移动,

所以此处选中hit状态,添加add behaviour。

添加animation behaviour文件夹。

把该脚本放进去。

动画状态机的脚本与普通脚本不同,继承的是stateMachineBehaviour。

我们在动画进入时,设定agent的isStopped为真代表无法移动,结束时再将其设定为可以移动。

除此之外,物体正在移动时,isStopped也必须设置为true,这是因为

每当我们点击去往某个地方时,agent的isStopped都是false,意味着此时将会变得可以移动。

 所以我们需要保证动画运行时候agent也是不能移动的,才这样:

 

 这样就实现了眩晕时候无法再次移动。

此处有另外一个问题,当敌人攻击玩家时,如果玩家移动敌人会跟着移动,那么就会出现敌人边攻击边移动。

实现这个办法只需要在attack的动画中添加之前那个无法移动的behaviour即可。

 前面设置的史莱姆同理(史莱姆设置之后乌龟不需要再次设置)。

接下来,当玩家杀死敌人时会出现一个报错。   这是因为敌人死的时候这个命令还在执行,

这是因为死亡时我们设置了:

回忆一下此处我们设置它为false是因为不希望死了之后还挡住人,那只需要设置半径为0即可。

 这样子就可以消除bug。

此处补充一个,人物旋转时 相机可能就不会跟着旋转,设定下面这个选项可以使得游戏运行时相机始终看着人物前方。

兽人代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class Grunt : EnemyController
{

    [Header("Basic Setting")]
    public float kickForce = 1f;

    public void KickOff()
    {
        Debug.Log("执行推开函数");
        if (attackTarget)
        {
            Debug.Log("目标存在,推开");
            if(attackTarget.GetComponent<NavMeshAgent>().isStopped == true)
            {
                Debug.Log("玩家此时agent静止");
            }

            transform.LookAt(attackTarget.transform);
            Vector3 direction = attackTarget.transform.position - transform.position;
            direction.Normalize();
            attackTarget.GetComponent<NavMeshAgent>().isStopped = true;
            attackTarget.GetComponent<NavMeshAgent>().velocity = kickForce*direction;
            attackTarget.GetComponent<Animator>().SetTrigger("Dizzy");
        }
    }
}

扩展方法

我们希望实现玩家在敌人的前方120度的范围内攻击才会受到伤害,否则不会受到伤害。

方法如下: 判断两个向量的点积如果大于0.5,则说明在范围内。

此处虽然可以不使用拓展方法,但是为了使用这个用法,所以使用下面的写法:

扩展方法就是在现有的类中去延展方法:

后面那个则是所需的参数

使用起来方法如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class Grunt : EnemyController
{

    [Header("Basic Setting")]
    public float kickForce = 1f;

    public void KickOff()
    {
        Debug.Log("执行推开函数");
        if (attackTarget)
        {
            Debug.Log("目标存在,推开");
            if(attackTarget.GetComponent<NavMeshAgent>().isStopped == true)
            {
                Debug.Log("玩家此时agent静止");
            }

            transform.LookAt(attackTarget.transform);
            Vector3 direction = attackTarget.transform.position - transform.position;
            direction.Normalize();
            attackTarget.GetComponent<NavMeshAgent>().isStopped = true;
            attackTarget.GetComponent<NavMeshAgent>().velocity = kickForce*direction;
            attackTarget.GetComponent<Animator>().SetTrigger("Dizzy");
        }
    }
}

设置石头人boss

创建一个石头人的动画器,源自重载的兽人animator。

再写一个代码,继承自enemycontroller。为了子类能使用这个变量,将其设定为protected

设定数据:

最终代码如下:

kick off会存在一个问题,玩家静止时无法改变其速度,也就是无法击退,要将auto braking 的勾选关掉。


接下来对之前代码产生的一些小问题进行修复:

回顾一下我们是怎么让玩家进行攻击的?:

当点击时会触发attack事件,然后执行移动,当敌人在玩家的攻击范围以外时,就会将敌人所在位置设置为目标点。当敌人进入玩家攻击范围时,设置玩家停止。(但是此时玩家的目标点仍然是敌人的position。)

 此处修改停止距离,

 设计一个变量获取初始时的停止距离:

然后此处修改停止距离为武器半径距离。

 

但是平常的时候我们希望停止距离是玩家的半径。


接下来修复另外一个问题,当人物受伤或者玩家受伤动画时,此时仍然可以触发攻击效果。

在gethit的behaviour里面添加

stop agent 的状态机代码:

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

public class StopAgent : StateMachineBehaviour
{
    // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
    override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        animator.GetComponent<NavMeshAgent>().isStopped = true;
    }

    // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
    override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        animator.GetComponent<NavMeshAgent>().isStopped = true;

    }

    // OnStateExit is called when a transition ends and the state machine finishes evaluating this state
    override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        animator.GetComponent<NavMeshAgent>().isStopped = false;

    }

    // OnStateMove is called right after Animator.OnAnimatorMove()
    //override public void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    //{
    //    // Implement code that processes and affects root motion
    //}

    // OnStateIK is called right after Animator.OnAnimatorIK()
    //override public void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    //{
    //    // Implement code that sets up animation IK (inverse kinematics)
    //}
}

为石头人boss添加丢石头

将石头预制件导入,添加代码碰撞体

石头代码:

将石头作为prefab

在石头人这里添加变量

 并将手的位置赋值进去,以及rock预制体

 

 记得补充石头人的攻击范围还有扔石头的力:

当手举到最高点时候添加rock函数:

帧事件中的扔石头:

注意,石头要添加mesh collider,并且还要勾选上convex才行:

 接下来即可实现扔石头。

但是有一个问题,石头人开始做丢石头的动作时,如果这个时候人物远离怪物的距离,则虽然会进行丢石头的动画,但是不会产生石头。

这是因为上面这里:


 如果attackTarget为空,则岩石也无法生成。

所以加一句话:

这样即使玩家此时走出范围依旧有效。

 除此之外,此处还有一个攻击范围的问题,我们希望离玩家远时用skii,近时用attack的话,那我们要保证同时只有一个条件为true。

如下图所示 ,添加条件保证同时只有一个条件成立。

还有一个问题,注意到只有玩家处于敌人视野时才会进入攻击状态,因此如果视野为10,但是丢石头的skil range为20,那么此时是没有用的。

因此如果想扩大石头人的攻击范围,得同时修改石头人的视野范围:

使用石头进行反击

接下来添加一些石头的状态:

 

石头需要产生伤害,但是注意到之前的take damage用的函数参数是两个states,此处重载一个:

然后在石头函数里写一个碰撞函数:

 记得对石头状态初始化:

 这样就可以实现石头的击退合受伤效果。

但是有个问题在于玩家被击晕后会自动回到原来的destiantion。修改此处即可,将动画状态机的exit函数注释掉即可,这样就不会自动回去。

 另一方面,石头可以用来反击石头人。

 思路就是可以反推回石头对石头人造成伤害,当石头触碰到玩家或者速度小于1时,则将石头状态改为hitnothing,即可以反推的状态。

在玩家点击时如果点击的是石头则将其传入event中,然后让石头反推回去。

具体的用法就是:

先给石头添加Attackable标签

在鼠标点击事件里添加一个控制函数,如果点击的是可攻击类的标签则将其传回event事件。

 

状态改为public

 在player的hit中修改,如果触碰的是可攻击的物体则调用石头进行攻击。

并且当石头状态为hitNothing时才能反击。(虽然这个条件可以删除) 

并且当攻击了石头后,石头自动变为hitEnemy状态。

接下来设定石头什么时候进入hitnothing的状态呢?有两种,一个是没碰到任何事物

一个是通过速度来判断:

可以暂停下来通过后面这个按钮逐帧播放进行调试:

逐帧判断之后发现石头刚生成的一瞬间速度是为0的。然后初始时设立状态为one,这样防止刚开始时就将其状态设置为hitNothing。

 同理,当player刚赋予岩石速度时,就要设置成Vector3.one,以防止其进入hitNothing状态。

除此之外,此处有个问题,在于玩家会穿过石头,但是玩家有了agent,不能直接添加rigidbody,否则会产生冲突,因此此时使用isKinematic的rigidbody,防止其受力的效果。(添加了rigidbody后就可以和石头产生碰撞效果。)

这样就可以实现产生石头和石头的反击。

接下来实现石头消失后的粒子效果:

关闭循环,设定持续时间,设定重力效果,以及随时间出来多少个

实现碰撞:

设定粒子形状:

 

 调整大小:

调整碎片炸裂角度:

 然后在rock消失前产生碎片即可。

最终效果:

代码

石头人代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class Golem : EnemyController
{
    [Header("Skill")]
    public float kickForce = 20;
    public GameObject rockPrefab;
    public Transform handPos;
    public bool agentIsStopped;
    public void ThrowRock()
    {
        if (attackTarget == null) attackTarget = FindObjectOfType<PlayerController>().gameObject;
        if (attackTarget != null)
        {
            var rock = Instantiate(rockPrefab,handPos.position,Quaternion.identity);
            rock.GetComponent<Rock>().target = attackTarget;
        }
    }

    public void KickOff()
    {
        Debug.Log("执行推开函数");
        if (attackTarget)
        {
            Debug.Log("目标存在,推开");
            //if (attackTarget.GetComponent<NavMeshAgent>().isStopped == true)
            //{
            //}
            agentIsStopped = attackTarget.GetComponent<NavMeshAgent>().isStopped;
            transform.LookAt(attackTarget.transform);
            Vector3 direction = attackTarget.transform.position - transform.position;
            direction.Normalize();
            attackTarget.GetComponent<NavMeshAgent>().velocity = kickForce * direction;
            Vector3 velocity = attackTarget.GetComponent<NavMeshAgent>().velocity;
            Debug.Log("kickForce:" + kickForce);
            Debug.Log("direction:" + direction.x+" "+direction.y+" "+direction.z);
            Debug.Log("velocity:" + velocity.x + " " + velocity.y + " " + velocity.z);

            //attackTarget.GetComponent<NavMeshAgent>().isStopped = true;

            attackTarget.GetComponent<Animator>().SetTrigger("Dizzy");
        }
    }

}

石头代码:

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

public class Rock : MonoBehaviour
{
    public enum RockStates { HitPlayer,HitEnemy,HitNothing};
    Rigidbody rb;
    [Header("Basic Settings")]
    public float force=10;
    public GameObject target;
    private Vector3 direction;
    public RockStates rockStates;
    int rockDamage = 10;
    public GameObject breakPatrical;

    private void Start()
    {
        rb = GetComponent<Rigidbody>();
        rockStates = RockStates.HitPlayer;
        FlyToTarget();
        rb.velocity = Vector3.one;
    }


    private void FixedUpdate()
    {
        if (rb.velocity.sqrMagnitude < 1)
        {
            rockStates = RockStates.HitNothing;
        }
        Debug.Log("rockStates:" + rockStates);
    }

    public void FlyToTarget()
    {
        direction = (target.transform.position - transform.position + Vector3.up).normalized;
        rb.AddForce(direction * force,ForceMode.Impulse);
    }

    private void OnCollisionEnter(Collision collision)
    {

            switch (rockStates)
            {
            case RockStates.HitPlayer:

                if (collision.gameObject.CompareTag("Player"))
                {
                    collision.gameObject.GetComponent<NavMeshAgent>().isStopped = true;
                    collision.gameObject.GetComponent<NavMeshAgent>().velocity = direction * force;

                    collision.gameObject.GetComponent<Animator>().SetTrigger("Dizzy");
                    collision.gameObject.GetComponent<CharacterStats>().TakeDamage(rockDamage,collision.gameObject.GetComponent<CharacterStats>());

                    rockStates = RockStates.HitNothing;
                }
                break;

            case RockStates.HitEnemy:

                if (collision.gameObject.GetComponent<Golem>())
                {
                    var otherStats=collision.gameObject.GetComponent<CharacterStats>();
                    otherStats.TakeDamage(rockDamage, otherStats);
                    Instantiate(breakPatrical, transform.position, Quaternion.identity);
                    Destroy(gameObject);
                }
                break;
        }
    }
}

CharaStats:

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

public class CharacterStats : MonoBehaviour
{
    public CharacterData_SO characterData;
    public CharacterData_SO templateData;

    #region Read from Data_SO
    private void Awake()
    {
        if (templateData != null) characterData = Instantiate(templateData);
    }
    public int MaxHealth//为间接可以将其写在一个片段里
    {
        get { if (characterData != null) return characterData.maxHealth; else return 0; }
        set { characterData.maxHealth = value; }
    }

    public int CurrentHealth//为间接可以将其写在一个片段里
    {
        get { if (characterData != null) return characterData.currentHealth; else return 0; }
        set { characterData.currentHealth = value; }
    }

    public int BaseDefence//为间接可以将其写在一个片段里
    {
        get { if (characterData != null) return characterData.baseDefence; else return 0; }
        set { characterData.baseDefence = value; }
    }
    public int CurrentDefence//为间接可以将其写在一个片段里
    {
        get { if (characterData != null) return characterData.currentDefence; else return 0; }
        set { characterData.currentDefence = value; }
    }
    #endregion

    public AttackData_SO attackData;
    [HideInInspector]
    public bool isCritical;

    public void TakeDamage(CharacterStats attacker,CharacterStats defener)
    {
        int damage = Mathf.Max(attacker.CurrentDamage() - defener.CurrentDefence,0);
        //Debug.Log("受到" + damage + "伤害!");//受到多少伤害
        CurrentHealth = Mathf.Max(CurrentHealth - damage,0);

        if (attacker.isCritical)
        {
            defener.GetComponent<Animator>().SetTrigger("Hit");
        }

    }
    public void TakeDamage(int damage,CharacterStats defener)
    {
        int currentDamage = Mathf.Max(damage - defener.CurrentDefence, 0);
        CurrentHealth = Mathf.Max(CurrentHealth - currentDamage, 0);
    }

    
    private int CurrentDamage()
    {
        float coreDamage = UnityEngine.Random.Range(attackData.minDamage, attackData.maxDamage);
        if (isCritical)
        {
            coreDamage *= attackData.criticalMultipler;
            //Debug.Log("暴击!" + coreDamage);
        }
        else
        {
            //Debug.Log("没暴击!" + coreDamage);
        }
        return (int)coreDamage;
    }
}

playercontroller:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class PlayerController : MonoBehaviour
{
    private NavMeshAgent agent;
    private Animator animator;
    private GameObject attackTarget;
    private float lastAttackTime = 0f;
    private CharacterStats characterStats;
    private bool isDead;

    
    // Start is called before the first frame update
    void Awake()
    {
        agent = GetComponent<NavMeshAgent>();
        animator = GetComponent<Animator>();
        characterStats = GetComponent<CharacterStats>();
    }

    void Start()
    {
        MouseManager.Instance.OnMouseClicked += MoveToTarget;
        MouseManager.Instance.OnEnemyClicked += EventAttack;
        characterStats.CurrentHealth = characterStats.MaxHealth;
        GameManager.Instance.RigisterPlayer(characterStats);//将信息注册进去
    }



    void Update()
    {
        lastAttackTime -= Time.deltaTime;
        isDead = characterStats.CurrentHealth == 0;
        SwitchAnimation();
        if (isDead) GameManager.Instance.NotifyObservers();
        Vector3 des = agent.destination;
        //Debug.Log("des:" + des.x + " " + des.y + " " + des.z);
    }
    private void SwitchAnimation()
    {
        animator.SetFloat("Speed", agent.velocity.sqrMagnitude);
        animator.SetBool("Death", isDead);
    }
    void MoveToTarget(Vector3 target)
    {
        StopAllCoroutines();//使得人物在走向目标的过程中也可以通过点击去往其他地方,打断操作

        if (isDead) return;
        agent.isStopped = false;//加上这条语句,就解决了人物一旦攻击之后就无法行动的问题
        agent.destination = target;
    }
    void EventAttack(GameObject target)
    {
        if (isDead) return;

        if (target != null)
        {
            attackTarget = target;
            characterStats.isCritical = UnityEngine.Random.value < characterStats.attackData.criticalChance;
            StartCoroutine(MoveToAttackTarget());
        }
    }

    IEnumerator MoveToAttackTarget()
    {
        //为防止这次点击后下次agent无法行动了,在这次点击的开头使用复原
        agent.isStopped = false;
        transform.LookAt(attackTarget.transform);

        while (Vector3.Distance(transform.position, attackTarget.transform.position) > characterStats.attackData.attackRange)
        {
            agent.destination = attackTarget.transform.position;
            yield return null;
        }
        //当到达指定地点时,命令agent停止
        agent.isStopped = true;

        //攻击具有cd时间
        if (lastAttackTime < 0)
        {
            animator.SetBool("Critical", characterStats.isCritical);
            animator.SetTrigger("Attack");
            lastAttackTime =characterStats.attackData.coolDown;
        }
    }

    void Hit()
    {
        if (attackTarget.CompareTag("Attackable"))
        {
            if (attackTarget.GetComponent<Rock>() && attackTarget.GetComponent<Rock>().rockStates == Rock.RockStates.HitNothing)
            {
                attackTarget.GetComponent<Rigidbody>().velocity = Vector3.one;
                attackTarget.GetComponent<Rock>().rockStates = Rock.RockStates.HitEnemy;//一旦发动攻击则将状态设置为hitEnemy
                attackTarget.GetComponent<Rigidbody>().AddForce(transform.forward * 20, ForceMode.Impulse);
            }
        }
        else {

            var targetStats = attackTarget.GetComponent<CharacterStats>();
            targetStats.TakeDamage(characterStats, targetStats);
        }
    }

}

猜你喜欢

转载自blog.csdn.net/weixin_43757333/article/details/122993978