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:
IKControldirectly uses the sharedInteractableItemslist, 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
相关产品推荐
相关产品推荐

