前排提示
代码部分会有部分保留空白,为后续的攀爬系统相关判定,暂不作解释。
在未输入任何其他指令时,人物会进入等待状态,等待状态下,每隔固定的时间,会播放一段等待的动画,播放完毕会恢回到普通状态,再次等待相同时间会循环;等待动画可以有多个每次,等待播放的等待动画随机;
当输入其他指令时,若当前处于等待或处于播放等待动画的状态,则立刻打断当前等待动作,优先执行其他指令
人物在地面上时,按下WASD
,输入移动指令;点按左ctrl
切换跑步/走路;移动时,可以进行跳跃,朝哪个方向移动,人物就会朝向哪个方向;W表示正前方,正前方由视角而定
当人物跳跃时,分两种情况,原地起跳/向移动方向跳跃。
原地起跳:跳跃后,未落地时,无法执行移动指令;
向移动方向跳跃:跳跃后,未落地时,无法执行移动指令,跳跃的瞬间,会沿着原本的移动方向跳跃一段距离;
时刻判断离地高度,如果离地高度接近于0
;
若离地高度不接近于0
,则判定离地高度是否高于设定阈值,如果低于,则判定为低空坠落;如果高于,则再次判断下坠速度,速度高于设定阈值时,判定为高空坠落;
各类参数定义
[Header("移动中")]
public bool IsMove;
[Header("是否允许移动")]
public bool canMove;
[Header("跑步速度")]
[Range(280, 320)]
public float runSpeed;
[Header("走路速度")]
[Range(80,120)]
public float walkSpeed;
[Header("旋转速度")]
public float rotSpeed = 17f;
[Header("移动平滑过渡时间")]
[Range(0,0.2f)]
public float moveDampTime;
[Header("地形层")]
public LayerMask environmentLayerMask;
private Animator ani;//动画组件
private Rigidbody rigidbody;//刚体组件
private JumpTest jumpTest;//跳跃脚本组件
private ClimbTest climbTest;//攀爬脚本组件
[HideInInspector]
public Vector3 moveDirection;//移动方向
private Vector3 upAxis;//Yaw轴
private Vector3 groundNormal;//地面法线
private ThirdPersonCamera thirdPersonCamera;//第三人称相机脚本组件
其他因素排除:重力变化;根运动;
var moveDirection = new Vector3(Input.GetAxis(GameConst.HORIZONTAL_AXIS), 0, Input.GetAxis(GameConst.VERTICAL_AXIS));
这里使用虚拟轴获取移动方向,由于获取的移动方向是以世界坐标系为准的,首先需要进行方向修正,修正为视角规定的正方向
///
/// 摄像机朝向-移动方向修正
///
private void MoveProcess()
{
Vector3 currentDir = Vector3.ProjectOnPlane(-thirdPersonCamera.myCurrentDir,upAxis);
float angle = Vector3.Angle(currentDir,Vector3.forward);
Vector3 cross = Vector3.Cross(Vector3.forward,currentDir);
if (cross.y < 0) angle = -angle;
moveDirection = Quaternion.AngleAxis(angle, upAxis) * moveDirection;
}
摄像机脚本中存储的当前当前方向的负值表示摄像机观测人物的方向向量,将此向量投影在地面上即可获得当前的正方向向量,获取此方向与世界坐标Z轴的夹角,并通过二者向量叉积判断左右方向,最终通过AngleAxis函数获得相对于世界y轴的四元数旋转,左乘移动方向,即可修正为当前视角的正方向向量;
当然仅仅是这样还不够,还需要考虑地面是否倾斜,如果地面倾斜,那么则需要根据地面倾斜角度再次修正移动方向
///
/// 地面坡度—移动方向修正
///
private void GroundProcess()
{
Vector3 origin = transform.position + GameConst.PLAYER_BOFY_OFFSET * Vector3.up;
RaycastHit hit;
Physics.Raycast(new Ray(origin, -upAxis), out hit,environmentLayerMask);
groundNormal = hit.normal;
moveDirection =Quaternion.FromToRotation(upAxis,groundNormal)*moveDirection;
}
这里首先需要通过射线检测地面,获取地面碰撞点hit
,在通过hit.normal
获得地面的法向量,通过地面法向量修正移动方向即可,如果不修正这个。
再然后就是修正人物的朝向为输入的移动方向
///
/// 人物朝向方向修正
///
private void RotationProcess()
{
Vector3 currentDir = Vector3.ProjectOnPlane(moveDirection, upAxis);
Quaternion readQuat = Quaternion.LookRotation(currentDir);
transform.rotation = Quaternion.Lerp(transform.rotation, readQuat, rotSpeed * Time.deltaTime);
}
修正人物朝向要排除垂直方向上的干扰,首先投影到地面上,在通过LookRotation
方法获取看向这个朝向的四元数
Lerp
给角色的rotation
即可,直接赋值的话感觉会很突兀;
这里还有一个与攀爬相关的墙体检测,这里我放到下一篇再讲,这里标记位置
//WallProcess//
由于这里不通过根运动移动,这里使用刚体进行移动,因此需要把方法放到FixedUpdate
里,而非Update
里,Update里则判断是否输入跑步/走路切换按键,来切换模式
private void Update()
{
if (Input.GetButtonDown(GameConst.MOVETYPECHANGE_BUTTON))
{
ani.SetBool(GameConst.ISWALK_PARAM, !ani.GetBool(GameConst.ISWALK_PARAM));
}
}
private void FixedUpdate()
{
if (!canMove) return;
moveDirection = new Vector3(Input.GetAxis(GameConst.HORIZONTAL_AXIS), 0, Input.GetAxis(GameConst.VERTICAL_AXIS));
if (moveDirection.magnitude > 0.2f)
{
MoveProcess();
GroundProcess();
WallProcess();
RotationProcess();
if (!ani.GetBool(GameConst.ISWALK_PARAM))
{
rigidbody.velocity = Vector3.ProjectOnPlane(moveDirection,upAxis) * runSpeed * Time.fixedDeltaTime+Vector3.up*rigidbody.velocity.y;
ani.SetFloat(GameConst.SPEED_PARAM, 1f);
IsMove = true;
}
else
{
rigidbody.velocity = Vector3.ProjectOnPlane(moveDirection, upAxis) * walkSpeed * Time.fixedDeltaTime + Vector3.up * rigidbody.velocity.y;
ani.SetFloat(GameConst.SPEED_PARAM, 1f);
IsMove = true;
ani.SetFloat(GameConst.SPEED_PARAM, 1f);
IsMove = true;
}
}
else
{
ani.SetFloat(GameConst.SPEED_PARAM, 0f,moveDampTime,Time.deltaTime);
IsMove = false;
}
}
这里首先判断canMove
是否能够移动,如果不能则返回;
在通过虚拟轴输入,判断是否输入虚拟轴,如果没有输入或者是误触,则将不移动,将动画设置为Idle
,我这里使用的是融合树,动画部分请阅者根据自己情况完成,动画控制器图,我放到最底下,仅供参考;
值得注意的时,将移动方向赋值给刚体速度时,需要除去垂直方向的速度赋值,并且将刚体原本的Y
轴速度保留。
如果不保留Y轴速度,那么移动时便无法跳跃;如果不除去垂直方向的速度赋值,那么在走斜坡的时候会出现抖动状况;
这样一来,为什么要做地面方向的修正呢,因为移动方向在后续需要通过他作攀爬系统相关的射线检测,如果不修正地面的朝向,攀爬系统中会出现很多BUG,具体下一篇在详解;
另外:代码中出现的GameConst类的相关变量解释:GameConst类为常数类,专门记录各类常数,不继承MonoBehaviour
各类参数定义
[Header("地形层")]
public LayerMask environmentLayerMask;
[Header("x 时间| y 弹跳力系数| z 方向力系数| w 时间偏移")]
public Vector4 arg;
[Header("上升力曲线")]
public AnimationCurve riseCurve = new AnimationCurve(
new Keyframe[] {new Keyframe(0,0),new Keyframe(0.5f,0.25f),new Keyframe(1,0.5f)}
);
[Header("方向力曲线")]
public AnimationCurve directionJumpCurce = new AnimationCurve(
new Keyframe[] {new Keyframe(0.5f, 1), new Keyframe(1, 0) }
);
[Header("曲线结束时间")]
public float curveEndTime;
[Header("是否在地面上")]
public bool isGround;
[Header("是否允许跳跃")]
public bool canJump;
[Header("延迟检测计时器")]
public float groundedDelay;
[Header("延迟检测时间")]
public float groundedDelayTime;
[Header("高空检测判定范围")]
public float highAltitude;
private Animator ani;//动画控制器组件
private Rigidbody rigidbody;//刚体组件
private MoveTest moveTest;//移动脚本组件
private ClimbTest climbTest;//攀爬脚本组件
private Vector3 upAxis;//Yaw轴
private Coroutine jumpCorotine;//跳跃协程
首先是最重要的高度判定
///
/// 检测是否在地面上
///
///
private bool GroundProcess()
{
Vector3 origin = transform.position + GameConst.GROUND_CHECK_ORIGION_OFFSET * Vector3.up;
RaycastHit hit;
Physics.Raycast(new Ray(origin, -upAxis), out hit,environmentLayerMask);
Physics.Raycast(new Ray(origin, -hit.normal), out hit, environmentLayerMask);
float height = Vector3.Distance(transform.position, hit.point);
if( height< 0.2f)
{
if (!climbTest.canClimb)
moveTest.canMove = true;
ani.SetBool(GameConst.ONLAND_PARAM, true);
return true;
}
if (height < highAltitude)
{
moveTest.canMove = false;
}
else
{
if (rigidbody.velocity.y < -4)
{
moveTest.canMove = false;
moveTest.wallDelayCounter = moveTest.wallDelayTime;
ani.SetBool(GameConst.ONLAND_PARAM, false);
ani.CrossFade(GameConst.FALL_STATE, 0f);
}
}
return false;
}
首先通过人物射向地面的射线获得地面hit
,再通过垂直射向地面的射线获得地面点,通过判断该点与角色坐标的距离,判断高度;如果距离接近于0
,则判断为位于地面,如果当前不处于攀爬状态则将移动脚本的canMove
设为true
表示可以移动,动画相关不作解释,阅者自己完成,后续不再赘述;如果高度不接近于0
,则判断高度是否达到设定阈值,如果未达到,则判断为低空坠落,并将canMove
设定为false
,表示此时无法移动;如果高度大于阈值,在继续判定坠落速度,如果达到设定阈值则判定为高空坠落,将canMove
设定为false
,其他的暂不作解释。
跳跃我们通过曲线实现
IEnumerator JumpCoroutine(Vector3 moveDiection,Vector3 upAxis)
{
groundedDelay = Time.maximumDeltaTime * groundedDelayTime;//延迟两帧
rigidbody.useGravity = false;
float t = arg.w;
do
{
float t_riseCurve = riseCurve.Evaluate(t);
float t_directionJumpCurve = directionJumpCurce.Evaluate(t);
Vector3 jumpForce = Vector3.Lerp(upAxis, -upAxis, t_riseCurve) * (arg.y);
Vector3 forward = Vector3.Lerp(Vector3.ProjectOnPlane(moveDiection,upAxis), Vector3.zero, t_directionJumpCurve) * arg.z;
rigidbody.AddForce(forward, ForceMode.Impulse);
rigidbody.AddForce(jumpForce, ForceMode.Impulse);
t += Time.fixedDeltaTime * arg.x;
yield return null;
} while (t < curveEndTime);
rigidbody.useGravity = true;
}
groundedDelay
为落地判定的延迟计时器,为什么要加计时器呢,后面再讲;
跳跃时,首先关闭重力,通过曲线的变化性受力模拟加速度变化。
两条曲线:一条表示上升力的变化,一条表示方向力的变化;
曲线需要阅者自己调试,通过调试才能的到效果完善的跳跃效果;
这里通过曲线与插值变化模拟受力的变化,并将力附加给刚体,在曲线结束时,重新开启重力;
上升力提供跳跃力,方向力提供惯性;
循环结束,恢复重力;
接着在Update里执行方法
private void Update()
{
if (!canJump) return;
isGround = GroundProcess();
if (groundedDelay > 0) isGround = false;//添加延迟检测,控制跳跃的时间,以此避免isGround提前变化
if (Input.GetButtonDown(GameConst.JUMP_BUTTON) && jumpCorotine == null && isGround)
{
jumpCorotine = StartCoroutine(JumpCoroutine(moveTest.moveDirection,upAxis));
}
else
{
if(groundedDelay>-1)
groundedDelay -= Time.deltaTime;
if (isGround && jumpCorotine != null)
{
if (rigidbody.useGravity = true)
{
StopCoroutine(jumpCorotine);
jumpCorotine = null;
}
}
}
}
这里先解释groundedDelay
的作用:用于落地复原的延迟,因为存在一种BUG,isGround提前变化或是来不及变化导致升天的问题或是无法跳跃的问题。
首先判断canJump
,如果为false
,则直接返回。如果此时在地面上,并且按下跳跃键,并且跳跃协程为空,则允许跳跃,开启协程;否则,首先随着时间扣除groundedDelay
并且判断如果当前在地面上并且协程不为空,则判断为跳跃已经落地,此时需要判断重力是否恢复,如果恢复,则停止协程,并将其置为空;为什么要判断重力是否恢复,这里防止重力未恢复就提前停止,导致跳跃僵硬并且防止重力已经恢复了而没有检测到;
这个相对简单,不多做解释,直接上代码
public class WaitTest : MonoBehaviour
{
[Header("等待动作切换时间间隔")]
public float waitTimeInterval;
private float waitTime = 0;
private float waitTimeCounter = 0;
private Animator ani;
private JumpTest jumpTest;
private MoveTest moveTest;
private ClimbTest climbTest;
private void Awake()
{
ani = GetComponent();
jumpTest = GetComponent();
moveTest = GetComponent();
climbTest = GetComponent();
}
private void Update()
{
if (jumpTest.isGround && !moveTest.IsMove && !climbTest.onWall)
{
Action_Wait();
}
else
{
ResetWait();
}
}
///
/// 重置等待动作
///
private void ResetWait()
{
waitTimeCounter = 0;
if (ani.GetCurrentAnimatorStateInfo(0).shortNameHash != GameConst.WAIT00_STATE)
{
ani.CrossFade(GameConst.WAIT00_STATE, 0.1f);
}
}
///
/// 待机动作
///
private void Action_Wait()
{
if (waitTimeCounter > waitTimeInterval)
{
Random random = new Random();
waitTimeCounter = 0;
waitTime = (float)random.NextDouble() * 5;
ani.SetFloat(GameConst.WAITTIME_PARAM, waitTime);
}
else
{
if (ani.GetCurrentAnimatorStateInfo(0).shortNameHash != GameConst.WAIT00_STATE)
{
ani.SetFloat(GameConst.WAITTIME_PARAM, 0);
}
else
{
waitTimeCounter += Time.deltaTime;
}
}
}
}
仅供参考