using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.Events; namespace SynapticPro.GOAP { /// /// GOAP Agent - Main component that executes GOAP planning and actions /// Attach to any GameObject to give it GOAP AI capabilities /// public class GOAPAgent : MonoBehaviour { [Header("Agent Settings")] [SerializeField] private bool autoStart = true; [SerializeField] private float replanInterval = 1f; [SerializeField] private float actionTimeout = 30f; [Header("Planning Settings")] [SerializeField] private int maxPlanIterations = 1000; [SerializeField] private int maxPlanDepth = 15; [Header("Movement")] [SerializeField] private float moveSpeed = 5f; [SerializeField] private float stoppingDistance = 0.5f; [Header("Debug")] [SerializeField] private bool debugMode = false; [Header("Events")] public UnityEvent OnGoalChanged; public UnityEvent OnActionStarted; public UnityEvent OnActionCompleted; public UnityEvent OnActionFailed; public UnityEvent OnPlanCreated; public UnityEvent OnPlanFailed; // Runtime state private GOAPPlanner planner; private WorldState worldState; private HashSet availableActions; private List goals; private Queue currentPlan; private GOAPActionBase currentAction; private GOAPGoal currentGoal; private float lastPlanTime; private float actionStartTime; private bool isRunning; /// /// Current world state /// public WorldState WorldState => worldState; /// /// Currently executing action /// public GOAPActionBase CurrentAction => currentAction; /// /// Current goal being pursued /// public GOAPGoal CurrentGoal => currentGoal; /// /// Whether agent is currently running /// public bool IsRunning => isRunning; /// /// Whether agent has a valid plan /// public bool HasPlan => currentPlan != null && currentPlan.Count > 0; /// /// Movement speed /// public float MoveSpeed { get => moveSpeed; set => moveSpeed = Mathf.Max(0, value); } private void Awake() { planner = new GOAPPlanner(); planner.SetMaxIterations(maxPlanIterations); planner.SetMaxDepth(maxPlanDepth); worldState = new WorldState(); availableActions = new HashSet(); goals = new List(); currentPlan = new Queue(); // Collect actions from this GameObject CollectActions(); // Collect goals from components CollectGoals(); } private void Start() { if (autoStart) { StartAgent(); } } private void Update() { if (!isRunning) return; // Check if replanning is needed if (Time.time - lastPlanTime > replanInterval) { TryReplan(); } // Execute current action ExecuteCurrentAction(); } /// /// Start the GOAP agent /// public void StartAgent() { isRunning = true; TryReplan(); } /// /// Stop the GOAP agent /// public void StopAgent() { isRunning = false; if (currentAction != null) { currentAction.OnInterrupted(this); currentAction = null; } currentPlan?.Clear(); } /// /// Collect all GOAPActionBase components /// private void CollectActions() { var actions = GetComponents(); foreach (var action in actions) { availableActions.Add(action); } // Also check children var childActions = GetComponentsInChildren(); foreach (var action in childActions) { availableActions.Add(action); } if (debugMode) { Debug.Log($"[GOAPAgent] Collected {availableActions.Count} actions"); } } /// /// Collect all GOAPGoalComponent goals /// private void CollectGoals() { var goalComponents = GetComponents(); foreach (var gc in goalComponents) { goals.Add(gc.Goal); } // Also check children var childGoals = GetComponentsInChildren(); foreach (var gc in childGoals) { if (!goals.Contains(gc.Goal)) { goals.Add(gc.Goal); } } if (debugMode) { Debug.Log($"[GOAPAgent] Collected {goals.Count} goals"); } } /// /// Add an action at runtime /// public void AddAction(GOAPActionBase action) { if (action != null) { availableActions.Add(action); } } /// /// Remove an action at runtime /// public void RemoveAction(GOAPActionBase action) { if (action != null) { availableActions.Remove(action); } } /// /// Add a goal at runtime /// public void AddGoal(GOAPGoal goal) { if (goal != null && !goals.Contains(goal)) { goals.Add(goal); } } /// /// Remove a goal at runtime /// public void RemoveGoal(GOAPGoal goal) { if (goal != null) { goals.Remove(goal); if (currentGoal == goal) { currentGoal = null; currentPlan?.Clear(); } } } /// /// Set world state value /// public void SetWorldState(string key, object value) { worldState.SetState(key, value); } /// /// Get world state value /// public T GetWorldState(string key) { return worldState.GetState(key); } /// /// Try to create a new plan /// private void TryReplan() { lastPlanTime = Time.time; // Find highest priority goal var bestGoal = SelectGoal(); if (bestGoal == null) { if (debugMode) { Debug.Log("[GOAPAgent] No active goals"); } return; } // Check if goal already satisfied if (bestGoal.IsSatisfied(worldState)) { if (debugMode) { Debug.Log($"[GOAPAgent] Goal '{bestGoal.GoalName}' already satisfied"); } bestGoal.OnAchieved(this); return; } // Check if we need to replan bool needsReplan = currentGoal != bestGoal || !HasPlan; if (!needsReplan && currentAction != null) { // Check if current action is still valid if (!currentAction.CheckProceduralPrecondition(this)) { needsReplan = true; } } if (needsReplan) { CreatePlan(bestGoal); } } /// /// Select the best goal to pursue /// private GOAPGoal SelectGoal() { GOAPGoal bestGoal = null; float bestRelevance = float.MinValue; foreach (var goal in goals) { if (!goal.IsActive) continue; float relevance = goal.GetRelevance(this); if (relevance > bestRelevance) { bestRelevance = relevance; bestGoal = goal; } } return bestGoal; } /// /// Create a plan to achieve the goal /// private void CreatePlan(GOAPGoal goal) { if (currentAction != null) { currentAction.OnInterrupted(this); currentAction = null; } if (currentGoal != null && currentGoal != goal) { currentGoal.OnDeactivate(this); } currentGoal = goal; currentGoal.OnActivate(this); OnGoalChanged?.Invoke(currentGoal); var plan = planner.Plan(this, availableActions, worldState, goal); if (plan != null && plan.Count > 0) { currentPlan = plan; OnPlanCreated?.Invoke(); if (debugMode) { Debug.Log($"[GOAPAgent] Plan created for '{goal.GoalName}': {string.Join(" -> ", plan.Select(a => a.ActionName))}"); } } else { currentPlan = new Queue(); OnPlanFailed?.Invoke(); if (debugMode) { Debug.LogWarning($"[GOAPAgent] Failed to create plan for '{goal.GoalName}'"); } } } /// /// Execute the current action /// private void ExecuteCurrentAction() { if (currentAction == null) { // Get next action from plan if (currentPlan != null && currentPlan.Count > 0) { currentAction = currentPlan.Dequeue(); actionStartTime = Time.time; // Check if we need to move to target if (!currentAction.IsInRange(this)) { // Move towards target MoveTowardsTarget(currentAction.Target); return; } // Start action if (currentAction.PrePerform(this)) { OnActionStarted?.Invoke(currentAction); if (debugMode) { Debug.Log($"[GOAPAgent] Started action: {currentAction.ActionName}"); } } else { // Action failed to start OnActionFailed?.Invoke(currentAction); currentAction = null; currentPlan.Clear(); // Force replan } } return; } // Check timeout if (Time.time - actionStartTime > actionTimeout) { if (debugMode) { Debug.LogWarning($"[GOAPAgent] Action '{currentAction.ActionName}' timed out"); } currentAction.OnInterrupted(this); OnActionFailed?.Invoke(currentAction); currentAction = null; currentPlan.Clear(); return; } // Check if we need to move to target if (!currentAction.IsInRange(this)) { MoveTowardsTarget(currentAction.Target); return; } // Execute action bool completed = currentAction.Perform(this); if (completed) { // Action completed if (currentAction.PostPerform(this)) { // Apply effects to world state foreach (var effect in currentAction.Effects) { worldState.SetState(effect.Key, effect.Value); } OnActionCompleted?.Invoke(currentAction); if (debugMode) { Debug.Log($"[GOAPAgent] Completed action: {currentAction.ActionName}"); } } else { OnActionFailed?.Invoke(currentAction); } currentAction = null; // Check if goal is achieved if (currentGoal != null && currentGoal.IsSatisfied(worldState)) { currentGoal.OnAchieved(this); if (debugMode) { Debug.Log($"[GOAPAgent] Goal achieved: {currentGoal.GoalName}"); } } } } /// /// Move towards target /// private void MoveTowardsTarget(GameObject target) { if (target == null) return; Vector3 direction = (target.transform.position - transform.position).normalized; direction.y = 0; // Keep on ground float distance = Vector3.Distance(transform.position, target.transform.position); if (distance > stoppingDistance) { transform.position += direction * moveSpeed * Time.deltaTime; transform.forward = direction; } } /// /// Force immediate replan /// public void ForceReplan() { if (currentAction != null) { currentAction.OnInterrupted(this); currentAction = null; } currentPlan?.Clear(); TryReplan(); } /// /// Interrupt current action /// public void InterruptCurrentAction() { if (currentAction != null) { currentAction.OnInterrupted(this); OnActionFailed?.Invoke(currentAction); currentAction = null; } } /// /// Get remaining actions in plan /// public List GetRemainingPlan() { var remaining = new List(); if (currentAction != null) { remaining.Add(currentAction); } if (currentPlan != null) { remaining.AddRange(currentPlan); } return remaining; } private void OnDrawGizmosSelected() { if (!debugMode) return; // Draw current action target if (currentAction != null && currentAction.Target != null) { Gizmos.color = Color.yellow; Gizmos.DrawLine(transform.position, currentAction.Target.transform.position); Gizmos.DrawWireSphere(currentAction.Target.transform.position, 0.5f); } } } }