实用指南:【unity实战】用unity实现效果极好的第三人称跳跃功能【CharacterController | Input System | BlendTree】

前言

在Unity中实现一个看似简单的“跳跃”功能,很多开发者可能会认为只需为角色添加一个向上的速度即可。然而,在实际开发中,尤其是使用CharacterController组件时,我们往往会遇到各种意想不到的挑战:角色移动显得僵硬、跳跃手感不佳、落地判定不准确,或是跳跃动画与逻辑难以完美同步。

本文将带你深入探究如何使用Unity的CharacterController、新的Input System输入系统以及Animator中的BlendTree,一步步实现一个手感顺滑、反馈精准且动画流畅的第三人称跳跃效果。无论你是刚刚接触Unity的新手,还是希望优化已有功能的开发者,相信本文都能为你提供有价值的参考和解决方案。

实战

1、开始项目

人物基本移动和相机跟随功能之前做过很多,但就不重复介绍了,不了解的可以查看我之前的一些文章:

3、地面检测
public bool isGrounded;
isGrounded = characterController.isGrounded;

不过他的使用远没有这么简单,具体注意事项可以参考我之前的文章:【零基础入门unity游戏开发——unity3D篇】unity CharacterController 3D角色控制器最详细的使用介绍,并实现俯视角、第三人称角色控制
在这里插入图片描述

3、为角色控制器提供一个向下的重力或者加速度
//重力
public float gravity = -9.8f;
//重力
void CaculateGravity()
{
if (character.isGrounded)
{
//地面也默认施加一个向下的重力
VerticalVelocity = gravity * Time.deltaTime;
return;
}
VerticalVelocity += gravity * Time.deltaTime;
}

在Update里我们调用这个方法

效果
在这里插入图片描述

4、实现跳跃

实现跳跃其实也就是在垂直方向施加一个力,这里有一个通用的近似物理效果的一个运动公式
在这里插入图片描述

定义Jump方法

//当前垂直方向上的速度
float VerticalVelocity;
//跳跃高度
public float jumpHeight = 1.5f;
void Jump()
{
if (isGrounded && inputController.isJump)
{
VerticalVelocity = Mathf.Sqrt(-2 * gravity * jumpHeight);
}
}

然后在动画位移的方法里,我们把垂直速度加上去

private void OnAnimatorMove()
{
Vector3 deltaPosition = animator.deltaPosition;
deltaPosition.y = VerticalVelocity * Time.deltaTime;
character.Move(v);
}

效果
在这里插入图片描述

5、播放跳跃动画

我们看看现在的跳跃动画
在这里插入图片描述
当然这不是我们要的,Unity的新手在为跳跃行为播放动画时常常会陷入一个误区:

  • 误区一:他们总是希望能够播放一套完整的跳跃动画,然而在实际开发中完整的跳跃动画,想要适配角色的实际行动是非常困难的,所以大家经常采用的方式是根据玩家角色的某些参数,在一些定格动画之间做混合。如果想要在空中播放一些特殊动画的话,比如魔兽世界里常见的翻跟头,可以在起跳之后达到某个特定条件时再做动画切换
  • 误区二:为跳跃做一个起跳动作,这样做会严重影响玩家的操作手感,玩家希望在按下跳跃键后,角色应该能够立刻跳起,而不是蹲下蓄力再跳起(当然某些需要蓄力跳跃的特殊游戏除外)。

所以我们需要剪切出这里的起跳、空中和下落动画,这三个动画基本就是定格动画(只需要一帧即可,太长了容易抖动)
在这里插入图片描述
打开我们角色的动画状态机,把它们放进1D BlendTree中,跳跃混合树设置如下:
在这里插入图片描述

在这里插入图片描述

这里jumpSpeed参数我们设置成5.4,-5.4的原因是我们在前面的Jump方法中设置了跳跃施加到角色上的初始垂直速度(也就是VerticalVelocity)约等同于5.4f

修改代码如下

void Jump()
{
if (isGrounded && inputController.isJump)
{
VerticalVelocity = Mathf.Sqrt(-2 * gravity * maxHeight);
}
animator.SetBool("isJump", !isGrounded);
animator.SetFloat("jumpSpeed", VerticalVelocity, 0.1f, Time.deltaTime);
}

效果
在这里插入图片描述

6、移动跳跃

我们可以看到,在空中的时候,角色在水平平面上的移动速度明显是不正常的,这是因为我们使用Root Motion里的运动信息来移动角色,而这个跳跃动画在水平方向上的运动量,明显是不符合我们的需求的
在这里插入图片描述
最简单的选择就是角色离地时,使用角色留在地面的最后一帧的速度,但是这样做的话效果会很差,因为你很难确定最后一帧的速度,不受一些奇怪的地形或者玩家奇怪操作的影响。

为了解决这一问题我们选择使用离地之前几帧的玩家的速度的平均值来当做玩家在空中的速度,这样可以尽量避免一些不确定的因素对速度的影响

// 定义缓存大小,设为3表示缓存最近3帧的速度数据
static readonly int CACHE_SIZE = 3;
// 用于存储速度历史的缓存数组
Vector3[] velCache = new Vector3[CACHE_SIZE];
// 当前缓存索引,指向下一个要写入的位置
int currentCacheIndex;
// 计算得到的平均速度
Vector3 averageVel;
// 计算平均速度的方法
Vector3 AverageVel(Vector3 newVel)
{
// 将新速度值存入缓存
velCache[currentCacheIndex] = newVel;
// 索引递增
currentCacheIndex++;
// 使用取模运算实现循环缓冲区,索引在0到CACHE_SIZE-1之间循环
currentCacheIndex %= CACHE_SIZE;
// 初始化累加变量
Vector3 average = Vector3.zero;
// 遍历缓存中的所有速度值
foreach (var vel in velCache)
{
// 累加速度值
average += vel;
}
// 返回平均值(总和除以缓存大小)
return average / CACHE_SIZE;
}
// Unity动画系统每帧调用的方法
private void OnAnimatorMove()
{
// 检查角色是否在地面上
if (isGrounded)
{
// 获取动画系统计算的位移增量(当前帧相对于上一帧的位移)
Vector3 deltaPosition = animator.deltaPosition;
// 用垂直速度替换Y轴位移(保持物理正确性)
deltaPosition.y = VerticalVelocity * Time.deltaTime;
// 移动CharacterController组件
characterController.Move(deltaPosition);
// 如果是普通地面位移,计算平均速度用于后续滞空状态
averageVel = AverageVel(animator.velocity);
}
else
{
// 在滞空状态下使用之前计算的平均速度
var deltaPosition = averageVel * Time.deltaTime;
// 添加上垂直方向的位移
deltaPosition.y = VerticalVelocity * Time.deltaTime;
// 移动角色控制器
characterController.Move(deltaPosition);
}
}

为什么这里要平均速度((animator.velocity)乘以时间,而不是直接使用平均位移(animator.deltaPosition),这是因为Delta Position会受帧率的影响帧率越高,Delta Position越小,帧率越低Delta Position越大。如此以来我们计算出的所谓平均位移是完全不具备参考性的,而且如果跳跃前和跳跃后的帧率不一致,我们使用跳跃前的帧率计算出的,这个所谓平均位移来推算跳跃后的位移,将会得到一个非常不确定的结果。

效果
在这里插入图片描述

7、原地起跳会位置会逐渐偏移

如果你发现角色站在原地起跳时,位置会逐渐偏移。这是因为动画本身的Root motion速度会对角色产生影响,我们选中这个落地动画可以看到它是有速度的。Root motion的使用具体可以参考:【unity实战】Animator启用root motion根运动动画,实现完美的动画动作匹配

那么要解决这个问题的话,我们就记得勾选它的Bake into pose即可。
在这里插入图片描述

8、解决跳跃轻飘飘

目前这个跳跃的动作,感觉有一些轻飘飘的,为了应对这样的问题,在大多数包含跳跃的游戏中角色下落的速度要比跳起的速度快

float fallMultiplier = 1.5f;
void CaculateGravity()
{
if (isGrounded)
{
//施加一个向下的重力
VerticalVelocity = gravity * Time.deltaTime;
return;
}
//这里的意思是说角色下降阶段受到的重力是上升阶段的1.5倍
if (VerticalVelocity <= 0)
{
VerticalVelocity += fallMultiplier * gravity * Time.deltaTime;
}
else
{
VerticalVelocity += gravity * Time.deltaTime;
}
}

效果
在这里插入图片描述

9、优化跳跃姿势

接下来我们看一看跳跃的姿势,目前这个跳跃的姿势很单调,偶尔跳一次也就算了,如果玩家闲着没事跳着玩的话,这个动作看上去就很单调。

我们来想办法解决一下,把这个滞空的blend tree从单变量混合数改成双变量混合数,新建一个float参数命名jumpPosture,对应到这个混合数的x轴上,y轴我们就沿用之前的垂直速度
在这里插入图片描述
这样我们每次跳越时,只要随机指定一个左右角变量,就可以让每次起跳的动作都不一样

void Jump()
{
if (isGrounded && inputController.isJump)
{
VerticalVelocity = Mathf.Sqrt(-2 * gravity * maxHeight);
//随机左右脚切换起跳
float jumpPosture = Random.Range(-1f, 1f);//注意这里参数用浮点数
animator.SetFloat("jumpPosture", jumpPosture);
}
animator.SetBool("isJump", !isGrounded);
animator.SetFloat("jumpSpeed", VerticalVelocity, 0.1f, Time.deltaTime);
}

效果
在这里插入图片描述

10、新增移动跳跃动画

大家可以发现我们的这个动作在原地起跳的时候,看起来没有什么问题,但是在移动时,尤其是高速移动时,这个跳越动画就显得有那么一些不协调了。毕竟没有谁会在高速运动跳越的同时,双脚的前后位置还是一致的。
在这里插入图片描述

我们可以添加新的移动跳跃动画(最好是左右脚前后不一致的跳跃动画),这里为了节约时间,我就直接使用原来的动画好了。
在这里插入图片描述
科普blend-tree的一个小知识点,放在同一个blend-tree里的动画,应该具有相同的动画特征
什么是所谓的动画特征?我们用双腿来做一个简单的说明,如果两个动画在其整体进度的相同位置,是同一只脚落地的话,我们就可以认为这两个动画,在腿部具有相同的动画特征。

  • 比如目前的奔跑动画,在进度50%的地方是左脚落地,进度0%的地方是右脚落地
  • 行走的动画也一样,在进度50%的地方左脚落地,0%的地方右脚落地
  • 冲刺动画也是
    在这里插入图片描述

那么我们要如何确定播放左脚在前,还是右脚在前的跳跃动画呢?我们制定一个简单的规则:起跳时如果左脚在右脚的后面,那么起跳后就播放左脚在前面的动画;起跳时如果右脚在左脚的后面,那么起跳后我们就播放右脚在前面的动画。

具体到目前的例子,我们可以认为当当前的动画播放到50%之前的位置时,右脚在后,所以起跳后应该播放右脚在前的动画。动画播放到50%和100%之间时,左脚在后,所以起跳后应该播放左脚在前的动画。

修改代码如下

void Jump()
{
if (isGrounded && inputController.isJump)
{
VerticalVelocity = Mathf.Sqrt(-2 * gravity * maxHeight);
// 获取当前动画播放进度
float feetTween  = animator.GetCurrentAnimatorStateInfo(0).normalizedTime % 1;
// 根据动画进度决定起跳姿势
// 前半周期为1(右脚起跳),后半周期为-1(左脚起跳)
float jumpPosture = feetTween  < 0.5f ? 1 : -1;
// 如果角色正在移动
if (isMove)
{
jumpPosture *= 2;
}
else
{
// 随机左右脚切换起跳
jumpPosture *= Random.Range(-1f, 1f);
}
animator.SetFloat("jumpPosture", jumpPosture);
}
animator.SetBool("isJump", !isGrounded);
animator.SetFloat("jumpSpeed", VerticalVelocity, 0.1f, Time.deltaTime);
}

其实我们还可以做的更加细致,比如行走时:jumpPosture=±2;奔跑时:jumpPosture=±3;冲刺时:jumpPosture=±4;等等。至于当前在什么状态,我们可以用移动速度来判断,不过这里我们就不做这么细致了。

效果
在这里插入图片描述

11、角色走下坡下楼梯问题

目前角色走下坡或者下楼梯时,会出现短暂的浮空,然后频繁切换到浮空动画,这不是我们要的
在这里插入图片描述
我想到的解决方案是从角色的底部向下做射线检测,如果一定的距离内有物体(地面),则不切换到悬空状态

//玩家是否可以跌落
bool couldFall;
//跌落的最小高度,小于此高度不会跌落
float fallHeight = 0.5f;
couldFall = !Physics.Raycast(transform.position, Vector3.down, fallHeight);
animator.SetBool("isJump", !isGrounded && couldFall);

效果
在这里插入图片描述

12、优化从高空跌落

我们可以为高空跌落配置单独的落地动画
在这里插入图片描述
然后修改代码,在下落到接触地面时,设置jumpPosture=0

12、大跳和小跳
void CaculateGravity()
{
if (playerPosture!=PlayerPosture.Midair)
{
VerticalVelocity = 0f;
VerticalVelocity = gravity * Time.deltaTime;
//isJumping = false;
return;
}
//这里的意思是说角色下降阶段受到的重力是上升阶段的1.5(fallMultiplier)倍
if (VerticalVelocity <= 0 || !isJumping)
{
VerticalVelocity += fallMultiplier*gravity * Time.deltaTime;
}
else
{
VerticalVelocity += gravity * Time.deltaTime;
}
}

如果玩家没有一直按着跳跃键,我们就让上升受到的重力也是1.5倍

13、其他优化
  • 跳跃cd时间,多段跳
  • 为了提升玩家的跳跃体验或者说跳跃手感,比如你可以区分一下,玩家下落的时候是跳跃之后的下降,还是直接从高速摔下来,并为他们准备不同的动画
  • 如果不是跳跃落地的话,你也不需要为他准备跳跃的CD时间,你还可以辨别一下,角色是不是跳到了更高的地方,如果是的话,你就把落地动画更换为一个类似爬上来的动画
  • 又比如说玩家前面有一个障碍物需要跳过,但是玩家按下跳跃键的时间过早了,这样他本来是没有办法越过前面的障碍物的,此时你可以人为的延迟跳跃的出发时间,让玩家认为是他按下跳跃键的时机刚刚好,才能够正好地越过这个障碍物
  • 预输入:比如你的角色可以连续跳跃,那么有的玩家就会在角色落地前,就按下下一次跳跃,为了保障玩家的手感,你可以记录一下落地前0.几秒的玩家跳跃操作,并在落地后立即出发跳跃
  • 土狼时间:还有玩家有的时候会跑的太快,以至于角色已经冲出悬崖了,才按下跳跃键,那么你也可以设置一个时间,比如0.1秒,让玩家在冲出悬崖后的短时间内,仍然可以使用跳跃功能

参考

https://space.bilibili.com/269749034


专栏推荐

地址
【unity游戏开发入门到精通——C#篇】
【unity游戏开发入门到精通——unity通用篇】
【unity游戏开发入门到精通——unity3D篇】
【unity游戏开发入门到精通——unity2D篇】
【unity实战】
【制作100个Unity游戏】
【推荐100个unity插件】
【实现100个unity特效】
【unity框架/工具集开发】
【unity游戏开发——模型篇】
【unity游戏开发——InputSystem】
【unity游戏开发——Animator动画】
【unity游戏开发——UGUI】
【unity游戏开发——联网篇】
【unity游戏开发——优化篇】
【unity游戏开发——shader篇】
【unity游戏开发——编辑器扩展】
【unity游戏开发——热更新】
【unity游戏开发——网络】

完结

好了,我是向宇,博客地址:https://xiangyu.blog.csdn.net,如果学习过程中遇到任何问题,也欢迎你评论私信找我。

赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!
在这里插入图片描述

posted @ 2025-10-26 17:01  ycfenxi  阅读(1)  评论(0)    收藏  举报