ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Unity投掷斧头技能系统实现:状态机、对象池与自动回收机制

Unity投掷斧头技能系统实现:状态机、对象池与自动回收机制 最近在做一个动作类角色技能迭代时遇到一个非常典型的战斗机制需求角色可以把手里的斧头直接扔出去斧头在空中旋转后落在地上玩家走过去靠近斧头就能捡回手里如果一直不去捡斧头在场上停留几秒后又会自动飞回角色身边。这类机制在动作游戏、MOBA、割草类 Roguelike 里都很常见但真正动手实现时你会发现它并不仅仅是“发射一颗子弹”那么简单。从需求描述来看核心只有三件事扔斧头、走过去拾取、自动回收。可这三件事背后牵扯到投掷物的状态管理、物理碰撞、触发检测、对象池、人物回调甚至还有动画和音效的衔接。如果你的项目里恰好也有类似的武器投掷、可回收道具、宠物回收、飞镖回旋等玩法需求这篇教程应该能帮你少走不少弯路。本文会基于 Unity C# 从零实现一套完整的“投掷斧头”系统包含状态机设计、物理碰撞与拾取触发分离、对象池复用、玩家投掷入口、自动回收逻辑以及常见问题和工程化建议。代码都是可以直接复制的你把它整理成预制体后稍作参数调整就能在项目中跑起来。1. 背景与核心概念1.1 这个技能机制到底是什么先来还原一下核心需求“新版牛魔可以扔斧头了可以走过去拾取也可以自动回收。”这句话拆开看其实包含三个连续的行为阶段投掷阶段角色把一个斧头投掷物丢出去斧头沿某个方向飞行同时伴随旋转表现。落地交互阶段斧头碰撞到地面、墙壁或敌人后停止飞行以“落在地面”的形态留在场景中。回收阶段玩家主动靠近斧头触发拾取或者等待计时器到期后斧头自动飞回角色手中。这套机制的特点是投掷物不会像普通子弹那样命中后立刻销毁而是作为场景中的一个可交互实体保留一段时间。玩家可以主动走过去捡起来也可以什么都不做系统会自动回收。这在玩法层面给了玩家两种操作策略近距离捡斧头更快远距离等自动回收更省事但代价是技能空窗期变长。1.2 为什么不能当作普通子弹实现不少同学第一次接到这种需求时会想到直接拿一个预制体 Instantiate然后用 Rigidbody 的 velocity 让它飞出去再在碰撞回调里把场景中的预制体 Destroy 掉。这个过程对于“一次性投射物”确实够用但对“斧头需要留在场地上等玩家捡”的场景就不合适了。主要原因有三个第一斧头落地后必须继续存在于场景中而且状态要从“飞行”切换成“停留可拾取”。如果一碰撞就销毁玩家就没有东西可以捡。第二回收逻辑不是简单的“倒计时销毁”斧头要从地面回到角色手里这需要它保持独立对象身份并且能在飞行、落地、回归三种模式之间来回切换。第三频繁 Instantiate 和 Destroy 会带来性能和 GC 压力。像斧头这种高频投掷且生命周期短的技能正确做法是使用对象池而不是每投一次就创建一次、每回收一次就销毁一次。1.3 核心难点在哪里结合我自己的实现经验这套机制真正的难点在于状态管理。斧头在一个完整的技能周期里会经历在人物手持位置InHand被投掷出去Flying碰撞后停留Landed自动回归人物Returning这四种状态之间互相切换而且切换条件不同飞行状态下碰撞才会进入停留停留状态下过了等待时间才进入回归回归到角色手上后就回到手持状态。如果不用状态机而是零零散散地写 if 判断代码很容易在两个逻辑分支互相拉扯时出问题。2. 环境准备与版本说明2.1 开发环境本文的示例基于 Unity 开发使用的语言是 C#。如果你用的是 Godot、Unreal 或者其他自研引擎只要理解状态机设计和物理触发分离这两条核心思路迁移过去并不困难。我这里演示的环境以 Unity 2022.3 LTS 为例渲染管线使用内置渲染管线或者 URP 均可。需要注意Unity 的版本迭代比较快不同版本之间的 API 有一定差异建议你以自己本机实际安装的版本为准。你需要准备的工具如下Unity Hub 与 Unity 编辑器一个 3D 空场景内置管线 URP 都行Visual Studio 或 Rider 作为 C# 脚本编辑器一个带 Collider 的地面和墙体对象用于测试投掷落点2.2 示例项目目录结构为了让代码结构更清晰建议先划分好项目的目录。我这里采用 Unity 常见的目录规范Assets/ ├── Scenes/ │ └── Main.unity ├── Prefabs/ │ └── Axe.prefab ├── Scripts/ │ ├── PlayerAxeThrow.cs │ ├── AxeController.cs │ ├── AxePool.cs │ └── SimplePlayerMotor.cs ├── Materials/ │ └── AxeMaterial.mat其中 Scenes 目录放测试场景Prefabs 目录放斧头预制体Scripts 目录放四个核心脚本Materials 目录用来放斧头外观材质。你完全可以根据自己的命名习惯调整但尽量保持脚本文件独立、职责清晰。2.3 场景中的基础要素这个示例场景至少需要以下内容地面任意带 BoxCollider 或 MeshCollider 的平面物体。墙体一个或多个带碰撞体的测试障碍物用于验证斧头撞击后落地。玩家角色一个带碰撞体的物体可以是一个 Capsule用来移动和投掷。主相机用于获得玩家的投掷朝向同时在玩家操作时提供视角。玩家的移动部分我会写一个最简单的脚本不引入复杂的第三人称控制器避免干扰技能系统的核心内容。3. 核心原理拆解3.1 投掷斧头的生命周期一个完整的投掷斧头生命周期可以简单概括为下面这条链路玩家发起投掷 → 斧头从手持位置进入飞行状态 → 飞行中检测物理碰撞 → 碰撞后进入停留状态 → 玩家靠近拾取或者自动回收 → 斧头回到手持位置 → 等待下一次投掷。这里最关键的一点是斧头对象在整个生命周期中始终复用。投掷时它不是从零创建而是从对象池取出回收时也不是销毁而是暂时失活等待下一次使用。这种“取出-使用-归还”的模式是投掷物类技能的核心设计思想。3.2 物理碰撞与拾取交互为什么要分离在实现过程中我发现一个容易踩坑的地方如果只用一个 Collider 同时承担“物理碰撞”和“拾取触发”两个职责就会遇到逻辑互相冲突的问题。飞行阶段斧头需要作为物理物体与地面、墙体发生碰撞此时它需要的是一个普通的物理 Collider碰撞事件通过OnCollisionEnter回调处理。落地阶段斧头需要感知玩家是否靠近此时它需要的是一个 Trigger 触发器通过OnTriggerEnter回调判断玩家是否进入拾取范围。问题在于同一个 Collider 很难同时做好这两件事。如果把 Collider 设置成 Trigger斧头会直接穿过地面飞出去如果不设置成 Trigger玩家走过来时又无法通过触发事件检测到拾取。因此最好的做法是给斧头预制体配置两个独立的碰撞体物理碰撞体一个 BoxCollider飞行阶段启用落地阶段禁用。拾取触发器一个 BoxCollider设置为 isTrigger落地阶段启用飞行阶段禁用。这样两个阶段互不干扰物理碰撞只发生在飞行中拾取触发只发生在停留后。3.3 自动回收的触发条件自动回收本质上是一个简单的计时器逻辑斧头进入 Landed 状态时记录当前时间然后每一帧检查当前时间与记录时间的差值一旦超过设定的等待时间就切换到 Returning 状态。具体的回归方式有两种常见做法直线飞回斧头直接向角色当前位置插值移动实现简单。曲线飞回斧头先飞回角色位置同时配合旋转和轨迹特效手感更好。教程里我会用最实用的直线飞回作为基础实现然后在最佳实践里说明如何扩展成曲线效果。3.4 对象池在投掷物系统中的角色对象池的核心思想是避免在游戏过程中反复创建和销毁对象而是预先创建一批实例存放起来用的时候激活不用的时候失活。具体到斧头技能玩家投掷时从池中取出一个未激活的斧头实例设置位置、速度后再激活。斧头被拾取或自动回收后把斧头对象失活归还给池。如果池中所有斧头都被占用了就创建一个新实例并加入池中。这样可以减少运行时实例化带来的性能开销尤其适合投掷类、弹幕类、武器回收类技能。4. 完整实战案例4.1 搭建 Unity 场景首先创建一个新的 3D 场景命名为 Main然后添加以下基础对象在 Hierarchy 中右键 → 3D Object → Plane命名为 Ground作为地面。右键 → 3D Object → Cube放在地面上方作为障碍物或测试墙体。右键 → 3D Object → Capsule命名为 Player作为玩家角色并把它的 Tag 设置为 Player。再创建一个空对象命名为 GameManager然后把后面要写的AxePool脚本挂到它上面。这样场景中的对象池就有了一个统一的持有者。4.2 配置斧头预制体创建一个斧头预制体是整套系统的关键。你可以在场景中先做一个临时物体配置完成后再拖进 Prefabs 目录生成预制体。斧头对象的层级结构如下Axe ├── AxeMesh斧头的外形网格可以是 Cube 组合或任意 3D 模型 └── PickupZone拾取检测子物体具体配置步骤创建一个空物体命名为 Axe。给 Axe 挂上 Rigidbody 组件。给 Axe 添加一个 BoxCollider这个名字随意但它是物理碰撞体不需要勾选 isTrigger。在 Axe 下创建一个子物体命名为 PickupZone。给 PickupZone 添加一个 BoxCollider把 isTrigger 勾选上并调整它的 Size 稍微大于斧头本体让玩家靠近一点就能拾取。给 Axe 添加一个简单的外观例如用两个 Cube 拼成斧头形状或者直接使用你自己项目的斧头模型。在 Axe 上挂载AxeController脚本。创建材质放在 Materials 目录拖到斧头网格上。把整个 Axe 对象拖到 Prefabs 目录生成斧头预制体然后删除场景中的临时 Axe 对象。这里要注意物理碰撞体不能放在子物体的同时又把 Rigidbody 挂在父物体上吗可以。但如果你把物理碰撞体放在子物体上需要确保子物体的 Collider 也被代码正确识别到。我后面提供的自动识别逻辑会扫描所有子物体上的 Collider所以两种放置方式都能兼容。4.3 编写 AxeController.cs核心逻辑现在进入代码部分。先定义一个接口用于让斧头在回到玩家手上时通知玩家。这个接口可以直接放在AxeController.cs文件里。using UnityEngine; // 斧头拥有者需要实现的接口 public interface IAxeOwner { void OnAxeRecovered(); }接口的作用是解耦斧头与玩家。斧头只需要知道“我的主人实现了 OnAxeRecovered 方法”不需要关心主人具体是玩家还是怪物也不需要通过类型转换直接操作玩家的属性。这在多人持有斧头或 AI 角色使用斧头时会非常方便。接下来是斧头控制器的完整代码。using UnityEngine; public enum AxeState { InHand, // 在角色手中 Flying, // 飞行中 Landed, // 落地停留等待拾取 Returning // 自动飞回角色 } public class AxeController : MonoBehaviour { [Header(自动回收参数)] [Tooltip(斧头落地后多少秒没被拾取就自动飞回角色)] public float autoReturnDelay 5f; [Header(回归速度)] [Tooltip(自动飞回角色时的移动速度)] public float returnSpeed 10f; public AxeState currentState AxeState.InHand; private Rigidbody rb; private Collider physicalCollider; private Collider pickupCollider; private Transform owner; private IAxeOwner ownerInterface; private float landedTime; private Vector3 lastVelocity; void Awake() { rb GetComponentRigidbody(); // 自动识别物理碰撞体和拾取触发体 Collider[] colliders GetComponentsInChildrenCollider(); foreach (Collider col in colliders) { if (col.isTrigger) { pickupCollider col; } else { physicalCollider col; } } } void Update() { switch (currentState) { case AxeState.Flying: // 兜底判断如果速度几乎为 0 但没有触发碰撞自动进入落地状态 if (rb.velocity.sqrMagnitude 0.01f) { EnterLandedState(); } break; case AxeState.Landed: // 等待玩家拾取超时后自动回收 if (Time.time - landedTime autoReturnDelay) { StartReturn(); } break; case AxeState.Returning: MoveToOwner(); break; } } void FixedUpdate() { // 在 FixedUpdate 中记录飞行速度供碰撞落地时修正朝向 if (currentState AxeState.Flying) { lastVelocity rb.velocity; } } // 对外投掷入口 public void Throw(Transform owner, Vector3 velocity) { this.owner owner; ownerInterface owner.GetComponentIAxeOwner(); currentState AxeState.Flying; rb.isKinematic false; rb.velocity velocity; rb.angularVelocity new Vector3(0, 0, 900f); // 高速飞行时建议使用连续动态检测减少穿透 rb.collisionDetectionMode CollisionDetectionMode.ContinuousDynamic; SetPhysicalColliderEnabled(true); SetPickupColliderEnabled(false); } // 物理碰撞回调飞行中碰到地面/墙壁后进入落地状态 void OnCollisionEnter(Collision collision) { if (currentState ! AxeState.Flying) return; EnterLandedState(); } // 触发回调停留状态下玩家进入拾取范围后捡起斧头 void OnTriggerEnter(Collider other) { if (currentState ! AxeState.Landed) return; if (other.CompareTag(Player)) { PickUp(); } } // 进入落地停留状态 private void EnterLandedState() { rb.isKinematic true; rb.velocity Vector3.zero; rb.angularVelocity Vector3.zero; currentState AxeState.Landed; landedTime Time.time; SetPhysicalColliderEnabled(false); SetPickupColliderEnabled(true); // 让斧头朝向飞行方向看起来更自然 if (lastVelocity.sqrMagnitude 0.01f) { transform.rotation Quaternion.LookRotation(lastVelocity.normalized, Vector3.up); } } // 启动自动回收 private void StartReturn() { currentState AxeState.Returning; rb.isKinematic true; SetPhysicalColliderEnabled(false); SetPickupColliderEnabled(false); } // 自动回收过程中向角色位置移动 private void MoveToOwner() { if (owner null) { PickUp(); return; } Vector3 targetPos owner.position owner.up * 1.2f; transform.position Vector3.MoveTowards( transform.position, targetPos, returnSpeed * Time.deltaTime ); if (Vector3.Distance(transform.position, targetPos) 0.15f) { PickUp(); } } // 拾取完成斧头回到手中失活并从场景中隐藏 private void PickUp() { currentState AxeState.InHand; gameObject.SetActive(false); // 通知所有者斧头已经回来了 if (ownerInterface ! null) { ownerInterface.OnAxeRecovered(); } } private void SetPhysicalColliderEnabled(bool enabled) { if (physicalCollider ! null) physicalCollider.enabled enabled; } private void SetPickupColliderEnabled(bool enabled) { if (pickupCollider ! null) pickupCollider.enabled enabled; } }这段代码是整个技能系统的核心我建议你重点理解状态切换的流程Throw是外部入口投掷后进入 Flying 状态此时启用物理碰撞体、关闭拾取触发器。OnCollisionEnter在飞行中被触发一旦撞击到物体就调用EnterLandedState进入落地状态。落地状态中每帧检查是否超过自动回收时间超时则调用StartReturn。玩家靠近时OnTriggerEnter检测到 Player 标签并调用PickUp。这里有个细节rb.collisionDetectionMode CollisionDetectionMode.ContinuousDynamic只在初始投掷时设置一次。如果你之后把斧头拿回手里再投掷这个属性会一直保留所以不需要每次都重复设置。我把它放在Throw方法内是为了初始化时的确定性实际项目中也可以放在Awake中。4.4 编写 PlayerAxeThrow.cs投掷入口接下来是玩家投掷脚本它负责接收鼠标输入、从对象池中取斧头、设置方向和力度。using UnityEngine; public class PlayerAxeThrow : MonoBehaviour, IAxeOwner { [Header(投掷参数)] public Transform handPoint; public float throwForce 18f; private GameObject currentAxe; void Update() { if (Input.GetMouseButtonDown(0)) { TryThrow(); } } private void TryThrow() { // 如果手上已经有一个激活状态的斧头说明还没回收回来不能投掷 if (currentAxe ! null currentAxe.activeSelf) { return; } currentAxe AxePool.Instance.GetAxe(); currentAxe.transform.position handPoint.position; Vector3 direction Camera.main.transform.forward; AxeController axeController currentAxe.GetComponentAxeController(); axeController.Throw(transform, direction * throwForce); } // 斧头回收后回调可以在这里播放音效或恢复技能冷却 public void OnAxeRecovered() { // 默认没有额外逻辑子类或事件系统可以扩展 } }这段代码的逻辑比较简单TryThrow首先判断手上是不是已经有激活的斧头防止玩家连续投掷多把斧头导致回收逻辑混乱。从对象池中取斧头后先把手持点作为斧头的初始位置。投掷方向取相机正前方保证斧头朝向玩家准星方向飞行。如果你希望斧头不依赖相机而是从角色正前方飞出也可以把方向改成transform.forward。两种方式所见即所得按项目手感选择即可。4.5 编写 AxePool.cs对象池管理对象池管理器负责对斧头实例的复用。using UnityEngine; using System.Collections.Generic; public class AxePool : MonoBehaviour { public static AxePool Instance; [Header(对象池配置)] public GameObject axePrefab; public int preloadCount 8; private readonly ListGameObject pool new ListGameObject(); void Awake() { if (Instance ! null) { Destroy(gameObject); return; } Instance this; // 预创建一批斧头实例并统一设为失活状态 for (int i 0; i preloadCount; i) { GameObject axe CreateAxe(); axe.SetActive(false); } } private GameObject CreateAxe() { GameObject axe Instantiate(axePrefab, transform); pool.Add(axe); return axe; } // 从池中取出一把斧头 public GameObject GetAxe() { foreach (GameObject axe in pool) { if (!axe.activeSelf) { axe.SetActive(true); return axe; } } // 池中没有可用实例时自动扩容 GameObject newAxe CreateAxe(); newAxe.SetActive(true); return newAxe; } }对象池的GetAxe方法比较简单暴力线性遍历池找到一个未激活的斧头就返回如果全部都在使用中就创建一个新的。对于预加载数量为 8 的斧头池来说线性遍历的性能开销完全可以接受。这里需要注意Unity 里对象的激活和失活是SetActive(true/false)这和 Git 里的“池”不是一个概念。你只需要保证斧头失活时对象仍在场景中只是被隐藏了而不是被销毁。4.6 编写简单玩家移动代码为了让玩家可以“走过去拾取斧头”需要一个最基础的移动脚本。using UnityEngine; public class SimplePlayerMotor : MonoBehaviour { public float moveSpeed 5f; public float rotateSpeed 120f; void Update() { float h Input.GetAxis(Horizontal); float v Input.GetAxis(Vertical); // 左右旋转角色 transform.Rotate(Vector3.up, h * rotateSpeed * Time.deltaTime); // 前后移动角色 transform.Translate(Vector3.forward * v * moveSpeed * Time.deltaTime); } }把SimplePlayerMotor挂到 Player 对象上再把PlayerAxeThrow也挂到 Player 上。这样玩家就可以用 WASD 控制移动、用鼠标左键投掷斧头了。4.7 运行验证与预期表现所有代码写完后回到 Unity 编辑器把脚本组件挂载到对应对象上Axe 预制体挂载AxeController。Player 对象挂载PlayerAxeThrow、SimplePlayerMotor并确保它带有 Collider。GameManager 对象挂载AxePool并把斧头预制体拖到axePrefab字段中。确保玩家对象的 Tag 设置为 Player。点击 Play 运行预期表现如下鼠标左键斧头从手持点飞出并在飞行过程中持续旋转。斧头撞到障碍物或地面停止运动停留在撞击位置附近朝向飞行方向。玩家不靠近斧头等待 5 秒后斧头自动飞回玩家身边然后消失在手中。玩家主动靠近斧头一旦进入 PickupZone 的触发范围斧头立即回到手中。斧头回收后玩家可以再次点击鼠标左键继续投掷。如果以上表现都符合预期说明这套投掷斧头技能系统已经跑通了基本流程。5. 常见问题与排查思路在实现过程中最容易出现的几个问题我整理成了表格方便你快速定位。问题现象常见原因解决思路斧头直接穿透地面或墙体飞行速度过快碰撞检测模式不合适设置 ContinuousDynamic并给斧头增加一个“射线补偿”或“胶囊碰撞体”斧头落地后玩家走过来没有拾取拾取触发器没有启用或者玩家 Tag 不正确检查 Landed 状态是否启用了 pickupCollider检查玩家 Tag 是否为 Player斧头拾取后无法再次投掷currentAxe 活跃状态判断有误或者回调没有触发检查 OnAxeRecovered 是否触发检查对象池取出的实例是否处于激活状态自动回收后斧头没有回到手中owner 为空或者目标点计算异常检查 Throw 方法是否正确传入了 owner Transform斧头把玩家自己砸死了物理碰撞矩阵允许斧头与玩家碰撞在 Layer Collision Matrix 中关闭斧头与玩家所在层的碰撞下面挑几个重点场景展开说明。5.1 斧头穿透问题斧头飞行速度较快时物理引擎可能因为单帧内位移距离过大导致碰撞检测没有捕捉到与墙体的交叉点出现穿透现象。解决方案有三层第一层把rb.collisionDetectionMode设置为CollisionDetectionMode.ContinuousDynamic这是 Unity 物理引擎针对高速运动物体的连续检测策略能有效减少隧穿。第二层不要使用非常薄的碰撞体尤其是斧头刀刃部分如果用很薄的 BoxCollider高速飞行时更容易穿透。建议把碰撞体稍微加厚一点。第三层如果投掷距离非常远、速度极高物理引擎仍然不可靠就需要在固定更新频率下做射线检测补偿先检测前方障碍物再手动把斧头移动到碰撞点。5.2 拾取功能不触发斧头落地后无法拾取最常见的检查顺序如下先确认EnterLandedState是否被调用可以通过在方法里加 Debug.Log 查看。再确认落地后物理碰撞体是否已经关闭、拾取触发器是否已经启用。如果 PickupZone 的 Collider 没有勾选 isTrigger或者层级结构中没有这个子物体脚本在Awake阶段识别到的pickupCollider可能为空。然后检查玩家的 Tag。OnTriggerEnter里我用的是other.CompareTag(Player)如果玩家对象的 Tag 没有设置为 Player判断会失败。最后检查玩家对象是否有 Collider。拾取触发检测需要两个物体都有 Collider而且至少一方带 Rigidbody。斧头本身带 Rigidbody玩家有 Collider 即可。5.3 单次循环投掷多次如果玩家可以在斧头未回收时反复投掷多半是currentAxe的状态判断失效。我的判断条件是if (currentAxe ! null currentAxe.activeSelf) { return; }当斧头被拾取或自动回收时PickUp会执行gameObject.SetActive(false)所以activeSelf变成 false判断条件自然失效。如果你在项目里改用了其他回收方式比如只移动位置而没有失活对象那么这个判断条件也要同步调整。6. 最佳实践与工程建议6.1 优先使用对象池而不是频繁创建销毁在投掷类技能中对象池是标配。每次投掷都 Instantiate、每次回收都 Destroy不仅会产生大量的 GC 压力还可能在短时间内让场景中的临时对象数量暴增。用对象池之后你的斧头实例数量会被控制在一个固定范围运行时更平稳。如果技能有多个阵营使用比如玩家和怪物都会扔斧头可以为不同阵营配置独立的对象池避免串用。6.2 物理碰撞和拾取触发严格分离这是我踩坑最深的点。一个 Collider 同时承担物理碰撞和触发拾取必然会在状态切换时出现各种诡异问题。正确做法是把物理碰撞体放在斧头根部把拾取触发体放在子物体上并且根据状态开关各自的 enabled。飞行阶段只保留物理碰撞体落地阶段关闭物理碰撞体、开启拾取触发器自动回归阶段两个都关闭。这样从物理引擎的角度看斧头在回归过程中不会和任何墙体发生多余的碰撞。6.3 代码结构尽量紧凑且可扩展我上面给出的代码没有引入复杂的 ScriptableObject 或者组件系统但在你扩展技能树时可以考虑把斧头的参数提取到一个配置对象中例如飞行速度、旋转速度、自动回收等待时间、拾取范围等统一存到技能配置表中。这样策划调整数值时就不需要打开代码去改字段了。另外IAxeOwner接口的设计非常值得保留。它不仅适用于玩家也可以让 AI 角色实现同样的接口。当斧头回到 AI 手里时AI 可以根据自己的行为树继续处理技能冷却逻辑。6.4 手感调优和表现层优化投掷斧头这类技能手感非常重要。几个常见的调优点飞行速度太快会让玩家看不清飞出去的方向太慢又缺乏打击感一般建议 15 到 25 之间根据项目格子大小调整。自动回收等待时间这个时间实际上决定了玩家“走过去拾取”的收益。等待时间短玩家就会懒得走过去等待时间长技能空窗期就会变大。要根据玩法的爽感合理取值推荐 3 到 6 秒。旋转速度旋转速度可以通过rb.angularVelocity设置900 只是示例值。如果你想让斧头甩出去时更“重”可以降低旋转速度并配合摄像机震动或音效。回收路径直线飞回虽然简单但视觉上比较干硬。可以在 MoveToOwner 中使用二次贝塞尔曲线让斧头先向侧面甩一下再旋转回手手感会明显提升。表现层方面可以考虑给斧头挂一个 TrailRenderer在飞行和回归时留下残影落地时可以播放一个尘土粒子特效回归到手时播放金属音效。这些都是低成本高收益的优化。6.5 联机项目要额外注意状态同步如果你的项目是多人联网游戏这套本地逻辑还需要增加一层网络状态同步。投掷斧头的时间、斧头当前状态、落地位置、回收触发者等信息应该由服务器或主机权威决定客户端只负责表现。其中最需要注意的是不要在客户端本地直接执行拾取逻辑要等服务器确认拾取者之后再由服务器广播结果。自动回收倒计时应在服务器上运行防止客户端变速或暂停导致回收时间不一致。对象池的实例 ID 需要在网络层区分避免两个客户端复用了同一个斧头对象造成语义混乱。6.6 防止角色自伤如果斧头在飞行阶段可以被玩家的碰撞体拦截就会出现角色自己扔出去的斧头砸死自己的情况。处理方式很简单在 Unity 的 Edit → Project Settings → Physics 中调整 Layer Collision Matrix单独为斧头设置一个 Axe 层为玩家设置 Player 层并取消 Axe 与 Player 的碰撞。斧头仍然可以和 Ground、Enemy 等层正常碰撞。如果你不想新增层也可以在OnCollisionEnter中判断碰撞对象是否带有“Owner”标识从逻辑上忽略角色自身的碰撞。但层碰撞矩阵的性能更高也更干净。7. 总结与学习路线这篇教程从一个很具体的技能需求入手完整实现了“投掷斧头 → 落地停留 → 玩家拾取或自动回收”的闭环。你可以在自己的 Unity 工程里直接复用AxeController、PlayerAxeThrow、AxePool三个核心脚本配合预制体配置和对象池管理让一个近战角色迅速拥有这套远程压制技能。代码虽然是围绕“斧头”写的但核心思路可以平移到很多玩法里投掷飞刀、回旋镖、可回收的特殊子弹、放置型陷阱道具本质上都在处理“飞行状态、停留状态、回收状态”三阶段切换。只要掌握状态机思维就能举一反三。接下来如果继续深入我建议你按这个顺序进阶先完善表现层加入拖尾、音效、落点特效让技能“看起来很强”。然后扩展数值配置把投掷距离、自动回收延迟、拾取范围做成可配置数据接入技能面板或 Excel 配置表。之后可以加入技能冷却和连招逻辑比如斧头回归后短暂增加移速或攻击力。最后再考虑联机同步、动画状态机衔接、多角色共用同一套系统的问题。如果你在实现过程中遇到了其他奇怪的问题欢迎留言一起讨论。这个机制虽然看起来只是“扔一把斧头”但里面值得抠的细节确实不少。
返回列表