using System; using Photon.Deterministic; using System.Collections.Generic; namespace Quantum { public static unsafe class GOAPManager { // PUBLIC MEMBERS public static EntityRef DebugEntity; // PRIVATE MEMBERS private static GOAPAStar.HeuristicCost _heuristicCost; // PUBLIC METHODS public static void Initialize(Frame frame, EntityRef entity, GOAPRoot root, GOAPAStar.HeuristicCost heuristicCost = null) { var agent = frame.Unsafe.GetPointer(entity); agent->Root = root; var disableTimes = frame.AllocateList(root.Goals.Length); for (int i = 0; i < root.GoalRefs.Length; i++) { disableTimes.Add(0); } agent->GoalDisableTimes = disableTimes; if (heuristicCost != null) { _heuristicCost = heuristicCost; } else if (_heuristicCost == null) { switch (sizeof(EWorldState)) { case 4: _heuristicCost = GOAPHeuristic.BitmaskDifferenceUInt32; break; //case 8: // _heuristicCost = GOAPHeuristic.BitmaskDifferenceUInt64; // break; default: throw new NotImplementedException($"Heuristic for EWorldState size of {sizeof(EWorldState)} bytes is not implemented"); } } } public static void Deinitialize(Frame frame, EntityRef entity) { var agent = frame.Unsafe.GetPointer(entity); agent->Root = default; frame.FreeList(agent->GoalDisableTimes); agent->GoalDisableTimes = default; } public static void Update(Frame frame, EntityRef entity, FP deltaTime) { var context = GetContext(frame, entity); var agent = context.Agent; bool debug = DebugEntity == entity; // Update disable times var goalDisableTimes = frame.ResolveList(agent->GoalDisableTimes); for (int i = 0; i < goalDisableTimes.Count; i++) { goalDisableTimes[i] = FPMath.Max(FP._0, goalDisableTimes[i] - deltaTime); } var currentGoal = agent->CurrentGoal.Id.IsValid == true ? frame.FindAsset(agent->CurrentGoal.Id) : null; var currentAction = GetCurrentAction(frame, agent); if (currentGoal != null) { // Decrease interruption timer agent->InterruptionCheckCooldown = FPMath.Max(agent->InterruptionCheckCooldown - deltaTime, 0); if (currentGoal.HasFinished(frame, context) == true) { StopCurrentGoal(frame, context, ref currentGoal, ref currentAction); } } if (currentGoal == null || (agent->InterruptionCheckCooldown <= 0 && currentGoal.IsInterruptible(currentAction) == true)) { FindNewGoal(frame, context, ref currentGoal, ref currentAction); } if (currentGoal != null) { UpdateCurrentGoal(frame, context, deltaTime, ref currentGoal, ref currentAction); } Pool.Return(context); } public static void StopCurrentGoal(Frame frame, EntityRef entity) { var context = GetContext(frame, entity); var currentGoal = context.Agent->CurrentGoal.Id.IsValid == true ? frame.FindAsset(context.Agent->CurrentGoal.Id) : null; var currentAction = GetCurrentAction(frame, context.Agent); StopCurrentGoal(frame, context, ref currentGoal, ref currentAction); } public static void SetGoalDisableTime(Frame frame, EntityRef entity, AssetRefGOAPGoal goal, FP disableTime) { if (goal.Id.IsValid == false) return; var agent = frame.Unsafe.GetPointer(entity); if (goal == agent->CurrentGoal) { StopCurrentGoal(frame, entity); } var root = frame.FindAsset(agent->Root.Id); int goalIndex = Array.IndexOf(root.GoalRefs, goal); if (goalIndex >= 0) { var disableTimes = frame.ResolveList(agent->GoalDisableTimes); disableTimes[goalIndex] = disableTime; } } // PRIVATE METHODS private static void UpdateCurrentGoal(Frame frame, GOAPEntityContext context, FP deltaTime, ref GOAPGoal currentGoal, ref GOAPAction currentAction) { var agent = context.Agent; bool debug = DebugEntity == context.Entity; if (currentAction != null && agent->CurrentState.Contains(currentAction.Effects) == true) { // This action is done, let's choose another one in next step StopCurrentAction(frame, context, ref currentAction); } // Activate next action from the plan if needed if (currentAction == null && agent->CurrentPlanSize > 0) { while (agent->CurrentActionIndex < agent->CurrentPlanSize - 1) { agent->LastProcessedActionIndex = agent->CurrentActionIndex; agent->CurrentActionIndex++; var nextAction = frame.FindAsset(agent->Plan[agent->CurrentActionIndex].Id); if (agent->CurrentState.Contains(nextAction.Conditions) == false) { // Conditions are not met, terminate whole plan StopCurrentGoal(frame, context, ref currentGoal, ref currentAction); break; } if (agent->CurrentState.Contains(nextAction.Effects) == false) { // This action is valid, activate it currentAction = nextAction; currentAction.Activate(frame, context); if (debug == true) { Log.Info($"GOAP: Action {currentAction.Path} activated"); } agent->CurrentActionTime = 0; break; } } if (currentAction == null && currentGoal != null) { if (debug == true) { Log.Info($"GOAP: Plan execution failed: Probably last action is finished but goal is not satisfied (state might change during execution). Goal: {currentGoal.Path}"); } StopCurrentGoal(frame, context, ref currentGoal, ref currentAction); } } // Update action if (currentAction != null) { var result = currentAction.Update(frame, context); if (result == GOAPAction.EResult.IsFailed) { StopCurrentGoal(frame, context, ref currentGoal, ref currentAction); } else if (result == GOAPAction.EResult.IsDone) { // This action claims to be done, apply effects and next action will be chosen next Update agent->CurrentState.Merge(currentAction.Effects); agent->LastProcessedActionIndex = agent->CurrentActionIndex; StopCurrentAction(frame, context, ref currentAction); } agent->CurrentActionTime += deltaTime; } if (currentGoal != null) { agent->CurrentGoalTime += deltaTime; } } private static void StopCurrentAction(Frame frame, GOAPEntityContext context, ref GOAPAction currentAction) { if (currentAction == null) return; if (context.Agent->Plan[context.Agent->CurrentActionIndex] != currentAction) { Log.Error($"GOAP: Trying to stop action {currentAction.Path} that isn't currently active."); return; } currentAction.Deactivate(frame, context); context.Agent->LastProcessedActionIndex = context.Agent->CurrentActionIndex; if (context.Entity == DebugEntity) { Log.Info($"GOAP: Action {currentAction.Path} deactivated"); } currentAction = null; } private static void StopCurrentGoal(Frame frame, GOAPEntityContext context, ref GOAPGoal currentGoal, ref GOAPAction currentAction) { var agent = context.Agent; StopCurrentAction(frame, context, ref currentAction); if (currentGoal != null) { currentGoal.Deactivate(frame, context); if (context.Entity == DebugEntity) { Log.Info($"GOAP: Goal {currentGoal.Path} deactivated"); } FP disableTime = currentGoal.GetDisableTime(frame, context); if (disableTime > 0) { var disableTimes = frame.ResolveList(agent->GoalDisableTimes); int goalIndex = Array.IndexOf(context.Root.Goals, currentGoal); if (goalIndex >= 0) { disableTimes[goalIndex] = disableTime; } } } agent->CurrentActionIndex = -1; agent->LastProcessedActionIndex = -1; agent->CurrentActionTime = 0; agent->CurrentPlanSize = 0; agent->CurrentGoal = default; agent->CurrentGoalTime = 0; currentGoal = null; currentAction = null; } private static void FindNewGoal(Frame frame, GOAPEntityContext context, ref GOAPGoal currentGoal, ref GOAPAction currentAction) { var agent = context.Agent; var goals = context.Root.Goals; GOAPGoal bestGoal = null; FP bestRelevancy = FP.MinValue; var disableTimes = frame.ResolveList(agent->GoalDisableTimes); for (int i = 0; i < goals.Length; i++) { if (disableTimes[i] > 0) continue; var goal = goals[i]; var startState = agent->CurrentState; startState.Merge(goal.StartState); if (startState.Contains(goal.TargetState) == true) continue; // Goal is satisfied FP relevancy = goal.GetRelevancy(frame, context); if (relevancy <= 0) continue; if (relevancy > bestRelevancy) { bestRelevancy = relevancy; bestGoal = goal; } } // Reset interruption timer agent->InterruptionCheckCooldown = context.Root.InterruptionCheckInterval; if (bestGoal == null || bestGoal == currentGoal) return; bool debug = context.Entity == DebugEntity; if (debug == true) { Log.Info($"GOAP: New best goal found: {bestGoal.Path}"); } GOAPState currentState = agent->CurrentState; GOAPState targetState = default; bestGoal.InitPlanning(frame, context, ref currentState, ref targetState); var aStar = Pool.Get(); List plan = null; if (debug == true) { using (new StopwatchBlock("GOAP: Backward A* search")) { plan = aStar.Run(frame, context, currentState, targetState, bestGoal, context.Root.Actions, _heuristicCost, Constants.MAX_PLAN_SIZE); } Log.Info($"GOAP: Search data - {aStar.Statistics.ToString()}"); } else { plan = aStar.Run(frame, context, currentState, targetState, bestGoal, context.Root.Actions, _heuristicCost, Constants.MAX_PLAN_SIZE); } if (plan == null) { if (debug == true) { Log.Info($"GOAP: Failed to find plan for goal {bestGoal.Path}"); } int goalIndex = Array.IndexOf(goals, bestGoal); // Ensure there will be at least one planning without this failed goal disableTimes[goalIndex] = FPMath.Max(FP._0_50, agent->InterruptionCheckCooldown + FP._0_10); Pool.Return(aStar); return; } if (currentGoal != null) { StopCurrentGoal(frame, context, ref currentGoal, ref currentAction); } agent->CurrentGoal = bestGoal; agent->CurrentGoalTime = 0; agent->CurrentState = currentState; agent->GoalState = targetState; agent->CurrentActionIndex = -1; agent->LastProcessedActionIndex = -1; agent->CurrentActionTime = 0; agent->CurrentPlanSize = 0; currentGoal = bestGoal; currentAction = null; for (int i = 0; i < plan.Count; i++) { var action = plan[i]; if (action == null) break; *agent->Plan.GetPointer(i) = action; agent->CurrentPlanSize++; } if (debug == true) { var planInfo = $"GOAP: Plan FOUND. Size: {agent->CurrentPlanSize} More..."; for (int i = 0; i < agent->CurrentPlanSize; i++) { planInfo += $"\nAction {i + 1}: {plan[i].Path}"; } Log.Info(planInfo); } currentGoal.Activate(frame, context); if (debug == true) { Log.Info($"GOAP: Goal {currentGoal.Path} activated"); } // Plan object is part of pooled GOAPAStar object // so GOAPAStar needs to be returned after plan is no longer needed Pool.Return(aStar); } private static GOAPAction GetCurrentAction(Frame frame, GOAPAgent* agent) { if (agent->CurrentActionIndex < 0) return null; if (agent->LastProcessedActionIndex >= agent->CurrentActionIndex) return null; return frame.FindAsset(agent->Plan[agent->CurrentActionIndex].Id); } private static GOAPEntityContext GetContext(Frame frame, EntityRef entity) { var context = Pool.Get(); context.Entity = entity; context.Agent = frame.Unsafe.GetPointer(entity); context.Blackboard = frame.Has(entity) ? frame.Unsafe.GetPointer(entity) : null; context.Root = frame.FindAsset(context.Agent->Root.Id); context.Config = frame.FindAsset(context.Agent->Config.Id); return context; } } public unsafe class GOAPEntityContext { public EntityRef Entity; public GOAPAgent* Agent; public GOAPRoot Root; public AIConfig Config; public AIBlackboardComponent* Blackboard; } }