简介
本文介绍如何在Unity中实现Avatar角色的跨越功能,如图所示:
初版源码已上传至SKFramework
框架Package Manager
中:
变量说明
Layer Mask
:物理检测的Layer层级;Max Distance
:物理检测的最大距离;Vault Key
:触发跨越的快捷键;Hand Radius
:手的半径大小;Hand Local Origin Pos
:手的初始局部坐标;
Cast Down Count Limit
:高度最多下降该次数来检测障碍物;
如图所示,当在初始位置向前进行SphereCast物理检测未检测到结果时,表明没有障碍物或者障碍物的高度可以被Avatar角色跨越,而有没有障碍物,需要不断在初始位置下降高度再次进行检测,在限制次数内检测到碰撞,说明有障碍物且可以被跨越,这便是该参数的作用。
Cast Forward Count Limit
:最多向前进行该次数的物理检测;
如图所示,在
Cast Down Count Limit
限制次数内检测到障碍物时,获得了初始点到碰撞点的距离即RaycastHit
碰撞信息中的distance
,在这之后,还需要在初始点加身体前方上述的碰撞距离单位后,再向下进行检测,当在限制次数内不再能检测到障碍时,表明障碍物的长度在可被Avatar角色跨越的范围内,这便是该参数的作用。
Vault Duration
:跨越动作时长(由动画片段长度决定)。
实现
障碍物高度检测
在手部上方一个hand radius
半径单位处开始检测,当检测不到碰撞时说明障碍物高度可以被Avatar角色跨越或者根本没有障碍物,参考上述Cast Down Count Limit
参数的说明,此时需要下降高度来检测前方是否有障碍物,代码如下:
//局部转全局坐标
Vector3 local2World = transform.TransformPoint(handLocalOriginPos);
//首先在手部上方一个半径单位的高度进行检测
Vector3 castOriginPos = local2World + Vector3.up * handRadius;
castResult = false;
//当检测不到时说明障碍物高度不高于手部 或者前方根本没有障碍物
if (!Physics.SphereCast(castOriginPos, handRadius, transform.forward, out hit, maxDistance, layerMask))
{
//高度满足再降低进行检测
for (int i = 1; i <= castDownCountLimit; i++)
{
//下降一个半径单位
Vector3 pos = castOriginPos - handRadius * i * Vector3.up;
castResult = Physics.SphereCast(pos, handRadius, transform.forward, out hit, maxDistance, layerMask);
//限制次数内检测到 跳出循环
if (castResult) break;
}
}
障碍物长度检测
参考上述Cast Forward Count Limit
参数的说明,在检测到障碍物且高度满足后需要检测障碍物的长度是否可以被跨越,第一次检测的位置=初始点+身体前方 x 高度检测时检测到的碰撞距离,在限制次数内不再能够检测到障碍物后,说明障碍物长度可以被Avatar角色跨越,代码如下所示:
if (castResult)
{
bool flag = false;
//每次从初始点向前加一个半径单位向下进行检测
for (int i = 1; i <= castForwardCountLimit; i++)
{
Vector3 pos = castOriginPos + (hit.distance + handRadius * i) * transform.forward;
//向下不再能检测到障碍物时 表示障碍物长度在可跨越范围内
if (!Physics.SphereCast(pos, handRadius, -transform.up, out RaycastHit raycastHit, maxDistance, layerMask))
{
flag = true;
break;
}
else
{
Debug.DrawLine(raycastHit.point, raycastHit.point + raycastHit.normal, Color.yellow);
}
}
castResult &= flag;
}
角色碰撞调整
如图所示,在跨越的过程中,需要调整Character Controller
角色控制器的Height
高度和Center
中心点,以便使Avatar角色跨越过障碍物,不被障碍物阻挡:
- 跨越之前:需要减小的高度 = 高度检测时的
RaycastHit
碰撞信息中的point
的碰撞点的y值 - Avatar角色的坐标y值 + 一个Hand Radius
* 2(直径)的高度,最终可以再加一点偏移值以确保碰撞器高于障碍物,代码示例如下:
//检测到碰撞
if (castResult)
{
//调试
Debug.DrawLine(hit.point, hit.point + hit.normal, Color.magenta);
//按下快捷键
if (!isVaulting && Input.GetKeyDown(vaultKey))
{
//播放动作
animator.SetTrigger(AnimParams.vault);
//禁用其他Avatar控制
GetComponent<AvatarController>().enabled = false;
//计算碰撞点与身体坐标Y的高度差
heightDelta = hit.point.y - transform.position.y + handRadius * 2f + .2f;
//调整角色控制器的高度和中心 以便Avatar跨越过去 防止被碰撞挡住
cc.height -= heightDelta;
cc.center += heightDelta * .5f * Vector3.up;
//正在跨越标识
isVaulting = true;
//重置计时器
vaultTimer = 0f;
//重置位于障碍物上方标识
isHoverVaultObj = false;
return;
}
}
- 跨越之后:在越过障碍物之后,通过插值方式使高度和中心点逐渐恢复初始值。在开始跨越时,
vault timer
计时器开始计时,因此插值率 =vault timer
/vault duration
:
//已经越过障碍物
if (isOverVaultObj)
{
//逐渐恢复角色控制器的高度和中心
cc.height = Mathf.Lerp(0f, ccOriginHeight, vaultTimer / vaultDuration);
cc.center = Vector3.Lerp(cacheCcCenterWhenOver, ccOriginCenter, vaultTimer / vaultDuration);
}
- 如何判断已经越过障碍物?如图所示,可以在跨越的过程中,持续向身体下方以
Character Controller
的Radius
为半径进行Sphere Cast
物理检测,当检测不到碰撞时,说明已经越过障碍物:
颜色变为红色时表示再也检测不到碰撞,已经越过障碍物。
//处于障碍物上方的过程中 持续检测 再也检测不到的时候 表明已经跨越过去
if (!Physics.SphereCast(transform.position + Vector3.up * (heightDelta + handRadius * 2f + cc.radius), cc.radius, -transform.up, out hoverHit, heightDelta, layerMask))
{
//不再处于障碍物上方
isHoverVaultObj = false;
//已经越过障碍物
isOverVaultObj = true;
//缓存中心点
cacheCcCenterWhenOver = cc.center;
}
手部IK
如图所示,在跨越过程中,要让手的位置和旋转比较准确的贴合在障碍物上,需要通过调用Animator
中Set IK Position
、Set IK Rotation
来设置手部IK,如何计算IK目标位置和旋转?可以在处于障碍物上方的跨越过程中,在手部的位置向下进行物理检测,如图所示,黄色球位置即检测到的目标IK位置:
//处于障碍物上方的过程中 持续检测 再也检测不到的时候 表明已经跨越过去
if (!Physics.SphereCast(transform.position + Vector3.up * (heightDelta + handRadius * 2f + cc.radius), cc.radius, -transform.up, out hoverHit, heightDelta, layerMask))
{
//不再处于障碍物上方
isHoverVaultObj = false;
//已经越过障碍物
isOverVaultObj = true;
//缓存中心点
cacheCcCenterWhenOver = cc.center;
}
else
{
//从手的位置向下进行检测 获取手的目标IK位置
if (Physics.SphereCast(handTrans.position, handRadius, -transform.up, out hoverHit, heightDelta, layerMask))
{
//调试
Debug.DrawLine(hoverHit.point, hoverHit.point + hoverHit.normal);
//目标IK坐标
targetHandIkPosition = hoverHit.point + transform.forward * handRadius;
//目标IK旋转
targetHandIkRotation = Quaternion.FromToRotation(handTrans.right, hoverHit.normal);
}
}
在设置IK目标位置和旋转之前,需要先设置手部IK的权重,并且需要注意所有IK的API调用需要写在On Animator IK
方法中:
private void OnAnimatorIK(int layerIndex)
{
if (targetHandIkPosition != Vector3.zero)
{
float normalizedTime = vaultTimer / vaultDuration;
//手IK权重
animator.SetIKPositionWeight(AvatarIKGoal.LeftHand, normalizedTime);
animator.SetIKRotationWeight(AvatarIKGoal.LeftHand, normalizedTime);
//当前位置
Vector3 currentHandIkPosition = animator.GetIKPosition(AvatarIKGoal.LeftHand);
Quaternion currentHandIkRotation = animator.GetIKRotation(AvatarIKGoal.LeftHand);
//插值
animator.SetIKPosition(AvatarIKGoal.LeftHand, Vector3.Lerp(currentHandIkPosition, targetHandIkPosition, normalizedTime));
animator.SetIKRotation(AvatarIKGoal.LeftHand, Quaternion.Lerp(currentHandIkRotation, targetHandIkRotation * currentHandIkRotation, normalizedTime));
}
//越过之后取消IK设置
if (isVaulting && isOverVaultObj)
{
//重置
targetHandIkPosition = Vector3.zero;
targetHandIkRotation = Quaternion.identity;
//手IK权重
animator.SetIKPositionWeight(AvatarIKGoal.LeftHand, 0f);
animator.SetIKRotationWeight(AvatarIKGoal.LeftHand, 0f);
}
}