Unity新输入系统实现2D平台跳跃蓄力冲刺:状态机与组件化设计

发布时间:2026/7/23 9:37:47

Unity新输入系统实现2D平台跳跃蓄力冲刺:状态机与组件化设计 1. 项目概述为什么2D平台跳跃离不开冲刺做2D平台跳跃游戏手感是灵魂。玩家操作角色在像素或矢量构成的关卡里蹦蹦跳跳每一次起落、每一次转向都直接决定了游戏的“好玩”程度。而“冲刺”Dash这个能力几乎是现代平台跳跃游戏的标配它不仅仅是让角色移动得更快那么简单。一个好的冲刺系统能瞬间提升游戏的节奏感、爽快感和策略深度。想象一下在《蔚蓝》里没有冲刺或者在《空洞骑士》里没有冲刺和蓄力冲刺那些精妙的关卡设计和紧张刺激的BOSS战会失色多少这次我们要聊的就是在Unity里如何为一个2D平台跳跃角色实现一套手感扎实、响应灵敏的冲刺系统并且是“蓄力冲刺”这种带点策略性的变种。更重要的是我们将完全基于Unity的“新输入系统”Input System Package来构建输入逻辑。为什么强调“新”系统因为老的Input Manager在处理复杂的按键组合、手柄支持、输入重映射时已经显得力不从心而新输入系统提供了更强大、更模块化、也更面向未来的解决方案。对于追求专业品质的独立开发者来说掌握它是必经之路。简单来说这个项目要实现的目标是玩家按下某个键比如Shift或手柄上的B键角色会瞬间向当前面朝方向高速冲刺一小段距离如果按住按键不放角色会开始“蓄力”蓄力期间可能有视觉反馈比如角色发光、能量条增长松开按键时根据蓄力时间长短释放出威力、速度或距离不同的“蓄力冲刺”。这听起来简单但要让手感“对味”里面涉及状态管理、物理模拟、输入处理、动画衔接和视觉反馈等一系列需要精心调校的细节。2. 核心思路与架构设计在动手写代码之前我们先得把脑子里的设计理清楚。一股脑地把所有逻辑塞进PlayerController里是新手常犯的错误结果就是代码变成一团乱麻改个冲刺冷却时间都可能引发一连串Bug。2.1 状态驱动用有限状态机FSM理清逻辑平台跳跃角色的行为是典型的“状态集合”。比如角色在某一时刻只能是“站立”、“奔跑”、“跳跃”、“下落”、“冲刺”、“受击”等状态中的一种。冲刺特别是蓄力冲刺会引入更多子状态“冲刺准备”或叫“蓄力”、“冲刺中”、“冲刺结束冷却”。用if-else链来管理这些状态很快就会失控。我的做法是采用一个轻量级的有限状态机FSM模式。不一定需要复杂的框架用一个enum定义所有状态并在一个核心的UpdateState方法里处理状态转换就行。public enum PlayerState { Idle, Running, Jumping, Falling, Dashing, // 普通冲刺中 ChargingDash, // 蓄力冲刺-蓄力中 ChargedDashing, // 蓄力冲刺-释放中 Cooldown // 冲刺后冷却 }每个状态都有其进入、持续和退出的逻辑。例如进入Dashing状态时我们要瞬间给角色一个高速速度并锁定其他输入比如不能再转向或二次跳跃在Dashing状态持续期间我们要维持这个速度并处理碰撞退出Dashing状态时我们要取消速度锁定并可能进入一个短暂的Cooldown状态。对于蓄力冲刺ChargingDash状态是关键。在这个状态下角色可能无法移动或移动缓慢但我们会累积一个“蓄力值”同时播放蓄力动画比如角色身体能量聚集的特效。当玩家松开按键根据蓄力值切换到ChargedDashing状态并赋予一个与蓄力值相关的冲刺速度或距离。2.2 新旧输入系统对比与选型为什么坚决用新输入系统我们来做个简单对比旧 Input Manager直接在Update里用Input.GetKeyDown(KeyCode.LeftShift)。简单粗暴但问题很多难以区分多个输入设备比如同时接键盘和手柄、输入重映射需要自己写大量代码、处理长按、连按、组合键的逻辑比较繁琐。新 Input System基于“输入动作”Input Action的概念。你创建一个Input Actions资产在里面定义“Dash”这个动作然后可以同时为它绑定键盘Shift键、手柄B键、甚至鼠标侧键。在代码中你只需要监听“Dash”动作的触发started、performed、canceled无需关心具体是哪个物理按键触发的。这对于支持多平台和自定义按键设置是革命性的便利。更重要的是新系统对“交互”Interactions有原生支持。比如我们可以为“Dash”动作添加一个“Hold”交互并设置holdTime比如0.5秒那么系统会自动帮我们判断这次输入是“点按”还是“长按”这正好完美契合了“点按冲刺”和“长按蓄力冲刺”的需求。我们无需自己用计时器去笨拙地判断按键时长。2.3 组件化设计分离关注点不要把所有的功能都堆在主角的PlayerController脚本里。我建议至少拆分成以下几个组件通过GetComponent或序列化字段引用进行通信PlayerInputHandler专门负责用新输入系统接收输入并将输入事件转化为对其他组件的调用。比如它监听到“Dash”动作触发就调用DashComponent的StartDash方法。DashComponent核心负责冲刺的所有逻辑包括状态管理、速度计算、冷却计时。它需要引用Rigidbody2D来修改速度引用Animator来触发动画。ChargeComponent如果蓄力逻辑比较复杂比如有多段蓄力、不同属性可以独立出来。负责管理蓄力值、蓄力速度、最大值并提供当前蓄力比例等接口。VisualFeedbackComponent负责所有视觉表现比如冲刺时的拖尾效果、蓄力时的粒子特效和UI能量条变化。这样DashComponent只关心逻辑通过事件或直接调用来通知这个组件更新表现。这种设计让每个脚本职责单一调试和修改起来也方便。比如你想调整冲刺拖尾的颜色只需要去VisualFeedbackComponent里改不用担心会影响到冲刺的速度计算。3. 新输入系统Input System集成详解理论说完了我们开始动手。第一步是把新输入系统引入项目并配置好我们的输入动作。3.1 安装与基础配置首先通过Unity的Package Manager安装Input System包。安装后在项目设置中可能需要将“Active Input Handling”从“Input Manager (Old)”切换到“Input System Package (New)”或“Both”。对于新项目建议只用新的。创建一个输入动作资产在Project窗口右键 - Create - Input Actions我通常命名为PlayerControls。双击打开它你会看到一个类似动画状态机的编辑器。创建Action Map一个Action Map可以理解为一套控制方案比如“Player”、“UI”、“Vehicle”。我们创建一个名为“Player”的Action Map所有游戏内的角色控制都放在这里。定义Actions在“Player” Map下创建我们需要的动作。Move类型为Value(Vector2)用于接收水平垂直移动输入。绑定WASD键和手柄左摇杆。Jump类型为Button绑定空格键和手柄A键。Dash这是关键类型为Button绑定左Shift键和手柄B键。3.2 为“Dash”动作配置“Hold”交互选中Dash动作在右边的Inspector面板找到“Interactions”一栏点击“”号添加一个“Hold”交互。Hold Time设置为0.5秒。这意味着按住按键超过0.5秒才会触发“长按”行为。Press Point一般保持默认这是触发按键的阈值。这个“Hold”交互会改变Dash动作的触发行为。当玩家按下Dash键时如果快速松开按住时间 0.5秒会触发一次started然后很快触发performed我们可以将此识别为“点按冲刺”。如果按住不放按住时间 0.5秒会触发started但不会立即触发performed而是进入等待。直到玩家松开按键时才会触发performed。同时在按住期间canceled不会被触发。我们可以将“按住”识别为“开始蓄力”将“松开”识别为“释放蓄力冲刺”。注意这里有个关键点新输入系统的performed回调在“Hold”交互下对于“点按”是在按下时触发对于“长按”是在松开时触发。我们需要结合started和canceled回调来精确区分状态。后面在代码部分会详细说明。3.3 创建PlayerInputHandler脚本现在我们来编写处理输入的C#脚本。首先让脚本实现IInputActionCollection接口相关的回调。using UnityEngine; using UnityEngine.InputSystem; // 引入新输入系统命名空间 public class PlayerInputHandler : MonoBehaviour { // 持有对Input Actions资产的引用 [SerializeField] private InputActionAsset inputActions; // 生成的C#类用于类型安全地访问动作 private PlayerControls playerControls; // 输入事件供其他组件订阅 public System.ActionVector2 OnMove; public System.Action OnJumpPressed; public System.Action OnDashPressed; // 点按冲刺 public System.Action OnDashHoldStarted; // 开始蓄力 public System.Actionfloat OnDashHoldReleased; // 释放蓄力参数是按住时长 private InputAction moveAction; private InputAction jumpAction; private InputAction dashAction; private float dashHoldStartTime; private bool isHoldingDash false; private void Awake() { // 启用Input Action Asset playerControls new PlayerControls(); inputActions.Enable(); // 获取具体的Action moveAction playerControls.Player.Move; jumpAction playerControls.Player.Jump; dashAction playerControls.Player.Dash; // 订阅事件 jumpAction.started OnJumpStarted; dashAction.started OnDashStarted; dashAction.performed OnDashPerformed; dashAction.canceled OnDashCanceled; } private void Update() { // 持续读取移动输入 Vector2 moveInput moveAction.ReadValueVector2(); OnMove?.Invoke(moveInput); } private void OnJumpStarted(InputAction.CallbackContext context) { OnJumpPressed?.Invoke(); } private void OnDashStarted(InputAction.CallbackContext context) { // 按下Dash键的瞬间 dashHoldStartTime Time.time; isHoldingDash true; // 我们先触发一个“开始蓄力”的事件即使最终可能是点按 // 这样视觉组件可以立即开始播放“预备”特效 OnDashHoldStarted?.Invoke(); } private void OnDashPerformed(InputAction.CallbackContext context) { // 这个回调的触发时机取决于交互类型。 // 对于“点按”它会在按下后很快触发。 // 对于“长按”Hold它会在松开按键时触发。 float holdDuration Time.time - dashHoldStartTime; if (holdDuration 0.5f) // 假设Hold Time是0.5秒 { // 这是点按冲刺 OnDashPressed?.Invoke(); } else { // 这是长按后松开释放蓄力冲刺 OnDashHoldReleased?.Invoke(holdDuration); } isHoldingDash false; } private void OnDashCanceled(InputAction.CallbackContext context) { // 如果按键在达到Hold Time之前就被取消比如异常情况我们可能需要处理 // 但在这个逻辑下如果玩家在0.5秒内松开会触发performed(点按)不会走到这里。 // 这里可以用于重置状态或者处理输入被意外中断的情况。 if (isHoldingDash) { // 例如蓄力被打断播放一个蓄力取消的特效 isHoldingDash false; } } private void OnEnable() { playerControls?.Enable(); } private void OnDisable() { playerControls?.Disable(); // 记得取消订阅防止内存泄漏 jumpAction.started - OnJumpStarted; dashAction.started - OnDashStarted; dashAction.performed - OnDashPerformed; dashAction.canceled - OnDashCanceled; } }这个PlayerInputHandler就像一个翻译官把底层的输入信号翻译成游戏逻辑能理解的、语义化的事件OnDashPressedOnDashHoldReleased。DashComponent等其他组件只需要订阅这些事件即可完全不用关心输入细节。4. 冲刺核心逻辑实现DashComponent输入准备好了接下来就是重头戏实现冲刺本身。我们创建一个DashComponent脚本。4.1 属性与状态定义首先定义冲刺所需的参数和内部状态。using UnityEngine; public class DashComponent : MonoBehaviour { [Header(基础冲刺参数)] [SerializeField] private float dashSpeed 20f; // 普通冲刺速度 [SerializeField] private float dashDuration 0.2f; // 普通冲刺持续时间 [SerializeField] private float dashCooldown 0.5f; // 冲刺冷却时间 [Header(蓄力冲刺参数)] [SerializeField] private float maxChargeTime 2f; // 最大蓄力时间 [SerializeField] private float minChargedDashSpeed 25f; // 最小蓄力冲刺速度 [SerializeField] private float maxChargedDashSpeed 40f; // 最大蓄力冲刺速度 [SerializeField] private float minChargedDashDuration 0.3f; // 最小蓄力冲刺持续时间 [SerializeField] private float maxChargedDashDuration 0.6f; // 最大蓄力冲刺持续时间 [Header(组件引用)] [SerializeField] private Rigidbody2D rb; [SerializeField] private Animator animator; [SerializeField] private PlayerInputHandler inputHandler; // 引用输入处理器 [SerializeField] private ChargeComponent chargeComponent; // 可选专门管理蓄力UI/逻辑 // 内部状态 private bool isDashing false; private bool isOnCooldown false; private float dashTimeLeft 0f; private float cooldownTimer 0f; private int facingDirection 1; // 1向右 -1向左需要从其他脚本如移动脚本获取 // 当前冲刺类型 private enum DashType { Normal, Charged } private DashType currentDashType; private float currentDashSpeed; private float currentDashDuration; private void Awake() { if (rb null) rb GetComponentRigidbody2D(); if (inputHandler null) inputHandler GetComponentPlayerInputHandler(); } private void Start() { // 订阅输入事件 if (inputHandler ! null) { inputHandler.OnDashPressed StartNormalDash; inputHandler.OnDashHoldStarted StartCharging; inputHandler.OnDashHoldReleased ReleaseChargedDash; } } }4.2 普通冲刺实现普通冲刺的逻辑相对直接瞬间赋予一个高速并在短时间内维持期间忽略其他水平输入。private void StartNormalDash() { // 检查是否允许冲刺不处于冲刺中、不处于冷却、角色在地面或空中根据设计 if (isDashing || isOnCooldown) return; // 还可以添加其他条件如能量值等 StartDash(DashType.Normal, dashSpeed, dashDuration); } private void StartDash(DashType type, float speed, float duration) { isDashing true; currentDashType type; currentDashSpeed speed; currentDashDuration duration; dashTimeLeft duration; // 1. 锁定输入在Dash期间我们通常不希望移动输入干扰冲刺方向 // 这个可以通过在PlayerController里根据isDashing状态忽略移动输入来实现 // 2. 设置速度 rb.velocity new Vector2(facingDirection * currentDashSpeed, rb.velocity.y); // 3. 临时修改物理属性可选例如冲刺期间重力降低或者Y轴速度锁定 // rb.gravityScale 0.2f; // 触发动画 animator.SetBool(IsDashing, true); // 触发视觉特效通过事件或直接调用 // visualFeedback.PlayDashEffect(facingDirection); } private void UpdateDash() { if (!isDashing) return; dashTimeLeft - Time.deltaTime; if (dashTimeLeft 0) { EndDash(); } else { // 在冲刺持续期间可以维持速度或者让速度有一个衰减曲线 // 这里我们选择维持恒定速度 rb.velocity new Vector2(facingDirection * currentDashSpeed, rb.velocity.y); } } private void EndDash() { isDashing false; // 恢复物理属性 // rb.gravityScale 1f; // 进入冷却 isOnCooldown true; cooldownTimer dashCooldown; // 普通冲刺使用基础冷却 // 停止动画 animator.SetBool(IsDashing, false); // 停止视觉特效 // visualFeedback.StopDashEffect(); }在Update方法中我们需要调用UpdateDash和UpdateCooldown。private void Update() { UpdateDash(); UpdateCooldown(); // 其他逻辑... } private void UpdateCooldown() { if (!isOnCooldown) return; cooldownTimer - Time.deltaTime; if (cooldownTimer 0) { isOnCooldown false; } }4.3 蓄力冲刺实现蓄力冲刺的关键在于“蓄力”阶段的管理和“释放”时的参数计算。private bool isCharging false; private float chargeStartTime; private float currentChargeRatio 0f; // 当前蓄力比例0~1 private void StartCharging() { // 同样需要检查是否允许开始蓄力如不在冷却、不在其他无法蓄力的状态 if (isDashing || isOnCooldown || isCharging) return; isCharging true; chargeStartTime Time.time; currentChargeRatio 0f; // 进入蓄力状态可能降低移动速度播放蓄力动画 animator.SetBool(IsCharging, true); // 开始播放蓄力粒子特效或声音 // chargeComponent.StartCharge(); } private void UpdateCharge() { if (!isCharging) return; float chargeTime Time.time - chargeStartTime; currentChargeRatio Mathf.Clamp01(chargeTime / maxChargeTime); // 计算蓄力比例 // 更新蓄力视觉反馈如UI能量条、角色发光强度 // chargeComponent.SetChargeRatio(currentChargeRatio); // visualFeedback.UpdateChargeEffect(currentChargeRatio); // 如果蓄力达到最大可以有一些特殊反馈比如震动、音效 if (currentChargeRatio 1f) { // visualFeedback.OnChargeFull(); } } private void ReleaseChargedDash(float holdDuration) { if (!isCharging) return; // 如果没在蓄力可能是误触发 isCharging false; animator.SetBool(IsCharging, false); // 计算实际蓄力时间不能超过最大 float actualChargeTime Mathf.Min(holdDuration, maxChargeTime); float chargeRatio Mathf.Clamp01(actualChargeTime / maxChargeTime); // 根据蓄力比例计算冲刺速度和持续时间线性或曲线插值 float speed Mathf.Lerp(minChargedDashSpeed, maxChargedDashSpeed, chargeRatio); float duration Mathf.Lerp(minChargedDashDuration, maxChargedDashDuration, chargeRatio); // 开始蓄力冲刺 StartDash(DashType.Charged, speed, duration); // 蓄力冲刺的冷却时间可以更长或者也基于蓄力比例 // 这里我们简单使用一个固定值或者也可以计算cooldownTimer dashCooldown * (1 chargeRatio); cooldownTimer dashCooldown * 1.5f; // 蓄力冲刺冷却更长 }同样需要在Update中调用UpdateCharge。重要提示蓄力期间角色通常应该不能进行普通移动或跳跃或者移动受到极大限制。这需要在PlayerController的移动逻辑中检查isCharging状态并做出相应限制。5. 物理、动画与视觉反馈的打磨逻辑跑通只是第一步让冲刺“感觉”对需要物理、动画和视觉的紧密配合。5.1 物理调校让冲刺手感扎实速度曲线上面的实现是恒定速度。但更高级的手感可以使用动画曲线AnimationCurve来定义速度随时间的变化。比如冲刺初期有一个小小的加速末期有一个减速会让手感更自然。[SerializeField] private AnimationCurve dashSpeedCurve; // 在Inspector中绘制曲线 private void UpdateDash() { // ... float curveRatio 1 - (dashTimeLeft / currentDashDuration); // 计算当前进度比例 float speedMultiplier dashSpeedCurve.Evaluate(curveRatio); rb.velocity new Vector2(facingDirection * currentDashSpeed * speedMultiplier, rb.velocity.y); }Y轴速度处理冲刺时是否保留原有的Y轴速度如下落速度通常为了操作稳定我们会锁定或大幅降低重力rb.gravityScale让冲刺路径更平直。但在一些强调空中机动性的游戏中可能允许Y轴速度叠加。碰撞体调整冲刺时有时会将角色的碰撞体CapsuleCollider2D调小或改变形状使其更容易穿过狭小缝隙这也是《空洞骑士》等游戏的常见技巧。Coyote Time与冲刺可以为冲刺也加入“Coyote Time”宽容时间即玩家离开平台边缘的极短时间内按下冲刺依然能生效这能极大提升操作容错率。5.2 动画状态机Animator配置在Animator Controller中你需要为角色添加“Dashing”和“Charging”状态。Dashing状态连接一个冲刺动画片段。从Any State或者从Running、Jumping等状态通过“Dash”触发器Trigger过渡到Dashing状态。同时从Dashing状态通过“IsDashing”布尔值为False时过渡回其他状态。Charging状态类似用于播放蓄力待机动画。通过“IsCharging”布尔值控制进入和退出。动画事件可以在冲刺动画的某一帧加入动画事件来触发声音、屏幕震动或生成冲击波粒子让反馈更精准。5.3 视觉与听觉反馈这是让技能感觉“强大”的关键。拖尾效果Trail Renderer为角色子对象添加一个Trail Renderer组件在StartDash时启用trailRenderer.emitting true在EndDash后延迟一小段时间禁用。可以调整颜色、宽度、材质使用粒子材质来匹配游戏风格。屏幕抖动Screen Shake冲刺开始和结束时调用一个简单的屏幕抖动脚本增强冲击力。public void ShakeCamera(float duration, float magnitude) { // 实现或调用已有的相机抖动逻辑 }粒子系统冲刺粒子在角色脚底或身后生成速度线粒子。蓄力粒子在角色周围环绕、收缩或亮度变化的粒子蓄力越满粒子越密集或颜色越亮。释放粒子蓄力冲刺释放瞬间一个圆形的冲击波粒子向外扩散。后期处理Post Processing冲刺时可以短暂增加运动模糊Motion Blur强度或者调整颜色分级Color Grading增加饱和度和对比度。音效至少需要三个音效冲刺启动音、蓄力循环音、蓄力释放音。蓄力循环音的音调Pitch可以随着蓄力比例升高释放音的音量和低频可以随蓄力比例增强。6. 常见问题、优化与扩展思路在实际实现中你肯定会遇到一些坑。这里记录几个常见问题和我的解决方案。6.1 输入冲突与状态优先级问题玩家在空中快速连续按下跳跃和冲刺或者冲刺中碰到墙壁状态可能混乱。解决明确状态优先级。通常“受击”优先级最高其次是“冲刺”、“蓄力”然后是“跳跃”、“下落”最后是“移动”。在PlayerController的顶层状态机中按照优先级检查。例如void UpdateState() { if (isHit) { /* 处理受击 */ return; } if (isDashing) { /* 处理冲刺 */ return; } if (isCharging) { /* 处理蓄力 */ return; } // ... 检查跳跃、下落等 HandleMovement(); // 最后处理基础移动 }6.2 冲刺方向与镜头跟随问题冲刺速度太快2D相机如果使用简单的SmoothFollow可能会严重滞后导致角色跑出屏幕。解决为冲刺状态单独配置相机跟随逻辑。可以在冲刺时临时增加相机的Follow Smooth Time或者让相机有一个预测偏移Look Ahead指向冲刺方向的前方一段距离。6.3 性能优化对象池Object Pooling冲刺和蓄力的粒子特效频繁生成销毁一定要用对象池管理。Update优化将DashComponent和ChargeComponent的Update逻辑放在一个统一的、由PlayerController控制的更新循环中避免多个脚本各自调用Update。或者使用FixedUpdate处理物理相关部分。动画器优化确保Animator Controller的状态结构简洁避免过多的过渡和层使用动画状态机的Culling Mode优化不可见时的更新。6.4 扩展玩法基础系统搭建好后可以很容易地扩展出更多玩法多段冲刺记录冲刺次数在空中允许冲刺N次。需要管理一个dashCount变量并在落地时重置。冲刺攻击在冲刺状态中如果碰到敌人触发伤害判定并可能取消冲刺。方向冲刺将输入从“按键”改为“按键方向”实现八方向甚至任意方向冲刺。这需要修改输入处理读取冲刺时的摇杆方向。冲刺穿墙通过设置物理图层Physics Layer和Physics2D.IgnoreLayerCollision让冲刺时暂时忽略某些图层如可穿透的平台实现“幽灵冲刺”效果。冲刺残影比拖尾更高级的效果在冲刺过程中每隔几帧生成一个角色半透明Sprite的克隆体形成残影序列视觉冲击力很强。实现这套系统的过程本质上是在打磨玩家手指与屏幕角色之间的那根“线”。每一个参数——0.2秒的冲刺时长、0.5秒的蓄力阈值、20到40的速度区间——都需要你反复进入游戏测试、感受、调整。没有绝对正确的值只有最适合你游戏节奏和手感的值。我自己的习惯是把所有这些参数都做成[SerializeField]并放在一个单独的ScriptableObject数据文件里这样策划或者就是明天的你调整起来不用翻代码在Play模式下调整还能实时看到效果效率高得多。当你调出那个按下按键后角色“嗖”地一声破空而出、屏幕微震、粒子飞溅并且蓄力到满时松开能带来更猛烈爆发的瞬间你就会知道所有这些细致的工作都是值得的。

相关新闻