So, I’d like to build a custom editor window to help plan out the various missions and objectives in my game. My current layout has a lot of lists within lists within lists, which is a nightmare to work with in the default inspector. To avoid creating hundreds of unique scriptable objects to contain the data I need, I figured I could potentially create a custom editor window to serve as my Mission Editor
. However, I’ve run into a snag with displaying some of the list information.
-
Below is a Photoshopped screenshot of what I’d like it to look like:
-
Versus a screenshot of where I’m at currently:
My code architecture is as follows:
-
A
Scriptable Object
of typeMissionsDataObject
that contains a list of typeMission
. These are the missions that show up on the left-most side of the editor, and their properties are what appear in the center pane of the editor window. -
Each Mission has a list of type
Objective
, which contains details about a specific objective for the mission, much like a quest. -
Each
Objective
has its own lists with more properties, though I’m not concerned with separating these lists in the editor window, just theObjectives List
.
The code for the custom window, as well as the aforementioned scripts can be found here, plus a couple other scripts to ensure it all compiles.
-> CLICK TO SEE ALL THE CODE <-
Condition.cs
using System;
using System.Collections.Generic;
[Serializable]
public class Condition {
public ConditionType conditionType;
public class DurationParams {
public Range range;
}
public class OccupyParams {
public List<GridPosition> gridPositionList;
}
public class WeaponParams {
public List<WeaponSO> weaponSOList;
}
public class AmmoParams {
public Range range;
}
}
public enum ConditionType {
Duration,
Ammo,
Weapon,
Occupy,
}
ExtendedEditorWindow.cs
using UnityEditor;
using UnityEngine;
public class ExtendedEditorWindow : EditorWindow {
protected SerializedObject serializedObject;
protected SerializedProperty currentProperty;
private string selectedPropertyPath;
protected SerializedProperty selectedProperty;
protected void DrawProperties(SerializedProperty prop, bool drawChildren) {
string lastPropPath = string.Empty;
foreach (SerializedProperty p in prop)
{
if (p.isArray && p.propertyType == SerializedPropertyType.Generic) {
EditorGUILayout.BeginHorizontal();
p.isExpanded = EditorGUILayout.Foldout(p.isExpanded, p.displayName);
EditorGUILayout.EndHorizontal();
if (p.isExpanded) {
EditorGUI.indentLevel++;
DrawProperties(p, drawChildren);
EditorGUI.indentLevel--;
}
} else {
if (!string.IsNullOrEmpty(lastPropPath) && p.propertyPath.Contains(lastPropPath)) {
continue;
}
lastPropPath = p.propertyPath;
EditorGUILayout.PropertyField(p, drawChildren);
}
}
}
protected void DrawButtons(SerializedProperty prop) {
foreach (SerializedProperty p in prop) {
if (GUILayout.Button(p.displayName)) {
selectedPropertyPath = p.propertyPath;
}
}
if (!string.IsNullOrEmpty(selectedPropertyPath)) {
selectedProperty = serializedObject.FindProperty(selectedPropertyPath);
}
}
protected void DrawField(string propName, bool relative) {
if (relative && currentProperty != null) {
EditorGUILayout.PropertyField(currentProperty.FindPropertyRelative(propName), true);
} else if (serializedObject != null) {
EditorGUILayout.PropertyField(serializedObject.FindProperty(propName), true);
}
}
protected void ApplyChanges() {
serializedObject.ApplyModifiedProperties();
}
}
``
### Goal.cs
```cs
using System;
using System.Collections.Generic;
using UnityEngine;
[Serializable]
public class Goal {
public GoalType goalType;
public Status goalStatus;
[Space]
public bool isRequired;
public string rewardIfOptional;
[Space]
public int requiredAmount;
public int progressAmount;
public List<Condition> conditionList;
}
public enum GoalType {
Defeat,
Rescue,
Collect,
Reach,
Survive,
Prevent,
Protect
}
Mission.cs
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.Search;
[Serializable]
public class Mission {
public string missionName;
[TextArea(2,4)]
public string missionDescription;
public int missionIndex;
[SearchContext("t:sprite MissionPreview", SearchViewFlags.GridView)]
public Sprite missionSelectPreview;
public List<NewObjective> objectiveList;
}
MissionsDataEditor.cs
using UnityEngine;
using UnityEditor;
using UnityEditor.Callbacks;
public class AssetHandler {
[OnOpenAsset()]
public static bool OpenEditor(int instanceId, int line) {
MissionsDataObject missionsDataObject = EditorUtility.InstanceIDToObject(instanceId) as MissionsDataObject;
if (missionsDataObject != null) {
MissionsDataEditorWindow.Open(missionsDataObject);
return true;
}
return false;
}
}
[CustomEditor(typeof(MissionsDataObject))]
public class MissionsDataEditor : Editor {
public override void OnInspectorGUI() {
if (GUILayout.Button("Open Editor")) {
MissionsDataEditorWindow.Open((MissionsDataObject)target);
}
}
}
MissionsDataEditorWindow.cs
using UnityEditor;
using UnityEngine;
public class MissionsDataEditorWindow : ExtendedEditorWindow {
public static void Open(MissionsDataObject missionsDataObject) {
MissionsDataEditorWindow window = GetWindow<MissionsDataEditorWindow>("Missions Data Editor");
window.serializedObject = new SerializedObject(missionsDataObject);
}
private void OnGUI() {
currentProperty = serializedObject.FindProperty("missionList");
EditorGUILayout.BeginHorizontal();
EditorGUILayout.BeginVertical("box", GUILayout.MaxWidth(150), GUILayout.ExpandHeight(true));
DrawButtons(currentProperty);
EditorGUILayout.EndVertical();
EditorGUILayout.BeginVertical("box", GUILayout.MaxWidth(300), GUILayout.ExpandHeight(true));
if (selectedProperty != null) {
DrawMissionPropertiesPanel();
} else {
EditorGUILayout.LabelField("Select a mission");
}
EditorGUILayout.EndVertical();
EditorGUILayout.BeginVertical("box", GUILayout.ExpandHeight(true));
if (selectedProperty != null) {
} else {
EditorGUILayout.LabelField("Select a mission");
}
EditorGUILayout.EndVertical();
EditorGUILayout.EndHorizontal();
ApplyChanges();
}
void DrawMissionPropertiesPanel() {
currentProperty = selectedProperty;
DrawField("missionSelectPreview", true);
DrawField("missionName", true);
DrawField("missionIndex", true);
EditorGUILayout.Space(10);
DrawField("missionDescription", true);
EditorGUILayout.Space(20);
DrawField("objectiveList", true);
}
}
MissionsDataObject.cs
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "NewMissionsDataObject", menuName = "ScriptableObjects/MissionsData")]
public class MissionsDataObject : ScriptableObject {
public List<Mission> missionList;
}
Objective.cs
using System;
using System.Collections.Generic;
using UnityEngine;
[Serializable]
public class Objective {
[TextArea(1,3)]
public string objectiveName;
public Sprite objectivePreviewImage;
public Status status = Status.NotStarted;
public List<Goal> goalList;
public bool IsObjectiveRequired() {
foreach (Goal goal in goalList) {
if (goal.isRequired) {
return true;
}
continue;
}
return false;
}
public override string ToString() {
return objectiveName;
}
}
My goal is to split up the Objective List
so that, in the center pane, it only shows a button with the element’s name. When pressed, that button would open all the details of the element in the right pane, but ONLY the details of the one pressed. For reference, this is very similar to what’s already happening with the Mission0
, Mission1
, etc. buttons on the right, but I can’t figure out a way to get it to work for the “second” tier of lists.
If possible, it would be nice to also the + and - buttons to add or remove elements from the list within the editor window, but if not possible, I could potentially switch to scriptable objects for the Missions to be able to edit the list outside of the window. Also, I know if I can get it working here, I can probably get it to work for the other levels of lists that I have within the Objective
class, which would be nice.