using UnityEngine; using UnityEditor; using UnityEditor.SceneManagement; using System.Collections.Generic; // Sci-Fi Ship Controller. Copyright (c) 2018-2023 SCSM Pty Ltd. All rights reserved. namespace SciFiShipController { [CustomEditor(typeof(DemoControlModule))] public class DemoControlModuleEditor : Editor { #region Custom Editor private variables // Formatting and style variables private string txtColourName = "Black"; private Color defaultTextColour = Color.black; private string labelText; private GUIStyle labelFieldRichText; private GUIStyle helpBoxRichText; private GUIStyle buttonCompact; private float defaultEditorLabelWidth = 0f; private float defaultEditorFieldWidth = 0f; private bool isSceneModified = false; // used in OnInspectorGUI() private DemoControlModule demoControlModule = null; private int squadronMoveDownPos = -1; private int squadronInsertPos = -1; private int squadronDeletePos = -1; // SceneGUI variables private Vector3 componentHandlePosition = Vector3.zero; private Quaternion componentHandleRotation = Quaternion.identity; private bool isSceneViewModified = false; // Used in SceneGUI(SceneView sv) private Squadron svSquadron = null; private Vector3 scale = Vector3.zero; private Vector3 theatrePos = Vector3.zero; #endregion #region GUIContent - Squadrons private readonly static GUIContent headerContent = new GUIContent("Demo Control Module\n\nThis module demonstrates how to spawn squadrons of ships"); private readonly static GUIContent squadronHeaderContent = new GUIContent("Squadrons are groups of typically the same ship type. They can contain 0, 1 or more ships from the same faction or alliance."); private readonly static GUIContent squadronIdContent = new GUIContent("Squadron Id", "The unique number or ID for this squadron"); private readonly static GUIContent squadronNameContent = new GUIContent("Squadron Name"); private readonly static GUIContent factionIdContent = new GUIContent("Faction Id", "The Faction that the squadron belongs to or fights for."); private readonly static GUIContent anchorPositionContent = new GUIContent("Anchor Position", "The initial front middle position of the squadron. If there is more than one row on y-axis, rows will be created above this position."); private readonly static GUIContent fwdDirectionContent = new GUIContent("Forward Direction", "Direction as a normalised vector"); private readonly static GUIContent anchorRotationContent = new GUIContent("Anchor Rotation", "The forward direction as euler angles. This is modified by setting the Forward Direction vector"); private readonly static GUIContent tacticalFormationContent = new GUIContent("Tactical Formation", "The type of formation in which to spawn ships"); private readonly static GUIContent rowsXContent = new GUIContent("Rows on x-axis", "The number of rows along the x-axis"); private readonly static GUIContent rowsZContent = new GUIContent("Rows on z-axis", "The number of rows along the z-axis"); private readonly static GUIContent rowsYContent = new GUIContent("Rows on y-axis", "The number of rows along the y-axis"); private readonly static GUIContent offsetXContent = new GUIContent("Row offset x-axis", "The distance between rows on the x-axis"); private readonly static GUIContent offsetZContent = new GUIContent("Row offset z-axis", "The distance between rows on the z-axis"); private readonly static GUIContent offsetYContent = new GUIContent("Row offset y-axis", "The distance between rows on the y-axis"); private readonly static GUIContent shipPrefabContent = new GUIContent("NPC Ship Prefab", "Non-Player-Character ship which will be used to populate the squadron"); private readonly static GUIContent playerShipContent = new GUIContent("Player Ship", "Optionally reference to a player ship in the scene to lead this squadron"); private readonly static GUIContent cameraTargetOffsetContent = new GUIContent("Camera Ship Offset", "The offset from the ship (in local space) for the camera to aim for."); #endregion #region GUIContent - AI Targets private readonly static GUIContent assignAITargetsContent = new GUIContent("Assign AI Targets",""); private readonly static GUIContent aiTargetsContent = new GUIContent("AI Targets",""); private readonly static GUIContent reassignTargetSecsContent = new GUIContent("Reassign Target Secs", ""); private readonly static GUIContent AddAIScriptIfMissingContent = new GUIContent("Add AI Script If Missing", ""); private readonly static GUIContent CrashAffectsHealthContent = new GUIContent("Crash Affects Health", ""); private readonly static GUIContent theatreBoundsContent = new GUIContent("Theatre Bounds", "The region that the ships can fly or operate in. Extents are distance from centre."); #endregion #region GUIContent - Radar private readonly static GUIContent useRadarContent = new GUIContent("Use Radar", "Enable radar tracking for all ships"); #endregion #region Serialized Properties private SerializedProperty squadronListProp; private SerializedProperty squadronProp; private SerializedProperty squadronIdProp; private SerializedProperty squadronNameProp; private SerializedProperty squadronShowInEditorProp; private SerializedProperty squadronTacticalFormationProp; private SerializedProperty squadronFwdDirectionProp; #endregion #region Event Methods private void OnEnable() { demoControlModule = (DemoControlModule)target; squadronListProp = serializedObject.FindProperty("squadronList"); #if UNITY_2019_1_OR_NEWER SceneView.duringSceneGui -= SceneGUI; SceneView.duringSceneGui += SceneGUI; #else SceneView.onSceneGUIDelegate -= SceneGUI; SceneView.onSceneGUIDelegate += SceneGUI; #endif Tools.hidden = true; // Used in Richtext labels if (EditorGUIUtility.isProSkin) { txtColourName = "White"; defaultTextColour = new Color(180f / 255f, 180f / 255f, 180f / 255f, 1f); } // Keep compiler happy - can remove this later if it isn't required if (defaultTextColour.a > 0f) { } } private void OnDisable() { Tools.hidden = false; Tools.current = Tool.Move; #if UNITY_2019_1_OR_NEWER SceneView.duringSceneGui -= SceneGUI; #else SceneView.onSceneGUIDelegate -= SceneGUI; #endif } private void OnDestroy() { Tools.hidden = false; Tools.current = Tool.Move; } #endregion #region OnInspectorGUI // This function overides what is normally seen in the inspector window // This allows stuff like buttons to be drawn there public override void OnInspectorGUI() { #region Initialise EditorGUIUtility.labelWidth = defaultEditorLabelWidth; EditorGUIUtility.fieldWidth = defaultEditorFieldWidth; #endregion #region Configure Buttons and Styles // Set up rich text GUIStyles helpBoxRichText = new GUIStyle("HelpBox"); helpBoxRichText.richText = true; labelFieldRichText = new GUIStyle("Label"); labelFieldRichText.richText = true; buttonCompact = new GUIStyle("Button"); buttonCompact.fontSize = 10; #endregion // Read in all the properties serializedObject.Update(); GUILayout.BeginVertical("HelpBox"); EditorGUILayout.LabelField("Sci-Fi Ship Controller Version " + ShipControlModule.SSCVersion + " " + ShipControlModule.SSCBetaVersion, labelFieldRichText); GUILayout.EndVertical(); EditorGUILayout.LabelField(headerContent, helpBoxRichText); #region Squadrons EditorGUILayout.LabelField(squadronHeaderContent, helpBoxRichText); if (squadronListProp == null) { // Apply property changes serializedObject.ApplyModifiedProperties(); demoControlModule.squadronList = new List(); isSceneModified = true; // Read in the properties serializedObject.Update(); } #region Add-Remove Squadrons int numSquadrons = squadronListProp.arraySize; GUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Squadrons: " + numSquadrons.ToString("00") + "", labelFieldRichText); if (GUILayout.Button("+", GUILayout.MaxWidth(30f))) { // Apply property changes serializedObject.ApplyModifiedProperties(); demoControlModule.squadronList.Add(new Squadron()); Undo.RecordObject(demoControlModule, "Add Squadron"); isSceneModified = true; // Read in the properties serializedObject.Update(); numSquadrons = squadronListProp.arraySize; if (numSquadrons > 0) { // Force new squadron to be serialized in scene squadronProp = squadronListProp.GetArrayElementAtIndex(numSquadrons - 1); squadronIdProp = squadronProp.FindPropertyRelative("squadronId"); squadronIdProp.intValue = 0; squadronIdProp.intValue = -1; } } if (GUILayout.Button("-", GUILayout.MaxWidth(30f))) { if (numSquadrons > 0) { // Get the last squadron in the list squadronProp = squadronListProp.GetArrayElementAtIndex(squadronListProp.arraySize - 1); if (EditorUtility.DisplayDialog("Delete Squadron?", "Do you wish to delete Squadron " + (squadronListProp.arraySize).ToString("00") + "?", "Delete Now", "Cancel")) { squadronListProp.arraySize -= 1; } } } GUILayout.EndHorizontal(); #endregion #region Squadron List // Reset button variables squadronMoveDownPos = -1; squadronInsertPos = -1; squadronDeletePos = -1; numSquadrons = squadronListProp.arraySize; for (int index = 0; index < numSquadrons; index++) { GUILayout.BeginVertical(EditorStyles.helpBox); squadronProp = squadronListProp.GetArrayElementAtIndex(index); squadronIdProp = squadronProp.FindPropertyRelative("squadronId"); squadronNameProp = squadronProp.FindPropertyRelative("squadronName"); squadronShowInEditorProp = squadronProp.FindPropertyRelative("showInEditor"); #region Squadron Move/Insert/Delete buttons GUILayout.BeginHorizontal(); EditorGUI.indentLevel += 1; squadronShowInEditorProp.boolValue = EditorGUILayout.Foldout(squadronShowInEditorProp.boolValue, (index + 1).ToString("00") + ": " + squadronNameProp.stringValue); EditorGUI.indentLevel -= 1; //EditorGUILayout.LabelField((index + 1).ToString("00") + " " + squadronNameProp.stringValue); // Move down button if (GUILayout.Button("V", buttonCompact, GUILayout.MaxWidth(20f)) && numSquadrons > 1) { squadronMoveDownPos = index; } // Create duplicate button if (GUILayout.Button("I", buttonCompact, GUILayout.MaxWidth(20f))) { squadronInsertPos = index; } // Delete button if (GUILayout.Button("X", buttonCompact, GUILayout.MaxWidth(20f))) { squadronDeletePos = index; } GUILayout.EndHorizontal(); #endregion #region Show/Edit Squadron if (squadronShowInEditorProp.boolValue) { squadronTacticalFormationProp = squadronProp.FindPropertyRelative("tacticalFormation"); squadronFwdDirectionProp = squadronProp.FindPropertyRelative("fwdDirection"); EditorGUILayout.PropertyField(squadronIdProp, squadronIdContent); EditorGUILayout.PropertyField(squadronNameProp, squadronNameContent); EditorGUILayout.PropertyField(squadronProp.FindPropertyRelative("factionId"), factionIdContent); EditorGUILayout.PropertyField(squadronProp.FindPropertyRelative("anchorPosition"), anchorPositionContent); // Prevent zero rotation if (squadronFwdDirectionProp.vector3Value == Vector3.zero) { squadronFwdDirectionProp.vector3Value = Vector3.forward; } // Show the forward direction as a non-editable rotation GUI.enabled = false; EditorGUILayout.Vector3Field(anchorRotationContent, Quaternion.LookRotation(squadronFwdDirectionProp.vector3Value, Vector3.up).eulerAngles); GUI.enabled = true; EditorGUILayout.PropertyField(squadronFwdDirectionProp, fwdDirectionContent); EditorGUILayout.PropertyField(squadronTacticalFormationProp, tacticalFormationContent); if (squadronTacticalFormationProp.intValue != (int)Squadron.TacticalFormation.Vic && squadronTacticalFormationProp.intValue != (int)Squadron.TacticalFormation.Wedge) { EditorGUILayout.PropertyField(squadronProp.FindPropertyRelative("rowsX"), rowsXContent); } EditorGUILayout.PropertyField(squadronProp.FindPropertyRelative("rowsZ"), rowsZContent); EditorGUILayout.PropertyField(squadronProp.FindPropertyRelative("rowsY"), rowsYContent); EditorGUILayout.PropertyField(squadronProp.FindPropertyRelative("offsetX"), offsetXContent); EditorGUILayout.PropertyField(squadronProp.FindPropertyRelative("offsetZ"), offsetZContent); EditorGUILayout.PropertyField(squadronProp.FindPropertyRelative("offsetY"), offsetYContent); EditorGUILayout.PropertyField(squadronProp.FindPropertyRelative("shipPrefab"), shipPrefabContent); EditorGUILayout.PropertyField(squadronProp.FindPropertyRelative("playerShip"), playerShipContent); EditorGUILayout.PropertyField(squadronProp.FindPropertyRelative("cameraTargetOffset"), cameraTargetOffsetContent); } #endregion GUILayout.EndVertical(); } #endregion #region Move/Insert/Delete Squadrons if (squadronDeletePos >= 0 || squadronInsertPos >= 0 || squadronMoveDownPos >= 0) { GUI.FocusControl(null); // Don't permit multiple operations in the same pass if (squadronMoveDownPos >= 0) { // Move down one position, or wrap round to start of list if (squadronMoveDownPos < squadronListProp.arraySize - 1) { squadronListProp.MoveArrayElement(squadronMoveDownPos, squadronMoveDownPos + 1); } else { squadronListProp.MoveArrayElement(squadronMoveDownPos, 0); } squadronMoveDownPos = -1; } else if (squadronInsertPos >= 0) { // Apply property changes before potential list changes serializedObject.ApplyModifiedProperties(); demoControlModule.squadronList.Insert(squadronInsertPos, new Squadron(demoControlModule.squadronList[squadronInsertPos])); // Read all properties from the DemoControlModule serializedObject.Update(); // Force new squadron to be serialized in scene squadronIdProp = squadronListProp.GetArrayElementAtIndex(squadronInsertPos).FindPropertyRelative("squadronId"); int originalID = squadronIdProp.intValue; squadronIdProp.intValue = -99; squadronIdProp.intValue = originalID; squadronInsertPos = -1; isSceneModified = true; } else if (squadronDeletePos >= 0) { // In U2019.4+ DisplayDialog seems to trigger another OnInspectorGUI() and squadronDeletePos is reset to -1. int _deleteIndex = squadronDeletePos; if (EditorUtility.DisplayDialog("Delete Squadron " + (squadronDeletePos + 1) + "?", "Squadron " + (squadronDeletePos + 1) + " will be deleted\n\nThis action will remove the squadron from the list and cannot be undone.", "Delete Now", "Cancel")) { squadronListProp.DeleteArrayElementAtIndex(_deleteIndex); squadronDeletePos = -1; } } #if UNITY_2019_3_OR_NEWER serializedObject.ApplyModifiedProperties(); // In U2019.4+ avoid: EndLayoutGroup: BeginLayoutGroup must be called first. if (!Application.isPlaying) { EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene()); } GUIUtility.ExitGUI(); #endif } #endregion #endregion #region AI Targets GUILayout.BeginVertical("HelpBox"); EditorGUILayout.PropertyField(serializedObject.FindProperty("assignAITargets"), assignAITargetsContent); EditorGUILayout.PropertyField(serializedObject.FindProperty("aiTargets"), aiTargetsContent); EditorGUILayout.PropertyField(serializedObject.FindProperty("reassignTargetSecs"), reassignTargetSecsContent); EditorGUILayout.PropertyField(serializedObject.FindProperty("AddAIScriptIfMissing"), AddAIScriptIfMissingContent); GUILayout.EndVertical(); GUILayout.BeginVertical("HelpBox"); EditorGUILayout.PropertyField(serializedObject.FindProperty("CrashAffectsHealth"), CrashAffectsHealthContent); EditorGUILayout.PropertyField(serializedObject.FindProperty("theatreBounds"), theatreBoundsContent); GUILayout.EndVertical(); #endregion #region Radar GUILayout.BeginVertical("HelpBox"); EditorGUILayout.PropertyField(serializedObject.FindProperty("useRadar"), useRadarContent); GUILayout.EndVertical(); #endregion // Apply property changes serializedObject.ApplyModifiedProperties(); #region Mark Scene Dirty if required if (isSceneModified && !Application.isPlaying) { isSceneModified = false; EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene()); } #endregion } #endregion #region Private Methods private void SceneGUI(SceneView sv) { // Only display if (demoControlModule != null) { int numSquadrons = demoControlModule.squadronList == null ? 0 : demoControlModule.squadronList.Count; isSceneViewModified = false; for (int sqIdx = 0; sqIdx < numSquadrons; sqIdx++) { svSquadron = demoControlModule.squadronList[sqIdx]; componentHandlePosition = svSquadron.anchorPosition; componentHandleRotation = Quaternion.LookRotation(svSquadron.fwdDirection, Vector3.up); // Draw a sphere in the scene that is non-interactable if (Event.current.type == EventType.Repaint) { using (new Handles.DrawingScope(Color.yellow)) { Handles.SphereHandleCap(0, componentHandlePosition, componentHandleRotation, 3f, EventType.Repaint); } } // Only display handle if the squadron is expanded in the editor if (svSquadron.showInEditor) { if (Tools.current == Tool.Rotate) { EditorGUI.BeginChangeCheck(); // Draw a rotation handle componentHandleRotation = Handles.RotationHandle(componentHandleRotation, componentHandlePosition); // Use the rotation handle to edit the direction of thrust if (EditorGUI.EndChangeCheck()) { isSceneViewModified = true; Undo.RecordObject(demoControlModule, "Rotate Squadron Anchor Position"); // ================================ // TODO - THIS MAY BE WRONG.... // ================================ svSquadron.fwdDirection = componentHandleRotation * Vector3.forward; } } #if UNITY_2017_3_OR_NEWER else if (Tools.current == Tool.Move || Tools.current == Tool.Transform) #else else if (Tools.current == Tool.Move) #endif { EditorGUI.BeginChangeCheck(); componentHandlePosition = Handles.PositionHandle(componentHandlePosition, componentHandleRotation); if (EditorGUI.EndChangeCheck()) { isSceneViewModified = true; Undo.RecordObject(demoControlModule, "Move Squadron Anchor Position"); svSquadron.anchorPosition = componentHandlePosition; } } } } // Draw the theatre of operations boundaries if (demoControlModule.theatreBounds.extents != Vector3.zero) { Handles.DrawWireCube(demoControlModule.theatreBounds.center, demoControlModule.theatreBounds.extents * 2f); } if (Tools.current == Tool.Scale) { scale = demoControlModule.theatreBounds.extents; theatrePos = demoControlModule.theatreBounds.center; EditorGUI.BeginChangeCheck(); scale = Handles.ScaleHandle(scale, theatrePos, Quaternion.identity, HandleUtility.GetHandleSize(theatrePos)); if (EditorGUI.EndChangeCheck()) { demoControlModule.theatreBounds.extents = scale; GUI.FocusControl(null); Undo.RecordObject(demoControlModule, "Modify Theatre Extents"); isSceneViewModified = true; } } #region Mark Scene Dirty if required if (isSceneViewModified && !Application.isPlaying) { isSceneViewModified = false; EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene()); } #endregion } } #endregion } }