init 1.1.2
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ea0329fda166bc34897dc91faeeff562
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,285 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using System.Reflection;
|
||||
using UnityEditorInternal;
|
||||
using System.Linq;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace EmeraldAI.Utility
|
||||
{
|
||||
[CustomEditor(typeof(EmeraldAnimation))]
|
||||
[CanEditMultipleObjects]
|
||||
public class EmeraldAnimationEditor : Editor
|
||||
{
|
||||
public static EditorWindow EditorWindowRef;
|
||||
#region SerializedProperties
|
||||
List<string> Type1AttackAnimationEnum = new List<string>();
|
||||
List<string> Type2AttackAnimationEnum = new List<string>();
|
||||
GUIStyle FoldoutStyle;
|
||||
Texture AnimationsEditorIcon;
|
||||
|
||||
bool IsInScene;
|
||||
bool IsInPrefabInstance;
|
||||
|
||||
SerializedProperty AnimationProfileProp, HideSettingsFoldout, AnimationProfileFoldout;
|
||||
#endregion
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
EmeraldAnimation self = (EmeraldAnimation)target;
|
||||
self.AIAnimator = self.GetComponent<Animator>();
|
||||
if (AnimationsEditorIcon == null) AnimationsEditorIcon = Resources.Load("Editor Icons/EmeraldAnimation") as Texture;
|
||||
|
||||
ApplyRuntimeAnimatorController(self);
|
||||
UpdateAbilityAnimationEnums();
|
||||
InitializeProperties();
|
||||
|
||||
IsInScene = self.gameObject.scene.IsValid();
|
||||
IsInPrefabInstance = StageUtility.GetStage(self.gameObject) != StageUtility.GetMainStage();
|
||||
}
|
||||
|
||||
void InitializeProperties()
|
||||
{
|
||||
HideSettingsFoldout = serializedObject.FindProperty("HideSettingsFoldout");
|
||||
AnimationProfileFoldout = serializedObject.FindProperty("AnimationProfileFoldout");
|
||||
AnimationProfileProp = serializedObject.FindProperty("m_AnimationProfile");
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
FoldoutStyle = CustomEditorProperties.UpdateEditorStyles();
|
||||
EmeraldAnimation self = (EmeraldAnimation)target;
|
||||
serializedObject.Update();
|
||||
|
||||
CustomEditorProperties.BeginScriptHeaderNew("Animations", AnimationsEditorIcon, new GUIContent(), HideSettingsFoldout);
|
||||
|
||||
MissingAnimationProfileMessage(self);
|
||||
|
||||
if (!HideSettingsFoldout.boolValue)
|
||||
{
|
||||
EditorGUILayout.Space();
|
||||
AnimationProfiles(self);
|
||||
EditorGUILayout.Space();
|
||||
UpdateEditor(self);
|
||||
}
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
CustomEditorProperties.EndScriptHeader();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays a missing Animation Profile message within the EmeraldAnimation.
|
||||
/// </summary>
|
||||
void MissingAnimationProfileMessage(EmeraldAnimation self)
|
||||
{
|
||||
if (self.m_AnimationProfile == null)
|
||||
{
|
||||
CustomEditorProperties.DisplaySetupWarning("This AI needs to have an Animation Profile. Press the 'Create New Animation Profile' button below to create a new one or assign one that has already been created.");
|
||||
}
|
||||
else if (self.m_AnimationProfile.AIAnimator == null)
|
||||
{
|
||||
CustomEditorProperties.DisplaySetupWarning("This AI has an Animation Profile, but an Animator Controller has not been generated for it. Please create one and assign all needed animations through the Animation Profile object. " +
|
||||
"You can press the 'Edit Animation Profile' to open up an editor window to begin editing.");
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateAbilityAnimationEnums()
|
||||
{
|
||||
EmeraldAnimation self = (EmeraldAnimation)target;
|
||||
|
||||
if (self.m_AnimationProfile == null)
|
||||
return;
|
||||
|
||||
//Populate the Type1AttackEnumAnimations array with the proper animation name.
|
||||
if (self.m_AnimationProfile.Type1Animations.AttackList.Count > 0)
|
||||
{
|
||||
for (int i = 0; i < self.m_AnimationProfile.Type1Animations.AttackList.Count; i++)
|
||||
{
|
||||
if (self.m_AnimationProfile.Type1Animations.AttackList[i].AnimationClip != null)
|
||||
Type1AttackAnimationEnum.Add(self.m_AnimationProfile.Type1Animations.AttackList[i].AnimationClip.name);
|
||||
}
|
||||
}
|
||||
|
||||
//Populate the Type2AttackEnumAnimations array with the proper animation name.
|
||||
if (self.m_AnimationProfile.Type2Animations.AttackList.Count > 0)
|
||||
{
|
||||
for (int i = 0; i < self.m_AnimationProfile.Type2Animations.AttackList.Count; i++)
|
||||
{
|
||||
if (self.m_AnimationProfile.Type2Animations.AttackList[i].AnimationClip != null)
|
||||
Type2AttackAnimationEnum.Add(self.m_AnimationProfile.Type2Animations.AttackList[i].AnimationClip.name);
|
||||
}
|
||||
}
|
||||
|
||||
//Pass the array to the EmeraldAnimation script so it can be stored
|
||||
self.Type1AttackEnumAnimations = Type1AttackAnimationEnum.ToArray();
|
||||
self.Type2AttackEnumAnimations = Type2AttackAnimationEnum.ToArray();
|
||||
}
|
||||
|
||||
void AnimationProfiles(EmeraldAnimation self)
|
||||
{
|
||||
AnimationProfileFoldout.boolValue = CustomEditorProperties.Foldout(AnimationProfileFoldout.boolValue, "Animation Profile Settings", true, FoldoutStyle);
|
||||
|
||||
if (AnimationProfileFoldout.boolValue)
|
||||
{
|
||||
CustomEditorProperties.BeginFoldoutWindowBox();
|
||||
CustomEditorProperties.TextTitleWithDescription("Animation Profile", "An Animation Profile holds all of an AI's animation data, including the Animator Controller this AI will use. This allows AI to share the same animation data with only needing to rely on a single " +
|
||||
"Animation Profile. Any changes made to an Animation Profile will affect any AI using that Animation Profile.", false);
|
||||
|
||||
CustomEditorProperties.TextTitleWithDescription("Note", "The animations must be compatible with this model and share the same Rig Type. If your AI doesn't play animations correctly, or falls through the floor, it is likely that you are missing an animation, the " +
|
||||
"Rig Type is not compatible, or that the animation is not compatible.", true);
|
||||
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.PropertyField(AnimationProfileProp);
|
||||
CustomEditorProperties.CustomHelpLabelField("The Animation Profile this AI is using. All animations, including the Animator Controller, will be used for this AI and any other AI using it.", false);
|
||||
|
||||
if (!IsInScene)
|
||||
{
|
||||
CustomEditorProperties.DisplayImportantMessage("The Animation Viewer can't be used in the Project tab. The AI must be within the Scene and in the Hierarchy tab.");
|
||||
}
|
||||
|
||||
if (IsInPrefabInstance)
|
||||
{
|
||||
CustomEditorProperties.DisplayImportantMessage("The Animation Viewer can't be used while editing a prefab.");
|
||||
}
|
||||
|
||||
EditorGUI.BeginDisabledGroup(!IsInScene || IsInPrefabInstance);//AA
|
||||
EditorGUI.BeginDisabledGroup(self.m_AnimationProfile == null || self.m_AnimationProfile.AIAnimator == null);
|
||||
EditorGUILayout.Space();
|
||||
if (GUILayout.Button(new GUIContent("Open Animation Viewer", "Preview all animations on the current Animation Profile, in real-time, on this AI within the Unity Scene."), GUILayout.Height(20)))
|
||||
{
|
||||
OpenAnimationPreview(self);
|
||||
}
|
||||
GUILayout.Space(2.5f);
|
||||
EditorGUI.EndDisabledGroup();
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
EditorGUI.BeginDisabledGroup(self.m_AnimationProfile == null);
|
||||
if (GUILayout.Button(new GUIContent("Edit Animation Profile", "Edit the current Animation Profile in a separate window so you can preview animations while keeping a reference to the current Animation Profile."), GUILayout.Height(20)))
|
||||
{
|
||||
EditAnimationProfile(self);
|
||||
}
|
||||
GUILayout.Space(2.5f);
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
EditorGUI.BeginDisabledGroup(self.m_AnimationProfile == null);
|
||||
if (GUILayout.Button(new GUIContent("Clear Animation Profile", "Clears the Animation Profile slot so a new one can be created. Note: The current Animation Profile object will remain in your project at its current path."), GUILayout.Height(20)))
|
||||
{
|
||||
AnimationProfileProp.objectReferenceValue = null;
|
||||
serializedObject.FindProperty("AIAnimator").objectReferenceValue = null;
|
||||
self.AnimatorControllerGenerated = false;
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
GUILayout.Space(2.5f);
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
EditorGUI.BeginDisabledGroup(self.m_AnimationProfile != null);
|
||||
if (GUILayout.Button(new GUIContent("Create New Animation Profile", "Creates a new Animation Profile within the Emerald AI/Animation Profiles folder. If you would like to create a new Animation Profile, remove the one in the current slot by pressing the 'Clear Animation Profile' button."), GUILayout.Height(20)))
|
||||
{
|
||||
CreateAnimationProfile(self);
|
||||
}
|
||||
GUILayout.Space(2.5f);
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
CustomEditorProperties.EndFoldoutWindowBox();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the Runtime Animator Controller from the Animation Profile to the AI's Animator.
|
||||
/// </summary>
|
||||
void ApplyRuntimeAnimatorController (EmeraldAnimation self)
|
||||
{
|
||||
if (self.AIAnimator != null && self.m_AnimationProfile != null && self.AIAnimator.runtimeAnimatorController == null && self.m_AnimationProfile.AIAnimator != null ||
|
||||
self.AIAnimator != null && self.m_AnimationProfile != null && self.m_AnimationProfile.AIAnimator != null && self.AIAnimator != self.m_AnimationProfile.AIAnimator)
|
||||
self.AIAnimator.runtimeAnimatorController = self.m_AnimationProfile.AIAnimator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Animation Profile object, using the object's name, to the user set folder.
|
||||
/// </summary>
|
||||
void CreateAnimationProfile(EmeraldAnimation self)
|
||||
{
|
||||
string FilePath = EditorUtility.SaveFilePanelInProject("Save as Animation Profile", "", "asset", "Please enter a file name to save the file to");
|
||||
|
||||
if (string.IsNullOrEmpty(FilePath))
|
||||
{
|
||||
//For some reason, EditorUtility.SaveFilePanelInProject throws an incorrect EditorGUILayout error when it's used with some custom properties. This fixes it...
|
||||
CustomEditorProperties.BeginScriptHeader("", null);
|
||||
CustomEditorProperties.BeginFoldoutWindowBox();
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(FilePath)))
|
||||
{
|
||||
AnimationProfile NewAnimationProfile = CreateInstance<AnimationProfile>();
|
||||
AssetDatabase.CreateAsset(NewAnimationProfile, FilePath);
|
||||
self.m_AnimationProfile = NewAnimationProfile;
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
else
|
||||
{
|
||||
var ExistingAnimationProfile = AssetDatabase.LoadAssetAtPath(FilePath, typeof(AnimationProfile));
|
||||
self.m_AnimationProfile = (AnimationProfile)ExistingAnimationProfile;
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
//For some reason, EditorUtility.SaveFilePanelInProject throws an incorrect EditorGUILayout error when it's used with some custom properties. This fixes it...
|
||||
CustomEditorProperties.BeginScriptHeader("", null);
|
||||
CustomEditorProperties.BeginFoldoutWindowBox();
|
||||
}
|
||||
|
||||
void OpenAnimationPreview (EmeraldAnimation self)
|
||||
{
|
||||
var m_AnimationPreviewEditor = (AnimationViewerManager)EditorWindow.GetWindow(typeof(AnimationViewerManager), true, "Animation Viewer Manager");
|
||||
m_AnimationPreviewEditor.Initialize(self.gameObject);
|
||||
}
|
||||
|
||||
void EditAnimationProfile (EmeraldAnimation self)
|
||||
{
|
||||
if (self.m_AnimationProfile == null)
|
||||
return;
|
||||
|
||||
//Close the static reference to any other Animation Profile PropertyEditors before creating a new one
|
||||
if (EditorWindowRef != null && EditorWindowRef.name == "Animation Profile")
|
||||
EditorWindowRef.Close();
|
||||
|
||||
System.Type propertyEditorType = typeof(Editor).Assembly.GetType("UnityEditor.PropertyEditor");
|
||||
System.Type[] callTypes = new[] { typeof(Object), typeof(bool) };
|
||||
object[] callOpenBuffer = { null, true };
|
||||
|
||||
//Use reflection to create a PropertyEditor, as there's no API to do so before Unity 2021.2, and pass the Animation Profile to open it in a separate tab.
|
||||
MethodInfo openPropertyEditorInfo;
|
||||
openPropertyEditorInfo = propertyEditorType.GetMethod("OpenPropertyEditor", BindingFlags.Static | BindingFlags.NonPublic, null, callTypes, null);
|
||||
self.m_AnimationProfile.EmeraldAnimationComponent = self; //Used for updating changes the currently edited AI (given that it isn't null)
|
||||
callOpenBuffer[0] = self.m_AnimationProfile;
|
||||
openPropertyEditorInfo.Invoke(null, callOpenBuffer);
|
||||
|
||||
//Cache the PropertyEditor and name it Sound Profile (only one can be active at a time)
|
||||
EditorWindowRef = EditorWindow.GetWindow(typeof(Editor).Assembly.GetType("UnityEditor.PropertyEditor"));
|
||||
EditorWindowRef.name = "Animation Profile";
|
||||
EditorWindowRef.minSize = new Vector2(Screen.currentResolution.width / 4f, Screen.currentResolution.height / 2f);
|
||||
}
|
||||
|
||||
void UpdateEditor (EmeraldAnimation self)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
Undo.RecordObject(self, "Undo");
|
||||
|
||||
if (GUI.changed)
|
||||
{
|
||||
EditorUtility.SetDirty(target);
|
||||
EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 20a8ffcfdc54c2947a2a59b32358ba5c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,279 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System;
|
||||
|
||||
namespace EmeraldAI.Utility
|
||||
{
|
||||
[CustomEditor(typeof(EmeraldBehaviors), true)]
|
||||
[CanEditMultipleObjects]
|
||||
public class EmeraldBehaviorsEditor : Editor
|
||||
{
|
||||
GUIStyle FoldoutStyle;
|
||||
Texture BehaviorsEditorIcon;
|
||||
FieldInfo[] CustomFields;
|
||||
SerializedProperty HideSettingsFoldout, BehaviorSettingsFoldout, CurrentBehaviorType, CustomSettingsFoldout, TargetToFollow, CautiousSeconds, ChaseSeconds,
|
||||
FleeSeconds, RequireObstruction, InfititeChase, FleeOnLowHealth, StayNearStartingArea, MaxDistanceFromStartingArea, UpdateFleePositionSeconds, PercentToFlee, FollowingStoppingDistance;
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
if (BehaviorsEditorIcon == null) BehaviorsEditorIcon = Resources.Load("Editor Icons/EmeraldBehaviors") as Texture;
|
||||
InitializeProperties();
|
||||
}
|
||||
|
||||
void InitializeProperties()
|
||||
{
|
||||
HideSettingsFoldout = serializedObject.FindProperty("HideSettingsFoldout");
|
||||
BehaviorSettingsFoldout = serializedObject.FindProperty("BehaviorSettingsFoldout");
|
||||
CustomSettingsFoldout = serializedObject.FindProperty("CustomSettingsFoldout");
|
||||
CurrentBehaviorType = serializedObject.FindProperty("CurrentBehaviorType");
|
||||
TargetToFollow = serializedObject.FindProperty("TargetToFollow");
|
||||
CautiousSeconds = serializedObject.FindProperty("CautiousSeconds");
|
||||
ChaseSeconds = serializedObject.FindProperty("ChaseSeconds");
|
||||
FleeSeconds = serializedObject.FindProperty("FleeSeconds");
|
||||
RequireObstruction = serializedObject.FindProperty("RequireObstruction");
|
||||
InfititeChase = serializedObject.FindProperty("InfititeChase");
|
||||
FleeOnLowHealth = serializedObject.FindProperty("FleeOnLowHealth");
|
||||
StayNearStartingArea = serializedObject.FindProperty("StayNearStartingArea");
|
||||
UpdateFleePositionSeconds = serializedObject.FindProperty("UpdateFleePositionSeconds");
|
||||
PercentToFlee = serializedObject.FindProperty("PercentToFlee");
|
||||
MaxDistanceFromStartingArea = serializedObject.FindProperty("MaxDistanceFromStartingArea");
|
||||
FollowingStoppingDistance = serializedObject.FindProperty("FollowingStoppingDistance");
|
||||
|
||||
//Get all variables that are not part of the parent class.
|
||||
CustomFields = target.GetType().GetFields(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public);
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
FoldoutStyle = CustomEditorProperties.UpdateEditorStyles();
|
||||
EmeraldBehaviors self = (EmeraldBehaviors)target;
|
||||
serializedObject.Update();
|
||||
|
||||
CustomEditorProperties.BeginScriptHeaderNew("Behaviors", BehaviorsEditorIcon, new GUIContent(), HideSettingsFoldout);
|
||||
|
||||
if (!HideSettingsFoldout.boolValue)
|
||||
{
|
||||
EditorGUILayout.Space();
|
||||
BehaviorSettings(self);
|
||||
if (self.GetType().ToString() != "EmeraldAI.EmeraldBehaviors")
|
||||
{
|
||||
EditorGUILayout.Space();
|
||||
CustomSettings(self);
|
||||
}
|
||||
EditorGUILayout.Space();
|
||||
}
|
||||
|
||||
CustomEditorProperties.EndScriptHeader();
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
void BehaviorSettings (EmeraldBehaviors self)
|
||||
{
|
||||
BehaviorSettingsFoldout.boolValue = EditorGUILayout.Foldout(BehaviorSettingsFoldout.boolValue, "Behavior Settings", true, FoldoutStyle);
|
||||
|
||||
if (BehaviorSettingsFoldout.boolValue)
|
||||
{
|
||||
CustomEditorProperties.BeginFoldoutWindowBox();
|
||||
CustomEditorProperties.TextTitleWithDescription("Behavior Settings", "Choose from 1 of the 3 available base behavior types. Companion and Pet options are avaialble witin these options by setting a Target to Follow.", true);
|
||||
EditorGUILayout.Space();
|
||||
|
||||
EditorGUILayout.Space();
|
||||
CustomEditorProperties.CustomPropertyField(CurrentBehaviorType, "Current Behavior Type", "The behavior this AI will use.", true); //Overridding the method will allow users to create their own version of the specified behavior.
|
||||
|
||||
PassiveSettings(self);
|
||||
CowardSettings(self);
|
||||
AggressiveSettings(self);
|
||||
|
||||
CustomEditorProperties.EndFoldoutWindowBox();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays all options that are intended for the Passive Behavior Type.
|
||||
/// </summary>
|
||||
void PassiveSettings (EmeraldBehaviors self)
|
||||
{
|
||||
if (self.CurrentBehaviorType != EmeraldBehaviors.BehaviorTypes.Passive)
|
||||
return;
|
||||
|
||||
CustomEditorProperties.TextTitleWithDescription("Passive Settings", "Passive AI will not attack or flee from targets. They will wander according to their Wander Type set within the Movement Component.", true);
|
||||
|
||||
CustomEditorProperties.CustomPropertyField(TargetToFollow, "Target to Follow", "Assigning a Target to Follow will turn an AI into a Pet AI (or a non-combat Componanion AI). Note: If a Target to Follow is assigned, they will ignore their Wander Type and follow their follower instead.", true);
|
||||
|
||||
if (self.TargetToFollow)
|
||||
{
|
||||
CustomEditorProperties.CustomFloatSlider(new Rect(), new GUIContent(), FollowingStoppingDistance, "Following Stopping Distance", 1, 15);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls the distance in which an AI will stop from their Target to Follow.", true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays all options that are intended for the Aggressive Behavior Type.
|
||||
/// </summary>
|
||||
void CowardSettings(EmeraldBehaviors self)
|
||||
{
|
||||
if (self.CurrentBehaviorType != EmeraldBehaviors.BehaviorTypes.Coward)
|
||||
return;
|
||||
|
||||
CustomEditorProperties.TextTitleWithDescription("Coward Settings", "Coward AI will flee from targets who they have an Enemy Relation Type with.", true);
|
||||
|
||||
CustomEditorProperties.CustomIntSliderPropertyField(CautiousSeconds, "Cautious Seconds", "Controls the amount of time an AI will remain in the Cautious State before fleeing from their target. " +
|
||||
"If an AI has a warning animation, this will automatically be played while in this state. If this value is set to 0, the cautious state will be ignored.", 0, 15, false);
|
||||
|
||||
if (self.CautiousSeconds > 0)
|
||||
{
|
||||
CustomEditorProperties.DisplayImportantMessage("Be aware that AI who are in a cautious state will not flee from a detected target until after the duration of their Cautious Seconds (unless they're attacked).");
|
||||
}
|
||||
|
||||
EditorGUI.BeginDisabledGroup(self.InfititeChase);
|
||||
CustomEditorProperties.CustomIntSliderPropertyField(FleeSeconds, "Flee Seconds", "Controls the amount of time an AI will flee from a target for returning its non-combat state. This happens when the current target is outside of an AI's detection radius.", 1, 60, true);
|
||||
|
||||
CustomEditorProperties.CustomPropertyField(RequireObstruction, "Require Obstruction", "Only allow the flee time to increase if the AI's current target is obstructed. This allows the AI to continuously flee the target while they are visible, " +
|
||||
"but give up if the target has been obstructed (or not visible) for the duration of the Flee Seconds.", true);
|
||||
|
||||
EditorGUILayout.Space();
|
||||
CustomEditorProperties.CustomFloatSliderPropertyField(UpdateFleePositionSeconds, "Update Flee Position Seconds", "Controls how often the flee position will be updated.", 0.25f, 5f, true);
|
||||
|
||||
EditorGUILayout.Space();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays all options that are intended for the Aggressive Behavior Type.
|
||||
/// </summary>
|
||||
void AggressiveSettings (EmeraldBehaviors self)
|
||||
{
|
||||
if (self.CurrentBehaviorType != EmeraldBehaviors.BehaviorTypes.Aggressive)
|
||||
return;
|
||||
|
||||
CustomEditorProperties.TextTitleWithDescription("Aggressive Settings", "Aggressive AI will attack targets who they have an Enemy Relation Type with.", true);
|
||||
|
||||
CustomEditorProperties.CustomPropertyField(TargetToFollow, "Target to Follow", "Assigning a Target to Follow will turn an AI into a Companion AI. They will also ignore their Wander Type and follow their the specified instead. Note: AI who have currently have a Target to Follow cannot use any of the settings below.", true);
|
||||
|
||||
if (self.TargetToFollow)
|
||||
{
|
||||
CustomEditorProperties.CustomFloatSlider(new Rect(), new GUIContent(), FollowingStoppingDistance, "Following Stopping Distance", 1, 15);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls the distance in which an AI will stop from their Target to Follow.", true);
|
||||
}
|
||||
|
||||
EditorGUI.BeginDisabledGroup(self.TargetToFollow);
|
||||
CustomEditorProperties.CustomIntSliderPropertyField(CautiousSeconds, "Cautious Seconds", "Controls the amount of time an AI will remain in the Cautious State before attacking their target. " +
|
||||
"If an AI has a warning animation, this will automatically be played while in this state. If this value is set to 0, the cautious state will be ignored.", 0, 15, false);
|
||||
|
||||
if (self.CautiousSeconds > 0)
|
||||
{
|
||||
CustomEditorProperties.DisplayImportantMessage("Be aware that AI who are in a cautious state will not attack a detected target until after the duration of their Cautious Seconds (unless they're attacked).");
|
||||
}
|
||||
|
||||
EditorGUILayout.Space();
|
||||
CustomEditorProperties.CustomPropertyField(InfititeChase, "Infitite Chase", "Controls whether or not the AI will chase their target without any distance or time resrtictions. Note: This will disable the Chase Seconds and Stay Near Starting Area settings.", true);
|
||||
|
||||
EditorGUI.BeginDisabledGroup(self.InfititeChase);
|
||||
CustomEditorProperties.CustomIntSliderPropertyField(ChaseSeconds, "Chase Seconds", "Controls the amount of time an AI will chase a target for before giving up and exiting its combat state. This happens when the current target is outside of an AI's detection radius.", 1, 60, true);
|
||||
|
||||
CustomEditorProperties.CustomPropertyField(RequireObstruction, "Require Obstruction", "Only allow the chase time to increase if the AI's current target is obstructed. This allows the AI to continuously chase the target while they are visible, " +
|
||||
"but give up if the target has been obstructed (or not visible) for the duration of the Chase Seconds.", true);
|
||||
|
||||
CustomEditorProperties.CustomPropertyField(StayNearStartingArea, "Stay Near Starting Area", "Controls whether or not an AI will give up on a target if it gets too far away from its starting area.", true);
|
||||
|
||||
if (self.StayNearStartingArea == YesOrNo.Yes)
|
||||
{
|
||||
CustomEditorProperties.BeginIndent();
|
||||
CustomEditorProperties.CustomIntSliderPropertyField(MaxDistanceFromStartingArea, "Max Distance From Starting Area", "Controls the maximum distance an AI is allowed to be from its starting area before giving up on a target and return to its starting position or area.", 10, 100, true);
|
||||
CustomEditorProperties.EndIndent();
|
||||
}
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
CustomEditorProperties.CustomPropertyField(FleeOnLowHealth, "Flee on Low Health", "Controls whether or not an AI will flee upon low health while in combat.", true);
|
||||
|
||||
if (self.FleeOnLowHealth == YesOrNo.Yes)
|
||||
{
|
||||
CustomEditorProperties.BeginIndent();
|
||||
CustomEditorProperties.CustomIntSliderPropertyField(PercentToFlee, "Percent to Flee", "Controls the percentage of low health needed to flee.", 1, 99, true);
|
||||
CustomEditorProperties.CustomFloatSliderPropertyField(UpdateFleePositionSeconds, "Update Flee Position Seconds", "Controls how often the flee position will be updated.", 0.25f, 5f, true);
|
||||
CustomEditorProperties.EndIndent();
|
||||
}
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
EditorGUILayout.Space();
|
||||
}
|
||||
|
||||
void OnSceneGUI()
|
||||
{
|
||||
EmeraldBehaviors self = (EmeraldBehaviors)target;
|
||||
DrawStartingAreaDistance(self);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the wander area, when using the Dynamic Wander Type.
|
||||
/// </summary>
|
||||
void DrawStartingAreaDistance(EmeraldBehaviors self)
|
||||
{
|
||||
if (self.StayNearStartingArea == YesOrNo.Yes && BehaviorSettingsFoldout.boolValue && !HideSettingsFoldout.boolValue)
|
||||
{
|
||||
Handles.color = new Color(0, 0.6f, 0, 1f);
|
||||
Handles.DrawWireDisc(self.transform.position, Vector3.up, (float)self.MaxDistanceFromStartingArea, 3f);
|
||||
Handles.color = Color.white;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays all custom variables in a separate part of the editor.
|
||||
/// </summary>
|
||||
void CustomSettings(EmeraldBehaviors self)
|
||||
{
|
||||
CustomSettingsFoldout.boolValue = EditorGUILayout.Foldout(CustomSettingsFoldout.boolValue, "Custom Settings", true, FoldoutStyle);
|
||||
|
||||
if (CustomSettingsFoldout.boolValue)
|
||||
{
|
||||
CustomEditorProperties.BeginFoldoutWindowBox();
|
||||
CustomEditorProperties.TextTitleWithDescription("Custom Settings", "Any variables added through a child class of EmeraldBehavior will be added here.", true);
|
||||
|
||||
foreach (FieldInfo field in CustomFields)
|
||||
{
|
||||
//Offset Arrays with extra space
|
||||
if (field.FieldType.GetElementType() != null)
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Space(15);
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty(field.Name));
|
||||
GUILayout.Space(1);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
//Offset Lists with extra space
|
||||
else if (field.FieldType.IsGenericType && field.FieldType.GetGenericTypeDefinition() == typeof(List<>))
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Space(15);
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty(field.Name));
|
||||
GUILayout.Space(1);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
else if (field.FieldType.IsClass && field.FieldType.ToString() != "System.String" && !field.FieldType.ToString().Contains("Unity"))
|
||||
{
|
||||
Debug.Log(field.FieldType);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
GUILayout.Space(15);
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty(field.Name));
|
||||
GUILayout.Space(1);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
//Don't apply an offset to single variables
|
||||
else
|
||||
{
|
||||
if (serializedObject.FindProperty(field.Name) != null)
|
||||
{
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty(field.Name));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
EditorGUILayout.Space();
|
||||
|
||||
CustomEditorProperties.EndFoldoutWindowBox();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 75ff12b1671dfcd4b9d0726e8b36e052
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,559 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEditorInternal;
|
||||
using System.Linq;
|
||||
|
||||
namespace EmeraldAI.Utility
|
||||
{
|
||||
[CustomEditor(typeof(EmeraldCombat))]
|
||||
[CanEditMultipleObjects]
|
||||
public class EmeraldCombatEditor : Editor
|
||||
{
|
||||
GUIStyle FoldoutStyle;
|
||||
|
||||
float CurrentAttackDistance = 0;
|
||||
float CurrentTooCloseDistance = 0;
|
||||
bool DrawDistanceActive = false;
|
||||
Texture CombatEditorIcon;
|
||||
|
||||
EmeraldAnimation EmeraldAnimation;
|
||||
EmeraldBehaviors EmeraldBehaviors;
|
||||
|
||||
SerializedProperty HideSettingsFoldout, DamageSettingsFoldout, WeaponType1SettingsFoldout, WeaponType2SettingsFoldout, SwitchWeaponSettingsFoldout, CombatActionSettingsFoldout;
|
||||
|
||||
//Enums
|
||||
SerializedProperty Type1PickTargetTypeProp, Type2PickTargetTypeProp, SwitchWeaponTypeProp, StartingWeaponTypeProp, Type1AttackPickTypeProp, Type2AttackPickTypeProp;
|
||||
|
||||
//Int
|
||||
SerializedProperty SwitchWeaponTimeMinProp, SwitchWeaponTimeMaxProp, SwitchWeaponTypesDistanceProp, SwitchWeaponTypesCooldownProp, MinResumeWanderProp, MaxResumeWanderProp;
|
||||
|
||||
//Float
|
||||
SerializedProperty Type1AttackCooldownProp, Type2AttackCooldownProp;
|
||||
|
||||
ReorderableList Type1Attacks, Type2Attacks, WeaponType1AttackTransforms, WeaponType2AttackTransforms, Type1ActionsList, Type2ActionsList;
|
||||
|
||||
string AttackTransformTooltip = "Each Attack Transform can be used individually during an CreateAbility by passing the Attack Transform's " +
|
||||
"name through the String parameter of the Animation Event. This allows an AI to have customizable points that attacks or abilities can come from, such as a grenade from a hand, a bullet from a barrel, or a spell from an AI's staff." +
|
||||
"\n\nNote: It is best to keep Attack Transform names consistent to allow them to work across multiple AI.";
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
EmeraldCombat self = (EmeraldCombat)target;
|
||||
EmeraldAnimation = self.GetComponent<EmeraldAnimation>();
|
||||
EmeraldBehaviors = self.GetComponent<EmeraldBehaviors>();
|
||||
if (CombatEditorIcon == null) CombatEditorIcon = Resources.Load("Editor Icons/EmeraldCombat") as Texture;
|
||||
InitializeProperties();
|
||||
InitializeLists(self);
|
||||
}
|
||||
|
||||
void InitializeProperties()
|
||||
{
|
||||
//Bool
|
||||
HideSettingsFoldout = serializedObject.FindProperty("HideSettingsFoldout");
|
||||
DamageSettingsFoldout = serializedObject.FindProperty("DamageSettingsFoldout");
|
||||
CombatActionSettingsFoldout = serializedObject.FindProperty("CombatActionSettingsFoldout");
|
||||
SwitchWeaponSettingsFoldout = serializedObject.FindProperty("SwitchWeaponSettingsFoldout");
|
||||
WeaponType1SettingsFoldout = serializedObject.FindProperty("WeaponType1SettingsFoldout");
|
||||
WeaponType2SettingsFoldout = serializedObject.FindProperty("WeaponType2SettingsFoldout");
|
||||
|
||||
//Enums
|
||||
SwitchWeaponTypeProp = serializedObject.FindProperty("SwitchWeaponType");
|
||||
StartingWeaponTypeProp = serializedObject.FindProperty("StartingWeaponType");
|
||||
Type1AttackPickTypeProp = serializedObject.FindProperty("Type1Attacks.AttackPickType");
|
||||
Type2AttackPickTypeProp = serializedObject.FindProperty("Type2Attacks.AttackPickType");
|
||||
Type1PickTargetTypeProp = serializedObject.FindProperty("Type1PickTargetType");
|
||||
Type2PickTargetTypeProp = serializedObject.FindProperty("Type2PickTargetType");
|
||||
|
||||
//Int
|
||||
SwitchWeaponTimeMinProp = serializedObject.FindProperty("SwitchWeaponTimeMin");
|
||||
SwitchWeaponTimeMaxProp = serializedObject.FindProperty("SwitchWeaponTimeMax");
|
||||
SwitchWeaponTypesDistanceProp = serializedObject.FindProperty("SwitchWeaponTypesDistance");
|
||||
SwitchWeaponTypesCooldownProp = serializedObject.FindProperty("SwitchWeaponTypesCooldown");
|
||||
MinResumeWanderProp = serializedObject.FindProperty("MinResumeWander");
|
||||
MaxResumeWanderProp = serializedObject.FindProperty("MaxResumeWander");
|
||||
|
||||
//Float
|
||||
Type1AttackCooldownProp = serializedObject.FindProperty("Type1AttackCooldown");
|
||||
Type2AttackCooldownProp = serializedObject.FindProperty("Type2AttackCooldown");
|
||||
}
|
||||
|
||||
void InitializeLists (EmeraldCombat self)
|
||||
{
|
||||
//Type 1 AttackTransforms
|
||||
WeaponType1AttackTransforms = new ReorderableList(serializedObject, serializedObject.FindProperty("WeaponType1AttackTransforms"), true, true, true, true);
|
||||
WeaponType1AttackTransforms.drawHeaderCallback = rect =>
|
||||
{
|
||||
EditorGUI.LabelField(rect, "Weapon Type 1 Attack Transforms List", EditorStyles.boldLabel);
|
||||
};
|
||||
WeaponType1AttackTransforms.drawElementCallback =
|
||||
(Rect rect, int index, bool isActive, bool isFocused) =>
|
||||
{
|
||||
|
||||
var element = WeaponType1AttackTransforms.serializedProperty.GetArrayElementAtIndex(index);
|
||||
EditorGUI.PropertyField(new Rect(rect.x, rect.y + 3f, rect.width, EditorGUIUtility.singleLineHeight), element, new GUIContent("Attack Transform " + (index + 1), AttackTransformTooltip));
|
||||
};
|
||||
|
||||
//Type 2 AttackTransforms
|
||||
WeaponType2AttackTransforms = new ReorderableList(serializedObject, serializedObject.FindProperty("WeaponType2AttackTransforms"), true, true, true, true);
|
||||
WeaponType2AttackTransforms.drawHeaderCallback = rect =>
|
||||
{
|
||||
EditorGUI.LabelField(rect, "Weapon Type 2 Attack Transforms List", EditorStyles.boldLabel);
|
||||
};
|
||||
WeaponType2AttackTransforms.drawElementCallback =
|
||||
(Rect rect, int index, bool isActive, bool isFocused) =>
|
||||
{
|
||||
|
||||
var element = WeaponType2AttackTransforms.serializedProperty.GetArrayElementAtIndex(index);
|
||||
EditorGUI.PropertyField(new Rect(rect.x, rect.y + 3f, rect.width, EditorGUIUtility.singleLineHeight), element, new GUIContent("Attack Transform " + (index + 1), AttackTransformTooltip));
|
||||
};
|
||||
|
||||
//TODO: Make into function so it can be used with type 1 and type 2
|
||||
//Type 1 Attacks
|
||||
Type1Attacks = new ReorderableList(serializedObject, serializedObject.FindProperty("Type1Attacks").FindPropertyRelative("AttackDataList"), true, true, true, true);
|
||||
|
||||
Type1Attacks.drawHeaderCallback = rect => {
|
||||
EditorGUI.LabelField(rect, "Type 1 Attacks", EditorStyles.boldLabel);
|
||||
};
|
||||
|
||||
Type1Attacks.drawElementCallback =
|
||||
(Rect rect, int index, bool isActive, bool isFocused) => {
|
||||
var element = Type1Attacks.serializedProperty.GetArrayElementAtIndex(index);
|
||||
Type1Attacks.elementHeight = EditorGUIUtility.singleLineHeight * 7.5f;
|
||||
|
||||
if (self.Type1Attacks.AttackDataList.Count > 0 && EmeraldAnimation.Type1AttackEnumAnimations != null)
|
||||
{
|
||||
CustomEditorProperties.CustomListPopup(new Rect(rect.x + 125, rect.y + 10, rect.width - 125, EditorGUIUtility.singleLineHeight), new GUIContent(), element.FindPropertyRelative("AttackAnimation"), "Attack Animation", EmeraldAnimation.Type1AttackEnumAnimations);
|
||||
EditorGUI.PrefixLabel(new Rect(rect.x, rect.y + 10, 125, EditorGUIUtility.singleLineHeight), new GUIContent("Attack Animation", "The animation that will be used for this attack.\n\nNote: Animations are based off of your AI's Attack Animation List within its Animation Profile."));
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUI.Popup(new Rect(rect.x + 125, rect.y + 10, rect.width - 125, EditorGUIUtility.singleLineHeight), 0, EmeraldAnimation.Type1AttackBlankOptions);
|
||||
EditorGUI.PrefixLabel(new Rect(rect.x, rect.y + 10, 125, EditorGUIUtility.singleLineHeight), new GUIContent("Attack Animation"));
|
||||
}
|
||||
|
||||
if (isActive)
|
||||
{
|
||||
CurrentAttackDistance = element.FindPropertyRelative("AttackDistance").floatValue;
|
||||
CurrentTooCloseDistance = element.FindPropertyRelative("TooCloseDistance").floatValue;
|
||||
DrawDistanceActive = true;
|
||||
}
|
||||
|
||||
if (element.FindPropertyRelative("AttackDistance").floatValue == 0) element.FindPropertyRelative("AttackDistance").floatValue = 2;
|
||||
if (element.FindPropertyRelative("TooCloseDistance").floatValue == 0) element.FindPropertyRelative("TooCloseDistance").floatValue = 0.5f;
|
||||
|
||||
EditorGUI.ObjectField(new Rect(rect.x, rect.y + 35, rect.width, EditorGUIUtility.singleLineHeight), element.FindPropertyRelative("AbilityObject"), new GUIContent("Ability Object", "The Ability Object that will be used for this attack."));
|
||||
CustomEditorProperties.CustomListFloatSlider(new Rect(rect.x, rect.y + 60, rect.width, EditorGUIUtility.singleLineHeight), new GUIContent("Attack Distance", "Controls the distance that this attack can happen within."), element.FindPropertyRelative("AttackDistance"), 0.5f, 75f);
|
||||
CustomEditorProperties.CustomListFloatSlider(new Rect(rect.x, rect.y + 85, rect.width, EditorGUIUtility.singleLineHeight), new GUIContent("Too Close Distance", "Controls the distance for when an AI will backup. This is useful for AI keeping their distance from attackers."), element.FindPropertyRelative("TooCloseDistance"), 0f, 35f);
|
||||
EditorGUI.BeginDisabledGroup(self.Type1Attacks.AttackPickType != AttackPickTypes.Odds);
|
||||
CustomEditorProperties.CustomListIntSlider(new Rect(rect.x, rect.y + 110, rect.width, EditorGUIUtility.singleLineHeight), new GUIContent("Attack Odds", "The odds that this attack will be used (when using the Odds Pick type)."), element.FindPropertyRelative("AttackOdds"), 1, 100);
|
||||
EditorGUI.EndDisabledGroup();
|
||||
};
|
||||
|
||||
Type1Attacks.drawHeaderCallback = rect =>
|
||||
{
|
||||
EditorGUI.LabelField(rect, "Type 1 Attacks", EditorStyles.boldLabel);
|
||||
};
|
||||
|
||||
|
||||
|
||||
//Type 2 Attacks
|
||||
Type2Attacks = new ReorderableList(serializedObject, serializedObject.FindProperty("Type2Attacks").FindPropertyRelative("AttackDataList"), true, true, true, true);
|
||||
|
||||
Type2Attacks.drawHeaderCallback = rect => {
|
||||
EditorGUI.LabelField(rect, "Type 2 Attacks", EditorStyles.boldLabel);
|
||||
};
|
||||
|
||||
Type2Attacks.drawElementCallback =
|
||||
(Rect rect, int index, bool isActive, bool isFocused) => {
|
||||
var element = Type2Attacks.serializedProperty.GetArrayElementAtIndex(index);
|
||||
Type2Attacks.elementHeight = EditorGUIUtility.singleLineHeight * 7.5f;
|
||||
|
||||
if (self.Type2Attacks.AttackDataList.Count > 0 && EmeraldAnimation.Type2AttackEnumAnimations != null)
|
||||
{
|
||||
CustomEditorProperties.CustomListPopup(new Rect(rect.x + 125, rect.y + 10, rect.width - 125, EditorGUIUtility.singleLineHeight), new GUIContent(), element.FindPropertyRelative("AttackAnimation"), "Attack Animation", EmeraldAnimation.Type2AttackEnumAnimations);
|
||||
EditorGUI.PrefixLabel(new Rect(rect.x, rect.y + 10, 125, EditorGUIUtility.singleLineHeight), new GUIContent("Attack Animation", "The animation that will be used for this attack.\n\nNote: Animations are based off of your AI's Attack Animation List within its Animation Profile."));
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUI.Popup(new Rect(rect.x + 125, rect.y + 10, rect.width - 125, EditorGUIUtility.singleLineHeight), 0, EmeraldAnimation.Type1AttackBlankOptions);
|
||||
EditorGUI.PrefixLabel(new Rect(rect.x, rect.y + 10, 125, EditorGUIUtility.singleLineHeight), new GUIContent("Attack Animation"));
|
||||
}
|
||||
|
||||
if (isActive)
|
||||
{
|
||||
CurrentAttackDistance = element.FindPropertyRelative("AttackDistance").floatValue;
|
||||
CurrentTooCloseDistance = element.FindPropertyRelative("TooCloseDistance").floatValue;
|
||||
DrawDistanceActive = true;
|
||||
}
|
||||
|
||||
EditorGUI.ObjectField(new Rect(rect.x, rect.y + 35, rect.width, EditorGUIUtility.singleLineHeight), element.FindPropertyRelative("AbilityObject"), new GUIContent("Ability Object", "The Ability Object that will be used for this attack."));
|
||||
CustomEditorProperties.CustomListFloatSlider(new Rect(rect.x, rect.y + 60, rect.width, EditorGUIUtility.singleLineHeight), new GUIContent("Attack Distance", "Controls the distance that this attack can happen within"), element.FindPropertyRelative("AttackDistance"), 0.5f, 75f);
|
||||
CustomEditorProperties.CustomListFloatSlider(new Rect(rect.x, rect.y + 85, rect.width, EditorGUIUtility.singleLineHeight), new GUIContent("Too Close Distance", "Controls the distance for when an AI will backup. This is useful for AI keeping their distance from attackers."), element.FindPropertyRelative("TooCloseDistance"), 0f, 35f);
|
||||
EditorGUI.BeginDisabledGroup(self.Type2Attacks.AttackPickType != AttackPickTypes.Odds);
|
||||
CustomEditorProperties.CustomListIntSlider(new Rect(rect.x, rect.y + 110, rect.width, EditorGUIUtility.singleLineHeight), new GUIContent("Attack Odds", "The odds that this attack will be used (when using the Odds Pick type)."), element.FindPropertyRelative("AttackOdds"), 1, 100);
|
||||
EditorGUI.EndDisabledGroup();
|
||||
};
|
||||
|
||||
Type2Attacks.drawHeaderCallback = rect =>
|
||||
{
|
||||
EditorGUI.LabelField(rect, "Type 2 Attacks", EditorStyles.boldLabel);
|
||||
};
|
||||
|
||||
//Type 1 Action List
|
||||
Type1ActionsList = new ReorderableList(serializedObject, serializedObject.FindProperty("Type1CombatActions"), true, true, true, true);
|
||||
Type1ActionsList.drawElementCallback =
|
||||
(Rect rect, int index, bool isActive, bool isFocused) =>
|
||||
{
|
||||
var element = Type1ActionsList.serializedProperty.GetArrayElementAtIndex(index);
|
||||
Type1ActionsList.elementHeight = EditorGUIUtility.singleLineHeight * 1.35f;
|
||||
|
||||
EditorGUI.PropertyField(new Rect(rect.x + 5f, rect.y + 2, rect.width, EditorGUIUtility.singleLineHeight), element.FindPropertyRelative("Enabled"), GUIContent.none); //Toggle
|
||||
EditorGUI.LabelField(new Rect(rect.x + 25f, rect.y + 2, rect.width, EditorGUIUtility.singleLineHeight), new GUIContent("Enabled", "Controls whether or not this action is enabled. If disabled, it will be ignored.")); //Toggle
|
||||
EditorGUI.BeginDisabledGroup(!element.FindPropertyRelative("Enabled").boolValue);
|
||||
EditorGUI.PropertyField(new Rect(rect.x + 100, rect.y + 4, rect.width - 100, EditorGUIUtility.singleLineHeight), element.FindPropertyRelative("emeraldAction"), GUIContent.none); //Action Object
|
||||
EditorGUI.EndDisabledGroup();
|
||||
};
|
||||
|
||||
Type1ActionsList.drawHeaderCallback = rect =>
|
||||
{
|
||||
EditorGUI.LabelField(rect, "Type 1 Combat Actions List", EditorStyles.boldLabel);
|
||||
};
|
||||
|
||||
//Type 2 Action List
|
||||
Type2ActionsList = new ReorderableList(serializedObject, serializedObject.FindProperty("Type2CombatActions"), true, true, true, true);
|
||||
Type2ActionsList.drawElementCallback =
|
||||
(Rect rect, int index, bool isActive, bool isFocused) =>
|
||||
{
|
||||
var element = Type2ActionsList.serializedProperty.GetArrayElementAtIndex(index);
|
||||
Type2ActionsList.elementHeight = EditorGUIUtility.singleLineHeight * 1.35f;
|
||||
|
||||
EditorGUI.PropertyField(new Rect(rect.x + 5f, rect.y + 2, rect.width, EditorGUIUtility.singleLineHeight), element.FindPropertyRelative("Enabled"), GUIContent.none); //Toggle
|
||||
EditorGUI.LabelField(new Rect(rect.x + 25f, rect.y + 2, rect.width, EditorGUIUtility.singleLineHeight), new GUIContent("Enabled", "Controls whether or not this action is enabled. If disabled, it will be ignored.")); //Toggle
|
||||
EditorGUI.BeginDisabledGroup(!element.FindPropertyRelative("Enabled").boolValue);
|
||||
EditorGUI.PropertyField(new Rect(rect.x + 100, rect.y + 4, rect.width - 100, EditorGUIUtility.singleLineHeight), element.FindPropertyRelative("emeraldAction"), GUIContent.none); //Action Object
|
||||
EditorGUI.EndDisabledGroup();
|
||||
};
|
||||
|
||||
Type2ActionsList.drawHeaderCallback = rect =>
|
||||
{
|
||||
EditorGUI.LabelField(rect, "Type 2 Combat Actions List", EditorStyles.boldLabel);
|
||||
};
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
FoldoutStyle = CustomEditorProperties.UpdateEditorStyles();
|
||||
EmeraldCombat self = (EmeraldCombat)target;
|
||||
serializedObject.Update();
|
||||
|
||||
CustomEditorProperties.BeginScriptHeaderNew("Combat", CombatEditorIcon, new GUIContent(), HideSettingsFoldout);
|
||||
|
||||
if (EmeraldBehaviors.CurrentBehaviorType != EmeraldBehaviors.BehaviorTypes.Aggressive)
|
||||
{
|
||||
CustomEditorProperties.DisplayImportantHeaderMessage("Only AI with an Aggressive behavior type use the Combat Component.");
|
||||
}
|
||||
|
||||
EditorGUI.BeginDisabledGroup(EmeraldBehaviors.CurrentBehaviorType != EmeraldBehaviors.BehaviorTypes.Aggressive);
|
||||
DisplayWarningMessage(self);
|
||||
|
||||
if (!HideSettingsFoldout.boolValue)
|
||||
{
|
||||
EditorGUILayout.Space();
|
||||
DamageSettings(self);
|
||||
EditorGUILayout.Space();
|
||||
CombatActionSettings(self);
|
||||
EditorGUILayout.Space();
|
||||
if (self.WeaponTypeAmount == EmeraldCombat.WeaponTypeAmounts.Two)
|
||||
{
|
||||
SwitchWeaponSettings(self);
|
||||
EditorGUILayout.Space();
|
||||
}
|
||||
|
||||
WeaponType1Settings(self);
|
||||
EditorGUILayout.Space();
|
||||
|
||||
if (self.WeaponTypeAmount == EmeraldCombat.WeaponTypeAmounts.Two)
|
||||
{
|
||||
WeaponType2Settings(self);
|
||||
EditorGUILayout.Space();
|
||||
}
|
||||
}
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
CustomEditorProperties.EndScriptHeader();
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
void DamageSettings(EmeraldCombat self)
|
||||
{
|
||||
DamageSettingsFoldout.boolValue = EditorGUILayout.Foldout(DamageSettingsFoldout.boolValue, "Combat Settings", true, FoldoutStyle);
|
||||
|
||||
if (DamageSettingsFoldout.boolValue)
|
||||
{
|
||||
CustomEditorProperties.BeginFoldoutWindowBox();
|
||||
CustomEditorProperties.TextTitleWithDescription("Combat Settings", "Controls various combat related settings.", true);
|
||||
|
||||
self.WeaponTypeAmount = (EmeraldCombat.WeaponTypeAmounts)EditorGUILayout.EnumPopup("Weapon Type Amount", self.WeaponTypeAmount);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls whether this AI uses 1 or 2 Weapon Types. This allows an AI to use different animations and abilities for its different weapon types, " +
|
||||
"such as sword attacks for type 1 and shooting spells for type 2. By default, this is set to 1. Combat Actions are used for both weapon types, if desired.", true);
|
||||
|
||||
EditorGUILayout.Space();
|
||||
CustomEditorProperties.CustomIntSlider(new Rect(), new GUIContent(), MinResumeWanderProp, "Min Resume Wandering", 0, 6);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls the minimum amount of time an AI will wait before they resume wandering (according to its Wandering Type) after being " +
|
||||
"in combat. This amount will be randomized with the Maximum Resume Wandering Delay.", true);
|
||||
|
||||
CustomEditorProperties.CustomIntSlider(new Rect(), new GUIContent(), MaxResumeWanderProp, "Max Resume Wandering", 0, 6);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls the maximum amount of time an AI will wait before they resume wandering (according to its Wandering Type) after being " +
|
||||
"in combat. This amount will be randomized with the Minimum Resume Wandering Delay.", true);
|
||||
|
||||
CustomEditorProperties.EndFoldoutWindowBox();
|
||||
}
|
||||
}
|
||||
|
||||
void DisplayWarningMessage (EmeraldCombat self)
|
||||
{
|
||||
if (EmeraldBehaviors.CurrentBehaviorType != EmeraldBehaviors.BehaviorTypes.Aggressive) return;
|
||||
|
||||
if (self.WeaponTypeAmount == EmeraldCombat.WeaponTypeAmounts.Two && self.Type2Attacks.AttackDataList.Count == 0)
|
||||
{
|
||||
CustomEditorProperties.DisplaySetupWarning("There currently aren't any Type 2 attacks applied to this AI. Please ensure there is at least 1 attack applied to the Type 2 Attacks List. This " +
|
||||
"can be found within the Weapon Type 2 Settings foldout");
|
||||
}
|
||||
else if (self.Type1Attacks.AttackDataList.Count == 0)
|
||||
{
|
||||
CustomEditorProperties.DisplaySetupWarning("There currently aren't any Type 1 attacks applied to this AI. Please ensure there is at least 1 attack applied to the Type 1 Attacks List. This " +
|
||||
"can be found within the Weapon Type 1 Settings foldout.");
|
||||
}
|
||||
}
|
||||
|
||||
void CombatActionSettings(EmeraldCombat self)
|
||||
{
|
||||
CombatActionSettingsFoldout.boolValue = EditorGUILayout.Foldout(CombatActionSettingsFoldout.boolValue, "Combat Actions Settings", true, FoldoutStyle);
|
||||
|
||||
if (CombatActionSettingsFoldout.boolValue)
|
||||
{
|
||||
CustomEditorProperties.BeginFoldoutWindowBox();
|
||||
CustomEditorProperties.TextTitleWithDescription("Combat Actions Settings", "Controls the combat actions an AI will use while in combat. Combat Actions that are disabled will be ignored. Only Aggressive AI can use Combat Actions.", true);
|
||||
|
||||
EditorGUILayout.Space();
|
||||
CustomEditorProperties.CustomHelpLabelField("A list of mudular combat actions an AI can use while actively fighting in combat using its Type 1 Weapon Type.", false);
|
||||
GUILayout.Box(new GUIContent("What's This?", "A list of mudular combat actions an AI can use while actively fighting in combat using its Type 1 Weapon Type."), EditorStyles.toolbarButton, GUILayout.ExpandWidth(false));
|
||||
Type1ActionsList.DoLayoutList();
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.Space();
|
||||
|
||||
if (self.WeaponTypeAmount == EmeraldCombat.WeaponTypeAmounts.Two)
|
||||
{
|
||||
EditorGUILayout.Space();
|
||||
CustomEditorProperties.CustomHelpLabelField("A list of mudular combat actions an AI can use while actively fighting in combat using its Type 2 Weapon Type.", false);
|
||||
GUILayout.Box(new GUIContent("What's This?", "A list of mudular combat actions an AI can use while actively fighting in combat using its Type 2 Weapon Type."), EditorStyles.toolbarButton, GUILayout.ExpandWidth(false));
|
||||
Type2ActionsList.DoLayoutList();
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.Space();
|
||||
}
|
||||
|
||||
CustomEditorProperties.EndFoldoutWindowBox();
|
||||
}
|
||||
}
|
||||
|
||||
void SwitchWeaponSettings (EmeraldCombat self)
|
||||
{
|
||||
if (self.WeaponTypeAmount == EmeraldCombat.WeaponTypeAmounts.Two)
|
||||
{
|
||||
SwitchWeaponSettingsFoldout.boolValue = EditorGUILayout.Foldout(SwitchWeaponSettingsFoldout.boolValue, "Switch Weapon Settings", true, FoldoutStyle);
|
||||
|
||||
if (SwitchWeaponSettingsFoldout.boolValue)
|
||||
{
|
||||
CustomEditorProperties.BeginFoldoutWindowBox();
|
||||
CustomEditorProperties.TextTitleWithDescription("Switch Weapon Settings", "Switch Weapon Settings.", true);
|
||||
|
||||
EditorGUILayout.PropertyField(StartingWeaponTypeProp, new GUIContent("Starting Weapon Type"));
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls which weapon type the AI will start with, or transition to first, upon entering comabt.", true);
|
||||
|
||||
if (self.SwitchWeaponType == EmeraldCombat.SwitchWeaponTypes.Distance)
|
||||
{
|
||||
CustomEditorProperties.DisplayImportantMessage("Important - When using the Distance Switch Type, your Starting Weapon Type needs to be the Weapon Type used for ranged combat. This setting is intended to be used with an AI that has close-range and range weapons.");
|
||||
GUILayout.Space(10);
|
||||
}
|
||||
|
||||
EditorGUILayout.PropertyField(SwitchWeaponTypeProp, new GUIContent("Switch Type"));
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls how the AI will switch its weapon type between Type 2 and Type 1. If none is used, the AI will always stay on the Starting Weapon Type.", true);
|
||||
|
||||
if (self.SwitchWeaponType == EmeraldCombat.SwitchWeaponTypes.Timed)
|
||||
{
|
||||
CustomEditorProperties.CustomIntSlider(new Rect(), new GUIContent(), SwitchWeaponTimeMinProp, "Switch Time Min", 5, 45);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls the minimum amount of time it takes for this AI to switch its weapon.", false);
|
||||
|
||||
CustomEditorProperties.CustomIntSlider(new Rect(), new GUIContent(), SwitchWeaponTimeMaxProp, "Switch Time Min", 10, 90);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls the minimum amount of time it takes for this AI to switch its weapon.", true);
|
||||
}
|
||||
else if (self.SwitchWeaponType == EmeraldCombat.SwitchWeaponTypes.Distance)
|
||||
{
|
||||
CustomEditorProperties.CustomIntSlider(new Rect(), new GUIContent(), SwitchWeaponTypesDistanceProp, "Switch Weapon Type Distance", 2, 15);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls the distance in which the AI will switch to between close-ranged and ranged damage. Any distance at or below this amount will be close-ranged" +
|
||||
" and any value greater will be ranged.", true);
|
||||
|
||||
CustomEditorProperties.CustomIntSlider(new Rect(), new GUIContent(), SwitchWeaponTypesCooldownProp, "Switch Weapon Type Cooldown", 1, 60);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls the cooldown in which an AI will switch between ranged and close-ranged combat, if the Switch Weapon Type Distance has been met. This" +
|
||||
" is to stop a weapon switch from happening too often.", true);
|
||||
}
|
||||
|
||||
CustomEditorProperties.EndFoldoutWindowBox();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WeaponType1Settings(EmeraldCombat self)
|
||||
{
|
||||
if (self.WeaponTypeAmount == EmeraldCombat.WeaponTypeAmounts.One || self.WeaponTypeAmount == EmeraldCombat.WeaponTypeAmounts.Two)
|
||||
{
|
||||
WeaponType1SettingsFoldout.boolValue = EditorGUILayout.Foldout(WeaponType1SettingsFoldout.boolValue, "Weapon Type 1 Settings", true, FoldoutStyle);
|
||||
|
||||
if (WeaponType1SettingsFoldout.boolValue)
|
||||
{
|
||||
CustomEditorProperties.BeginFoldoutWindowBox();
|
||||
CustomEditorProperties.TextTitleWithDescription("Weapon Type 1 Settings", "Weapon Type 1 Settings.", true);
|
||||
|
||||
PickTargetTypeSetting(Type1PickTargetTypeProp);
|
||||
|
||||
CustomEditorProperties.CustomFloatSliderPropertyField(Type1AttackCooldownProp, "Type 1 Attack Cooldown", "Controls the cooldown needed to trigger an attack. " +
|
||||
"Note: An attack can take longer than the specified cooldown if an AI is busy with another action.", 0.35f, 5, false);
|
||||
GUILayout.Space(12);
|
||||
|
||||
GUILayout.Box(new GUIContent("What's This?", AttackTransformTooltip), EditorStyles.toolbarButton, GUILayout.ExpandWidth(false));
|
||||
WeaponType1AttackTransforms.DoLayoutList();
|
||||
GUILayout.Space(12);
|
||||
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls how Type 1's Attacks are picked.", false);
|
||||
CustomEditorProperties.CustomPropertyField(Type1AttackPickTypeProp, "Attack Pick Type", "", false);
|
||||
|
||||
if (self.Type1Attacks.AttackPickType == AttackPickTypes.Odds)
|
||||
{
|
||||
CustomEditorProperties.CustomHelpLabelField("Odds - Type 1 Attacks are picked based off of each of the Type 1 Attack's odds.", true);
|
||||
}
|
||||
else if (self.Type1Attacks.AttackPickType == AttackPickTypes.Order)
|
||||
{
|
||||
CustomEditorProperties.CustomHelpLabelField("Order - Type 1 Attacks are picked based on the order of the AI's Type 1 Attacks list.", true);
|
||||
}
|
||||
else if (self.Type1Attacks.AttackPickType == AttackPickTypes.Random)
|
||||
{
|
||||
CustomEditorProperties.CustomHelpLabelField("Random - Type 1 Attacks are picked randomly from the AI's Type 1 Attacks list.", true);
|
||||
}
|
||||
GUILayout.Space(10);
|
||||
|
||||
if (EmeraldAnimation.m_AnimationProfile != null && EmeraldAnimation.m_AnimationProfile.Type1Animations.AttackList.Count == 0 || EmeraldAnimation.Type1AttackEnumAnimations == null)
|
||||
{
|
||||
CustomEditorProperties.BeginIndent(12);
|
||||
CustomEditorProperties.DisplaySetupWarning("Please add at least one Type 1 Attack animation to the Type 1 Attack Animation list (located within this AI's Animation Profile) to " +
|
||||
"choose the type of animations these attacks will use.");
|
||||
CustomEditorProperties.EndIndent();
|
||||
}
|
||||
|
||||
GUILayout.Box(new GUIContent("What's This?", "A list of an AI's attacks. You can hover the mouse over each setting to view its tooltip. " +
|
||||
"You can select each attack within the attack list (making it active) to see the attack distance drawn around the AI."), EditorStyles.toolbarButton, GUILayout.ExpandWidth(false));
|
||||
Type1Attacks.DoLayoutList();
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.Space();
|
||||
|
||||
CustomEditorProperties.EndFoldoutWindowBox();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WeaponType2Settings (EmeraldCombat self)
|
||||
{
|
||||
if (self.WeaponTypeAmount == EmeraldCombat.WeaponTypeAmounts.Two)
|
||||
{
|
||||
WeaponType2SettingsFoldout.boolValue = EditorGUILayout.Foldout(WeaponType2SettingsFoldout.boolValue, "Weapon Type 2 Settings", true, FoldoutStyle);
|
||||
|
||||
if (WeaponType2SettingsFoldout.boolValue)
|
||||
{
|
||||
EditorGUILayout.BeginVertical("Box");
|
||||
CustomEditorProperties.TextTitleWithDescription("Weapon Type 2 Settings", "Weapon Type 2 Settings.", true);
|
||||
|
||||
PickTargetTypeSetting(Type2PickTargetTypeProp);
|
||||
|
||||
CustomEditorProperties.CustomFloatSliderPropertyField(Type2AttackCooldownProp, "Type 2 Attack Cooldown", "Controls the cooldown needed to trigger an attack. " +
|
||||
"Note: An attack can take longer than the specified cooldown if an AI is busy with another action.", 0.35f, 5, false);
|
||||
GUILayout.Space(12);
|
||||
|
||||
GUILayout.Box(new GUIContent("What's This?", AttackTransformTooltip), EditorStyles.toolbarButton, GUILayout.ExpandWidth(false));
|
||||
WeaponType2AttackTransforms.DoLayoutList();
|
||||
GUILayout.Space(12);
|
||||
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls how Type 2's Attacks are picked.", false);
|
||||
CustomEditorProperties.CustomPropertyField(Type2AttackPickTypeProp, "Attack Pick Type", "", false);
|
||||
|
||||
if (self.Type2Attacks.AttackPickType == AttackPickTypes.Odds)
|
||||
{
|
||||
CustomEditorProperties.CustomHelpLabelField("Odds - Type 2 Attacks are picked based off of each of the Type 2 Attack's odds.", true);
|
||||
}
|
||||
else if (self.Type2Attacks.AttackPickType == AttackPickTypes.Order)
|
||||
{
|
||||
CustomEditorProperties.CustomHelpLabelField("Order - Type 2 Attacks are picked based on the order of the AI's Type 2 Attacks list.", true);
|
||||
}
|
||||
else if (self.Type2Attacks.AttackPickType == AttackPickTypes.Random)
|
||||
{
|
||||
CustomEditorProperties.CustomHelpLabelField("Random - Type 2 Attacks are picked randomly from the AI's Type 2 Attacks list.", true);
|
||||
}
|
||||
GUILayout.Space(10);
|
||||
|
||||
if (EmeraldAnimation.m_AnimationProfile != null && EmeraldAnimation.m_AnimationProfile.Type2Animations.AttackList.Count == 0 || EmeraldAnimation.Type2AttackEnumAnimations == null)
|
||||
{
|
||||
CustomEditorProperties.BeginIndent(12);
|
||||
CustomEditorProperties.DisplaySetupWarning("Please add at least one Type 2 Attack animation to the Type 2 Attack Animation list (located within this AI's Animation Profile) to " +
|
||||
"choose the type of animations these attacks will use.");
|
||||
CustomEditorProperties.EndIndent();
|
||||
}
|
||||
|
||||
GUILayout.Box(new GUIContent("What's This?", "A list of an AI's attacks. You can hover the mouse over each setting to view its tooltip. " +
|
||||
"You can select each attack within the attack list (making it active) to see the attack distance drawn around the AI."), EditorStyles.toolbarButton, GUILayout.ExpandWidth(false));
|
||||
Type2Attacks.DoLayoutList();
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.Space();
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PickTargetTypeSetting (SerializedProperty PickTargetTypeProp)
|
||||
{
|
||||
EditorGUILayout.PropertyField(PickTargetTypeProp);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls the method an AI uses to pick a target.", false);
|
||||
|
||||
if ((PickTargetTypes)PickTargetTypeProp.enumValueIndex == PickTargetTypes.Closest)
|
||||
{
|
||||
CustomEditorProperties.CustomHelpLabelField("Closest - Picks the tagret that is closest and currently visible to the AI.", true);
|
||||
}
|
||||
else if ((PickTargetTypes)PickTargetTypeProp.enumValueIndex == PickTargetTypes.FirstDetected)
|
||||
{
|
||||
CustomEditorProperties.CustomHelpLabelField("First Detected - Picks the tagret that was first detected and currently visible to the AI.", true);
|
||||
}
|
||||
else if ((PickTargetTypes)PickTargetTypeProp.enumValueIndex == PickTargetTypes.Random)
|
||||
{
|
||||
CustomEditorProperties.CustomHelpLabelField("Random - Picks a random target from all currently visible targets within an AI's detection radius.", true);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSceneGUI()
|
||||
{
|
||||
EmeraldCombat self = (EmeraldCombat)target;
|
||||
DrawCombatRadii(self);
|
||||
}
|
||||
|
||||
void DrawCombatRadii (EmeraldCombat self)
|
||||
{
|
||||
if (DrawDistanceActive)
|
||||
{
|
||||
Handles.color = new Color(255, 0, 0, 1.0f);
|
||||
Handles.DrawWireDisc(self.transform.position, Vector3.up, CurrentAttackDistance);
|
||||
Handles.color = new Color(1, 0.9f, 0, 1.0f);
|
||||
Handles.DrawWireDisc(self.transform.position, Vector3.up, CurrentTooCloseDistance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cd27278d10fb3f649a5b452943f2747e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,367 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: afd0b139d27162d45ac1c5d7b806afd4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,194 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using UnityEditorInternal;
|
||||
|
||||
namespace EmeraldAI.Utility
|
||||
{
|
||||
[CustomEditor(typeof(EmeraldHealth))]
|
||||
[CanEditMultipleObjects]
|
||||
public class EmeraldHealthEditor : Editor
|
||||
{
|
||||
GUIStyle FoldoutStyle;
|
||||
Texture HealthEditorIcon;
|
||||
|
||||
//Int
|
||||
SerializedProperty StartingHealthProp, HealRateProp;
|
||||
|
||||
//Enum
|
||||
SerializedProperty UseHitEffectProp;
|
||||
|
||||
//Bool
|
||||
SerializedProperty HideSettingsFoldout, HealthFoldout, HitEffectFoldout, ImmortalProp;
|
||||
|
||||
//Float
|
||||
SerializedProperty HitEffectTimeoutSecondsProp;
|
||||
|
||||
//Vector
|
||||
SerializedProperty HitEffectPosOffsetProp;
|
||||
|
||||
ReorderableList HitEffectsList;
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
if (HealthEditorIcon == null) HealthEditorIcon = Resources.Load("Editor Icons/EmeraldHealth") as Texture;
|
||||
InitializeProperties();
|
||||
InitializeList();
|
||||
}
|
||||
|
||||
void InitializeProperties()
|
||||
{
|
||||
//Ints
|
||||
StartingHealthProp = serializedObject.FindProperty("StartingHealth");
|
||||
HealRateProp = serializedObject.FindProperty("HealRate");
|
||||
|
||||
//Bool
|
||||
HideSettingsFoldout = serializedObject.FindProperty("HideSettingsFoldout");
|
||||
HealthFoldout = serializedObject.FindProperty("HealthFoldout");
|
||||
HitEffectFoldout = serializedObject.FindProperty("HitEffectFoldout");
|
||||
ImmortalProp = serializedObject.FindProperty("Immortal");
|
||||
|
||||
//Float
|
||||
HitEffectTimeoutSecondsProp = serializedObject.FindProperty("HitEffectTimeoutSeconds");
|
||||
|
||||
//Vector
|
||||
HitEffectPosOffsetProp = serializedObject.FindProperty("HitEffectPosOffset");
|
||||
|
||||
//Enum
|
||||
UseHitEffectProp = serializedObject.FindProperty("UseHitEffect");
|
||||
}
|
||||
|
||||
void InitializeList ()
|
||||
{
|
||||
//Hit Effects List
|
||||
HitEffectsList = new ReorderableList(serializedObject, serializedObject.FindProperty("HitEffectsList"), true, true, true, true);
|
||||
HitEffectsList.drawHeaderCallback = rect =>
|
||||
{
|
||||
EditorGUI.LabelField(rect, "Hit Effects List", EditorStyles.boldLabel);
|
||||
};
|
||||
HitEffectsList.drawElementCallback =
|
||||
(Rect rect, int index, bool isActive, bool isFocused) =>
|
||||
{
|
||||
var element = HitEffectsList.serializedProperty.GetArrayElementAtIndex(index);
|
||||
EditorGUI.ObjectField(new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight), element, GUIContent.none);
|
||||
};
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
FoldoutStyle = CustomEditorProperties.UpdateEditorStyles();
|
||||
EmeraldHealth self = (EmeraldHealth)target;
|
||||
serializedObject.Update();
|
||||
|
||||
CustomEditorProperties.BeginScriptHeaderNew("Health", HealthEditorIcon, new GUIContent(), HideSettingsFoldout);
|
||||
|
||||
if (!HideSettingsFoldout.boolValue)
|
||||
{
|
||||
EditorGUILayout.Space();
|
||||
HealthSettings(self);
|
||||
EditorGUILayout.Space();
|
||||
HitEffectSettings(self);
|
||||
EditorGUILayout.Space();
|
||||
}
|
||||
|
||||
CustomEditorProperties.EndScriptHeader();
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
void HealthSettings(EmeraldHealth self)
|
||||
{
|
||||
HealthFoldout.boolValue = EditorGUILayout.Foldout(HealthFoldout.boolValue, "Health Settings", true, FoldoutStyle);
|
||||
|
||||
if (HealthFoldout.boolValue)
|
||||
{
|
||||
CustomEditorProperties.BeginFoldoutWindowBox();
|
||||
CustomEditorProperties.TextTitleWithDescription("Health Settings", "Controls various health related settings.", true);
|
||||
|
||||
EditorGUILayout.PropertyField(ImmortalProp, new GUIContent("Immortal"));
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls whether or not an AI is immune to damage and is unkillable. If this is enabled, it will disable other settings.", true);
|
||||
|
||||
EditorGUI.BeginDisabledGroup(self.Immortal);
|
||||
CustomEditorProperties.CustomIntField(new Rect(), new GUIContent(), StartingHealthProp, "Starting Health");
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls how much starting health an AI will have.", true);
|
||||
|
||||
CustomEditorProperties.CustomPropertyField(HealRateProp, "Heal Rate", "Controls how much an AI will heal per second when not actively in combat, given their health is below its max.", true);
|
||||
EditorGUI.EndDisabledGroup();
|
||||
|
||||
DrawHealthBar(self);
|
||||
|
||||
CustomEditorProperties.EndFoldoutWindowBox();
|
||||
}
|
||||
}
|
||||
|
||||
void DrawHealthBar (EmeraldHealth self)
|
||||
{
|
||||
GUILayout.Space(45);
|
||||
GUIStyle LabelStyle = new GUIStyle();
|
||||
LabelStyle.alignment = TextAnchor.MiddleCenter;
|
||||
LabelStyle.padding.bottom = 4;
|
||||
LabelStyle.fontStyle = FontStyle.Bold;
|
||||
LabelStyle.normal.textColor = Color.white;
|
||||
|
||||
Rect r = EditorGUILayout.BeginVertical();
|
||||
GUI.backgroundColor = Color.white;
|
||||
float CurrentHealth = ((float)self.CurrentHealth / (float)self.StartingHealth);
|
||||
|
||||
EditorGUI.DrawRect(new Rect(r.x, r.position.y - 39f, ((r.width)), 32), new Color(0.05f, 0.05f, 0.05f, 0.5f)); //Health Bar BG Outline
|
||||
EditorGUI.DrawRect(new Rect(r.x + 4, r.position.y - 35f, ((r.width - 8)), 24), new Color(0.16f, 0.16f, 0.16f, 1f)); //Health Bar BG
|
||||
Color HealthBarColor = Color.Lerp(new Color(0.6f, 0.1f, 0.1f, 1f), new Color(0.15f, 0.42f, 0.15f, 1f), CurrentHealth);
|
||||
EditorGUI.DrawRect(new Rect(r.x + 4, r.position.y - 35f, ((r.width - 8) * CurrentHealth), 24), HealthBarColor); //Health Bar Main
|
||||
|
||||
if (self.CurrentHealth > 0)
|
||||
{
|
||||
EditorGUI.LabelField(new Rect(r.x, r.position.y - 35f, (r.width), 26), "Current Health: " + self.CurrentHealth + "/" + self.StartingHealth, LabelStyle);
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUI.LabelField(new Rect(r.x, r.position.y - 35f, (r.width), 26), "Current Health: " + self.CurrentHealth + "/" + self.StartingHealth + " (Dead)", LabelStyle);
|
||||
}
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
self.CurrentHealth = self.StartingHealth;
|
||||
}
|
||||
}
|
||||
|
||||
void HitEffectSettings(EmeraldHealth self)
|
||||
{
|
||||
HitEffectFoldout.boolValue = EditorGUILayout.Foldout(HitEffectFoldout.boolValue, "Hit Effect Settings", true, FoldoutStyle);
|
||||
|
||||
if (HitEffectFoldout.boolValue)
|
||||
{
|
||||
CustomEditorProperties.BeginFoldoutWindowBox();
|
||||
CustomEditorProperties.TextTitleWithDescription("Hit Effect Settings", "Allows an AI to display a random hit affect after receiving damage.", true);
|
||||
|
||||
EditorGUILayout.PropertyField(UseHitEffectProp, new GUIContent("Use Hit Effect"));
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls whether or not this AI will use a hit effect when it receives melee damage.", true);
|
||||
|
||||
if (self.UseHitEffect == YesOrNo.Yes)
|
||||
{
|
||||
CustomEditorProperties.BeginIndent();
|
||||
|
||||
CustomEditorProperties.CustomHelpLabelField("The hit effect that will appear when this AI receives damage.", true);
|
||||
HitEffectsList.DoLayoutList();
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.Space();
|
||||
|
||||
EditorGUILayout.PropertyField(HitEffectTimeoutSecondsProp, new GUIContent("Hit Effect Timeout Seconds"));
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls how long the hit effect will be visible before being deactivated.", true);
|
||||
EditorGUILayout.Space();
|
||||
|
||||
EditorGUILayout.PropertyField(HitEffectPosOffsetProp, new GUIContent("Hit Effect Position Offset"));
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls the offset position of the hit effect using the AI's Hit Transform position.", true);
|
||||
EditorGUILayout.Space();
|
||||
|
||||
CustomEditorProperties.EndIndent();
|
||||
}
|
||||
|
||||
CustomEditorProperties.EndFoldoutWindowBox();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3ed4641fe6c076e40af6304b8c677cf1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,743 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using UnityEditorInternal;
|
||||
|
||||
namespace EmeraldAI.Utility
|
||||
{
|
||||
[CustomEditor(typeof(EmeraldMovement))]
|
||||
[CanEditMultipleObjects]
|
||||
public class EmeraldMovementEditor : Editor
|
||||
{
|
||||
GUIStyle FoldoutStyle;
|
||||
EmeraldAnimation EmeraldAnimation;
|
||||
Texture MovementEditorIcon;
|
||||
int CurrentWaypointIndex = -1;
|
||||
|
||||
#region SerializedProperties
|
||||
//Foldouts
|
||||
SerializedProperty HideSettingsFoldout, WanderFoldout, WaypointsFoldout, WaypointsListFoldout, MovementFoldout, AlignmentFoldout, TurnFoldout;
|
||||
|
||||
//Int
|
||||
SerializedProperty StationaryIdleSecondsMinProp, StationaryIdleSecondsMaxProp, WanderRadiusProp, MaxSlopeLimitProp, WalkSpeedProp, RunSpeedProp, MinimumWaitTimeProp, MaximumWaitTimeProp,
|
||||
WalkBackwardsSpeedProp, StationaryTurningSpeedCombatProp, MovingTurningSpeedCombatProp, BackupTurningSpeedProp;
|
||||
|
||||
//Floats
|
||||
SerializedProperty StoppingDistanceProp, NonCombatAngleToTurnProp, CombatAngleToTurnProp, StationaryTurningSpeedNonCombatProp, MovingTurnSpeedNonCombatProp, MovementTurningSensitivityProp, MaxNormalAngleProp, NonCombatAlignSpeedProp,
|
||||
CombatAlignSpeedProp, ForceWalkDistanceProp, DecelerationDampTimeProp;
|
||||
|
||||
//Enums
|
||||
SerializedProperty WanderTypeProp, WaypointTypeProp, AlignAIWithGroundProp, CurrentMovementStateProp, AnimatorTypeProp, AlignmentQualityProp, AlignAIOnStartProp;
|
||||
|
||||
//LayerMask
|
||||
SerializedProperty DynamicWanderLayerMaskProp, BackupLayerMaskProp, AlignmentLayerMaskProp;
|
||||
|
||||
//Bool
|
||||
SerializedProperty UseRandomRotationOnStartProp, AnimationsUpdatedProp;
|
||||
|
||||
//Objects
|
||||
SerializedProperty WaypointObjectProp;
|
||||
#endregion
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
EmeraldMovement self = (EmeraldMovement)target;
|
||||
EmeraldAnimation = self.GetComponent<EmeraldAnimation>();
|
||||
if (MovementEditorIcon == null) MovementEditorIcon = Resources.Load("Editor Icons/EmeraldMovement") as Texture;
|
||||
|
||||
InitializeProperties();
|
||||
}
|
||||
|
||||
void InitializeProperties ()
|
||||
{
|
||||
//Enums
|
||||
WanderTypeProp = serializedObject.FindProperty("WanderType");
|
||||
WaypointTypeProp = serializedObject.FindProperty("WaypointType");
|
||||
AlignAIWithGroundProp = serializedObject.FindProperty("AlignAIWithGround");
|
||||
CurrentMovementStateProp = serializedObject.FindProperty("CurrentMovementState");
|
||||
AnimatorTypeProp = serializedObject.FindProperty("MovementType");
|
||||
AlignmentQualityProp = serializedObject.FindProperty("AlignmentQuality");
|
||||
AlignAIOnStartProp = serializedObject.FindProperty("AlignAIOnStart");
|
||||
|
||||
//Ints
|
||||
StationaryIdleSecondsMinProp = serializedObject.FindProperty("StationaryIdleSecondsMin");
|
||||
StationaryIdleSecondsMaxProp = serializedObject.FindProperty("StationaryIdleSecondsMax");
|
||||
WanderRadiusProp = serializedObject.FindProperty("WanderRadius");
|
||||
MaxSlopeLimitProp = serializedObject.FindProperty("MaxSlopeLimit");
|
||||
WanderRadiusProp = serializedObject.FindProperty("WanderRadius");
|
||||
MinimumWaitTimeProp = serializedObject.FindProperty("MinimumWaitTime");
|
||||
MaximumWaitTimeProp = serializedObject.FindProperty("MaximumWaitTime");
|
||||
WalkSpeedProp = serializedObject.FindProperty("WalkSpeed");
|
||||
WalkBackwardsSpeedProp = serializedObject.FindProperty("WalkBackwardsSpeed");
|
||||
RunSpeedProp = serializedObject.FindProperty("RunSpeed");
|
||||
BackupTurningSpeedProp = serializedObject.FindProperty("BackupTurningSpeed");
|
||||
|
||||
CombatAngleToTurnProp = serializedObject.FindProperty("CombatAngleToTurn");
|
||||
NonCombatAngleToTurnProp = serializedObject.FindProperty("NonCombatAngleToTurn");
|
||||
StationaryTurningSpeedNonCombatProp = serializedObject.FindProperty("StationaryTurningSpeedNonCombat");
|
||||
StationaryTurningSpeedCombatProp = serializedObject.FindProperty("StationaryTurningSpeedCombat");
|
||||
MovingTurnSpeedNonCombatProp = serializedObject.FindProperty("MovingTurnSpeedNonCombat");
|
||||
MovingTurningSpeedCombatProp = serializedObject.FindProperty("MovingTurnSpeedCombat");
|
||||
|
||||
//Floats
|
||||
StoppingDistanceProp = serializedObject.FindProperty("StoppingDistance");
|
||||
MovementTurningSensitivityProp = serializedObject.FindProperty("MovementTurningSensitivity");
|
||||
DecelerationDampTimeProp = serializedObject.FindProperty("DecelerationDampTime");
|
||||
MaxNormalAngleProp = serializedObject.FindProperty("MaxNormalAngle");
|
||||
NonCombatAlignSpeedProp = serializedObject.FindProperty("NonCombatAlignmentSpeed");
|
||||
CombatAlignSpeedProp = serializedObject.FindProperty("CombatAlignmentSpeed");
|
||||
ForceWalkDistanceProp = serializedObject.FindProperty("ForceWalkDistance");
|
||||
|
||||
//LayerMask
|
||||
DynamicWanderLayerMaskProp = serializedObject.FindProperty("DynamicWanderLayerMask");
|
||||
BackupLayerMaskProp = serializedObject.FindProperty("BackupLayerMask");
|
||||
AlignmentLayerMaskProp = serializedObject.FindProperty("AlignmentLayerMask");
|
||||
|
||||
//Bool
|
||||
HideSettingsFoldout = serializedObject.FindProperty("HideSettingsFoldout");
|
||||
WanderFoldout = serializedObject.FindProperty("WanderFoldout");
|
||||
WaypointsFoldout = serializedObject.FindProperty("WaypointsFoldout");
|
||||
WaypointsListFoldout = serializedObject.FindProperty("WaypointsListFoldout");
|
||||
MovementFoldout = serializedObject.FindProperty("MovementFoldout");
|
||||
AlignmentFoldout = serializedObject.FindProperty("AlignmentFoldout");
|
||||
TurnFoldout = serializedObject.FindProperty("TurnFoldout");
|
||||
UseRandomRotationOnStartProp = serializedObject.FindProperty("UseRandomRotationOnStart");
|
||||
AnimationsUpdatedProp = serializedObject.FindProperty("AnimationsUpdated"); //Note: Used by multiple scripts currently, ensure this doesn't cause issues.
|
||||
|
||||
//Objects
|
||||
WaypointObjectProp = serializedObject.FindProperty("m_WaypointObject");
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
FoldoutStyle = CustomEditorProperties.UpdateEditorStyles();
|
||||
EmeraldMovement self = (EmeraldMovement)target;
|
||||
serializedObject.Update();
|
||||
|
||||
CustomEditorProperties.BeginScriptHeaderNew("Movement", MovementEditorIcon, new GUIContent(), HideSettingsFoldout);
|
||||
|
||||
if (!HideSettingsFoldout.boolValue)
|
||||
{
|
||||
EditorGUILayout.Space();
|
||||
MovementSettings(self);
|
||||
EditorGUILayout.Space();
|
||||
TurnSettings(self);
|
||||
EditorGUILayout.Space();
|
||||
AlignmentSettings(self);
|
||||
EditorGUILayout.Space();
|
||||
WanderSettings(self);
|
||||
EditorGUILayout.Space();
|
||||
WaypointSettings(self);
|
||||
if (self.WanderType == EmeraldMovement.WanderTypes.Waypoints) EditorGUILayout.Space();
|
||||
}
|
||||
|
||||
CustomEditorProperties.EndScriptHeader();
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds all waypoint related settings and displays them through the EmeraldAIMovementEditor.
|
||||
/// </summary>
|
||||
void WaypointSettings (EmeraldMovement self)
|
||||
{
|
||||
if (self.WanderType == EmeraldMovement.WanderTypes.Waypoints)
|
||||
{
|
||||
WaypointsFoldout.boolValue = EditorGUILayout.Foldout(WaypointsFoldout.boolValue, "Waypoint Settings", true, FoldoutStyle);
|
||||
|
||||
if (WaypointsFoldout.boolValue)
|
||||
{
|
||||
CustomEditorProperties.BeginFoldoutWindowBox();
|
||||
|
||||
CustomEditorProperties.TextTitleWithDescription("Waypoint Editor", "Below you can define waypoints for your AI to follow. Simply press the 'Add Waypoint' button to create a waypoint. The AI will follow each created waypoint in the order " +
|
||||
"they are created. A line will be drawn to visually represent this.", true);
|
||||
|
||||
if (self.WaypointsList != null && Selection.objects.Length == 1)
|
||||
{
|
||||
EditorGUILayout.LabelField("Controls what an AI will do when it reaches its last waypoint.", EditorStyles.helpBox);
|
||||
EditorGUILayout.PropertyField(WaypointTypeProp, new GUIContent("Waypoint Type"));
|
||||
GUI.backgroundColor = new Color(0.1f, 0.1f, 0.1f, 0.19f);
|
||||
GUI.backgroundColor = Color.white;
|
||||
|
||||
if (self.WaypointType == (EmeraldMovement.WaypointTypes.Loop))
|
||||
{
|
||||
CustomEditorProperties.CustomHelpLabelField("Loop - Allows an AI to continiously move to each waypoint, in order, without ever stopping. When an AI reaches its last waypoint, it will set the first waypoint as its next waypoint thus creating a loop.", false);
|
||||
}
|
||||
else if (self.WaypointType == (EmeraldMovement.WaypointTypes.Reverse))
|
||||
{
|
||||
CustomEditorProperties.CustomHelpLabelField("Reverse - Allows an AI to continiously move to each waypoint, in order, without stopping until it reaches its last waypoint. When this happens, it will idle " +
|
||||
"for the length of its Wait Time seconds then reverse the AI's waypoints making the last waypoint its first and repeat this process.", false);
|
||||
}
|
||||
else if (self.WaypointType == (EmeraldMovement.WaypointTypes.Random))
|
||||
{
|
||||
CustomEditorProperties.CustomHelpLabelField("Random - Allows an AI to patrol randomly through all waypoints. An AI will stop and idle each time it reaches a waypoint for as long as its Wait Time seconds are set.", false);
|
||||
}
|
||||
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.Space();
|
||||
CustomEditorProperties.CustomHelpLabelField("Imports all waypoints from the current Waypoint Object.", false);
|
||||
EditorGUILayout.PropertyField(WaypointObjectProp);
|
||||
|
||||
if (GUILayout.Button("Import Waypoint Data") && EditorUtility.DisplayDialog("Import Waypoint Data?", "Are you sure you want to clear all of this AI's waypoints and import waypoints from the applied Waypoint Object? This process cannot be undone.", "Yes", "Cancel"))
|
||||
{
|
||||
if (self.m_WaypointObject == null)
|
||||
{
|
||||
Debug.LogError("There's no Waypoint Object applied. Please apply one to import waypoint data.");
|
||||
return;
|
||||
}
|
||||
|
||||
self.WaypointsList = new List<Vector3>(self.m_WaypointObject.Waypoints);
|
||||
EditorUtility.SetDirty(self);
|
||||
}
|
||||
EditorGUILayout.Space();
|
||||
CustomEditorProperties.CustomHelpLabelField("Exports all waypoints to a Waypoint Object to be imported and shared with other AI so waypoints don't have to be recreated manually.", false);
|
||||
if (GUILayout.Button("Export Waypoint Data"))
|
||||
{
|
||||
//Export all of the AI's current waypoints to a Waypoint Object so it can be imported to other AI.
|
||||
string SavePath = EditorUtility.SaveFilePanelInProject("Save Waypoint Data", "New Waypoint Object", "asset", "Please enter a file name to save the file to");
|
||||
if (SavePath != string.Empty)
|
||||
{
|
||||
var m_WaypointObject = CreateInstance<EmeraldWaypointObject>();
|
||||
m_WaypointObject.Waypoints = new List<Vector3>(self.WaypointsList);
|
||||
AssetDatabase.CreateAsset(m_WaypointObject, SavePath);
|
||||
}
|
||||
|
||||
//For some reason, EditorUtility.SaveFilePanelInProject throws an incorrect EditorGUILayout error when it's used with some custom properties. This fixes it...
|
||||
CustomEditorProperties.BeginScriptHeader("", null);
|
||||
CustomEditorProperties.BeginFoldoutWindowBox();
|
||||
}
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.Space();
|
||||
|
||||
if (GUILayout.Button("Add Waypoint"))
|
||||
{
|
||||
Vector3 newPoint = new Vector3(0, 0, 0);
|
||||
|
||||
if (self.WaypointsList.Count == 0)
|
||||
{
|
||||
newPoint = self.transform.position + Vector3.forward * (self.StoppingDistance * 2);
|
||||
}
|
||||
else if (self.WaypointsList.Count > 0)
|
||||
{
|
||||
newPoint = self.WaypointsList[self.WaypointsList.Count - 1] + Vector3.forward * (self.StoppingDistance * 2);
|
||||
}
|
||||
|
||||
Undo.RecordObject(self, "Add Waypoint");
|
||||
self.WaypointsList.Add(newPoint);
|
||||
EditorUtility.SetDirty(self);
|
||||
}
|
||||
|
||||
var style = new GUIStyle(GUI.skin.button);
|
||||
style.normal.textColor = Color.red;
|
||||
|
||||
if (GUILayout.Button("Clear All Waypoints", style) && EditorUtility.DisplayDialog("Clear Waypoints?", "Are you sure you want to clear all of this AI's waypoints? This process cannot be undone.", "Yes", "Cancel"))
|
||||
{
|
||||
self.WaypointsList.Clear();
|
||||
EditorUtility.SetDirty(self);
|
||||
}
|
||||
GUI.contentColor = Color.white;
|
||||
GUI.backgroundColor = Color.white;
|
||||
|
||||
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.Space();
|
||||
|
||||
CustomEditorProperties.BeginIndent();
|
||||
WaypointsListFoldout.boolValue = CustomEditorProperties.Foldout(WaypointsListFoldout.boolValue, "Waypoints List", true, FoldoutStyle);
|
||||
|
||||
if (WaypointsListFoldout.boolValue)
|
||||
{
|
||||
CustomEditorProperties.BeginFoldoutWindowBox();
|
||||
CustomEditorProperties.TextTitleWithDescription("Waypoints List", "All of this AI's current waypoints. Waypoints can be individually removed by pressing the ''Remove Point'' button.", true);
|
||||
EditorGUILayout.Space();
|
||||
|
||||
if (self.WaypointsList.Count > 0)
|
||||
{
|
||||
for (int j = 0; j < self.WaypointsList.Count; ++j)
|
||||
{
|
||||
GUI.backgroundColor = new Color(0.1f, 0.1f, 0.1f, 0.19f);
|
||||
EditorGUILayout.LabelField("Waypoint " + (j + 1), EditorStyles.toolbarButton);
|
||||
GUI.backgroundColor = Color.white;
|
||||
|
||||
EditorGUILayout.BeginHorizontal(GUILayout.ExpandWidth(true));
|
||||
if (j < self.WaypointsList.Count - 1)
|
||||
{
|
||||
if (GUILayout.Button(new GUIContent("Insert", "Inserts a point between this point and the next point."), EditorStyles.miniButton, GUILayout.Height(18)))
|
||||
{
|
||||
Undo.RecordObject(self, "Insert Waypoint Above this Point");
|
||||
self.WaypointsList.Insert(j + 1, (self.WaypointsList[j] + self.WaypointsList[j + 1]) / 2f);
|
||||
CurrentWaypointIndex = j + 1;
|
||||
EditorUtility.SetDirty(self);
|
||||
HandleUtility.Repaint();
|
||||
}
|
||||
}
|
||||
|
||||
if (GUILayout.Button(new GUIContent("Remove", "Remove this point from the waypoint list."), EditorStyles.miniButton, GUILayout.Height(18)))
|
||||
{
|
||||
Undo.RecordObject(self, "Remove Point");
|
||||
self.WaypointsList.RemoveAt(j);
|
||||
EditorUtility.SetDirty(self);
|
||||
HandleUtility.Repaint();
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
|
||||
|
||||
GUILayout.Space(10);
|
||||
}
|
||||
}
|
||||
CustomEditorProperties.EndFoldoutWindowBox();
|
||||
}
|
||||
|
||||
CustomEditorProperties.EndIndent();
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.Space();
|
||||
}
|
||||
else if (self.WaypointsList != null && Selection.objects.Length > 1)
|
||||
{
|
||||
CustomEditorProperties.DisplayWarningMessage("Waypoints do not support multi-object editing. If you'd like to edit an AI's waypoints, please only have 1 AI selected at a time.");
|
||||
}
|
||||
|
||||
CustomEditorProperties.EndFoldoutWindowBox();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds all movement related settings and displays them through the EmeraldAIMovementEditor.
|
||||
/// </summary>
|
||||
void MovementSettings (EmeraldMovement self)
|
||||
{
|
||||
MovementFoldout.boolValue = EditorGUILayout.Foldout(MovementFoldout.boolValue, "Movement Settings", true, FoldoutStyle);
|
||||
|
||||
if (MovementFoldout.boolValue)
|
||||
{
|
||||
CustomEditorProperties.BeginFoldoutWindowBox();
|
||||
|
||||
CustomEditorProperties.TextTitleWithDescription("Movement Settings", "Controls all speed and distance related settings.", true);
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
EditorGUILayout.PropertyField(AnimatorTypeProp, new GUIContent("Movement Type"));
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls how an AI is moved. This is either driven by the Root Motion animation or by the NavMesh component.", true);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
if (EmeraldAnimation.m_AnimationProfile.AnimatorControllerGenerated)
|
||||
{
|
||||
//Assign this directly as AnimatorTypeProp becomes desynced with self.AnimatorType when regenerating the Animator for this setting.
|
||||
self.MovementType = (EmeraldMovement.MovementTypes)AnimatorTypeProp.intValue;
|
||||
EmeraldAnimatorGenerator.GenerateAnimatorController(EmeraldAnimation.m_AnimationProfile);
|
||||
}
|
||||
}
|
||||
|
||||
//Movement Type
|
||||
CustomEditorProperties.BeginIndent();
|
||||
|
||||
GUI.backgroundColor = new Color(5f, 0.5f, 0.5f, 1f);
|
||||
EditorGUILayout.LabelField("When using the Root Motion Movement Type, an AI's Movement Speed is controlled by its animation speed. You can adjust this through an AI's Animation Profile.", EditorStyles.helpBox);
|
||||
GUI.backgroundColor = Color.white;
|
||||
EditorGUI.BeginDisabledGroup(self.MovementType == EmeraldMovement.MovementTypes.RootMotion);
|
||||
CustomEditorProperties.CustomFloatSlider(new Rect(), new GUIContent(), WalkSpeedProp, "Walk Speed", 0.5f, 5);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls how fast your AI walks.", true);
|
||||
|
||||
CustomEditorProperties.CustomFloatSlider(new Rect(), new GUIContent(), RunSpeedProp, "Run Speed", 0.5f, 10);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls how fast your AI runs.", true);
|
||||
|
||||
CustomFloatAnimationField(new Rect(), new GUIContent(), WalkBackwardsSpeedProp, "Walk Backwards Speed", 0.5f, 3f);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls how fast your AI walks backwards.", true);
|
||||
|
||||
//Update the Animator as this is required when updating NavMesh speed settings.
|
||||
if (EmeraldAnimation.m_AnimationProfile != null && EmeraldAnimation.m_AnimationProfile.AnimatorControllerGenerated && self.MovementType == EmeraldMovement.MovementTypes.NavMeshDriven)
|
||||
{
|
||||
if (EmeraldAnimation.m_AnimationProfile.AnimationsUpdated || EmeraldAnimation.m_AnimationProfile.AnimationListsChanged)
|
||||
{
|
||||
EmeraldAnimatorGenerator.GenerateAnimatorController(EmeraldAnimation.m_AnimationProfile);
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUI.EndDisabledGroup();
|
||||
CustomEditorProperties.EndIndent();
|
||||
//Movement Type
|
||||
|
||||
EditorGUILayout.PropertyField(CurrentMovementStateProp, new GUIContent("Movement Animation"));
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls the type of animation your AI will use when using waypoints, moving to its destination, or wandering. " +
|
||||
"Note: If needed, this can be changed programmatically during runtime.", true);
|
||||
EditorGUILayout.Space();
|
||||
|
||||
CustomEditorProperties.CustomFloatSlider(new Rect(), new GUIContent(), ForceWalkDistanceProp, "Force Walk Distance", 0.0f, 8.0f);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls the distance in which an AI will start walking instead of running as it approaches its target or destination. This can be set to 0 if you would like this feature disabled.", true);
|
||||
|
||||
CustomEditorProperties.CustomFloatSlider(new Rect(), new GUIContent(), StoppingDistanceProp, "Stopping Distance", 0.25f, 40);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls the distance in which an AI will stop before waypoints and non-combat related destinations.", true);
|
||||
|
||||
CustomEditorProperties.CustomFloatSlider(new Rect(), new GUIContent(), DecelerationDampTimeProp, "Deceleration Damp Time", 0.1f, 0.4f);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls the damp time of an AI's animations when decelerating. Lower values mean faster blending of animations between movement and stopping.", true);
|
||||
|
||||
EditorGUILayout.PropertyField(BackupLayerMaskProp, new GUIContent("Backup Layers"));
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls which layers will affect the AI's backing up process. Colliders detected within a few units behind the AI will stop the backing up process.", true);
|
||||
|
||||
if (BackupLayerMaskProp.intValue == 0)
|
||||
{
|
||||
GUI.backgroundColor = new Color(10f, 0.0f, 0.0f, 0.25f);
|
||||
EditorGUILayout.LabelField("The Backup LayerMask cannot contain Nothing.", EditorStyles.helpBox);
|
||||
GUI.backgroundColor = Color.white;
|
||||
}
|
||||
|
||||
CustomEditorProperties.EndFoldoutWindowBox();
|
||||
}
|
||||
}
|
||||
|
||||
void TurnSettings (EmeraldMovement self)
|
||||
{
|
||||
TurnFoldout.boolValue = EditorGUILayout.Foldout(TurnFoldout.boolValue, "Turn Settings", true, FoldoutStyle);
|
||||
|
||||
if (TurnFoldout.boolValue)
|
||||
{
|
||||
CustomEditorProperties.BeginFoldoutWindowBox();
|
||||
|
||||
CustomEditorProperties.TextTitleWithDescription("Turn Settings", "Controls all settings and speeds related to turning.", true);
|
||||
|
||||
CustomEditorProperties.CustomIntSlider(new Rect(), new GUIContent(), NonCombatAngleToTurnProp, "Turning Angle", 15, 90);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls the angle needed to play a turn animation while an AI is not in combat. Emerald can automatically detect whether an AI is " +
|
||||
"turing left or right. Note: You can use a walking animation in place of a turning animation if your AI doesn't one.", true);
|
||||
|
||||
CustomEditorProperties.CustomIntSlider(new Rect(), new GUIContent(), CombatAngleToTurnProp, "Combat Turning Angle", 20, 90);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls the angle needed to play a turn animation while an AI is in combat. Emerald can automatically detect whether an AI is " +
|
||||
"turing left or right. Note: You can use a walking animation in place of a turning animation if your AI doesn't one.", true);
|
||||
|
||||
CustomEditorProperties.CustomIntSlider(new Rect(), new GUIContent(), StationaryTurningSpeedNonCombatProp, "Stationary Turn Speed", 1, 200);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls how fast your AI turns while not in combat and is stationary. Note: Lower speeds are meant for the Root Motion setting" +
|
||||
" where the turning animations help assist an AI's turning. If you find an AI not turning quick enough while wandering, even with Root Motion enabled, you will most likely need to increasing this setting.", true);
|
||||
|
||||
CustomEditorProperties.CustomIntSlider(new Rect(), new GUIContent(), StationaryTurningSpeedCombatProp, "Stationary Combat Turn Speed", 1, 200);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls how fast your AI turns while in combat and is stationary.", true);
|
||||
|
||||
CustomEditorProperties.CustomIntSlider(new Rect(), new GUIContent(), MovingTurnSpeedNonCombatProp, "Moving Turn Speed", 50, 750);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls how fast your AI turns while not in combat and is moving. Note: Lower speeds are meant for the Root Motion setting" +
|
||||
" where the turning animations help assist an AI's turning. If you find an AI not turning quick enough while wandering, even with Root Motion enabled, you will most likely need to increasing this setting.", true);
|
||||
|
||||
CustomEditorProperties.CustomIntSlider(new Rect(), new GUIContent(), MovingTurningSpeedCombatProp, "Moving Combat Turn Speed", 50, 750);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls how fast your AI turns while in combat and is moving.", true);
|
||||
|
||||
CustomEditorProperties.CustomIntSlider(new Rect(), new GUIContent(), BackupTurningSpeedProp, "Backup Turn Speed", 5, 750);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls how fast your AI turns while backing up.", true);
|
||||
|
||||
CustomEditorProperties.CustomFloatSlider(new Rect(), new GUIContent(), MovementTurningSensitivityProp, "Movement Turning Sensitivity", 0.5f, 3f);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls how sensitive the movement blend trees are when playing movement turning animations. This is especially noticeable for quadruped models with turning animations.", true);
|
||||
|
||||
EditorGUILayout.PropertyField(UseRandomRotationOnStartProp, new GUIContent("Random Roation on Start"));
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls whether or not AI will be randomly rotated on Start.", true);
|
||||
|
||||
CustomEditorProperties.EndFoldoutWindowBox();
|
||||
}
|
||||
}
|
||||
|
||||
void AlignmentSettings (EmeraldMovement self)
|
||||
{
|
||||
AlignmentFoldout.boolValue = EditorGUILayout.Foldout(AlignmentFoldout.boolValue, "Alignment Settings", true, FoldoutStyle);
|
||||
|
||||
if (AlignmentFoldout.boolValue)
|
||||
{
|
||||
CustomEditorProperties.BeginFoldoutWindowBox();
|
||||
|
||||
CustomEditorProperties.TextTitleWithDescription("Alignment Settings", "Allows AI to align themselves to slopes and surfaces (Disable if you are using a full body IK system like Final IK)", true);
|
||||
|
||||
EditorGUILayout.PropertyField(AlignAIWithGroundProp, new GUIContent("Align AI"));
|
||||
CustomEditorProperties.CustomHelpLabelField("Aligns the AI to the angle of the terrain and other objects for added realism. Disable this feature for improved performance per AI.", true);
|
||||
|
||||
if (self.AlignAIWithGround == YesOrNo.Yes)
|
||||
{
|
||||
CustomEditorProperties.BeginIndent();
|
||||
|
||||
EditorGUILayout.PropertyField(AlignmentLayerMaskProp, new GUIContent("Alignment Layers"));
|
||||
CustomEditorProperties.CustomHelpLabelField("The layers the AI will use for aligning itself with the angles of surfaces. Any layers not included above will be ignred.", true);
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
EditorGUILayout.PropertyField(AlignmentQualityProp, new GUIContent("Align Quality"));
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls the quality of the Align AI feature by controlling how often it's updated.", true);
|
||||
|
||||
CustomEditorProperties.CustomIntSlider(new Rect(), new GUIContent(), NonCombatAlignSpeedProp, "Non-Combat Align Speed", 5, 200);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls the speed in which the AI is aligned with the ground while not in combat.", true);
|
||||
|
||||
CustomEditorProperties.CustomIntSlider(new Rect(), new GUIContent(), CombatAlignSpeedProp, "Combat Align Speed", 5, 200);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls the speed in which the AI is aligned with the ground while in combat.", true);
|
||||
|
||||
CustomEditorProperties.CustomIntSlider(new Rect(), new GUIContent(), MaxNormalAngleProp, "Max Angle", 5, 50);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls the maximum angle for an AI to rotate to when aligning with the ground.", true);
|
||||
|
||||
EditorGUILayout.PropertyField(AlignAIOnStartProp, new GUIContent("Align on Start"));
|
||||
CustomEditorProperties.CustomHelpLabelField("Calculates the Align AI feature on Start.", true);
|
||||
|
||||
CustomEditorProperties.EndIndent();
|
||||
}
|
||||
|
||||
CustomEditorProperties.EndFoldoutWindowBox();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds all wander type settings and displays them through the EmeraldAIMovementEditor.
|
||||
/// </summary>
|
||||
void WanderSettings (EmeraldMovement self)
|
||||
{
|
||||
WanderFoldout.boolValue = EditorGUILayout.Foldout(WanderFoldout.boolValue, "Wander Type Settings", true, FoldoutStyle);
|
||||
|
||||
if (WanderFoldout.boolValue)
|
||||
{
|
||||
CustomEditorProperties.BeginFoldoutWindowBox();
|
||||
|
||||
CustomEditorProperties.TextTitleWithDescription("Wander Type Settings", "Controls how an AI wanders when not in combat. Using the Waypoints Wander Type will make the waypoint editor visible.", true);
|
||||
EditorGUILayout.LabelField("Controls the type of wandering mechanics this AI will use. While wandering, AI will react to targets according to their Behavior Type, given they are visible and within their field of view.", EditorStyles.helpBox);
|
||||
EditorGUILayout.PropertyField(WanderTypeProp, new GUIContent("Wander Type"));
|
||||
|
||||
CustomEditorProperties.BeginIndent();
|
||||
if (self.WanderType == EmeraldMovement.WanderTypes.Dynamic)
|
||||
{
|
||||
CustomEditorProperties.CustomHelpLabelField("Dynamic - Allows an AI to randomly wander by dynamically generate waypoints around their Wander Radius.", true);
|
||||
}
|
||||
else if (self.WanderType == EmeraldMovement.WanderTypes.Waypoints)
|
||||
{
|
||||
CustomEditorProperties.CustomHelpLabelField("Waypoints - Allows you to define waypoints that the AI will move between. Note: The Waypoint Settings can be found in the foldout below this foldout.", true);
|
||||
if (GUILayout.Button("Open Waypoint Settings"))
|
||||
{
|
||||
self.WanderFoldout = false;
|
||||
self.WaypointsFoldout = true;
|
||||
}
|
||||
}
|
||||
else if (self.WanderType == EmeraldMovement.WanderTypes.Stationary)
|
||||
{
|
||||
CustomEditorProperties.CustomHelpLabelField("Stationary - Allows an AI to stay stationary in the same position and will not move unless a target enters their trigger radius.", true);
|
||||
|
||||
CustomEditorProperties.CustomIntField(new Rect(), new GUIContent(), StationaryIdleSecondsMinProp, "Min Idle Animation Seconds");
|
||||
CustomEditorProperties.CustomHelpLabelField("When using more than 1 idle animation, this controls the minimum amount of seconds needed before switching to the next idle " +
|
||||
"animation. This will be randomized with the Max Idle Animation Seconds.", true);
|
||||
|
||||
CustomEditorProperties.CustomIntField(new Rect(), new GUIContent(), StationaryIdleSecondsMaxProp, "Max Idle Animation Seconds");
|
||||
CustomEditorProperties.CustomHelpLabelField("When using more than 1 idle animation, this controls the maximum amount of seconds needed before switching to the next idle " +
|
||||
"animation. This will be randomized with the Min Idle Animation Seconds.", true);
|
||||
}
|
||||
else if (self.WanderType == EmeraldMovement.WanderTypes.Destination)
|
||||
{
|
||||
CustomEditorProperties.CustomHelpLabelField("Destination - Allows an AI to travel to a single destination relying on Unity's NavMesh Pathfinding to get there. Once it reaches the destination, it will stay stationary.", true);
|
||||
|
||||
if (GUILayout.Button("Reset Destination Point"))
|
||||
{
|
||||
self.SingleDestination = self.transform.position + self.transform.forward * 2;
|
||||
}
|
||||
}
|
||||
else if (self.WanderType == EmeraldMovement.WanderTypes.Custom)
|
||||
{
|
||||
CustomEditorProperties.CustomHelpLabelField("Custom - Allows an AI to travel to a destination set through code, which relies on Unity's NavMesh Pathfinding to get there. Once it reaches the destination, it will stay stationary.", false);
|
||||
}
|
||||
CustomEditorProperties.EndIndent();
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
if (self.WanderType == EmeraldMovement.WanderTypes.Dynamic)
|
||||
{
|
||||
CustomEditorProperties.CustomIntSlider(new Rect(), new GUIContent(), WanderRadiusProp, "Dynamic Wander Radius", ((int)self.StoppingDistance + 3), 300);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls the radius that the AI uses to wander. The AI will randomly pick waypoints within this radius.", true);
|
||||
|
||||
CustomEditorProperties.CustomIntSlider(new Rect(), new GUIContent(), MaxSlopeLimitProp, "Max Slope Limit", 10, 60);
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls the maximum slope that a waypoint can be generated on.", true);
|
||||
|
||||
EditorGUILayout.PropertyField(DynamicWanderLayerMaskProp, new GUIContent("Dynamic Wander Layers"));
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls what layers will be used when generating Dynamic Waypoints.", false);
|
||||
|
||||
if (DynamicWanderLayerMaskProp.intValue == 0)
|
||||
{
|
||||
GUI.backgroundColor = new Color(10f, 0.0f, 0.0f, 0.25f);
|
||||
EditorGUILayout.LabelField("The Dynamic Wander LayerMask cannot contain Nothing.", EditorStyles.helpBox);
|
||||
GUI.backgroundColor = Color.white;
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
if (self.WanderType == EmeraldMovement.WanderTypes.Dynamic || self.WanderType == EmeraldMovement.WanderTypes.Waypoints)
|
||||
{
|
||||
CustomEditorProperties.CustomIntField(new Rect(), new GUIContent(), MinimumWaitTimeProp, "Min Wait Time");
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls the minimum amount of seconds before generating a new waypoint, when using the Dynamic and Random waypoint Wander Type. This amount is " +
|
||||
"randomized with the Maximim Wait Time.", true);
|
||||
|
||||
CustomEditorProperties.CustomIntField(new Rect(), new GUIContent(), MaximumWaitTimeProp, "Max Wait Time");
|
||||
CustomEditorProperties.CustomHelpLabelField("Controls the maximum amount of seconds before generating a new waypoint, when using the Dynamic and Random waypoint Wander Type. This amount " +
|
||||
"is randomized with the Minimum Wait Time.", true);
|
||||
}
|
||||
|
||||
CustomEditorProperties.EndFoldoutWindowBox();
|
||||
}
|
||||
|
||||
if (self.WanderType == EmeraldMovement.WanderTypes.Destination)
|
||||
{
|
||||
if (self.SingleDestination == Vector3.zero)
|
||||
{
|
||||
self.SingleDestination = new Vector3(self.transform.position.x, self.transform.position.y, self.transform.position.z + 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used for drawing all of the Emerald AI Movement Settings.
|
||||
/// </summary>
|
||||
void OnSceneGUI()
|
||||
{
|
||||
EmeraldMovement self = (EmeraldMovement)target;
|
||||
DrawWaypoints(self);
|
||||
DrawWanderArea(self);
|
||||
DrawSingleDestinationPoint(self);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draw the user created waypoints, when using the Waypoint Wander Type.
|
||||
/// </summary>
|
||||
void DrawWaypoints (EmeraldMovement self)
|
||||
{
|
||||
if (Event.current != null && Event.current.isKey && Event.current.type.Equals(EventType.KeyDown) && Event.current.keyCode == KeyCode.Delete)
|
||||
{
|
||||
Event.current.Use();
|
||||
|
||||
if (CurrentWaypointIndex != -1)
|
||||
{
|
||||
Undo.RecordObject(self, "Deleted Waypoint");
|
||||
self.WaypointsList.RemoveAt(CurrentWaypointIndex);
|
||||
}
|
||||
}
|
||||
|
||||
if (self.WanderType == EmeraldMovement.WanderTypes.Waypoints && WaypointsFoldout.boolValue && !HideSettingsFoldout.boolValue)
|
||||
{
|
||||
if (self.WaypointsList.Count > 0 && self.WaypointsList != null)
|
||||
{
|
||||
Handles.color = Color.blue;
|
||||
Handles.DrawLine(self.transform.position, self.WaypointsList[0]);
|
||||
Handles.color = Color.white;
|
||||
|
||||
Handles.color = Color.green;
|
||||
if (self.WaypointType != (EmeraldMovement.WaypointTypes.Random))
|
||||
{
|
||||
for (int i = 0; i < self.WaypointsList.Count - 1; i++)
|
||||
{
|
||||
Handles.DrawLine(self.WaypointsList[i], self.WaypointsList[i + 1]);
|
||||
}
|
||||
}
|
||||
else if (self.WaypointType == (EmeraldMovement.WaypointTypes.Random))
|
||||
{
|
||||
for (int i = 0; i < self.WaypointsList.Count; i++)
|
||||
{
|
||||
for (int j = (i + 1); j < self.WaypointsList.Count; j++)
|
||||
{
|
||||
Handles.DrawLine(self.WaypointsList[i], self.WaypointsList[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Handles.color = Color.white;
|
||||
|
||||
Handles.color = Color.green;
|
||||
if (self.WaypointType == (EmeraldMovement.WaypointTypes.Loop))
|
||||
{
|
||||
Handles.DrawLine(self.WaypointsList[0], self.WaypointsList[self.WaypointsList.Count - 1]);
|
||||
}
|
||||
|
||||
//Track last grabbed waypoint. If delete button is pressed (using EventType) delete point (will need undo and redo)
|
||||
Handles.color = new Color(0, 1, 0, 0.25f);
|
||||
for (int i = 0; i < self.WaypointsList.Count; i++)
|
||||
{
|
||||
if (CurrentWaypointIndex != i)
|
||||
Handles.color = new Color(1, 1, 1, 0.05f);
|
||||
else
|
||||
Handles.color = new Color(1, 1, 0, 0.05f);
|
||||
Handles.zTest = UnityEngine.Rendering.CompareFunction.LessEqual;
|
||||
Handles.DrawSolidDisc(self.WaypointsList[i], Vector3.up, self.StoppingDistance);
|
||||
Handles.color = new Color(0, 0, 0, 0.5f);
|
||||
Handles.zTest = UnityEngine.Rendering.CompareFunction.Always;
|
||||
Handles.DrawSolidDisc(self.WaypointsList[i], Vector3.up, 0.25f);
|
||||
}
|
||||
|
||||
//Handles.zTest = UnityEngine.Rendering.CompareFunction.Always;
|
||||
for (int i = 0; i < self.WaypointsList.Count; i++)
|
||||
{
|
||||
EditorGUI.BeginChangeCheck();
|
||||
Vector3 Pos = Handles.PositionHandle(self.WaypointsList[i], Quaternion.identity);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
Undo.RecordObject(self, "Changed Waypoint Position");
|
||||
self.WaypointsList[i] = Pos;
|
||||
CurrentWaypointIndex = i;
|
||||
}
|
||||
|
||||
Handles.color = Color.white;
|
||||
CustomEditorProperties.DrawString("Waypoint " + (i + 1), self.WaypointsList[i] + Vector3.up, Color.white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the wander area, when using the Dynamic Wander Type.
|
||||
/// </summary>
|
||||
void DrawWanderArea (EmeraldMovement self)
|
||||
{
|
||||
if (self.WanderType == EmeraldMovement.WanderTypes.Dynamic && WanderFoldout.boolValue && !HideSettingsFoldout.boolValue)
|
||||
{
|
||||
Handles.color = new Color(0, 0.6f, 0, 1f);
|
||||
Handles.DrawWireDisc(self.transform.position, Vector3.up, (float)self.WanderRadius, 3f);
|
||||
Handles.color = Color.white;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the destination point, when using the Destination Wander Type.
|
||||
/// </summary>
|
||||
void DrawSingleDestinationPoint (EmeraldMovement self)
|
||||
{
|
||||
if (self.WanderType == EmeraldMovement.WanderTypes.Destination && self.SingleDestination != Vector3.zero && WanderFoldout.boolValue && !HideSettingsFoldout.boolValue)
|
||||
{
|
||||
Handles.color = Color.green;
|
||||
Handles.DrawLine(self.transform.position, self.SingleDestination);
|
||||
Handles.color = Color.white;
|
||||
|
||||
Handles.zTest = UnityEngine.Rendering.CompareFunction.LessEqual;
|
||||
Handles.SphereHandleCap(0, self.SingleDestination, Quaternion.identity, 0.5f, EventType.Repaint);
|
||||
CustomEditorProperties.DrawString("Destination Point", self.SingleDestination + Vector3.up, Color.white);
|
||||
|
||||
Handles.zTest = UnityEngine.Rendering.CompareFunction.Always;
|
||||
self.SingleDestination = Handles.PositionHandle(self.SingleDestination, Quaternion.identity);
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
Vector3 Pos = Handles.PositionHandle(self.SingleDestination, Quaternion.identity);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
Undo.RecordObject(self, "Changed Destination Position");
|
||||
self.SingleDestination = Pos;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
EditorUtility.SetDirty(self);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void CustomFloatAnimationField(Rect position, GUIContent label, SerializedProperty property, string Name, float Min, float Max)
|
||||
{
|
||||
label = EditorGUI.BeginProperty(position, label, property);
|
||||
EditorGUI.BeginChangeCheck();
|
||||
var newValue = EditorGUILayout.Slider(Name, property.floatValue, Min, Max);
|
||||
|
||||
if (newValue != property.floatValue)
|
||||
{
|
||||
AnimationsUpdatedProp.boolValue = true;
|
||||
}
|
||||
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
property.floatValue = newValue;
|
||||
}
|
||||
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 32751c7551a63ee43bd61a32bc698b23
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,174 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using UnityEditorInternal;
|
||||
using System.Reflection;
|
||||
|
||||
namespace EmeraldAI.Utility
|
||||
{
|
||||
[CustomEditor(typeof(EmeraldSounds))]
|
||||
[CanEditMultipleObjects]
|
||||
public class EmeraldSoundsEditor : Editor
|
||||
{
|
||||
public static EditorWindow EditorWindowRef;
|
||||
GUIStyle FoldoutStyle;
|
||||
Texture SoundsEditorIcon;
|
||||
|
||||
#region SerializedProperties
|
||||
SerializedProperty HideSettingsFoldout, SoundProfileProp, SoundProfileFoldout;
|
||||
#endregion
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
if (SoundsEditorIcon == null) SoundsEditorIcon = Resources.Load("Editor Icons/EmeraldSounds") as Texture;
|
||||
InitializeProperties();
|
||||
}
|
||||
|
||||
void InitializeProperties ()
|
||||
{
|
||||
HideSettingsFoldout = serializedObject.FindProperty("HideSettingsFoldout");
|
||||
SoundProfileFoldout = serializedObject.FindProperty("SoundProfileFoldout");
|
||||
SoundProfileProp = serializedObject.FindProperty("SoundProfile");
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
FoldoutStyle = CustomEditorProperties.UpdateEditorStyles();
|
||||
EmeraldSounds self = (EmeraldSounds)target;
|
||||
serializedObject.Update();
|
||||
|
||||
CustomEditorProperties.BeginScriptHeaderNew("Sounds", SoundsEditorIcon, new GUIContent(), HideSettingsFoldout);
|
||||
|
||||
MissingSoundProfileMessage(self);
|
||||
|
||||
if (!HideSettingsFoldout.boolValue)
|
||||
{
|
||||
EditorGUILayout.Space();
|
||||
DisplaySoundProfile(self);
|
||||
EditorGUILayout.Space();
|
||||
}
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
CustomEditorProperties.EndScriptHeader();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays a missing Sound Profile message within the EmeraldAISoundsEditor.
|
||||
/// </summary>
|
||||
void MissingSoundProfileMessage(EmeraldSounds self)
|
||||
{
|
||||
if (self.SoundProfile == null)
|
||||
{
|
||||
CustomEditorProperties.DisplaySetupWarning("This AI needs to have a Sound Profile. Press the 'Create New Sound Profile' button below to create a new one or assign one that has already been created.");
|
||||
}
|
||||
}
|
||||
|
||||
void DisplaySoundProfile(EmeraldSounds self)
|
||||
{
|
||||
SoundProfileFoldout.boolValue = CustomEditorProperties.Foldout(SoundProfileFoldout.boolValue, "Sound Profile Settings", true, FoldoutStyle);
|
||||
|
||||
if (SoundProfileFoldout.boolValue)
|
||||
{
|
||||
CustomEditorProperties.BeginFoldoutWindowBox();
|
||||
CustomEditorProperties.TextTitleWithDescription("Sound Profile", "A Sound Profile holds all of an AI's sound data. This allows AI to share the same sound data with only needing to rely on a single " +
|
||||
"Sound Profile. Any changes made to a Sound Profile will affect any AI using that Sound Profile. However, as many sound profiles can be created as needed. You can hover over the buttons below for more info.", true);
|
||||
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.PropertyField(SoundProfileProp);
|
||||
CustomEditorProperties.CustomHelpLabelField("The Sound Profile this AI is using. All sounds and volumes will be used for this AI and any other AI using it.", false);
|
||||
|
||||
EditorGUI.BeginDisabledGroup(self.SoundProfile == null);
|
||||
EditorGUILayout.Space();
|
||||
if (GUILayout.Button(new GUIContent("Edit Sound Profile", "Edit the current Sound Profile in a separate window so you can preview sounds while keeping a reference to the current Sound Profile."), GUILayout.Height(20)))
|
||||
{
|
||||
EditSoundProfile(self);
|
||||
}
|
||||
GUILayout.Space(2.5f);
|
||||
|
||||
if (GUILayout.Button(new GUIContent("Clear Sound Profile", "Clears the Sound Profile slot so a new one can be created. Note: The current Sound Profile object will remain in your project at its current path."), GUILayout.Height(20)))
|
||||
{
|
||||
SoundProfileProp.objectReferenceValue = null;
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
EditorGUI.EndDisabledGroup();
|
||||
GUILayout.Space(2.5f);
|
||||
|
||||
EditorGUI.BeginDisabledGroup(self.SoundProfile != null);
|
||||
if (GUILayout.Button(new GUIContent("Create New Sound Profile", "Creates a new Sound Profile within the Emerald AI/Sound Profiles folder. If you would like to create a new Sound Profile, remove the one in the current slot by pressing the 'Clear Sound Profile' button."), GUILayout.Height(20)))
|
||||
{
|
||||
CreateSoundProfile(self);
|
||||
}
|
||||
EditorGUI.EndDisabledGroup();
|
||||
GUILayout.Space(2.5f);
|
||||
|
||||
CustomEditorProperties.EndFoldoutWindowBox();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Sound Profile object, using the object's name, to the user set folder.
|
||||
/// </summary>
|
||||
void CreateSoundProfile(EmeraldSounds self)
|
||||
{
|
||||
string FilePath = EditorUtility.SaveFilePanelInProject("Save as Sound Profile", "", "asset", "Please enter a file name to save the file to");
|
||||
|
||||
if (string.IsNullOrEmpty(FilePath))
|
||||
{
|
||||
//For some reason, EditorUtility.SaveFilePanelInProject throws an incorrect EditorGUILayout error when it's used with some custom properties. This fixes it...
|
||||
CustomEditorProperties.BeginScriptHeader("", null);
|
||||
CustomEditorProperties.BeginFoldoutWindowBox();
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(AssetDatabase.AssetPathToGUID(FilePath)))
|
||||
{
|
||||
EmeraldSoundProfile NewSoundProfile = CreateInstance<EmeraldSoundProfile>();
|
||||
AssetDatabase.CreateAsset(NewSoundProfile, FilePath);
|
||||
self.SoundProfile = NewSoundProfile;
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
else
|
||||
{
|
||||
var ExistingSoundProfile = AssetDatabase.LoadAssetAtPath(FilePath, typeof(EmeraldSoundProfile));
|
||||
self.SoundProfile = (EmeraldSoundProfile)ExistingSoundProfile;
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
|
||||
//For some reason, EditorUtility.SaveFilePanelInProject throws an incorrect EditorGUILayout error when it's used with some custom properties. This fixes it...
|
||||
CustomEditorProperties.BeginScriptHeader("", null);
|
||||
CustomEditorProperties.BeginFoldoutWindowBox();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the current Sound Profile in a separate window so users can preview sounds while keeping a reference to the Sound Profile.
|
||||
/// </summary>
|
||||
void EditSoundProfile (EmeraldSounds self)
|
||||
{
|
||||
if (self.SoundProfile == null)
|
||||
return;
|
||||
|
||||
//Close the static reference to any other Sound Profile PropertyEditors before creating a new one
|
||||
if (EditorWindowRef != null && EditorWindowRef.name == "Sound Profile")
|
||||
EditorWindowRef.Close();
|
||||
|
||||
System.Type propertyEditorType = typeof(Editor).Assembly.GetType("UnityEditor.PropertyEditor");
|
||||
System.Type[] callTypes = new[] { typeof(Object), typeof(bool) };
|
||||
object[] callOpenBuffer = { null, true };
|
||||
|
||||
//Use reflection to create a PropertyEditor, as there's no API to do so before Unity 2021.2, and pass the Sound Profile to open it in a separate tab.
|
||||
MethodInfo openPropertyEditorInfo;
|
||||
openPropertyEditorInfo = propertyEditorType.GetMethod("OpenPropertyEditor",BindingFlags.Static | BindingFlags.NonPublic, null, callTypes, null);
|
||||
callOpenBuffer[0] = self.SoundProfile;
|
||||
openPropertyEditorInfo.Invoke(null, callOpenBuffer);
|
||||
|
||||
//Cache the PropertyEditor and name it Sound Profile (only one can be active at a time)
|
||||
EditorWindowRef = EditorWindow.GetWindow(typeof(Editor).Assembly.GetType("UnityEditor.PropertyEditor"));
|
||||
EditorWindowRef.name = "Sound Profile";
|
||||
EditorWindowRef.minSize = new Vector2(Screen.currentResolution.width / 4f, Screen.currentResolution.height / 2f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1691202b02595364dbd33f48b1abe0c5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,722 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using EmeraldAI.Utility;
|
||||
|
||||
namespace EmeraldAI
|
||||
{
|
||||
[HelpURL("https://black-horizon-studios.gitbook.io/emerald-ai-wiki/emerald-components-required/animation-component")]
|
||||
public class EmeraldAnimation : MonoBehaviour
|
||||
{
|
||||
#region Animation States
|
||||
public AnimatorStateInfo CurrentStateInfo;
|
||||
public bool InternalDodge; //In order to detect dodges between transitions, a custom bool is needed to avoid it being missed or playing while hit is playing.
|
||||
public bool InternalBlock; //In order to detect blocks between transitions, a custom bool is needed to avoid it being missed or playing while attack is playing.
|
||||
public bool InternalHit; //In order to detect hits between transitions, a custom bool is needed to avoid it being missed or playing while dodge is playing.
|
||||
public bool IsEmoting;
|
||||
public bool IsIdling;
|
||||
public bool IsAttacking;
|
||||
public bool IsStrafing;
|
||||
public bool IsBlocking;
|
||||
public bool IsDodging;
|
||||
public bool IsRecoiling;
|
||||
public bool IsStunned;
|
||||
public bool IsGettingHit;
|
||||
public bool IsEquipping;
|
||||
public bool IsBackingUp;
|
||||
public bool IsTurning;
|
||||
public bool IsTurningLeft, IsTurningRight;
|
||||
public bool IsSwitchingWeapons;
|
||||
public bool IsWarning;
|
||||
public bool IsMoving;
|
||||
public bool IsDead;
|
||||
public bool m_IdleAnimaionIndexOverride = false;
|
||||
#endregion
|
||||
|
||||
#region Animation Variables
|
||||
public EmeraldAI.Utility.AnimationProfile m_AnimationProfile;
|
||||
public bool AnimatorControllerGenerated = false;
|
||||
public bool AnimationListsChanged = false;
|
||||
public bool MissingRuntimeController = false;
|
||||
public bool AnimationsUpdated = false;
|
||||
public Animator AIAnimator;
|
||||
public bool AttackingTracker; //Called right when an attack is generated
|
||||
public bool AttackTriggered; //Briefly called while an attack is playing
|
||||
public bool WarningAnimationTriggered = false;
|
||||
public bool BusyBetweenStates = false;
|
||||
public AnimationStateTypes CurrentAnimationState = AnimationStateTypes.Idling;
|
||||
public delegate void GetHitHandler();
|
||||
public event GetHitHandler OnGetHit;
|
||||
public delegate void RecoilHandler();
|
||||
public event RecoilHandler OnRecoil;
|
||||
public delegate void StartAttackAnimationHandler();
|
||||
public event StartAttackAnimationHandler OnStartAttackAnimation;
|
||||
public delegate void EndAttackAnimationHandler();
|
||||
public event StartAttackAnimationHandler OnEndAttackAnimation;
|
||||
float LastHitTime;
|
||||
Coroutine StunnedCoroutine;
|
||||
EmeraldSystem EmeraldComponent;
|
||||
#endregion
|
||||
|
||||
#region Editor Variables
|
||||
public string[] Type1AttackEnumAnimations;
|
||||
public string[] Type2AttackEnumAnimations;
|
||||
public string[] Type1AttackBlankOptions = { "No Type 1 Attack Animations" };
|
||||
public string[] Type2AttackBlankOptions = { "No Type 2 Attack Animations" };
|
||||
public bool HideSettingsFoldout;
|
||||
public bool AnimationProfileFoldout;
|
||||
#endregion
|
||||
|
||||
void Start()
|
||||
{
|
||||
InitailizeAnimations();
|
||||
SetupAnimator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the Animation Component.
|
||||
/// </summary>
|
||||
void InitailizeAnimations()
|
||||
{
|
||||
AIAnimator = GetComponent<Animator>();
|
||||
AIAnimator.runtimeAnimatorController = m_AnimationProfile.AIAnimator;
|
||||
EmeraldComponent = GetComponent<EmeraldSystem>();
|
||||
|
||||
EmeraldComponent.HealthComponent.OnTakeDamage += PlayHitAnimation; //Subscribe to the OnTakeDamage event for Hit Animations
|
||||
EmeraldComponent.HealthComponent.OnTakeCritDamage += PlayHitAnimation; //Subscribe to the OnTakeCritDamage event for Hit Animations
|
||||
EmeraldComponent.HealthComponent.OnBlock += PlayHitAnimation; //Subscribe to the OnTakeCritDamage event for Block Hit Animations
|
||||
EmeraldComponent.HealthComponent.OnDeath += PlayDeathAnimation; //Subscribe to the OnDeath event for Death Animations
|
||||
EmeraldComponent.MovementComponent.OnReachedWaypoint += PlayIdleAnimation; //Subscribe to the OnReachedWaypoint event for Idle Animations
|
||||
EmeraldComponent.CombatComponent.OnExitCombat += ReturnToDefaultState; //Subscribe to the OnExitCombat event for ReturnToDefaultState
|
||||
AIAnimator.cullingMode = m_AnimationProfile.AnimatorCullingMode;
|
||||
|
||||
InitializeWeaponTypeAnimationAndSettings();
|
||||
AIAnimator.updateMode = AnimatorUpdateMode.Normal;
|
||||
AIAnimator.SetFloat("Offset", Random.Range(0.0f, 1.0f)); //Add a randomized offset so AI sharing animations don't start at the exact same frame.
|
||||
}
|
||||
|
||||
public void AnimationUpdate ()
|
||||
{
|
||||
EmeraldComponent.AnimationComponent.CurrentStateInfo = AIAnimator.GetCurrentAnimatorStateInfo(0); //Update the CurrentStateInfo, which is used for getting the current state and tracking the AI's current states and animations.
|
||||
CheckAnimationStates(); //Keeps track of the current animation state.
|
||||
|
||||
//While simple, this is needed to check when the current attack animation finishes. When it does, generate a new attack.
|
||||
if (IsAttacking && !AttackingTracker)
|
||||
{
|
||||
OnStartAttackAnimation?.Invoke();
|
||||
AttackingTracker = true;
|
||||
AttackTriggered = true;
|
||||
Invoke(nameof(StopAttackTrigger), 0.5f);
|
||||
}
|
||||
else if (!IsAttacking && AttackingTracker)
|
||||
{
|
||||
OnEndAttackAnimation?.Invoke();
|
||||
EmeraldCombatManager.GenerateNextAttack(EmeraldComponent); //Generate the next attack once the current one has concluded.
|
||||
}
|
||||
|
||||
//Set AttackTriggered to false if an priority state is triggered.
|
||||
//This can happen if an attack was generated, but another state gets set active before it could trigger through the Animator.
|
||||
if (AttackTriggered)
|
||||
{
|
||||
if (IsMoving || IsTurning || IsStunned || IsStrafing || IsBackingUp || IsBlocking || IsDodging || InternalHit || !AttackingTracker) AttackTriggered = false;
|
||||
}
|
||||
}
|
||||
|
||||
void StopAttackTrigger()
|
||||
{
|
||||
AttackTriggered = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Keeps track of the current animation state.
|
||||
/// </summary>
|
||||
public void CheckAnimationStates()
|
||||
{
|
||||
if (!EmeraldComponent.CombatComponent.CombatState) IsIdling = CurrentStateInfo.IsName("Movement") && AIAnimator.GetFloat("Speed") < 0.1f && !IsBackingUp || CurrentStateInfo.IsTag("Idle");
|
||||
if (!EmeraldComponent.CombatComponent.CombatState) IsMoving = CurrentStateInfo.IsName("Movement") && AIAnimator.GetFloat("Speed") >= 0.1f && !IsBackingUp;
|
||||
|
||||
if (EmeraldComponent.CombatComponent.CombatState && EmeraldComponent.CombatComponent.CurrentWeaponType == EmeraldCombat.WeaponTypes.Type1)
|
||||
{
|
||||
IsIdling = CurrentStateInfo.IsName("Combat Movement (Type 1)") && AIAnimator.GetFloat("Speed") < 0.1f;
|
||||
IsMoving = CurrentStateInfo.IsName("Combat Movement (Type 1)") && AIAnimator.GetFloat("Speed") >= 0.1f && !IsBackingUp && !IsAttacking;
|
||||
}
|
||||
else if (EmeraldComponent.CombatComponent.CombatState && EmeraldComponent.CombatComponent.CurrentWeaponType == EmeraldCombat.WeaponTypes.Type2)
|
||||
{
|
||||
IsIdling = CurrentStateInfo.IsName("Combat Movement (Type 2)") && AIAnimator.GetFloat("Speed") < 0.1f;
|
||||
IsMoving = CurrentStateInfo.IsName("Combat Movement (Type 2)") && AIAnimator.GetFloat("Speed") >= 0.1f && !IsBackingUp && !IsAttacking;
|
||||
}
|
||||
|
||||
IsEquipping = CurrentStateInfo.IsTag("Equip");
|
||||
IsBlocking = CurrentStateInfo.IsTag("Block");
|
||||
IsRecoiling = CurrentStateInfo.IsTag("Recoil");
|
||||
IsStunned = CurrentStateInfo.IsTag("Stunned");
|
||||
IsStrafing = CurrentStateInfo.IsTag("Strafing");
|
||||
IsDodging = CurrentStateInfo.IsTag("Dodging") || InternalDodge;
|
||||
IsBackingUp = CurrentStateInfo.IsTag("Backing Up") || AIAnimator.GetBool("Walk Backwards");
|
||||
IsAttacking = CurrentStateInfo.IsTag("Attack");
|
||||
IsGettingHit = CurrentStateInfo.IsTag("Hit");
|
||||
IsWarning = CurrentStateInfo.IsTag("Warning");
|
||||
IsEmoting = CurrentStateInfo.IsTag("Emote");
|
||||
|
||||
//This is used to determine when an AI is in between combat and non-combat states. This stops undsired rotations that happens during these transitions and allows the mechanics to function much smoother.
|
||||
BusyBetweenStates = AIAnimator.GetAnimatorTransitionInfo(0).IsName("Combat Movement (Type 1) -> Movement") || AIAnimator.GetAnimatorTransitionInfo(0).IsName("Combat Movement (Type 2) -> Movement") ||
|
||||
AIAnimator.GetAnimatorTransitionInfo(0).IsName("Movement -> Combat Movement (Type 1)") || AIAnimator.GetAnimatorTransitionInfo(0).IsName("Movement -> Combat Movement (Type 2)") ||
|
||||
AIAnimator.GetAnimatorTransitionInfo(0).IsName("Combat Movement (Type 1) -> Put Away Weapon (Type 1)") || AIAnimator.GetAnimatorTransitionInfo(0).IsName("Combat Movement (Type 2) -> Put Away Weapon (Type 2)") ||
|
||||
AIAnimator.GetAnimatorTransitionInfo(0).IsName("Put Away Weapon (Type 1) -> Movement") || AIAnimator.GetAnimatorTransitionInfo(0).IsName("Put Away Weapon (Type 2) -> Movement");
|
||||
|
||||
if (IsIdling) CurrentAnimationState = AnimationStateTypes.Idling;
|
||||
if (IsMoving) CurrentAnimationState = AnimationStateTypes.Moving;
|
||||
if (IsTurningLeft) CurrentAnimationState = AnimationStateTypes.TurningLeft;
|
||||
if (IsTurningRight) CurrentAnimationState = AnimationStateTypes.TurningRight;
|
||||
if (IsEquipping) CurrentAnimationState = AnimationStateTypes.Equipping;
|
||||
if (IsBlocking) CurrentAnimationState = AnimationStateTypes.Blocking;
|
||||
if (IsRecoiling) CurrentAnimationState = AnimationStateTypes.Recoiling;
|
||||
if (IsStunned) CurrentAnimationState = AnimationStateTypes.Stunned;
|
||||
if (IsStrafing) CurrentAnimationState = AnimationStateTypes.Strafing;
|
||||
if (IsDodging) CurrentAnimationState = AnimationStateTypes.Dodging;
|
||||
if (IsBackingUp) CurrentAnimationState = AnimationStateTypes.BackingUp;
|
||||
if (IsAttacking) CurrentAnimationState = AnimationStateTypes.Attacking;
|
||||
if (IsGettingHit) CurrentAnimationState = AnimationStateTypes.GettingHit;
|
||||
if (IsDead) CurrentAnimationState = AnimationStateTypes.Dead;
|
||||
if (IsEmoting) CurrentAnimationState = AnimationStateTypes.Emoting;
|
||||
if (IsSwitchingWeapons) CurrentAnimationState = AnimationStateTypes.SwitchingWeapons;
|
||||
}
|
||||
|
||||
public void ResetSettings()
|
||||
{
|
||||
//Reapply the AI's Animator Controller settings applied on Start because, when the
|
||||
//Animator Controller is disabled, they're reset to their default settings.
|
||||
SetWeaponTypeAnimationState();
|
||||
AIAnimator.SetBool("Idle Active", false);
|
||||
InitializeWeaponTypeAnimationAndSettings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up all Animator related settings.
|
||||
/// </summary>
|
||||
public void SetupAnimator ()
|
||||
{
|
||||
AIAnimator = GetComponent<Animator>();
|
||||
EmeraldComponent = GetComponent<EmeraldSystem>(); ;
|
||||
|
||||
if (AIAnimator.layerCount >= 2)
|
||||
AIAnimator.SetLayerWeight(1, 1);
|
||||
|
||||
if (GetComponent<EmeraldMovement>().MovementType == EmeraldMovement.MovementTypes.RootMotion)
|
||||
{
|
||||
EmeraldComponent.m_NavMeshAgent.speed = 0;
|
||||
AIAnimator.applyRootMotion = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
AIAnimator.applyRootMotion = false;
|
||||
}
|
||||
|
||||
if (AIAnimator.layerCount >= 2)
|
||||
{
|
||||
AIAnimator.SetLayerWeight(1, 1);
|
||||
}
|
||||
|
||||
SetWeaponTypeAnimationState();
|
||||
|
||||
AIAnimator.SetInteger("Idle Index", Random.Range(0, m_AnimationProfile.NonCombatAnimations.IdleList.Count));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up all Animator related weapon type settings.
|
||||
/// </summary>
|
||||
public void InitializeWeaponTypeAnimationAndSettings()
|
||||
{
|
||||
if (EmeraldComponent.CombatComponent.WeaponTypeAmount == EmeraldCombat.WeaponTypeAmounts.Two)
|
||||
{
|
||||
if (EmeraldComponent.CombatComponent.StartingWeaponType == EmeraldCombat.WeaponTypes.Type1)
|
||||
{
|
||||
AIAnimator.SetInteger("Weapon Type State", 1);
|
||||
EmeraldComponent.CombatComponent.CurrentWeaponType = EmeraldCombat.WeaponTypes.Type1;
|
||||
EmeraldComponent.DetectionComponent.PickTargetType = EmeraldComponent.CombatComponent.Type1PickTargetType;
|
||||
}
|
||||
else if (EmeraldComponent.CombatComponent.StartingWeaponType == EmeraldCombat.WeaponTypes.Type2)
|
||||
{
|
||||
AIAnimator.SetInteger("Weapon Type State", 2);
|
||||
EmeraldComponent.CombatComponent.CurrentWeaponType = EmeraldCombat.WeaponTypes.Type2;
|
||||
EmeraldComponent.DetectionComponent.PickTargetType = EmeraldComponent.CombatComponent.Type2PickTargetType;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AIAnimator.SetInteger("Weapon Type State", 1);
|
||||
EmeraldComponent.CombatComponent.CurrentWeaponType = EmeraldCombat.WeaponTypes.Type1;
|
||||
EmeraldComponent.DetectionComponent.PickTargetType = EmeraldComponent.CombatComponent.Type1PickTargetType;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Controls whether or not Animate Weapon State will be enabled (depending on if the user has applied the equipping and uneuipping animations).
|
||||
/// </summary>
|
||||
void SetWeaponTypeAnimationState ()
|
||||
{
|
||||
if (EmeraldComponent.CombatComponent.WeaponTypeAmount == EmeraldCombat.WeaponTypeAmounts.One)
|
||||
{
|
||||
if (m_AnimationProfile.Type1Animations.PutAwayWeapon.AnimationClip == null || m_AnimationProfile.Type1Animations.PullOutWeapon.AnimationClip == null)
|
||||
AIAnimator.SetBool("Animate Weapon State", false);
|
||||
else if (m_AnimationProfile.Type1Animations.PutAwayWeapon.AnimationClip != null && m_AnimationProfile.Type1Animations.PullOutWeapon.AnimationClip != null)
|
||||
AIAnimator.SetBool("Animate Weapon State", true);
|
||||
}
|
||||
else if (EmeraldComponent.CombatComponent.WeaponTypeAmount == EmeraldCombat.WeaponTypeAmounts.Two)
|
||||
{
|
||||
if (m_AnimationProfile.Type1Animations.PutAwayWeapon.AnimationClip == null && m_AnimationProfile.Type1Animations.PullOutWeapon.AnimationClip == null &&
|
||||
m_AnimationProfile.Type2Animations.PutAwayWeapon.AnimationClip == null && m_AnimationProfile.Type2Animations.PullOutWeapon.AnimationClip == null)
|
||||
AIAnimator.SetBool("Animate Weapon State", false);
|
||||
else if (m_AnimationProfile.Type1Animations.PutAwayWeapon.AnimationClip != null && m_AnimationProfile.Type1Animations.PullOutWeapon.AnimationClip != null &&
|
||||
m_AnimationProfile.Type2Animations.PutAwayWeapon.AnimationClip != null && m_AnimationProfile.Type2Animations.PullOutWeapon.AnimationClip != null)
|
||||
AIAnimator.SetBool("Animate Weapon State", true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a random idle animation index and plays an idle animation.
|
||||
/// </summary>
|
||||
public void PlayIdleAnimation ()
|
||||
{
|
||||
if (!EmeraldComponent.AnimationComponent.m_IdleAnimaionIndexOverride && m_AnimationProfile.NonCombatAnimations.IdleList.Count > 0 &&
|
||||
EmeraldComponent.MovementComponent.WaypointType != EmeraldMovement.WaypointTypes.Loop)
|
||||
{
|
||||
AIAnimator.SetInteger("Idle Index", Random.Range(1, m_AnimationProfile.NonCombatAnimations.IdleList.Count+1));
|
||||
AIAnimator.SetBool("Idle Active", true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays an AI's Warning animation for the current Weapon Type.
|
||||
/// </summary>
|
||||
public void PlayWarningAnimation ()
|
||||
{
|
||||
if (EmeraldComponent.CombatComponent.CurrentWeaponType == EmeraldCombat.WeaponTypes.Type1 && m_AnimationProfile.Type1Animations.IdleWarning.AnimationClip == null ||
|
||||
EmeraldComponent.CombatComponent.CurrentWeaponType == EmeraldCombat.WeaponTypes.Type2 && m_AnimationProfile.Type2Animations.IdleWarning.AnimationClip == null ||
|
||||
WarningAnimationTriggered)
|
||||
return;
|
||||
|
||||
AIAnimator.SetTrigger("Warning");
|
||||
WarningAnimationTriggered = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a stunned animation depending on the StunnedLength.
|
||||
/// </summary>
|
||||
public void PlayStunnedAnimation (float StunnedLength)
|
||||
{
|
||||
if (EmeraldComponent.CombatComponent.CurrentWeaponType == EmeraldCombat.WeaponTypes.Type1 && m_AnimationProfile.Type1Animations.Stunned.AnimationClip == null ||
|
||||
EmeraldComponent.CombatComponent.CurrentWeaponType == EmeraldCombat.WeaponTypes.Type2 && m_AnimationProfile.Type2Animations.Stunned.AnimationClip == null) return;
|
||||
|
||||
if (!IsStunned && !AIAnimator.GetBool("Blocking") && !AIAnimator.GetBool("Dodge Triggered") && !IsDodging && transform.localScale != Vector3.one * 0.003f)
|
||||
{
|
||||
if (StunnedCoroutine != null) StopCoroutine(StunnedCoroutine);
|
||||
StunnedCoroutine = StartCoroutine(SetStunned(StunnedLength));
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator SetStunned(float StunnedLength)
|
||||
{
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
if (IsDodging || IsDead) yield break; //If this AI is doding or is dead, don't trigger a stun.
|
||||
AIAnimator.SetBool("Stunned Active", true);
|
||||
yield return new WaitForSeconds(StunnedLength);
|
||||
AIAnimator.SetBool("Stunned Active", false);
|
||||
EmeraldComponent.BehaviorsComponent.IsAiming = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a random death animation from the AI's DeathList. If either DeathList is empty, it is assumed ragdoll deaths are being used.
|
||||
/// </summary>
|
||||
public void PlayDeathAnimation ()
|
||||
{
|
||||
//Only play a death animation if the current weapon type death animation lists have animations in them.
|
||||
if (EmeraldComponent.CombatComponent.CurrentWeaponType == EmeraldCombat.WeaponTypes.Type1 && m_AnimationProfile.Type1Animations.DeathList.Count == 0 ||
|
||||
EmeraldComponent.CombatComponent.CurrentWeaponType == EmeraldCombat.WeaponTypes.Type2 && m_AnimationProfile.Type2Animations.DeathList.Count == 0)
|
||||
return;
|
||||
|
||||
if (EmeraldComponent.CombatComponent.CurrentWeaponType == EmeraldCombat.WeaponTypes.Type1)
|
||||
{
|
||||
AIAnimator.SetInteger("Death Index", Random.Range(1, m_AnimationProfile.Type1Animations.DeathList.Count + 1));
|
||||
int DeathIndex = AIAnimator.GetInteger("Death Index");
|
||||
StartCoroutine(DisableAnimator(m_AnimationProfile.Type1Animations.DeathList[DeathIndex-1].AnimationClip.length / m_AnimationProfile.Type1Animations.DeathList[DeathIndex - 1].AnimationSpeed));
|
||||
}
|
||||
else if (EmeraldComponent.CombatComponent.CurrentWeaponType == EmeraldCombat.WeaponTypes.Type2)
|
||||
{
|
||||
AIAnimator.SetInteger("Death Index", Random.Range(1, m_AnimationProfile.Type2Animations.DeathList.Count + 1));
|
||||
int DeathIndex = AIAnimator.GetInteger("Death Index");
|
||||
StartCoroutine(DisableAnimator(m_AnimationProfile.Type2Animations.DeathList[DeathIndex-1].AnimationClip.length / m_AnimationProfile.Type2Animations.DeathList[DeathIndex - 1].AnimationSpeed));
|
||||
}
|
||||
|
||||
AIAnimator.SetTrigger("Dead");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a hit animation depending on the user set Animation State Conditions.
|
||||
/// </summary>
|
||||
public void PlayHitAnimation ()
|
||||
{
|
||||
//Get the current hit animation cooldown depending on the weapon type.
|
||||
float CurrentHitAnimationCooldown = EmeraldComponent.CombatComponent.CurrentWeaponType == EmeraldCombat.WeaponTypes.Type1 ? m_AnimationProfile.Type1HitAnimationCooldown : m_AnimationProfile.Type2HitAnimationCooldown;
|
||||
|
||||
//Don't play a hit animation if an AI is dead or if the CurrentHitAnimationCooldown hasn't passed.
|
||||
if (EmeraldComponent.HealthComponent.CurrentHealth <= 0 || Time.time < (LastHitTime + CurrentHitAnimationCooldown))
|
||||
return;
|
||||
|
||||
LastHitTime = Time.time;
|
||||
|
||||
if (!EmeraldComponent.CombatComponent.CombatState)
|
||||
{
|
||||
if (m_AnimationProfile.NonCombatAnimations.HitList.Count == 0 && !IsBlocking)
|
||||
return;
|
||||
|
||||
int CurrentIndex = AIAnimator.GetInteger("Hit Index");
|
||||
AIAnimator.SetInteger("Hit Index", CurrentIndex);
|
||||
CurrentIndex++;
|
||||
if (CurrentIndex == m_AnimationProfile.NonCombatAnimations.HitList.Count + 1) CurrentIndex = 1;
|
||||
AIAnimator.SetInteger("Hit Index", CurrentIndex);
|
||||
}
|
||||
else if (EmeraldComponent.CombatComponent.CombatState && !IsBlocking)
|
||||
{
|
||||
if (EmeraldComponent.CombatComponent.CurrentWeaponType == EmeraldCombat.WeaponTypes.Type1)
|
||||
{
|
||||
if (m_AnimationProfile.Type1Animations.HitList.Count == 0 && !IsBlocking)
|
||||
return;
|
||||
|
||||
int CurrentIndex = AIAnimator.GetInteger("Hit Index");
|
||||
AIAnimator.SetInteger("Hit Index", CurrentIndex);
|
||||
CurrentIndex++;
|
||||
if (CurrentIndex >= m_AnimationProfile.Type1Animations.HitList.Count + 1) CurrentIndex = 1;
|
||||
AIAnimator.SetInteger("Hit Index", CurrentIndex);
|
||||
}
|
||||
else if (EmeraldComponent.CombatComponent.CurrentWeaponType == EmeraldCombat.WeaponTypes.Type2)
|
||||
{
|
||||
if (m_AnimationProfile.Type2Animations.HitList.Count == 0 && !IsBlocking)
|
||||
return;
|
||||
|
||||
int CurrentIndex = AIAnimator.GetInteger("Hit Index");
|
||||
AIAnimator.SetInteger("Hit Index", CurrentIndex);
|
||||
CurrentIndex++;
|
||||
if (CurrentIndex >= m_AnimationProfile.Type2Animations.HitList.Count + 1) CurrentIndex = 1;
|
||||
AIAnimator.SetInteger("Hit Index", CurrentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
//This helps cancel block if an AI doesn't block in time
|
||||
if (!IsBlocking && AIAnimator.GetBool("Blocking"))
|
||||
{
|
||||
AIAnimator.SetBool("Blocking", false);
|
||||
}
|
||||
|
||||
//Only play a hit animation if the conditions are right. Some states are automatically excluded.
|
||||
if (!IsDodging && !IsSwitchingWeapons && !IsEquipping && !AIAnimator.GetBool("Dodge Triggered") && EmeraldComponent.HealthComponent.CurrentActiveEffects.Count == 0)
|
||||
{
|
||||
var Type1Conditions = (((int)m_AnimationProfile.Type1HitConditions) & ((int)CurrentAnimationState)) != 0;
|
||||
var Type2Conditions = (((int)m_AnimationProfile.Type2HitConditions) & ((int)CurrentAnimationState)) != 0;
|
||||
|
||||
if (Type1Conditions && EmeraldComponent.CombatComponent.CurrentWeaponType == EmeraldCombat.WeaponTypes.Type1 || Type2Conditions && EmeraldComponent.CombatComponent.CurrentWeaponType == EmeraldCombat.WeaponTypes.Type2)
|
||||
{
|
||||
//In order to bypass issues between frames and triggers, use an internal bool to determine when an AI is hit.
|
||||
//If an AI was hit within 0.5 seconds, an AI will ignore a dodge if it was triggered.
|
||||
InternalHit = true;
|
||||
Invoke(nameof(ResetInternalHit), 0.5f);
|
||||
AttackTriggered = false;
|
||||
|
||||
AIAnimator.SetTrigger("Hit");
|
||||
OnGetHit?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
AIAnimator.ResetTrigger("Attack");
|
||||
}
|
||||
|
||||
void ResetInternalHit()
|
||||
{
|
||||
InternalHit = false;
|
||||
AttackTriggered = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays an attack animation depending on the EmeraldComponent.CombatComponent.CurrentAnimationIndex.
|
||||
/// </summary>
|
||||
public void PlayAttackAnimation ()
|
||||
{
|
||||
if (!EmeraldComponent.CombatComponent.CurrentAttackData.CooldownIgnored) EmeraldComponent.CombatComponent.CurrentAttackData.CooldownTimeStamp = Time.time;
|
||||
|
||||
AIAnimator.SetInteger("Attack Index", EmeraldComponent.CombatComponent.CurrentAnimationIndex + 1);
|
||||
AIAnimator.SetTrigger("Attack");
|
||||
AttackTriggered = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates which turn animations to use while stationary.
|
||||
/// </summary>
|
||||
public void CalculateTurnAnimations(bool ByPassConditions = false)
|
||||
{
|
||||
//Lock turning, as soon as the angle threshold is met, for 1 second. This prevents an AI from getting stuck transitioning between two turning animations.
|
||||
if (!EmeraldComponent.CombatComponent.CombatState && EmeraldComponent.MovementComponent.DestinationAdjustedAngle <= EmeraldComponent.MovementComponent.AngleToTurn && !EmeraldComponent.MovementComponent.LockTurning)
|
||||
{
|
||||
EmeraldComponent.MovementComponent.LockTurning = true;
|
||||
StartCoroutine(LockTurns());
|
||||
DisableTurning();
|
||||
}
|
||||
|
||||
Vector3 DestinationDirection = EmeraldComponent.MovementComponent.DestinationDirection;
|
||||
|
||||
if (ByPassConditions || CanPlayTurningAnimation(DestinationDirection))
|
||||
{
|
||||
if (Time.timeSinceLevelLoad < 1f || EmeraldComponent.MovementComponent.LockTurning && !EmeraldComponent.CombatComponent.CombatState || IsBackingUp)
|
||||
return;
|
||||
|
||||
Vector3 cross = Vector3.Cross(transform.forward, Quaternion.LookRotation(DestinationDirection, Vector3.up) * Vector3.forward);
|
||||
|
||||
if (cross.y > 0.0f) //Right
|
||||
{
|
||||
EmeraldComponent.AnimationComponent.IsTurning = true;
|
||||
EmeraldComponent.AnimationComponent.IsTurningRight = true;
|
||||
EmeraldComponent.AnimationComponent.IsTurningLeft = false;
|
||||
AIAnimator.SetBool("Idle Active", false);
|
||||
AIAnimator.SetBool("Turn Right", true);
|
||||
AIAnimator.SetBool("Turn Left", false);
|
||||
}
|
||||
else if (cross.y < 0.0f) //Left
|
||||
{
|
||||
EmeraldComponent.AnimationComponent.IsTurning = true;
|
||||
EmeraldComponent.AnimationComponent.IsTurningLeft = true;
|
||||
EmeraldComponent.AnimationComponent.IsTurningRight = false;
|
||||
AIAnimator.SetBool("Idle Active", false);
|
||||
AIAnimator.SetBool("Turn Left", true);
|
||||
AIAnimator.SetBool("Turn Right", false);
|
||||
}
|
||||
|
||||
}
|
||||
else if (EmeraldComponent.CombatComponent.CombatState)
|
||||
{
|
||||
EmeraldComponent.AnimationComponent.IsTurning = false;
|
||||
EmeraldComponent.AnimationComponent.IsTurningLeft = false;
|
||||
EmeraldComponent.AnimationComponent.IsTurningRight = false;
|
||||
AIAnimator.SetBool("Turn Left", false);
|
||||
AIAnimator.SetBool("Turn Right", false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if a turn animation can be played.
|
||||
/// </summary>
|
||||
bool CanPlayTurningAnimation (Vector3 DestinationDirection)
|
||||
{
|
||||
if (!EmeraldComponent.CombatComponent.CombatState)
|
||||
{
|
||||
return EmeraldComponent.MovementComponent.DestinationAdjustedAngle >= EmeraldComponent.MovementComponent.AngleToTurn && DestinationDirection != Vector3.zero &&
|
||||
EmeraldComponent.MovementComponent.AIAgentActive && EmeraldComponent.m_NavMeshAgent.remainingDistance > EmeraldComponent.m_NavMeshAgent.stoppingDistance;
|
||||
}
|
||||
else
|
||||
{
|
||||
return !EmeraldComponent.CombatComponent.DeathDelayActive && EmeraldComponent.MovementComponent.DestinationAdjustedAngle >= EmeraldComponent.MovementComponent.AngleToTurn && DestinationDirection != Vector3.zero &&
|
||||
EmeraldComponent.MovementComponent.AIAgentActive && !IsAttacking && !IsBlocking && !IsGettingHit && !IsRecoiling && !IsStrafing && !IsDodging && !IsStunned && !IsSwitchingWeapons && !IsEquipping;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lock turning, as soon as the angle threshold is met, for 1 second. This prevents an AI from getting stuck transitioning between two turning animations.
|
||||
/// </summary>
|
||||
IEnumerator LockTurns()
|
||||
{
|
||||
yield return new WaitForSeconds(1f);
|
||||
EmeraldComponent.MovementComponent.LockTurning = false;
|
||||
}
|
||||
|
||||
void DisableTurning()
|
||||
{
|
||||
IsTurning = false;
|
||||
IsTurningLeft = false;
|
||||
IsTurningRight = false;
|
||||
AIAnimator.SetBool("Turn Right", false);
|
||||
AIAnimator.SetBool("Turn Left", false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a recoil animation when an AI is attacking and their target blocks.
|
||||
/// </summary>
|
||||
public void PlayRecoilAnimation ()
|
||||
{
|
||||
if (EmeraldComponent.CombatComponent.CurrentWeaponType == EmeraldCombat.WeaponTypes.Type1 && m_AnimationProfile.Type1Animations.Recoil.AnimationClip == null ||
|
||||
EmeraldComponent.CombatComponent.CurrentWeaponType == EmeraldCombat.WeaponTypes.Type2 && m_AnimationProfile.Type2Animations.Recoil.AnimationClip == null) return;
|
||||
|
||||
if (EmeraldComponent.CombatTarget != null && EmeraldComponent.CurrentTargetInfo != null && EmeraldComponent.CurrentTargetInfo.CurrentICombat.IsBlocking())
|
||||
{
|
||||
AIAnimator.ResetTrigger("Attack");
|
||||
AIAnimator.SetTrigger("Recoil");
|
||||
OnRecoil?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Strafe State according to the passed state bool parameter.
|
||||
/// </summary>
|
||||
public void SetStrafeState (bool State)
|
||||
{
|
||||
int Direction = AIAnimator.GetInteger("Strafe Direction");
|
||||
if (State) Direction = Random.Range(0, 2); //Only change the strafe direction if setting the strafe state to true.
|
||||
|
||||
if (EmeraldComponent.CombatComponent.CurrentWeaponType == EmeraldCombat.WeaponTypes.Type1)
|
||||
{
|
||||
if (Direction == 0 && m_AnimationProfile.Type1Animations.StrafeLeft.AnimationClip == null) return;
|
||||
if (Direction == 1 && m_AnimationProfile.Type1Animations.StrafeRight.AnimationClip == null) return;
|
||||
}
|
||||
else if(EmeraldComponent.CombatComponent.CurrentWeaponType == EmeraldCombat.WeaponTypes.Type1)
|
||||
{
|
||||
if (Direction == 0 && m_AnimationProfile.Type2Animations.StrafeLeft.AnimationClip == null) return;
|
||||
if (Direction == 1 && m_AnimationProfile.Type2Animations.StrafeRight.AnimationClip == null) return;
|
||||
}
|
||||
|
||||
AIAnimator.SetBool("Strafe Active", State);
|
||||
if (State) AIAnimator.SetInteger("Strafe Direction", Direction);
|
||||
if (State) AIAnimator.SetTrigger("Strafing Triggered");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Strafe State according to the passed state bool parameter.
|
||||
/// </summary>
|
||||
public void TriggerDodgeState()
|
||||
{
|
||||
int Direction = Random.Range(0, 3);
|
||||
|
||||
//Override the dodge direction to be equal to the strafe direction, if strafing is active during a dodge.
|
||||
if (IsStrafing || AIAnimator.GetBool("Strafe Active"))
|
||||
{
|
||||
int StrafeDirection = AIAnimator.GetInteger("Strafe Direction");
|
||||
if (StrafeDirection == 0) Direction = 0;
|
||||
if (StrafeDirection == 1) Direction = 2;
|
||||
}
|
||||
/*
|
||||
//Override the dodge direction to be backwards, if the AI is currently backing up during a dodge.
|
||||
else if (IsBackingUp)
|
||||
{
|
||||
Direction = 1;
|
||||
}
|
||||
*/
|
||||
|
||||
//Return if the chosen dodge animation is empty.
|
||||
if (EmeraldComponent.CombatComponent.CurrentWeaponType == EmeraldCombat.WeaponTypes.Type1)
|
||||
{
|
||||
if (Direction == 0 && m_AnimationProfile.Type1Animations.DodgeLeft.AnimationClip == null) return;
|
||||
if (Direction == 1 && m_AnimationProfile.Type1Animations.DodgeBack.AnimationClip == null) return;
|
||||
if (Direction == 2 && m_AnimationProfile.Type1Animations.DodgeRight.AnimationClip == null) return;
|
||||
}
|
||||
else if(EmeraldComponent.CombatComponent.CurrentWeaponType == EmeraldCombat.WeaponTypes.Type2)
|
||||
{
|
||||
if (Direction == 0 && m_AnimationProfile.Type2Animations.DodgeLeft.AnimationClip == null) return;
|
||||
if (Direction == 1 && m_AnimationProfile.Type2Animations.DodgeBack.AnimationClip == null) return;
|
||||
if (Direction == 2 && m_AnimationProfile.Type2Animations.DodgeRight.AnimationClip == null) return;
|
||||
}
|
||||
|
||||
AIAnimator.SetInteger("Dodge Direction", Direction);
|
||||
AIAnimator.SetTrigger("Dodge Triggered");
|
||||
AIAnimator.SetBool("Walk Backwards", false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Block State according to the passed state bool parameter.
|
||||
/// </summary>
|
||||
public void PlayBlockAnimation (bool State)
|
||||
{
|
||||
if (EmeraldComponent.CombatComponent.CurrentWeaponType == EmeraldCombat.WeaponTypes.Type1 && m_AnimationProfile.Type1Animations.BlockIdle.AnimationClip == null ||
|
||||
EmeraldComponent.CombatComponent.CurrentWeaponType == EmeraldCombat.WeaponTypes.Type2 && m_AnimationProfile.Type2Animations.BlockIdle.AnimationClip == null) return;
|
||||
AIAnimator.SetBool("Blocking", State);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets all deafult Animator values before initializing another action. This is to prevent multiple triggers being active at once, which can cause actions to be missed or skipped.
|
||||
/// </summary>
|
||||
public void ResetTriggers(float Delay)
|
||||
{
|
||||
StartCoroutine(ResetTriggersInternal(Delay));
|
||||
}
|
||||
|
||||
IEnumerator ResetTriggersInternal(float Delay)
|
||||
{
|
||||
yield return new WaitForSeconds(Delay);
|
||||
EmeraldComponent.AIAnimator.ResetTrigger("Hit");
|
||||
EmeraldComponent.AIAnimator.ResetTrigger("Attack");
|
||||
EmeraldComponent.AIAnimator.SetBool("Blocking", false);
|
||||
EmeraldComponent.AIAnimator.ResetTrigger("Dodge Triggered");
|
||||
EmeraldComponent.AIAnimator.ResetTrigger("Strafing Triggered");
|
||||
EmeraldComponent.AIAnimator.SetBool("Strafe Active", false);
|
||||
EmeraldComponent.AIAnimator.ResetTrigger("Attack Cancelled");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the Animator back to its default (non-combat) state. This is called through the OnExitCombat callback.
|
||||
/// </summary>
|
||||
void ReturnToDefaultState ()
|
||||
{
|
||||
EmeraldComponent.AIAnimator.SetBool("Combat State Active", false);
|
||||
EmeraldComponent.AnimationComponent.WarningAnimationTriggered = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays an emote animation according to the Animation Clip parameter.
|
||||
/// </summary>
|
||||
public void PlayEmoteAnimation(int EmoteAnimationID)
|
||||
{
|
||||
//Look through each animation in the EmoteAnimationList for the appropriate ID.
|
||||
//Once found, play the animaition of the same index as the found ID.
|
||||
for (int i = 0; i < m_AnimationProfile.EmoteAnimationList.Count; i++)
|
||||
{
|
||||
if (m_AnimationProfile.EmoteAnimationList[i].AnimationID == EmoteAnimationID)
|
||||
{
|
||||
AIAnimator.SetInteger("Emote Index", EmoteAnimationID);
|
||||
AIAnimator.SetTrigger("Emote Trigger");
|
||||
IsMoving = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loops an emote animation according to the Animation Clip parameter until it is called to stop. Note: If you are using this during combat,
|
||||
/// it is important that you handle canceling it or the AI will not be able to return to combat.
|
||||
/// </summary>
|
||||
public void LoopEmoteAnimation(int EmoteAnimationID)
|
||||
{
|
||||
//Look through each animation in the EmoteAnimationList for the appropriate ID.
|
||||
//Once found, play the animaition of the same index as the found ID.
|
||||
for (int i = 0; i < m_AnimationProfile.EmoteAnimationList.Count; i++)
|
||||
{
|
||||
if (m_AnimationProfile.EmoteAnimationList[i].AnimationID == EmoteAnimationID)
|
||||
{
|
||||
AIAnimator.SetInteger("Emote Index", EmoteAnimationID);
|
||||
AIAnimator.SetBool("Emote Loop", true);
|
||||
IsMoving = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loops an emote animation according to the Animation Clip parameter until it is called to stop.
|
||||
/// </summary>
|
||||
public void StopLoopEmoteAnimation(int EmoteAnimationID)
|
||||
{
|
||||
//Look through each animation in the EmoteAnimationList for the appropriate ID.
|
||||
//Once found, play the animaition of the same index as the found ID.
|
||||
for (int i = 0; i < m_AnimationProfile.EmoteAnimationList.Count; i++)
|
||||
{
|
||||
if (m_AnimationProfile.EmoteAnimationList[i].AnimationID == EmoteAnimationID)
|
||||
{
|
||||
AIAnimator.SetInteger("Emote Index", EmoteAnimationID);
|
||||
AIAnimator.SetBool("Emote Loop", false);
|
||||
IsMoving = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delays the call to disable the Emerald AI components until after the death animation has finished playing.
|
||||
/// </summary>
|
||||
IEnumerator DisableAnimator(float AnimationLength)
|
||||
{
|
||||
yield return new WaitForSeconds(AnimationLength);
|
||||
EmeraldComponent.AIAnimator.enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 871be6a68bd875f49b5587968e1ac56c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,368 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using EmeraldAI.Utility;
|
||||
|
||||
namespace EmeraldAI
|
||||
{
|
||||
/// <summary>
|
||||
/// This script handles all of Emerald AI's behaviors and states. Most functions can be overridden to create custom behaviors or functionality.
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
[HelpURL("https://black-horizon-studios.gitbook.io/emerald-ai-wiki/emerald-components-required/behaviors-component")]
|
||||
public class EmeraldBehaviors : MonoBehaviour
|
||||
{
|
||||
#region Behavior Variables
|
||||
protected EmeraldSystem EmeraldComponent;
|
||||
public enum BehaviorTypes { Passive = 0, Coward = 1, Aggressive = 2};
|
||||
public BehaviorTypes CurrentBehaviorType = BehaviorTypes.Aggressive;
|
||||
|
||||
public Transform TargetToFollow;
|
||||
public int CautiousSeconds = 0;
|
||||
public bool InfititeChase;
|
||||
public int ChaseSeconds = 5;
|
||||
public int FleeSeconds = 5;
|
||||
public bool RequireObstruction;
|
||||
public int PercentToFlee = 20;
|
||||
public float UpdateFleePositionSeconds = 1.5f;
|
||||
public int MaxDistanceFromStartingArea = 30;
|
||||
public float FollowingStoppingDistance = 2f;
|
||||
public bool IsAiming;
|
||||
|
||||
public delegate void StartFleeHandler();
|
||||
public event StartFleeHandler OnFlee;
|
||||
|
||||
public YesOrNo FleeOnLowHealth = YesOrNo.No;
|
||||
public YesOrNo StayNearStartingArea = YesOrNo.No;
|
||||
|
||||
/// <summary>
|
||||
/// A timer used for tracking how long an AI is in the cautious state.
|
||||
/// </summary>
|
||||
protected float CautiousTimer;
|
||||
/// <summary>
|
||||
/// A timer used for controlling how often flee positions are updated.
|
||||
/// </summary>
|
||||
protected float UpdateFleePositionTimer;
|
||||
/// <summary>
|
||||
/// A timer used for tracking how long a target is outside of an AI's detection radius.
|
||||
/// </summary>
|
||||
protected float GiveUpTimer;
|
||||
/// <summary>
|
||||
/// A timer used for tracking the cooldown length of an AI's attacks.
|
||||
/// </summary>
|
||||
protected float AttackTimer;
|
||||
/// <summary>
|
||||
/// A string used for tracking an AI's current behavior state. This is a string so it can be customized as needed, given that a behvaior has multiple stages or states.
|
||||
/// </summary>
|
||||
public string BehaviorState = "Non Combat";
|
||||
#endregion
|
||||
|
||||
#region Editor Variables
|
||||
[HideInInspector] public bool HideSettingsFoldout;
|
||||
[HideInInspector] public bool BehaviorSettingsFoldout;
|
||||
[HideInInspector] public bool CustomSettingsFoldout;
|
||||
#endregion
|
||||
|
||||
public virtual void Start()
|
||||
{
|
||||
InitailizeBehaviors();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the Behavior Component.
|
||||
/// </summary>
|
||||
public virtual void InitailizeBehaviors ()
|
||||
{
|
||||
EmeraldComponent = GetComponent<EmeraldSystem>();
|
||||
|
||||
ResetState();
|
||||
|
||||
if (TargetToFollow != null)
|
||||
{
|
||||
if (!TargetToFollow.gameObject.activeSelf)
|
||||
{
|
||||
Debug.LogError("The '" + gameObject.name + "' AI's Follower Target '" + TargetToFollow.name + "' is disabled so it has been removed as the AI's follower. You can enable said gameobject or use the SetFollowerTarget(Transform) API to assign a follower through code if needed.");
|
||||
TargetToFollow = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
StartCoroutine(SetFollowerTargetInternal()); //Use a slight delay to ensure all other components have been initialized before assigning the AI's Target to Follow.
|
||||
}
|
||||
}
|
||||
|
||||
EmeraldComponent.DetectionComponent.OnEnemyTargetDetected += OnDetectTarget;
|
||||
EmeraldComponent.DetectionComponent.OnNullTarget += ResetState;
|
||||
EmeraldComponent.CombatComponent.OnKilledTarget += OnKilledTarget;
|
||||
EmeraldComponent.HealthComponent.OnTakeDamage += OnTakeDamage;
|
||||
|
||||
if (CurrentBehaviorType == BehaviorTypes.Passive)
|
||||
{
|
||||
if (gameObject.tag != "Untagged")
|
||||
{
|
||||
gameObject.tag = "Untagged";
|
||||
}
|
||||
if (gameObject.layer != 0)
|
||||
{
|
||||
gameObject.layer = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use a slight delay to ensure all other components have been initialized before assigning the AI's Target to Follow.
|
||||
/// </summary>
|
||||
IEnumerator SetFollowerTargetInternal ()
|
||||
{
|
||||
yield return new WaitForSeconds(0.1f);
|
||||
EmeraldComponent.DetectionComponent.SetTargetToFollow(TargetToFollow);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Continiously updates the BehaviorObject. This acts like an Update function that can run within this behavior using the information from the passed EmeraldComponent and its
|
||||
/// </summary>
|
||||
public virtual void BehaviorUpdate()
|
||||
{
|
||||
if (EmeraldComponent.AnimationComponent.IsDead)
|
||||
return;
|
||||
|
||||
switch (BehaviorState)
|
||||
{
|
||||
case "Non Combat":
|
||||
WanderBehavior();
|
||||
break;
|
||||
case "Cautious":
|
||||
CautiousBehavior();
|
||||
break;
|
||||
case "Aggressive":
|
||||
AggressiveBehavior();
|
||||
break;
|
||||
case "Flee":
|
||||
CowardBehavior();
|
||||
break;
|
||||
}
|
||||
|
||||
//Update the DetectTargetTracker virtual method (which tracks when targets are within the detection radius and clears them when needed)
|
||||
//This can be overridden if these mechanics need to be customized.
|
||||
DetectTargetTracker();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Play a warning animation and look at the current target. If the CautiousSeconds are met, change the state to be aggressive.
|
||||
/// </summary>
|
||||
public virtual void CautiousBehavior()
|
||||
{
|
||||
if (EmeraldComponent.CombatTarget && EmeraldComponent.CombatComponent.CombatState && EmeraldComponent.MovementComponent.AIAgentActive && CurrentBehaviorType != BehaviorTypes.Passive)
|
||||
{
|
||||
//Bypass the cautious timer if the AI has a follower target.
|
||||
if (CurrentBehaviorType == BehaviorTypes.Aggressive && EmeraldComponent.TargetToFollow) BehaviorState = "Aggressive";
|
||||
|
||||
CautiousTimer += Time.deltaTime;
|
||||
|
||||
if (CautiousTimer >= CautiousSeconds)
|
||||
{
|
||||
if (CurrentBehaviorType == BehaviorTypes.Aggressive)
|
||||
{
|
||||
BehaviorState = "Aggressive";
|
||||
}
|
||||
else if (CurrentBehaviorType == BehaviorTypes.Coward)
|
||||
{
|
||||
OnFlee?.Invoke(); //Invoke the OnFlee delegate.
|
||||
BehaviorState = "Flee";
|
||||
}
|
||||
|
||||
CautiousTimer = 0;
|
||||
}
|
||||
|
||||
if (CautiousTimer > 2)
|
||||
{
|
||||
EmeraldComponent.AnimationComponent.PlayWarningAnimation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Actively chase and attack the current target.
|
||||
/// </summary>
|
||||
public virtual void AggressiveBehavior()
|
||||
{
|
||||
if (EmeraldComponent.CombatComponent.CombatState && EmeraldComponent.MovementComponent.AIAgentActive && EmeraldComponent.CombatTarget)
|
||||
{
|
||||
//Only attempt to chase and attack the current target if a path exists to them.
|
||||
bool CanReachTarget = EmeraldComponent.MovementComponent.CanReachTarget;
|
||||
|
||||
//Use Emerald AI's built-in Combat Movement (which simply sets the AI's destination equal to the current target's position). A custom function can be used for added functionality, if desired.
|
||||
//This will also backup the AI if they get too close to their target.
|
||||
if (!EmeraldComponent.MovementComponent.DefaultMovementPaused)
|
||||
{
|
||||
EmeraldComponent.MovementComponent.CombatMovement();
|
||||
}
|
||||
else if (!EmeraldComponent.MovementComponent.DefaultMovementPaused && CanReachTarget)
|
||||
{
|
||||
EmeraldComponent.m_NavMeshAgent.SetDestination(transform.position + transform.forward * 2);
|
||||
}
|
||||
|
||||
EmeraldComponent.CombatComponent.UpdateActions(); //Updates an AI's list of Combat Actions, but only while in the Aggressive state (Combat State).
|
||||
|
||||
Attack(); //Continuously check to see if the conditions are right to trigger an attack, given the AI is in the Aggressive State.
|
||||
|
||||
//If FleeOnLowHealth is enabled, and the AI's health reaches the threshold, set the AI's BehaviorState to Flee.
|
||||
if (FleeOnLowHealth == YesOrNo.Yes && ((float)EmeraldComponent.HealthComponent.CurrentHealth / (float)EmeraldComponent.HealthComponent.StartingHealth) < (PercentToFlee * 0.01f))
|
||||
{
|
||||
EmeraldComponent.AnimationComponent.ResetTriggers(0);
|
||||
OnFlee?.Invoke(); //Invoke the OnFlee delegate.
|
||||
BehaviorState = "Flee";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use Emerald AI's built-in Flee Movement (which simply generates a destination opposite to the current target's position). This is updated based
|
||||
/// on the UpdateFleePositionSeconds or if the AI gets close the generated waypoint. A custom function can be used for added functionality, if desired.
|
||||
/// </summary>
|
||||
public virtual void CowardBehavior()
|
||||
{
|
||||
if (!EmeraldComponent.MovementComponent.DefaultMovementPaused)
|
||||
{
|
||||
UpdateFleePositionTimer += Time.deltaTime;
|
||||
if (UpdateFleePositionTimer > UpdateFleePositionSeconds || EmeraldComponent.m_NavMeshAgent.remainingDistance <= EmeraldComponent.MovementComponent.StoppingDistance)
|
||||
{
|
||||
EmeraldComponent.MovementComponent.FleeMovement();
|
||||
UpdateFleePositionTimer = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use Emerald AI's built-in Wandering when not in combat (based off of the user set WanderType within the Emerald AI Movement Editor)
|
||||
/// </summary>
|
||||
public virtual void WanderBehavior()
|
||||
{
|
||||
if (EmeraldComponent.MovementComponent.AIAgentActive && !EmeraldComponent.CombatComponent.CombatState && !EmeraldComponent.CombatComponent.DeathDelayActive)
|
||||
{
|
||||
if (!EmeraldComponent.TargetToFollow)
|
||||
{
|
||||
EmeraldComponent.MovementComponent.Wander();
|
||||
}
|
||||
else
|
||||
{
|
||||
EmeraldComponent.MovementComponent.FollowCompanionTarget(FollowingStoppingDistance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used for tracking when a target is outside of an AI's detection radius.
|
||||
/// </summary>
|
||||
public virtual void DetectTargetTracker()
|
||||
{
|
||||
if (!EmeraldComponent.CombatComponent.CombatState || EmeraldComponent.CombatComponent.DeathDelayActive || InfititeChase || EmeraldComponent.TargetToFollow)
|
||||
return;
|
||||
|
||||
//Track how long a target is outside of the AI's detection radius. If the time is exceeded, give up on the target and set the AI to its defaul state.
|
||||
if (EmeraldComponent.CombatComponent.DistanceFromTarget > EmeraldComponent.DetectionComponent.DetectionRadius && !RequireObstruction || RequireObstruction && EmeraldComponent.DetectionComponent.TargetObstructed)
|
||||
{
|
||||
GiveUpTimer += Time.deltaTime;
|
||||
|
||||
if (GiveUpTimer >= ChaseSeconds && CurrentBehaviorType == BehaviorTypes.Aggressive || GiveUpTimer >= FleeSeconds && CurrentBehaviorType == BehaviorTypes.Coward || BehaviorState == "Cautious")
|
||||
{
|
||||
CancelCombat(); //Stops the AI from fighting and chasing, or fleeing from, its current target.
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
GiveUpTimer = 0;
|
||||
}
|
||||
|
||||
//Used for tracking when an AI's distance from its starting position is exceeded.
|
||||
if (CurrentBehaviorType == BehaviorTypes.Aggressive && StayNearStartingArea == YesOrNo.Yes && Vector3.Distance(EmeraldComponent.MovementComponent.StartingDestination, transform.position) > MaxDistanceFromStartingArea)
|
||||
{
|
||||
EmeraldComponent.MovementComponent.EnableReturnToStart(); //Returns the AI to its starting area
|
||||
CancelCombat(); //Stops the AI from fighting and chasing its current target
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Continuously check to see if the conditions are right to trigger an attack, given the AI is in the Aggressive State. This is a priority state.
|
||||
/// </summary>
|
||||
public virtual void Attack()
|
||||
{
|
||||
var EnterConditions = EmeraldComponent.AnimationComponent.IsIdling || EmeraldComponent.AnimationComponent.IsMoving;
|
||||
var CooldownConditions = EmeraldComponent.AnimationComponent.IsIdling || EmeraldComponent.AnimationComponent.IsMoving || EmeraldComponent.AnimationComponent.IsBackingUp ||
|
||||
EmeraldComponent.AnimationComponent.IsTurningLeft || EmeraldComponent.AnimationComponent.IsTurningRight || EmeraldComponent.AnimationComponent.IsGettingHit;
|
||||
|
||||
if (CooldownConditions) AttackTimer += Time.deltaTime;
|
||||
|
||||
if (EmeraldCombatManager.AllowedToAttack(EmeraldComponent) && EnterConditions && !IsAiming && AttackTimer >= EmeraldComponent.CombatComponent.CurrentAttackCooldown)
|
||||
{
|
||||
EmeraldComponent.AnimationComponent.IsMoving = false;
|
||||
EmeraldComponent.CombatComponent.AdjustCooldowns();
|
||||
EmeraldComponent.CombatComponent.AttackPosition = EmeraldComponent.CombatTarget.position - transform.position;
|
||||
EmeraldComponent.CombatComponent.AttackPosition.y = 0;
|
||||
EmeraldComponent.AnimationComponent.PlayAttackAnimation();
|
||||
AttackTimer = 0;
|
||||
}
|
||||
|
||||
//Cancel the attack if it's triggered and the target is out of range
|
||||
if (AttackTimer >= EmeraldComponent.CombatComponent.CurrentAttackCooldown)
|
||||
{
|
||||
if (EmeraldComponent.m_NavMeshAgent.remainingDistance > EmeraldComponent.m_NavMeshAgent.stoppingDistance && EmeraldComponent.AIAnimator.GetBool("Attack"))
|
||||
{
|
||||
EmeraldComponent.AIAnimator.ResetTrigger("Attack");
|
||||
AttackTimer = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the AI from fighting and chasing, or fleeing from, its current target.
|
||||
/// </summary>
|
||||
public virtual void CancelCombat()
|
||||
{
|
||||
EmeraldComponent.CombatComponent.ClearTarget();
|
||||
EmeraldComponent.CombatComponent.DeathDelayActive = true;
|
||||
EmeraldComponent.m_NavMeshAgent.SetDestination(transform.position + EmeraldComponent.transform.forward * (EmeraldComponent.MovementComponent.StoppingDistance * 1.5f));
|
||||
EmeraldComponent.AnimationComponent.ResetTriggers(0);
|
||||
ResetState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reset the settings back to their default values.
|
||||
/// </summary>
|
||||
public virtual void ResetState ()
|
||||
{
|
||||
BehaviorState = "Non Combat";
|
||||
CautiousTimer = 0;
|
||||
UpdateFleePositionTimer = 0;
|
||||
GiveUpTimer = 0;
|
||||
AttackTimer = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When killing a target, return the BehaviorState back to Non Combat. If another target is found, it will be updated.
|
||||
/// </summary>
|
||||
public virtual void OnKilledTarget()
|
||||
{
|
||||
BehaviorState = "Non Combat";
|
||||
GiveUpTimer = 0;
|
||||
EmeraldComponent.AnimationComponent.WarningAnimationTriggered = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When detecting a target, update the AI's current destination to be equal to their current position until the combat movement code can take over.
|
||||
/// </summary>
|
||||
public virtual void OnDetectTarget()
|
||||
{
|
||||
BehaviorState = "Cautious";
|
||||
if (isActiveAndEnabled) EmeraldComponent.m_NavMeshAgent.SetDestination(transform.position);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If an AI takes damage before its Cautious State has finished, change the BehaviorState so it can handle its attack according to its behavior.
|
||||
/// </summary>
|
||||
public virtual void OnTakeDamage()
|
||||
{
|
||||
if (BehaviorState == "Cautious" && CurrentBehaviorType == BehaviorTypes.Aggressive) BehaviorState = "Aggressive";
|
||||
else if (BehaviorState == "Cautious" && CurrentBehaviorType == BehaviorTypes.Coward) BehaviorState = "Flee";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ff18b11debafb2944b9a076aa0d8052a
|
||||
timeCreated: 1548786587
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,579 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using EmeraldAI.Utility;
|
||||
|
||||
namespace EmeraldAI
|
||||
{
|
||||
[HelpURL("https://black-horizon-studios.gitbook.io/emerald-ai-wiki/emerald-components-required/combat-component")]
|
||||
public class EmeraldCombat : MonoBehaviour, ICombat
|
||||
{
|
||||
#region Combat Variables
|
||||
public List<EmeraldWeaponCollision> WeaponColliders = new List<EmeraldWeaponCollision>();
|
||||
public EmeraldWeaponCollision CurrentWeaponCollision;
|
||||
|
||||
public int MinResumeWander = 2;
|
||||
public int MaxResumeWander = 4;
|
||||
|
||||
public float CurrentAttackCooldown;
|
||||
public float Type1AttackCooldown = 0.35f;
|
||||
public float Type2AttackCooldown = 0.35f;
|
||||
|
||||
public float SwitchWeaponTimer = 0;
|
||||
public bool SwitchWeaponTypeTriggered = false;
|
||||
public bool CombatActionActive;
|
||||
|
||||
[SerializeField] public List<ActionsClass> CombatActions = new List<ActionsClass>();
|
||||
[SerializeField] public List<ActionsClass> Type1CombatActions = new List<ActionsClass>();
|
||||
[SerializeField] public List<ActionsClass> Type2CombatActions = new List<ActionsClass>();
|
||||
|
||||
public Vector3 AttackPosition;
|
||||
|
||||
//These can be set through Action Objects so actions like block and dodge can mitigate damage.
|
||||
public int MitigationAmount = 50;
|
||||
public float MaxMitigationAngle = 75;
|
||||
|
||||
public delegate void KilledTargetHandler();
|
||||
public event KilledTargetHandler OnKilledTarget;
|
||||
public delegate void DoDamageHandler();
|
||||
public event DoDamageHandler OnDoDamage;
|
||||
public delegate void DoCritDamageHandler();
|
||||
public event DoCritDamageHandler OnDoCritDamage;
|
||||
public delegate void StartCombatHandler();
|
||||
public event StartCombatHandler OnStartCombat;
|
||||
public delegate void EndCombatHandler();
|
||||
public event EndCombatHandler OnEndCombat;
|
||||
|
||||
//This differs from OnEndCombat as it's called when AI actually exits their combat state.
|
||||
//There are various components that subscribe to this for transitioning back to their non-combat states.
|
||||
public delegate void ExitCombatHandler();
|
||||
public event ExitCombatHandler OnExitCombat;
|
||||
|
||||
public bool CombatState;
|
||||
|
||||
public enum WeaponTypes { Type1 = 0, Type2 = 1 };
|
||||
public WeaponTypes StartingWeaponType = WeaponTypes.Type1;
|
||||
public WeaponTypes CurrentWeaponType = WeaponTypes.Type1;
|
||||
|
||||
public PickTargetTypes Type1PickTargetType = PickTargetTypes.Closest;
|
||||
public PickTargetTypes Type2PickTargetType = PickTargetTypes.Closest;
|
||||
|
||||
public enum WeaponTypeAmounts { One, Two };
|
||||
public WeaponTypeAmounts WeaponTypeAmount = WeaponTypeAmounts.One;
|
||||
|
||||
[SerializeField]
|
||||
public AttackClass Type1Attacks;
|
||||
[SerializeField]
|
||||
public AttackClass Type2Attacks;
|
||||
|
||||
public int SwitchWeaponTypesCooldown = 10;
|
||||
public int SwitchWeaponTypesDistance = 8;
|
||||
|
||||
public Transform CurrentAttackTransform;
|
||||
public List<Transform> WeaponType1AttackTransforms = new List<Transform>();
|
||||
public List<Transform> WeaponType2AttackTransforms = new List<Transform>();
|
||||
|
||||
public enum SwitchWeaponTypes { Distance, Timed, None};
|
||||
public SwitchWeaponTypes SwitchWeaponType = SwitchWeaponTypes.Timed;
|
||||
public int SwitchWeaponTimeMin = 10;
|
||||
public int SwitchWeaponTimeMax = 20;
|
||||
public float SwitchWeaponTime = 0;
|
||||
|
||||
public float DistanceFromTarget;
|
||||
public float TargetAngle;
|
||||
public int ReceivedRagdollForceAmount;
|
||||
public Transform RagdollTransform;
|
||||
public Vector3 TargetDestination;
|
||||
public bool FirstTimeInCombat = true;
|
||||
public float DeathDelay;
|
||||
public bool DeathDelayActive;
|
||||
public float DeathDelayTimer;
|
||||
public int CurrentAnimationIndex = 0;
|
||||
public bool TargetDetectionActive;
|
||||
public float TooCloseDistance = 1;
|
||||
public float AttackDistance = 2.5f;
|
||||
public EmeraldAbilityObject CurrentEmeraldAIAbility;
|
||||
EmeraldSystem EmeraldComponent;
|
||||
public AttackClass.AttackData CurrentAttackData;
|
||||
public Transform LastAttacker;
|
||||
#endregion
|
||||
|
||||
#region Private Variables
|
||||
bool m_WeaponTypeSwitchDelay;
|
||||
Coroutine SwitchWeaponCoroutine;
|
||||
#endregion
|
||||
|
||||
#region Editor Variable
|
||||
public bool HideSettingsFoldout;
|
||||
public bool DamageSettingsFoldout;
|
||||
public bool CombatActionSettingsFoldout;
|
||||
public bool SwitchWeaponSettingsFoldout;
|
||||
public bool WeaponType1SettingsFoldout;
|
||||
public bool WeaponType2SettingsFoldout;
|
||||
#endregion
|
||||
|
||||
void Start()
|
||||
{
|
||||
InitializeCombat();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the Combat Component.
|
||||
/// </summary>
|
||||
void InitializeCombat ()
|
||||
{
|
||||
EmeraldComponent = GetComponent<EmeraldSystem>();
|
||||
EmeraldComponent.HealthComponent.OnDeath += CancelAllCombatActions; //Subscribe to the OnDeath event for CancelCombatActions
|
||||
EmeraldComponent.DetectionComponent.OnEnemyTargetDetected += EnterCombat; //Subscribe to the OnDeath event for CancelCombatActions
|
||||
EmeraldComponent.DetectionComponent.OnNullTarget += NullCombatTarget; //Subscribe to the OnNullTarget event for NullCombatTarget
|
||||
OnKilledTarget += CancelAllCombatActions; //Subscribe to the OnKilledTarget event for CancelCombatActions
|
||||
TargetDetectionActive = true;
|
||||
FirstTimeInCombat = true;
|
||||
SwitchWeaponTime = Random.Range((float)SwitchWeaponTimeMin, SwitchWeaponTimeMax + 1);
|
||||
DeathDelay = Random.Range(MinResumeWander, MaxResumeWander + 1);
|
||||
Invoke(nameof(InitializeAttacks), 0.1f);
|
||||
}
|
||||
|
||||
void InitializeAttacks()
|
||||
{
|
||||
//Generate an attack based on the current weapon type
|
||||
if (CurrentWeaponType == WeaponTypes.Type1)
|
||||
{
|
||||
EmeraldCombatManager.GenerateAttack(EmeraldComponent, Type1Attacks);
|
||||
EmeraldComponent.DetectionComponent.PickTargetType = Type1PickTargetType;
|
||||
CurrentAttackCooldown = Type1AttackCooldown;
|
||||
|
||||
}
|
||||
else if (CurrentWeaponType == WeaponTypes.Type2)
|
||||
{
|
||||
EmeraldCombatManager.GenerateAttack(EmeraldComponent, Type2Attacks);
|
||||
EmeraldComponent.DetectionComponent.PickTargetType = Type2PickTargetType;
|
||||
CurrentAttackCooldown = Type2AttackCooldown;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A custom update function for the EmeraldCombat script called through the EmeraldAISystem script.
|
||||
/// </summary>
|
||||
public void CombatUpdate()
|
||||
{
|
||||
if (CombatState)
|
||||
{
|
||||
DistanceFromTarget = EmeraldCombatManager.GetDistanceFromTarget(EmeraldComponent); //Update current distance from the target.
|
||||
TargetAngle = EmeraldCombatManager.TargetAngle(EmeraldComponent); //Update current angle from the target.
|
||||
}
|
||||
else if (!CombatState)
|
||||
{
|
||||
DistanceFromTarget = EmeraldCombatManager.GetDistanceFromLookTarget(EmeraldComponent); //Update current distance from the target.
|
||||
TargetAngle = EmeraldCombatManager.TransformAngle(EmeraldComponent, EmeraldComponent.LookAtTarget); //Update current angle from the target.
|
||||
}
|
||||
|
||||
CheckForTargetDeath(); //Monitor the current target's health for when it dies.
|
||||
UpdateWeaponTypeState(); //Check for when to switch weapons.
|
||||
UpdateDeathDelay(); //Controls when an AI will go back to its non-combat state after killing a target.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Controls when the death delay feature has lapsed.
|
||||
/// </summary>
|
||||
void UpdateDeathDelay ()
|
||||
{
|
||||
if (DeathDelayActive)
|
||||
{
|
||||
DeathDelayTimer += Time.deltaTime;
|
||||
|
||||
if (DeathDelayTimer > DeathDelay)
|
||||
{
|
||||
ExitCombat();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the AI's current target is within the angle limit to be attacked.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool TargetWithinAngleLimit ()
|
||||
{
|
||||
return TargetAngle <= EmeraldComponent.MovementComponent.CombatAngleToTurn;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke the OnEnterCombat event that happens when the AI starts fighting its first target for the current battle.
|
||||
/// </summary>
|
||||
public void EnterCombat ()
|
||||
{
|
||||
if (FirstTimeInCombat) OnStartCombat?.Invoke();
|
||||
FirstTimeInCombat = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets various settings when exiting combat.
|
||||
/// </summary>
|
||||
public void ExitCombat ()
|
||||
{
|
||||
CombatState = false;
|
||||
SwitchWeaponTimer = 0;
|
||||
ClearTarget();
|
||||
FirstTimeInCombat = true;
|
||||
DeathDelayTimer = 0;
|
||||
DeathDelayActive = false;
|
||||
OnExitCombat?.Invoke(); //This is used in the Movement, Detection, and Animation components to return them to their default states.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an AI's list of actions while in combat.
|
||||
/// </summary>
|
||||
public void UpdateActions()
|
||||
{
|
||||
if (CurrentWeaponType == WeaponTypes.Type1) CombatActions = Type1CombatActions;
|
||||
else if (CurrentWeaponType == WeaponTypes.Type2) CombatActions = Type2CombatActions;
|
||||
|
||||
if (EmeraldComponent.CombatTarget != null && !EmeraldComponent.AnimationComponent.IsDead && !EmeraldComponent.AnimationComponent.IsStunned && !EmeraldComponent.AIAnimator.GetBool("Stunned Active"))
|
||||
{
|
||||
for (int i = 0; i < CombatActions.Count; i++)
|
||||
{
|
||||
if (CombatActions[i].Enabled)
|
||||
{
|
||||
CombatActions[i].emeraldAction.UpdateAction(EmeraldComponent, CombatActions[i]);
|
||||
var Conditions = (((int)CombatActions[i].emeraldAction.CooldownConditions) & ((int)EmeraldComponent.AnimationComponent.CurrentAnimationState)) != 0;
|
||||
|
||||
//Only update the cooldown timer if the conditions are met for this action.
|
||||
if (Conditions && !EmeraldComponent.AIAnimator.GetBool("Attack") && !CombatActions[i].IsActive)
|
||||
CombatActions[i].CooldownLengthTimer += Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels all combat actions that are currently active.
|
||||
/// </summary>
|
||||
public void CancelAllCombatActions ()
|
||||
{
|
||||
if (CombatActions.Count == 0)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < CombatActions.Count; i++)
|
||||
{
|
||||
if (CombatActions[i].IsActive)
|
||||
{
|
||||
CombatActions[i].emeraldAction.CancelAction(EmeraldComponent, CombatActions[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adjusts the all action cooldowns so two actions aren't triggered simultaneously (due to Animation States needing time to transition). This is called after some actions have been successfully triggered.
|
||||
/// </summary>
|
||||
public void AdjustCooldowns()
|
||||
{
|
||||
for (int i = 0; i < CombatActions.Count; i++)
|
||||
{
|
||||
if (CombatActions[i].CooldownLengthTimer >= CombatActions[i].emeraldAction.CooldownLength - 0.25f)
|
||||
CombatActions[i].CooldownLengthTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This function invokes the ability attached to the AI's Ability Object slot, of its Attack List of the Combat Component, when the set attack animation is playing. This should be set through an Animation Event.
|
||||
/// </summary>
|
||||
public void CreateAbility(AnimationEvent AttackEventParameters)
|
||||
{
|
||||
//Allow the objectReferenceParameter to override the current ability
|
||||
if (AttackEventParameters.objectReferenceParameter != null)
|
||||
{
|
||||
CurrentEmeraldAIAbility = (EmeraldAbilityObject)AttackEventParameters.objectReferenceParameter;
|
||||
}
|
||||
|
||||
EmeraldCombatManager.UpdateAttackTransforms(EmeraldComponent, AttackEventParameters.stringParameter); //Updates the AI's current attack and weapon transforms based on the sent AttackTransformName from an EmeraldAttackEvent Animation Event.
|
||||
if (CurrentEmeraldAIAbility != null) CurrentEmeraldAIAbility.InvokeAbility(gameObject, CurrentAttackTransform); //Invoke the ability, if the ability slot is not emepty.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This function invokes a charge effect from the attached Ability Object slot, of its Attack List of the Combat Component, when the set attack animation is playing. This should be set through an Animation Event.
|
||||
/// </summary>
|
||||
public void ChargeEffect(AnimationEvent AttackEventParameters)
|
||||
{
|
||||
Transform AttackTransform = EmeraldCombatManager.GetAttackTransform(EmeraldComponent, AttackEventParameters.stringParameter); //Gets the weapon transform based on the sent AttackTransformName from an EmeraldChargeAttack Animation Event.
|
||||
if (CurrentEmeraldAIAbility != null && AttackTransform != null) CurrentEmeraldAIAbility.ChargeAbility(gameObject, AttackTransform); //Invoke the ability's charge, if the ability slot is not emepty.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called through the OnNullTarget callback when a target becomes null. Clear the Current Target and look for another.
|
||||
/// If no new target is found, wait for death delay to lapse before returning to the non-combat state.
|
||||
/// </summary>
|
||||
void NullCombatTarget()
|
||||
{
|
||||
if (EmeraldComponent.CombatTarget == null && CombatState && !EmeraldComponent.MovementComponent.ReturningToStartInProgress && !DeathDelayActive)
|
||||
{
|
||||
DeathDelay = Random.Range(MinResumeWander, MaxResumeWander + 1);
|
||||
DeathDelayActive = true;
|
||||
EmeraldComponent.m_NavMeshAgent.ResetPath();
|
||||
ClearTarget();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Watch the CurrentIDamageable for when the health reaches 0.
|
||||
/// </summary>
|
||||
void CheckForTargetDeath()
|
||||
{
|
||||
if (EmeraldComponent.CurrentTargetInfo.CurrentIDamageable != null)
|
||||
{
|
||||
if (EmeraldComponent.CurrentTargetInfo.CurrentIDamageable.Health <= 0 && !DeathDelayActive)
|
||||
{
|
||||
OnKilledTarget?.Invoke();
|
||||
DeathDelay = Random.Range(MinResumeWander, MaxResumeWander + 1);
|
||||
DeathDelayActive = true;
|
||||
EmeraldComponent.m_NavMeshAgent.ResetPath();
|
||||
Invoke(nameof(ClearTarget), 0.75f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the AI's current target.
|
||||
/// </summary>
|
||||
public void ClearTarget()
|
||||
{
|
||||
if (EmeraldComponent.CombatTarget != null)
|
||||
{
|
||||
//Remove the CurrentTarget from the AI's LineOfSightTargets list.
|
||||
if (EmeraldComponent.DetectionComponent.LineOfSightTargets.Contains(EmeraldComponent.CombatTarget.GetComponent<Collider>()))
|
||||
EmeraldComponent.DetectionComponent.LineOfSightTargets.Remove(EmeraldComponent.CombatTarget.GetComponent<Collider>());
|
||||
}
|
||||
else
|
||||
{
|
||||
//The CurrentTarget is null, remove it, and any other null targes, from the list.
|
||||
for (int i = 0; i < EmeraldComponent.DetectionComponent.LineOfSightTargets.Count; i++)
|
||||
{
|
||||
if (EmeraldComponent.DetectionComponent.LineOfSightTargets[i] == null)
|
||||
{
|
||||
EmeraldComponent.DetectionComponent.LineOfSightTargets.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//Clear the current target references
|
||||
EmeraldComponent.CombatTarget = null;
|
||||
EmeraldComponent.CurrentTargetInfo.TargetSource = null;
|
||||
EmeraldComponent.CurrentTargetInfo.CurrentIDamageable = null;
|
||||
EmeraldComponent.CurrentTargetInfo.CurrentICombat = null;
|
||||
|
||||
//Invoke the OnEndCombat callback if there's no remaining detectable enemy targets nearby.
|
||||
if (EmeraldComponent.DetectionComponent.LineOfSightTargets.Count == 0) OnEndCombat?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked below with cooldown delay and controls the AI's weapon type switching from happening too often
|
||||
/// </summary>
|
||||
void WeaponSwitchCooldown()
|
||||
{
|
||||
m_WeaponTypeSwitchDelay = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the AI's weapon type to switch between weapon types.
|
||||
/// </summary>
|
||||
void UpdateWeaponTypeState()
|
||||
{
|
||||
if (!CombatState || DeathDelayActive)
|
||||
return;
|
||||
|
||||
if (WeaponTypeAmount == WeaponTypeAmounts.Two)
|
||||
{
|
||||
if (SwitchWeaponTypeTriggered && !m_WeaponTypeSwitchDelay)
|
||||
{
|
||||
SwitchWeaponTypeTriggered = false;
|
||||
m_WeaponTypeSwitchDelay = true;
|
||||
Invoke(nameof(WeaponSwitchCooldown), SwitchWeaponTypesCooldown);
|
||||
}
|
||||
|
||||
//Switches the current weapon type based on distance.
|
||||
if (SwitchWeaponType == SwitchWeaponTypes.Distance && EmeraldComponent.CombatTarget != null && !m_WeaponTypeSwitchDelay &&
|
||||
!EmeraldComponent.AnimationComponent.IsSwitchingWeapons && !EmeraldComponent.AnimationComponent.IsAttacking && !EmeraldComponent.AnimationComponent.IsMoving && !EmeraldComponent.AnimationComponent.IsBackingUp && !EmeraldComponent.AnimationComponent.IsTurning && !EmeraldComponent.AIAnimator.GetBool("Strafe Active"))
|
||||
{
|
||||
if (DistanceFromTarget > SwitchWeaponTypesDistance && CurrentWeaponType != StartingWeaponType && !SwitchWeaponTypeTriggered)
|
||||
{
|
||||
SwapWeaponType();
|
||||
SwitchWeaponTypeTriggered = true;
|
||||
}
|
||||
if (DistanceFromTarget < SwitchWeaponTypesDistance && CurrentWeaponType == StartingWeaponType && !SwitchWeaponTypeTriggered)
|
||||
{
|
||||
SwapWeaponType();
|
||||
SwitchWeaponTypeTriggered = true;
|
||||
}
|
||||
}
|
||||
//Switches the current weapon type based on a random time of SwitchWeaponTimeMin and SwitchWeaponTimeMax.
|
||||
else if (SwitchWeaponType == SwitchWeaponTypes.Timed)
|
||||
{
|
||||
if (!EmeraldComponent.AnimationComponent.IsSwitchingWeapons && !EmeraldComponent.AnimationComponent.IsEquipping && !EmeraldComponent.AnimationComponent.IsMoving)
|
||||
SwitchWeaponTimer += Time.deltaTime;
|
||||
|
||||
if (EmeraldComponent.CombatTarget != null && SwitchWeaponTimer >= SwitchWeaponTime &&
|
||||
!EmeraldComponent.AnimationComponent.IsSwitchingWeapons && !EmeraldComponent.AnimationComponent.IsEquipping && !EmeraldComponent.AnimationComponent.IsGettingHit && !EmeraldComponent.AnimationComponent.IsAttacking && !EmeraldComponent.AnimationComponent.IsMoving && !EmeraldComponent.AnimationComponent.IsBackingUp && !EmeraldComponent.AnimationComponent.IsTurning && !EmeraldComponent.AIAnimator.GetBool("Strafe Active"))
|
||||
{
|
||||
SwapWeaponType();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Swaps the current weapon type.
|
||||
/// </summary>
|
||||
public void SwapWeaponType()
|
||||
{
|
||||
if (CurrentWeaponType == WeaponTypes.Type1)
|
||||
{
|
||||
if (SwitchWeaponCoroutine != null) StopCoroutine(SwitchWeaponCoroutine);
|
||||
SwitchWeaponCoroutine = StartCoroutine(ChangeWeaponType("Type2")); //Switch to the Weapon Type 2
|
||||
}
|
||||
else if (CurrentWeaponType == WeaponTypes.Type2)
|
||||
{
|
||||
if (SwitchWeaponCoroutine != null) StopCoroutine(SwitchWeaponCoroutine);
|
||||
SwitchWeaponCoroutine = StartCoroutine(ChangeWeaponType("Type1")); //Switch to the Weapon Type 1
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator ChangeWeaponType(string WeaponTypeName)
|
||||
{
|
||||
EmeraldComponent.AnimationComponent.IsSwitchingWeapons = true;
|
||||
EmeraldCombatManager.ResetWeaponSwapTime(EmeraldComponent);
|
||||
CurrentAnimationIndex = 1;
|
||||
EmeraldComponent.AIAnimator.SetInteger("Attack Index", 1);
|
||||
EmeraldComponent.AIAnimator.SetInteger("Hit Index", 1);
|
||||
EmeraldComponent.AIAnimator.ResetTrigger("Attack");
|
||||
EmeraldComponent.AIAnimator.SetBool("Walk Backwards", false);
|
||||
EmeraldComponent.AIAnimator.ResetTrigger("Hit");
|
||||
yield return new WaitForSeconds(0.1f);
|
||||
|
||||
if (WeaponTypeName == "Type1")
|
||||
{
|
||||
yield return new WaitUntil(()=>EmeraldComponent.AnimationComponent.IsIdling);
|
||||
EmeraldComponent.AIAnimator.SetInteger("Weapon Type State", 1);
|
||||
CurrentAttackCooldown = Type1AttackCooldown;
|
||||
EmeraldComponent.DetectionComponent.PickTargetType = Type1PickTargetType;
|
||||
}
|
||||
else if (WeaponTypeName == "Type2")
|
||||
{
|
||||
yield return new WaitUntil(() => EmeraldComponent.AnimationComponent.IsIdling);
|
||||
EmeraldComponent.AIAnimator.SetInteger("Weapon Type State", 2);
|
||||
CurrentAttackCooldown = Type2AttackCooldown;
|
||||
EmeraldComponent.DetectionComponent.PickTargetType = Type2PickTargetType;
|
||||
}
|
||||
|
||||
CurrentWeaponType = (WeaponTypes)System.Enum.Parse(typeof(WeaponTypes), WeaponTypeName);
|
||||
|
||||
if (EmeraldComponent.AIAnimator.GetBool("Animate Weapon State"))
|
||||
{
|
||||
while (!EmeraldComponent.AnimationComponent.IsEquipping)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//If an AI doesn't have equipping animations, bypass the need for equipping animations and enable/disable them here.
|
||||
EmeraldItems m_EmeraldItems = GetComponent<EmeraldItems>();
|
||||
|
||||
if (m_EmeraldItems != null)
|
||||
{
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
|
||||
if (WeaponTypeName == "Type1")
|
||||
{
|
||||
m_EmeraldItems.UnequipWeapon("Weapon Type 2");
|
||||
m_EmeraldItems.EquipWeapon("Weapon Type 1");
|
||||
}
|
||||
else if (WeaponTypeName == "Type2")
|
||||
{
|
||||
m_EmeraldItems.UnequipWeapon("Weapon Type 1");
|
||||
m_EmeraldItems.EquipWeapon("Weapon Type 2");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EmeraldComponent.AIAnimator.ResetTrigger("Hit");
|
||||
UpdateWeaponTypeValues();
|
||||
EmeraldComponent.AIAnimator.SetBool("Walk Backwards", false);
|
||||
EmeraldComponent.AnimationComponent.IsSwitchingWeapons = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regenerate an attack based on the current weapon type and update the needed settings.
|
||||
/// </summary>
|
||||
void UpdateWeaponTypeValues()
|
||||
{
|
||||
EmeraldComponent.AIAnimator.ResetTrigger("Attack");
|
||||
EmeraldComponent.AIAnimator.SetInteger("Attack Index", 1);
|
||||
if (CurrentWeaponType == WeaponTypes.Type1)
|
||||
EmeraldCombatManager.GenerateAttack(EmeraldComponent, Type1Attacks);
|
||||
else if (CurrentWeaponType == WeaponTypes.Type2)
|
||||
EmeraldCombatManager.GenerateAttack(EmeraldComponent, Type2Attacks);
|
||||
}
|
||||
|
||||
public void InvokeDoDamage ()
|
||||
{
|
||||
OnDoDamage?.Invoke();
|
||||
}
|
||||
|
||||
public void InvokeDoCritDamage()
|
||||
{
|
||||
OnDoCritDamage?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used for getting the transform of the target.
|
||||
/// </summary>
|
||||
public Transform TargetTransform()
|
||||
{
|
||||
return transform;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used for detecting when a target is attacking.
|
||||
/// </summary>
|
||||
public bool IsAttacking()
|
||||
{
|
||||
return EmeraldComponent.AnimationComponent.AttackTriggered;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used for detecting when this target is blocking.
|
||||
/// </summary>
|
||||
public bool IsBlocking()
|
||||
{
|
||||
return EmeraldComponent.AnimationComponent.IsBlocking && EmeraldComponent.AIAnimator.GetBool("Blocking");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used for detecting when this target is dodging.
|
||||
/// </summary>
|
||||
public bool IsDodging()
|
||||
{
|
||||
return EmeraldComponent.AnimationComponent.IsDodging;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used referencing the AI's damage position when an AI takes damage from external sources.
|
||||
/// </summary>
|
||||
public Vector3 DamagePosition()
|
||||
{
|
||||
if (EmeraldComponent.TPMComponent != null)
|
||||
return new Vector3(EmeraldComponent.TPMComponent.TransformSource.position.x, EmeraldComponent.TPMComponent.TransformSource.position.y + EmeraldComponent.TPMComponent.PositionModifier, EmeraldComponent.TPMComponent.TransformSource.position.z);
|
||||
else
|
||||
return transform.position + new Vector3(0, transform.localScale.y, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when an ability generates a stun (used through the ICombat inferface).
|
||||
/// </summary>
|
||||
public void TriggerStun(float StunLength)
|
||||
{
|
||||
EmeraldComponent.AnimationComponent.PlayStunnedAnimation(StunLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e451fe8b9a2336b4189fd779cf157926
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,650 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using System.Linq;
|
||||
using static UnityEngine.GraphicsBuffer;
|
||||
|
||||
namespace EmeraldAI
|
||||
{
|
||||
[HelpURL("https://black-horizon-studios.gitbook.io/emerald-ai-wiki/emerald-components-required/detection-component")]
|
||||
public class EmeraldDetection : MonoBehaviour, IFaction
|
||||
{
|
||||
#region Detection Variables
|
||||
public List<Collider> IgnoredColliders = new List<Collider>();
|
||||
public static LayerMask LBDLayers;
|
||||
public Transform CurrentObstruction;
|
||||
public Transform HeadTransform;
|
||||
public float DetectionFrequency = 1;
|
||||
public LayerMask DetectionLayerMask = 3;
|
||||
public LayerMask ObstructionDetectionLayerMask = 4;
|
||||
LayerMask InternalObstructionLayerMask = 4;
|
||||
public string PlayerTag = "Player";
|
||||
public float ObstructionDetectionFrequency = 0.1f;
|
||||
public float ObstructionDetectionUpdateTimer;
|
||||
public float ObstructionSeconds = 1.5f;
|
||||
public int StartingDetectionRadius;
|
||||
public int DetectionRadius = 18;
|
||||
public int StartingChaseDistance;
|
||||
public int FieldOfViewAngle = 270;
|
||||
public int StartingFieldOfViewAngle;
|
||||
public enum DetectionStates { Alert = 0, Unaware = 1 };
|
||||
public DetectionStates CurrentDetectionState = DetectionStates.Unaware;
|
||||
public PickTargetTypes PickTargetType = PickTargetTypes.Closest;
|
||||
public bool TargetObstructed = false;
|
||||
public enum ObstructedTypes { AI, Other, None };
|
||||
public ObstructedTypes ObstructionType = ObstructedTypes.None;
|
||||
public List<Collider> LineOfSightTargets = new List<Collider>();
|
||||
public List<Transform> CurrentFollowers = new List<Transform>();
|
||||
public delegate void OnDetectTargetHandler();
|
||||
public event OnDetectTargetHandler OnDetectionUpdate;
|
||||
public delegate void OnEnemyTargetDetectedHandler();
|
||||
public event OnEnemyTargetDetectedHandler OnEnemyTargetDetected;
|
||||
public delegate void OnNullTargetHandler();
|
||||
public event OnNullTargetHandler OnNullTarget;
|
||||
public delegate void OnPlayerDetectedHandler();
|
||||
public event OnPlayerDetectedHandler OnPlayerDetected;
|
||||
public static List<Transform> IgnoredTargetsList = new List<Transform>();
|
||||
#endregion
|
||||
|
||||
#region Faction Variables
|
||||
[SerializeField]
|
||||
public int CurrentFaction;
|
||||
public static EmeraldFactionData FactionData;
|
||||
[SerializeField]
|
||||
public static List<string> StringFactionList = new List<string>();
|
||||
public List<int> FactionRelations = new List<int>();
|
||||
[SerializeField]
|
||||
public List<FactionClass> FactionRelationsList = new List<FactionClass>();
|
||||
[SerializeField]
|
||||
public List<int> AIFactionsList = new List<int>();
|
||||
#endregion
|
||||
|
||||
#region Editor Specific Variables
|
||||
public bool HideSettingsFoldout;
|
||||
public bool DetectionFoldout;
|
||||
public bool TagFoldout;
|
||||
public bool FactionFoldout;
|
||||
#endregion
|
||||
|
||||
#region Private Variables
|
||||
float DetectionTimer;
|
||||
Vector3 TargetDirection;
|
||||
float ObstructionTimer;
|
||||
EmeraldSystem EmeraldComponent;
|
||||
#endregion
|
||||
|
||||
void Start()
|
||||
{
|
||||
InitializeDetection();
|
||||
Invoke(nameof(InitializeLayers), 0.1f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the layers from the ObstructionDetectionLayerMask. This also adds the AI's internal
|
||||
/// collider layer to its layers so its own colliders don't cause a false obstruction.
|
||||
/// </summary>
|
||||
void InitializeLayers ()
|
||||
{
|
||||
InternalObstructionLayerMask = ObstructionDetectionLayerMask;
|
||||
|
||||
for (int i = 0; i < 32; i++)
|
||||
{
|
||||
if (LBDLayers == (LBDLayers | (1 << i)))
|
||||
{
|
||||
InternalObstructionLayerMask |= (1 << i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize all detection related settings.
|
||||
/// </summary>
|
||||
void InitializeDetection ()
|
||||
{
|
||||
EmeraldComponent = GetComponent<EmeraldSystem>();
|
||||
EmeraldComponent.CombatComponent.OnExitCombat += ReturnToDefaultState; //Subscribe the ReturnToDefaultState function to the OnExitCombat delegate
|
||||
EmeraldComponent.HealthComponent.OnDeath += ClearTargetToFollow; //Subscribe the RemoveTargetToFollow function to the OnDeath delegate
|
||||
OnNullTarget += NullNonCombatTarget; //Subscribe the NullNonCombatTarget function to the OnNullTarget delegate
|
||||
|
||||
if (FactionData == null) FactionData = Resources.Load("Faction Data") as EmeraldFactionData;
|
||||
if (EmeraldComponent.LBDComponent == null) Utility.EmeraldCombatManager.DisableRagdoll(EmeraldComponent);
|
||||
|
||||
StartingDetectionRadius = DetectionRadius;
|
||||
TargetObstructed = true;
|
||||
StartingFieldOfViewAngle = FieldOfViewAngle;
|
||||
StartingDetectionRadius = DetectionRadius;
|
||||
|
||||
//If the user forgot to add a head transform, create a temporary one to avoid an error and still allow the AI to function.
|
||||
if (HeadTransform == null)
|
||||
{
|
||||
Transform TempHeadTransform = new GameObject("AI Head Transform").transform;
|
||||
TempHeadTransform.SetParent(transform);
|
||||
TempHeadTransform.localPosition = new Vector3(0, 1, 0);
|
||||
HeadTransform = TempHeadTransform;
|
||||
}
|
||||
|
||||
SetupFactions();
|
||||
Invoke(nameof(CheckFactionRelations), 0.1f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used during initialization to check and notify the user if an AI has its own faction as an Enemy Relation.
|
||||
/// </summary>
|
||||
void CheckFactionRelations()
|
||||
{
|
||||
if (AIFactionsList.Contains(CurrentFaction) && FactionRelations[AIFactionsList.IndexOf(CurrentFaction)] == 0)
|
||||
{
|
||||
Debug.LogError("The AI '" + gameObject.name + "' contains an Enemy Faction Relation of its own Faction '" + GetTargetFactionName(transform) +
|
||||
"'. Please remove the faction from the AI Faction Relation List (within the AI's Detection Component) or change it to Friendly to avoid incorrect target detection.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the Factions to be used during runtime.
|
||||
/// </summary>
|
||||
void SetupFactions()
|
||||
{
|
||||
for (int i = 0; i < FactionRelationsList.Count; i++)
|
||||
{
|
||||
AIFactionsList.Add(FactionRelationsList[i].FactionIndex);
|
||||
FactionRelations.Add((int)FactionRelationsList[i].RelationType);
|
||||
}
|
||||
}
|
||||
|
||||
void FixedUpdate()
|
||||
{
|
||||
if (EmeraldComponent.BehaviorsComponent.CurrentBehaviorType == EmeraldBehaviors.BehaviorTypes.Passive) return; //Don't allow passive AI to use line of sight
|
||||
|
||||
if (!EmeraldComponent.CombatComponent.CombatState || EmeraldComponent.CombatComponent.DeathDelayActive)
|
||||
{
|
||||
LineOfSightDetection();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A custom update function for the EmeraldDetection script called through the EmeraldAISystem script.
|
||||
/// </summary>
|
||||
public void DetectionUpdate()
|
||||
{
|
||||
if (EmeraldComponent.CombatComponent.CombatState) CheckForObstructions(EmeraldComponent.CombatTarget); //When in combat, check for obstructions by casting a ray from the AI's Head Transform to its target.
|
||||
else if (!EmeraldComponent.CombatComponent.CombatState) CheckForObstructions(EmeraldComponent.LookAtTarget); //When in not in combat, check for obstructions by casting a ray from the AI's Head Transform to its look at target.
|
||||
|
||||
//Update the AI's OverlapShere function based on the DetectionFrequency
|
||||
if (EmeraldComponent.CombatComponent.TargetDetectionActive && !EmeraldComponent.MovementComponent.ReturningToStartInProgress)
|
||||
{
|
||||
DetectionTimer += Time.deltaTime;
|
||||
|
||||
if (DetectionTimer >= DetectionFrequency)
|
||||
{
|
||||
UpdateAIDetection(); //Casts a Physics.OverlapSphere and only searches for layers based on the user set DetectionLayerMask.
|
||||
LookAtTargetDistanceCheck(); //Check that the LookAtTarget is within the AI's DetectionRadius.
|
||||
OnDetectionUpdate?.Invoke(); //Invoke the OnDetectionUpdate event.
|
||||
DetectionTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
CheckForNullTarget(); //Monitors the AI's TargetSource to see if it becomes null. If it does, invoke the OnNullTarget callback.
|
||||
CheckLookAtTarget(); //Monitors the AI's LookAtTarget to see if its health reaches 0. If it does, clear the LookAtTarget information.
|
||||
ObstructionAction(); //Controls what happens depending on if the AI is obstructed by another AI or by something else, while in combat.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Monitors the AI's TargetSource to see if it becomes null. If it does, invoke the OnNullTarget callback.
|
||||
/// </summary>
|
||||
void CheckForNullTarget()
|
||||
{
|
||||
if (EmeraldComponent.CurrentTargetInfo.TargetSource == null && EmeraldComponent.CurrentTargetInfo.CurrentICombat != null)
|
||||
{
|
||||
OnNullTarget?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called through the OnNullTarget callback when a target becomes null. If this happens, clear all non-combat targets.
|
||||
/// </summary>
|
||||
void NullNonCombatTarget ()
|
||||
{
|
||||
if (!EmeraldComponent.CombatComponent.CombatState)
|
||||
{
|
||||
EmeraldComponent.TargetToFollow = null;
|
||||
EmeraldComponent.LookAtTarget = null;
|
||||
EmeraldComponent.CurrentTargetInfo.TargetSource = null;
|
||||
EmeraldComponent.CurrentTargetInfo.CurrentIDamageable = null;
|
||||
EmeraldComponent.CurrentTargetInfo.CurrentICombat = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Monitors the AI's LookAtTarget to see if its health reaches 0. If it does, clear the LookAtTarget information.
|
||||
/// </summary>
|
||||
void CheckLookAtTarget ()
|
||||
{
|
||||
if (EmeraldComponent.LookAtTarget && !EmeraldComponent.CombatComponent.CombatState && EmeraldComponent.CurrentTargetInfo.CurrentIDamageable.Health <= 0)
|
||||
{
|
||||
EmeraldComponent.LookAtTarget = null;
|
||||
EmeraldComponent.CurrentTargetInfo.CurrentIDamageable = null;
|
||||
EmeraldComponent.CurrentTargetInfo.CurrentICombat = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Controls what happens depending on if the AI is obstructed by another AI or by something else, while in combat.
|
||||
/// </summary>
|
||||
void ObstructionAction ()
|
||||
{
|
||||
if (TargetObstructed && EmeraldComponent.CombatComponent.CombatState && EmeraldComponent.MovementComponent.CanReachTarget)
|
||||
{
|
||||
ObstructionTimer += Time.deltaTime;
|
||||
if (ObstructionTimer >= ObstructionSeconds)
|
||||
{
|
||||
if (ObstructionType == ObstructedTypes.AI)
|
||||
{
|
||||
SearchForTarget(PickTargetTypes.Random);
|
||||
}
|
||||
else if (ObstructionType == ObstructedTypes.Other)
|
||||
{
|
||||
EmeraldComponent.m_NavMeshAgent.stoppingDistance = 3;
|
||||
EmeraldAPI.Internal.GenerateRandomPositionWithinRadius(EmeraldComponent);
|
||||
}
|
||||
|
||||
ObstructionTimer = 0;
|
||||
}
|
||||
}
|
||||
else if (!TargetObstructed && EmeraldComponent.CombatComponent.CombatState && !EmeraldComponent.AnimationComponent.IsBackingUp)
|
||||
{
|
||||
EmeraldComponent.m_NavMeshAgent.stoppingDistance = EmeraldComponent.CombatComponent.AttackDistance;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Casts a Physics.OverlapSphere and only searches for layers based on the user set DetectionLayerMask.
|
||||
/// </summary>
|
||||
public void UpdateAIDetection()
|
||||
{
|
||||
if (LineOfSightTargets.Count > 0) LineOfSightTargetsDistanceCheck();
|
||||
|
||||
Collider[] CurrentlyDetectedTargets = Physics.OverlapSphere(transform.position, DetectionRadius, DetectionLayerMask);
|
||||
|
||||
foreach (Collider C in CurrentlyDetectedTargets)
|
||||
{
|
||||
if (C.gameObject != this.gameObject && IsValidTarget(C.transform))
|
||||
{
|
||||
DetectTarget(C.transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assigns a target based on the passed parameter and an AI's settings. Store the target within LineOfSightTargets to be used elsewhere.
|
||||
/// </summary>
|
||||
void DetectTarget(Transform Target)
|
||||
{
|
||||
if (IgnoredTargetsList.Contains(Target))
|
||||
return;
|
||||
|
||||
if (Target != EmeraldComponent.TargetToFollow && !CurrentFollowers.Contains(Target) && IsEnemyTarget(Target) && EmeraldComponent.BehaviorsComponent.CurrentBehaviorType != EmeraldBehaviors.BehaviorTypes.Passive)
|
||||
{
|
||||
CurrentDetectionState = DetectionStates.Alert;
|
||||
if (!LineOfSightTargets.Contains(Target.GetComponent<Collider>()))
|
||||
LineOfSightTargets.Add(Target.GetComponent<Collider>());
|
||||
}
|
||||
|
||||
if (EmeraldComponent.LookAtTarget == null && EmeraldComponent.CombatTarget == null)
|
||||
{
|
||||
if (IsLookAtTarget(Target))
|
||||
{
|
||||
EmeraldComponent.LookAtTarget = Target;
|
||||
GetTargetInfo(EmeraldComponent.LookAtTarget);
|
||||
OnPlayerDetected?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the AI's line of sight mechanics. For each target that is within the AI's LineOfSightTargets, cast a raycast. If a target is unobstructed, and within the AI's line of sight angle, call the SearchForTarget function.
|
||||
/// </summary>
|
||||
void LineOfSightDetection ()
|
||||
{
|
||||
if (CurrentDetectionState == DetectionStates.Alert && EmeraldComponent.CombatTarget == null && !EmeraldComponent.AnimationComponent.IsDead)
|
||||
{
|
||||
for (int i = LineOfSightTargets.Count - 1; i >= 0; --i)
|
||||
{
|
||||
if (LineOfSightTargets[i] == null)
|
||||
{
|
||||
LineOfSightTargets.RemoveAt(i);
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector3 direction = LineOfSightTargets[i].bounds.center - HeadTransform.position;
|
||||
float angle = Vector3.Angle(new Vector3(direction.x, 0, direction.z), transform.forward);
|
||||
|
||||
//Only check targets that are within the AI's line of sight.
|
||||
if (angle < FieldOfViewAngle * 0.5f)
|
||||
{
|
||||
if (!EmeraldComponent.CombatComponent.CombatState)
|
||||
{
|
||||
RaycastHit hit;
|
||||
//Use a special layer mask that also includes the layers of internal colliders (from the LDB component) as these can block the AI's line of sight.
|
||||
if (Physics.Raycast(HeadTransform.position, direction, out hit, DetectionRadius, ~InternalObstructionLayerMask))
|
||||
{
|
||||
if (hit.collider != null && LineOfSightTargets.Contains(hit.collider))
|
||||
{
|
||||
SearchForTarget(PickTargetType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (EmeraldComponent.CombatComponent.CombatState)
|
||||
{
|
||||
SearchForTarget(PickTargetType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return all currently visible targets within the AI's detection radius.
|
||||
/// </summary>
|
||||
public List<Transform> GetVisibleTargets()
|
||||
{
|
||||
List<Transform> VisibleTargets = new List<Transform>();
|
||||
|
||||
foreach (Collider C in LineOfSightTargets.ToArray())
|
||||
{
|
||||
RaycastHit hit;
|
||||
Vector3 direction = C.bounds.center - HeadTransform.position;
|
||||
|
||||
if (Physics.Raycast(HeadTransform.position, direction, out hit, DetectionRadius, ~InternalObstructionLayerMask))
|
||||
{
|
||||
if (hit.collider != null && LineOfSightTargets.Contains(hit.collider))
|
||||
{
|
||||
if (!VisibleTargets.Contains(hit.collider.transform) && EmeraldComponent.CombatTarget != hit.collider.transform || hit.collider.CompareTag("Player"))
|
||||
{
|
||||
VisibleTargets.Add(hit.collider.transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return VisibleTargets;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches for a currently visible target within the LineOfSightTargets list using passed PickTargetType. This can be assigned using EmeraldDetection.PickTargetTypes.
|
||||
/// </summary>
|
||||
public void SearchForTarget (PickTargetTypes pickTargetType)
|
||||
{
|
||||
List<Transform> VisibleTargets = GetVisibleTargets();
|
||||
if (EmeraldComponent.CombatTarget != null) VisibleTargets.Remove(EmeraldComponent.CombatTarget); //Remove the current target so it isn't picked again
|
||||
|
||||
if (VisibleTargets.Count > 0)
|
||||
{
|
||||
if (pickTargetType == PickTargetTypes.Closest)
|
||||
{
|
||||
VisibleTargets = VisibleTargets.OrderBy(Target => (Target.position - transform.position).sqrMagnitude).ToList();
|
||||
SetDetectedTarget(VisibleTargets[0]);
|
||||
}
|
||||
else if (pickTargetType == PickTargetTypes.Random)
|
||||
{
|
||||
SetDetectedTarget(VisibleTargets[Random.Range(0, VisibleTargets.Count)]);
|
||||
}
|
||||
else if (pickTargetType == PickTargetTypes.FirstDetected)
|
||||
{
|
||||
SetDetectedTarget(VisibleTargets[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check for obstructions by casting a ray from the AI's Head Transform to its target.
|
||||
/// </summary>
|
||||
void CheckForObstructions (Transform TargetSource)
|
||||
{
|
||||
ObstructionDetectionUpdateTimer += Time.deltaTime;
|
||||
|
||||
if (ObstructionDetectionUpdateTimer >= ObstructionDetectionFrequency && TargetSource != null && EmeraldComponent.CurrentTargetInfo.CurrentICombat != null)
|
||||
{
|
||||
TargetDirection = EmeraldComponent.CurrentTargetInfo.CurrentICombat.DamagePosition() - HeadTransform.position;
|
||||
|
||||
RaycastHit hit;
|
||||
|
||||
//Check for obstructions and incrementally lower our AI's stopping distance until one is found. If none are found when the distance has reached 5 or below, search for a new target to see if there is a better option
|
||||
if (Physics.Raycast(HeadTransform.position, (TargetDirection), out hit, EmeraldComponent.CombatComponent.DistanceFromTarget, ~InternalObstructionLayerMask))
|
||||
{
|
||||
if (!hit.collider.transform.IsChildOf(TargetSource) && !hit.collider.transform.IsChildOf(this.transform) && hit.collider.transform != TargetSource && !IgnoredColliders.Contains(hit.collider))
|
||||
{
|
||||
//Set the ObstructionType so different actions can be taken when an AI's line of sight becomes obstructed.
|
||||
if ((LBDLayers & (1 << hit.collider.gameObject.layer)) != 0 || (DetectionLayerMask & (1 << hit.collider.gameObject.layer)) != 0)
|
||||
{
|
||||
ObstructionType = ObstructedTypes.AI;
|
||||
}
|
||||
else
|
||||
{
|
||||
ObstructionType = ObstructedTypes.Other;
|
||||
}
|
||||
|
||||
EmeraldComponent.AIAnimator.ResetTrigger("Attack");
|
||||
TargetObstructed = true;
|
||||
CurrentObstruction = hit.collider.transform;
|
||||
}
|
||||
else
|
||||
{
|
||||
TargetObstructed = false;
|
||||
CurrentObstruction = null;
|
||||
ObstructionType = ObstructedTypes.None;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TargetObstructed = false;
|
||||
CurrentObstruction = null;
|
||||
ObstructionType = ObstructedTypes.None;
|
||||
}
|
||||
|
||||
ObstructionDetectionUpdateTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects the passed target's Target Type and assigns it as the AI's current target.
|
||||
/// </summary>
|
||||
public void SetDetectedTarget (Transform DetectedTarget)
|
||||
{
|
||||
//Don't assign the newly detected target if it's the same as the current target.
|
||||
if (EmeraldComponent.CombatTarget == DetectedTarget) return;
|
||||
|
||||
EmeraldAI.Utility.EmeraldCombatManager.ActivateCombatState(EmeraldComponent); //Active the Combat State
|
||||
ResetDetectionValues(); //Once a target has been found, reset some of its settings back to their defaults.
|
||||
GetTargetInfo(DetectedTarget);
|
||||
EmeraldComponent.CombatTarget = DetectedTarget;
|
||||
OnEnemyTargetDetected?.Invoke(); //Invoke the OnEnemyTargetDetected when an enemy target has been found.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Once a target has been found, reset some of its settings back to their defaults.
|
||||
/// </summary>
|
||||
void ResetDetectionValues ()
|
||||
{
|
||||
DetectionRadius = StartingDetectionRadius;
|
||||
FieldOfViewAngle = StartingFieldOfViewAngle;
|
||||
EmeraldComponent.m_NavMeshAgent.stoppingDistance = EmeraldComponent.CombatComponent.AttackDistance;
|
||||
EmeraldComponent.AnimationComponent.IsTurning = false;
|
||||
EmeraldComponent.CombatComponent.DeathDelayActive = false;
|
||||
EmeraldComponent.CombatComponent.DeathDelayTimer = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the target's info (from the passed Target parameter).
|
||||
/// </summary>
|
||||
public void GetTargetInfo (Transform Target, bool? OverrideFactionRequirement = false)
|
||||
{
|
||||
if (Target != null)
|
||||
{
|
||||
EmeraldComponent.CurrentTargetInfo.TargetSource = Target;
|
||||
EmeraldComponent.CurrentTargetInfo.CurrentIDamageable = Target.GetComponent<IDamageable>();
|
||||
EmeraldComponent.CurrentTargetInfo.CurrentICombat = Target.GetComponent<ICombat>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check that each LineOfSightTarget is within the AI's DetectionRadius. If not, remove it from the list.
|
||||
/// </summary>
|
||||
void LineOfSightTargetsDistanceCheck()
|
||||
{
|
||||
for (int i = 0; i < LineOfSightTargets.Count; i++)
|
||||
{
|
||||
//Remove any targets that become null during the distance check.
|
||||
if (LineOfSightTargets[i] == null)
|
||||
{
|
||||
LineOfSightTargets.RemoveAt(i);
|
||||
}
|
||||
else
|
||||
{
|
||||
float distance = Vector3.Distance(LineOfSightTargets[i].transform.position, transform.position);
|
||||
|
||||
//If the distance of the detected target is greater than the DetectionRadius, remove it from the LineOfSightTargets list.
|
||||
if (distance > DetectionRadius)
|
||||
LineOfSightTargets.Remove(LineOfSightTargets[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check that the LookAtTarget is within the AI's DetectionRadius. If not, remove it as the current LookAtTarget.
|
||||
/// </summary>
|
||||
void LookAtTargetDistanceCheck ()
|
||||
{
|
||||
if (EmeraldComponent.LookAtTarget != null)
|
||||
{
|
||||
float distance = Vector3.Distance(EmeraldComponent.LookAtTarget.transform.position, transform.position);
|
||||
|
||||
//If the distance of the detected LookAtTarget is greater than the DetectionRadius, remove it as the current LookAtTarget.
|
||||
if (distance > DetectionRadius)
|
||||
NullNonCombatTarget();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return to the default state and assign the Look At Target info, if it's not null. This is called through the OnExitCombat callback.
|
||||
/// </summary>
|
||||
void ReturnToDefaultState ()
|
||||
{
|
||||
CurrentDetectionState = DetectionStates.Unaware;
|
||||
if (EmeraldComponent.LookAtTarget != null)
|
||||
GetTargetInfo(EmeraldComponent.LookAtTarget);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Faction Relation name of the passed target and this AI in the form of a string (Enemy, Neutral, or Friendly). If a faction cannot be found, or if it is not a valid target, you will receive a value of Invalid Target.
|
||||
/// </summary>
|
||||
public string GetTargetFactionRelation (Transform Target)
|
||||
{
|
||||
return EmeraldAPI.Faction.GetTargetFactionRelation(EmeraldComponent, Target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the faction name of the passed AI target.
|
||||
/// </summary>
|
||||
public string GetTargetFactionName(Transform Target)
|
||||
{
|
||||
return EmeraldAPI.Faction.GetTargetFactionName(Target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assigns a new follow target for an AI to follow.
|
||||
/// </summary>
|
||||
public void SetTargetToFollow(Transform Target, bool CopyFactionData = true)
|
||||
{
|
||||
EmeraldSystem TargetEmeraldComponent = Target.GetComponent<EmeraldSystem>(); //Attempt to get the Target's EmeraldComponent
|
||||
if (TargetEmeraldComponent != null)
|
||||
{
|
||||
if (TargetEmeraldComponent.CombatTarget == transform) TargetEmeraldComponent.CombatComponent.ClearTarget(); //If the Target is another AI, clear its targets
|
||||
TargetEmeraldComponent.DetectionComponent.CurrentFollowers.Add(transform); //Add this AI as a follower of the leader AI
|
||||
if (TargetEmeraldComponent.CombatComponent.CombatState) TargetEmeraldComponent.CombatComponent.DeathDelayActive = true;
|
||||
|
||||
//Copies the Target to Follow's Faction Data so it will react the same way the follower does to detected targets.
|
||||
if (CopyFactionData)
|
||||
{
|
||||
CurrentFaction = TargetEmeraldComponent.DetectionComponent.CurrentFaction; //Make the Current Faction the same as the AI's new Target to Follow
|
||||
AIFactionsList = TargetEmeraldComponent.DetectionComponent.AIFactionsList; //Make the Faction List the same as the AI's new Target to Follow
|
||||
FactionRelations = TargetEmeraldComponent.DetectionComponent.FactionRelations; //Make the Faction Relations the same as the AI's new Target to Follow
|
||||
FactionRelationsList = TargetEmeraldComponent.DetectionComponent.FactionRelationsList; //Make the Faction Relations List the same as the AI's new Target to Follow
|
||||
}
|
||||
}
|
||||
|
||||
if (Target == EmeraldComponent.CombatTarget) EmeraldComponent.CombatComponent.ClearTarget();
|
||||
if (EmeraldComponent.CombatComponent.CombatState) EmeraldComponent.CombatComponent.DeathDelayActive = true;
|
||||
//EmeraldComponent.m_NavMeshAgent.stoppingDistance = EmeraldComponent.MovementComponent.FollowingStoppingDistance;
|
||||
EmeraldComponent.TargetToFollow = Target;
|
||||
EmeraldComponent.BehaviorsComponent.TargetToFollow = Target;
|
||||
EmeraldComponent.BehaviorsComponent.ResetState();
|
||||
EmeraldComponent.MovementComponent.CurrentMovementState = EmeraldMovement.MovementStates.Run;
|
||||
EmeraldComponent.MovementComponent.WanderType = EmeraldMovement.WanderTypes.Stationary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the AI's Target to Follow transform so it will be no longer following it. This will also stop the AI from being a Companion AI.
|
||||
/// </summary>
|
||||
public void ClearTargetToFollow()
|
||||
{
|
||||
//This is also called through the OnDeath callback and removes this AI as a follower of its Target to Follow.
|
||||
if (EmeraldComponent.TargetToFollow)
|
||||
{
|
||||
EmeraldSystem TargetEmeraldComponent = EmeraldComponent.TargetToFollow.GetComponent<EmeraldSystem>(); //Attempt to get this AI's Target to Follow
|
||||
|
||||
if (TargetEmeraldComponent)
|
||||
{
|
||||
TargetEmeraldComponent.DetectionComponent.CurrentFollowers.Remove(transform); //Remove this AI as a follower of the leader AI
|
||||
}
|
||||
}
|
||||
|
||||
EmeraldComponent.BehaviorsComponent.TargetToFollow = null;
|
||||
EmeraldComponent.TargetToFollow = null;
|
||||
|
||||
if (!EmeraldComponent.AnimationComponent.IsDead)
|
||||
{
|
||||
EmeraldComponent.BehaviorsComponent.ResetState();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the currently passed transform is an enemy target.
|
||||
/// </summary>
|
||||
bool IsEnemyTarget (Transform Target)
|
||||
{
|
||||
int ReceivedFaction = Target.GetComponent<IFaction>().GetFaction();
|
||||
return AIFactionsList.Contains(ReceivedFaction) && FactionRelations[AIFactionsList.IndexOf(ReceivedFaction)] == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the currently passed transform is a valid look at target.
|
||||
/// </summary>
|
||||
bool IsLookAtTarget(Transform Target)
|
||||
{
|
||||
return Target.gameObject.CompareTag(PlayerTag) && GetTargetFactionRelation(Target) != "Enemy";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks to see if the passed target is a valid player, AI, or non-AI target.
|
||||
/// </summary>
|
||||
bool IsValidTarget (Transform Target)
|
||||
{
|
||||
if (Target.GetComponent<IFaction>() != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("The " + Target.name + " object is set as a valid target (both Tag and Layer), but does not have a Faction Extension component on it. Please add one in order for this target to be properly detected.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public int GetFaction()
|
||||
{
|
||||
return CurrentFaction;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fed907ec0d3ff2a4b90eb7a27b5fad73
|
||||
timeCreated: 1540066880
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,249 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using System;
|
||||
using EmeraldAI.Utility;
|
||||
|
||||
namespace EmeraldAI
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles how an receives damage and track health. The Damage function is called through the IDamageable interface script.
|
||||
/// </summary>
|
||||
[HelpURL("https://black-horizon-studios.gitbook.io/emerald-ai-wiki/emerald-components-required/health-component")]
|
||||
public class EmeraldHealth : MonoBehaviour, IDamageable
|
||||
{
|
||||
#region Health variables
|
||||
public int CurrentHealth = 50;
|
||||
public int StartingHealth = 50;
|
||||
public int HealRate = 0;
|
||||
public bool Immortal = false;
|
||||
public List<string> CurrentActiveEffects;
|
||||
public bool HitEffectFoldout;
|
||||
public YesOrNo UseHitEffect = YesOrNo.No;
|
||||
public Vector3 HitEffectPosOffset;
|
||||
public float HitEffectTimeoutSeconds = 3f;
|
||||
public List<GameObject> HitEffectsList = new List<GameObject>();
|
||||
public delegate void DamageHandler();
|
||||
public event DamageHandler OnTakeDamage;
|
||||
public delegate void TakeCritDamageHandler();
|
||||
public event TakeCritDamageHandler OnTakeCritDamage;
|
||||
public delegate void AnyDamageHandler();
|
||||
public event DamageHandler OnTakeAnyDamage;
|
||||
public delegate void BlockHandler();
|
||||
public event BlockHandler OnBlock;
|
||||
public delegate void DodgeHandler();
|
||||
public event DodgeHandler OnDodge;
|
||||
public delegate void DeathHandler();
|
||||
public event DeathHandler OnDeath;
|
||||
public delegate void HealRateTickHandler();
|
||||
public event HealRateTickHandler OnHealRateTick;
|
||||
public delegate void HealthChangeHandler();
|
||||
public event HealthChangeHandler OnHealthChange;
|
||||
EmeraldSystem EmeraldComponent;
|
||||
|
||||
public List<string> ActiveEffects { get => CurrentActiveEffects; set => CurrentActiveEffects = value; }
|
||||
public int Health { get => CurrentHealth; set => CurrentHealth = value; }
|
||||
public int StartHealth { get => StartingHealth; set => StartingHealth = value; }
|
||||
#endregion
|
||||
|
||||
#region Editor Variables
|
||||
public bool HideSettingsFoldout;
|
||||
public bool HealthFoldout;
|
||||
#endregion
|
||||
|
||||
void Start ()
|
||||
{
|
||||
CurrentHealth = StartingHealth;
|
||||
EmeraldComponent = GetComponent<EmeraldSystem>();
|
||||
EmeraldComponent.CombatComponent.OnExitCombat += StartHealing; //Subscribe to the OnExitCombat event for StartHealing
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Damages the AI and allows it to block and mitigate damage, if enabled. To use the ragdoll feature, all
|
||||
/// parameters need to be used where AttackerTransform is the current attacker.
|
||||
/// </summary>
|
||||
/// <param name="DamageAmount">Amount of damage caused during attack.</param>
|
||||
/// <param name="AttackerTransform">The transform of the current attacker.</param>
|
||||
/// <param name="RagdollForce">The amount of force to apply to this AI when they die. (Use Ragdoll must be enabled on this AI)</param>
|
||||
public void Damage(int DamageAmount, Transform AttackerTransform = null, int RagdollForce = 100, bool CriticalHit = false)
|
||||
{
|
||||
if (EmeraldComponent.AnimationComponent.IsDead || transform.localScale == Vector3.one * 0.003f || AttackerTransform && AttackerTransform == EmeraldComponent.TargetToFollow || AttackerTransform && EmeraldComponent.DetectionComponent.GetTargetFactionRelation(AttackerTransform) == "Friendly") return;
|
||||
|
||||
//Check for an attacker if there's no current target.
|
||||
if (AttackerTransform != null) CheckForAttacker(AttackerTransform);
|
||||
|
||||
//Cache the reference to the newest/current attacker.
|
||||
EmeraldComponent.CombatComponent.LastAttacker = AttackerTransform;
|
||||
|
||||
//Get the angle from the current attacker to determine if the incoming hit can be blocked or dodge.
|
||||
float AttackerAngle = EmeraldCombatManager.TransformAngle(EmeraldComponent, AttackerTransform);
|
||||
|
||||
//Check to see if the current attack is being blocked
|
||||
bool Blocked = (EmeraldComponent.AnimationComponent.IsBlocking && AttackerAngle <= EmeraldComponent.CombatComponent.MaxMitigationAngle && EmeraldComponent.AIAnimator.GetBool("Blocking"));
|
||||
|
||||
//Check to see if the current attack is being dodged
|
||||
bool Dodged = (EmeraldComponent.AnimationComponent.IsDodging && AttackerAngle <= EmeraldComponent.CombatComponent.MaxMitigationAngle);
|
||||
|
||||
//Set the base calculated damage equal to DamageAmount. If it is dodged or blocked, the amount will be adjusted below.
|
||||
int CalculatedDamage = DamageAmount;
|
||||
|
||||
if (Blocked) CalculatedDamage = Mathf.FloorToInt(Mathf.Abs((DamageAmount * ((EmeraldComponent.CombatComponent.MitigationAmount) * 0.01f)) - DamageAmount)); //Mitigate the damage from blocking
|
||||
if (Dodged) CalculatedDamage = Mathf.FloorToInt(Mathf.Abs((DamageAmount * ((EmeraldComponent.CombatComponent.MitigationAmount) * 0.01f)) - DamageAmount)); //Mitigate the damage from dodging
|
||||
|
||||
//Don't reduce an AI's health if Immortal is enabled
|
||||
if (!Immortal)
|
||||
Health -= CalculatedDamage;
|
||||
|
||||
//Display the damage dealt through the Combat Text System, given that it's enabled.
|
||||
if (CalculatedDamage > 0) CombatTextSystem.Instance.CreateCombatTextAI(CalculatedDamage, EmeraldComponent.CombatComponent.DamagePosition(), CriticalHit, false);
|
||||
|
||||
//In order to have the most reliable On Do Damage events, simply invoke the attacker's OnDoDoamage callback through a public function, given it is an Emerald AI agent.
|
||||
if (AttackerTransform != null)
|
||||
{
|
||||
EmeraldSystem AttackEmeraldComponent = AttackerTransform.GetComponent<EmeraldSystem>();
|
||||
if (AttackEmeraldComponent != null) AttackEmeraldComponent.CombatComponent.InvokeDoDamage();
|
||||
if (AttackEmeraldComponent != null && CriticalHit) AttackEmeraldComponent.CombatComponent.InvokeDoCritDamage();
|
||||
}
|
||||
|
||||
//Invoke the damage delegates
|
||||
if (!CriticalHit && CalculatedDamage > 0) OnTakeDamage?.Invoke();
|
||||
else if (CriticalHit && CalculatedDamage > 0) OnTakeCritDamage?.Invoke();
|
||||
OnTakeAnyDamage?.Invoke();
|
||||
|
||||
//Create hit effect, if it's enabled, the AI is not blocking or dodging, and the damage is greater than 0.
|
||||
if (!Blocked && !Dodged && CalculatedDamage > 0) CreateHitEffect();
|
||||
|
||||
if (Blocked) OnBlock?.Invoke(); //Invoke the block delegate
|
||||
else if (Dodged) OnDodge?.Invoke(); //Invoke the dodge delegate
|
||||
|
||||
//The AI has died, initialize its death state.
|
||||
if (Health <= 0 && !EmeraldComponent.AnimationComponent.IsDead)
|
||||
{
|
||||
EmeraldComponent.CombatComponent.ReceivedRagdollForceAmount = RagdollForce;
|
||||
Health = 0;
|
||||
Death();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when an AI receives damage, but has no current target (typically from an unseen attacker). When this happenss, assign the
|
||||
/// attacker as the current target (if the have a Neutral Relation Type or higher) or search for any visible targets within the AI's Line of Sight.
|
||||
/// </summary>
|
||||
void CheckForAttacker (Transform AttackerTransform)
|
||||
{
|
||||
if (EmeraldComponent.CombatTarget == null && !EmeraldComponent.CombatComponent.CombatState)
|
||||
{
|
||||
StartCoroutine(DelaySetDetectedTarget(AttackerTransform));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delay SetDetectedTarget to give the AI time to play its non-combat hit animation and transfer to its combat state.
|
||||
/// </summary>
|
||||
IEnumerator DelaySetDetectedTarget(Transform AttackerTransform)
|
||||
{
|
||||
string RelationName = EmeraldComponent.DetectionComponent.GetTargetFactionRelation(AttackerTransform);
|
||||
|
||||
yield return new WaitForSeconds(0.6f);
|
||||
|
||||
if (RelationName == "Neutral" || RelationName == "Enemy")
|
||||
{
|
||||
EmeraldComponent.DetectionComponent.SetDetectedTarget(AttackerTransform);
|
||||
}
|
||||
else
|
||||
{
|
||||
EmeraldComponent.DetectionComponent.SearchForTarget(PickTargetTypes.Closest);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the AI's health reaches 0. The OnDeath delegate is also invoked and is responsible for triggering any death functionality to external subscribers.
|
||||
/// </summary>
|
||||
void Death()
|
||||
{
|
||||
OnDeath?.Invoke(); //Invoke the AI death event.
|
||||
EmeraldComponent.AnimationComponent.IsDead = true;
|
||||
EmeraldCombatManager.DisableComponents(EmeraldComponent);
|
||||
EmeraldCombatManager.EnableRagdoll(EmeraldComponent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refills the AI's health to full instantly
|
||||
/// </summary>
|
||||
public void InstantlyRefillAIHealth()
|
||||
{
|
||||
Health = StartHealth;
|
||||
CurrentHealth = StartHealth;
|
||||
OnHealthChange?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Instantly kills this AI.
|
||||
/// </summary>
|
||||
public void KillAI()
|
||||
{
|
||||
EmeraldAPI.Combat.KillAI(EmeraldComponent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called through the OnExitCombat callback when an AI exits combat. This heals the AI over time according to its HealRate.
|
||||
/// </summary>
|
||||
void StartHealing ()
|
||||
{
|
||||
StartCoroutine(StartHealingInternal());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called through the StartHealing function which increases the AI's health each HealRate. This gets canceled if the target enters combat.
|
||||
/// </summary>
|
||||
IEnumerator StartHealingInternal ()
|
||||
{
|
||||
float t = 0;
|
||||
|
||||
while (CurrentHealth < StartingHealth)
|
||||
{
|
||||
t += Time.deltaTime;
|
||||
|
||||
if (t >= 1)
|
||||
{
|
||||
CurrentHealth = CurrentHealth + HealRate;
|
||||
OnHealRateTick?.Invoke();
|
||||
t = 0;
|
||||
}
|
||||
|
||||
if (EmeraldComponent.CombatComponent.CombatState) yield break;
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
CurrentHealth = StartingHealth;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the AI's Max Health and Current Health.
|
||||
/// </summary>
|
||||
public void UpdateHealth(int MaxHealth, int CurrentHealth)
|
||||
{
|
||||
Health = CurrentHealth;
|
||||
StartHealth = MaxHealth;
|
||||
OnHealthChange?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates (an optional) hit effect (outside of an AI's Ability Objects) when an AI takes damage.
|
||||
/// </summary>
|
||||
void CreateHitEffect()
|
||||
{
|
||||
if (EmeraldComponent.HealthComponent.UseHitEffect == YesOrNo.Yes && !EmeraldComponent.LBDComponent && EmeraldComponent.HealthComponent.HitEffectsList.Count > 0 && !EmeraldComponent.AnimationComponent.IsDead)
|
||||
{
|
||||
GameObject RandomBloodEffect = EmeraldComponent.HealthComponent.HitEffectsList[UnityEngine.Random.Range(0, EmeraldComponent.HealthComponent.HitEffectsList.Count)];
|
||||
if (RandomBloodEffect != null)
|
||||
{
|
||||
GameObject SpawnedBlood = EmeraldObjectPool.SpawnEffect(RandomBloodEffect, Vector3.zero, EmeraldComponent.transform.rotation, EmeraldComponent.HealthComponent.HitEffectTimeoutSeconds) as GameObject;
|
||||
SpawnedBlood.transform.SetParent(EmeraldComponent.transform);
|
||||
SpawnedBlood.transform.position = EmeraldComponent.CombatComponent.DamagePosition() + EmeraldComponent.HealthComponent.HitEffectPosOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4435fe59ad515fd4d94c2dd09006f199
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6a91628e3b6f5c0418d73c9a0ccf4df1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,562 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace EmeraldAI
|
||||
{
|
||||
[HelpURL("https://black-horizon-studios.gitbook.io/emerald-ai-wiki/emerald-components-required/sounds-component")]
|
||||
public class EmeraldSounds : MonoBehaviour
|
||||
{
|
||||
#region Variables
|
||||
public Utility.EmeraldSoundProfile SoundProfile;
|
||||
public bool SoundProfileFoldout;
|
||||
public bool HideSettingsFoldout;
|
||||
public int IdleSoundsSeconds;
|
||||
public float IdleSoundsTimer;
|
||||
|
||||
public AudioSource m_AudioSource;
|
||||
public AudioSource m_SecondaryAudioSource;
|
||||
public AudioSource m_EventAudioSource;
|
||||
|
||||
EmeraldSystem EmeraldComponent;
|
||||
EmeraldHealth EmeraldHealth;
|
||||
EmeraldItems EmeraldItems;
|
||||
#endregion
|
||||
|
||||
void Awake()
|
||||
{
|
||||
InitializeSounds(); //Initialize the EmeraldSounds script.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the sound settings.
|
||||
/// </summary>
|
||||
public void InitializeSounds()
|
||||
{
|
||||
EmeraldHealth = GetComponent<EmeraldHealth>();
|
||||
EmeraldItems = GetComponent<EmeraldItems>();
|
||||
EmeraldComponent = GetComponent<EmeraldSystem>();
|
||||
|
||||
//Do not subscribe to any delegates if the sound profile is null.
|
||||
if (SoundProfile == null)
|
||||
return;
|
||||
|
||||
EmeraldHealth.OnTakeDamage += PlayInjuredSound; //Subscribe to the OnTakeDamage event for Injured Sounds
|
||||
EmeraldHealth.OnTakeCritDamage += PlayInjuredSound; //Subscribe to the OnTakeCritDamage event for Injured Sounds
|
||||
EmeraldHealth.OnBlock += PlayBlockSound; //Subscribe to the OnTakeDamage event for Block Sounds
|
||||
EmeraldHealth.OnDeath += PlayDeathSound; //Subscribe to the OnDeath event for Death Sounds
|
||||
|
||||
if (EmeraldItems != null)
|
||||
{
|
||||
EmeraldItems.OnEquipWeapon += PlayEquipSound; //Subscribe to the OnEquipWeapon event for Equip Sounds
|
||||
EmeraldItems.OnUnequipWeapon += PlayUnequipSound; //Subscribe to the OnUnequipWeapon event for Unequip Sounds
|
||||
}
|
||||
|
||||
IdleSoundsSeconds = Random.Range(SoundProfile.IdleSoundsSecondsMin, SoundProfile.IdleSoundsSecondsMax + 1);
|
||||
m_AudioSource = GetComponent<AudioSource>();
|
||||
m_SecondaryAudioSource = gameObject.AddComponent<AudioSource>();
|
||||
m_SecondaryAudioSource.priority = m_AudioSource.priority;
|
||||
m_SecondaryAudioSource.spatialBlend = m_AudioSource.spatialBlend;
|
||||
m_SecondaryAudioSource.minDistance = m_AudioSource.minDistance;
|
||||
m_SecondaryAudioSource.maxDistance = m_AudioSource.maxDistance;
|
||||
m_SecondaryAudioSource.rolloffMode = m_AudioSource.rolloffMode;
|
||||
m_EventAudioSource = gameObject.AddComponent<AudioSource>();
|
||||
m_EventAudioSource.priority = m_AudioSource.priority;
|
||||
m_EventAudioSource.spatialBlend = m_AudioSource.spatialBlend;
|
||||
m_EventAudioSource.minDistance = m_AudioSource.minDistance;
|
||||
m_EventAudioSource.maxDistance = m_AudioSource.maxDistance;
|
||||
m_EventAudioSource.rolloffMode = m_AudioSource.rolloffMode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Play a random idle sound when the IdleSoundsSeconds have been met.
|
||||
/// </summary>
|
||||
public void IdleSoundsUpdate ()
|
||||
{
|
||||
IdleSoundsTimer += Time.deltaTime;
|
||||
if (IdleSoundsTimer >= IdleSoundsSeconds)
|
||||
{
|
||||
PlayIdleSound();
|
||||
IdleSoundsTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a sound clip according to the Clip parameter.
|
||||
/// </summary>
|
||||
public void PlaySoundClip(AudioClip Clip)
|
||||
{
|
||||
if (!m_AudioSource.isPlaying)
|
||||
{
|
||||
m_AudioSource.volume = 1;
|
||||
m_AudioSource.PlayOneShot(Clip);
|
||||
}
|
||||
else if (!m_SecondaryAudioSource.isPlaying)
|
||||
{
|
||||
m_SecondaryAudioSource.volume = 1;
|
||||
m_SecondaryAudioSource.PlayOneShot(Clip);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_EventAudioSource.volume = 1;
|
||||
m_EventAudioSource.PlayOneShot(Clip);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a sound clip according to the Clip parameter with optional volume control.
|
||||
/// </summary>
|
||||
public void PlayAudioClip(AudioClip Clip, float Volume = 1)
|
||||
{
|
||||
if (!m_AudioSource.isPlaying)
|
||||
{
|
||||
m_AudioSource.volume = Volume;
|
||||
m_AudioSource.PlayOneShot(Clip);
|
||||
}
|
||||
else if (!m_SecondaryAudioSource.isPlaying)
|
||||
{
|
||||
m_SecondaryAudioSource.volume = Volume;
|
||||
m_SecondaryAudioSource.PlayOneShot(Clip);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_EventAudioSource.volume = Volume;
|
||||
m_EventAudioSource.PlayOneShot(Clip);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a sound clip according to the Clip parameter with a customizable volume.
|
||||
/// </summary>
|
||||
public void PlaySoundClipWithVolume(AudioClip Clip, float Volume)
|
||||
{
|
||||
if (!m_AudioSource.isPlaying)
|
||||
{
|
||||
m_AudioSource.volume = Volume;
|
||||
m_AudioSource.PlayOneShot(Clip);
|
||||
}
|
||||
else if (!m_SecondaryAudioSource.isPlaying)
|
||||
{
|
||||
m_SecondaryAudioSource.volume = Volume;
|
||||
m_SecondaryAudioSource.PlayOneShot(Clip);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_EventAudioSource.volume = Volume;
|
||||
m_EventAudioSource.PlayOneShot(Clip);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a random attack sound based on your AI's Attack Sounds list. Can also be called through Animation Events.
|
||||
/// </summary>
|
||||
public void PlayIdleSound()
|
||||
{
|
||||
if (SoundProfile && SoundProfile.IdleSounds.Count > 0)
|
||||
{
|
||||
if (!m_AudioSource.isPlaying)
|
||||
{
|
||||
AudioClip m_RandomIdleSoundClip = SoundProfile.IdleSounds[Random.Range(0, SoundProfile.IdleSounds.Count)];
|
||||
if (m_RandomIdleSoundClip != null)
|
||||
{
|
||||
m_AudioSource.volume = SoundProfile.IdleVolume;
|
||||
m_AudioSource.PlayOneShot(m_RandomIdleSoundClip);
|
||||
IdleSoundsSeconds = Random.Range(SoundProfile.IdleSoundsSecondsMin, SoundProfile.IdleSoundsSecondsMax);
|
||||
IdleSoundsSeconds = (int)m_RandomIdleSoundClip.length + IdleSoundsSeconds;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AudioClip m_RandomIdleSoundClip = SoundProfile.IdleSounds[Random.Range(0, SoundProfile.IdleSounds.Count)];
|
||||
if (m_RandomIdleSoundClip != null)
|
||||
{
|
||||
m_SecondaryAudioSource.volume = SoundProfile.IdleVolume;
|
||||
m_SecondaryAudioSource.PlayOneShot(m_RandomIdleSoundClip);
|
||||
IdleSoundsSeconds = Random.Range(SoundProfile.IdleSoundsSecondsMin, SoundProfile.IdleSoundsSecondsMax);
|
||||
IdleSoundsSeconds = (int)m_RandomIdleSoundClip.length + IdleSoundsSeconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a random attack sound based on your AI's Attack Sounds list. Can also be called through Animation Events.
|
||||
/// </summary>
|
||||
public void PlayAttackSound()
|
||||
{
|
||||
if (SoundProfile.AttackSounds.Count > 0)
|
||||
{
|
||||
if (!m_AudioSource.isPlaying)
|
||||
{
|
||||
m_AudioSource.volume = SoundProfile.AttackVolume;
|
||||
m_AudioSource.pitch = Mathf.Round(Random.Range(0.9f, 1.1f) * 10) / 10;
|
||||
m_AudioSource.PlayOneShot(SoundProfile.AttackSounds[Random.Range(0, SoundProfile.AttackSounds.Count)]);
|
||||
}
|
||||
else if (!m_SecondaryAudioSource.isPlaying)
|
||||
{
|
||||
m_SecondaryAudioSource.volume = SoundProfile.AttackVolume;
|
||||
m_SecondaryAudioSource.PlayOneShot(SoundProfile.AttackSounds[Random.Range(0, SoundProfile.AttackSounds.Count)]);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_EventAudioSource.volume = SoundProfile.AttackVolume;
|
||||
m_EventAudioSource.PlayOneShot(SoundProfile.AttackSounds[Random.Range(0, SoundProfile.AttackSounds.Count)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a equip sound based on your AI's Equip Weapon sounds (is called automatically through the EquipWeapon Animation Event).
|
||||
/// </summary>
|
||||
public void PlayEquipSound (string WeaponType)
|
||||
{
|
||||
if (WeaponType == "Weapon Type 1")
|
||||
{
|
||||
if (SoundProfile.UnsheatheWeapon != null)
|
||||
{
|
||||
m_AudioSource.volume = SoundProfile.EquipVolume;
|
||||
m_SecondaryAudioSource.volume = SoundProfile.EquipVolume;
|
||||
|
||||
if (!m_AudioSource.isPlaying)
|
||||
{
|
||||
m_AudioSource.PlayOneShot(SoundProfile.UnsheatheWeapon);
|
||||
}
|
||||
else if (!m_SecondaryAudioSource.isPlaying)
|
||||
{
|
||||
m_SecondaryAudioSource.PlayOneShot(SoundProfile.UnsheatheWeapon);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_EventAudioSource.PlayOneShot(SoundProfile.UnsheatheWeapon);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (WeaponType == "Weapon Type 2")
|
||||
{
|
||||
if (SoundProfile.RangedUnsheatheWeapon != null)
|
||||
{
|
||||
m_AudioSource.volume = SoundProfile.RangedEquipVolume;
|
||||
m_SecondaryAudioSource.volume = SoundProfile.RangedEquipVolume;
|
||||
|
||||
if (!m_AudioSource.isPlaying)
|
||||
{
|
||||
m_AudioSource.PlayOneShot(SoundProfile.RangedUnsheatheWeapon);
|
||||
}
|
||||
else if (!m_SecondaryAudioSource.isPlaying)
|
||||
{
|
||||
m_SecondaryAudioSource.PlayOneShot(SoundProfile.RangedUnsheatheWeapon);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_EventAudioSource.PlayOneShot(SoundProfile.RangedUnsheatheWeapon);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a unequip sound based on your AI's Unequip Weapon sounds (is called automatically through the UnequipWeapon Animation Event).
|
||||
/// </summary>
|
||||
public void PlayUnequipSound(string WeaponType)
|
||||
{
|
||||
if (WeaponType == "Weapon Type 1")
|
||||
{
|
||||
if (SoundProfile.SheatheWeapon != null)
|
||||
{
|
||||
m_AudioSource.volume = SoundProfile.UnequipVolume;
|
||||
m_SecondaryAudioSource.volume = SoundProfile.UnequipVolume;
|
||||
|
||||
if (!m_AudioSource.isPlaying)
|
||||
{
|
||||
m_AudioSource.PlayOneShot(SoundProfile.SheatheWeapon);
|
||||
}
|
||||
else if (!m_SecondaryAudioSource.isPlaying)
|
||||
{
|
||||
m_SecondaryAudioSource.PlayOneShot(SoundProfile.SheatheWeapon);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_EventAudioSource.PlayOneShot(SoundProfile.SheatheWeapon);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (WeaponType == "Weapon Type 2")
|
||||
{
|
||||
if (SoundProfile.RangedSheatheWeapon != null)
|
||||
{
|
||||
m_AudioSource.volume = SoundProfile.RangedUnequipVolume;
|
||||
m_SecondaryAudioSource.volume = SoundProfile.RangedUnequipVolume;
|
||||
|
||||
if (!m_AudioSource.isPlaying)
|
||||
{
|
||||
m_AudioSource.PlayOneShot(SoundProfile.RangedSheatheWeapon);
|
||||
}
|
||||
else if (!m_SecondaryAudioSource.isPlaying)
|
||||
{
|
||||
m_SecondaryAudioSource.PlayOneShot(SoundProfile.RangedSheatheWeapon);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_EventAudioSource.PlayOneShot(SoundProfile.RangedSheatheWeapon);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a random attack sound based on your AI's Attack Sounds list. Can also be called through Animation Events.
|
||||
/// </summary>
|
||||
public void PlayWarningSound()
|
||||
{
|
||||
if (SoundProfile.WarningSounds.Count > 0)
|
||||
{
|
||||
if (!m_AudioSource.isPlaying)
|
||||
{
|
||||
m_AudioSource.volume = SoundProfile.WarningVolume;
|
||||
m_AudioSource.PlayOneShot(SoundProfile.WarningSounds[Random.Range(0, SoundProfile.WarningSounds.Count)]);
|
||||
}
|
||||
else if (!m_SecondaryAudioSource.isPlaying)
|
||||
{
|
||||
m_SecondaryAudioSource.volume = SoundProfile.WarningVolume;
|
||||
m_SecondaryAudioSource.PlayOneShot(SoundProfile.WarningSounds[Random.Range(0, SoundProfile.WarningSounds.Count)]);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_EventAudioSource.volume = SoundProfile.WarningVolume;
|
||||
m_EventAudioSource.PlayOneShot(SoundProfile.WarningSounds[Random.Range(0, SoundProfile.WarningSounds.Count)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a random block sound based on your AI's Block Sounds list.
|
||||
/// </summary>
|
||||
public void PlayBlockSound()
|
||||
{
|
||||
if (SoundProfile.BlockingSounds.Count > 0)
|
||||
{
|
||||
if (!m_AudioSource.isPlaying)
|
||||
{
|
||||
m_AudioSource.volume = SoundProfile.BlockVolume;
|
||||
m_AudioSource.pitch = Mathf.Round(Random.Range(0.7f, 1.1f) * 10) / 10;
|
||||
m_AudioSource.PlayOneShot(SoundProfile.BlockingSounds[Random.Range(0, SoundProfile.BlockingSounds.Count)]);
|
||||
}
|
||||
else if (!m_SecondaryAudioSource.isPlaying)
|
||||
{
|
||||
m_SecondaryAudioSource.volume = SoundProfile.BlockVolume;
|
||||
m_SecondaryAudioSource.pitch = Mathf.Round(Random.Range(0.7f, 1.1f) * 10) / 10;
|
||||
m_SecondaryAudioSource.PlayOneShot(SoundProfile.BlockingSounds[Random.Range(0, SoundProfile.BlockingSounds.Count)]);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_EventAudioSource.volume = SoundProfile.BlockVolume;
|
||||
m_EventAudioSource.pitch = Mathf.Round(Random.Range(0.7f, 1.1f) * 10) / 10;
|
||||
m_EventAudioSource.PlayOneShot(SoundProfile.BlockingSounds[Random.Range(0, SoundProfile.BlockingSounds.Count)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a random injured sound based on your AI's Injured Sounds list.
|
||||
/// </summary>
|
||||
public void PlayInjuredSound()
|
||||
{
|
||||
int Odds = Random.Range(1, 101);
|
||||
if (Odds > SoundProfile.InjuredSoundOdds) return;
|
||||
|
||||
if (SoundProfile.InjuredSounds.Count > 0 && !EmeraldComponent.AnimationComponent.IsBlocking)
|
||||
{
|
||||
if (!m_AudioSource.isPlaying)
|
||||
{
|
||||
m_AudioSource.volume = SoundProfile.InjuredVolume;
|
||||
m_AudioSource.pitch = Mathf.Round(Random.Range(0.8f, 1.1f) * 10) / 10;
|
||||
m_AudioSource.PlayOneShot(SoundProfile.InjuredSounds[Random.Range(0, SoundProfile.InjuredSounds.Count)]);
|
||||
}
|
||||
else if (!m_SecondaryAudioSource.isPlaying)
|
||||
{
|
||||
m_SecondaryAudioSource.volume = SoundProfile.InjuredVolume;
|
||||
m_SecondaryAudioSource.PlayOneShot(SoundProfile.InjuredSounds[Random.Range(0, SoundProfile.InjuredSounds.Count)]);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_EventAudioSource.volume = SoundProfile.InjuredVolume;
|
||||
m_EventAudioSource.PlayOneShot(SoundProfile.InjuredSounds[Random.Range(0, SoundProfile.InjuredSounds.Count)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a random death sound based on your AI's Death Sounds list. Can also be called through Animation Events.
|
||||
/// </summary>
|
||||
public void PlayDeathSound()
|
||||
{
|
||||
if (SoundProfile.DeathSounds.Count > 0)
|
||||
{
|
||||
if (!m_AudioSource.isPlaying)
|
||||
{
|
||||
m_AudioSource.volume = SoundProfile.DeathVolume;
|
||||
m_AudioSource.PlayOneShot(SoundProfile.DeathSounds[Random.Range(0, SoundProfile.DeathSounds.Count)]);
|
||||
}
|
||||
else if (!m_SecondaryAudioSource.isPlaying)
|
||||
{
|
||||
m_SecondaryAudioSource.volume = SoundProfile.DeathVolume;
|
||||
m_SecondaryAudioSource.PlayOneShot(SoundProfile.DeathSounds[Random.Range(0, SoundProfile.DeathSounds.Count)]);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_EventAudioSource.volume = SoundProfile.DeathVolume;
|
||||
m_EventAudioSource.PlayOneShot(SoundProfile.DeathSounds[Random.Range(0, SoundProfile.DeathSounds.Count)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If using a Footstep Component, this creates a footstep effect and sound based on the detected surface of the footstep (requires a Footstep Component that has been set up).
|
||||
/// If NOT using a Footstep Component, this plays a random footstep sound based off of your AI's Walk Sound List.
|
||||
/// </summary>
|
||||
public void Footstep()
|
||||
{
|
||||
if (GetComponent<EmeraldFootsteps>() != null) return;
|
||||
|
||||
if (EmeraldComponent.MovementComponent.CanPlayWalkFootstepSound() || EmeraldComponent.MovementComponent.CanPlayRunFootstepSound())
|
||||
{
|
||||
float StepVolume = EmeraldComponent.MovementComponent.CanPlayWalkFootstepSound() ? SoundProfile.WalkFootstepVolume : SoundProfile.RunFootstepVolume;
|
||||
|
||||
if (SoundProfile.FootStepSounds.Count > 0)
|
||||
{
|
||||
if (!m_AudioSource.isPlaying)
|
||||
{
|
||||
m_AudioSource.volume = StepVolume;
|
||||
m_AudioSource.PlayOneShot(SoundProfile.FootStepSounds[Random.Range(0, SoundProfile.FootStepSounds.Count)]);
|
||||
}
|
||||
else if (!m_SecondaryAudioSource.isPlaying)
|
||||
{
|
||||
m_SecondaryAudioSource.volume = StepVolume;
|
||||
m_SecondaryAudioSource.PlayOneShot(SoundProfile.FootStepSounds[Random.Range(0, SoundProfile.FootStepSounds.Count)]);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_EventAudioSource.volume = StepVolume;
|
||||
m_EventAudioSource.PlayOneShot(SoundProfile.FootStepSounds[Random.Range(0, SoundProfile.FootStepSounds.Count)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a footstep sound from the AI's Footstep Sounds list to use when the AI is walking. This should be setup through an Animation Event.
|
||||
/// </summary>
|
||||
public void WalkFootstepSound()
|
||||
{
|
||||
if (GetComponent<EmeraldFootsteps>() != null) return;
|
||||
|
||||
if (EmeraldComponent.MovementComponent.CanPlayWalkFootstepSound())
|
||||
{
|
||||
if (SoundProfile.FootStepSounds.Count > 0)
|
||||
{
|
||||
if (!m_AudioSource.isPlaying)
|
||||
{
|
||||
m_AudioSource.volume = SoundProfile.WalkFootstepVolume;
|
||||
m_AudioSource.PlayOneShot(SoundProfile.FootStepSounds[Random.Range(0, SoundProfile.FootStepSounds.Count)]);
|
||||
}
|
||||
else if (!m_SecondaryAudioSource.isPlaying)
|
||||
{
|
||||
m_SecondaryAudioSource.volume = SoundProfile.WalkFootstepVolume;
|
||||
m_SecondaryAudioSource.PlayOneShot(SoundProfile.FootStepSounds[Random.Range(0, SoundProfile.FootStepSounds.Count)]);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_EventAudioSource.volume = SoundProfile.WalkFootstepVolume;
|
||||
m_EventAudioSource.PlayOneShot(SoundProfile.FootStepSounds[Random.Range(0, SoundProfile.FootStepSounds.Count)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a footstep sound from the AI's Footstep Sounds list to use when the AI is running. This should be setup through an Animation Event.
|
||||
/// </summary>
|
||||
public void RunFootstepSound()
|
||||
{
|
||||
if (GetComponent<EmeraldFootsteps>() != null) return;
|
||||
|
||||
if (EmeraldComponent.MovementComponent.CanPlayRunFootstepSound())
|
||||
{
|
||||
if (SoundProfile.FootStepSounds.Count > 0)
|
||||
{
|
||||
if (!m_AudioSource.isPlaying)
|
||||
{
|
||||
m_AudioSource.volume = SoundProfile.RunFootstepVolume;
|
||||
m_AudioSource.PlayOneShot(SoundProfile.FootStepSounds[Random.Range(0, SoundProfile.FootStepSounds.Count)]);
|
||||
}
|
||||
else if (!m_SecondaryAudioSource.isPlaying)
|
||||
{
|
||||
m_SecondaryAudioSource.volume = SoundProfile.RunFootstepVolume;
|
||||
m_SecondaryAudioSource.PlayOneShot(SoundProfile.FootStepSounds[Random.Range(0, SoundProfile.FootStepSounds.Count)]);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_EventAudioSource.volume = SoundProfile.RunFootstepVolume;
|
||||
m_EventAudioSource.PlayOneShot(SoundProfile.FootStepSounds[Random.Range(0, SoundProfile.FootStepSounds.Count)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a random sound effect from the AI's General Sounds list.
|
||||
/// </summary>
|
||||
public void PlayRandomSoundEffect()
|
||||
{
|
||||
if (SoundProfile.InteractSounds.Count > 0)
|
||||
{
|
||||
if (!m_AudioSource.isPlaying)
|
||||
{
|
||||
m_AudioSource.volume = 1;
|
||||
m_AudioSource.PlayOneShot(SoundProfile.InteractSounds[Random.Range(0, SoundProfile.InteractSounds.Count)].SoundEffectClip);
|
||||
}
|
||||
else if (!m_SecondaryAudioSource.isPlaying)
|
||||
{
|
||||
m_SecondaryAudioSource.volume = 1;
|
||||
m_SecondaryAudioSource.PlayOneShot(SoundProfile.InteractSounds[Random.Range(0, SoundProfile.InteractSounds.Count)].SoundEffectClip);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_EventAudioSource.volume = 1;
|
||||
m_EventAudioSource.PlayOneShot(SoundProfile.InteractSounds[Random.Range(0, SoundProfile.InteractSounds.Count)].SoundEffectClip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a sound effect from the AI's General Sounds list using the Sound Effect ID as the parameter.
|
||||
/// </summary>
|
||||
public void PlaySoundEffect(int SoundEffectID)
|
||||
{
|
||||
if (SoundProfile.InteractSounds.Count > 0)
|
||||
{
|
||||
for (int i = 0; i < SoundProfile.InteractSounds.Count; i++)
|
||||
{
|
||||
if (SoundProfile.InteractSounds[i].SoundEffectID == SoundEffectID)
|
||||
{
|
||||
if (!m_AudioSource.isPlaying)
|
||||
{
|
||||
m_AudioSource.volume = 1;
|
||||
m_AudioSource.PlayOneShot(SoundProfile.InteractSounds[i].SoundEffectClip);
|
||||
}
|
||||
else if (!m_SecondaryAudioSource.isPlaying)
|
||||
{
|
||||
m_SecondaryAudioSource.volume = 1;
|
||||
m_SecondaryAudioSource.PlayOneShot(SoundProfile.InteractSounds[i].SoundEffectClip);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_EventAudioSource.volume = 1;
|
||||
m_EventAudioSource.PlayOneShot(SoundProfile.InteractSounds[i].SoundEffectClip);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b69db04fa6da5fc498d3d30b4df09c69
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,158 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine.AI;
|
||||
using EmeraldAI.Utility;
|
||||
using EmeraldAI.SoundDetection;
|
||||
|
||||
namespace EmeraldAI
|
||||
{
|
||||
/// <summary>
|
||||
/// Updates most required Emerald Components so they are only using 1 update function. This also allows all components to easily be accessed from one source.
|
||||
/// </summary>
|
||||
#region Required Components
|
||||
[RequireComponent(typeof(EmeraldAnimation))]
|
||||
[RequireComponent(typeof(EmeraldDetection))]
|
||||
[RequireComponent(typeof(EmeraldSounds))]
|
||||
[RequireComponent(typeof(EmeraldCombat))]
|
||||
[RequireComponent(typeof(EmeraldBehaviors))]
|
||||
[RequireComponent(typeof(EmeraldMovement))]
|
||||
[RequireComponent(typeof(EmeraldHealth))]
|
||||
[RequireComponent(typeof(BoxCollider))]
|
||||
[RequireComponent(typeof(NavMeshAgent))]
|
||||
[RequireComponent(typeof(AudioSource))]
|
||||
[SelectionBase]
|
||||
#endregion
|
||||
[HelpURL("https://black-horizon-studios.gitbook.io/emerald-ai-wiki/")]
|
||||
public class EmeraldSystem : MonoBehaviour
|
||||
{
|
||||
#region Target Info
|
||||
//Since these are evenly used across multiple components, these are kept in the main Emerald System script.
|
||||
[HideInInspector] public Transform CombatTarget;
|
||||
[HideInInspector] public Transform TargetToFollow;
|
||||
[HideInInspector] public Transform LookAtTarget;
|
||||
[HideInInspector] [SerializeField] public CurrentTargetInfoClass CurrentTargetInfo = null;
|
||||
[System.Serializable]
|
||||
public class CurrentTargetInfoClass
|
||||
{
|
||||
public Transform TargetSource;
|
||||
public IDamageable CurrentIDamageable;
|
||||
public ICombat CurrentICombat;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Internal Components
|
||||
public static GameObject ObjectPool;
|
||||
public static GameObject CombatTextSystemObject;
|
||||
[HideInInspector] public NavMeshAgent m_NavMeshAgent;
|
||||
[HideInInspector] public BoxCollider AIBoxCollider;
|
||||
[HideInInspector] public Animator AIAnimator;
|
||||
#endregion
|
||||
|
||||
#region AI Components
|
||||
[HideInInspector] public EmeraldDetection DetectionComponent;
|
||||
[HideInInspector] public EmeraldBehaviors BehaviorsComponent;
|
||||
[HideInInspector] public EmeraldMovement MovementComponent;
|
||||
[HideInInspector] public EmeraldAnimation AnimationComponent;
|
||||
[HideInInspector] public EmeraldCombat CombatComponent;
|
||||
[HideInInspector] public EmeraldSounds SoundComponent;
|
||||
[HideInInspector] public EmeraldHealth HealthComponent;
|
||||
[HideInInspector] public EmeraldOptimization OptimizationComponent;
|
||||
[HideInInspector] public EmeraldInverseKinematics InverseKinematicsComponent;
|
||||
[HideInInspector] public EmeraldEvents EventsComponent;
|
||||
[HideInInspector] public EmeraldDebugger DebuggerComponent;
|
||||
[HideInInspector] public EmeraldUI UIComponent;
|
||||
[HideInInspector] public EmeraldItems ItemsComponent;
|
||||
[HideInInspector] public EmeraldSoundDetector SoundDetectorComponent;
|
||||
[HideInInspector] public TargetPositionModifier TPMComponent;
|
||||
[HideInInspector] public LocationBasedDamage LBDComponent;
|
||||
#endregion
|
||||
|
||||
//Initialize Emerald AI and its components
|
||||
void Awake()
|
||||
{
|
||||
MovementComponent = GetComponent<EmeraldMovement>();
|
||||
AnimationComponent = GetComponent<EmeraldAnimation>();
|
||||
SoundComponent = GetComponent<EmeraldSounds>();
|
||||
DetectionComponent = GetComponent<EmeraldDetection>();
|
||||
BehaviorsComponent = GetComponent<EmeraldBehaviors>();
|
||||
CombatComponent = GetComponent<EmeraldCombat>();
|
||||
HealthComponent = GetComponent<EmeraldHealth>();
|
||||
OptimizationComponent = GetComponent<EmeraldOptimization>();
|
||||
EventsComponent = GetComponent<EmeraldEvents>();
|
||||
DebuggerComponent = GetComponent<EmeraldDebugger>();
|
||||
UIComponent = GetComponent<EmeraldUI>();
|
||||
ItemsComponent = GetComponent<EmeraldItems>();
|
||||
SoundDetectorComponent = GetComponent<EmeraldSoundDetector>();
|
||||
InverseKinematicsComponent = GetComponent<EmeraldInverseKinematics>();
|
||||
TPMComponent = GetComponent<TargetPositionModifier>();
|
||||
m_NavMeshAgent = GetComponent<NavMeshAgent>();
|
||||
AIBoxCollider = GetComponent<BoxCollider>();
|
||||
AIAnimator = GetComponent<Animator>();
|
||||
InitializeEmeraldObjectPool();
|
||||
InitializeCombatText();
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
//When the AI is enabled, and it has been killed, reset the AI to its default settings.
|
||||
//This is intended for being used with Object Pooling or spawning systems such as Crux.
|
||||
if (AnimationComponent.IsDead)
|
||||
{
|
||||
ResetAI();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the Emerald Object Pool. The ObjectPool is a static variable so it's only done once.
|
||||
/// </summary>
|
||||
void InitializeEmeraldObjectPool()
|
||||
{
|
||||
if (EmeraldSystem.ObjectPool == null)
|
||||
{
|
||||
EmeraldSystem.ObjectPool = new GameObject();
|
||||
EmeraldSystem.ObjectPool.name = "Emerald AI Pool";
|
||||
EmeraldObjectPool.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the Emerald Combat Text System. The CombatTextSystemObject is a static variable so it's only done once.
|
||||
/// </summary>
|
||||
void InitializeCombatText()
|
||||
{
|
||||
if (EmeraldSystem.CombatTextSystemObject == null)
|
||||
{
|
||||
GameObject m_CombatTextSystem = Instantiate((GameObject)Resources.Load("Combat Text System") as GameObject, Vector3.zero, Quaternion.identity);
|
||||
m_CombatTextSystem.name = "Combat Text System";
|
||||
GameObject m_CombatTextCanvas = Instantiate((GameObject)Resources.Load("Combat Text Canvas") as GameObject, Vector3.zero, Quaternion.identity);
|
||||
m_CombatTextCanvas.name = "Combat Text Canvas";
|
||||
EmeraldSystem.CombatTextSystemObject = m_CombatTextCanvas;
|
||||
CombatTextSystem.Instance.CombatTextCanvas = m_CombatTextCanvas;
|
||||
CombatTextSystem.Instance.Initialize();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update all scripts through the EmeraldSystem's update function.
|
||||
/// </summary>
|
||||
void Update()
|
||||
{
|
||||
if (HealthComponent.CurrentHealth <= 0) return;
|
||||
|
||||
AnimationComponent.AnimationUpdate(); //A custom update function for the EmeraldAnimation called through the EmeraldAISystem script.
|
||||
MovementComponent.MovementUpdate(); //A custom update function for the EmeraldMovement called through the EmeraldAISystem script.
|
||||
BehaviorsComponent.BehaviorUpdate(); //A custom update function for the EmeraldBehaviors script called through the EmeraldAISystem script.
|
||||
DetectionComponent.DetectionUpdate(); //A custom update function for the EmeraldDetection script called through the EmeraldAISystem script.
|
||||
CombatComponent.CombatUpdate(); //A custom update function for the EmeraldCombat script called through the EmeraldAISystem script.
|
||||
if (DebuggerComponent) DebuggerComponent.DebuggerUpdate(); //A custom update function for the EmeraldDebugger script called through the EmeraldAISystem script.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets an AI to its default state. This is useful if an AI is being respawned.
|
||||
/// </summary>
|
||||
public void ResetAI()
|
||||
{
|
||||
EmeraldAPI.Combat.ResetAI(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0827074d85bacde4485f082ac0820a93
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user