3D RPG Course | Core | Unity学习笔记(七)

(一)制作石头人boss

        希望为石头人设置近战击飞和远程两种攻击动作。由于动画逻辑与兽人敌人一致,所以可以使用override方式设计AnimatorController。

        脚本也继承自EnemyController,首先实现近战击飞效果。

//代码结构与之前兽人的技能类似
[Header("skill")]
public float kickForce = 20;//击飞的力

public void KickOff()//近战并击飞玩家
{
    if (attackTarget != null && transform.IsFacingTarget(attackTarget.transform))//先判断攻击对象是否为空,防止玩家已经离开,并判断攻击目标是否在前方扇形范围内
    {
        
        //实现击飞
        if(attackTarget!=null)
        {
            var TargetStats = attackTarget.GetComponent<CharacterStats>();
            Vector3 direction = attackTarget.transform.position - transform.position;//玩家被击飞的方向
            direction.Normalize();//量化
            attackTarget.GetComponent<NavMeshAgent>().isStopped = true;//目标被击飞之前先被打断动作
            attackTarget.GetComponent<NavMeshAgent>().velocity = direction*kickForce;
            //attackTarget.GetComponent<Animator>().SetTrigger("dizzy");
            TargetStats.TakeDamage(characterStats, TargetStats);
        }
    }
}

        其他部分也像其他单位一样设置,这样近战机能就可以正常运行了。

        (注:这里修复一下player因为敌人半径大于agent的停止距离导致一直移动的问题,教程采用在攻击时修改停止距离的方式,但由于代码实现逻辑会在到达目标后停止agent,所以也可以采用让移动判断条件改为Vector3.Distance(attackTarget.transform.position,transform.position) > characterStats.attackData.attackRange+attackTarget.GetComponent<NavMeshAgent>().radius的形式。此外记得在受伤的动画中加上StopAgent行为脚本来保证受伤动画也可以打断移动。)

(二)制作可以扔出的石头

        为这颗石头创建代码,使用刚体组件来让石头拥有速度等物理属性,并添加MeshCollider来让石头能够碰撞到物体。构建好石头后,修改石头人的代码,以“判定距离-控制动画-触发动画事件-调用技能方法”的逻辑来完成一次攻击的实现。(如果测试时发现石头人只远程不近战,则改变攻击逻辑。)当发现石头人和兽人的击退效果无效时,在击退生效前先将player的agent.autoBraking关闭,player移动时再开启即可。

        教程希望让石头也能够成为玩家攻击boss的方法,所以需要让石头拥有多种状态。使用OnCollisionEnter方法可以在发生碰撞时被调用,获得被碰撞体的信息来切换石头状态并采取不同的处理。

        脚本中在控制游戏数据的CharacterStats中已经有了伤害方法,但石头的脚本没有必要的参数取调用,为了减少工作量可以对方法进行重载来实现石头对撞击目标的伤害:

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

        接着实现石头用于攻击boss的状态(脚本中的gameObject代表所挂载的游戏对象),包括造成伤害和销毁石头自身,并修改MouseManager的点击方法和PlayerController的攻击方法,让玩家在攻击到tag为Attackable的对象时,能够改变石头的状态并将其击飞出去:

void Hit()//动画事件调用此方法来完成攻击扣血
{
    if(attackTarget.CompareTag("Attackable"))
    {
        if(attackTarget.GetComponent<Rock>() && attackTarget.GetComponent<Rock>().rockState== Rock.RockStates.HitNothing)//当攻击了已经不再攻击玩家的石头时,把石头打回去
        {
            attackTarget.GetComponent<Rock>().rockState = Rock.RockStates.HitEnemy;
            attackTarget.GetComponent<Rigidbody>().velocity = Vector3.one;//防止速度为0的帧,使得状态被设置为HitNothing
            attackTarget.GetComponent<Rigidbody>().AddForce(transform.forward * kickForce,ForceMode.Impulse);//击飞石头
        }
    }
    else
    {
        var TargetStats = attackTarget.GetComponent<CharacterStats>();
        TargetStats.TakeDamage(characterStats, TargetStats);
    }

}

        想要击飞石头,就需要石头保持在HitNothing状态,由于希望通过物理属性判断石头的情况来转换状态,所以要采用FixedUpdate()方法来代替Update()方法(unity中游戏暂停是ctrl+shift+p,使用暂停加逐帧运行可以方便的观察数值变化)。由于之前采用了攻击距离加半径的方式来改正敌人半径大于agent停止距离的问题,所以石头上也需要添加NavMeshAgent组件防止报错,并停用NavMeshAgent防止与刚体组件冲突。,如果采用原教程的方法就不需要。为了防止player直接穿过石头,需要给player也加上刚体,并勾选"is Kinematic"防止与NavMeshAgent冲突。

        希望让石头的消失更加真实,可以在Hierarchy中创建 Effects-Particle System (粒子系统,详见中文手册)实现石头碎裂效果。参数设置完成后,保存为预制体并在Rock脚本中调用。

        最终的石头脚本:

public class Rock : MonoBehaviour
{
    private Rigidbody rb;
    public enum RockStates { HitPlayer,HitEnemy,HitNothing}//石头的不同状态
    public RockStates rockState;

    [Header("basic setting")]
    public float force;
    public int damage;//基础伤害
    public GameObject target;//砸向的目标
    private Vector3 direction;//飞行方向

    public GameObject breakEffect;//消失的粒子效果

    private void Start()
    {
        rb = GetComponent<Rigidbody>();//生成时执行
        rb.velocity = Vector3.one;//防止刚生成时速度为0,使得状态被设置为HitNothing从而无法攻击玩家
        rockState = RockStates.HitPlayer;//初始状态为攻击玩家
        FlyToTarget();//从生成开始就要飞向目标
    }

    private void FixedUpdate()
    {
        if (rb.velocity.sqrMagnitude<1f)//向量平方长度,比Magnitude计算更快
        {
            rockState = RockStates.HitNothing;
        }
    }

    private void FlyToTarget()
    {
        if(target== null)//石头刚生成出来玩家就跑出了范围时
        {
            target=FindObjectOfType<PlayerController>().gameObject;
        }
        direction = (target.transform.position - transform.position+Vector3.up).normalized;//为了让石头能够有更长的滞空时间,为方向向量加一个向上的分量。
        rb.AddForce(direction*force,ForceMode.Impulse);//采用冲击力的模式让石头一下飞出去。

    }

    private void OnCollisionEnter(Collision other)//此函数能够在发生碰撞时获得被撞击的目标的信息并执行
    {
        switch (rockState)
        {
            case RockStates.HitPlayer:
                if (other.gameObject.CompareTag("Player"))//撞到player时将其击退
                {
                    other.gameObject.GetComponent<NavMeshAgent>().autoBraking = false;
                    other.gameObject.GetComponent<NavMeshAgent>().isStopped = true;
                    //击退方向已经定义过了,可以重复利用
                    other.gameObject.GetComponent<NavMeshAgent>().velocity = direction * force;
                    other.gameObject.GetComponent<Animator>().SetTrigger("dizzy");
                    other.gameObject.GetComponent<CharacterStats>().TakeDamage(damage, other.gameObject.GetComponent<CharacterStats>());
                    rockState = RockStates.HitNothing;
                }
                break;
            case RockStates.HitEnemy: 
                if(other.gameObject.GetComponent<Golem>())//石头在此状态碰到石头人,则造成伤害并销毁
                {
                    var otherStats = other.gameObject.GetComponent<CharacterStats>();
                    otherStats.TakeDamage(damage, otherStats);
                    Instantiate(breakEffect, transform.position, Quaternion.identity);//消失前生成粒子特效
                    Destroy(gameObject);//销毁石头
                }
                break;
            case RockStates.HitNothing: 
                break;
        }
    }
}

(三)制作血条显示

        使用Unity UI来创建血条。UI制作首先需要创建canvas,画布 (Canvas) 是应该容纳所有 UI 元素的区域。画布是一种带有画布组件的游戏对象,所有 UI 元素都必须是此类画布的子项。创建一个大小合适的image作为第一个血条。在包管理器中安装2D sprite来使用其中的2D素材,从中复制一份Squre到资源文件夹中。通过放置两个相覆盖的红绿矩形来代表血条,上层可变的绿色血条以 Filled模式从左侧填充,这样,只要在脚本中实现实时控制上层图片的覆盖范围就可以实现血条变化。(需要prefab的脚本可以直接在unity的project窗口中配置prefab)

        想设置血条在敌人的头顶的话,可以在敌人对象身上创建代表头顶的子对象来获得位置。在数据脚本CharacterStats中产生伤害的方法里需要在扣血后更新UI,类似于鼠标的点击功能,可以采用事件的形式实现。在 CharacterStats中设置事件“public event Action<int,int> UpdateHealthBarOnAttack;”,并在造成伤害后触发事件"UpdateHealthBarOnAttack?.Invoke(CurrentHealth, MaxHealth);"。回到血条脚本,将方法注册到事件中。并在Awake和OnEnable中完成血条的生成。由于场景树基于transform构建,因此获得血条子对象需要通过其transform获得。

        接下来实现血条的跟随和显示时间。Update在每一帧执行,而LateUpdate会在上一帧结束后执行,两者之间存在一定时间差,在血条实现中采用后者来体现血条跟随在人物后移动与面朝摄像机。

        最终的血条脚本如下:

public class HealthBarUI : MonoBehaviour
{
    public GameObject healthUIPrefab;//血条UI预制体
    public Transform barPoint;//血条位置

    public bool alwaysVisible;//控制血条是否始终可见
    public float visibleTime;//最大可见时间
    private float timeLeft;//剩余可见时间

    Image healthSlider;
    Transform UIbar;//血条位置变量
    Transform cam;//用于保持面朝摄像机!

    CharacterStats currentStats;

    private void Awake()
    {
        currentStats = GetComponent<CharacterStats>();
        currentStats.UpdateHealthBarOnAttack += UpdateHealthBar;//将方法登记在事件中
    }

    private void OnEnable()//启动时调用
    {
        cam = Camera.main.transform;//获得主相机位置
        foreach(Canvas canvas in FindObjectsOfType<Canvas>())//在所有canvas对象中搜索属于血条的HealthBar Canvas
        {
            //在对应Canvas上生成血条
            if(canvas.name== "HealthBar Canvas")//或canvas.renderMode==RenderMode.WorldSpace
            {
                UIbar = Instantiate(healthUIPrefab, canvas.transform).transform;//生成血条对象并获得坐标变量
                healthSlider = UIbar.GetChild(0).GetComponent<Image>();//获得绿色血量滑块子对象
                UIbar.gameObject.SetActive(alwaysVisible);//设置是否随时可见
            }
        }
    }

    private void UpdateHealthBar(int CurrentHealth, int MaxHealth)
    {
        if(CurrentHealth==0)
        {
            Destroy(UIbar.gameObject);//敌人死亡时血条也一同消失。
        }
        UIbar.gameObject.SetActive(true);//受伤时血条应为可见
        timeLeft = visibleTime;//恢复可见时间
        float sliderPercent = (float) CurrentHealth / MaxHealth;
        healthSlider.fillAmount=sliderPercent;//将绿色血条图像设置为当前生命值百分比

    }

    private void LateUpdate()
    {
        if(UIbar!=null)//血条没有被销毁,防止空指针报错
        {
            UIbar.position = barPoint.position;//血条追踪单位
            UIbar.forward = - cam.forward;//始终面朝相机
            if (timeLeft <= 0&&!alwaysVisible)//控制剩余显示时间
            {
                UIbar.gameObject.SetActive(false);
            }
            else if(timeLeft>0)
            {
                timeLeft-=Time.deltaTime;
            }
        }
    }
}

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇

)">
下一篇>>