├── .gitignore ├── Assets └── _Project │ └── Scripts │ ├── Preconditions.cs │ ├── Preconditions.cs.meta │ ├── UtilityAI.meta │ └── UtilityAI │ ├── Actions.meta │ ├── Actions │ ├── AIAction.cs │ ├── AIAction.cs.meta │ ├── IdleAIAction.cs │ ├── IdleAIAction.cs.meta │ ├── MoveToTargetAIAction.cs │ └── MoveToTargetAIAction.cs.meta │ ├── Brain.meta │ ├── Brain │ ├── AnimationController.cs │ ├── AnimationController.cs.meta │ ├── Brain.cs │ ├── Brain.cs.meta │ ├── Context.cs │ ├── Context.cs.meta │ ├── DrawGizmo.cs │ ├── DrawGizmo.cs.meta │ ├── Sensor.cs │ └── Sensor.cs.meta │ ├── Considerations.meta │ ├── Considerations │ ├── CompositeConsideration.cs │ ├── CompositeConsideration.cs.meta │ ├── Consideration.cs │ ├── Consideration.cs.meta │ ├── ConstantConsideration.cs │ ├── ConstantConsideration.cs.meta │ ├── CurveConsideration.cs │ ├── CurveConsideration.cs.meta │ ├── InRangeConsideration.cs │ └── InRangeConsideration.cs.meta │ ├── Editor.meta │ ├── Editor │ ├── BrainEditor.cs │ └── BrainEditor.cs.meta │ ├── Health.cs │ ├── Health.cs.meta │ ├── HealthUI.cs │ ├── HealthUI.cs.meta │ ├── ScriptableObjects.meta │ └── ScriptableObjects │ ├── Actions.meta │ ├── Actions │ ├── Idle.asset │ ├── Idle.asset.meta │ ├── MoveToEnemy.asset │ ├── MoveToEnemy.asset.meta │ ├── MoveToHealth.asset │ └── MoveToHealth.asset.meta │ ├── Considerations.meta │ └── Considerations │ ├── HealthAmount.asset │ ├── HealthAmount.asset.meta │ ├── IdleConsideration.asset │ ├── IdleConsideration.asset.meta │ ├── InRangeOfHealth.asset │ ├── InRangeOfHealth.asset.meta │ ├── InRangeOfTarget.asset │ ├── InRangeOfTarget.asset.meta │ ├── ShouldHeal.asset │ └── ShouldHeal.asset.meta ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # This .gitignore file should be placed at the root of your Unity project directory 2 | # 3 | # Get latest from https://github.com/github/gitignore/blob/main/Unity.gitignore 4 | # 5 | /[Ll]ibrary/ 6 | /[Tt]emp/ 7 | /[Oo]bj/ 8 | /[Bb]uild/ 9 | /[Bb]uildFullScreen/ 10 | /[Bb]uilds/ 11 | /[Ll]ogs/ 12 | /[Uu]ser[Ss]ettings/ 13 | /CCDBuildData/ 14 | 15 | # Ignore everything under Assets except the _Project folder 16 | Assets/* 17 | !Assets/_Project/ 18 | 19 | # MemoryCaptures can get excessive in size. 20 | # They also could contain extremely sensitive data 21 | /[Mm]emoryCaptures/ 22 | 23 | # Recordings can get excessive in size 24 | /[Rr]ecordings/ 25 | 26 | # Uncomment this line if you wish to ignore the asset store tools plugin 27 | # /[Aa]ssets/AssetStoreTools* 28 | 29 | # Autogenerated Jetbrains Rider plugin 30 | /[Aa]ssets/Plugins/Editor/JetBrains* 31 | 32 | # Visual Studio cache directory 33 | .vs/ 34 | 35 | # Gradle cache directory 36 | .gradle/ 37 | 38 | # Autogenerated VS/MD/Consulo solution and project files 39 | ExportedObj/ 40 | .consulo/ 41 | *.csproj 42 | *.unityproj 43 | *.sln 44 | *.suo 45 | *.tmp 46 | *.user 47 | *.userprefs 48 | *.pidb 49 | *.booproj 50 | *.svd 51 | *.pdb 52 | *.mdb 53 | *.opendb 54 | *.VC.db 55 | 56 | # Unity3D generated meta files 57 | *.pidb.meta 58 | *.pdb.meta 59 | *.mdb.meta 60 | 61 | # Unity3D generated file on crash reports 62 | sysinfo.txt 63 | 64 | # Builds 65 | *.apk 66 | *.aab 67 | *.unitypackage 68 | *.app 69 | 70 | # Crashlytics generated file 71 | crashlytics-build.properties 72 | 73 | # Packed Addressables 74 | /[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin* 75 | 76 | # Temporary auto-generated Android Assets 77 | /[Aa]ssets/[Ss]treamingAssets/aa.meta 78 | /[Aa]ssets/[Ss]treamingAssets/aa/* 79 | 80 | # Custom 81 | Assets/SceneDependencyCache* 82 | Assets/NetCodeGenerated* 83 | .idea/ 84 | .DS_Store 85 | RiderScriptEditorPersistedState.asset 86 | Packages/packages-lock.json 87 | -------------------------------------------------------------------------------- /Assets/_Project/Scripts/Preconditions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace UnityUtils { 4 | public class Preconditions { 5 | Preconditions() { } 6 | 7 | public static T CheckNotNull(T reference) { 8 | return CheckNotNull(reference, null); 9 | } 10 | 11 | public static T CheckNotNull(T reference, string message) { 12 | if (reference is UnityEngine.Object obj && obj.OrNull() == null) { 13 | throw new ArgumentNullException(message); 14 | } 15 | if (reference is null) { 16 | throw new ArgumentNullException(message); 17 | } 18 | return reference; 19 | } 20 | 21 | public static void CheckState(bool expression) { 22 | CheckState(expression, null); 23 | } 24 | 25 | public static void CheckState(bool expression, string messageTemplate, params object[] messageArgs) { 26 | CheckState(expression, string.Format(messageTemplate, messageArgs)); 27 | } 28 | 29 | public static void CheckState(bool expression, string message) { 30 | if (expression) { 31 | return; 32 | } 33 | 34 | throw message == null ? new InvalidOperationException() : new InvalidOperationException(message); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /Assets/_Project/Scripts/Preconditions.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3b6d89b87fb46c74cbde05a077741cb3 -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 904083fe58264d409ecfe35f0e5c5fc4 3 | timeCreated: 1726241232 -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Actions.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e79f90b358f54b2da85be2d55c970c07 3 | timeCreated: 1726241511 -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Actions/AIAction.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace UtilityAI { 4 | public abstract class AIAction : ScriptableObject { 5 | public string targetTag; 6 | public Consideration consideration; 7 | 8 | public virtual void Initialize(Context context) { 9 | // Optional initialization logic 10 | } 11 | 12 | public float CalculateUtility(Context context) => consideration.Evaluate(context); 13 | 14 | public abstract void Execute(Context context); 15 | } 16 | } -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Actions/AIAction.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 70a0595cb37f4fa7b3e36bb53cfb190e 3 | timeCreated: 1726241616 -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Actions/IdleAIAction.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace UtilityAI { 4 | [CreateAssetMenu(menuName = "UtilityAI/Actions/IdleAction")] 5 | public class IdleAIAction : AIAction { 6 | public override void Execute(Context context) { 7 | context.agent.SetDestination(context.agent.transform.position); 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Actions/IdleAIAction.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6a66a41743ab4960bc79910d90925921 3 | timeCreated: 1726244973 -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Actions/MoveToTargetAIAction.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace UtilityAI { 4 | [CreateAssetMenu(menuName = "UtilityAI/Actions/MoveToTargetAction")] 5 | public class MoveToTargetAIAction : AIAction { 6 | public override void Initialize(Context context) { 7 | context.sensor.targetTags.Add(targetTag); 8 | } 9 | 10 | public override void Execute(Context context) { 11 | var target = context.sensor.GetClosestTarget(targetTag); 12 | if (target == null) return; 13 | 14 | context.target = target; 15 | 16 | context.agent.SetDestination(target.position); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Actions/MoveToTargetAIAction.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 341025acd0384ee4abf8cf6a423abbe5 3 | timeCreated: 1726244016 -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Brain.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e8978df84ade45bd9039857d835ae3fa 3 | timeCreated: 1726241506 -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Brain/AnimationController.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEngine.AI; 3 | 4 | namespace UtilityAI { 5 | public class AnimationController : MonoBehaviour { 6 | Animator animator; 7 | NavMeshAgent agent; 8 | readonly int speedHash = Animator.StringToHash("Speed"); 9 | float currentSpeed; 10 | float speedVelocity; 11 | public float smoothTime = 0.3f; 12 | 13 | void Start() { 14 | animator = GetComponentInChildren(); 15 | agent = GetComponent(); 16 | } 17 | 18 | void Update() { 19 | float targetSpeed = agent.velocity.magnitude; 20 | 21 | currentSpeed = Mathf.SmoothDamp(currentSpeed, targetSpeed, ref speedVelocity, smoothTime); 22 | animator.SetFloat(speedHash, currentSpeed); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Brain/AnimationController.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 17e3b012c7264a50a4226bc006f6f2af 3 | timeCreated: 1726251480 -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Brain/Brain.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEngine; 3 | using UnityEngine.AI; 4 | 5 | namespace UtilityAI { 6 | [RequireComponent(typeof(NavMeshAgent), typeof(Sensor))] 7 | public class Brain : MonoBehaviour { 8 | public List actions; 9 | public Context context; 10 | 11 | public Health health; 12 | 13 | void Awake() { 14 | context = new Context(this); 15 | health = GetComponent(); 16 | 17 | foreach (var action in actions) { 18 | action.Initialize(context); 19 | } 20 | } 21 | 22 | void Update() { 23 | UpdateContext(); 24 | 25 | AIAction bestAction = null; 26 | float highestUtility = float.MinValue; 27 | 28 | foreach (var action in actions) { 29 | float utility = action.CalculateUtility(context); 30 | if (utility > highestUtility) { 31 | highestUtility = utility; 32 | bestAction = action; 33 | } 34 | } 35 | 36 | if (bestAction != null) { 37 | bestAction.Execute(context); 38 | } 39 | } 40 | 41 | void UpdateContext() { 42 | context.SetData("health", health.normalizedHealth); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Brain/Brain.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ffa5bbd2baab4720842f1b3d302fe5f8 3 | timeCreated: 1726241490 -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Brain/Context.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEngine; 3 | using UnityEngine.AI; 4 | using UnityUtils; 5 | 6 | namespace UtilityAI { 7 | public class Context { 8 | public Brain brain; 9 | public NavMeshAgent agent; 10 | public Transform target; 11 | public Sensor sensor; 12 | 13 | readonly Dictionary data = new(); 14 | 15 | public Context(Brain brain) { 16 | Preconditions.CheckNotNull(brain, nameof(brain)); 17 | 18 | this.brain = brain; 19 | this.agent = brain.gameObject.GetOrAdd(); 20 | this.sensor = brain.gameObject.GetOrAdd(); 21 | } 22 | 23 | public T GetData(string key) => data.TryGetValue(key, out var value) ? (T)value : default; 24 | public void SetData(string key, object value) => data[key] = value; 25 | } 26 | } -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Brain/Context.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9c8dca6f32a148de8531a39260244672 3 | timeCreated: 1726242317 -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Brain/DrawGizmo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | namespace UtilityAI { 5 | public class DrawGizmo : MonoBehaviour { 6 | public Color color = Color.yellow; 7 | public float radius = 1f; 8 | 9 | [SerializeField] SphereCollider collider; 10 | 11 | void Start() { 12 | if (collider == null) collider = GetComponent(); 13 | radius = collider.radius; 14 | } 15 | 16 | void OnDrawGizmos() { 17 | Gizmos.color = color; 18 | Gizmos.DrawWireSphere(transform.position, radius); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Brain/DrawGizmo.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 25d32c660dcb4eaf8e273639c2f48a8f 3 | timeCreated: 1726289170 -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Brain/Sensor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | namespace UtilityAI { 6 | [RequireComponent(typeof(SphereCollider))] 7 | public class Sensor : MonoBehaviour { 8 | public float detectionRadius = 10f; 9 | public List targetTags = new(); 10 | 11 | readonly List detectedObjects = new(10); 12 | SphereCollider sphereCollider; 13 | 14 | void Start() { 15 | sphereCollider = GetComponent(); 16 | sphereCollider.isTrigger = true; 17 | sphereCollider.radius = detectionRadius; 18 | 19 | Collider[] colliders = Physics.OverlapSphere(transform.position, detectionRadius); 20 | foreach (var c in colliders) { 21 | ProcessTrigger(c, transform => detectedObjects.Add(transform)); 22 | } 23 | } 24 | 25 | void OnTriggerEnter(Collider other) { 26 | ProcessTrigger(other, transform => detectedObjects.Add(transform)); 27 | } 28 | 29 | void OnTriggerExit(Collider other) { 30 | ProcessTrigger(other, transform => detectedObjects.Remove(transform)); 31 | } 32 | 33 | void ProcessTrigger(Collider other, Action action) { 34 | if (other.CompareTag("Untagged")) return; 35 | 36 | foreach (string t in targetTags) { 37 | if (other.CompareTag(t)) { 38 | action(other.transform); 39 | } 40 | } 41 | } 42 | 43 | public Transform GetClosestTarget(string tag) { 44 | if (detectedObjects.Count == 0) return null; 45 | 46 | Transform closestTarget = null; 47 | float closestDistanceSqr = Mathf.Infinity; 48 | Vector3 currentPosition = transform.position; 49 | 50 | foreach (Transform potentialTarget in detectedObjects) { 51 | if (potentialTarget.CompareTag(tag)) { 52 | Vector3 directionToTarget = potentialTarget.position - currentPosition; 53 | float dSqrToTarget = directionToTarget.sqrMagnitude; 54 | if (dSqrToTarget < closestDistanceSqr) { 55 | closestDistanceSqr = dSqrToTarget; 56 | closestTarget = potentialTarget; 57 | } 58 | } 59 | } 60 | return closestTarget; 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Brain/Sensor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 050fd9ee263947e99c5079dcae3b7aa3 3 | timeCreated: 1726246191 -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Considerations.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 80603d1ca69949e5b1221c5291cbc525 3 | timeCreated: 1726241498 -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Considerations/CompositeConsideration.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEngine; 3 | 4 | namespace UtilityAI { 5 | [CreateAssetMenu(menuName = "UtilityAI/Considerations/CompositeConsideration")] 6 | public class CompositeConsideration : Consideration { 7 | public enum OperationType { Average, Multiply, Add, Subtract, Divide, Max, Min } 8 | 9 | public bool allMustBeNonZero = true; 10 | 11 | public OperationType operation = OperationType.Max; 12 | public List considerations; 13 | 14 | public override float Evaluate(Context context) { 15 | if (considerations == null || considerations.Count == 0) return 0f; 16 | 17 | float result = considerations[0].Evaluate(context); 18 | if (result == 0f && allMustBeNonZero) return 0f; 19 | 20 | // Suggestion: Only 2 Considerations per Composite 21 | for (int i = 1; i < considerations.Count; i++) { 22 | float value = considerations[i].Evaluate(context); 23 | 24 | if (value == 0f && allMustBeNonZero) return 0f; 25 | 26 | switch (operation) { 27 | case OperationType.Average: 28 | result = (result + value) / 2; 29 | break; 30 | case OperationType.Multiply: 31 | result *= value; 32 | break; 33 | case OperationType.Add: 34 | result += value; 35 | break; 36 | case OperationType.Subtract: 37 | result -= value; 38 | break; 39 | case OperationType.Divide: 40 | result = value != 0 ? result / value : result; // Prevent division by zero 41 | break; 42 | case OperationType.Max: 43 | result = Mathf.Max(result, value); 44 | break; 45 | case OperationType.Min: 46 | result = Mathf.Min(result, value); 47 | break; 48 | } 49 | } 50 | 51 | return Mathf.Clamp01(result); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Considerations/CompositeConsideration.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3b27bbacb0ad43789b2b4aa5036bfab7 3 | timeCreated: 1726243143 -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Considerations/Consideration.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace UtilityAI { 4 | public abstract class Consideration : ScriptableObject { 5 | public abstract float Evaluate(Context context); 6 | } 7 | } -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Considerations/Consideration.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: d8e3ec8fe329411c8d108de315843587 3 | timeCreated: 1726241245 -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Considerations/ConstantConsideration.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace UtilityAI { 4 | [CreateAssetMenu(menuName = "UtilityAI/Considerations/Constant")] 5 | public class ConstantConsideration : Consideration { 6 | public float value; 7 | 8 | public override float Evaluate(Context context) => value; 9 | } 10 | } -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Considerations/ConstantConsideration.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 42518f58f6b14e9f999cd6139295acf7 3 | timeCreated: 1726241598 -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Considerations/CurveConsideration.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace UtilityAI { 4 | [CreateAssetMenu(menuName = "UtilityAI/Considerations/CurveConsideration")] 5 | public class CurveConsideration : Consideration { 6 | public AnimationCurve curve; 7 | public string contextKey; 8 | 9 | public override float Evaluate(Context context) { 10 | float inputValue = context.GetData(contextKey); 11 | 12 | float utility = curve.Evaluate(inputValue); 13 | return Mathf.Clamp01(utility); 14 | } 15 | 16 | void Reset() { 17 | curve = new AnimationCurve( 18 | new Keyframe(0f, 1f), // At normalized distance 0, utility is 1 19 | new Keyframe(1f, 0f) // At normalized distance 1, utility is 0 20 | ); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Considerations/CurveConsideration.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: afbfac89f33d4516afac8cc324f9cb10 3 | timeCreated: 1726241605 -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Considerations/InRangeConsideration.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityUtils; 3 | 4 | namespace UtilityAI { 5 | [CreateAssetMenu(menuName = "UtilityAI/Considerations/InRangeConsideration")] 6 | public class InRangeConsideration : Consideration { 7 | public float maxDistance = 10f; 8 | public float maxAngle = 360f; 9 | public string targetTag = "Target"; 10 | public AnimationCurve curve; 11 | 12 | public override float Evaluate(Context context) { 13 | if (!context.sensor.targetTags.Contains(targetTag)) { 14 | context.sensor.targetTags.Add(targetTag); 15 | } 16 | 17 | Transform targetTransform = context.sensor.GetClosestTarget(targetTag); 18 | if (targetTransform == null) return 0f; 19 | 20 | Transform agentTransform = context.agent.transform; 21 | 22 | bool isInRange = agentTransform.InRangeOf(targetTransform, maxDistance, maxAngle); 23 | if (!isInRange) return 0f; 24 | 25 | Vector3 directionToTarget = targetTransform.position - agentTransform.position; 26 | float distanceToTarget = directionToTarget.With(y:0).magnitude; 27 | 28 | float normalizedDistance = Mathf.Clamp01(distanceToTarget / maxDistance); 29 | 30 | float utility = curve.Evaluate(normalizedDistance); 31 | return Mathf.Clamp01(utility); 32 | } 33 | 34 | void Reset() { 35 | curve = new AnimationCurve( 36 | new Keyframe(0f, 1f), 37 | new Keyframe(1f, 0f) 38 | ); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Considerations/InRangeConsideration.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 32cbbe48415348bfa2603544c5233608 3 | timeCreated: 1726245818 -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3d0e34df114b6024780b27f121ab8b62 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Editor/BrainEditor.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | 4 | namespace UtilityAI { 5 | [CustomEditor(typeof(Brain))] 6 | public class BrainEditor : Editor { 7 | void OnEnable() { 8 | this.RequiresConstantRepaint(); 9 | } 10 | 11 | public override void OnInspectorGUI() { 12 | base.OnInspectorGUI(); // Draw the default inspector 13 | 14 | Brain brain = (Brain) target; 15 | 16 | if (Application.isPlaying) { 17 | AIAction chosenAction = GetChosenAction(brain); 18 | 19 | if (chosenAction != null) { 20 | EditorGUILayout.LabelField($"Current Chosen Action: {chosenAction.name}", EditorStyles.boldLabel); 21 | } 22 | 23 | EditorGUILayout.Space(); 24 | EditorGUILayout.LabelField("Actions/Considerations", EditorStyles.boldLabel); 25 | 26 | 27 | foreach (AIAction action in brain.actions) { 28 | float utility = action.CalculateUtility(brain.context); 29 | EditorGUILayout.LabelField($"Action: {action.name}, Utility: {utility:F2}"); 30 | 31 | // Draw the single consideration for the action 32 | DrawConsideration(action.consideration, brain.context, 1); 33 | } 34 | } else { 35 | EditorGUILayout.HelpBox("Enter Play mode to view utility values.", MessageType.Info); 36 | } 37 | } 38 | 39 | private void DrawConsideration(Consideration consideration, Context context, int indentLevel) { 40 | EditorGUI.indentLevel = indentLevel; 41 | 42 | if (consideration is CompositeConsideration compositeConsideration) { 43 | EditorGUILayout.LabelField( 44 | $"Composite Consideration: {compositeConsideration.name}, Operation: {compositeConsideration.operation}" 45 | ); 46 | 47 | foreach (Consideration subConsideration in compositeConsideration.considerations) { 48 | DrawConsideration(subConsideration, context, indentLevel + 1); 49 | } 50 | } else { 51 | float value = consideration.Evaluate(context); 52 | EditorGUILayout.LabelField($"Consideration: {consideration.name}, Value: {value:F2}"); 53 | } 54 | 55 | EditorGUI.indentLevel = indentLevel - 1; // Reset indentation after drawing 56 | } 57 | 58 | private AIAction GetChosenAction(Brain brain) { 59 | float highestUtility = float.MinValue; 60 | AIAction chosenAction = null; 61 | 62 | foreach (var action in brain.actions) { 63 | float utility = action.CalculateUtility(brain.context); 64 | if (utility > highestUtility) { 65 | highestUtility = utility; 66 | chosenAction = action; 67 | } 68 | } 69 | 70 | return chosenAction; 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Editor/BrainEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1451acbf1ee1430f9752f708be8b288c 3 | timeCreated: 1726277355 -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Health.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace UtilityAI { 4 | public class Health : MonoBehaviour { 5 | public float maxHealth = 100; 6 | public float Current; 7 | public float normalizedHealth => Current / maxHealth; 8 | 9 | void Start() { 10 | Current = maxHealth; 11 | } 12 | 13 | public void Heal(float value) { 14 | Current += value; 15 | } 16 | 17 | public void TakeDamage(float damage) { 18 | Current -= damage; 19 | if (Current <= 0) { 20 | Die(); 21 | } 22 | } 23 | 24 | void Die() { 25 | // Destroy(gameObject); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/Health.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f5f5427bdccb4039a0deaa40e645a770 3 | timeCreated: 1726278016 -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/HealthUI.cs: -------------------------------------------------------------------------------- 1 | using Renge.PPB; 2 | using UnityEngine; 3 | using UnityEngine.UI; 4 | 5 | namespace UtilityAI { 6 | public class HealthUI : MonoBehaviour { 7 | [SerializeField] ProceduralProgressBar progressBar; 8 | [SerializeField] Button healthButton; 9 | [SerializeField] Button damageButton; 10 | [SerializeField] Health health; 11 | 12 | void Start() { 13 | healthButton.onClick.AddListener(() => Heal(10)); 14 | damageButton.onClick.AddListener(() => TakeDamage(10)); 15 | } 16 | 17 | void Update() { 18 | if (progressBar) { 19 | progressBar.Value = health.Current / health.maxHealth; 20 | } 21 | } 22 | 23 | void Heal(float value) => health.Heal(value); 24 | void TakeDamage(float damage) => health.TakeDamage(damage); 25 | } 26 | } -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/HealthUI.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1430ea1cba604252b0018cfe0ab49578 3 | timeCreated: 1726278273 -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/ScriptableObjects.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 87e64fb40872b064a98e0a91c38ea6d7 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/ScriptableObjects/Actions.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 52558197ed5554b45a34a865389dc9bd 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/ScriptableObjects/Actions/Idle.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!114 &11400000 4 | MonoBehaviour: 5 | m_ObjectHideFlags: 0 6 | m_CorrespondingSourceObject: {fileID: 0} 7 | m_PrefabInstance: {fileID: 0} 8 | m_PrefabAsset: {fileID: 0} 9 | m_GameObject: {fileID: 0} 10 | m_Enabled: 1 11 | m_EditorHideFlags: 0 12 | m_Script: {fileID: 11500000, guid: 6a66a41743ab4960bc79910d90925921, type: 3} 13 | m_Name: Idle 14 | m_EditorClassIdentifier: 15 | targetTag: 16 | consideration: {fileID: 11400000, guid: 89290a8b6c5a14a4c9e7070eb781eccc, type: 2} 17 | -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/ScriptableObjects/Actions/Idle.asset.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 94cd36161c8a68548bda1aae9c5abe2b 3 | NativeFormatImporter: 4 | externalObjects: {} 5 | mainObjectFileID: 11400000 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/ScriptableObjects/Actions/MoveToEnemy.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!114 &11400000 4 | MonoBehaviour: 5 | m_ObjectHideFlags: 0 6 | m_CorrespondingSourceObject: {fileID: 0} 7 | m_PrefabInstance: {fileID: 0} 8 | m_PrefabAsset: {fileID: 0} 9 | m_GameObject: {fileID: 0} 10 | m_Enabled: 1 11 | m_EditorHideFlags: 0 12 | m_Script: {fileID: 11500000, guid: 341025acd0384ee4abf8cf6a423abbe5, type: 3} 13 | m_Name: MoveToEnemy 14 | m_EditorClassIdentifier: 15 | targetTag: Target 16 | consideration: {fileID: 11400000, guid: b50310b21cfe1d34da8ce0236c843b7a, type: 2} 17 | -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/ScriptableObjects/Actions/MoveToEnemy.asset.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9f99fccaf84517c4e9fc40bddec39bf3 3 | NativeFormatImporter: 4 | externalObjects: {} 5 | mainObjectFileID: 11400000 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/ScriptableObjects/Actions/MoveToHealth.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!114 &11400000 4 | MonoBehaviour: 5 | m_ObjectHideFlags: 0 6 | m_CorrespondingSourceObject: {fileID: 0} 7 | m_PrefabInstance: {fileID: 0} 8 | m_PrefabAsset: {fileID: 0} 9 | m_GameObject: {fileID: 0} 10 | m_Enabled: 1 11 | m_EditorHideFlags: 0 12 | m_Script: {fileID: 11500000, guid: 341025acd0384ee4abf8cf6a423abbe5, type: 3} 13 | m_Name: MoveToHealth 14 | m_EditorClassIdentifier: 15 | targetTag: Health 16 | consideration: {fileID: 11400000, guid: 0b94afbce0679ad41ae83e5b1ec8e2e7, type: 2} 17 | -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/ScriptableObjects/Actions/MoveToHealth.asset.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 434b71187a0d77a4ea21aaec1eb15093 3 | NativeFormatImporter: 4 | externalObjects: {} 5 | mainObjectFileID: 11400000 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/ScriptableObjects/Considerations.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 8879f80621c471e4995c8f032da90c7a 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/ScriptableObjects/Considerations/HealthAmount.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!114 &11400000 4 | MonoBehaviour: 5 | m_ObjectHideFlags: 0 6 | m_CorrespondingSourceObject: {fileID: 0} 7 | m_PrefabInstance: {fileID: 0} 8 | m_PrefabAsset: {fileID: 0} 9 | m_GameObject: {fileID: 0} 10 | m_Enabled: 1 11 | m_EditorHideFlags: 0 12 | m_Script: {fileID: 11500000, guid: afbfac89f33d4516afac8cc324f9cb10, type: 3} 13 | m_Name: HealthAmount 14 | m_EditorClassIdentifier: 15 | curve: 16 | serializedVersion: 2 17 | m_Curve: 18 | - serializedVersion: 3 19 | time: 0 20 | value: 1 21 | inSlope: 0 22 | outSlope: 0 23 | tangentMode: 0 24 | weightedMode: 0 25 | inWeight: 0 26 | outWeight: 0 27 | - serializedVersion: 3 28 | time: 1 29 | value: 0 30 | inSlope: 0 31 | outSlope: 0 32 | tangentMode: 0 33 | weightedMode: 0 34 | inWeight: 0 35 | outWeight: 0 36 | m_PreInfinity: 2 37 | m_PostInfinity: 2 38 | m_RotationOrder: 4 39 | contextKey: health 40 | -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/ScriptableObjects/Considerations/HealthAmount.asset.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 491b6a2316899444991bb55f496cb913 3 | NativeFormatImporter: 4 | externalObjects: {} 5 | mainObjectFileID: 11400000 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/ScriptableObjects/Considerations/IdleConsideration.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!114 &11400000 4 | MonoBehaviour: 5 | m_ObjectHideFlags: 0 6 | m_CorrespondingSourceObject: {fileID: 0} 7 | m_PrefabInstance: {fileID: 0} 8 | m_PrefabAsset: {fileID: 0} 9 | m_GameObject: {fileID: 0} 10 | m_Enabled: 1 11 | m_EditorHideFlags: 0 12 | m_Script: {fileID: 11500000, guid: 42518f58f6b14e9f999cd6139295acf7, type: 3} 13 | m_Name: IdleConsideration 14 | m_EditorClassIdentifier: 15 | value: 0.2 16 | -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/ScriptableObjects/Considerations/IdleConsideration.asset.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 89290a8b6c5a14a4c9e7070eb781eccc 3 | NativeFormatImporter: 4 | externalObjects: {} 5 | mainObjectFileID: 11400000 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/ScriptableObjects/Considerations/InRangeOfHealth.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!114 &11400000 4 | MonoBehaviour: 5 | m_ObjectHideFlags: 0 6 | m_CorrespondingSourceObject: {fileID: 0} 7 | m_PrefabInstance: {fileID: 0} 8 | m_PrefabAsset: {fileID: 0} 9 | m_GameObject: {fileID: 0} 10 | m_Enabled: 1 11 | m_EditorHideFlags: 0 12 | m_Script: {fileID: 11500000, guid: 32cbbe48415348bfa2603544c5233608, type: 3} 13 | m_Name: InRangeOfHealth 14 | m_EditorClassIdentifier: 15 | maxDistance: 10 16 | maxAngle: 360 17 | targetTag: Health 18 | curve: 19 | serializedVersion: 2 20 | m_Curve: 21 | - serializedVersion: 3 22 | time: 0 23 | value: 1 24 | inSlope: 0 25 | outSlope: 0 26 | tangentMode: 0 27 | weightedMode: 0 28 | inWeight: 0 29 | outWeight: 0 30 | - serializedVersion: 3 31 | time: 1 32 | value: 0 33 | inSlope: 0 34 | outSlope: 0 35 | tangentMode: 0 36 | weightedMode: 0 37 | inWeight: 0 38 | outWeight: 0 39 | m_PreInfinity: 2 40 | m_PostInfinity: 2 41 | m_RotationOrder: 4 42 | -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/ScriptableObjects/Considerations/InRangeOfHealth.asset.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5d34af0edc6174a4ba0cfb89e1b8d62c 3 | NativeFormatImporter: 4 | externalObjects: {} 5 | mainObjectFileID: 11400000 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/ScriptableObjects/Considerations/InRangeOfTarget.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!114 &11400000 4 | MonoBehaviour: 5 | m_ObjectHideFlags: 0 6 | m_CorrespondingSourceObject: {fileID: 0} 7 | m_PrefabInstance: {fileID: 0} 8 | m_PrefabAsset: {fileID: 0} 9 | m_GameObject: {fileID: 0} 10 | m_Enabled: 1 11 | m_EditorHideFlags: 0 12 | m_Script: {fileID: 11500000, guid: 32cbbe48415348bfa2603544c5233608, type: 3} 13 | m_Name: InRangeOfTarget 14 | m_EditorClassIdentifier: 15 | maxDistance: 10 16 | maxAngle: 180 17 | targetTag: Target 18 | curve: 19 | serializedVersion: 2 20 | m_Curve: 21 | - serializedVersion: 3 22 | time: 0 23 | value: 1 24 | inSlope: 0 25 | outSlope: 0 26 | tangentMode: 0 27 | weightedMode: 0 28 | inWeight: 0 29 | outWeight: 0 30 | - serializedVersion: 3 31 | time: 1 32 | value: 0 33 | inSlope: 0 34 | outSlope: 0 35 | tangentMode: 0 36 | weightedMode: 0 37 | inWeight: 0 38 | outWeight: 0 39 | m_PreInfinity: 2 40 | m_PostInfinity: 2 41 | m_RotationOrder: 4 42 | -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/ScriptableObjects/Considerations/InRangeOfTarget.asset.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b50310b21cfe1d34da8ce0236c843b7a 3 | NativeFormatImporter: 4 | externalObjects: {} 5 | mainObjectFileID: 11400000 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/ScriptableObjects/Considerations/ShouldHeal.asset: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!114 &11400000 4 | MonoBehaviour: 5 | m_ObjectHideFlags: 0 6 | m_CorrespondingSourceObject: {fileID: 0} 7 | m_PrefabInstance: {fileID: 0} 8 | m_PrefabAsset: {fileID: 0} 9 | m_GameObject: {fileID: 0} 10 | m_Enabled: 1 11 | m_EditorHideFlags: 0 12 | m_Script: {fileID: 11500000, guid: 3b27bbacb0ad43789b2b4aa5036bfab7, type: 3} 13 | m_Name: ShouldHeal 14 | m_EditorClassIdentifier: 15 | allMustBeNonZero: 1 16 | operation: 5 17 | considerations: 18 | - {fileID: 11400000, guid: 491b6a2316899444991bb55f496cb913, type: 2} 19 | - {fileID: 11400000, guid: 5d34af0edc6174a4ba0cfb89e1b8d62c, type: 2} 20 | -------------------------------------------------------------------------------- /Assets/_Project/Scripts/UtilityAI/ScriptableObjects/Considerations/ShouldHeal.asset.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0b94afbce0679ad41ae83e5b1ec8e2e7 3 | NativeFormatImporter: 4 | externalObjects: {} 5 | mainObjectFileID: 11400000 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unity Utility AI 2 | ![UtilityAI](https://github.com/user-attachments/assets/3272ecd2-1df4-4d2a-bbfb-44c9a64fa349) 3 | 4 | 5 | 6 | Unity Utility AI is your key to creating intelligent agents with dynamic decision-making! 7 | In this episode, we dive deep into the inner workings of the AI Brain, exploring how Actions 8 | are chosen based on a combination of Considerations and Composite evaluations. 9 | Learn how to fine-tune your AI's decision-making process with smart Calculations and Curves, 10 | allowing your game to respond intelligently to any situation. Whether you're new to AI or 11 | looking to advance your skills, this tutorial will equip you with the tools to make smarter AI in Unity! 12 | 13 | ## Dependencies 14 | 15 | The package makes use of the following dependencies: 16 | - [Unity Utilities](https://github.com/adammyhre/Unity-Utils) 17 | 18 | ## YouTube 19 | 20 | [**Utility AI Video**]https://youtu.be/S4oyqrsU2WU) 21 | 22 | You can also check out my [YouTube channel](https://www.youtube.com/@git-amend?sub_confirmation=1) for more Unity content. 23 | 24 | [![YouTube Channel](https://img.shields.io/badge/YouTube-Subscribe-red?logo=youtube&style=social)](https://www.youtube.com/@git-amend?sub_confirmation=1) 25 | --------------------------------------------------------------------------------