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

Unity中如何在编辑器或运行时检测层级对象删除并更新Inspector交互物品列表,同时同步IK注视目标列表?

Hey Daniel, let's tackle these two problems efficiently—no more janky Update loops or stale cluttering your inspector! Here's a polished, performant solution tailored to your workflow:


1. Auto-Clean Missing References in InteractableItems List

Instead of polling every frame, we'll leverage editor events and efficient list operations to keep your references fresh. We'll also add safeguards for runtime.

Updated InteractableItems Script

using System.Collections.Generic;
using UnityEngine;
using System;

public class InteractableItems : MonoBehaviour
{
    public List<GameObject> interactableItems = new List<GameObject>();
    public event Action OnItemsUpdated; // Triggered when list changes

    private void Awake()
    {
        #if UNITY_EDITOR
        // Listen for hierarchy changes (e.g., object deletion) in editor
        UnityEditor.EditorApplication.hierarchyChanged += CleanupMissingReferences;
        #endif
    }

    private void OnDestroy()
    {
        #if UNITY_EDITOR
        UnityEditor.EditorApplication.hierarchyChanged -= CleanupMissingReferences;
        #endif
    }

    // Efficiently remove all null references in one pass
    public void CleanupMissingReferences()
    {
        interactableItems.RemoveAll(item => item == null);
        OnItemsUpdated?.Invoke(); // Notify listeners of list change
    }

    // Centralized method to add items (avoids duplicates)
    public void AddInteractableItem(GameObject item)
    {
        if (item != null && !interactableItems.Contains(item))
        {
            interactableItems.Add(item);
            OnItemsUpdated?.Invoke();
        }
    }
}

Updated InteractableItemsEditor Script

using UnityEditor;
using UnityEditorInternal;
using UnityEngine;

[CustomEditor(typeof(InteractableItems))]
public class InteractableItemsEditor : Editor
{
    private ReorderableList _myList;
    private InteractableItems _target;

    [MenuItem("GameObject/Make Object Interactable", false, 30)]
    public static void GeneratePickupItems()
    {
        if (Selection.gameObjects.Length == 0) return;

        // Auto-create manager if none exists
        var interactableManager = FindObjectOfType<InteractableItems>();
        if (interactableManager == null)
        {
            var managerObj = new GameObject("Interactable Items Manager");
            interactableManager = managerObj.AddComponent<InteractableItems>();
        }

        foreach (var obj in Selection.gameObjects)
        {
            obj.tag = "Interactable Item";
            interactableManager.AddInteractableItem(obj);
        }

        EditorUtility.SetDirty(interactableManager); // Mark for save
    }

    public void OnEnable()
    {
        _target = (InteractableItems)target;
        _myList = new ReorderableList(serializedObject, serializedObject.FindProperty("interactableItems"))
        {
            draggable = false,
            displayAdd = false,
            displayRemove = false,
            drawHeaderCallback = rect => EditorGUI.LabelField(rect, "Interactable Items", EditorStyles.boldLabel),
            drawElementCallback = (rect, index, isActive, isFocused) =>
            {
                if (index >= _myList.serializedProperty.arraySize) return;
                var element = _myList.serializedProperty.GetArrayElementAtIndex(index);
                var originalColor = GUI.color;

                EditorGUI.BeginDisabledGroup(true);
                {
                    // Highlight missing references in red
                    GUI.color = element.objectReferenceValue == null ? Color.red : Color.green;
                    EditorGUI.PropertyField(new Rect(rect.x, rect.y, rect.width - 20, EditorGUIUtility.singleLineHeight), element, GUIContent.none);
                }
                EditorGUI.EndDisabledGroup();
                GUI.color = originalColor;
            }
        };
    }

    public override void OnInspectorGUI()
    {
        serializedObject.Update();

        // Clean references every time inspector draws
        _target.CleanupMissingReferences();

        _myList.DoLayoutList();

        // Manual cleanup button for user convenience
        if (GUILayout.Button("Cleanup Missing References"))
        {
            _target.CleanupMissingReferences();
            EditorUtility.SetDirty(_target);
        }

        serializedObject.ApplyModifiedProperties();
    }
}

2. Sync InteractableItems with IKControl's Look Targets

Instead of maintaining two separate lists, we'll have IKControl directly use the InteractableItems list. This eliminates sync issues entirely.

Updated IKControl Script

using UnityEngine;

[RequireComponent(typeof(Animator))]
public class IKControl : MonoBehaviour
{
    public InteractableItems interactableItemsManager; // Reference to our manager
    public float weightDamping = 1.5f;
    private Animator animator;
    private Transform lastPrimaryTarget;
    private float lerpEndDistance = 0.1f;
    private float finalLookWeight = 0;
    private bool transitionToNextTarget = false;

    void Start()
    {
        animator = GetComponent<Animator>();
        
        // Auto-find manager if not assigned
        if (interactableItemsManager == null)
        {
            interactableItemsManager = FindObjectOfType<InteractableItems>();
        }
    }

    void OnAnimatorIK()
    {
        // Exit early if no manager or items exist
        if (interactableItemsManager == null || interactableItemsManager.interactableItems.Count == 0)
        {
            ResetLookWeight();
            return;
        }

        Transform primaryTarget = null;
        float closestLookWeight = 0;

        // Use the shared interactable items list directly
        foreach (var item in interactableItemsManager.interactableItems)
        {
            if (item == null) continue; // Double safety check

            Vector3 lookAt = item.transform.position - transform.position;
            lookAt.y = 0f;
            float dotProduct = Vector3.Dot(new Vector3(transform.forward.x, 0f, transform.forward.z).normalized, lookAt.normalized);
            float lookWeight = Mathf.Clamp(dotProduct, 0f, 1f);

            if (lookWeight > closestLookWeight)
            {
                closestLookWeight = lookWeight;
                primaryTarget = item.transform;
            }
        }

        // Existing IK logic, now using the shared list
        if (primaryTarget != null)
        {
            if ((lastPrimaryTarget != null) && (lastPrimaryTarget != primaryTarget) && (finalLookWeight > 0f))
            {
                transitionToNextTarget = true;
            }
        }

        if ((primaryTarget != null) && !transitionToNextTarget)
        {
            lastPrimaryTarget = primaryTarget;
            finalLookWeight = Mathf.Lerp(finalLookWeight, closestLookWeight, Time.deltaTime * weightDamping);
            float bodyWeight = finalLookWeight * .75f;
            animator.SetLookAtWeight(finalLookWeight, bodyWeight, 1f);
            animator.SetLookAtPosition(primaryTarget.position);
        }

        if ((primaryTarget == null && lastPrimaryTarget != null) || transitionToNextTarget)
        {
            finalLookWeight = Mathf.Lerp(finalLookWeight, 0f, Time.deltaTime * weightDamping);
            float bodyWeight = finalLookWeight * .75f;
            animator.SetLookAtWeight(finalLookWeight, bodyWeight, 1f);
            animator.SetLookAtPosition(lastPrimaryTarget.position);

            if (finalLookWeight < lerpEndDistance)
            {
                transitionToNextTarget = false;
                finalLookWeight = 0f;
                lastPrimaryTarget = null;
            }
        }
    }

    // Helper to reset look weight when no targets exist
    private void ResetLookWeight()
    {
        if (finalLookWeight > 0)
        {
            finalLookWeight = Mathf.Lerp(finalLookWeight, 0f, Time.deltaTime * weightDamping);
            float bodyWeight = finalLookWeight * .75f;
            animator.SetLookAtWeight(finalLookWeight, bodyWeight, 1f);
            if (lastPrimaryTarget != null)
            {
                animator.SetLookAtPosition(lastPrimaryTarget.position);
            }
            if (finalLookWeight < lerpEndDistance)
            {
                finalLookWeight = 0f;
                lastPrimaryTarget = null;
            }
        }
    }
}

Key Improvements:

  • Performance: Uses List.RemoveAll() instead of looping and removing items one by one (way faster for large lists)
  • Editor Integration: Listens to hierarchy changes to auto-clean references immediately when objects are deleted
  • No Sync Issues: IKControl directly uses the shared InteractableItems list, so you never have mismatched targets
  • Safety: Adds null checks everywhere to prevent runtime errors
  • User-Friendly: Highlights missing references in red and adds a manual cleanup button

内容的提问来源于stack exchange,提问作者Daniel Lip

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.29 00:42:44