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)
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 ?