横版过关里那种"从下往上跳能穿过去、从上面掉下来能站住、站着按一下键还能掉下去"的浮空平台,几乎是2D平台跳跃的标配。我最早做这块的时候用了很笨的办法:给平台挂触发器,角色一进来就把碰撞体关掉,结果角色在半空被顶飞、贴着边缘抖、跳到一半突然掉下去,全来了。后来项目里统一换成 PlatformEffector2D,问题一下少了大半,剩下的都是参数和恢复时机上的细节。这篇就把我在横版过关项目里做单向通行(One Way Platform)的完整做法摊开讲:组件到底怎么挂、Used By Effector 为什么是最容易漏的一步、Surface Arc 这个看着像"角度装饰"的参数其实直接决定边缘手感、下穿逻辑怎么写才不抖不断,以及一份我自己用了很久的排查清单。内容适合已经摸过 Rigidbody2D 和 Collider2D、正在做2D平台跳跃的新手,也适合已经上了 Effector 但被"角色黏在平台侧边"折磨过的老手。
1. 单向通行的本质:需求先拆清楚再选方案
1.1 从"跳上去卡住"说起,一条平台其实有五个需求
很多人以为单向平台就是"从下面能穿",实际做起来会发现这是五个互相牵制的需求,少一个玩家就会觉得别扭。第一个,角色从下方起跳、以向上的速度接触平台时,必须完全忽略这次碰撞,不能有半点减速或者被顶一下的感觉。第二个,角色从上方下落、以向下的速度接触平台时,必须正常站住,并且站稳之后不能因为重力累积而缓慢下沉。第三个,角色站在平台上左右移动,走到平台边缘之外要自然掉落,不能"悬空踩着空气"。第四个,站着的时候按方向下加跳跃键,要能主动下穿,而且下穿过程中不能再被这块平台挡住。第五个,角色从平台侧面水平撞过去(比如从旁边的墙跳过来),不能被弹飞或者卡住不动。
这五条里,第三条和第五条是最容易被忽略的。我见过不少项目,前两条做得很顺,结果玩家在平台边缘反复横跳时会出现"半个身子悬在外面还能站着"的诡异画面,原因就是碰撞体比角色视觉宽,或者平台的 Composite Collider 合并后的轮廓比想象中大。所以动手之前,我建议先拿张纸把这五条列出来,做完之后逐条测,比事后到处救火省事得多。
1.2 三条常见路线的取舍,为什么我最后选了 Effector
做单向通行大致有三条路,每条我都实际用过,优缺点很明显。
| 路线 | 核心做法 | 优点 | 主要问题 |
|---|---|---|---|
| 触发器开关碰撞体 | OnTriggerEnter2D 里把平台 collider.enabled 设为 false | 上手最快,逻辑直观 | 物理帧与触发帧不同步,高速下落会直接穿过去;角色停在触发器范围内时碰撞体一直关着,人直接掉下去 |
| 射线检测改碰撞关系 | 每帧打射线判断角色在平台上还是下,动态调 IgnoreCollision 或换 Layer | 可控性极强,能做出各种花样 | 状态机很容易写乱,多平台叠加、平台移动、角色贴边时判断条件爆炸 |
| PlatformEffector2D | 引擎层按碰撞法线与相对运动方向做单向判定 | 几乎不用写代码,稳定,性能好 | 判定方向强依赖 transform.up,灵活性靠补丁补 |
第一条路的问题在于"时机"。触发器回调和物理求解不在同一拍上,角色速度一快,触发器的回调还没跑到,人已经穿过去了;而且角色站在平台上时,如果碰撞体是关的,重力会直接把人拽下去,你得再加一套"站住了就打开"的逻辑,越写越像在跟引擎打架。
第二条路不是不能用,如果你要做"只看角色脚底一小段"的精细判定,它确实更自由。但它本质上是自己实现一遍物理判定,边界情况多得吓人,我在一个项目里为了修"下穿后立刻被顶回去"的 bug 花了两天。
第三条路把判定交给引擎,我只要负责"什么时候允许穿"这一件事,工作量直接砍掉一大半。所以我的默认选择是 PlatformEffector2D,遇到它办不到的需求(比如同一块平台对玩家单向、对敌人实体),再在它上面打补丁。
1.3 组件挂上去没反应?先检查 Used By Effector
这是新手踩得最多的一脚,包括我自己。PlatformEffector2D 挂在平台上、参数全开,跳上去照样被挡住,翻半天文档才发现问题不在 Effector 上,而在 Collider2D 上那个不起眼的复选框:Used By Effector。
它没勾,Effector 就是一个纯装饰组件,引擎根本不拿它参与碰撞求解。所以每次我新建平台,动作顺序固定是:先加碰撞体,再把 Used By Effector 勾上,最后加 PlatformEffector2D。反过来做,很容易在中间某一步漏掉。另外还有两个隐性前提:一个碰撞体同一时间只能被一个 Effector 驱动,别想着叠两个;碰撞体不能是 Trigger,Trigger 根本不参与实体碰撞,Effector 自然也无从下手。这两点看起来是常识,但真出问题的时候,人往往先怀疑参数,最后才发现是这两个开关。
2. 平台怎么搭:组件关系与参数落地
2.1 挂载结构与三种平台的差异
静态平台的搭法最简单:一个空 GameObject 加 BoxCollider2D,勾上 Used By Effector,再挂 PlatformEffector2D,完事。平台本身不需要 Rigidbody2D,引擎会把它当作静态碰撞体处理,性能上是划算的。
移动平台就得多加一个 Rigidbody2D,Body Type 设成 Kinematic,Interpolate 打开。这里有个细节值得说:Kinematic 刚体移动时最好用MovePosition或者直接改linearVelocity(老版本叫velocity),别在 Update 里直接改 transform.position。直接改位置等于瞬移,物理系统拿不到速度信息,站在上面的角色不会被"带着走",表现就是人在移动平台上原地打滑。我一般会给移动平台配一个简单的位移组件,把速度算出来写进刚体,这样角色站在上面能被正确带飞。
瓦片地图做的平台要注意层级:Tilemap 上挂 TilemapCollider2D,再加 CompositeCollider2D 把碎块合成一个整体轮廓,勾 Used By Effector 的是CompositeCollider2D,不是 TilemapCollider2D。勾错的话要么完全没效果,要么只有部分瓦片生效,排查起来很折磨人。合成之后记得把 CompositeCollider2D 的 Geometry Type 设成 Outlines,能省不少顶点。
2.2 参数逐个拆:哪些能调,哪些别碰
PlatformEffector2D 的参数不多,但每一个都有明确的物理含义,我按"改不改"分成三类。
| 参数 | 含义 | 常规平台取值 | 我的建议 |
|---|---|---|---|
| Use One Way | 是否启用单向判定 | 开 | 关掉就退化成实体平台,做可破坏平台时可以用它切换状态 |
| Use One Way Grouping | 多个碰撞体是否作为整体参与单向判定 | 复合碰撞体、瓦片地图时开 | 单碰撞体的平台关着就行 |
| Surface Arc | 被当作"平台面"的角度范围,以局部上方向为中心向两侧展开 | 180 | 别乱调,见 3.2 |
| Use Side Friction | 侧面是否施加摩擦 | 关 | 开了必黏边,除非你就是要这个手感 |
| Use Side Bounce | 侧面是否施加弹力 | 关 | 想做弹跳墙才开 |
| Side Arc | 侧弧角度,由 Surface Arc 自动推算 | 自动 | 只读,不用管 |
| Rotational Offset | 单向方向相对局部上方向的偏移角 | 0 | 斜坡素材不能旋转时用它救场 |
| Collider Mask | 哪些层的碰撞体受这个 Effector 约束 | 默认全部 | 出问题先确认没被改成空 |
Use Side Friction 这一项我想多说两句。它的作用是给"侧弧"范围内的接触面加摩擦,听起来挺合理,实际表现是角色贴着平台侧边往上跳的时候会被"吸住"一下,落地时如果脚趾刚好擦到平台侧边,人会贴着边缘慢慢滑下去而不是直接掉。这个手感非常糟糕,所以我的习惯是永远关掉它,需要侧面摩擦的场合(比如贴着墙缓降)单独做一层墙体碰撞体,别跟单向平台混在一起。
Use One Way Grouping 这个选项文档里的描述比较抽象,我的理解是:当同一块平台由多个碰撞体拼成时,如果不分组,每个碰撞体各自做一次单向判定,角色在拼接处就可能出现"一只脚被这块挡住、另一只脚从旁边穿过去"的情况;开了分组,这些碰撞体在判定上被当作一个整体,行为就统一了。用 Composite Collider 的场景基本可以无脑开。
Collider Mask 我吃过一次亏:某次为了做"单向平台只对玩家生效",把这个 Mask 改成了只包含玩家层,结果敌人确实能穿过去了,但玩家站在平台上时也没法被平台的其他逻辑检测到,地面检测直接失效。后来改成"平台层不变,敌人层不与平台层碰撞",问题解决。Mask 尽量别动,多角色需求用 Layer 碰撞矩阵解决更干净。
2.3 角色那边也得配合,不然 Effector 白搭
平台配完了,角色没配好照样出问题。我的角色 Rigidbody2D 一般这样设:Body Type 选 Dynamic,Gravity Scale 根据手感在 3 到 6 之间,Freeze Rotation Z 一定要勾上(不勾的话角色会在落地瞬间被物理引擎转个角度),Collision Detection 根据最高下落速度决定,速度快就上 Continuous,Interpolate 打开让视觉更顺滑。
碰撞体形状是个容易被低估的点。CapsuleCollider2D 在斜坡上滑得顺,但它的底部是个圆角,角色下落时如果脚正好落在平台边缘的角附近,那个圆角很容易"咬"住平台,表现为落地后被轻轻弹开或者卡在边缘。BoxCollider2D 落地更干脆,但走斜坡会一顿一顿的。我的做法是角色用 CapsuleCollider2D,但把底部半径压小,同时给平台碰撞体保证至少 0.2 个单位的厚度,让落点判定有余量。如果项目里斜坡不多,直接上 Box 更省心。
摩擦力这块,我建议在角色身上挂一个 Physics Material 2D,Friction 设 0 或者很低,Bounciness 设 0。平台不加材质。这样所有平台的摩擦手感由角色统一控制,改一次全局生效;反过来做(每个平台加材质)项目一大就彻底失控。真正需要"冰面"手感的地面,再单独给它加低摩擦材质覆盖角色设置。
3. 机制深挖:判定方向、表面弧与下穿
3.1 transform.up 决定一切,这条要记牢
单向平台"哪一面是实心的",完全由平台 GameObject 的局部上方向(也就是 transform.up,场景里那根绿箭头)决定。绿箭头指哪边,哪边就是可以站的地板面。
这条规则带来两个直接结论。第一,平台旋转 Z 轴 180 度,实心面就翻到下面去了,角色会变成从上方穿过去、站到平台底部。所以别随手旋转平台,转之前想清楚。第二,绝对不要用负缩放来翻转平台,负缩放会让局部坐标系整体反号,单向判定直接乱掉,表现是"有时候能站有时候不能站",这种问题极难查。要翻转视觉表现,用 SpriteRenderer 的 Flip X,别动 Transform 的 Scale。
斜坡同理:平台转 30 度,transform.up 跟着转 30 度,判定面自动贴着斜坡走,这种情况下你什么都不用改。
3.2 Surface Arc 的几何含义,别为了"更好穿"去调小它
Surface Arc 描述的是"以局部上方向为中心,向两侧各展开多少角度"的范围,默认 180 度,也就是一个完整的半圆。落在弧内的接触面被当作"平台面"处理——单向判定加上摩擦;弧外的部分被平分成左右两个侧弧,按"墙面"对待,始终是实体,是否带摩擦弹力由那两个 Side 开关决定。
这意味着 Surface Arc 越小,侧弧占的比例就越大,平台左右两端"像墙"的区域就越厚。角色从斜下方贴着平台角往上跳的时候,接触法线可能落在侧弧里,于是被当成撞墙处理,人被挡下来——这大概就是"明明是从下面跳却被卡住"的常见来源之一。
所以我的建议是:常规平台保持 180,不要动。只有在做斜坡单向平台、或者想刻意制造"边缘能挂一下"的手感时,才试着往下调到 165 到 175 之间,并且改完一定要实机跑一遍完整的跳跃测试:从下方正中跳、从下方贴边跳、从上方正中落、从上方贴边落、站在边缘横移掉下去。这五个动作跑完没问题,才算调对了。
3.3 侧边黏住的两个开关,按顺序关
角色黏在平台侧边的现象我在三个项目里都遇到过,基本是这两个原因之一。
第一顺位检查Use Side Friction是不是被勾上了,勾了就关掉,八成能解决。第二顺位看平台的碰撞体是不是比视觉宽 —— 尤其是用了 Composite Collider 或者 Sprite 自带物理形状的情况,轮廓会往外扩一圈,角色看着是站在平台边缘,实际早就压在碰撞体上了。这种情况下把碰撞体轮廓往里收一点即可,Unity 的 Sprite 编辑器里可以直接编辑物理形状的顶点。
还有一种更隐蔽的情况:角色自己身上有其他碰撞体(比如攻击判定、拾取范围)没设成 Trigger,无意中和平台发生了碰撞。这类问题看现象非常像 Effector 的问题,实际一查是别的碰撞体在作妖,我是靠 Physics2D 的碰撞回调把接触双方的名字打出来才定位到的。
3.4 下穿(Drop Through)的三种做法与取舍
站着按方向下要能掉下去,这件事 Effector 本身不管,得自己写。常见三种做法,我按推荐度排序。
做法一:临时忽略碰撞(推荐)。用Physics2D.IgnoreCollision把角色碰撞体和脚下这块平台碰撞体的碰撞关系临时关掉,持续一小段时间后恢复。优点是只影响这两个碰撞体,角色其他碰撞体、其他平台全都不受影响;缺点是恢复时机要算准,恢复早了会被顶回去,恢复晚了角色穿过平台后可能又被吸住。
做法二:临时切成 Trigger。把平台的 Collider2D 设成 isTrigger,掉下去之后再改回来。问题是平台一旦变成 Trigger,所有站在它上面的角色(包括敌人、其他物体)都会一起掉,多人场景直接崩。除非你的游戏里这类平台永远只有一个角色会站,否则别用。
做法三:换 Layer。把角色切到"不与平台碰撞"的层,等掉出平台范围再切回来。问题是切层是全局生效的,如果角色此时正站在另一块同层的平台上,也会一起穿过去。多层平台堆叠的场景特别容易出 bug。
我选做法一,并且把它写成一个独立组件,任何角色挂上就能用。
3.5 忽略时长到底设多少,给个算式
忽略时长不能拍脑袋。太短了角色还没离开平台碰撞体,碰撞一恢复就会被顶回平台上;太长了玩家会觉得"掉下去要等一下才动",操作发黏。
粗略的算法是:忽略时长 ≥ (平台碰撞体厚度 + 角色碰撞体高度) ÷ 角色下落时的速度,再取一个安全系数。举个例子,平台厚 0.3 单位,角色碰撞体高 1.0 单位,重力 4、起跳下落初速度约 0,落到平台时被忽略后开始加速,取下落一段后的平均速度 8 单位每秒,那么 (0.3 + 1.0) ÷ 8 ≈ 0.1625 秒,取 1.5 倍安全系数就是 0.24 秒左右,实际取 0.25 秒比较舒服。
这只是基准值,手感还得靠实机调。项目里我一般把时长做成可配置字段,先在编辑器里调,调好了再固化到预制体上。另外一定要在恢复碰撞之前做一次"是否还在平台范围内"的检查,不然角色刚离开就被恢复的话,还是会有一帧被顶的感觉。
4. 从零到可用:一份能直接抄的实操流程
4.1 场景搭建的十步
- 新建空物体命名 Platform_OneWay,位置放在关卡需要的高度。
- 加 BoxCollider2D,把 Size 调成你想要的平台尺寸,厚度不要低于 0.2。
- 勾上 BoxCollider2D 的Used By Effector。这一步最容易漏,做完回头看一眼。
- 加 PlatformEffector2D,确认 Use One Way 勾上。
- 确认 Use Side Friction 和 Use Side Bounce 都没勾。
- Surface Arc 保持 180,Rotational Offset 保持 0。
- 给平台加个 SpriteRenderer 或者交给 Tilemap 渲染,视觉宽度比碰撞体略大一点点,玩家会觉得"看着能站就能站"。
- 角色身上:Rigidbody2D 设 Dynamic,Freeze Rotation Z,Gravity Scale 4 左右。
- 角色身上:CapsuleCollider2D,底部半径压小;加 Physics Material 2D,Friction 为 0。
- 给角色挂下穿组件,把角色的碰撞体和平台层 Mask 填进去。
跑一遍五个基础动作,都过了再往下做。
4.2 下穿组件的完整实现
下面这份是我项目里一直在用的版本,做了简化,能直接跑。核心思路是:先用一个薄薄的探针盒在脚底找脚下的平台碰撞体,找到之后逐个忽略,等一段时间再检查一次脚下,确认离开了才恢复。
using System.Collections; using UnityEngine; [RequireComponent(typeof(Rigidbody2D))] public class OneWayDropThrough : MonoBehaviour { [SerializeField] private Collider2D bodyCollider; [SerializeField] private LayerMask platformMask; [Tooltip("忽略碰撞的基础时长,按平台厚度和下落速度估算")] [SerializeField] private float ignoreDuration = 0.25f; [Tooltip("恢复碰撞前的额外等待上限")] [SerializeField] private float maxExtraWait = 0.5f; private readonly Collider2D[] probeHits = new Collider2D[8]; private Coroutine running; private Collider2D[] ignored = new Collider2D[8]; private int ignoredCount; public bool IsDropping => running != null; public bool TryDrop() { if (running != null) return false; if (!IsStandingOnOneWayPlatform()) return false; running = StartCoroutine(DropRoutine()); return true; } private bool IsStandingOnOneWayPlatform() { int count = ProbeFeet(); for (int i = 0; i < count; i++) { if (probeHits[i].GetComponent<PlatformEffector2D>() != null) return true; } return false; } private int ProbeFeet() { Bounds b = bodyCollider.bounds; Vector2 center = new Vector2(b.center.x, b.min.y - 0.02f); Vector2 size = new Vector2(Mathf.Max(b.size.x - 0.06f, 0.05f), 0.08f); return Physics2D.OverlapBoxNonAlloc( center, size, 0f, probeHits, platformMask); } private IEnumerator DropRoutine() { ignoredCount = ProbeFeet(); if (ignoredCount == 0) { running = null; yield break; } for (int i = 0; i < ignoredCount; i++) { ignored[i] = probeHits[i]; if (ignored[i] != null) Physics2D.IgnoreCollision(bodyCollider, ignored[i], true); } float t = 0f; while (t < ignoreDuration) { t += Time.deltaTime; yield return null; } float extra = 0f; while (extra < maxExtraWait && ProbeFeet() > 0) { extra += Time.deltaTime; yield return null; } for (int i = 0; i < ignoredCount; i++) { if (ignored[i] != null) Physics2D.IgnoreCollision(bodyCollider, ignored[i], false); ignored[i] = null; } ignoredCount = 0; running = null; } }调用侧就一行,在角色控制里判断输入:
if (inputDropPressed && isGrounded) { dropThrough.TryDrop(); // 关键:立刻给一个向下的初速度,别等重力慢慢加速 rb.linearVelocity = new Vector2(rb.linearVelocity.x, -dropInitialSpeed); }这里有个细节值得单独拎出来:按下的瞬间要主动给一个向下速度。只靠重力,角色会有一小段时间"贴着平台不动",因为重力还没积累起来,玩家会觉得按键没反应。给一个初始向下速度(我一般取 5 到 8),下穿会干脆很多。注意这一脚别给太大,给大了角色会像被弹射下去一样。
另外linearVelocity是较新版本的字段名,老工程里对应的是velocity,两个都存在过一段时间,按你项目用的版本写就行,别混着抄。
4.3 验收清单,五加三
做完之后按这张表跑一遍,全过再提交。
| 测试动作 | 期望表现 | 不通过时先查 |
|---|---|---|
| 从下方正中跳起穿过 | 完全无阻挡,速度不受影响 | Used By Effector 是否勾上 |
| 从下方贴边跳起 | 不被卡住,能穿过或被轻微推开但不停止 | Surface Arc 是否被调小、侧摩擦是否关了 |
| 从上方落下站住 | 站稳不下沉、不抖动 | 角色摩擦材质、重力与碰撞检测模式 |
| 站在边缘横移 | 走出边缘自然掉落 | 碰撞体轮廓是否比视觉宽 |
| 按下加跳跃下穿 | 干脆下坠,不反弹 | 是否给了向下初速度、忽略时长是否过短 |
| 下穿后立刻起跳 | 能正常跳上其他平台 | 碰撞恢复时机 |
| 连续快速下穿两次 | 第二次不会卡在半空 | 协程重入是否被拦住 |
| 站在移动平台上随行 | 跟随移动不滑落 | 平台是否用 MovePosition 移动 |
最后一条经常被忽略。移动平台上的单向通行是另一个故事:平台在动,Effector 的判定方向不变,但角色的相对速度变了,站在上面如果平台向下走,角色可能出现"短暂脱离平台再落回"的顿挫。我的处理是给移动平台加一点点向上位移补偿,或者干脆在移动平台上把角色的父级临时改成平台(我不太喜欢这个做法,遇到角色死亡、传送会出乱子,慎用)。
5. 踩过的坑与排查速查
5.1 问题速查表
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
| 加了 Effector 完全没效果 | Used By Effector 没勾 / 碰撞体是 Trigger | 勾上复选框,取消 Trigger |
| 从下方跳被挡住 | 平台旋转过导致 up 方向反了 / 负缩放 | 检查 Z 轴旋转和 Scale |
| 从上方落下却穿过去 | 平台被旋转了 180 度 / Transform.up 被父物体带偏 | 检查父级旋转,必要时重置 |
| 角色贴着平台侧边慢慢滑 | Use Side Friction 被开启 | 关掉 |
| 落地瞬间被轻弹一下 | 角色碰撞体摩擦材质 Bounciness 不为 0 / 胶囊底部圆角咬边 | 材质 Bounciness 归零,压小底部半径 |
| 下穿一半被顶回来 | 忽略时长太短,或者恢复碰撞前没检查重叠 | 加长时长,恢复前做探测 |
| 角色站在平台上自己缓慢下沉 | 碰撞检测模式不合适,或者平台被做成 Kinematic 后在移动 | 开 Continuous,检查平台位移方式 |
| 站在平台边缘悬空 | Composite 轮廓比视觉大 | 编辑物理形状顶点 |
5.2 三个印象最深的坑
第一个是高速下落穿模。有个项目角色下落速度能到 25 单位每秒,单向平台怎么调都会偶尔穿过去。查了很久才想明白:问题不在 Effector,而在离散碰撞检测本身。物理步长 0.02 秒,25 单位每秒意味着一步走 0.5 个单位,平台厚度只有 0.3,这一步直接跳过去了,引擎根本没检测到接触,Effector 自然也没机会做判定。解决办法两个:给角色开 Continuous 碰撞检测,或者把平台厚度加到 0.5 以上。我用前者,代价是性能开销稍高,但角色通常只有一两个,完全能接受。
第二个是下穿之后被"吸"回去。现象是按下键掉下去,掉到一半突然弹回平台上。原因是忽略时长结束时角色恰好还在平台的接触范围内,碰撞一恢复,物理引擎要解决重叠,就把角色往上推。我后来把恢复逻辑改成"先探测脚下是否还有平台碰撞体,有就继续等,最多再等半秒",问题消失。
第三个是斜坡上的单向平台会自行滑动。平台转 20 度做斜坡,角色站上去会慢慢往下滑。这不是 Effector 的锅,是重力分量在斜面上没有足够的静摩擦力抵消。处理办法有两种:给角色的物理材质加上一点点摩擦(但会影响其他场景),或者在斜坡上给角色做"落地吸附",把速度在斜坡方向上钳住。我用的是后者,写了个小脚本在落地且坡度小于阈值时把沿坡向下的速度清零,代价不高,手感也稳。
6. 再往前走一点:多层、瓦片与移动平台
6.1 多层堆叠时的优先级问题
关卡里经常出现两层甚至三层单向平台上下叠放,角色从最下面一层往上跳,穿过第一层、穿过第二层、落到第二层上。这种场景里,Effector 本身没问题,出问题的是你的"地面检测"。
如果地面检测用的是单个射线或者单个 OverlapCircle,角色跳起来穿过第一层的瞬间,检测结果里会出现第一层,此时角色明明在空中,却被判定成落地,于是二段跳被消耗、跳跃被截断。我的做法是:地面检测记录"接触到的所有碰撞体里,位置最低且法线朝上的那个",而不是只看第一个;同时加一个很小的初始向上速度宽容期,起跳后的前 0.05 秒内不接受落地判定。这两条加上,多层跳跃的手感就正常了。
还有一点,Physics2D.queriesStartInColliders这个全局设置会影响射线检测能不能打到"起点所在的那个碰撞体"。角色起跳瞬间如果还嵌在平台碰撞体里,射线可能直接从内部穿过去,检测不到落地。遇到"刚站上去检测不到地面"这种怪现象,先去检查这个设置。
6.2 瓦片地图与 Composite Collider 的配合细节
用瓦片地图做平台,流程固定:Tilemap 上挂 TilemapCollider2D 和 CompositeCollider2D,给 Tilemap 加 Rigidbody2D 设 Static,让 Composite 自动合并。合并之后,Used By Effector 要勾在 CompositeCollider2D 上,Effector 挂在有 Composite 的那个物体上。
这里有两个坑。一是合并后的轮廓会向外扩张,尤其是 Geometry Type 用 Outlines 加偏移的情况下,玩家会觉得"没碰到就被挡住了"。把 Composite 的 Offset Distance 调成很小的值或者 0,再手动修一下边缘顶点。二是瓦片地图里不同瓦片如果有不同朝向(比如斜坡瓦片),合并后轮廓是一个整体,一个 Effector 只能给一个方向,做不了"这块斜坡向上、那块向下"。这种场合必须把斜坡单独拎出来做成独立的平台物体,不要塞进同一个 Tilemap 里。我为了省事塞进去过一次,最后整张地图重做。
6.3 移动平台上的单向通行
移动平台加单向,是我觉得最容易出意外气的组合。平台一动,角色的相对速度就变了,而 Effector 的判定基于接触时的相对运动方向,于是会出现"平台上升时角色被短暂顶起"、"平台下降时角色脚下悬空一帧"的现象。
我的处理有两条。第一条,移动平台的速度别太快,尤其别在下穿忽略时间内移动,否则角色会被平台"追上"或者"落下",恢复碰撞时的重叠更严重。第二条,角色站在移动平台上时,把平台的速度合成到角色的移动计算里,让角色的物理表现保持一致,而不是靠物理引擎的摩擦去带。前者是可预测的,后者是各种玄学顿挫的来源。
顺带说一句,需要做"平台上升顶住角色往天花板挤压"的效果时,单向平台反而不合适,应该改用普通的实体碰撞体加一个触发区域,让玩家自己决定要不要跳过去。单向平台的价值在于"可穿",一旦你开始需要"被顶",说明这个平台的设计目标变了。
6.4 给个可扩展的方向
如果项目里平台类型比较多(单向、实体、可破碎、会消失、只能从某侧穿),我建议不要给每个平台写一个脚本,而是做一个简单的平台类型枚举加一个统一的配置组件,把"是否单向、碰撞层、Effector 开关、破碎后的表现"都收在一个数据结构里,运行时按类型初始化。这样新增一种平台只要加一条配置,不用动代码逻辑。
我在一个项目里就是这么做,前期多花了半天搭结构,后面加了六种平台类型,每种只写十几行配置,省下来的时间远超那半天。反过来如果一开始就各种 If 判断堆在一起,到第五种平台的时候你会想推倒重来。
最后分享一个我自己调参的小习惯:把角色的重力、下落速度上限、平台的忽略时长这三个值写在一张便签上,每次改动只动一个,改完立刻跑五个基础动作。三个一起改是绝对查不出是谁的锅的,这个教训我交了不止一次学费。