最近玩了下Unity AssetStore上《Stealth》游戏,感觉很有意思,所以花了好些时间来读它的代码,感觉最让我值得学习的是其中机器人守卫的动画与寻路控制,运用到了NavMeshAgent和Aniamtor的结合,还有向量投射,点积叉积的运算,所以记录一下:
其中大体思路是: 敌人按给定的四个点巡逻,如果玩家进入视线范围内就转身停下,然后追赶或者射击玩家。
敌人守卫使用了带位移的动画,而其寻路导航却是用了NavMeshAgent。 所以做法是:
> 位移:
不直接使用动画所带的位移(就是把Animator里的ApplayRootMotion的勾去掉),而是把动画里的位移读取到NavMeshAgen的速率,再另外自定设置speed。
这样之后NavMeshAgen就根据目标点会产生一个应该达到的速率值desiredVelocity来读取供控制BlendTree动画的两个参数(下面有介绍)。> 转向:
机器人守卫transform.rotation直接读取动画中的rotation。
在敌人GameObject下的脚本DoneEnemyAnimation写“void
OnAnimatorMove()”,这个方法会在Animator的ApplayRootMotion勾掉后,并且动画带有位移时每帧执行,所以可以在这里读取动画中的位移和转向参数,然后赋给位移NavMeshAgent,以达到原本符合动画的位移速度,当然还有动画的转向角度也读取出来。
把这里的勾去掉,在代脚本里写”OnAnimatorMove()”读取动画位移:
先看看机器人敌人使用的Animator的构成:
一共用了3层Layer,带位移的是”BaseLayer”,另外的”Shooting”和”Gun”是控制开枪手臂对象目标玩家的带IK动画和开枪的动作,它们的权重都是设为1f,也就是当它们两个的执行条件为true时,它们的动画会覆盖掉BaseLayer的动画。
但我们需要关注的只是”BaseLayer”,在”BaseLayer”中使用了”BlendTree”混合动画,在左下角的”Parameter”增加两个参数”AngularSpeed”和”Speed”来控制这个”BlendTree”。> 如何控制:
1,在Inspector中把”BlenTree”的”BlenType”选择为”2D Freeform Cartersian”;
2,然后把”BlendType”的x和y选择为”AngularSpeed”和”Speed”,然后就可以通过脚本获取到控制这两个参数后控制动画。也可以移动动画控制坐标中的红点感受下对动画的操作和在Scene中查看两个控制参数的变化
。
机器人的Animator设置好了,要读取到动画的位移,那还需要让机器人敌人根据不同情景做出不同动画,这样才能够读取到这个动作的位移,要不没有动画,当然就不存在动画的位移。
所以有了这样的这个类:DoneEnemyAnimation.cs
using UnityEngine;
using System.Collections;
public class DoneEnemyAnimation : MonoBehaviour
{
public float deadZone = 5f;
private Transform player;
private DoneEnemySight enemySight;
private NavMeshAgent nav;
private Animator anim;
private DoneHashIDs hash;
private DoneAnimatorSetup animSetup;
void Awake ()
{
player = GameObject.FindGameObjectWithTag(DoneTags.player).transform;
enemySight = GetComponent<DoneEnemySight>();
nav = GetComponent<NavMeshAgent>();
anim = GetComponent<Animator>();
hash = GameObject.FindGameObjectWithTag(DoneTags.gameController).GetComponent<DoneHashIDs>();
//因为勾掉Animator的ApplyRootMotion,添加“OnAnimatorMove”,所以动画的位移和转向都不起作用。
//NavMeshAgent挂到了机器人上,nav导航的转向不一定和动画转向吻合,所以不使用nav的转向,转向是读取动画的转向来直接赋值到transform控制。
nav.updateRotation = false;
// 这个获取到动画的层“BaseLayer”层引用
animSetup = new DoneAnimatorSetup(anim, hash);
// 设置瞄准和开枪动画层的权重
anim.SetLayerWeight(1, 1f);
anim.SetLayerWeight(2, 1f);
//换成弧度角计算
deadZone *= Mathf.Deg2Rad;
}
void Update ()
{
NavAnimSetup();
}
void OnAnimatorMove()
{
// 设置NavMeshAgent的速度 等于上一帧的移动速度 =动画位移本应向量的增量/所使用的时间增量
//拿到最后一帧的速度的原因:因为动画的位移速度不一定是均匀的,所以拿到最后一帧以最大的达到动画本身位移的此帧本应的移动
nav.velocity = anim.deltaPosition / Time.deltaTime;
// 敌人转向由动画控制
//敌人的转向仍然由动画本身的转向控制
transform.rotation = anim.rootRotation;
}
void NavAnimSetup ()
{
float speed;
float angle;
//如果玩家进入了敌人的视线
if(enemySight.playerInSight)
{
//敌人停止
speed = 0f;
//为了让敌人转身面向玩家,求出需要转身的角度
//这个角度 = 敌人的transform.forward向量 和 敌人到玩家的向量 的夹角
angle = FindAngle(transform.forward, player.position - transform.position, transform.up);
}
else
{
//向量投影: 巡逻状态是期望速率在前方向上的投影的标量长度
speed = Vector3.Project(nav.desiredVelocity, transform.forward).magnitude;
//转向下个目标点需要转的角度
angle = FindAngle(transform.forward, nav.desiredVelocity, transform.up);
//如果转向目标点的剩余角度小于死角,则直面向目标点
//为什么会产生死角,应为读取动画控制的Rotation值,而动画转向是随着机器人走路时会产生左右轻轻摇摆
if(Mathf.Abs(angle) < deadZone)
{
//直接面向目标点,nav的desiredVelocity是nav期望达到的,应该达到的速率
//面向这个矢量,也就是获得向前的方向,然后直接面向
transform.LookAt(transform.position + nav.desiredVelocity);
angle = 0f;
}
}
//这个实力的Setup调用就是控制上面所提到控制BlendTree的两个参数,从而控制动画。之后才能从动画读出位移和转向。
animSetup.Setup(speed, angle);
}
//
float FindAngle (Vector3 fromVector, Vector3 toVector, Vector3 upVector)
{
if(toVector == Vector3.zero)
return 0f;
//创建一个float保存 当前向量和目标向量的夹角
float angle = Vector3.Angle(fromVector, toVector);
//(获取敌人的法向量,以便和当前机器人守卫的上方向进行点积运算)
//叉积: 计算当前向量与目标向量的向量积 得到法向量(如果目标向量在当前向量的右边 那么法向量的方向向上).
Vector3 normal = Vector3.Cross(fromVector, toVector);
//(法向量与机器人守卫的上方向进行点积运算,这里只是为了知道向左转还是向右转,也就是获取夹角值的正负号)
//点积: 计算法向量与敌人正上方向量的点积 如果是同方向 值为正 反之则为负
angle *= Mathf.Sign(Vector3.Dot(normal, upVector));
// 把角度转换为弧度
angle *= Mathf.Deg2Rad;
return angle;
}
}
上面只是实现了动画移动转向的控制,AI的控制在这个类中:
DoneEnemyAI.cs:
这个类主要是给NavMeshAgent设置目标点和speed,如里面的下面的这个方法:
void Patrolling ()
{
// 设置巡逻速度
nav.speed = patrolSpeed;
// 设置目标点
if(nav.destination == lastPlayerSighting.resetPosition || nav.remainingDistance < nav.stoppingDistance)
{
// ... increment the timer.
patrolTimer += Time.deltaTime;
// If the timer exceeds the wait time...
if(patrolTimer >= patrolWaitTime)
{
// ... increment the wayPointIndex.
if(wayPointIndex == patrolWayPoints.Length - 1)
wayPointIndex = 0;
else
wayPointIndex++;
// Reset the timer.
patrolTimer = 0;
}
}
else
// If not near a destination, reset the timer.
patrolTimer = 0;
// Set the destination to the patrolWayPoint.
nav.destination = patrolWayPoints[wayPointIndex].position;
}
就辣么样的完成了看起来毫无违和感的动画与位移转向结合了哈O(∩_∩)O~~