Unity 2D跳跃手感从零调优:土狼时间、跳跃缓冲与角色控制器实战
2026/9/18 20:41:40 网站建设 项目流程

简介:一份Unity2D平台跳跃游戏demo源码,面向使用Unity2018及以上版本、希望从零掌握2D跳跃游戏核心开发的读者。资源以角色控制器、物理引擎、碰撞检测、跳跃逻辑、动画系统、UI与音频管理等模块组织,适合初学者对照学习,也适合开发者快速扩展玩法。压缩包共3810个文件,以bin、meta、asset、dll、cs、prefab等类型为主,完整包含工程配置、C#脚本、预制体与插件依赖,整体仅13.94MB,结构紧凑。已有2239人学习下载,热度良好。通过研读源码,可理解CharacterController2D/Rigidbody2D的移动实现、OnCollisionEnter2D等碰撞事件、Animator动画切换,以及简单UI和流程控制的具体写法,可在此基础上延伸二段跳、关卡与音效设计,实用性较强。资源内的代码注释与模块划分便于检索,可帮助开发者快速定位跳跃、物理、动画等关键功能,直接作为课程设计或个人练手项目的起点。 不知道你有没有过这种经历:看着别人做的Unity 2D跳跃游戏Demo,觉得“不就是跳一下嘛”,但轮到自己动手写的时候,角色要么飘得像个气球,要么落地跟踩了冰一样,怎么调都不对劲。我前前后后做过几个Unity 2D横版跳跃的小Demo,踩了无数坑之后才算摸清了里面的门道。这篇博文就把我整理出来的那套可复用的源码结构、参数调优方法和排坑经验一次性写出来,给正在做同类Demo的朋友一个直接能抄的参考。

先交代一下这个Demo的定位:它是一个用Unity 2021.3 LTS制作的标准2D横版跳跃Demo(Built-in渲染管线),核心玩法就是“移动+跳跃+落地”,目标只有一个——把角色控制和跳跃手感调到接近《空洞骑士》那种“黏手”的舒服程度。它适合三类人:刚学Unity想搞懂2D物理的新手、想快速搭平台跳跃原型的独立开发者、以及做面试作品需要一套干净可扩展控制代码的同学。

1. Demo定位与整体设计:别一上来就写代码

1.1 为什么“跳跃”值得单独做一个Demo

很多人觉得跳跃是2D游戏里最简单的东西,其实恰恰相反。平台跳跃、动作冒险、甚至不少Roguelike,角色的移动手感直接决定了玩家会不会坚持玩下去。跳跃这件事涉及输入响应、刚体物理、碰撞检测、动画切换四套系统协同,任何一个环节有延迟或者参数不合理,手感就会露馅。所以拿跳跃Demo练手,本质上是把游戏物理和输入反馈的核心过一遍,后面做任何带操作的角色都能复用这套思路。

1.2 技术选型与项目配置

我做这个Demo时用的Unity 2021.3 LTS,虽然是两年前的版本,但胜在稳定,社区资料也多。项目模板选2D Core,渲染管线用默认的Built-in,没有上URP——因为Demo不涉及复杂光照,Built-in启动快,打包体积也小。如果你是新项目,直接用Unity 6或2022 LTS也完全没问题,核心代码基本通用。

关键配置注意几点:导入素材后,记得把Sprite的PPU(Pixels Per Unit)统一,我用的16x16像素的素材,PPU设为16,保证1个单位长度正好对应16个像素,后续调碰撞体和物理参数都是按“单位”算,不会对不上。

2. 物理与碰撞:2D跳跃的地基

2.1 为什么移动得用Rigidbody2D的velocity,而不能直接改Transform

新手最容易犯的错就是用Transform.position去移动角色。这样做的结果是什么?角色和地图碰撞体完全不认账,会直接穿墙,因为Transform不会参与物理模拟。正确做法是给角色挂一个Rigidbody2D组件,用物理引擎去驱动移动。

但这里有个细节:Rigidbody2D有三种刚体类型——Dynamic、Kinematic和Static。角色身上必须用Dynamic(动态刚体),因为它需要响应重力和碰撞;地面和平台用Static(静态刚体),因为它们是固定不动的;Kinematic适合用来做移动平台,它不会被重力影响,但能推动Dynamic刚体,这个后面做移动平台时可以玩出很多花样。

移动的核心代码是给刚体的velocity直接赋值,而不是用AddForce:

rb.velocity = new Vector2(moveInput * moveSpeed, rb.velocity.y);

为什么这样写?因为AddForce是累加力,每帧都会加一次,最终速度和期望值差异很大,调试起来脑壳疼。直接给velocity赋值则相当于“每帧重新指定速度”,结果可控,是2D平台跳跃里最主流的做法。记住一个原则:水平方向用velocity控制,垂直方向的重力交给物理引擎,不要手动改rb.velocity.y,否则重力效果会被破坏。

2.2 Collider2D怎么选,物理材质要不要加

角色的碰撞体我推荐用CapsuleCollider2D(胶囊体),因为它模拟人形轮廓最准确,站在平台边缘时不容易“挂”在半空。但要注意,如果角色缩放比例不统一,胶囊体半径和高度要手动调好,让碰撞体刚好包住Sprite的主体,别让碰撞范围超出贴图太多,否则会出现“明明站得很远却被碰飞”的怪事。

地面和平台用BoxCollider2D就够了。还有一个容易忽略的东西:Physics Material 2D(物理材质)。角色默认的摩擦力和弹性如果不处理,会遇到两个问题:一是站在斜坡上会慢慢滑下去(摩擦力太小),二是从高处落下会轻微反弹(弹性不是0)。解决办法是在Project窗口右键创建两个Physics Material 2D,一个给角色用(Friction=0,Elasticity=0),一个给地面用(Friction=1,Elasticity=0)。你可能要问了:角色摩擦力调0,站斜坡上不会滑吗?不会,因为我们的移动是直接写velocity的,摩擦力对角色几乎不起作用,反而是碰撞体本身的形状决定它在斜坡上的表现,这个坑后面还会展开讲。

3. 角色控制器:手感才是灵魂

3.1 移动逻辑与起跳前摇的补偿

基础移动很简单,但要让手感“不飘”,必须处理加速和停止的问题。直接写velocity的时候,角色的起停是瞬时的,虽然很跟手,但在高速移动时会显得僵硬。我采用的方案是:用Mathf.Lerp对目标速度做插值,相当于给起停加了一个极短时间的过渡:

float targetSpeed = moveInput * moveSpeed; float acceleration = Mathf.Abs(targetSpeed) > 0.01f ? accelAirborne : accelGrounded; rb.velocity = new Vector2( Mathf.Lerp(rb.velocity.x, targetSpeed, acceleration * Time.fixedDeltaTime), rb.velocity.y );

因为这是Demo,我没有引入太多动作游戏的复杂参数,但加了一组“起跳前摇”的设计:主角在起跳前有0.05秒的准备动作,这是通过Animation Event实现的,起跳动作的最后一帧触发跳跃逻辑。这个细节让跳跃显得更有力度,而不是“一按就起飞”。

3.2 地面检测:别用碰撞体判断“有没有踩地”

很多新手写跳跃时会用碰撞回调OnCollisionEnter2D来判断角色是否在地面上。这个做法最大的问题是:回调触发有延迟,而且无法区分“脚下”和“侧身”接触。角色站在墙边时,侧面碰到墙也算碰撞,跳跃就会变成“蹭墙跳”,体验很怪。

我的做法是单独放置一个空物体作为GroundCheck点,放在角色底部中心,略微超出碰撞体下边缘一点点(大约0.05个单位),然后每帧用Physics2D.OverlapCircle检测这个点周围是否碰到地面层:

isGrounded = Physics2D.OverlapCircle(groundCheck.position, checkRadius, groundLayer);

这里三个参数很关键:GroundCheck点的位置、检测半径(我设为0.1)、以及地面层LayerMask。LayerMask的作用是把检测范围限定在“地面”这一个Layer上,玩家和敌人放在别的层,这样角色踩在敌人头上时不会误判成踩到地面。在Unity里把地面物体都放到“Ground”层,然后在代码里引用这个层即可(这里我直接按层名索引)。

3.3 跳跃实现与可变跳跃高度

跳跃本质是一瞬间给刚体一个向上的速度。但具体给多少,需要算一下。假设我们要的角色最大跳跃高度是3个单位(以16x16像素素材来说,相当于跳3格高),重力缩放设为3(2D平台跳跃常用2~4这个区间),那么初始跳跃速度v可以用公式v = sqrt(2 * g * h)算出来,其中g = 9.81 * 3 = 29.43。算出来v约等于13.29。这就是你在代码里要填的jumpForce的初始值。

但光有固定跳跃高度不够,手感好的跳跃必须有“可变跳跃高度”——玩家轻点跳跃键跳得矮,长按跳得高。实现方式非常经典:按下跳跃键起跳后,如果发现跳跃键被松开了,就把向上的速度削减一半以上:

if (rb.velocity.y < 0 && !jumpHeld) { rb.velocity = new Vector2(rb.velocity.x, rb.velocity.y * 0.6f); }

要注意这个削减只发生在上升阶段,而且要在FixedUpdate里每一帧判断,这样角色就会迅速开始下落,实现“小跳”。数值我调的是0.6,相当于松开手之后只保留60%的向上速度,手感比较干脆;如果你想做偏“重”的角色,可以改成0.4,下落更敏捷。

3.4 跳跃手感三件套:跳跃缓冲、土狼时间、跳跃削减

这是整个Demo里最值钱的部分,也是我从几个成熟开源项目里学到的。三个概念的组合几乎能让任何平台跳跃游戏手感上一个台阶。

跳跃缓冲(Jump Buffer):玩家在落地前0.1秒按下跳跃键,系统记住这个输入,等角色一落地就立刻起跳。没有这个机制的话,玩家会感觉“我明明按了跳跃却没反应”,尤其是从平台边缘下落时特别明显。实现就一行:记录按下跳跃键的时间,在isGrounded为true时检查这个时间差是否小于0.1秒,是则触发起跳。

土狼时间(Coyote Time):角色走出平台边缘后0.1秒内,仍然允许起跳。这个机制解决的是“脚刚刚离开地面但视觉上还在平台边缘”的情况,没有它,玩家会在平台边缘反复起跳失败,非常挫败。实现方式和跳跃缓冲完全对称:记录isGrounded从true变为false的时间,在跳跃条件里检查。

这两个缓冲加起来,跳跃的“宽容度”就起来了。玩家体验到的不是“死板的物理判定”,而是一个“愿意配合你的系统”,这种隐性宽容度是手感口碑的关键。

4. 动画状态机与摄像机组装

4.1 Animator状态机的参数设计

动画这块我用的是Animator状态机,状态有Idle、Run、Jump、Fall四个。关键是状态切换参数:用一个Float类型参数speed(水平移动速度的绝对值)控制Idle和Run之间切换,用一个Bool类型参数isGrounded控制在地上和空中两套状态之间切换,再加一个Float类型参数verticalVelocity来判断Jump和Fall的过渡。注意阈值:speed大于0.1进入Run,verticalVelocity大于0.1进入Jump,小于-0.1进入Fall,别把阈值设成0,不然动画会在临界点来回横跳,特别丑。

4.2 状态切换的细节坑

状态机里有个特别容易踩的坑:Fall转Idle的时机。很多人直接在isGrounded变为true的瞬间切回Idle,结果角色在落地瞬间播放的“落地拉伸”动画压根没机会显示,直接被切走。我的处理方式是:落地后不立刻切Idle,而是先切到一个叫Land的短暂状态(大概0.1秒),在Land状态播放完毕后通过Mecanim的Has Exit Time属性自动回到Idle。这样落地动作的“重量感”就出来了,而且作为控制方,玩家在这0.1秒内不能重新起跳,反而更显真实。

4.3 摄像机跟随:用插值别看直接赋值

摄像机跟随看似简单,但直接设置transform.position = player.position会带来一个严重问题:摄像机移动完全刚硬,角色跳起下落时画面会跟着剧烈抖动。正确做法是用Vector3.Lerp或SmoothDamp做平滑跟随:

Vector3 targetPos = new Vector3(player.position.x, player.position.y, -10f); Vector3 smoothedPos = Vector3.SmoothDamp(transform.position, targetPos, ref velocity, 0.2f); transform.position = smoothedPos;

SmoothDamp的第四个参数是平滑时间,我调的是0.2秒,既跟手又不会抖。另外记得把摄像机的Z轴固定在-10(正交相机的默认距离),否则镜头会跑到场景看不清楚角色。

5. 常见问题与排查技巧实录

5.1 问题一:跳跃偶尔不触发,或者延迟特别大

排查思路从两个方向来:第一,检查GroundCheck点位置是否正确,我实测发现很多Demo里GroundCheck点放在了角色中心或者被碰撞体挡在内部,导致检测不到地面;第二,检查是不是没有加跳跃缓冲,玩家在离开平台边缘的瞬间按跳跃,按理说是不允许的,但玩家不这么想,他们会觉得“我按了还没跳”,所以至少加0.1秒的缓冲时间,能解决80%这种问题。

5.2 问题二:角色站在斜坡上会往下滑

如果是斜坡地形,只把碰撞体设为BoxCollider2D是不够的。BoxCollider和斜坡的接触面积小,角色容易在斜坡上“挂不住”,物理上会往下滑。解决办法有两个:一是用EdgeCollider2D或PolygonCollider2D贴合斜坡形状;二是把角色Collider换成CapsuleCollider2D,因为胶囊体的曲线部分和斜坡的接触更稳定。两个方案搭配使用效果最好。把角色的物理材质Friction调为0是我试过的另一个办法,实测对斜坡滑动有一定缓解,但最优解还是换碰撞体形状。

5.3 问题三:角色在高帧率下抖动,或者有延迟感

物理逻辑一定要写在FixedUpdate里,而不是Update。我见过很多新手把移动逻辑写在Update,导致帧率高的时候移动速度不一样,还叠加了物理引擎的FixedUpdate循环,产生“一卡一卡”的现象。标准写法是:在Update里读输入(Input.GetAxisRaw),在FixedUpdate里执行物理操作(改velocity)。这样输入是每帧读取的,物理是固定时间步长步进的,组合起来最平滑。

另外2D游戏还有一个经典抖动问题:角色跳到平台边缘时,镜头会上下跳。原因是相机跟随脚本里直接用了角色transform位置,而角色的碰撞体在碰到平台时会发生微小位移。治本的方法是给相机跟随加SmoothDamp插值,你甚至可以把平滑时间稍微加大到0.25秒,牺牲一点跟手度换来观感稳定。

5.4 问题四:动画和物理状态对不上

经常会出现“角色已经落在地上了,但播放的还是Jump动画”。这通常是动画状态机的切换条件和实际物理状态没同步。建议在Animator的每个相关状态里勾选“Can Transition To Self = false”,同时把isGrounded的更新放到Update而不是FixedUpdate里,因为动画是逐帧渲染的,物理是固定步长,如果动画读取的isGrounded还是上一帧的物理状态,就会出现一两帧的错位,人眼对动画错位非常敏感。

我把这堆坑都填完之后,角色的跳跃手感稳定在了“按一下就跳,松手就落,平台边缘还能补救”的状态。特别是那个土狼时间和跳跃缓冲的组合,让我第一次感觉到了“手感”这个词原来是可以被量化、被代码实现的。

如果你正在做自己的2D跳跃Demo,建议在把这些基础机制跑通之后,再往下面加一两个拓展玩法:比如二段跳、冲刺、踩怪反弹。到那个时候你会发现,前面打好的物理和手感地基,能让你的扩展代码少写好几倍。我在实际项目里已经把这套控制器直接迁移到了一个Roguelike原型里,只改了部分动画触发逻辑,核心的跳跃缓冲和土狼时间代码一行没动,这就是先把手感基础打牢的价值。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询