Unity笔记-04

Unity笔记-04

练习项目脚本规划

项目需求(部分)

开始,生成指定数量的敌人。

为每人随机选择一条可以使用的路线,要求:敌人类型,产生的时间随机

当敌人死亡后再产生下一个敌人,直到生成数量达到上限为止。

需求分析

创建脚本:敌人马达类,提供移动,旋转,寻路等功能

创建脚本:敌人状态信息类,提供受伤,死亡等功能

创建脚本:敌人动画类,定义各种动画名称,播放动画的功能

创建脚本:敌人AI类,通过判断状态执行寻路或者攻击

部分代码与改动

动画类与动画工具类

动画工具类

/// <summary>
/// 动画工具类,提供有关动画播放行为的功能
/// </summary>
public class AnimationTool
{
    /// <summary>
    /// 附加在敌人动画上的引用
    /// </summary>
    private Animation anim;

    /// <summary>
    /// 创建动画行为类
    /// </summary>
    /// <param name="animation">附在敌人动画上的引用</param>
    public AnimationTool(Animation animation)
    {
        this.anim = animation;
    }

    public void Play(string animationName)
    {
        anim.CrossFade(animationName);
    }
}

动画工具类,不继承MonoBehaviour类,仅提供播放动画的方法,因此要写构造函数,并传入播放组件,提供播放方法,后续有需要可添加例如:判断动画是否在播放之类的方法

动画类

/// <summary>
/// 敌人动画类
/// </summary>
public class EnemyAnimation : MonoBehaviour
{
    /// <summary>
    /// 跑步动画
    /// </summary>
    public string runAnimation;
    /// <summary>
    /// 攻击动画
    /// </summary>
    public string AttackAnimation;
    /// <summary>
    /// 攻击后摇动画
    /// </summary>
    public string AttackRollBackAnimation;
    /// <summary>
    /// 死亡动画
    /// </summary>
    public string DeathAnimation;
    /// <summary>
    /// 动画播放工具类
    /// </summary>
    private AnimationTool action;

    private void Awake()
    {
        //注意:动画脚本要挂在敌人的父空物体上,以便日后模型更换,而不需要重新调试脚本
        //action = new AnimationTool(?);
        //FindObjectOfType<EnemyStatusInfo>().GetComponent<Animation>()
        action = new AnimationTool(this.GetComponentInChildren<Animation>());
    }
}

对于动画类的说明:

提前定义多个动画的名称,在初始化的时候给动画名称赋值:比如攻击动画,移动动画,这些动画的名称赋值给名称变量,之后需要播放该动画的时候,只需要通过动画工具类的播放方法去播放该动画即可,这样做的原因是保持类的功能单一性,降低耦合度。

动画类的作用就好像提供一个播放列表,根据你的需要去播放你需要播放的动画

注意:

1.动画脚本应当挂在对象物体的父空物体上,通过父空物体去调用对象的动画,这样做可以方便日后的模型更换

2.这里为了方便把所有定义的动画名称都设置为public方便调试

路线类

/// <summary>
/// 路线类
/// </summary>
public class WayLine
{
    /// <summary>
    /// 当前路点坐标
    /// </summary>
    public WayPoints[] MyProperty;

}
/// <summary>
/// 路点类
/// </summary>
public class WayPoints
{
    /// <summary>
    /// 点的坐标
    /// </summary>
    public Vector3 position;
    /// <summary>
    /// 点是否可用
    /// </summary>
    public bool IsUseable;
}

该类没有继承MonoBehaviour类,作为一个工具类提供路线的存储

position储存点的位置,IsUseable储存点是否可用

敌人AI类

/// <summary>
/// 敌人AI
/// </summary>
[RequireComponent(typeof(EnemyAnimation))]
[RequireComponent(typeof(EnemyMotor))]
[RequireComponent(typeof(EnemyStatusInfo))]
public class EnemyAI : MonoBehaviour
{
    public enum State
    {
        /// <summary>
        /// 攻击状态
        /// </summary>
        Attack,
        /// <summary>
        /// 寻路状态
        /// </summary>
        PathFinding
    }
    /// <summary>
    /// 运动类
    /// </summary>
    private EnemyMotor motor;
    /// <summary>
    /// 动画类
    /// </summary>
    private EnemyAnimation anim;
    /// <summary>
    /// 敌人当前状态
    /// </summary>
    private State currentState;
    /// <summary>
    /// 路线
    /// </summary>
    private WayLine wayLine;
    /// <summary>
    /// 手动输入路点集
    /// </summary>
    public Transform[] Points;

    /// <summary>
    /// 初始化
    /// </summary>
    private void Start()
    {
        #region 初始化工具
        motor = this.GetComponent<EnemyMotor>();//初始化移动马达
        anim = this.GetComponentInParent<EnemyAnimation>();//获得父空物体上挂的动画脚本
        #endregion

        #region 初始化路线
        wayLine = new WayLine();//创建路线实例
        wayLine.MyProperty = new WayPoints[Points.Length];//初始化点集长度
        for (int i = 0; i < Points.Length; i++)
        {
            wayLine.MyProperty[i] = new WayPoints();//创建点实例
            wayLine.MyProperty[i].position = Points[i].position;//初始化路点集
            wayLine.MyProperty[i].IsUseable = true;
        }
        #endregion

        #region 初始化状态
        currentState = State.PathFinding;

        #endregion
    }
    private float AttackTime=0;
    private float intervalTime=2;
    
    /// <summary>
    /// 渲染更新
    /// </summary>
    private void Update()
    {
        switch (currentState)
        {
            case State.Attack:
                Attack();
                break;
            case State.PathFinding:
                PathFinding();
                break;
        }
    }
    private void PathFinding()
    {
        //播放动画
        //执行寻路
        //检查状态
        //修改状态
        if (!motor.PathFinding(wayLine))
        {
            currentState = State.Attack;
        }
    }
    private void Attack()
    {
        if (!anim.action.isPlay(anim.AttackAnimation))
        {
            //播放闲置动画,暂无
        }
        if (AttackTime < Time.time)
        {
            //执行攻击
            //播放攻击动画
            anim.action.Play(anim.AttackAnimation);
            AttackTime += intervalTime;//动画播放时间-间隔也就是攻击后摇
        }
        //播放攻击后腰动画
        //检查状态
        //修改状态
    }
}

敌人AI类,主要通过当前状态来判断当前应当执行哪种动作

这里方便调试,手动输入路点集,后续会修正

这里要把寻路和攻击两个放大独立出来成为单独方法,方便日后维护

敌人状态类

/// <summary>
/// 敌人状态信息类,提供敌人生命值,受伤,阵亡等功能
/// </summary>
public class EnemyStatusInfo : MonoBehaviour
{
    /// <summary>
    /// 当前生命值
    /// </summary>
    public int HP;
    /// <summary>
    /// 最大生命值
    /// </summary>
    public int MaxHP;

    /// <summary>
    /// 受伤,减少生命值
    /// </summary>
    public void Damage(int attackNumber)
    {
        if (HP > 0)
        {
            HP -= attackNumber;
        }
        else
        {
            Death();
        }
    }
    /// <summary>
    /// 销毁延迟时间
    /// </summary>
    private float deathDelay=5;
    /// <summary>
    /// 阵亡
    /// </summary>
    public void Death()
    {
        var anim = this.GetComponent<EnemyAnimation>();
        //播放死亡动画
        anim.action.Play(anim.DeathAnimation);
        //销毁物体
        Destroy(this.gameObject, deathDelay);
    }
}

销毁物体需要等待死亡动画播放完毕,因此要设置延迟时间。

敌人马达类

/// <summary>
/// 敌人马达运动类,提供前进,注视旋转,寻路功能
/// </summary>
public class EnemyMotor : MonoBehaviour
{
    /// <summary>
    /// 设置旋转速度以及移动速度
    /// </summary>
    public float speed=2;
    /// <summary>
    /// 记录位移向量差
    /// </summary>
    private Vector3 Relative;
    /// <summary>
    /// 记录转到下次路点的旋转四元值
    /// </summary>
    private Quaternion rotation;
    /// <summary>
    /// 记录当前走到的路点索引
    /// </summary>
    private int count = 0;
    /// <summary>
    /// 记录上一个路点的索引
    /// </summary>
    private int lastCount;
    /// <summary>
    /// 获得控制件下对象的动画组件
    /// </summary>
    private Animation anim;
    /// <summary>
    /// 前进
    /// </summary>
    public void MoveForward(WayPoints Point)
    {
        if (Point.IsUseable)//判断该路点当前是否被占用
        {
            //transform.Translate(Relative.normalized*Time.deltaTime);
            transform.position = Vector3.MoveTowards(transform.position, Point.position,speed*Time.deltaTime);
            anim.CrossFade("EnemyRun");
            //注意:如果要判断是否到达,不要用“==”来判断,因为计算机是离散的,精度不可能完全精准,要用两点间的距离来判断
            if (Vector3.Distance(transform.position, Point.position)<0.01f)
            {
                anim.Stop("EnemyRun");
                lastCount = count;//存储上一个路点
                Point.IsUseable = false;
                count++;//当前路点自增
            }
        }
    }
        
    /// <summary>
    /// 注视旋转
    /// </summary>
    public void LookRotate(Quaternion rotation)
    {
        this.transform.rotation = Quaternion.Slerp(this.transform.rotation,rotation,3*speed*Time.deltaTime);
    }
    /// <summary>
    /// 寻路
    /// </summary>
    public bool PathFinding(WayLine wayLine)
    {
        if (wayLine!= null && count < wayLine.MyProperty.Length)
        {
            WayPoints Point = wayLine.MyProperty[count];
            Relative = Point.position - this.transform.position;//获得向量差值
            rotation = Quaternion.LookRotation(Relative);//获得旋转
            if (transform.rotation != rotation)//如果方位不相同,那么进行旋转
            {
                LookRotate(rotation);
            }
            else
            {
                MoveForward(Point);
                wayLine.MyProperty[lastCount].IsUseable = true;
            }
            return true;
        }
        else
        {
          //寻路完成
            return false;
        }
        
    }
    /// <summary>
    /// 初始化阶段
    /// </summary>
    private void Start()
    {
        anim = this.GetComponentInChildren<Animation>();
    }
}

敌人马达类,提供移动,转动,寻路等功能。该功能的基本原理为:提供实现给定的一系列点集组成一条路线,在让敌人沿着这条路线移动。

基本思路:

首先注视旋转将面朝方向对准下一个点,然后移动,直到移动到最后一个点。这里由于敌人生成器还未写,对于路线能否使用的判断暂不做解释(可能有误)。

有关移动

主要有以下两种方法:

一:

Translate方法,具体使用方法

transform.Translate(方向向量);

二:

Vector3.MoveTowards方法,具体使用方法

Vector3.MoveTowards(自身位置,移动的目标点,移动速度);

但是要做到持续移动,更加推荐第二种方法,第一种方法因为移动路段固定,可能在某种特定情况下出现穿墙等bug

这里通过两点之间的向量差得到这两点的方向向量:

Relative = Point.position - this.transform.position;

有关注视旋转

也有两种方法(不全):

一:

Transform.LookAt(目标点) 瞬间转向目标点

二:

rotation = Quaternion.LookRotation(向量差);Relative该方法返回旋转四元值(Quaternion)

transform.rotation = Quaternion.Slerp(当前物体的四元值,rotation,旋转速度);

可以持续的旋转,直到正方向(默认为Z轴)朝向目标点

注意

这里的wayline在实际运行的时候会在Start方法里进行初始化赋值,寻路只需要将路线给到寻路方法即可自动寻路

一些的问题和疑问

在实际调试过程中

  1. 由于动画和移动判定的冲突,我将移动脚本挂在了对象的父空物体上,通过控制父空物体的移动来控制对象的移动,而动画则单独控制对象,这样便避免了冲突。
  2. 关于第二种旋转方法的判定问题:如果出现逆时针旋转,通过调试,四元数(x,y,z,w)会出现相反数的情况,而导致判定错误,如果都是顺时针旋转那么没有问题,我的判定方法为:transform.rotation != rotation也就是之间判断二者是否相等,显然我这样的判定存在问题,但是是否单独拿出四元数进行绝对值判断就正确呢?这里存在疑问
  3. 由于寻路方法同时涵盖了旋转和移动,这导致动画类无法在敌人AI类中独立出播放,而必须耦合进旋转和移动的方法,否则就会导致动画错误例如:旋转的过程中就播放移动动画,这显然是错误的;寻路方法是否应该放到敌人AI类里会更好一些。
上一篇:Android动画攻略—帧动画、补间动画、属性动画


下一篇:layui常用插件(一) 轮播图