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

Unity3D中为单个对象复用按钮的问题求助

问题:可复用交互按钮始终固定在第一个挂载对象位置

我是Unity新手,正在制作一款简单游戏,想要实现一个可复用的"查看"按钮——玩家接触对象时,按钮显示在对应对象的上方。但实际测试时发现,碰撞触发后按钮只会显示在第一个挂载该脚本的对象位置,哪怕把脚本挂载到其他对象上,按钮仍会固定在第一个对象处。

原脚本代码:

using UnityEngine;
using UnityEngine.UI;

public class InteractNPC : MonoBehaviour
{
    //public Button UI;
    [SerializeField] GameObject uiUse, nameBtn, dialogBox;
    [SerializeField] TextAsset characterData;
    // [SerializeField] UnityEngine.UI.Text dialogMessage;
    private Transform head;
    private Vector3 offset = new Vector3(0, 1.0f, 0);

    // Start is called before the first frame update
    void Start()
    {
        //uiUse = Instantiate(UI, FindObjectOfType<Canvas>().transform).GetComponent<Button>();
        uiUse.gameObject.SetActive(true);
        head = this.transform.GetChild(0);
        uiUse.transform.position = Camera.main.WorldToScreenPoint(head.position + offset);
        nameBtn.transform.position = Camera.main.WorldToScreenPoint(head.position + offset);

        nameBtn.GetComponent<Button>().onClick.AddListener(onNameClick);
    }

    // Update is called once per frame
    void Update()
    {
        uiUse.transform.position = Camera.main.WorldToScreenPoint(head.position + offset);
        nameBtn.transform.position = Camera.main.WorldToScreenPoint(head.position + offset);
    }

    private void OnTriggerEnter(Collider collisionInfo)
    {
        if (collisionInfo.CompareTag("Player"))
        {
            //Character characterJson = JsonUtility.FromJson<Character>(characterData.text);

            nameBtn.gameObject.SetActive(true);
            // Text lbl = nameBtn.gameObject.GetComponentInChildren(typeof(Text), true) as Text;
            // lbl.text = "BOBO";
            // nameBtn.GetComponent<Button>().GetComponentInChildren<Text>().text = characterJson.name;
        }
    }

    private void OnTriggerExit(Collider collisionInfo)
    {
        if (collisionInfo.CompareTag("Player"))
        {
            nameBtn.gameObject.SetActive(false);
        }
    }

    // DIALOGUE SYSTEM
    public void onNameClick()
    {
        Text dialogMessage, dialogName;
        if (dialogBox.gameObject.activeInHierarchy)
        {  
            dialogName = GameObject.Find("Canvas/DialogBox/DialogueName").GetComponent<Text>();
            dialogMessage = GameObject.Find("Canvas/DialogBox/Dialogue").GetComponent<Text>();
            if (dialogMessage != null && dialogName != null)
            {
                loadCharacterData(dialogMessage, dialogName);
                Debug.Log("not null dialog message");
            }
            else
            {
                Debug.Log("null dialog message");
            }
        }
    }

    public void loadCharacterData(Text dialogMessage, Text dialogName)
    {
        Character characterJson = JsonUtility.FromJson<Character>(characterData.text);
        dialogName.text = characterJson.name;

        dialogMessage.text = characterJson.dialogs[0].choices[1];
    }
}

问题根源

你当前的uiUse和nameBtn是直接拖拽了场景中同一个UI实例到所有脚本的字段中,所有NPC脚本都在修改同一个UI对象的位置。由于Update方法的执行顺序,最后生效的是第一个挂载脚本的对象位置,导致按钮看起来固定在那里。


解决方案

方案1:为每个NPC实例化独立的专属按钮(推荐)

将按钮做成预制体,每个NPC启动时创建属于自己的按钮实例,保证每个按钮只对应一个NPC:

using UnityEngine;
using UnityEngine.UI;

public class InteractNPC : MonoBehaviour
{
    // 改为拖拽按钮预制体,而非场景中的现有实例
    [SerializeField] GameObject uiUsePrefab, nameBtnPrefab, dialogBox;
    [SerializeField] TextAsset characterData;
    private Transform head;
    private Vector3 offset = new Vector3(0, 1.0f, 0);
    // 存储当前NPC的专属按钮实例
    private GameObject _uiUse, _nameBtn;
    private Camera _mainCam;

    void Start()
    {
        // 缓存主相机,避免Update中频繁查找
        _mainCam = Camera.main;
        head = transform.GetChild(0);
        
        // 获取场景中的Canvas
        Canvas canvas = FindObjectOfType<Canvas>();
        // 实例化专属按钮,父对象设为Canvas
        _uiUse = Instantiate(uiUsePrefab, canvas.transform);
        _nameBtn = Instantiate(nameBtnPrefab, canvas.transform);
        
        // 默认隐藏按钮
        _uiUse.SetActive(false);
        _nameBtn.SetActive(false);
        
        // 绑定点击事件
        _nameBtn.GetComponent<Button>().onClick.AddListener(onNameClick);
    }

    void Update()
    {
        // 仅当按钮激活时更新位置
        if (_uiUse.activeSelf)
            _uiUse.transform.position = _mainCam.WorldToScreenPoint(head.position + offset);
        if (_nameBtn.activeSelf)
            _nameBtn.transform.position = _mainCam.WorldToScreenPoint(head.position + offset);
    }

    private void OnTriggerEnter(Collider collisionInfo)
    {
        if (collisionInfo.CompareTag("Player"))
        {
            _nameBtn.SetActive(true);
            _uiUse.SetActive(true);
            // 可选:设置NPC名字到按钮文本
            // _nameBtn.GetComponentInChildren<Text>().text = "你的NPC名字";
        }
    }

    private void OnTriggerExit(Collider collisionInfo)
    {
        if (collisionInfo.CompareTag("Player"))
        {
            _nameBtn.SetActive(false);
            _uiUse.SetActive(false);
        }
    }

    // 对话系统代码保持不变
    public void onNameClick()
    {
        Text dialogMessage, dialogName;
        if (dialogBox.gameObject.activeInHierarchy)
        {  
            dialogName = GameObject.Find("Canvas/DialogBox/DialogueName").GetComponent<Text>();
            dialogMessage = GameObject.Find("Canvas/DialogBox/Dialogue").GetComponent<Text>();
            if (dialogMessage != null && dialogName != null)
            {
                loadCharacterData(dialogMessage, dialogName);
                Debug.Log("not null dialog message");
            }
            else
            {
                Debug.Log("null dialog message");
            }
        }
    }

    public void loadCharacterData(Text dialogMessage, Text dialogName)
    {
        Character characterJson = JsonUtility.FromJson<Character>(characterData.text);
        dialogName.text = characterJson.name;

        dialogMessage.text = characterJson.dialogs[0].choices[1];
    }
}

方案2:按钮对象池(适合大量NPC场景)

如果你的游戏中有大量NPC,频繁实例化/销毁UI会有性能开销,可以提前创建一批按钮存入对象池,玩家触发NPC时取出闲置按钮绑定到当前NPC,离开时将按钮归还池并隐藏。


额外注意事项

  • 确保每个NPC的Collider勾选了IsTrigger,玩家对象带有Rigidbody组件(否则触发事件无法触发)
  • 场景中只能有一个Camera标记为MainCamera,否则Camera.main会返回null
  • 避免在Update中频繁调用GameObject.Find,可以提前缓存对话UI的引用

内容的提问来源于stack exchange,提问作者Richard Johnson

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 04:30:30