前言
本章来实现爬墙的功能,示例如下:
一、动画设置
enter为上墙动作,hold为悬挂动作,exit为往上爬后站立,left和right为横向攀爬,fall为往下落,land为落地动作。状态转换关系:fall->land为接触地面,其他转换均为动画结束后转换。横向攀爬为inplace动作,其他均为rootMotion,可以根据自己需求进行设置。
二、墙体检测
1、原理
首先可以参考我上一期教程
Unity Arpg学习日志五:跑酷攀爬功能———低障碍物撑跳、翻越(检测算法的改进)
与上期不同,上期翻越低障碍物要考虑厚度,这次不用考虑,故我们采用Physics.CapsuleCast向前检测,最先获取前提的前点,检测原理如下
二次检测
2、参数
这里是用到的参数
3、代码
protected override void Check()
{
Vector3 p1 = player.position + player.up * (minValueHeight + capsuleCastRadius);
Vector3 p2 = player.position + player.up * (maxValueHeight - capsuleCastRadius);
//debug为我自己编写的一个绘制检测物体的脚本,大家可以从上一期找到
if (debug)
{
debug.DrawCapsule(p1+player.forward*maxCheckDistance, p2+player.forward*maxCheckDistance, capsuleCastRadius, Color.black);
}
//检测前方是否有合适墙体
if (Physics.CapsuleCast(p1,p2,capsuleCastRadius,player.forward,out RaycastHit checkHit,maxCheckDistance,mask,QueryTriggerInteraction.Collide))
{
//没有爬出边界
outWall = false ;
//碰头
if(Physics.Raycast(player.position+player.up*HeadBumpHeight,player.forward,maxCheckDistance+1f,Physics.AllLayers))
{
//检测失败
Debug.Log("碰头了");
isClimbUp=false;
SetClimbTypeActive(ClimbType.Short, false);
}
else isClimbUp = true;//往上爬不碰头的话可以继续往上爬
//最高检测点
Vector3 startTop=checkHit.point;
startTop.y=player.position.y+maxValueHeight+capsuleCastRadius;
//绘制检测到的墙体信息
if (debug)
{
debug.DrawSphere(checkHit.point, capsuleCastRadius, Color.yellow);
}
//检测最高点
if (Physics.SphereCast(startTop,capsuleCastRadius,Vector3.down,out RaycastHit top,maxValueHeight-minValueHeight,mask,QueryTriggerInteraction.Collide))
{
//匹配点为顶点
matchTarget1 = top.point;
//绘制顶点和最高检测点
if (debug)
{
debug.DrawSphere(startTop, capsuleCastRadius, Color.red);
debug.DrawSphere(matchTarget1, capsuleCastRadius, Color.blue);
}
//检测成功
//修正玩家朝向,让玩家正面对墙体
playerDir = -checkHit.normal;
//Debug.Log("检测成功");
}
else
{
//检测失败
//Debug.Log("太高了");
}
}
else
{
//检测失败
//Debug.Log("没检测到");
outWall=true;//爬出墙外了
}
}
4、示例
三.enter与hold
代码如下(示例):
进入状态时直接播放enter动画
protected override void Enter()
{
base.Enter();
animator.Play("climb_short_enter");
//状态启用
isRun = true;
animator.applyRootMotion = true;
rb.isKinematic = true;
}
进行目标匹配
void AnimMatch()
{
Vector3 offset1 = player.forward * offsetType1.z + player.right * offsetType1.x + player.up * offsetType1.y;
Vector3 offset2 = player.forward * offsetType2.z + player.right * offsetType2.x + player.up * offsetType2.y;
if (animator.GetCurrentAnimatorStateInfo(0).IsName("climb_short_enter"))
{
//让玩家一直朝向墙体
player.forward = playerDir;
animator.MatchTarget(matchTarget1 + offset2, Quaternion.identity, AvatarTarget.RightHand, new MatchTargetWeightMask(Vector3.one, 0), 0.0f, 0.2f);
}
}
这里让玩家一直朝向墙体,在玩家在有一定角度的情况下进行攀爬可以尽量避免穿模,否则。。。。
当enter动画播放完后会进入hold的动画,表现为一直悬挂在墙面上
这时创建一个StateCheck()函数进行状态判定
void StateCheck()
{
float h = PlayerInputManager.Instance.MoveInput().x;
float v=PlayerInputManager.Instance.MoveInput().y;
if(animator.GetCurrentAnimatorStateInfo(0).IsName("climb_short_hold"))
{
if (h == 0 && v > 0 && PlayerInputManager.Instance.JumpInput() > 0.99f)
{
animator.Play("climb_short_exit");
}
else if ((h == 0 && v < 0)&&PlayerInputManager.Instance.JumpInput()>0.99f)
{
animator.Play("climb_short_fall");
//刚体在这里进行特别处理,否则无法检测已经落地
rb.isKinematic = false;
}
else if(v==0&&h>0)
{
animator.Play("climb_short_right");
}
else if (v == 0 && h < 0)
{
animator.Play("climb_short_left");
}
}
}
状态判定和目标匹配均在帧更新中进行
public override void OnLateUpdate()
{
base.OnLateUpdate();
if (isRun)
{
AnimMatch();
StateCheck();
}
}
演示
四.exit和fall->land
1、exit
当完成输入后,动作由悬挂转为爬上墙体,目标匹配如下
else if (animator.GetCurrentAnimatorStateInfo(0).IsName("climb_short_exit"))
{
float time=animator.GetCurrentAnimatorStateInfo(0).normalizedTime;
if(time<0.5f)
{
animator.MatchTarget(matchTarget1, Quaternion.identity, AvatarTarget.LeftFoot, new MatchTargetWeightMask(Vector3.one, 0), 0.1f, 0.9f);
//手部IK大家可以先不管
OpenHandIk(0.5f, matchTarget1 +offset1 , 0.5f, matchTarget1 + offset1);
}
else if(time >0.5f&&time<0.6f)
{
//手部IK大家可以先不管
OffHandIk();
}
else if(time>0.99f)
{
Exit();
//执行退出状态,回到Idle或其他状态
}
}
在代码中可以看到我对手部动画进行了IK,原因是
可以看到,即使我使用了目标匹配,但可能是由于动画的原因,还是会出现非常明显的穿模现象,这时进行手部IK可以大幅改进,但是由于我的IK技术不成熟,手臂动画还是会显得比较扭曲。。。
关于IK控制可以参考我这篇文章
Unity插件学习之Final IK:(一)Full Body Biped IK的简单使用
我使用了一个单例类脚本专门控制各部位Ik,大家可以参考参考
public void OpenHandIk(float leftHandWeight, Vector3 leftHandTarget, float rightHandWeight, Vector3 rightHandTarget)
{
//权重
fullBodyBipedIK.solver.leftHandEffector.positionWeight = leftHandWeight;
//位置
fullBodyBipedIK.solver.leftHandEffector.target.transform.position = leftHandTarget;
fullBodyBipedIK.solver.rightHandEffector.positionWeight = rightHandWeight;
fullBodyBipedIK.solver.rightHandEffector.target.transform.position = rightHandTarget;
}
public void OffHandIk()
{
fullBodyBipedIK.solver.leftHandEffector.positionWeight = 0f;
fullBodyBipedIK.solver.rightHandEffector.positionWeight = 0f;
}
2、fall->land
当完成输入后,动作由悬挂转为下落动作,当下落时判定落地后,则转换为落地动作,地面检测代码大家自行完成,只需要给动画状态机提供地面检测的参数即可
fall时为RootMotion 不需要目标匹配,然后再land动画结束后,退出状态
else if (animator.GetCurrentAnimatorStateInfo(0).IsName("climb_short_land"))
{
float time = animator.GetCurrentAnimatorStateInfo(0).normalizedTime;
if(time>0.99f)
{
Exit();
//执行退出状态
}
}
演示
五、横向攀爬
StateCheck()中关于横向攀爬
else if((animator.GetCurrentAnimatorStateInfo(0).IsName("climb_short_left")|| animator.GetCurrentAnimatorStateInfo(0).IsName("climb_short_right")))
{
//如果爬出边界则下落
if(outWall)
{
animator.Play("climb_short_fall");
//刚体在这里进行特别处理
rb.isKinematic = false;
}
//防止按住按键时多次重复播放动画
if(animator.GetCurrentAnimatorStateInfo(0).normalizedTime > 0.8f)
{
if (v == 0 && h > 0)
{
animator.Play("climb_short_right");
}
else if (v == 0 && h < 0)
{
animator.Play("climb_short_left");
}
}
}
关于位移只是简单的处理
else if(animator.GetCurrentAnimatorStateInfo(0).IsName("climb_short_right"))
{
//让玩家一直朝向墙体
player.forward = playerDir;
player.position += player.right * 2f*Time.deltaTime;
}