Unity2D横版动作游戏开发:从移动、战斗到状态机的核心实现
1. 项目概述为什么选择Unity2D做横版动作游戏如果你和我一样是个对像素风、流畅连招和爽快打击感毫无抵抗力的游戏爱好者同时又有点手痒想自己动手做点什么那Unity2D横版动作游戏开发绝对是个能让你“痛并快乐着”的绝佳选择。这项目听起来挺唬人什么“从基础移动到战斗系统”感觉要学一大堆东西。但说穿了它的核心就是两件事让角色能按你的想法动起来以及让角色动起来的时候能打得又帅又爽。市面上很多独立游戏比如《空洞骑士》、《死亡细胞》的早期原型其实都是从这两个最基础的模块开始的。Unity引擎在这方面的优势太明显了。它不像一些底层引擎需要你从零造轮子Unity提供了一套非常成熟的2D物理系统、动画状态机和丰富的社区资源。这意味着你可以把更多精力花在游戏性的设计上而不是纠结于一个碰撞体该怎么写。用Unity2D你很快就能看到一个能跑能跳、能砍能杀的小人在屏幕上动起来这种即时反馈对保持开发热情至关重要。我当初就是被这种“快速成型”的能力吸引从一个小方块跳跃开始一步步搭出了自己的第一个横版关卡。这个实战项目适合谁呢首先是有一定C#基础至少了解变量、循环、类和方法的程序员朋友。其次是对游戏设计有想法不满足于只玩更想亲手创造的玩家。哪怕你是个美术或策划想通过技术手段验证自己的点子跟着这个流程走一遍也能对游戏开发的完整链路有个清晰的认知。我们会从最朴素的键盘控制移动开始逐步加入重力、跳跃、攻击最后形成一个有模有样的战斗框架。过程中踩过的坑、优化的小技巧我都会毫无保留地分享出来。2. 核心系统设计思路与框架搭建在动手敲代码之前花点时间理清思路比盲目开干重要十倍。一个结构混乱的动作游戏项目后期添加新技能或修改手感会是灾难。我的核心设计哲学是高内聚低耦合数据驱动。2.1 核心组件职责划分我习惯将游戏角色Player拆分成多个独立的脚本组件各司其职而不是写一个几千行的“PlayerController”巨无霸。主要组件如下PlayerInputHandler输入处理器这是一个“中间层”。它不直接执行移动或攻击而是负责监听键盘、手柄的原始输入并将其转化为一个干净的、易于理解的“命令”信号。例如它将“A键按下”转化为MoveInput -1将“J键按下”转化为AttackButtonDown true。这样做的好处是未来如果你想切换为手柄操作或者支持按键重映射只需要修改这个脚本其他逻辑完全不用动。PlayerMovement移动控制器这是游戏手感的核心。它接收来自InputHandler的移动指令并综合角色的当前状态是否着地、是否受伤、是否在攻击硬直中通过修改Rigidbody2D的速度或直接施加力来计算最终位移。这里会处理水平移动、跳跃、下蹲、爬梯等所有与位移相关的逻辑。一个关键技巧将移动速度、跳跃力、空中控制系数等参数公开为[SerializeField]这样你可以在Unity编辑器里实时调整快速微调手感无需反复编译代码。PlayerCombat战斗控制器负责管理攻击逻辑。它维护一个攻击指令队列处理连招序列管理攻击动画的播放并在攻击动画的特定帧通过Animation Events触发激活攻击碰撞框。它需要与PlayerMovement通信例如在攻击时限制移动或在受击时打断攻击。PlayerStateMachine角色状态机这是控制角色行为的“大脑”。角色在任何时刻都处于一个明确的状态如Idle待机、Running奔跑、Jumping跳跃、Attacking攻击、Hurt受伤等。状态机定义了状态之间的转换条件例如从Idle到Running的条件是MoveInput ! 0 IsGrounded。每个状态有自己的进入、更新、退出逻辑。使用状态机可以让复杂的角色行为变得清晰、可维护是动作游戏的标配。Health/Damage System生命/伤害系统一个相对独立的系统负责计算和管理角色的生命值。它提供一个公共方法如TakeDamage(int amount, GameObject damageSource)当攻击碰撞框检测到命中时就调用目标的这个方法。这里可以扩展出暴击、伤害类型、无敌帧等复杂机制。2.2 物理与动画的协作框架横版动作游戏离不开物理和动画的紧密配合。我的方案是物理驱动为主动画跟随之角色的核心位移位置变化由Rigidbody2D物理引擎计算。这能免费获得逼真的重力、碰撞反馈处理斜坡、移动平台也更自然。动画系统Animator则负责表现让角色的Sprite精灵图根据物理状态播放对应的动画。例如Rigidbody2D.velocity.y 0时播放跳跃上升动画 0时播放下落动画。使用Animation Events动画事件这是连接动画和游戏逻辑的桥梁。在攻击动画的某一帧比如武器挥到最远处插入一个事件触发PlayerCombat脚本里的OnAttackHitFrame()方法在这个方法里才激活攻击碰撞体。这样可以确保攻击判定和视觉表现完美同步避免“刀光没到伤害已出”的诡异情况。分层动画控制使用Animator的动画层Layers和遮罩Avatar Masks。例如基础层控制身体的移动动画走、跑、跳上层控制上半身的攻击动画。这样角色可以在移动的同时进行攻击两个动画互不干扰组合出更丰富的表现。3. 基础移动系统实现详解基础移动是游戏手感的基石一个“跟脚”的角色能让玩家瞬间沉浸一个“漂移”或“迟滞”的角色则会让玩家立刻放弃。我们一步步来构建它。3.1 物理组件配置与地面检测首先为角色GameObject添加Rigidbody2D和Collider2D推荐CapsuleCollider2D对斜坡处理更好。在Rigidbody2D组件中将Body Type设置为DynamicGravity Scale重力缩放设为3这是一个常用的值比默认的1下落更快手感更干脆。将Collision Detection碰撞检测设为Continuous连续检测防止高速移动时穿墙。冻结Z轴旋转防止角色被撞得东倒西歪。地面检测是移动的逻辑前提。我强烈推荐使用“射线检测Raycast”或“盒子检测BoxCast”而非单纯检查Rigidbody2D.IsTouchingLayers。后者在从平台边缘掉落时有延迟手感不跟手。// 在PlayerMovement脚本中 [SerializeField] private Transform groundCheckPoint; // 脚底下的一个空物体 [SerializeField] private float groundCheckRadius 0.1f; [SerializeField] private LayerMask groundLayer; // 在Unity编辑器中指定哪些层是地面 private bool isGrounded; void CheckGrounded() { // 在groundCheckPoint位置检测一个半径为groundCheckRadius的圆是否与groundLayer接触 Collider2D[] colliders Physics2D.OverlapCircleAll(groundCheckPoint.position, groundCheckRadius, groundLayer); isGrounded colliders.Length 0; // 只要检测到任何一个地面碰撞体就算着地 // 额外技巧如果你希望从平台边缘下落更迅速可以在检测到地面时将刚体的y轴速度设为一个很小的负值模拟“压实”地面的感觉。 if (isGrounded rigidbody.velocity.y 0) { rigidbody.velocity new Vector2(rigidbody.velocity.x, -0.5f); } }注意groundLayer一定要在Unity的Layer设置中预先定义好如“Ground”、“Platform”并在检视面板中勾选。不要把整个场景都设为地面层。3.2 平滑的水平移动控制水平移动的核心是修改Rigidbody2D.velocity的x分量。但直接设置一个固定值会显得非常生硬。我们需要模拟一个加速和减速的过程。[Header(移动参数)] [SerializeField] private float maxRunSpeed 8f; [SerializeField] private float groundAcceleration 50f; // 地面加速度 [SerializeField] private float groundDeceleration 70f; // 地面减速度通常大于加速度停下更快 [SerializeField] private float airAccelerationFactor 0.6f; // 空中控制系数0-1之间 private float targetSpeed; // 目标速度 private float currentSpeed; // 当前计算出的速度 void CalculateHorizontalMovement() { // 1. 确定目标速度 targetSpeed moveInput * maxRunSpeed; // moveInput来自InputHandler范围[-1, 1] // 2. 计算加速度该加速还是减速 float acceleration (Mathf.Abs(targetSpeed) 0.01f) ? groundAcceleration : groundDeceleration; // 如果在空中控制力减弱 if (!isGrounded) { acceleration * airAccelerationFactor; } // 3. 使用Mathf.MoveTowards平滑过渡到目标速度 // 这是实现平滑手感的关键比直接 Lerp 或 SmoothDamp 在物理更新中更可控 currentSpeed Mathf.MoveTowards(rigidbody.velocity.x, targetSpeed, acceleration * Time.fixedDeltaTime); // 4. 应用速度但保留y轴速度重力影响 rigidbody.velocity new Vector2(currentSpeed, rigidbody.velocity.y); }为什么用Mathf.MoveTowards因为它能提供一个线性的、可预测的速度变化曲线方便我们精确控制加速时间和停止距离。Time.fixedDeltaTime是固定物理帧间隔时间在FixedUpdate中使用能保证物理计算的稳定性不受帧率波动影响。3.3 跳跃手感的多维度调优跳跃是动作游戏的灵魂。一个“好跳”需要满足响应即时、力度可控、空中灵活、落地稳定。[Header(跳跃参数)] [SerializeField] private float jumpForce 13f; // 初始跳跃力 [SerializeField] private float jumpCutMultiplier 0.5f; // 跳跃键提前松开时的减速乘子 [SerializeField] private float coyoteTime 0.1f; // 土狼时间离地后短暂内仍可跳跃 [SerializeField] private float jumpBufferTime 0.1f; // 跳跃缓冲落地前按跳跃键落地后自动跳起 [SerializeField] private int maxAirJumps 1; // 允许的空中跳跃次数二段跳 private bool isJumping; private float lastGroundedTime; // 记录最后一次着地的时间 private float lastJumpPressedTime; // 记录最后一次按下跳跃键的时间 private int airJumpCount; void UpdateTimers() { // 每帧更新计时器用于实现土狼时间和跳跃缓冲 if (isGrounded) { lastGroundedTime coyoteTime; airJumpCount 0; // 重置空中跳跃计数 } else { lastGroundedTime - Time.deltaTime; } if (jumpButtonPressed) { lastJumpPressedTime jumpBufferTime; } else { lastJumpPressedTime - Time.deltaTime; } } void HandleJump() { // 跳跃条件判断满足土狼时间或跳跃缓冲并且在地面或空中跳跃次数未满 bool canNormalJump lastGroundedTime 0 !isJumping; bool canAirJump (airJumpCount maxAirJumps) lastJumpPressedTime 0; if (canNormalJump || canAirJump) { // 执行跳跃 rigidbody.velocity new Vector2(rigidbody.velocity.x, 0); // 取消当前y轴速度使每次跳跃高度一致 rigidbody.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse); // 使用冲量模式瞬间施加力 isJumping true; lastGroundedTime 0; // 消耗土狼时间 lastJumpPressedTime 0; // 消耗跳跃缓冲 if (!isGrounded) { airJumpCount; } } // 跳跃键提前松开实现小跳跳得低 if (isJumping rigidbody.velocity.y 0 !jumpButtonHeld) { rigidbody.velocity new Vector2(rigidbody.velocity.x, rigidbody.velocity.y * jumpCutMultiplier); } // 如果已经开始下落重置跳跃状态 if (rigidbody.velocity.y 0) { isJumping false; } }土狼时间Coyote Time和跳跃缓冲Jump Buffer是现代平台游戏的标准配置能极大提升操作容错率和手感流畅度。前者让玩家在离开平台边缘的瞬间按下跳跃仍能起跳后者让玩家在落地前几帧按下跳跃角色一接触地面就会自动跳起。这两个小技巧能让你游戏的跳跃手感立刻提升一个档次。4. 战斗系统核心机制拆解移动解决了接下来就是让角色“能打”。战斗系统不仅仅是播放一个动画它涉及输入响应、连招管理、伤害判定、受击反馈等多个子系统。4.1 基于状态机的攻击逻辑管理我们使用一个简单的状态机来管理攻击状态。状态包括None无攻击Attack1Attack2Attack3假设是三连击SpecialAttack重击/技能。public enum AttackState { None, Attack1, Attack2, Attack3, Special } private AttackState currentAttackState AttackState.None; private float attackComboResetTimer; // 连招重置计时器 private bool attackInputReceived; // 标记是否有攻击输入 void UpdateAttackState() { // 连招重置如果一段时间内没有进行下一次攻击则重置连招 if (currentAttackState ! AttackState.None) { attackComboResetTimer - Time.deltaTime; if (attackComboResetTimer 0) { currentAttackState AttackState.None; } } // 检测攻击输入 if (attackButtonPressed) { attackInputReceived true; } } void FixedUpdateAttack() { if (!attackInputReceived || !CanStartNewAttack()) return; // 检查是否可攻击如是否处于硬直、受伤状态 attackInputReceived false; AttackState nextState GetNextAttackState(); // 根据当前状态决定下一段攻击 switch (nextState) { case AttackState.Attack1: StartCoroutine(PerformAttack(Attack1, 0.3f, 0.1f)); // 攻击动画名总时长伤害判定帧时间 currentAttackState AttackState.Attack1; attackComboResetTimer 0.5f; // 设置0.5秒的连招窗口 break; case AttackState.Attack2: StartCoroutine(PerformAttack(Attack2, 0.4f, 0.15f)); currentAttackState AttackState.Attack2; attackComboResetTimer 0.5f; break; // ... 其他攻击状态 } } private bool CanStartNewAttack() { // 检查角色是否处于可攻击状态是否着地、是否在受伤、是否在其它动作硬直中 return isGrounded !isHurt currentAttackState ! AttackState.Special; // 举例重击不能取消 } private AttackState GetNextAttackState() { // 简单的连招序列逻辑 switch (currentAttackState) { case AttackState.None: return AttackState.Attack1; case AttackState.Attack1: return AttackState.Attack2; case AttackState.Attack2: return AttackState.Attack3; case AttackState.Attack3: return AttackState.Special; // 第三击后接重击 default: return AttackState.Attack1; // 默认回到第一击 } }4.2 攻击判定的精准实现攻击判定是战斗系统的核心。我们使用一个GameObject作为攻击碰撞体通常是一个细长的矩形或与武器形状匹配的碰撞体默认禁用在攻击动画的特定帧激活。步骤一设置攻击碰撞体在角色手上或武器上创建一个子物体挂上BoxCollider2D或PolygonCollider2D将其设为Is Trigger触发器。为它添加一个脚本例如AttackHitBox。步骤二通过动画事件触发在Unity动画窗口中选中攻击动画在你想让攻击生效的那一帧比如武器挥到水平时添加一个事件。在函数选择处选择你PlayerCombat脚本中的ActivateHitBox方法。// PlayerCombat.cs [SerializeField] private GameObject attackHitBox; // 拖入攻击碰撞体对象 [SerializeField] private float attackDamage 10f; public void ActivateHitBox() { attackHitBox.SetActive(true); // 可以在这里播放武器划过的粒子特效或音效 } public void DeactivateHitBox() { attackHitBox.SetActive(false); } // AttackHitBox.cs void OnTriggerEnter2D(Collider2D other) { // 确保不会打到自己 if (other.gameObject player) return; // 检查击中的物体是否有生命值组件 Health otherHealth other.GetComponentHealth(); if (otherHealth ! null) { // 传递伤害和攻击者信息 otherHealth.TakeDamage(attackDamage, player); // 可以在这里触发击中停顿Hit Stop、屏幕抖动、打击音效等 Debug.Log($Hit {other.name} for {attackDamage} damage!); } }步骤三加入攻击方向与位移为了让攻击更有力度通常会让角色在攻击时有一个小幅度的前冲。IEnumerator PerformAttack(string animationTrigger, float totalDuration, float hitFrameTime) { // 播放攻击动画 animator.SetTrigger(animationTrigger); // 攻击前冲在攻击开始时给予一个短暂的x轴速度 float attackMoveSpeed 2f; // 前冲速度 rigidbody.velocity new Vector2(transform.localScale.x * attackMoveSpeed, rigidbody.velocity.y); // 根据角色面向方向 // 限制玩家在攻击动画期间的输入移动可选增强打击感 movement.CanMove false; // 等待到伤害判定帧 yield return new WaitForSeconds(hitFrameTime); // 此时动画事件会调用ActivateHitBox() // 等待攻击动画剩余部分 yield return new WaitForSeconds(totalDuration - hitFrameTime); // 攻击结束恢复移动 movement.CanMove true; DeactivateHitBox(); }4.3 受击反馈与生命值系统被打中要有反馈否则战斗毫无实感。受击反馈包括角色后仰/击退、屏幕特效、无敌帧、伤害数字等。生命值脚本Health.cspublic class Health : MonoBehaviour { [SerializeField] private int maxHealth 100; private int currentHealth; private bool isInvulnerable false; // 无敌状态标志 private float invulnerabilityDuration 0.5f; // 受击后无敌时间 void Start() { currentHealth maxHealth; } public void TakeDamage(int damage, GameObject damageSource) { if (isInvulnerable || currentHealth 0) return; currentHealth - damage; Debug.Log(${gameObject.name} took {damage} damage. Health: {currentHealth}); // 触发受击反馈 StartCoroutine(HurtFeedback(damageSource)); if (currentHealth 0) { Die(); } } IEnumerator HurtFeedback(GameObject damageSource) { // 1. 视觉反馈闪烁通过修改SpriteRenderer的color SpriteRenderer sr GetComponentSpriteRenderer(); Color originalColor sr.color; sr.color Color.red; // 变红 yield return new WaitForSeconds(0.1f); sr.color originalColor; // 2. 物理反馈击退 Rigidbody2D rb GetComponentRigidbody2D(); if (rb ! null) { Vector2 knockbackDirection (transform.position - damageSource.transform.position).normalized; knockbackDirection.y 0.3f; // 给一点向上的击飞 rb.velocity Vector2.zero; // 先清零速度 rb.AddForce(knockbackDirection * 5f, ForceMode2D.Impulse); // 施加击退力 } // 3. 进入无敌状态 isInvulnerable true; yield return new WaitForSeconds(invulnerabilityDuration); isInvulnerable false; } void Die() { Debug.Log(${gameObject.name} died!); // 播放死亡动画销毁对象触发游戏结束逻辑等 // animator.SetTrigger(Die); // Destroy(gameObject, 1f); } }5. 动画状态机与视觉表现整合Animator Controller是Unity动画系统的核心它像一个流程图控制着动画片段之间的切换。对于横版动作游戏一个清晰的状态机至关重要。5.1 构建角色主状态机在Animator窗口中创建以下状态StatesIdle待机Run奔跑Jump跳跃上升Fall下落Attack1,Attack2,Attack3,SpecialAttack攻击Hurt受击Die死亡然后创建状态之间的转换Transitions。转换的条件Conditions通常基于脚本传递给Animator的参数Parameters。常用的参数类型有Float如Speed速度大小用于Idle和Run之间的混合。Bool如IsGrounded,IsAttacking。Trigger如JumpTrigger,HurtTrigger用于触发一次性的状态转换。关键技巧使用Blend Tree混合树平滑移动动画对于Idle到Run的过渡不要简单地在两个动画片段间切换那样会生硬。创建一个Blend Tree以Speed参数为阈值混合Idle动画和Run动画。当Speed接近0时播放Idle随着Speed增大逐渐过渡到Run动画。这样角色的起步和停止会非常自然。5.2 动画事件与脚本通信如前所述动画事件是连接动画和逻辑的桥梁。除了触发攻击判例外它还可以用于脚步声在跑步动画的特定帧触发脚步声效。尘土粒子在跳跃落地帧触发尘土飞扬的特效。结束攻击状态在攻击动画的最后一帧发送事件通知PlayerCombat脚本攻击动作结束可以接收下一个输入。在动画窗口添加事件非常简单选中动画片段将时间线拖到目标帧点击“Add Event”按钮然后从下拉菜单中选择你的脚本和方法。5.3 解决常见动画融合问题问题攻击动画播放时移动动画还在播放导致角色“滑步”。解决使用动画层和遮罩。创建一个只控制上半身骨骼的动画层Layer专门播放攻击动画。基础层Base Layer控制下半身的移动Idle, Run, Jump, Fall。这样角色下半身可以正常移动或跳跃而上半身独立播放攻击动作互不干扰。在Animator中创建新层为其设置一个Avatar Mask只勾选上半身的骨骼。问题从跳跃状态直接进入攻击状态动画切换突兀。解决调整状态转换的“退出时间Exit Time”和“过渡持续时间Transition Duration”。不要总是使用“Has Exit Time”。对于跳跃到攻击这类转换可以设置条件为IsGrounded false AttackTrigger并给予一个较短的过渡时间如0.1秒让系统能平滑地混合两个动画。6. 性能优化与扩展思路当基础功能都实现后我们需要考虑代码的健壮性和项目的可扩展性为添加更多内容如敌人AI、技能系统、关卡道具打下基础。6.1 对象池管理攻击特效与弹道频繁实例化Instantiate和销毁Destroy游戏对象如攻击火花、子弹、伤害数字是性能杀手。对象池Object Pooling是标准解决方案。// 一个简单的对象池示例 public class ObjectPool : MonoBehaviour { public GameObject prefab; public int initialSize 10; private QueueGameObject pool new QueueGameObject(); void Start() { for (int i 0; i initialSize; i) { CreateNewObject(); } } private GameObject CreateNewObject() { GameObject obj Instantiate(prefab); obj.SetActive(false); obj.transform.SetParent(transform); // 统一管理 pool.Enqueue(obj); return obj; } public GameObject GetObject() { if (pool.Count 0) { CreateNewObject(); } GameObject obj pool.Dequeue(); obj.SetActive(true); return obj; } public void ReturnObject(GameObject obj) { obj.SetActive(false); pool.Enqueue(obj); } }使用时需要生成特效的地方调用pool.GetObject()特效播放完毕后调用pool.ReturnObject()。6.2 使用ScriptableObject管理游戏数据将角色的属性血量、攻击力、移动速度和技能数据伤害、冷却时间、特效预制体从脚本中剥离出来用ScriptableObject来管理。这有三大好处非程序员友好策划或美术可以在Unity中创建和调整数据资产无需修改代码。易于复用和平衡可以轻松创建多个敌人类型它们共享同一套数据结构但数值不同。热重载在编辑器模式下修改ScriptableObject的数据游戏运行时能立即生效方便调试。创建一个CharacterStats的ScriptableObject[CreateAssetMenu(fileName NewCharacterStats, menuName Game/Character Stats)] public class CharacterStats : ScriptableObject { public int maxHealth; public float moveSpeed; public float jumpForce; public int baseAttackDamage; // ... 其他属性 }在角色的脚本中声明一个public CharacterStats stats;然后将创建好的数据资产拖拽赋值即可。6.3 输入系统的抽象与多平台适配Unity的新输入系统Input System Package比传统的Input.GetKey更强大、更灵活。它允许你定义“Action”如“Move”、“Jump”、“Attack”然后为每个Action绑定多种输入设备键盘、手柄、触摸屏的按键。这样切换输入设备或修改按键配置变得极其简单。尽早接入新输入系统虽然初期学习有点成本但长期来看会让你的项目更专业也为未来移植到主机或手机平台铺平道路。核心是创建一个PlayerInput组件和对应的Input Actions资产。7. 开发中高频问题与调试技巧在实际开发中你一定会遇到各种奇怪的问题。这里记录了几个我踩过的大坑和解决方法。问题一角色卡在斜坡或平台边缘抖动不止。原因Rigidbody2D的碰撞体与斜坡的碰撞体持续发生微小穿透和修正。解决检查角色和地面的碰撞体确保没有过于尖锐的边缘。使用CapsuleCollider2D通常比BoxCollider2D对斜坡更友好。在Rigidbody2D组件上适当增加Collision Detection的预算默认是10可以尝试提高到30但会略微增加性能开销。最有效的一招在移动代码中当检测到水平输入为0时玩家没按方向键将刚体的x轴速度直接设为0而不是让它自然减速。这能立刻消除因微小速度导致的抖动。if (Mathf.Abs(moveInput) 0.01f isGrounded) { rigidbody.velocity new Vector2(0, rigidbody.velocity.y); }问题二攻击有时会打中有时会穿过敌人。原因帧率问题。攻击碰撞体激活的帧通过动画事件可能刚好在物理更新帧之间敌人的碰撞体还没来得及更新位置。解决确保攻击碰撞体是Trigger并使用OnTriggerEnter2D进行检测它对快速移动的物体更敏感。稍微增大攻击碰撞体的尺寸或者在攻击判定的那一帧使用Physics2D.OverlapBoxAll或Physics2D.OverlapCircleAll进行一次物理查询而不是依赖单帧的触发检测。这能捕捉到那一瞬间所有在范围内的目标。public void ActivateHitBox() { Collider2D[] hits Physics2D.OverlapBoxAll(hitBoxTransform.position, hitBoxSize, 0, targetLayer); foreach (var hit in hits) { // 处理伤害 } }问题三二段跳后角色在空中还能无限跳。原因地面检测逻辑有误。可能你的地面检测射线/盒子范围太大或者检测频率不对。解决在场景视图中将groundCheckPoint的Gizmos辅助图标画出来确保其大小和位置准确通常就在脚底略下方一点。确保groundLayer设置正确没有把敌人、道具等层也包含进去。在FixedUpdate中进行地面检测而不是Update以保证与物理更新同步。添加一个调试信息在屏幕上实时打印isGrounded的值观察其变化是否符合预期。调试利器自定义Debug绘制在OnDrawGizmos或OnDrawGizmosSelected方法中使用Gizmos.DrawWireSphere,Gizmos.DrawWireCube等函数将你的检测范围、攻击范围可视化。这是调试碰撞和物理问题最直观的方法。void OnDrawGizmosSelected() { if (groundCheckPoint ! null) { Gizmos.color Color.green; Gizmos.DrawWireSphere(groundCheckPoint.position, groundCheckRadius); } // 绘制攻击范围 Gizmos.color Color.red; Gizmos.DrawWireCube(hitBoxTransform.position, hitBoxSize); }走到这一步你已经拥有了一个角色能跑、能跳、能打出连招、会有受击反馈的横版动作游戏核心框架。这个框架就像一副坚实的骨架接下来你要做的就是为它填充血肉——设计有趣的敌人、构建精巧的关卡、添加丰富的技能和装备系统。记住手感调优是一个持续的过程多玩自己的游戏多找朋友测试收集反馈反复打磨。每一个优秀的独立游戏都是从这样一个看似简单但足够扎实的核心循环开始的。

相关新闻