Unity物品拾取放置系统BUG求助:物品放置后异常回退
问题描述
玩家对象下包含Head节点,Head节点下有Hand节点。按下E键可拾取物品,使其成为Hand的子物体跟随玩家移动;按下F键可将物品放置在当前看向的位置。大部分场景下功能正常,但偶尔会出现物品先移动到指定位置,随即回到Hand节点下并掉落的异常情况。
问题代码
using UnityEngine; public class Player : MonoBehaviour { Rigidbody rb; float horizontalAxis; float verticalAxis; float mouseHorizontal; float mouseVertical; public float moveSpeed; public float mouseSensivity; float mouseLimitX = 70; GameObject head; GameObject hand; CharacterController controller; public Material outlinerMaterial; // 状态变量 bool running = false; bool cursorLock = false; bool grounded = false; bool holding = false; // 能力参数 float runSpeed; float holdDistance = 4f; // 拾取系统相关 Renderer objectRenderer = null; Material[] originalMaterials = null; bool isLookingObject = false; GameObject lookingItem = null; GameObject holdingItem = null; Quaternion lookingItemRotation; void Start() { rb = GetComponent<Rigidbody>(); runSpeed = 0.7f; moveSpeed = 1f + runSpeed; mouseSensivity = 500f; head = GameObject.Find("Head"); hand = GameObject.Find("Hand"); head.transform.localEulerAngles = Vector3.zero; CursorLock(); } void FixedUpdate() { } private void Update() { GetCharAxis(); if (cursorLock) { InputController(); MouseLook(); HoldingControl(); Movement(); DebugList(); } } private void LateUpdate() { // 添加空引用检查,避免异常 if (holding && holdingItem != null) { holdingItem.transform.position = hand.transform.position; holdingItem.transform.localRotation = lookingItemRotation; } } void GetCharAxis() { horizontalAxis = Input.GetAxisRaw("Horizontal"); verticalAxis = Input.GetAxisRaw("Vertical"); mouseHorizontal = Input.GetAxis("Mouse X"); mouseVertical = Input.GetAxis("Mouse Y"); } void MouseLook() { Vector3 currentRotation = head.transform.localEulerAngles; float newRotationX = currentRotation.x - mouseVertical * Time.deltaTime * mouseSensivity; float newRotationY = currentRotation.y + mouseHorizontal * Time.deltaTime * mouseSensivity; newRotationX = Mathf.Repeat(newRotationX + 180f, 360f) - 180f; newRotationX = Mathf.Clamp(newRotationX, -mouseLimitX, mouseLimitX); head.transform.localEulerAngles = new Vector3(newRotationX, newRotationY, 0); } void Movement() { Vector3 moveDirection = head.transform.forward * verticalAxis + head.transform.right * horizontalAxis; moveDirection.y = 0f; moveDirection.Normalize(); rb.Move(rb.position + moveDirection * moveSpeed * Time.fixedDeltaTime, Quaternion.identity); } void HoldingControl() { RaycastHit hit; if (Physics.Raycast(head.transform.position, head.transform.forward, out hit, holdDistance)) { if (hit.collider.gameObject.CompareTag("Item") && holding == false) { // 忽略刚放下的物品,避免立即重新选中 if (lookingItem != hit.collider.gameObject && hit.collider.gameObject != holdingItem) { ResetLookingObject(); lookingItem = hit.collider.gameObject; lookingItemRotation = lookingItem.transform.localRotation; objectRenderer = lookingItem.GetComponent<Renderer>(); if (objectRenderer != null) { SetOutlineMaterial(); } } } else { ResetLookingObject(); } } else { ResetLookingObject(); } } void ResetLookingObject() { if (isLookingObject && lookingItem != null) { objectRenderer.materials = originalMaterials; lookingItem = null; objectRenderer = null; isLookingObject = false; originalMaterials = null; } } void SetOutlineMaterial() { originalMaterials = objectRenderer.materials; Material[] newMaterials = new Material[originalMaterials.Length + 1]; for (int i = 0; i < originalMaterials.Length; i++) { newMaterials[i] = originalMaterials[i]; } newMaterials[originalMaterials.Length] = outlinerMaterial; objectRenderer.materials = newMaterials; isLookingObject = true; } bool CursorLock() { if (cursorLock == false) { Cursor.lockState = CursorLockMode.Locked; Cursor.visible = false; cursorLock = true; } else if (cursorLock == true) { Cursor.lockState = CursorLockMode.None; Cursor.visible = true; cursorLock = false; } return cursorLock; } void InputController() { if (grounded) { HandleRunning(); // 改为GetKeyDown避免多次触发跳跃 if (Input.GetKeyDown(KeyCode.Space)) { rb.AddRelativeForce(new Vector3(0, 100, 0)); grounded = false; } HandleItemInteraction(); } } void HandleRunning() { if (Input.GetKey(KeyCode.LeftShift) && !running) { running = true; moveSpeed += runSpeed; } else if (!Input.GetKey(KeyCode.LeftShift) && running) { running = false; moveSpeed -= runSpeed; } } void HandleItemInteraction() { // 改为GetKeyDown,确保放置操作只触发一次 if (holdingItem != null && holding && Input.GetKeyDown(KeyCode.F)) { PutItem(); } // 改为GetKeyDown,确保拾取操作只触发一次 if (Input.GetKeyDown(KeyCode.E) && lookingItem != null && isLookingObject && !holding) { PickupItem(); } // 改为GetKeyDown,确保丢弃操作只触发一次 else if (Input.GetKeyDown(KeyCode.Q) && holding) { DropItem(); } } void PutItem() { RaycastHit hit; if (Physics.Raycast(head.transform.position, head.transform.forward, out hit, holdDistance)) { Debug.Log("PutItem work"); holdingItem.transform.SetParent(null); Rigidbody itemRB = holdingItem.gameObject.GetComponent<Rigidbody>(); itemRB.isKinematic = false; // 清零速度,消除惯性影响 itemRB.velocity = Vector3.zero; itemRB.angularVelocity = Vector3.zero; itemRB.gameObject.transform.position = hit.point + new Vector3(0,0.02f,0); GameObject justPutItem = holdingItem; holding = false; holdingItem = null; ResetLookingObject(); // 临时忽略刚放下的物品,避免立即重新选中 StartCoroutine(IgnoreItemTemporarily(justPutItem)); } else { Debug.Log("PutItem error"); } } void PickupItem() { Debug.Log("PickupItem work"); lookingItem.gameObject.transform.SetParent(hand.transform, false); Rigidbody itemRB = lookingItem.gameObject.GetComponent<Rigidbody>(); itemRB.isKinematic = true; // 直接操作目标物体transform,避免依赖子物体索引 lookingItem.transform.localPosition = Vector3.zero; holdingItem = lookingItem; ResetLookingObject(); holding = true; } void DropItem() { Debug.Log("DropItem work"); holdingItem.transform.SetParent(null); Rigidbody itemRB = holdingItem.gameObject.GetComponent<Rigidbody>(); itemRB.isKinematic = false; // 清零速度,消除惯性影响 itemRB.velocity = Vector3.zero; itemRB.angularVelocity = Vector3.zero; GameObject justDroppedItem = holdingItem; holding = false; holdingItem = null; ResetLookingObject(); // 临时忽略刚丢弃的物品,避免立即重新选中 StartCoroutine(IgnoreItemTemporarily(justDroppedItem)); } // 协程:临时忽略刚操作的物品,防止立即被重新选中 IEnumerator IgnoreItemTemporarily(GameObject item) { if (item == null) yield break; float ignoreDuration = 0.5f; while (ignoreDuration > 0) { ignoreDuration -= Time.deltaTime; if (lookingItem == item) { ResetLookingObject(); } yield return null; } } private void DebugList() { print($"Holding = {holding},\n LookingItem = {lookingItem}"); } private void OnCollisionExit(Collision other) { if (other.gameObject.CompareTag("Floor")) { grounded = false; } } private void OnCollisionStay(Collision other) { if (other.gameObject.CompareTag("Floor")) { grounded = true; } } }
排查修复说明
核心问题分析
- 输入重复触发:原代码使用
Input.GetKey处理拾取/放置操作,按住按键时会在每一帧重复执行逻辑,导致物品被多次放置/拾取,出现位置跳变。 - 物理惯性干扰:物品被放下时未清零速度,会因跟随玩家移动的惯性继续运动,回到玩家射线检测范围内,增加误触发概率。
- 状态同步不严谨:
LateUpdate未对holdingItem做空引用检查,可能触发空指针异常;刚放下的物品会立即被射线检测到,容易被重新选中。
修复措施
- 替换输入检测方式:将
Input.GetKey改为Input.GetKeyDown,确保每个操作只在按键按下的第一帧触发一次,避免重复执行。 - 消除物理惯性:在
PutItem和DropItem中,将物品Rigidbody的velocity和angularVelocity清零,确保物品稳定停在目标位置。 - 优化状态检查逻辑:
- 在
LateUpdate中添加holdingItem != null的空引用检查,避免异常。 - 添加协程
IgnoreItemTemporarily,临时忽略刚操作的物品0.5秒,防止立即被重新选中。
- 在
- 简化拾取逻辑:原拾取代码依赖子物体索引获取物品,改为直接操作目标物体的transform,避免索引错误。
内容的提问来源于stack exchange,提问作者BF Bulut
相关产品推荐
相关产品推荐

