using System; using System.Collections.Generic; using System.Linq; using UnityEngine; namespace SynapticPro.GOAP { /// /// GOAP Planner - A* based action planning system /// Finds optimal sequence of actions to achieve goals /// public class GOAPPlanner { private int maxPlanningIterations = 1000; private int maxPlanDepth = 15; /// /// Plan node for A* search /// private class PlanNode : IComparable { public WorldState State; public GOAPActionBase Action; public PlanNode Parent; public float GCost; // Cost from start public float HCost; // Heuristic cost to goal public float FCost => GCost + HCost; public int Depth; public int CompareTo(PlanNode other) { int compare = FCost.CompareTo(other.FCost); if (compare == 0) { compare = HCost.CompareTo(other.HCost); } return compare; } } /// /// Create a plan to achieve the goal from current state /// /// The GOAP agent /// Actions the agent can perform /// Current world state /// Goal to achieve /// Queue of actions to execute, or null if no plan found public Queue Plan( GOAPAgent agent, HashSet availableActions, WorldState currentState, GOAPGoal goal) { if (goal == null || availableActions == null || availableActions.Count == 0) { return null; } // Reset actions foreach (var action in availableActions) { action.Reset(); } // Get usable actions (procedural preconditions check) var usableActions = new HashSet(); foreach (var action in availableActions) { if (action.CheckProceduralPrecondition(agent)) { usableActions.Add(action); } } if (usableActions.Count == 0) { return null; } // A* search var openList = new List(); var closedSet = new HashSet(); // Start node var startNode = new PlanNode { State = currentState.Clone(), Action = null, Parent = null, GCost = 0, HCost = CalculateHeuristic(currentState, goal), Depth = 0 }; openList.Add(startNode); int iterations = 0; PlanNode goalNode = null; while (openList.Count > 0 && iterations < maxPlanningIterations) { iterations++; // Get node with lowest F cost openList.Sort(); var currentNode = openList[0]; openList.RemoveAt(0); // Check if goal is satisfied if (GoalSatisfied(currentNode.State, goal)) { goalNode = currentNode; break; } // Skip if max depth reached if (currentNode.Depth >= maxPlanDepth) { continue; } // Generate state hash for closed set string stateHash = currentNode.State.GetHashCode().ToString(); if (closedSet.Contains(stateHash)) { continue; } closedSet.Add(stateHash); // Expand node - try each action foreach (var action in usableActions) { // Check if action's preconditions are met if (!PreconditionsMet(currentNode.State, action)) { continue; } // Apply action effects to create new state var newState = currentNode.State.Clone(); ApplyEffects(newState, action); // Create new node var newNode = new PlanNode { State = newState, Action = action, Parent = currentNode, GCost = currentNode.GCost + action.Cost, HCost = CalculateHeuristic(newState, goal), Depth = currentNode.Depth + 1 }; // Check if already in open list with better cost string newStateHash = newState.GetHashCode().ToString(); if (!closedSet.Contains(newStateHash)) { openList.Add(newNode); } } } // Build plan from goal node if (goalNode != null) { return BuildPlan(goalNode); } return null; } /// /// Calculate heuristic (estimated cost to goal) /// private float CalculateHeuristic(WorldState state, GOAPGoal goal) { if (goal.DesiredState == null || goal.DesiredState.Count == 0) { return 0; } int unsatisfiedConditions = 0; foreach (var condition in goal.DesiredState) { if (!state.HasState(condition.Key) || !state.GetState(condition.Key).Equals(condition.Value)) { unsatisfiedConditions++; } } return unsatisfiedConditions; } /// /// Check if goal is satisfied by current state /// private bool GoalSatisfied(WorldState state, GOAPGoal goal) { if (goal.DesiredState == null || goal.DesiredState.Count == 0) { return true; } foreach (var condition in goal.DesiredState) { if (!state.HasState(condition.Key)) { return false; } var currentValue = state.GetState(condition.Key); if (!currentValue.Equals(condition.Value)) { return false; } } return true; } /// /// Check if action's preconditions are met /// private bool PreconditionsMet(WorldState state, GOAPActionBase action) { if (action.Preconditions == null || action.Preconditions.Count == 0) { return true; } foreach (var precondition in action.Preconditions) { if (!state.HasState(precondition.Key)) { return false; } var currentValue = state.GetState(precondition.Key); if (!currentValue.Equals(precondition.Value)) { return false; } } return true; } /// /// Apply action effects to state /// private void ApplyEffects(WorldState state, GOAPActionBase action) { if (action.Effects == null) { return; } foreach (var effect in action.Effects) { state.SetState(effect.Key, effect.Value); } } /// /// Build plan queue from goal node by backtracking /// private Queue BuildPlan(PlanNode goalNode) { var plan = new List(); var node = goalNode; while (node != null) { if (node.Action != null) { plan.Add(node.Action); } node = node.Parent; } plan.Reverse(); var queue = new Queue(); foreach (var action in plan) { queue.Enqueue(action); } return queue; } /// /// Set maximum planning iterations /// public void SetMaxIterations(int max) { maxPlanningIterations = Mathf.Max(100, max); } /// /// Set maximum plan depth /// public void SetMaxDepth(int depth) { maxPlanDepth = Mathf.Max(1, depth); } } }