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

使用Extract Method重构:方法放当前类还是新类?Unity代码优化咨询

优化Unity Cutscene Editor的优雅方案

Hey there! I totally get your pain—Extract Method is great for readability, but it’s easy to end up with a "god class" that’s holding way too much responsibility. Let’s break down how to refactor this CutsceneEditor without adding unnecessary complexity, while leaning into SOLID principles and Unity’s editor ecosystem.

First, Let’s Diagnose the Problem

Your current CutsceneEditor is doing way too many things:

  • Managing editor state (foldouts, current selection, scroll offsets)
  • Rendering the entire Inspector UI (global tabs + per-sequence UI)
  • Handling sequence CRUD operations (add/remove/duplicate)
  • Drawing scene paths and managing their visibility
  • Manipulating serialized properties for sequences

This violates the Single Responsibility Principle—each class should have one job. Let’s split these responsibilities cleanly.


Step 1: Split Per-Sequence UI into a Sub-Editor

The bulk of your code is rendering individual cutscene sequences. Unity’s editor system supports creating sub-editors for collection elements, which is perfect here. Create a dedicated property drawer for CutsceneSequence:

#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(CutsceneSequence))]
public class CutsceneSequenceDrawer : PropertyDrawer
{
    private static readonly string[] actionOptions = {"Movement", "Poses"};
    private bool _isExpanded;
    private float _scrollbarOffset;

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        _isExpanded = EditorGUI.BeginFoldoutHeaderGroup(position, _isExpanded, $"Cutscene Action #{property.arrayIndex}");
        if (_isExpanded)
        {
            EditorGUI.indentLevel++;
            var actionSelection = property.FindPropertyRelative("actionSelectionIndex");
            var cutsceneAction = property.FindPropertyRelative("cutsceneAction");
            var timeToWait = property.FindPropertyRelative("timeToWait");
            var maxTimeToWait = property.FindPropertyRelative("maxTimeToWait");
            var hasDialogue = property.FindPropertyRelative("hasDialogue");
            var voiceClip = property.FindPropertyRelative("voiceClip");
            var voiceClipVolume = property.FindPropertyRelative("voiceClipVolume");
            var voiceClipDelay = property.FindPropertyRelative("voiceClipDelay");

            // Action selection row
            Rect actionRow = EditorGUI.IndentedRect(position);
            actionRow.height = EditorGUIUtility.singleLineHeight;
            EditorGUI.BeginProperty(actionRow, label, actionSelection);
            actionSelection.intValue = EditorGUI.Popup(actionRow, "Action Type", actionSelection.intValue, actionOptions);
            
            actionRow.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
            if (actionSelection.intValue == 0)
            {
                cutsceneAction.objectReferenceValue = EditorGUI.ObjectField(actionRow, "Movement Action", cutsceneAction.objectReferenceValue, typeof(MovementAction), false);
            }
            else
            {
                cutsceneAction.objectReferenceValue = EditorGUI.ObjectField(actionRow, "Pose Action", cutsceneAction.objectReferenceValue, typeof(PoseAction), false);
            }
            EditorGUI.EndProperty();

            // Time settings row
            actionRow.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
            EditorGUI.BeginChangeCheck();
            timeToWait.floatValue = EditorGUI.Slider(actionRow, "Wait Time", timeToWait.floatValue, 0, maxTimeToWait.floatValue);
            actionRow.x += 200;
            actionRow.width = 50;
            maxTimeToWait.floatValue = EditorGUI.FloatField(actionRow, maxTimeToWait.floatValue);
            if (EditorGUI.EndChangeCheck())
            {
                // Trigger path redraw via parent editor (we can add a callback for this)
                var editor = Editor.CreateEditor(property.serializedObject.targetObject);
                if (editor is CutsceneEditor cutsceneEditor)
                {
                    cutsceneEditor.RedrawPath(property.arrayIndex);
                }
                DestroyImmediate(editor);
            }

            // Voice options
            actionRow.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
            actionRow.x = EditorGUI.IndentedRect(position).x;
            actionRow.width = EditorGUI.IndentedRect(position).width;
            hasDialogue.boolValue = EditorGUI.ToggleLeft(actionRow, "Show Voice Options", hasDialogue.boolValue);
            
            if (hasDialogue.boolValue)
            {
                actionRow.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
                EditorGUI.PropertyField(actionRow, voiceClip);
                
                actionRow.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
                voiceClipVolume.floatValue = EditorGUI.Slider(actionRow, "Volume", voiceClipVolume.floatValue, 0, 2);
                
                actionRow.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
                voiceClipDelay.floatValue = EditorGUI.Slider(actionRow, "Delay", voiceClipDelay.floatValue, 0, maxTimeToWait.floatValue);
                
                if (voiceClip.objectReferenceValue != null)
                {
                    actionRow.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
                    if (GUI.Button(actionRow, "Remove Voice Clip"))
                    {
                        voiceClip.objectReferenceValue = null;
                        voiceClipVolume.floatValue = 1;
                        voiceClipDelay.floatValue = 0;
                    }
                }
            }

            // Sequence buttons (add/remove/duplicate)
            actionRow.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
            EditorGUI.BeginHorizontal(actionRow);
            if (GUILayout.Button("New Move"))
            {
                property.serializedObject.FindProperty("cutsceneSequences").InsertArrayElementAtIndex(property.arrayIndex);
                CutsceneSequenceDisplay.InitializeNewMove(property.serializedObject.FindProperty("cutsceneSequences").GetArrayElementAtIndex(property.arrayIndex + 1));
            }
            if (property.arrayIndex > 0)
            {
                if (GUILayout.Button("Duplicate"))
                {
                    property.serializedObject.FindProperty("cutsceneSequences").InsertArrayElementAtIndex(property.arrayIndex);
                }
                if (GUILayout.Button("Delete"))
                {
                    property.serializedObject.FindProperty("cutsceneSequences").DeleteArrayElementAtIndex(property.arrayIndex);
                }
            }
            EditorGUI.EndHorizontal();

            // Black separator box
            actionRow.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
            EditorGUI.DrawRect(actionRow, new Color(0,0,0,0.2f));

            EditorGUI.indentLevel--;
        }
        EditorGUI.EndFoldoutHeaderGroup();
    }

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        if (!_isExpanded) return EditorGUIUtility.singleLineHeight;
        var hasDialogue = property.FindPropertyRelative("hasDialogue").boolValue;
        var baseHeight = EditorGUIUtility.singleLineHeight * 8;
        var dialogueHeight = hasDialogue ? EditorGUIUtility.singleLineHeight * 4 : 0;
        return baseHeight + dialogueHeight + EditorGUIUtility.standardVerticalSpacing * (hasDialogue ? 10 : 6);
    }
}
#endif

This moves all per-sequence UI and button logic out of your main editor. Now your CutsceneEditor can just render the sequence list with a single PropertyField call!


Step 2: Extract Path Drawing to a Static Utility Class

All the scene path rendering logic (DrawCutscenePath, IsPathIsVisible, HideCutscenePath) doesn’t belong in the editor class—move it to a static utility:

public static class CutscenePathUtility
{
    public static bool IsPathVisible(GameObject parent)
    {
        if (parent == null) return false;
        return parent.transform.Cast<Transform>().Any(child => child.gameObject.activeSelf);
    }

    public static void TogglePathVisibility(GameObject parent)
    {
        if (parent == null) return;
        bool anyVisible = IsPathVisible(parent);
        foreach (Transform child in parent.transform)
        {
            child.gameObject.SetActive(!anyVisible);
        }
    }

    public static void DrawPath(int startIndex, SerializedProperty sequenceList, SerializedObject serializedObject, ref GameObject currentMarker)
    {
        GameObject[] parents = GameObject.FindGameObjectsWithTag("ParentUI");
        if (!IsPathVisible(GameObject.FindGameObjectWithTag("PathUI")) && parents.Length == 0)
            return;

        // Clean up existing path objects
        foreach (var parent in parents.Where(p => p.activeSelf))
        {
            Object.DestroyImmediate(parent);
        }

        // Draw new path segments
        for (int i = startIndex; i < sequenceList.arraySize; i++)
        {
            var sequence = sequenceList.GetArrayElementAtIndex(i);
            var action = sequence.FindPropertyRelative("cutsceneAction").objectReferenceValue;
            
            if (action is MovementAction)
            {
                CutsceneSequenceDisplay.ProcessMovementPathing(i, sequenceList, serializedObject, ref currentMarker);
            }
            else if (action is PoseAction)
            {
                CutsceneSequenceDisplay.ProcessPosePathing(i, sequenceList, serializedObject, ref currentMarker);
            }
        }
    }
}

Step 3: Simplify the Main CutsceneEditor

With all the heavy lifting split out, your main editor becomes lean and focused:

#if UNITY_EDITOR
[CustomEditor(typeof(Cutscene))]
public class CutsceneEditor : Editor
{
    private Cutscene _cutscene;
    private GameObject _pathParent;
    private SerializedProperty _startMarker;
    private SerializedProperty _sequenceList;
    private GameObject _currentMarker;

    void OnEnable()
    {
        _cutscene = (Cutscene)target;
        _pathParent = GameObject.FindGameObjectWithTag("PathUI");
        _startMarker = serializedObject.FindProperty("startMarker");
        _sequenceList = serializedObject.FindProperty("cutsceneSequences");

        if (!Application.isPlaying && CutscenePathUtility.IsPathVisible(_pathParent))
        {
            CutscenePathUtility.DrawPath(1, _sequenceList, serializedObject, ref _currentMarker);
        }
    }

    public override void OnInspectorGUI()
    {
        if (Application.isPlaying) return;

        // Draw global settings (start marker, action markers)
        DrawGlobalSettings();
        
        // Draw sequence list (now handled by the property drawer)
        EditorGUILayout.PropertyField(_sequenceList, true);
        
        // Draw path toggle button
        DrawPathToggleButton();

        serializedObject.ApplyModifiedProperties();
    }

    private void DrawGlobalSettings()
    {
        EditorGUILayout.LabelField("Cutscene Global Settings", EditorStyles.boldLabel);
        
        EditorGUILayout.BeginHorizontal();
        EditorGUILayout.PropertyField(_startMarker, new GUIContent("Start Point"));
        var actionMarker = serializedObject.FindProperty("actionMarker");
        var actionMarkerArrow = serializedObject.FindProperty("actionMarkerArrow");
        EditorGUILayout.PropertyField(actionMarker, new GUIContent("Action Marker"));
        EditorGUILayout.PropertyField(actionMarkerArrow, new GUIContent("Arrow"));
        EditorGUILayout.EndHorizontal();

        // Sync start position to marker
        if (_startMarker.objectReferenceValue != null)
        {
            var startObj = (GameObject)_startMarker.objectReferenceValue;
            var firstSequence = _sequenceList.GetArrayElementAtIndex(0);
            firstSequence.FindPropertyRelative("cutsceneStartPoint").vector3Value = startObj.transform.position;
            firstSequence.FindPropertyRelative("cutsceneStartRotation").quaternionValue = startObj.transform.rotation;
        }

        EditorGUILayout.Space(10);
    }

    private void DrawPathToggleButton()
    {
        EditorGUILayout.Space(40);
        string buttonText = CutscenePathUtility.IsPathVisible(_pathParent) ? "Hide Cutscene Path" : "Show Cutscene Path";
        if (GUILayout.Button(buttonText))
        {
            CutscenePathUtility.TogglePathVisibility(_pathParent);
        }
    }

    // Callback for path redraw from sequence drawer
    public void RedrawPath(int index)
    {
        CutscenePathUtility.DrawPath(index, _sequenceList, serializedObject, ref _currentMarker);
    }
}
#endif

Key Benefits of This Approach

  • Single Responsibility: Each class does one thing (editor handles overall UI, drawer handles sequence UI, utility handles path drawing)
  • Maintainability: If you need to tweak sequence UI, you only touch the drawer class. If path drawing breaks, you fix the utility.
  • Reduced Complexity: The main editor is now short and easy to read—no more scrolling through hundreds of lines of mixed logic.
  • Unity-Friendly: Uses Unity’s built-in property drawers and editor patterns, so it fits naturally with the engine’s workflow.

Final Note

Don’t worry about "over-engineering" here—this refactoring is about reducing cognitive load. Each small class is easier to test, debug, and modify than a single monolithic editor. As you keep learning SOLID, this kind of split will become second nature!

内容的提问来源于stack exchange,提问作者Dani Arkadia

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.11 09:03:11