1

Synopsis:

I am working on a level editor for a game written using Unity, the game is actually a new engine for an old game. This is a +20 years old game and its data structures are by today standards, hard to deal with in an environment such as Unity. Hence why I'm writing a level editor to ease the pain.

Basically the role of this level editor is to ease the process of creating human-friendly entities from references to graphic primitives such as models and polygons. The major features are helpers that simplify picking them for creating friendly entities (e.g. pick models 9 and 10 and create a 'billboard' entity).

Issues:

Now I am on the phase of fixing nasty bugs mainly due to events occurring from within Unity, such as when some values should be reset due to external changes and so on.

So I've decided to take a look at the class using Code Map in Visual Studio and it's getting increasingly harder to follow the structures thanks to the fixes aforementioned:

(members in red are Hubs found by Code Map) enter image description here

Code:

I am posting the code (~500 lines) here as a reference, so you can take a look if you'd like to. I am quite happy with it in the sense that methods are shorts and it hasn't gone out of control, yet.

using System;
using System.Collections.Generic;
using System.Linq;
using Assets.Scenes;
using Assets.Source.Game.Core;
using JetBrains.Annotations;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using Object = UnityEngine.Object;

namespace Assets.Source.Game.Scene.Editor
{
    public sealed class SceneEditorWindow : EditorWindow
    {
        #region Fields/properties

        private const string SceneInputPath = "Assets/Scenes/SceneEditorInput.unity";
        private const string SceneOutputPath = "Assets/Scenes/SceneEditorOutput.unity";
        private bool _highlight;
        private GameObject[] _highlightObjects;
        private Dictionary<int, ScenePart> _partsInHierarchy;
        private Dictionary<int, ScenePrimitive> _primitivesInHierarchy;
        private Dictionary<int, ScenePrimitive> _primitivesNotReferenced;
        private Dictionary<int, ScenePrimitive> _primitivesReferenced;
        private SceneRenderer _sceneRenderer;
        private Track _sceneRendererTrack;
        private SceneRoot _sceneRoot;

        /// <summary>
        ///     Dictionary of parts in hierarchy.
        /// </summary>
        private Dictionary<int, ScenePart> PartsInHierarchy
        {
            get
            {
                if (_partsInHierarchy == null)
                {
                    var parts = FindObjectsOfType<ScenePart>();
                    _partsInHierarchy =
                        parts.ToDictionary(k => k.gameObject.GetInstanceID(), v => v);
                }
                return _partsInHierarchy;
            }
            set { _partsInHierarchy = value; }
        }

        /// <summary>
        ///     Dictionary of primitives in hierarchy.
        /// </summary>
        private Dictionary<int, ScenePrimitive> PrimitivesInHierarchy
        {
            get
            {
                if (_primitivesInHierarchy == null)
                {
                    var primitives = FindObjectsOfType<ScenePrimitive>();
                    _primitivesInHierarchy =
                        primitives.ToDictionary(k => k.gameObject.GetInstanceID(), v => v);
                }
                return _primitivesInHierarchy;
            }
            set { _primitivesInHierarchy = value; }
        }

        private Dictionary<int, ScenePrimitive> PrimitivesNotReferenced
        {
            get
            {
                if (_primitivesNotReferenced == null)
                {
                    _primitivesNotReferenced = PrimitivesInHierarchy
                        .Except(PrimitivesReferenced)
                        .OrderBy(s => s.Value.Reference)
                        .ToDictionary(p => p.Key, p => p.Value);
                }
                return _primitivesNotReferenced;
            }
            set { _primitivesNotReferenced = value; }
        }

        /// <summary>
        ///     Dictionary of primitives referenced in hierarchy.
        /// </summary>
        private Dictionary<int, ScenePrimitive> PrimitivesReferenced
        {
            get
            {
                if (_primitivesReferenced == null)
                {
                    var comparer = new SceneReferenceEqualityComparer();
                    _primitivesReferenced = PrimitivesInHierarchy
                        .Where(s => ScenePrimitiveReferenced(s.Value, comparer))
                        .ToDictionary(k => k.Key, v => v.Value);
                }
                return _primitivesReferenced;
            }
            set { _primitivesReferenced = value; }
        }

        /// <summary>
        ///     Scene renderer in use.
        /// </summary>
        private SceneRenderer SceneRenderer
        {
            get
            {
                if (_sceneRenderer == null)
                    _sceneRenderer = FindObjectsOfType<SceneRenderer>().Single();
                return _sceneRenderer;
            }
        }

        /// <summary>
        ///     Scene root element.
        /// </summary>
        private SceneRoot SceneRoot
        {
            get
            {
                if (_sceneRoot == null)
                    _sceneRoot = FindObjectsOfType<SceneRoot>().Single();
                return _sceneRoot;
            }
        }

        #endregion

        #region Unity

        [MenuItem("WXX-REBIRTH/Scene editor")]
        [UsedImplicitly]
        private static void Init()
        {
            var window = GetWindow<SceneEditorWindow>("Scene editor", true);
            window.minSize = new Vector2(360.0f, 225.0f);
        }

        [UsedImplicitly]
        private void OnEnable()
        {
            EditorApplication.hierarchyWindowChanged += OnHierarchyWindowChanged;
            EditorApplication.hierarchyWindowItemOnGUI += OnHierarchyWindowItemOnGUI;
            EditorApplication.update += OnUpdate;
            Repaint();
            HierarchyReset();

            // TODO this currently de-selects, shouldn't (renderer does rebuild on enable)
            HighlightReset();
        }

        private void OnUpdate()
        {
            // reset scene-object-related stuff after a track change
            var track = SceneRenderer.Track;
            if (track != _sceneRendererTrack)
            {
                HighlightReset();
                HierarchyReset();
                HighlightReset();
                _sceneRendererTrack = track;
            }
        }

        [UsedImplicitly]
        private void OnGUI()
        {
            if (GUIWorkspace())
            {
                GUIActions();
                GUIOptions();
                GUIStats();
            }

            HelpBox.Draw();
        }

        private void OnHierarchyWindowChanged()
        {
            // TODO is this one really needed ?
            HierarchyReset(); // let's keep it simple by simply doing this

            Repaint();
        }

        private void OnHierarchyWindowItemOnGUI(int id, Rect rect)
        {
            // if a primitive is referenced by a part, add a green icon, else add a red icon

            if (!PrimitivesInHierarchy.ContainsKey(id))
                return;

            var primitive = PrimitivesInHierarchy[id];
            var comparer = new SceneReferenceEqualityComparer(); // won't work otherwise thanks to Unity IDs :)
            var referenced = ScenePrimitiveReferenced(primitive, comparer);
            var iconType = referenced ? IconType.CircleOkGreen16 : IconType.CircleErrorRed16;

            Texture2D[] icons = { IconHelper.GetIcon(iconType) };

            var rect1 = rect;
            foreach (var icon in icons)
            {
                rect1.x = rect1.xMax - 16.0f;
                rect1.width = 16.0f;
                GUI.DrawTexture(rect1, icon);
            }
        }


        [UsedImplicitly]
        private void OnSelectionChange()
        {
            Highlight();
            Repaint(); // for actions
        }

        #endregion

        #region GUI

        /// <summary>
        ///     Workspace screen in GUI.
        /// </summary>
        /// <returns></returns>
        private bool GUIWorkspace()
        {
            // already open ?

            var scenes = new[]
            {
                new SceneSetup {path = SceneInputPath, isLoaded = true, isActive = true},
                new SceneSetup {path = SceneOutputPath, isLoaded = true, isActive = false}
            };

            var setup = EditorSceneManager.GetSceneManagerSetup();

            if (scenes.All(s => setup.Any(t => t.path == s.path && t.isLoaded == s.isLoaded)))
                return true;

            // try open it, ensure current dirty scenes are saved first if any

            HelpBox.LogError("Workspace is not opened.");

            if (!GUIHelper.GUIButtonLargeCenter("Open workspace"))
                return false;

            if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
                return false;

            EditorSceneManager.RestoreSceneManagerSetup(scenes);

            HierarchyReset(); // they're all "null" at this point

            return true;
        }

        /// <summary>
        ///     Actions section in GUI.
        /// </summary>
        private void GUIActions()
        {
            GUIHelper.GUILabelLarge("Actions");

            // here an action will be disabled
            // - if there is no targetted type in selection
            // - if there are unrelated objects in selection

            var gameObjects = Selection.gameObjects;
            var primitives = SelectedPrimitives();
            var parts = SelectedParts();
            var objects = SelectedObjects();
            var n = !gameObjects.Any();
            var n1 = !primitives.Any();
            var n2 = !parts.Any();
            var n3 = !objects.Any();
            var m1 = gameObjects.Length != primitives.Length;
            var m2 = gameObjects.Length != parts.Length;
            var m3 = gameObjects.Length != objects.Length;

            var d1 = n || n1 || m1 || primitives.Select(s => s.Reference.Type).Distinct().Count() > 1;
            ActionNewPartFromSelection(d1, primitives.Select(s => s.Reference).ToArray());

            var d2 = n || n2 || m2;
            ActionNewObjectFromSelection(d2, parts);

            var d3 = n || n3 || m3;
            ActionNewGroupFromSelection(d3, objects);

            var d4 = n || n1 || m1 || primitives.Any(s => s.Reference.Type != SceneReferenceType.Polygon);
            ActionSelectParentModels(d4, primitives);

            var d5 = PrimitivesNotReferenced == null || !PrimitivesNotReferenced.Any();
            ActionSelectFirstNotReferenced(d5);
        }

        /// <summary>
        ///     Options section in GUI.
        /// </summary>
        private void GUIOptions()
        {
            GUIHelper.GUILabelLarge("Options");

            // highlighting
            var highlight = _highlight;
            if (highlight != (_highlight = EditorGUILayout.ToggleLeft("Highlight selection", highlight)))
                Highlight(); // make it react at an editor change instead of a selection change

            EditorGUILayout.Space();
        }

        /// <summary>
        ///     Stats section in GUI.
        /// </summary>
        private void GUIStats()
        {
            GUIHelper.GUILabelLarge("Stats");

            var count1 = PrimitivesInHierarchy.Count;
            EditorGUILayout.LabelField("Primitives in hierarchy", count1.ToString());

            var count2 = PrimitivesReferenced.Count;
            var percent1 = 1.0d / count1 * count2 * 100.0d;
            EditorGUILayout.LabelField("Primitives referenced", string.Format("{0} ({1:F}%)", count2, percent1));

            var count3 = PrimitivesNotReferenced.Count;
            var percent2 = 1.0d / count1 * count3 * 100.0d;
            EditorGUILayout.LabelField("Primitives not referenced", string.Format("{0} ({1:F}%)", count3, percent2));

            var count4 = PartsInHierarchy.Count;
            EditorGUILayout.LabelField("Parts in hierarchy", count4.ToString());

            // TODO objects

            EditorGUILayout.Space();
        }

        #endregion

        #region Everything else

        /// <summary>
        ///     Action that creates an object from a selection of parts.
        /// </summary>
        /// <param name="disabled"></param>
        /// <param name="parts"></param>
        private void ActionNewObjectFromSelection(bool disabled, [NotNull] ScenePart[] parts)
        {
            if (parts == null)
                throw new ArgumentNullException("parts");

            // realize the button
            var tooltip = "To create an object, select one or more parts.";
            if (!GUIHelper.GUIButtonAction(disabled, new GUIContent("+ New object from selection", tooltip)))
                return;

            // create new object with these parts
            using (new SceneEditorObjectScope("Object", SceneRoot, parts))
            {
            }
        }

        /// <summary>
        ///     Action that creates a part from a selection of references (primitives).
        /// </summary>
        /// <param name="disabled"></param>
        /// <param name="references"></param>
        private void ActionNewPartFromSelection(bool disabled, [NotNull] SceneReference[] references)
        {
            if (references == null)
                throw new ArgumentNullException("references");

            // realize the button
            var tooltip = "To create a part, select either models or polygons.";
            var content = new GUIContent("+ New part from selection", tooltip);
            if (!GUIHelper.GUIButtonAction(disabled, content))
                return;

            // create new part with these references
            using (new SceneEditorPartScope("Part", SceneRoot, references))
            {
            }
        }

        /// <summary>
        ///     Action that creates a group from a selection of objects.
        /// </summary>
        /// <param name="disabled"></param>
        /// <param name="objects"></param>
        private void ActionNewGroupFromSelection(bool disabled, [NotNull] SceneObject[] objects)
        {
            if (objects == null)
                throw new ArgumentNullException("objects");

            // realize the button
            var tooltip = "To create a group, select one or more objects.";
            var content = new GUIContent("+ New group from selection", tooltip);
            if (!GUIHelper.GUIButtonAction(disabled, content))
                return;

            // create new group with these objects
            using (new SceneEditorGroupScope("Group", SceneRoot, objects))
            {
            }
        }

        /// <summary>
        ///     Action that selects parent models of selected primitives.
        /// </summary>
        /// <param name="disabled"></param>
        /// <param name="primitives"></param>
        private void ActionSelectParentModels(bool disabled, [NotNull] ScenePrimitive[] primitives)
        {
            if (primitives == null)
                throw new ArgumentNullException("primitives");

            // realize the button
            var tooltip = "To select parents, select one or more polygons.";
            if (!GUIHelper.GUIButtonAction(disabled, new GUIContent("+ Select parent model(s)", tooltip)))
                return;

            // get polygon sources
            var polygons = primitives
                .Where(s => s.Reference.Type == SceneReferenceType.Polygon)
                .ToArray();

            // get polygons parents
            var collection = PrimitivesInHierarchy.Values;
            var parents = polygons
                .Select(polygon => collection.SingleOrDefault(s =>
                {
                    var r = s.Reference;
                    var b = r.Type == SceneReferenceType.Model &&
                            r.Container == polygon.Reference.Container &&
                            r.Model == polygon.Reference.Model;
                    return b;
                }))
                .Where(parent => parent != null)
                .Distinct()
                .Select(s => (Object)s.gameObject)
                .ToArray();

            Selection.objects = parents;

            // make it easy for single selection
            if (parents.Length == 1) SelectionHelper.SelectFramePing(parents.First());
        }

        private void ActionSelectFirstNotReferenced(bool disabled)
        {
            string tooltip = "TODO";
            if (!GUIHelper.GUIButtonAction(disabled, new GUIContent("+ Select first not referenced", tooltip)))
                return;
            var primitive = this.PrimitivesNotReferenced.Values.First();
            SelectionHelper.SelectFramePing(primitive);
        }

        /// <summary>
        ///     Resets hierarchy-related data, this will force repopulation on next query.
        /// </summary>
        private void HierarchyReset()
        {

            // re-populate these

            PrimitivesInHierarchy = null; // when user disable GOs
            PrimitivesReferenced = null;
            PrimitivesNotReferenced = null;
            PartsInHierarchy = null;

        }

        /// <summary>
        ///     Highlights currently selected primitives.
        /// </summary>
        private void Highlight() // todo rename all once 2nd highlight is in place
        {
            // color all childs of all objects
            Action<GameObject[], Color> action = (gameObjects, color) =>
            {
                foreach (var o in gameObjects)
                {
                    var renderers = o.GetComponentsInChildren<MeshRenderer>();
                    foreach (var renderer in renderers)
                    {
                        renderer.sharedMaterial.color = color;
                    }
                }
            };

            // NOTE let's just stick to null pattern even though zero length arrays are returned

            // clear highlighting for previous objects
            if (_highlightObjects != null)
                action(_highlightObjects, Color.white);

            // pick scene primitives in selection (polygons might be hit twice but that'll do)
            var primitives = SelectedPrimitives();
            _highlightObjects = primitives.Any() ? primitives.Select(s => s.gameObject).ToArray() : null;

            // highlight currents if any
            if (_highlight && _highlightObjects != null)
                action(_highlightObjects, Color.magenta);
        }

        /// <summary>
        ///     Resets highlighting by clearing cache.
        /// </summary>
        private void HighlightReset()
        {
            _highlightObjects = null;
        }

        /// <summary>
        ///     Gets if a primitive is referenced.
        /// </summary>
        /// <param name="primitive"></param>
        /// <param name="comparer"></param>
        /// <returns></returns>
        private bool ScenePrimitiveReferenced([NotNull] ScenePrimitive primitive,
            [NotNull] SceneReferenceEqualityComparer comparer)
        {
            if (primitive == null)
                throw new ArgumentNullException("primitive");

            if (comparer == null)
                throw new ArgumentNullException("comparer");

            var r = primitive.Reference;

            var b = PartsInHierarchy.Any(s => s.Value.References.Contains(r, comparer));

            // if a polygon is not referenced but parent is, then it is too
            if (r.Type == SceneReferenceType.Polygon)
            {
                b |= PartsInHierarchy.Any(
                    s => s.Value.References.Any(
                        t => t.Container == r.Container && t.Model == r.Model && t.Type == SceneReferenceType.Model));
            }

            return b;
        }

        /// <summary>
        ///     Gets currently selected primitives in hierarchy.
        /// </summary>
        /// <returns></returns>
        private ScenePrimitive[] SelectedPrimitives()
        {
            var primitives = Selection.GetFiltered(typeof(ScenePrimitive), SelectionMode.Unfiltered)
                .Cast<ScenePrimitive>()
                .ToArray();
            return primitives;
        }

        /// <summary>
        ///     Gets currently selected parts in hierarchy.
        /// </summary>
        /// <returns></returns>
        private ScenePart[] SelectedParts()
        {
            var parts = Selection.GetFiltered(typeof(ScenePart), SelectionMode.Unfiltered)
                .Cast<ScenePart>()
                .ToArray();
            return parts;
        }

        /// <summary>
        ///     Gets currently selected objects in hierarchy.
        /// </summary>
        /// <returns></returns>
        private SceneObject[] SelectedObjects()
        {
            var objects = Selection.GetFiltered(typeof(SceneObject), SelectionMode.Unfiltered)
                .Cast<SceneObject>()
                .ToArray();
            return objects;
        }

        #endregion
    }
}

Question:

What kind of approach/strategy to adopt when more and more calls are being made between members of a class, and one wants to reduce their amount ?

4

The 1st tip would be : know your tools

That helps a lot, filtering out non-essential stuff already gives a clearer view of the problem:

(properties have been filtered out) enter image description here

(cheap but handy refactoring: some methods converted to properties) enter image description here

2nd tip : attempt simple/affordable/easily reversible refactoring

(renamed some methods, introduced some 'hub' methods, defects already visible !)

enter image description here

At this point 10 minutes of small and easy changes but with significant gains !

Right, I have simply hidden some stuff in the diagram but it really helped out in finding my way in that mess. Devil-in-the-details code is already written, it wouldn't have changed anyway since that was (one of) the only way to do it.

  • The entire structure would be even simpler if you put the whole bunch of ActionXXX methods into a factory class. – t3chb0t Aug 27 '16 at 18:59
  • Sorry but I don't get it, can you elaborate ? – Aybe Aug 27 '16 at 20:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.