You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;
        }
    }
}
排查修复说明

核心问题分析

  1. 输入重复触发:原代码使用Input.GetKey处理拾取/放置操作,按住按键时会在每一帧重复执行逻辑,导致物品被多次放置/拾取,出现位置跳变。
  2. 物理惯性干扰:物品被放下时未清零速度,会因跟随玩家移动的惯性继续运动,回到玩家射线检测范围内,增加误触发概率。
  3. 状态同步不严谨:LateUpdate未对holdingItem做空引用检查,可能触发空指针异常;刚放下的物品会立即被射线检测到,容易被重新选中。

修复措施

  1. 替换输入检测方式:将Input.GetKey改为Input.GetKeyDown,确保每个操作只在按键按下的第一帧触发一次,避免重复执行。
  2. 消除物理惯性:在PutItem和DropItem中,将物品Rigidbody的velocity和angularVelocity清零,确保物品稳定停在目标位置。
  3. 优化状态检查逻辑:
    • 在LateUpdate中添加holdingItem != null的空引用检查,避免异常。
    • 添加协程IgnoreItemTemporarily,临时忽略刚操作的物品0.5秒,防止立即被重新选中。
  4. 简化拾取逻辑:原拾取代码依赖子物体索引获取物品,改为直接操作目标物体的transform,避免索引错误。

内容的提问来源于stack exchange,提问作者BF Bulut

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 05:00:56