Files
2025-07-20 10:01:29 +08:00

367 lines
19 KiB
C#

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditorInternal;
namespace EmeraldAI.Utility
{
[CustomEditor(typeof(EmeraldDetection))]
[CanEditMultipleObjects]
public class EmeraldDetectionEditor : Editor
{
#region Variables
GUIStyle FoldoutStyle;
EmeraldBehaviors BehaviorsComponent;
Texture DetectionEditorIcon;
//Ints
SerializedProperty FieldOfViewAngleProp, DetectionRadiusProp, CurrentFactionProp;
//Floats
SerializedProperty ObstructionDetectionFrequencyProp;
//Reorderable List
ReorderableList FactionsList;
//String
SerializedProperty PlayerTagProp, RagdollTagProp;
//Bool
SerializedProperty HideSettingsFoldout, DetectionFoldout, TagFoldout, FactionFoldout;
//Float
SerializedProperty DetectionFrequencyProp, ObstructionSecondsProp;
//Object
SerializedProperty HeadTransformProp;
//LayerMasks
SerializedProperty DetectionLayerMaskProp, ObstructionDetectionLayerMaskProp;
#endregion
void OnEnable()
{
EmeraldDetection self = (EmeraldDetection)target;
BehaviorsComponent = self.GetComponent<EmeraldBehaviors>();
if (DetectionEditorIcon == null) DetectionEditorIcon = Resources.Load("Editor Icons/EmeraldDetection") as Texture;
RefreshFactionData();
}
void RefreshFactionData ()
{
LoadFactionData();
InitializeProperties();
InitializeFactionList();
}
void MissingComponentsMessage (EmeraldDetection self)
{
if (!self.HeadTransform)
{
CustomEditorProperties.DisplaySetupWarning("The AI's Head Transform has not been applied and is needed for accurate raycast calculations, please apply it. This is located within the Detection Settings foldout.");
}
else if (self.FactionRelationsList.Count == 0 && BehaviorsComponent.CurrentBehaviorType != EmeraldBehaviors.BehaviorTypes.Passive)
{
CustomEditorProperties.DisplaySetupWarning("This AI needs at least 1 Faction Relation to function properly. Please apply one through the Faction Settings foldout below.");
}
}
void InitializeProperties()
{
//Ints
FieldOfViewAngleProp = serializedObject.FindProperty("FieldOfViewAngle");
DetectionRadiusProp = serializedObject.FindProperty("DetectionRadius");
CurrentFactionProp = serializedObject.FindProperty("CurrentFaction");
//Bool
HideSettingsFoldout = serializedObject.FindProperty("HideSettingsFoldout");
DetectionFoldout = serializedObject.FindProperty("DetectionFoldout");
TagFoldout = serializedObject.FindProperty("TagFoldout");
FactionFoldout = serializedObject.FindProperty("FactionFoldout");
//String
PlayerTagProp = serializedObject.FindProperty("PlayerTag");
RagdollTagProp = serializedObject.FindProperty("RagdollTag");
//Float
DetectionFrequencyProp = serializedObject.FindProperty("DetectionFrequency");
ObstructionDetectionFrequencyProp = serializedObject.FindProperty("ObstructionDetectionFrequency");
ObstructionSecondsProp = serializedObject.FindProperty("ObstructionSeconds");
//Object
HeadTransformProp = serializedObject.FindProperty("HeadTransform");
//LayerMasks
DetectionLayerMaskProp = serializedObject.FindProperty("DetectionLayerMask");
ObstructionDetectionLayerMaskProp = serializedObject.FindProperty("ObstructionDetectionLayerMask");
}
void InitializeFactionList()
{
//Factions List
FactionsList = new ReorderableList(serializedObject, serializedObject.FindProperty("FactionRelationsList"), true, true, true, true);
FactionsList.drawElementCallback =
(Rect rect, int index, bool isActive, bool isFocused) =>
{
var element = FactionsList.serializedProperty.GetArrayElementAtIndex(index);
FactionsList.elementHeight = EditorGUIUtility.singleLineHeight * 3.75f;
if (element.FindPropertyRelative("RelationType").intValue == 0)
{
EditorGUI.DrawRect(new Rect(rect.x - 16, rect.y + 2f, rect.width + 17, EditorGUIUtility.singleLineHeight * 3.5f), new Color(1.0f, 0.0f, 0.0f, 0.15f));
}
else if (element.FindPropertyRelative("RelationType").intValue == 1)
{
EditorGUI.DrawRect(new Rect(rect.x - 16, rect.y + 2f, rect.width + 17, EditorGUIUtility.singleLineHeight * 3.5f), new Color(0.1f, 0.1f, 0.1f, 0.1f));
}
else if (element.FindPropertyRelative("RelationType").intValue == 2)
{
EditorGUI.DrawRect(new Rect(rect.x - 16, rect.y + 2f, rect.width + 17, EditorGUIUtility.singleLineHeight * 3.5f), new Color(0.0f, 1.0f, 0.0f, 0.15f));
}
EditorGUI.PropertyField(new Rect(rect.x, rect.y + 35, rect.width, EditorGUIUtility.singleLineHeight), element.FindPropertyRelative("RelationType"), new GUIContent("Relation Type", "The type of relation this AI has with this faction."));
CustomEditorProperties.CustomListPopup(new Rect(rect.x + 125, rect.y + 10, rect.width - 125, EditorGUIUtility.singleLineHeight), new GUIContent(), element.FindPropertyRelative("FactionIndex"), "Faction", EmeraldDetection.StringFactionList.ToArray());
EditorGUI.PrefixLabel(new Rect(rect.x, rect.y + 10, 125, EditorGUIUtility.singleLineHeight),
new GUIContent("Faction", "Factions are based on all factions within the Faction Manager. An AI can have as many faction relations as needed."));
};
FactionsList.drawHeaderCallback = rect =>
{
EditorGUI.LabelField(rect, "AI Faction Relations", EditorStyles.boldLabel);
};
}
public override void OnInspectorGUI()
{
FoldoutStyle = CustomEditorProperties.UpdateEditorStyles();
EmeraldDetection self = (EmeraldDetection)target;
serializedObject.Update();
CustomEditorProperties.BeginScriptHeaderNew("Detection", DetectionEditorIcon, new GUIContent(), HideSettingsFoldout);
MissingComponentsMessage(self);
if (!HideSettingsFoldout.boolValue)
{
EditorGUILayout.Space();
DetectionSettings(self);
EditorGUILayout.Space();
TagSettings(self);
EditorGUILayout.Space();
FactionSettings(self);
EditorGUILayout.Space();
}
CustomEditorProperties.EndScriptHeader();
serializedObject.ApplyModifiedProperties();
}
void DetectionSettings(EmeraldDetection self)
{
DetectionFoldout.boolValue = EditorGUILayout.Foldout(DetectionFoldout.boolValue, "Detection Settings", true, FoldoutStyle);
if (DetectionFoldout.boolValue)
{
CustomEditorProperties.BeginFoldoutWindowBox();
CustomEditorProperties.TextTitleWithDescription("Detection Settings", "Controls various detection settings such as radius distances, target detection, and field of view.", true);
CustomEditorProperties.CustomIntSlider(new Rect(), new GUIContent(), FieldOfViewAngleProp, "Field of View", 1, 360);
CustomEditorProperties.CustomHelpLabelField("Controls the field of view an AI uses to detect targets.", true);
CustomEditorProperties.CustomIntSlider(new Rect(), new GUIContent(), DetectionRadiusProp, "Detection Distance", 1, 100);
CustomEditorProperties.CustomHelpLabelField("Controls the distance of the field of view as well as the AI's detection radius.", true);
CustomEditorProperties.CustomFloatSlider(new Rect(), new GUIContent(), DetectionFrequencyProp, "Detection Frequency", 0.1f, 2f);
CustomEditorProperties.CustomHelpLabelField("Controls how often the AI's detection calculations update.", true);
CustomEditorProperties.CustomFloatSlider(new Rect(), new GUIContent(), ObstructionSecondsProp, "Obstruction Seconds", 0.5f, 5f);
CustomEditorProperties.CustomHelpLabelField("Controls how many seconds must pass, while obstructed, before an AI will switch to a new target.", true);
CustomEditorProperties.CustomFloatSlider(new Rect(), new GUIContent(), ObstructionDetectionFrequencyProp, "Obstruction Detection Frequency", 0.05f, 1f);
CustomEditorProperties.CustomHelpLabelField("Controls how often the AI checks for obstructions between them and their current target.", false);
CustomEditorProperties.BeginIndent();
EditorGUILayout.PropertyField(ObstructionDetectionLayerMaskProp, new GUIContent("Obstruction Ignore Layers"));
CustomEditorProperties.CustomHelpLabelField("The layers that should be ignored when an AI is using its obstruction detection for attacking." +
"These are objects that may prevent an AI from seeing its target. If your target has nothing that will block the AI's sight, you can " +
"set the layermask to Nothing.", true);
CustomEditorProperties.EndIndent();
EditorGUILayout.Space();
EditorGUILayout.PropertyField(HeadTransformProp, new GUIContent("Head Transform"));
CustomEditorProperties.CustomHelpLabelField("The head transform of your AI. This is used for accurate head looking and raycast calculations related to sight and obstruction detection. " +
"This should be your AI's head object within its bone objects.", false);
CustomEditorProperties.AutoFindHeadTransform(new Rect(), new GUIContent(), HeadTransformProp, self.transform);
EditorGUILayout.Space();
EditorGUILayout.Space();
CustomEditorProperties.EndFoldoutWindowBox();
}
}
void TagSettings(EmeraldDetection self)
{
TagFoldout.boolValue = EditorGUILayout.Foldout(TagFoldout.boolValue, "Tag & Layer Settings", true, FoldoutStyle);
if (TagFoldout.boolValue)
{
CustomEditorProperties.BeginFoldoutWindowBox();
CustomEditorProperties.TextTitleWithDescription("Tag & Layer Settings", "Controls an AI's Detection Layers. These are used to allow the AI to know what Layers are detectable targets.", true);
CustomEditorProperties.TutorialButton("For a tutorial on setting up an AI's Detection Layers and Player Tag, please see the tutorial below.",
"https://black-horizon-studios.gitbook.io/emerald-ai-wiki/emerald-components-required/detection-component/setting-up-the-detection-layers-and-player-tag");
CustomEditorProperties.NoticeTextTitleWithDescription("Important", "The Player Relation is handled through the AI's Faction Relations List (within the Faction Settings foldout below). The Player Unity Tag is used to determine certain internal functionality.", false);
CustomEditorProperties.CustomTagField(new Rect(), new GUIContent(), PlayerTagProp, "Player Unity Tag");
CustomEditorProperties.CustomHelpLabelField("The Unity Tag used to define Player objects. This is the tag that was assigned using Unity's Tag pulldown at the top of " +
"the gameobject.", true);
EditorGUILayout.Space();
EditorGUILayout.PropertyField(DetectionLayerMaskProp, new GUIContent("Detection Layers"));
CustomEditorProperties.CustomHelpLabelField("The Detection Layers controls what layers this AI can detect as possible targets.", false);
if (DetectionLayerMaskProp.intValue == 0 || DetectionLayerMaskProp.intValue == 1)
{
GUI.backgroundColor = new Color(10f, 0.0f, 0.0f, 0.25f);
EditorGUILayout.LabelField("The Detection Layers cannot contain Nothing, Default, or Everything.", EditorStyles.helpBox);
GUI.backgroundColor = Color.white;
}
EditorGUILayout.Space();
EditorGUILayout.Space();
CustomEditorProperties.EndFoldoutWindowBox();
}
}
void FactionSettings(EmeraldDetection self)
{
FactionFoldout.boolValue = EditorGUILayout.Foldout(FactionFoldout.boolValue, "Faction Settings", true, FoldoutStyle);
if (FactionFoldout.boolValue)
{
CustomEditorProperties.BeginFoldoutWindowBox();
CustomEditorProperties.TextTitleWithDescription("Faction Settings", "The Faction Settings allow you to control which Factions your AI " +
"sees as enemies or allies, including the relations with the AI and the player.", true);
CustomEditorProperties.TutorialButton("For a tutorial on setting up an AI's faction relations, please see the tutorial below.",
"https://black-horizon-studios.gitbook.io/emerald-ai-wiki/emerald-components-required/detection-component/faction-relations#setting-up-an-ais-faction-relations");
EditorGUILayout.Space();
CustomEditorProperties.CustomEnum(new Rect(), new GUIContent(), CurrentFactionProp, "Faction");
CustomEditorProperties.CustomHelpLabelField("An AI's Faction is the name used to control combat reaction with other AI. This is the name other AI will use when " +
"looking for opposing targets.", true);
CustomEditorProperties.CustomHelpLabelField("Factions can be created and removed using the Faction Manager. ", false);
if (GUILayout.Button("Open Faction Manager"))
{
EditorWindow APS = EditorWindow.GetWindow(typeof(EmeraldFactionManager));
APS.minSize = new Vector2(600f, 775f);
}
EditorGUILayout.Space();
EditorGUILayout.Space();
EditorGUILayout.LabelField("AI Faction Relations", EditorStyles.boldLabel);
CustomEditorProperties.CustomHelpLabelField("Controls which factions this AI sees as enemies and allies. You can hover the mouse over each setting to view its tooltip.", false);
GUI.backgroundColor = new Color(1f, 1, 0.25f, 0.25f);
EditorGUILayout.LabelField("Note: The AI Faction Relations use an AI's Faction not Unity tags. You can add and remove factions through the Faction Manager. " +
"This can be opened by pressing the button below.", EditorStyles.helpBox);
GUI.backgroundColor = Color.white;
EditorGUILayout.Space();
if (GUILayout.Button("Open Faction Manager"))
{
EditorWindow APS = EditorWindow.GetWindow(typeof(EmeraldFactionManager));
APS.minSize = new Vector2(600f, 775f);
}
EditorGUILayout.Space();
EditorGUILayout.Space();
FactionsList.DoLayoutList();
EditorGUILayout.Space();
EditorGUILayout.Space();
CustomEditorProperties.EndFoldoutWindowBox();
}
}
private void OnSceneGUI()
{
EmeraldDetection self = (EmeraldDetection)target;
DrawDetectionSettings(self);
}
public Vector3 DirFromAngle(Transform transform, float angleInDegrees, bool angleIsGlobal, EmeraldDetection self)
{
if (!angleIsGlobal)
angleInDegrees += transform.eulerAngles.y;
return transform.rotation * Quaternion.Euler(new Vector3(0, -transform.eulerAngles.y, 0)) * new Vector3(Mathf.Sin(angleInDegrees * Mathf.Deg2Rad), 0, Mathf.Cos(angleInDegrees * Mathf.Deg2Rad));
}
void DrawDetectionSettings (EmeraldDetection self)
{
if (DetectionFoldout.boolValue && !HideSettingsFoldout.boolValue)
{
//Red areas not covered by the line of sight, but the areas in yellow are.
Handles.color = Color.red;
Handles.DrawWireArc(self.transform.position, self.transform.up, self.transform.forward, (float)self.FieldOfViewAngle / 2f, self.DetectionRadius, 3f);
Handles.DrawWireArc(self.transform.position, self.transform.up, self.transform.forward, -(float)self.FieldOfViewAngle / 2f, self.DetectionRadius, 3f);
Handles.color = Color.yellow;
Handles.DrawWireArc(self.transform.position, self.transform.up, -self.transform.forward, (360 - self.FieldOfViewAngle) / 2f, self.DetectionRadius, 3f);
Handles.DrawWireArc(self.transform.position, self.transform.up, -self.transform.forward, -(360 - self.FieldOfViewAngle) / 2f, self.DetectionRadius, 3f);
Vector3 viewAngleA = DirFromAngle(self.transform, -self.FieldOfViewAngle / 2f, false, self);
Vector3 viewAngleB = DirFromAngle(self.transform, self.FieldOfViewAngle / 2f, false, self);
Handles.color = Color.red;
if (self.FieldOfViewAngle < 360)
{
Handles.DrawLine(self.transform.position, self.transform.position + viewAngleA * self.DetectionRadius, 3f);
Handles.DrawLine(self.transform.position, self.transform.position + viewAngleB * self.DetectionRadius, 3f);
}
Handles.color = Color.white;
}
}
void LoadFactionData()
{
EmeraldDetection.StringFactionList.Clear();
string path = AssetDatabase.GetAssetPath(Resources.Load("Faction Data"));
EmeraldFactionData FactionData = (EmeraldFactionData)AssetDatabase.LoadAssetAtPath(path, typeof(EmeraldFactionData));
if (FactionData != null)
{
foreach (string s in FactionData.FactionNameList)
{
if (!EmeraldDetection.StringFactionList.Contains(s) && s != "")
{
EmeraldDetection.StringFactionList.Add(s);
}
}
}
}
void CustomTag(Rect position, GUIContent label, SerializedProperty property)
{
label = EditorGUI.BeginProperty(position, label, property);
EditorGUI.BeginChangeCheck();
var newValue = EditorGUI.TagField(position, property.stringValue);
if (EditorGUI.EndChangeCheck())
property.stringValue = newValue;
EditorGUI.EndProperty();
}
}
}