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