[Add] Synaptic AI Pro
https://assetstore.unity.com/packages/tools/generative-ai/synaptic-ai-pro-natural-language-control-for-unity-336030
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SynapticPro.GOAP
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for all GOAP actions
|
||||
/// Inherit from this to create custom actions
|
||||
/// </summary>
|
||||
public abstract class GOAPActionBase : MonoBehaviour
|
||||
{
|
||||
[Header("Action Settings")]
|
||||
[SerializeField] protected string actionName = "Action";
|
||||
[SerializeField] protected float cost = 1f;
|
||||
[SerializeField] protected float duration = 1f;
|
||||
|
||||
[Header("Target")]
|
||||
[SerializeField] protected GameObject target;
|
||||
[SerializeField] protected bool requiresTarget = false;
|
||||
[SerializeField] protected float targetRange = 2f;
|
||||
|
||||
/// <summary>
|
||||
/// Preconditions that must be true for this action to run
|
||||
/// </summary>
|
||||
public Dictionary<string, object> Preconditions { get; protected set; } = new Dictionary<string, object>();
|
||||
|
||||
/// <summary>
|
||||
/// Effects this action has on the world state
|
||||
/// </summary>
|
||||
public Dictionary<string, object> Effects { get; protected set; } = new Dictionary<string, object>();
|
||||
|
||||
/// <summary>
|
||||
/// Cost of performing this action
|
||||
/// </summary>
|
||||
public float Cost
|
||||
{
|
||||
get => cost;
|
||||
set => cost = Mathf.Max(0.01f, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Name of this action
|
||||
/// </summary>
|
||||
public string ActionName => actionName;
|
||||
|
||||
/// <summary>
|
||||
/// Duration of this action
|
||||
/// </summary>
|
||||
public float Duration => duration;
|
||||
|
||||
/// <summary>
|
||||
/// Current target
|
||||
/// </summary>
|
||||
public GameObject Target
|
||||
{
|
||||
get => target;
|
||||
set => target = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether action is currently running
|
||||
/// </summary>
|
||||
public bool IsRunning { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether action has completed
|
||||
/// </summary>
|
||||
public bool IsDone { get; protected set; }
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
SetupPreconditionsAndEffects();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override to setup preconditions and effects
|
||||
/// </summary>
|
||||
protected virtual void SetupPreconditionsAndEffects()
|
||||
{
|
||||
// Override in derived classes
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a precondition
|
||||
/// </summary>
|
||||
public void AddPrecondition(string key, object value)
|
||||
{
|
||||
if (!Preconditions.ContainsKey(key))
|
||||
{
|
||||
Preconditions.Add(key, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
Preconditions[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a precondition
|
||||
/// </summary>
|
||||
public void RemovePrecondition(string key)
|
||||
{
|
||||
if (Preconditions.ContainsKey(key))
|
||||
{
|
||||
Preconditions.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an effect
|
||||
/// </summary>
|
||||
public void AddEffect(string key, object value)
|
||||
{
|
||||
if (!Effects.ContainsKey(key))
|
||||
{
|
||||
Effects.Add(key, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
Effects[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove an effect
|
||||
/// </summary>
|
||||
public void RemoveEffect(string key)
|
||||
{
|
||||
if (Effects.ContainsKey(key))
|
||||
{
|
||||
Effects.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reset action state
|
||||
/// </summary>
|
||||
public virtual void Reset()
|
||||
{
|
||||
IsRunning = false;
|
||||
IsDone = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if agent is in range of target
|
||||
/// </summary>
|
||||
public bool IsInRange(GOAPAgent agent)
|
||||
{
|
||||
if (!requiresTarget || target == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
float distance = Vector3.Distance(agent.transform.position, target.transform.position);
|
||||
return distance <= targetRange;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check procedural preconditions (runtime checks)
|
||||
/// Override for custom runtime conditions
|
||||
/// </summary>
|
||||
public virtual bool CheckProceduralPrecondition(GOAPAgent agent)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called before action starts
|
||||
/// Return true if action can start
|
||||
/// </summary>
|
||||
public virtual bool PrePerform(GOAPAgent agent)
|
||||
{
|
||||
IsRunning = true;
|
||||
IsDone = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called each frame while action is running
|
||||
/// Return true when action is complete
|
||||
/// </summary>
|
||||
public abstract bool Perform(GOAPAgent agent);
|
||||
|
||||
/// <summary>
|
||||
/// Called after action completes
|
||||
/// Return true if action succeeded
|
||||
/// </summary>
|
||||
public virtual bool PostPerform(GOAPAgent agent)
|
||||
{
|
||||
IsRunning = false;
|
||||
IsDone = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when action is interrupted
|
||||
/// </summary>
|
||||
public virtual void OnInterrupted(GOAPAgent agent)
|
||||
{
|
||||
IsRunning = false;
|
||||
IsDone = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get string representation
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{actionName} (Cost: {cost})";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple action that completes after duration
|
||||
/// Use for basic actions without complex logic
|
||||
/// </summary>
|
||||
public class GOAPSimpleAction : GOAPActionBase
|
||||
{
|
||||
private float elapsedTime;
|
||||
|
||||
public override bool Perform(GOAPAgent agent)
|
||||
{
|
||||
elapsedTime += Time.deltaTime;
|
||||
|
||||
if (elapsedTime >= duration)
|
||||
{
|
||||
elapsedTime = 0f;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
elapsedTime = 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dfd639f3261cb4d0f98ecac7725beec2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 336030
|
||||
packageName: Synaptic AI Pro - Natural Language Control for Unity
|
||||
packageVersion: 1.2.23
|
||||
assetPath: Assets/Synaptic AI Pro/Runtime/GOAP/GOAPActionBase.cs
|
||||
uploadId: 920982
|
||||
@@ -0,0 +1,555 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace SynapticPro.GOAP
|
||||
{
|
||||
/// <summary>
|
||||
/// GOAP Agent - Main component that executes GOAP planning and actions
|
||||
/// Attach to any GameObject to give it GOAP AI capabilities
|
||||
/// </summary>
|
||||
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<GOAPGoal> OnGoalChanged;
|
||||
public UnityEvent<GOAPActionBase> OnActionStarted;
|
||||
public UnityEvent<GOAPActionBase> OnActionCompleted;
|
||||
public UnityEvent<GOAPActionBase> OnActionFailed;
|
||||
public UnityEvent OnPlanCreated;
|
||||
public UnityEvent OnPlanFailed;
|
||||
|
||||
// Runtime state
|
||||
private GOAPPlanner planner;
|
||||
private WorldState worldState;
|
||||
private HashSet<GOAPActionBase> availableActions;
|
||||
private List<GOAPGoal> goals;
|
||||
private Queue<GOAPActionBase> currentPlan;
|
||||
private GOAPActionBase currentAction;
|
||||
private GOAPGoal currentGoal;
|
||||
private float lastPlanTime;
|
||||
private float actionStartTime;
|
||||
private bool isRunning;
|
||||
|
||||
/// <summary>
|
||||
/// Current world state
|
||||
/// </summary>
|
||||
public WorldState WorldState => worldState;
|
||||
|
||||
/// <summary>
|
||||
/// Currently executing action
|
||||
/// </summary>
|
||||
public GOAPActionBase CurrentAction => currentAction;
|
||||
|
||||
/// <summary>
|
||||
/// Current goal being pursued
|
||||
/// </summary>
|
||||
public GOAPGoal CurrentGoal => currentGoal;
|
||||
|
||||
/// <summary>
|
||||
/// Whether agent is currently running
|
||||
/// </summary>
|
||||
public bool IsRunning => isRunning;
|
||||
|
||||
/// <summary>
|
||||
/// Whether agent has a valid plan
|
||||
/// </summary>
|
||||
public bool HasPlan => currentPlan != null && currentPlan.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Movement speed
|
||||
/// </summary>
|
||||
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<GOAPActionBase>();
|
||||
goals = new List<GOAPGoal>();
|
||||
currentPlan = new Queue<GOAPActionBase>();
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start the GOAP agent
|
||||
/// </summary>
|
||||
public void StartAgent()
|
||||
{
|
||||
isRunning = true;
|
||||
TryReplan();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop the GOAP agent
|
||||
/// </summary>
|
||||
public void StopAgent()
|
||||
{
|
||||
isRunning = false;
|
||||
if (currentAction != null)
|
||||
{
|
||||
currentAction.OnInterrupted(this);
|
||||
currentAction = null;
|
||||
}
|
||||
currentPlan?.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collect all GOAPActionBase components
|
||||
/// </summary>
|
||||
private void CollectActions()
|
||||
{
|
||||
var actions = GetComponents<GOAPActionBase>();
|
||||
foreach (var action in actions)
|
||||
{
|
||||
availableActions.Add(action);
|
||||
}
|
||||
|
||||
// Also check children
|
||||
var childActions = GetComponentsInChildren<GOAPActionBase>();
|
||||
foreach (var action in childActions)
|
||||
{
|
||||
availableActions.Add(action);
|
||||
}
|
||||
|
||||
if (debugMode)
|
||||
{
|
||||
Debug.Log($"[GOAPAgent] Collected {availableActions.Count} actions");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collect all GOAPGoalComponent goals
|
||||
/// </summary>
|
||||
private void CollectGoals()
|
||||
{
|
||||
var goalComponents = GetComponents<GOAPGoalComponent>();
|
||||
foreach (var gc in goalComponents)
|
||||
{
|
||||
goals.Add(gc.Goal);
|
||||
}
|
||||
|
||||
// Also check children
|
||||
var childGoals = GetComponentsInChildren<GOAPGoalComponent>();
|
||||
foreach (var gc in childGoals)
|
||||
{
|
||||
if (!goals.Contains(gc.Goal))
|
||||
{
|
||||
goals.Add(gc.Goal);
|
||||
}
|
||||
}
|
||||
|
||||
if (debugMode)
|
||||
{
|
||||
Debug.Log($"[GOAPAgent] Collected {goals.Count} goals");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an action at runtime
|
||||
/// </summary>
|
||||
public void AddAction(GOAPActionBase action)
|
||||
{
|
||||
if (action != null)
|
||||
{
|
||||
availableActions.Add(action);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove an action at runtime
|
||||
/// </summary>
|
||||
public void RemoveAction(GOAPActionBase action)
|
||||
{
|
||||
if (action != null)
|
||||
{
|
||||
availableActions.Remove(action);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a goal at runtime
|
||||
/// </summary>
|
||||
public void AddGoal(GOAPGoal goal)
|
||||
{
|
||||
if (goal != null && !goals.Contains(goal))
|
||||
{
|
||||
goals.Add(goal);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a goal at runtime
|
||||
/// </summary>
|
||||
public void RemoveGoal(GOAPGoal goal)
|
||||
{
|
||||
if (goal != null)
|
||||
{
|
||||
goals.Remove(goal);
|
||||
if (currentGoal == goal)
|
||||
{
|
||||
currentGoal = null;
|
||||
currentPlan?.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set world state value
|
||||
/// </summary>
|
||||
public void SetWorldState(string key, object value)
|
||||
{
|
||||
worldState.SetState(key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get world state value
|
||||
/// </summary>
|
||||
public T GetWorldState<T>(string key)
|
||||
{
|
||||
return worldState.GetState<T>(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to create a new plan
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Select the best goal to pursue
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a plan to achieve the goal
|
||||
/// </summary>
|
||||
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<GOAPActionBase>();
|
||||
OnPlanFailed?.Invoke();
|
||||
|
||||
if (debugMode)
|
||||
{
|
||||
Debug.LogWarning($"[GOAPAgent] Failed to create plan for '{goal.GoalName}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute the current action
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move towards target
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force immediate replan
|
||||
/// </summary>
|
||||
public void ForceReplan()
|
||||
{
|
||||
if (currentAction != null)
|
||||
{
|
||||
currentAction.OnInterrupted(this);
|
||||
currentAction = null;
|
||||
}
|
||||
currentPlan?.Clear();
|
||||
TryReplan();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interrupt current action
|
||||
/// </summary>
|
||||
public void InterruptCurrentAction()
|
||||
{
|
||||
if (currentAction != null)
|
||||
{
|
||||
currentAction.OnInterrupted(this);
|
||||
OnActionFailed?.Invoke(currentAction);
|
||||
currentAction = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get remaining actions in plan
|
||||
/// </summary>
|
||||
public List<GOAPActionBase> GetRemainingPlan()
|
||||
{
|
||||
var remaining = new List<GOAPActionBase>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cb04014db39de4036863a1e3c469158a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 336030
|
||||
packageName: Synaptic AI Pro - Natural Language Control for Unity
|
||||
packageVersion: 1.2.23
|
||||
assetPath: Assets/Synaptic AI Pro/Runtime/GOAP/GOAPAgent.cs
|
||||
uploadId: 920982
|
||||
@@ -0,0 +1,505 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SynapticPro.GOAP
|
||||
{
|
||||
/// <summary>
|
||||
/// GOAP Basic Behavior Pattern Template Collection
|
||||
/// </summary>
|
||||
public static class GOAPBehaviorTemplates
|
||||
{
|
||||
/// <summary>
|
||||
/// 1. Guard AI - Patrol and Intruder Response
|
||||
/// </summary>
|
||||
public static BehaviorTemplate GuardTemplate = new BehaviorTemplate
|
||||
{
|
||||
Name = "Guard AI",
|
||||
Description = "Patrols designated area and responds to threats",
|
||||
Goals = new List<Goal>
|
||||
{
|
||||
new Goal { Name = "MaintainSecurity", Priority = 100 },
|
||||
new Goal { Name = "PatrolArea", Priority = 80 },
|
||||
new Goal { Name = "InvestigateDisturbance", Priority = 90 },
|
||||
new Goal { Name = "EliminateThreat", Priority = 110 }
|
||||
},
|
||||
Actions = new List<GOAPAction>
|
||||
{
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "PatrolWaypoints",
|
||||
Cost = 1f,
|
||||
Preconditions = new[] { "on_duty", "no_threats" },
|
||||
Effects = new[] { "area_patrolled" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "InvestigateSound",
|
||||
Cost = 1.5f,
|
||||
Preconditions = new[] { "sound_detected" },
|
||||
Effects = new[] { "sound_investigated" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "RaiseAlarm",
|
||||
Cost = 0.5f,
|
||||
Preconditions = new[] { "threat_confirmed" },
|
||||
Effects = new[] { "alarm_raised", "backup_requested" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "EngageIntruder",
|
||||
Cost = 2f,
|
||||
Preconditions = new[] { "has_weapon", "intruder_in_range" },
|
||||
Effects = new[] { "intruder_neutralized" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "CallBackup",
|
||||
Cost = 0.3f,
|
||||
Preconditions = new[] { "radio_available", "threat_detected" },
|
||||
Effects = new[] { "backup_called" }
|
||||
}
|
||||
},
|
||||
Sensors = new[] { "sound_detector", "motion_sensor", "threat_evaluator", "radio_checker" },
|
||||
InitialWorldState = new Dictionary<string, object>
|
||||
{
|
||||
["on_duty"] = true,
|
||||
["has_weapon"] = true,
|
||||
["radio_available"] = true,
|
||||
["patrol_route"] = "defined"
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 2. Collector Worker AI - Resource Gathering and Transportation
|
||||
/// </summary>
|
||||
public static BehaviorTemplate CollectorTemplate = new BehaviorTemplate
|
||||
{
|
||||
Name = "Collector AI",
|
||||
Description = "Gathers resources and delivers them to storage",
|
||||
Goals = new List<Goal>
|
||||
{
|
||||
new Goal { Name = "MaximizeResourceCollection", Priority = 100 },
|
||||
new Goal { Name = "MaintainEfficiency", Priority = 70 },
|
||||
new Goal { Name = "AvoidDanger", Priority = 90 }
|
||||
},
|
||||
Actions = new List<GOAPAction>
|
||||
{
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "LocateResource",
|
||||
Cost = 1f,
|
||||
Preconditions = new[] { "inventory_not_full", "energy_available" },
|
||||
Effects = new[] { "resource_located" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "MoveToResource",
|
||||
Cost = 1.5f,
|
||||
Preconditions = new[] { "resource_located" },
|
||||
Effects = new[] { "at_resource" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "GatherResource",
|
||||
Cost = 2f,
|
||||
Preconditions = new[] { "at_resource", "has_tool" },
|
||||
Effects = new[] { "resource_collected", "inventory_increased" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "ReturnToBase",
|
||||
Cost = 1.5f,
|
||||
Preconditions = new[] { "inventory_full" },
|
||||
Effects = new[] { "at_base" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "DepositResource",
|
||||
Cost = 0.5f,
|
||||
Preconditions = new[] { "at_base", "has_resource" },
|
||||
Effects = new[] { "resource_deposited", "inventory_empty" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "Rest",
|
||||
Cost = 1f,
|
||||
Preconditions = new[] { "energy_low" },
|
||||
Effects = new[] { "energy_restored" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "FleeFromDanger",
|
||||
Cost = 0.1f,
|
||||
Preconditions = new[] { "danger_detected" },
|
||||
Effects = new[] { "safe_distance" }
|
||||
}
|
||||
},
|
||||
Sensors = new[] { "resource_scanner", "inventory_monitor", "energy_tracker", "danger_detector" },
|
||||
InitialWorldState = new Dictionary<string, object>
|
||||
{
|
||||
["has_tool"] = true,
|
||||
["inventory_capacity"] = 10,
|
||||
["energy"] = 100,
|
||||
["base_location"] = "known"
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 3. Combat Soldier AI - Tactical Combat and Cooperative Behavior
|
||||
/// </summary>
|
||||
public static BehaviorTemplate SoldierTemplate = new BehaviorTemplate
|
||||
{
|
||||
Name = "Combat Soldier AI",
|
||||
Description = "Engages enemies tactically with squad coordination",
|
||||
Goals = new List<Goal>
|
||||
{
|
||||
new Goal { Name = "EliminateEnemies", Priority = 100 },
|
||||
new Goal { Name = "SurviveCombat", Priority = 95 },
|
||||
new Goal { Name = "SupportSquad", Priority = 85 },
|
||||
new Goal { Name = "SecureObjective", Priority = 90 }
|
||||
},
|
||||
Actions = new List<GOAPAction>
|
||||
{
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "TakeCover",
|
||||
Cost = 0.5f,
|
||||
Preconditions = new[] { "under_fire", "cover_available" },
|
||||
Effects = new[] { "in_cover", "damage_reduced" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "AimAndShoot",
|
||||
Cost = 1f,
|
||||
Preconditions = new[] { "enemy_visible", "has_ammo", "weapon_ready" },
|
||||
Effects = new[] { "damage_dealt", "ammo_consumed" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "SuppressiveFire",
|
||||
Cost = 2f,
|
||||
Preconditions = new[] { "enemy_position_known", "ammo_sufficient" },
|
||||
Effects = new[] { "enemy_suppressed" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "Reload",
|
||||
Cost = 1.5f,
|
||||
Preconditions = new[] { "ammo_low", "has_magazine" },
|
||||
Effects = new[] { "weapon_reloaded" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "ThrowGrenade",
|
||||
Cost = 2f,
|
||||
Preconditions = new[] { "has_grenade", "enemy_clustered" },
|
||||
Effects = new[] { "area_cleared" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "RequestMedic",
|
||||
Cost = 0.3f,
|
||||
Preconditions = new[] { "health_critical", "medic_available" },
|
||||
Effects = new[] { "healing_requested" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "FlankEnemy",
|
||||
Cost = 2.5f,
|
||||
Preconditions = new[] { "flank_route_available", "squad_covering" },
|
||||
Effects = new[] { "enemy_flanked" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "CoverAlly",
|
||||
Cost = 1f,
|
||||
Preconditions = new[] { "ally_needs_cover", "position_good" },
|
||||
Effects = new[] { "ally_covered" }
|
||||
}
|
||||
},
|
||||
Sensors = new[] { "enemy_tracker", "squad_coordinator", "ammo_counter", "health_monitor", "cover_finder" },
|
||||
InitialWorldState = new Dictionary<string, object>
|
||||
{
|
||||
["weapon"] = "assault_rifle",
|
||||
["ammo"] = 120,
|
||||
["magazines"] = 4,
|
||||
["grenades"] = 2,
|
||||
["health"] = 100,
|
||||
["squad_size"] = 4
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 4. Wildlife AI - Survival Instincts and Territorial Behavior
|
||||
/// </summary>
|
||||
public static BehaviorTemplate WildlifeTemplate = new BehaviorTemplate
|
||||
{
|
||||
Name = "Wildlife AI",
|
||||
Description = "Survives through hunting, territorial behavior, and avoiding predators",
|
||||
Goals = new List<Goal>
|
||||
{
|
||||
new Goal { Name = "Survive", Priority = 100 },
|
||||
new Goal { Name = "FindFood", Priority = 90 },
|
||||
new Goal { Name = "DefendTerritory", Priority = 70 },
|
||||
new Goal { Name = "Reproduce", Priority = 60 }
|
||||
},
|
||||
Actions = new List<GOAPAction>
|
||||
{
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "HuntPrey",
|
||||
Cost = 2f,
|
||||
Preconditions = new[] { "hungry", "prey_detected", "energy_sufficient" },
|
||||
Effects = new[] { "food_obtained" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "Graze",
|
||||
Cost = 1f,
|
||||
Preconditions = new[] { "vegetation_available", "safe_area" },
|
||||
Effects = new[] { "hunger_reduced" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "DrinkWater",
|
||||
Cost = 0.5f,
|
||||
Preconditions = new[] { "thirsty", "water_source_nearby" },
|
||||
Effects = new[] { "thirst_quenched" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "FleeFromPredator",
|
||||
Cost = 0.1f,
|
||||
Preconditions = new[] { "predator_detected" },
|
||||
Effects = new[] { "safe_from_predator" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "DefendTerritory",
|
||||
Cost = 1.5f,
|
||||
Preconditions = new[] { "intruder_in_territory", "strong_enough" },
|
||||
Effects = new[] { "territory_secured" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "MarkTerritory",
|
||||
Cost = 0.5f,
|
||||
Preconditions = new[] { "territory_unmarked" },
|
||||
Effects = new[] { "territory_marked" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "RestInDen",
|
||||
Cost = 1f,
|
||||
Preconditions = new[] { "tired", "den_available" },
|
||||
Effects = new[] { "energy_restored" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "CallMate",
|
||||
Cost = 1f,
|
||||
Preconditions = new[] { "mating_season", "no_mate" },
|
||||
Effects = new[] { "mate_attracted" }
|
||||
}
|
||||
},
|
||||
Sensors = new[] { "smell_sensor", "hearing_sensor", "hunger_monitor", "thirst_monitor", "threat_detector", "territory_scanner" },
|
||||
InitialWorldState = new Dictionary<string, object>
|
||||
{
|
||||
["species"] = "wolf",
|
||||
["hunger"] = 50,
|
||||
["thirst"] = 30,
|
||||
["energy"] = 80,
|
||||
["health"] = 100,
|
||||
["territory_size"] = 100
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 5. Merchant NPC AI - Trading and Economic Activities
|
||||
/// </summary>
|
||||
public static BehaviorTemplate MerchantTemplate = new BehaviorTemplate
|
||||
{
|
||||
Name = "Merchant NPC AI",
|
||||
Description = "Trades with players, manages inventory, and maximizes profit",
|
||||
Goals = new List<Goal>
|
||||
{
|
||||
new Goal { Name = "MaximizeProfit", Priority = 100 },
|
||||
new Goal { Name = "MaintainInventory", Priority = 80 },
|
||||
new Goal { Name = "BuildReputation", Priority = 70 },
|
||||
new Goal { Name = "StaySafe", Priority = 90 }
|
||||
},
|
||||
Actions = new List<GOAPAction>
|
||||
{
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "GreetCustomer",
|
||||
Cost = 0.2f,
|
||||
Preconditions = new[] { "customer_nearby", "shop_open" },
|
||||
Effects = new[] { "customer_engaged" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "NegotiatePrice",
|
||||
Cost = 1f,
|
||||
Preconditions = new[] { "customer_interested", "item_available" },
|
||||
Effects = new[] { "price_negotiated" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "CompleteSale",
|
||||
Cost = 0.5f,
|
||||
Preconditions = new[] { "price_agreed", "item_in_stock" },
|
||||
Effects = new[] { "sale_completed", "gold_increased" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "RestockInventory",
|
||||
Cost = 3f,
|
||||
Preconditions = new[] { "stock_low", "gold_sufficient" },
|
||||
Effects = new[] { "inventory_restocked" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "HireGuard",
|
||||
Cost = 2f,
|
||||
Preconditions = new[] { "threat_level_high", "gold_available" },
|
||||
Effects = new[] { "shop_protected" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "AdvertiseWares",
|
||||
Cost = 1f,
|
||||
Preconditions = new[] { "customers_few" },
|
||||
Effects = new[] { "customers_attracted" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "CloseShop",
|
||||
Cost = 0.5f,
|
||||
Preconditions = new[] { "danger_imminent" },
|
||||
Effects = new[] { "shop_secured" }
|
||||
},
|
||||
new GOAPAction
|
||||
{
|
||||
Name = "OfferDiscount",
|
||||
Cost = 1.5f,
|
||||
Preconditions = new[] { "inventory_excess", "customer_hesitant" },
|
||||
Effects = new[] { "sale_likely" }
|
||||
}
|
||||
},
|
||||
Sensors = new[] { "customer_detector", "inventory_tracker", "market_analyzer", "threat_assessor", "reputation_monitor" },
|
||||
InitialWorldState = new Dictionary<string, object>
|
||||
{
|
||||
["gold"] = 1000,
|
||||
["shop_location"] = "market_square",
|
||||
["inventory_slots"] = 20,
|
||||
["reputation"] = 50,
|
||||
["shop_open"] = true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Behavior Template Structure
|
||||
/// </summary>
|
||||
public class BehaviorTemplate
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
public List<Goal> Goals { get; set; }
|
||||
public List<GOAPAction> Actions { get; set; }
|
||||
public string[] Sensors { get; set; }
|
||||
public Dictionary<string, object> InitialWorldState { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GOAP Goal Template Data (for serialization/templates)
|
||||
/// Use GOAPGoal class for runtime goals
|
||||
/// </summary>
|
||||
public class Goal
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public float Priority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Convert to runtime GOAPGoal
|
||||
/// </summary>
|
||||
public GOAPGoal ToRuntimeGoal()
|
||||
{
|
||||
return new GOAPGoal(Name, (int)Priority);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GOAP Action Template Data (for serialization/templates)
|
||||
/// Use GOAPActionBase or GOAPDynamicAction for runtime actions
|
||||
/// </summary>
|
||||
public class GOAPAction
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public float Cost { get; set; }
|
||||
public string[] Preconditions { get; set; }
|
||||
public string[] Effects { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create runtime action from this template
|
||||
/// </summary>
|
||||
public GOAPDynamicAction CreateRuntimeAction(GameObject parent)
|
||||
{
|
||||
return GOAPActionFactory.CreateFromBehaviorData(
|
||||
parent,
|
||||
Name,
|
||||
Cost,
|
||||
Preconditions,
|
||||
Effects
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for applying templates to agents
|
||||
/// </summary>
|
||||
public static class BehaviorTemplateExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Apply a behavior template to a GOAPAgent
|
||||
/// </summary>
|
||||
public static void ApplyTemplate(this GOAPAgent agent, BehaviorTemplate template)
|
||||
{
|
||||
if (agent == null || template == null) return;
|
||||
|
||||
// Apply goals
|
||||
if (template.Goals != null)
|
||||
{
|
||||
foreach (var goalData in template.Goals)
|
||||
{
|
||||
var goal = goalData.ToRuntimeGoal();
|
||||
goal.IsActive = true;
|
||||
agent.AddGoal(goal);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply actions
|
||||
if (template.Actions != null)
|
||||
{
|
||||
foreach (var actionData in template.Actions)
|
||||
{
|
||||
var action = actionData.CreateRuntimeAction(agent.gameObject);
|
||||
agent.AddAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply initial world state
|
||||
if (template.InitialWorldState != null)
|
||||
{
|
||||
foreach (var kvp in template.InitialWorldState)
|
||||
{
|
||||
agent.SetWorldState(kvp.Key, kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"[GOAP] Applied template '{template.Name}' to agent '{agent.name}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 12bafd09c0eae4c659f870ecfb642515
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 336030
|
||||
packageName: Synaptic AI Pro - Natural Language Control for Unity
|
||||
packageVersion: 1.2.23
|
||||
assetPath: Assets/Synaptic AI Pro/Runtime/GOAP/GOAPBehaviorTemplates.cs
|
||||
uploadId: 920982
|
||||
@@ -0,0 +1,592 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace SynapticPro.GOAP
|
||||
{
|
||||
/// <summary>
|
||||
/// Class for debugging and visualizing GOAP AI
|
||||
/// </summary>
|
||||
public class GOAPDebugVisualizer : MonoBehaviour
|
||||
{
|
||||
[Header("Debug Settings")]
|
||||
public bool showCurrentGoal = true;
|
||||
public bool showActionPlan = true;
|
||||
public bool showWorldState = true;
|
||||
public bool showDecisionGraph = false;
|
||||
public bool showPerformanceMetrics = true;
|
||||
|
||||
[Header("Visual Settings")]
|
||||
public Color goalColor = Color.green;
|
||||
public Color actionColor = Color.blue;
|
||||
public Color completedActionColor = Color.gray;
|
||||
public Color failedActionColor = Color.red;
|
||||
public float debugWindowWidth = 300f;
|
||||
|
||||
[Header("Performance")]
|
||||
public int maxPlanDepth = 10;
|
||||
public float planningTimeout = 0.1f; // 100ms
|
||||
|
||||
// Debug information
|
||||
private GOAPDebugInfo debugInfo = new GOAPDebugInfo();
|
||||
private Queue<PlanningMetrics> performanceHistory = new Queue<PlanningMetrics>(100);
|
||||
|
||||
// For GUI
|
||||
private Vector2 scrollPosition;
|
||||
private bool showDebugWindow = true;
|
||||
|
||||
void Start()
|
||||
{
|
||||
// Initialize demo data
|
||||
InitializeDemoData();
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// Update debug information
|
||||
UpdateDebugInfo();
|
||||
|
||||
// Performance measurement
|
||||
if (showPerformanceMetrics && Time.frameCount % 60 == 0)
|
||||
{
|
||||
UpdatePerformanceMetrics();
|
||||
}
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
if (!showDebugWindow) return;
|
||||
|
||||
// Draw debug window
|
||||
GUILayout.Window(0, new Rect(10, 10, debugWindowWidth, 600), DrawDebugWindow, "GOAP Debug");
|
||||
|
||||
// World space visualization
|
||||
if (showDecisionGraph)
|
||||
{
|
||||
DrawDecisionGraphOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
void OnDrawGizmos()
|
||||
{
|
||||
if (!enabled) return;
|
||||
|
||||
// Visualize agent's current state
|
||||
DrawAgentStatus();
|
||||
|
||||
// Visualize action execution status
|
||||
if (showActionPlan)
|
||||
{
|
||||
DrawActionPlan();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawDebugWindow(int windowID)
|
||||
{
|
||||
scrollPosition = GUILayout.BeginScrollView(scrollPosition);
|
||||
|
||||
// Current goal
|
||||
if (showCurrentGoal)
|
||||
{
|
||||
GUILayout.Label("=== Current Goal ===", GetBoldLabelStyle());
|
||||
GUI.color = goalColor;
|
||||
GUILayout.Label($"Goal: {debugInfo.currentGoal}");
|
||||
GUILayout.Label($"Priority: {debugInfo.goalPriority}");
|
||||
GUI.color = Color.white;
|
||||
GUILayout.Space(10);
|
||||
}
|
||||
|
||||
// Action plan
|
||||
if (showActionPlan)
|
||||
{
|
||||
GUILayout.Label("=== Action Plan ===", GetBoldLabelStyle());
|
||||
if (debugInfo.currentPlan != null && debugInfo.currentPlan.Count > 0)
|
||||
{
|
||||
for (int i = 0; i < debugInfo.currentPlan.Count; i++)
|
||||
{
|
||||
var action = debugInfo.currentPlan[i];
|
||||
GUI.color = GetActionColor(action);
|
||||
GUILayout.Label($"{i + 1}. {action.name} (Cost: {action.cost})");
|
||||
|
||||
if (i == debugInfo.currentActionIndex)
|
||||
{
|
||||
GUILayout.Label($" Status: {action.status}");
|
||||
GUILayout.Label($" Progress: {action.progress:P}");
|
||||
}
|
||||
}
|
||||
GUI.color = Color.white;
|
||||
GUILayout.Label($"Total Cost: {debugInfo.planCost}");
|
||||
}
|
||||
else
|
||||
{
|
||||
GUILayout.Label("No active plan");
|
||||
}
|
||||
GUILayout.Space(10);
|
||||
}
|
||||
|
||||
// World state
|
||||
if (showWorldState)
|
||||
{
|
||||
GUILayout.Label("=== World State ===", GetBoldLabelStyle());
|
||||
foreach (var state in debugInfo.worldState)
|
||||
{
|
||||
GUILayout.Label($"{state.Key}: {state.Value}");
|
||||
}
|
||||
GUILayout.Space(10);
|
||||
}
|
||||
|
||||
// Performance metrics
|
||||
if (showPerformanceMetrics)
|
||||
{
|
||||
GUILayout.Label("=== Performance ===", GetBoldLabelStyle());
|
||||
GUILayout.Label($"Planning Time: {debugInfo.lastPlanningTime:F3}s");
|
||||
GUILayout.Label($"Plan Attempts: {debugInfo.planAttempts}");
|
||||
GUILayout.Label($"Graph Nodes: {debugInfo.graphNodes}");
|
||||
GUILayout.Label($"Graph Edges: {debugInfo.graphEdges}");
|
||||
GUILayout.Label($"Memory Usage: {debugInfo.memoryUsage:F2} MB");
|
||||
|
||||
// Performance graph
|
||||
DrawPerformanceGraph();
|
||||
GUILayout.Space(10);
|
||||
}
|
||||
|
||||
// Control buttons
|
||||
GUILayout.Label("=== Controls ===", GetBoldLabelStyle());
|
||||
if (GUILayout.Button("Force Replan"))
|
||||
{
|
||||
ForceReplan();
|
||||
}
|
||||
if (GUILayout.Button("Clear Plan"))
|
||||
{
|
||||
ClearPlan();
|
||||
}
|
||||
if (GUILayout.Button("Export Debug Log"))
|
||||
{
|
||||
ExportDebugLog();
|
||||
}
|
||||
|
||||
showDecisionGraph = GUILayout.Toggle(showDecisionGraph, "Show Decision Graph");
|
||||
|
||||
GUILayout.EndScrollView();
|
||||
|
||||
GUI.DragWindow();
|
||||
}
|
||||
|
||||
private void DrawAgentStatus()
|
||||
{
|
||||
// Display state at agent position
|
||||
Vector3 agentPos = transform.position + Vector3.up * 2f;
|
||||
|
||||
// Display current goal
|
||||
if (!string.IsNullOrEmpty(debugInfo.currentGoal))
|
||||
{
|
||||
Gizmos.color = goalColor;
|
||||
DrawString(agentPos, debugInfo.currentGoal, Color.green);
|
||||
}
|
||||
|
||||
// Display current action
|
||||
if (debugInfo.currentPlan != null && debugInfo.currentActionIndex >= 0 &&
|
||||
debugInfo.currentActionIndex < debugInfo.currentPlan.Count)
|
||||
{
|
||||
var currentAction = debugInfo.currentPlan[debugInfo.currentActionIndex];
|
||||
Gizmos.color = actionColor;
|
||||
DrawString(agentPos + Vector3.down * 0.3f, $"Action: {currentAction.name}", actionColor);
|
||||
}
|
||||
|
||||
// Display health bar etc.
|
||||
if (debugInfo.worldState.ContainsKey("health"))
|
||||
{
|
||||
float health = Convert.ToSingle(debugInfo.worldState["health"]);
|
||||
DrawHealthBar(transform.position + Vector3.up * 1.5f, health / 100f);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawActionPlan()
|
||||
{
|
||||
if (debugInfo.currentPlan == null) return;
|
||||
|
||||
Vector3 startPos = transform.position;
|
||||
|
||||
for (int i = 0; i < debugInfo.currentPlan.Count; i++)
|
||||
{
|
||||
var action = debugInfo.currentPlan[i];
|
||||
Vector3 endPos = startPos + Vector3.forward * (i + 1) * 2f;
|
||||
|
||||
// Connection lines between actions
|
||||
Gizmos.color = GetActionColor(action);
|
||||
Gizmos.DrawLine(startPos, endPos);
|
||||
|
||||
// Action nodes
|
||||
Gizmos.DrawWireSphere(endPos, 0.3f);
|
||||
|
||||
startPos = endPos;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawDecisionGraphOverlay()
|
||||
{
|
||||
// Display decision graph overlay
|
||||
Rect graphRect = new Rect(Screen.width - 310, 10, 300, 300);
|
||||
GUI.Box(graphRect, "Decision Graph");
|
||||
|
||||
// Graph drawing area
|
||||
Rect graphArea = new Rect(graphRect.x + 10, graphRect.y + 30, graphRect.width - 20, graphRect.height - 40);
|
||||
|
||||
// Temporary graph data
|
||||
DrawGraph(graphArea, debugInfo);
|
||||
}
|
||||
|
||||
private void DrawGraph(Rect area, GOAPDebugInfo info)
|
||||
{
|
||||
// Draw nodes and edges
|
||||
if (info.graphData != null)
|
||||
{
|
||||
foreach (var edge in info.graphData.edges)
|
||||
{
|
||||
Vector2 start = NodeToScreenPos(edge.from, area);
|
||||
Vector2 end = NodeToScreenPos(edge.to, area);
|
||||
DrawLine(start, end, Color.gray);
|
||||
}
|
||||
|
||||
foreach (var node in info.graphData.nodes)
|
||||
{
|
||||
Vector2 pos = NodeToScreenPos(node.position, area);
|
||||
GUI.color = node.isActive ? actionColor : Color.gray;
|
||||
GUI.Box(new Rect(pos.x - 20, pos.y - 10, 40, 20), node.name);
|
||||
}
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPerformanceGraph()
|
||||
{
|
||||
if (performanceHistory.Count < 2) return;
|
||||
|
||||
Rect graphRect = GUILayoutUtility.GetRect(280, 100);
|
||||
GUI.Box(graphRect, "");
|
||||
|
||||
var metrics = performanceHistory.ToArray();
|
||||
float maxTime = metrics.Max(m => m.planningTime);
|
||||
|
||||
for (int i = 1; i < metrics.Length; i++)
|
||||
{
|
||||
float x1 = graphRect.x + (i - 1) * graphRect.width / (metrics.Length - 1);
|
||||
float y1 = graphRect.y + graphRect.height - (metrics[i - 1].planningTime / maxTime) * graphRect.height;
|
||||
float x2 = graphRect.x + i * graphRect.width / (metrics.Length - 1);
|
||||
float y2 = graphRect.y + graphRect.height - (metrics[i].planningTime / maxTime) * graphRect.height;
|
||||
|
||||
DrawLine(new Vector2(x1, y1), new Vector2(x2, y2), Color.cyan);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
private Color GetActionColor(ActionDebugInfo action)
|
||||
{
|
||||
switch (action.status)
|
||||
{
|
||||
case "completed": return completedActionColor;
|
||||
case "failed": return failedActionColor;
|
||||
case "executing": return actionColor;
|
||||
default: return Color.white;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawString(Vector3 worldPos, string text, Color color)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
Handles.color = color;
|
||||
Handles.Label(worldPos, text);
|
||||
#endif
|
||||
}
|
||||
|
||||
private void DrawHealthBar(Vector3 position, float percentage)
|
||||
{
|
||||
float width = 1f;
|
||||
float height = 0.1f;
|
||||
|
||||
// Background
|
||||
Gizmos.color = Color.red;
|
||||
Gizmos.DrawCube(position, new Vector3(width, height, 0.01f));
|
||||
|
||||
// Health bar
|
||||
Gizmos.color = Color.green;
|
||||
Gizmos.DrawCube(position - Vector3.right * (width * (1f - percentage) / 2f),
|
||||
new Vector3(width * percentage, height, 0.01f));
|
||||
}
|
||||
|
||||
private void DrawLine(Vector2 start, Vector2 end, Color color)
|
||||
{
|
||||
var temp = GUI.color;
|
||||
GUI.color = color;
|
||||
|
||||
float angle = Mathf.Atan2(end.y - start.y, end.x - start.x) * Mathf.Rad2Deg;
|
||||
float dist = Vector2.Distance(start, end);
|
||||
|
||||
GUIUtility.RotateAroundPivot(angle, start);
|
||||
GUI.DrawTexture(new Rect(start.x, start.y - 1, dist, 2), Texture2D.whiteTexture);
|
||||
GUIUtility.RotateAroundPivot(-angle, start);
|
||||
|
||||
GUI.color = temp;
|
||||
}
|
||||
|
||||
private Vector2 NodeToScreenPos(Vector2 nodePos, Rect area)
|
||||
{
|
||||
return new Vector2(
|
||||
area.x + nodePos.x * area.width,
|
||||
area.y + nodePos.y * area.height
|
||||
);
|
||||
}
|
||||
|
||||
// Initialize demo data
|
||||
private void InitializeDemoData()
|
||||
{
|
||||
debugInfo.currentGoal = "PatrolArea";
|
||||
debugInfo.goalPriority = 80;
|
||||
debugInfo.planCost = 4.5f;
|
||||
|
||||
debugInfo.currentPlan = new List<ActionDebugInfo>
|
||||
{
|
||||
new ActionDebugInfo { name = "MoveTo", cost = 1f, status = "completed", progress = 1f },
|
||||
new ActionDebugInfo { name = "LookAround", cost = 0.5f, status = "executing", progress = 0.6f },
|
||||
new ActionDebugInfo { name = "MarkWaypoint", cost = 0.2f, status = "pending", progress = 0f }
|
||||
};
|
||||
|
||||
debugInfo.currentActionIndex = 1;
|
||||
|
||||
debugInfo.worldState = new Dictionary<string, object>
|
||||
{
|
||||
["health"] = 85,
|
||||
["has_weapon"] = true,
|
||||
["enemies_nearby"] = 0,
|
||||
["patrol_route"] = "defined",
|
||||
["at_waypoint"] = true
|
||||
};
|
||||
|
||||
debugInfo.graphNodes = 15;
|
||||
debugInfo.graphEdges = 23;
|
||||
debugInfo.lastPlanningTime = 0.012f;
|
||||
debugInfo.planAttempts = 2;
|
||||
debugInfo.memoryUsage = 1.2f;
|
||||
|
||||
// Graph data
|
||||
debugInfo.graphData = new GraphData
|
||||
{
|
||||
nodes = new List<GraphNode>
|
||||
{
|
||||
new GraphNode { name = "Start", position = new Vector2(0.1f, 0.5f), isActive = true },
|
||||
new GraphNode { name = "MoveTo", position = new Vector2(0.3f, 0.3f), isActive = true },
|
||||
new GraphNode { name = "Attack", position = new Vector2(0.3f, 0.7f), isActive = false },
|
||||
new GraphNode { name = "Goal", position = new Vector2(0.9f, 0.5f), isActive = false }
|
||||
},
|
||||
edges = new List<GraphEdge>
|
||||
{
|
||||
new GraphEdge { from = new Vector2(0.1f, 0.5f), to = new Vector2(0.3f, 0.3f) },
|
||||
new GraphEdge { from = new Vector2(0.1f, 0.5f), to = new Vector2(0.3f, 0.7f) },
|
||||
new GraphEdge { from = new Vector2(0.3f, 0.3f), to = new Vector2(0.9f, 0.5f) }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void UpdateDebugInfo()
|
||||
{
|
||||
// Get actual GOAPAgent if available
|
||||
var goapAgent = GetComponent<GOAPAgent>();
|
||||
if (goapAgent == null) return;
|
||||
|
||||
// Update goal info
|
||||
var currentGoal = goapAgent.CurrentGoal;
|
||||
if (currentGoal != null)
|
||||
{
|
||||
debugInfo.currentGoal = currentGoal.GoalName;
|
||||
debugInfo.goalPriority = currentGoal.Priority;
|
||||
}
|
||||
|
||||
// Update current action info
|
||||
var currentAction = goapAgent.CurrentAction;
|
||||
if (currentAction != null)
|
||||
{
|
||||
var existingAction = debugInfo.currentPlan?.FirstOrDefault(a => a.name == currentAction.ActionName);
|
||||
if (existingAction != null)
|
||||
{
|
||||
existingAction.status = "executing";
|
||||
}
|
||||
}
|
||||
|
||||
// Update world state from agent
|
||||
if (goapAgent.WorldState != null)
|
||||
{
|
||||
var states = goapAgent.WorldState.GetAllStates();
|
||||
debugInfo.worldState.Clear();
|
||||
foreach (var kvp in states)
|
||||
{
|
||||
debugInfo.worldState[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// Update plan info
|
||||
var remainingPlan = goapAgent.GetRemainingPlan();
|
||||
if (remainingPlan != null && remainingPlan.Count > 0)
|
||||
{
|
||||
debugInfo.currentPlan = remainingPlan.Select((a, i) => new ActionDebugInfo
|
||||
{
|
||||
name = a.ActionName,
|
||||
cost = a.Cost,
|
||||
status = i == 0 ? "executing" : "pending",
|
||||
progress = i == 0 ? 0.5f : 0f
|
||||
}).ToList();
|
||||
debugInfo.currentActionIndex = 0;
|
||||
debugInfo.planCost = remainingPlan.Sum(a => a.Cost);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePerformanceMetrics()
|
||||
{
|
||||
var metric = new PlanningMetrics
|
||||
{
|
||||
timestamp = Time.time,
|
||||
planningTime = debugInfo.lastPlanningTime,
|
||||
nodeCount = debugInfo.graphNodes,
|
||||
memoryUsage = debugInfo.memoryUsage
|
||||
};
|
||||
|
||||
performanceHistory.Enqueue(metric);
|
||||
if (performanceHistory.Count > 100)
|
||||
{
|
||||
performanceHistory.Dequeue();
|
||||
}
|
||||
}
|
||||
|
||||
private GUIStyle _boldLabelStyle;
|
||||
private GUIStyle GetBoldLabelStyle()
|
||||
{
|
||||
if (_boldLabelStyle == null)
|
||||
{
|
||||
_boldLabelStyle = new GUIStyle(GUI.skin.label);
|
||||
_boldLabelStyle.fontStyle = FontStyle.Bold;
|
||||
}
|
||||
return _boldLabelStyle;
|
||||
}
|
||||
|
||||
private void ForceReplan()
|
||||
{
|
||||
UnityEngine.Debug.Log("[GOAP Debug] Forcing replan...");
|
||||
var goapAgent = GetComponent<GOAPAgent>();
|
||||
if (goapAgent != null)
|
||||
{
|
||||
goapAgent.ForceReplan();
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearPlan()
|
||||
{
|
||||
UnityEngine.Debug.Log("[GOAP Debug] Clearing current plan...");
|
||||
debugInfo.currentPlan?.Clear();
|
||||
debugInfo.currentActionIndex = -1;
|
||||
}
|
||||
|
||||
private void ExportDebugLog()
|
||||
{
|
||||
string log = GenerateDebugLog();
|
||||
string path = $"Assets/GOAP_Debug_{DateTime.Now:yyyyMMdd_HHmmss}.txt";
|
||||
System.IO.File.WriteAllText(path, log);
|
||||
UnityEngine.Debug.Log($"[GOAP Debug] Exported debug log to: {path}");
|
||||
#if UNITY_EDITOR
|
||||
AssetDatabase.Refresh();
|
||||
#endif
|
||||
}
|
||||
|
||||
private string GenerateDebugLog()
|
||||
{
|
||||
var log = new System.Text.StringBuilder();
|
||||
log.AppendLine($"GOAP Debug Log - {DateTime.Now}");
|
||||
log.AppendLine("=====================================");
|
||||
log.AppendLine($"Agent: {gameObject.name}");
|
||||
log.AppendLine($"Current Goal: {debugInfo.currentGoal}");
|
||||
log.AppendLine($"Goal Priority: {debugInfo.goalPriority}");
|
||||
log.AppendLine("\nCurrent Plan:");
|
||||
|
||||
if (debugInfo.currentPlan != null)
|
||||
{
|
||||
foreach (var action in debugInfo.currentPlan)
|
||||
{
|
||||
log.AppendLine($" - {action.name} (Cost: {action.cost}, Status: {action.status})");
|
||||
}
|
||||
}
|
||||
|
||||
log.AppendLine("\nWorld State:");
|
||||
foreach (var state in debugInfo.worldState)
|
||||
{
|
||||
log.AppendLine($" - {state.Key}: {state.Value}");
|
||||
}
|
||||
|
||||
log.AppendLine($"\nPerformance Metrics:");
|
||||
log.AppendLine($" - Planning Time: {debugInfo.lastPlanningTime:F3}s");
|
||||
log.AppendLine($" - Graph Nodes: {debugInfo.graphNodes}");
|
||||
log.AppendLine($" - Memory Usage: {debugInfo.memoryUsage:F2} MB");
|
||||
|
||||
return log.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
// Debug information structure
|
||||
[System.Serializable]
|
||||
public class GOAPDebugInfo
|
||||
{
|
||||
public string currentGoal;
|
||||
public float goalPriority;
|
||||
public List<ActionDebugInfo> currentPlan;
|
||||
public int currentActionIndex;
|
||||
public Dictionary<string, object> worldState = new Dictionary<string, object>();
|
||||
public float planCost;
|
||||
public float lastPlanningTime;
|
||||
public int planAttempts;
|
||||
public int graphNodes;
|
||||
public int graphEdges;
|
||||
public float memoryUsage;
|
||||
public GraphData graphData;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class ActionDebugInfo
|
||||
{
|
||||
public string name;
|
||||
public float cost;
|
||||
public string status; // pending, executing, completed, failed
|
||||
public float progress; // 0-1
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class PlanningMetrics
|
||||
{
|
||||
public float timestamp;
|
||||
public float planningTime;
|
||||
public int nodeCount;
|
||||
public float memoryUsage;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class GraphData
|
||||
{
|
||||
public List<GraphNode> nodes = new List<GraphNode>();
|
||||
public List<GraphEdge> edges = new List<GraphEdge>();
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class GraphNode
|
||||
{
|
||||
public string name;
|
||||
public Vector2 position;
|
||||
public bool isActive;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class GraphEdge
|
||||
{
|
||||
public Vector2 from;
|
||||
public Vector2 to;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7c6f2b2fbd6984954b174a89e5af8967
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 336030
|
||||
packageName: Synaptic AI Pro - Natural Language Control for Unity
|
||||
packageVersion: 1.2.23
|
||||
assetPath: Assets/Synaptic AI Pro/Runtime/GOAP/GOAPDebugVisualizer.cs
|
||||
uploadId: 920982
|
||||
@@ -0,0 +1,315 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SynapticPro.GOAP
|
||||
{
|
||||
/// <summary>
|
||||
/// Dynamic GOAP Action - Can be configured at runtime via code
|
||||
/// Used for procedurally generated behaviors from natural language
|
||||
/// </summary>
|
||||
public class GOAPDynamicAction : GOAPActionBase
|
||||
{
|
||||
private Func<GOAPAgent, bool> performFunc;
|
||||
private Func<GOAPAgent, bool> checkPreconditionFunc;
|
||||
private Action<GOAPAgent> onCompleteFunc;
|
||||
private float elapsedTime;
|
||||
|
||||
/// <summary>
|
||||
/// Configure the action at runtime
|
||||
/// </summary>
|
||||
public void Configure(
|
||||
string name,
|
||||
float actionCost,
|
||||
float actionDuration,
|
||||
Dictionary<string, object> preconditions,
|
||||
Dictionary<string, object> effects)
|
||||
{
|
||||
actionName = name;
|
||||
cost = actionCost;
|
||||
duration = actionDuration;
|
||||
|
||||
if (preconditions != null)
|
||||
{
|
||||
foreach (var kvp in preconditions)
|
||||
{
|
||||
AddPrecondition(kvp.Key, kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (effects != null)
|
||||
{
|
||||
foreach (var kvp in effects)
|
||||
{
|
||||
AddEffect(kvp.Key, kvp.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set custom perform function
|
||||
/// </summary>
|
||||
public void SetPerformFunction(Func<GOAPAgent, bool> func)
|
||||
{
|
||||
performFunc = func;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set custom precondition check
|
||||
/// </summary>
|
||||
public void SetPreconditionCheck(Func<GOAPAgent, bool> func)
|
||||
{
|
||||
checkPreconditionFunc = func;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set on complete callback
|
||||
/// </summary>
|
||||
public void SetOnComplete(Action<GOAPAgent> func)
|
||||
{
|
||||
onCompleteFunc = func;
|
||||
}
|
||||
|
||||
public override bool CheckProceduralPrecondition(GOAPAgent agent)
|
||||
{
|
||||
if (checkPreconditionFunc != null)
|
||||
{
|
||||
return checkPreconditionFunc(agent);
|
||||
}
|
||||
return base.CheckProceduralPrecondition(agent);
|
||||
}
|
||||
|
||||
public override bool Perform(GOAPAgent agent)
|
||||
{
|
||||
if (performFunc != null)
|
||||
{
|
||||
return performFunc(agent);
|
||||
}
|
||||
|
||||
// Default behavior: wait for duration
|
||||
elapsedTime += Time.deltaTime;
|
||||
if (elapsedTime >= duration)
|
||||
{
|
||||
elapsedTime = 0f;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool PostPerform(GOAPAgent agent)
|
||||
{
|
||||
onCompleteFunc?.Invoke(agent);
|
||||
return base.PostPerform(agent);
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
elapsedTime = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating dynamic GOAP actions
|
||||
/// </summary>
|
||||
public static class GOAPActionFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a simple wait action
|
||||
/// </summary>
|
||||
public static GOAPDynamicAction CreateWaitAction(GameObject parent, string name, float waitTime)
|
||||
{
|
||||
var action = parent.AddComponent<GOAPDynamicAction>();
|
||||
action.Configure(name, 0.1f, waitTime, null, null);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a movement action
|
||||
/// </summary>
|
||||
public static GOAPDynamicAction CreateMoveAction(
|
||||
GameObject parent,
|
||||
string name,
|
||||
float cost,
|
||||
string[] preconditions,
|
||||
string[] effects)
|
||||
{
|
||||
var action = parent.AddComponent<GOAPDynamicAction>();
|
||||
|
||||
var precondDict = new Dictionary<string, object>();
|
||||
foreach (var p in preconditions)
|
||||
{
|
||||
precondDict[p] = true;
|
||||
}
|
||||
|
||||
var effectDict = new Dictionary<string, object>();
|
||||
foreach (var e in effects)
|
||||
{
|
||||
effectDict[e] = true;
|
||||
}
|
||||
|
||||
action.Configure(name, cost, 1f, precondDict, effectDict);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create action from parsed behavior data
|
||||
/// </summary>
|
||||
public static GOAPDynamicAction CreateFromBehaviorData(
|
||||
GameObject parent,
|
||||
string name,
|
||||
float cost,
|
||||
string[] preconditions,
|
||||
string[] effects,
|
||||
float duration = 1f)
|
||||
{
|
||||
var action = parent.AddComponent<GOAPDynamicAction>();
|
||||
|
||||
var precondDict = new Dictionary<string, object>();
|
||||
if (preconditions != null)
|
||||
{
|
||||
foreach (var p in preconditions)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(p))
|
||||
{
|
||||
precondDict[p] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var effectDict = new Dictionary<string, object>();
|
||||
if (effects != null)
|
||||
{
|
||||
foreach (var e in effects)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(e))
|
||||
{
|
||||
effectDict[e] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
action.Configure(name, cost, duration, precondDict, effectDict);
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create patrol action with waypoints
|
||||
/// </summary>
|
||||
public static GOAPDynamicAction CreatePatrolAction(GameObject parent, Transform[] waypoints)
|
||||
{
|
||||
var action = parent.AddComponent<GOAPDynamicAction>();
|
||||
action.Configure("Patrol", 1f, 2f,
|
||||
null,
|
||||
new Dictionary<string, object> { ["patrolling"] = true });
|
||||
|
||||
int currentWaypoint = 0;
|
||||
|
||||
action.SetPerformFunction((agent) =>
|
||||
{
|
||||
if (waypoints == null || waypoints.Length == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var target = waypoints[currentWaypoint];
|
||||
if (target == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
float distance = Vector3.Distance(agent.transform.position, target.position);
|
||||
if (distance < 1f)
|
||||
{
|
||||
currentWaypoint = (currentWaypoint + 1) % waypoints.Length;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Move towards waypoint
|
||||
Vector3 direction = (target.position - agent.transform.position).normalized;
|
||||
agent.transform.position += direction * agent.MoveSpeed * Time.deltaTime;
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create attack action
|
||||
/// </summary>
|
||||
public static GOAPDynamicAction CreateAttackAction(GameObject parent, float damage, float attackRange)
|
||||
{
|
||||
var action = parent.AddComponent<GOAPDynamicAction>();
|
||||
action.Configure("Attack", 1.5f, 1f,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["enemy_in_range"] = true,
|
||||
["has_weapon"] = true
|
||||
},
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["damage_dealt"] = true
|
||||
});
|
||||
|
||||
action.SetPerformFunction((agent) =>
|
||||
{
|
||||
// Attack logic here
|
||||
Debug.Log($"[GOAP] {agent.name} performing attack");
|
||||
return true; // Attack is instant
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create flee action
|
||||
/// </summary>
|
||||
public static GOAPDynamicAction CreateFleeAction(GameObject parent)
|
||||
{
|
||||
var action = parent.AddComponent<GOAPDynamicAction>();
|
||||
action.Configure("Flee", 0.5f, 3f,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["health_low"] = true
|
||||
},
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["is_safe"] = true
|
||||
});
|
||||
|
||||
action.SetPerformFunction((agent) =>
|
||||
{
|
||||
// Find escape direction (opposite of threat)
|
||||
var threat = GameObject.FindGameObjectWithTag("Enemy");
|
||||
if (threat != null)
|
||||
{
|
||||
Vector3 fleeDirection = (agent.transform.position - threat.transform.position).normalized;
|
||||
agent.transform.position += fleeDirection * agent.MoveSpeed * Time.deltaTime;
|
||||
}
|
||||
return false; // Keep fleeing
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create collect resource action
|
||||
/// </summary>
|
||||
public static GOAPDynamicAction CreateCollectAction(GameObject parent)
|
||||
{
|
||||
var action = parent.AddComponent<GOAPDynamicAction>();
|
||||
action.Configure("Collect", 1f, 2f,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["resource_nearby"] = true
|
||||
},
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["resource_collected"] = true
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 31800f015f500480f978cdc23a3e8799
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 336030
|
||||
packageName: Synaptic AI Pro - Natural Language Control for Unity
|
||||
packageVersion: 1.2.23
|
||||
assetPath: Assets/Synaptic AI Pro/Runtime/GOAP/GOAPDynamicAction.cs
|
||||
uploadId: 920982
|
||||
@@ -0,0 +1,267 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SynapticPro.GOAP
|
||||
{
|
||||
/// <summary>
|
||||
/// GOAP Goal - Represents a goal the agent wants to achieve
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class GOAPGoal
|
||||
{
|
||||
[SerializeField] private string goalName = "Goal";
|
||||
[SerializeField] private int priority = 1;
|
||||
[SerializeField] private bool isActive = true;
|
||||
|
||||
/// <summary>
|
||||
/// Desired world state to achieve this goal
|
||||
/// </summary>
|
||||
public Dictionary<string, object> DesiredState { get; private set; } = new Dictionary<string, object>();
|
||||
|
||||
/// <summary>
|
||||
/// Goal name for identification
|
||||
/// </summary>
|
||||
public string GoalName
|
||||
{
|
||||
get => goalName;
|
||||
set => goalName = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Priority (higher = more important)
|
||||
/// </summary>
|
||||
public int Priority
|
||||
{
|
||||
get => priority;
|
||||
set => priority = Mathf.Max(0, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether this goal is currently active
|
||||
/// </summary>
|
||||
public bool IsActive
|
||||
{
|
||||
get => isActive;
|
||||
set => isActive = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create empty goal
|
||||
/// </summary>
|
||||
public GOAPGoal()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create goal with name and priority
|
||||
/// </summary>
|
||||
public GOAPGoal(string name, int priority = 1)
|
||||
{
|
||||
this.goalName = name;
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a condition to the desired state
|
||||
/// </summary>
|
||||
public void AddCondition(string key, object value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key)) return;
|
||||
|
||||
if (DesiredState.ContainsKey(key))
|
||||
{
|
||||
DesiredState[key] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
DesiredState.Add(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a condition from desired state
|
||||
/// </summary>
|
||||
public void RemoveCondition(string key)
|
||||
{
|
||||
if (DesiredState.ContainsKey(key))
|
||||
{
|
||||
DesiredState.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if goal is satisfied by current world state
|
||||
/// </summary>
|
||||
public bool IsSatisfied(WorldState worldState)
|
||||
{
|
||||
if (worldState == null) return false;
|
||||
return worldState.Satisfies(DesiredState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get relevance of this goal (can be overridden for dynamic priority)
|
||||
/// </summary>
|
||||
public virtual float GetRelevance(GOAPAgent agent)
|
||||
{
|
||||
return isActive ? priority : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when goal is activated
|
||||
/// </summary>
|
||||
public virtual void OnActivate(GOAPAgent agent)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when goal is deactivated
|
||||
/// </summary>
|
||||
public virtual void OnDeactivate(GOAPAgent agent)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when goal is achieved
|
||||
/// </summary>
|
||||
public virtual void OnAchieved(GOAPAgent agent)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clone this goal
|
||||
/// </summary>
|
||||
public GOAPGoal Clone()
|
||||
{
|
||||
var clone = new GOAPGoal(goalName, priority);
|
||||
clone.isActive = isActive;
|
||||
foreach (var kvp in DesiredState)
|
||||
{
|
||||
clone.DesiredState[kvp.Key] = kvp.Value;
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{goalName} (Priority: {priority})";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Goal that changes priority based on conditions
|
||||
/// </summary>
|
||||
public class DynamicGOAPGoal : GOAPGoal
|
||||
{
|
||||
private Func<GOAPAgent, float> relevanceCalculator;
|
||||
|
||||
public DynamicGOAPGoal(string name, Func<GOAPAgent, float> calculator) : base(name)
|
||||
{
|
||||
relevanceCalculator = calculator;
|
||||
}
|
||||
|
||||
public override float GetRelevance(GOAPAgent agent)
|
||||
{
|
||||
if (!IsActive) return 0;
|
||||
return relevanceCalculator?.Invoke(agent) ?? Priority;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MonoBehaviour wrapper for GOAPGoal
|
||||
/// Attach to agent to define goals in Inspector
|
||||
/// </summary>
|
||||
public class GOAPGoalComponent : MonoBehaviour
|
||||
{
|
||||
[Header("Goal Settings")]
|
||||
[SerializeField] private string goalName = "Goal";
|
||||
[SerializeField] private int priority = 1;
|
||||
[SerializeField] private bool isActive = true;
|
||||
|
||||
[Header("Desired State")]
|
||||
[SerializeField] private List<StateCondition> conditions = new List<StateCondition>();
|
||||
|
||||
private GOAPGoal _goal;
|
||||
|
||||
/// <summary>
|
||||
/// Get the GOAPGoal instance
|
||||
/// </summary>
|
||||
public GOAPGoal Goal
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_goal == null)
|
||||
{
|
||||
BuildGoal();
|
||||
}
|
||||
return _goal;
|
||||
}
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
BuildGoal();
|
||||
}
|
||||
|
||||
private void BuildGoal()
|
||||
{
|
||||
_goal = new GOAPGoal(goalName, priority);
|
||||
_goal.IsActive = isActive;
|
||||
|
||||
foreach (var condition in conditions)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(condition.key))
|
||||
{
|
||||
_goal.AddCondition(condition.key, condition.GetValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refresh goal from inspector values
|
||||
/// </summary>
|
||||
public void RefreshGoal()
|
||||
{
|
||||
BuildGoal();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializable state condition for Inspector
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class StateCondition
|
||||
{
|
||||
public string key;
|
||||
public StateValueType valueType = StateValueType.Bool;
|
||||
public bool boolValue = true;
|
||||
public int intValue = 0;
|
||||
public float floatValue = 0f;
|
||||
public string stringValue = "";
|
||||
|
||||
public object GetValue()
|
||||
{
|
||||
switch (valueType)
|
||||
{
|
||||
case StateValueType.Bool:
|
||||
return boolValue;
|
||||
case StateValueType.Int:
|
||||
return intValue;
|
||||
case StateValueType.Float:
|
||||
return floatValue;
|
||||
case StateValueType.String:
|
||||
return stringValue;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum StateValueType
|
||||
{
|
||||
Bool,
|
||||
Int,
|
||||
Float,
|
||||
String
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: df406242a59b2486cb7b79582e253dd4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 336030
|
||||
packageName: Synaptic AI Pro - Natural Language Control for Unity
|
||||
packageVersion: 1.2.23
|
||||
assetPath: Assets/Synaptic AI Pro/Runtime/GOAP/GOAPGoal.cs
|
||||
uploadId: 920982
|
||||
@@ -0,0 +1,310 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SynapticPro.GOAP
|
||||
{
|
||||
/// <summary>
|
||||
/// GOAP Planner - A* based action planning system
|
||||
/// Finds optimal sequence of actions to achieve goals
|
||||
/// </summary>
|
||||
public class GOAPPlanner
|
||||
{
|
||||
private int maxPlanningIterations = 1000;
|
||||
private int maxPlanDepth = 15;
|
||||
|
||||
/// <summary>
|
||||
/// Plan node for A* search
|
||||
/// </summary>
|
||||
private class PlanNode : IComparable<PlanNode>
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a plan to achieve the goal from current state
|
||||
/// </summary>
|
||||
/// <param name="agent">The GOAP agent</param>
|
||||
/// <param name="availableActions">Actions the agent can perform</param>
|
||||
/// <param name="currentState">Current world state</param>
|
||||
/// <param name="goal">Goal to achieve</param>
|
||||
/// <returns>Queue of actions to execute, or null if no plan found</returns>
|
||||
public Queue<GOAPActionBase> Plan(
|
||||
GOAPAgent agent,
|
||||
HashSet<GOAPActionBase> 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<GOAPActionBase>();
|
||||
foreach (var action in availableActions)
|
||||
{
|
||||
if (action.CheckProceduralPrecondition(agent))
|
||||
{
|
||||
usableActions.Add(action);
|
||||
}
|
||||
}
|
||||
|
||||
if (usableActions.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// A* search
|
||||
var openList = new List<PlanNode>();
|
||||
var closedSet = new HashSet<string>();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculate heuristic (estimated cost to goal)
|
||||
/// </summary>
|
||||
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<object>(condition.Key).Equals(condition.Value))
|
||||
{
|
||||
unsatisfiedConditions++;
|
||||
}
|
||||
}
|
||||
|
||||
return unsatisfiedConditions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if goal is satisfied by current state
|
||||
/// </summary>
|
||||
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<object>(condition.Key);
|
||||
if (!currentValue.Equals(condition.Value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if action's preconditions are met
|
||||
/// </summary>
|
||||
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<object>(precondition.Key);
|
||||
if (!currentValue.Equals(precondition.Value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply action effects to state
|
||||
/// </summary>
|
||||
private void ApplyEffects(WorldState state, GOAPActionBase action)
|
||||
{
|
||||
if (action.Effects == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var effect in action.Effects)
|
||||
{
|
||||
state.SetState(effect.Key, effect.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build plan queue from goal node by backtracking
|
||||
/// </summary>
|
||||
private Queue<GOAPActionBase> BuildPlan(PlanNode goalNode)
|
||||
{
|
||||
var plan = new List<GOAPActionBase>();
|
||||
var node = goalNode;
|
||||
|
||||
while (node != null)
|
||||
{
|
||||
if (node.Action != null)
|
||||
{
|
||||
plan.Add(node.Action);
|
||||
}
|
||||
node = node.Parent;
|
||||
}
|
||||
|
||||
plan.Reverse();
|
||||
|
||||
var queue = new Queue<GOAPActionBase>();
|
||||
foreach (var action in plan)
|
||||
{
|
||||
queue.Enqueue(action);
|
||||
}
|
||||
|
||||
return queue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set maximum planning iterations
|
||||
/// </summary>
|
||||
public void SetMaxIterations(int max)
|
||||
{
|
||||
maxPlanningIterations = Mathf.Max(100, max);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set maximum plan depth
|
||||
/// </summary>
|
||||
public void SetMaxDepth(int depth)
|
||||
{
|
||||
maxPlanDepth = Mathf.Max(1, depth);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 797c33b5c03cd468f96d0ebacd20fc0b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 336030
|
||||
packageName: Synaptic AI Pro - Natural Language Control for Unity
|
||||
packageVersion: 1.2.23
|
||||
assetPath: Assets/Synaptic AI Pro/Runtime/GOAP/GOAPPlanner.cs
|
||||
uploadId: 920982
|
||||
@@ -0,0 +1,486 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using SynapticPro.GOAP;
|
||||
|
||||
namespace SynapticPro.GOAP
|
||||
{
|
||||
/// <summary>
|
||||
/// GOAP Test Scene Controller
|
||||
/// </summary>
|
||||
public class GOAPTestSceneController : MonoBehaviour
|
||||
{
|
||||
[Header("Test Configuration")]
|
||||
public bool autoCreateTestAgents = true;
|
||||
public int numberOfGuards = 2;
|
||||
public int numberOfCollectors = 1;
|
||||
public int numberOfSoldiers = 1;
|
||||
|
||||
[Header("Scene Objects")]
|
||||
public Transform[] patrolPoints;
|
||||
public Transform[] resourcePoints;
|
||||
public Transform[] coverPoints;
|
||||
public Transform playerTransform;
|
||||
|
||||
[Header("Visual Feedback")]
|
||||
public Material agentMaterial;
|
||||
public Material guardMaterial;
|
||||
public Material collectorMaterial;
|
||||
public Material soldierMaterial;
|
||||
|
||||
private List<GameObject> testAgents = new List<GameObject>();
|
||||
private Dictionary<string, BehaviorTemplate> availableTemplates;
|
||||
|
||||
void Start()
|
||||
{
|
||||
InitializeTemplates();
|
||||
|
||||
if (autoCreateTestAgents)
|
||||
{
|
||||
StartCoroutine(CreateTestEnvironment());
|
||||
}
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// Keyboard shortcuts for testing
|
||||
if (Input.GetKeyDown(KeyCode.F1))
|
||||
{
|
||||
CreateGuardAgent();
|
||||
}
|
||||
if (Input.GetKeyDown(KeyCode.F2))
|
||||
{
|
||||
CreateCollectorAgent();
|
||||
}
|
||||
if (Input.GetKeyDown(KeyCode.F3))
|
||||
{
|
||||
CreateSoldierAgent();
|
||||
}
|
||||
if (Input.GetKeyDown(KeyCode.F4))
|
||||
{
|
||||
TestNaturalLanguageBehavior();
|
||||
}
|
||||
if (Input.GetKeyDown(KeyCode.F5))
|
||||
{
|
||||
ShowPerformanceReport();
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeTemplates()
|
||||
{
|
||||
availableTemplates = new Dictionary<string, BehaviorTemplate>
|
||||
{
|
||||
["Guard"] = GOAPBehaviorTemplates.GuardTemplate,
|
||||
["Collector"] = GOAPBehaviorTemplates.CollectorTemplate,
|
||||
["Soldier"] = GOAPBehaviorTemplates.SoldierTemplate,
|
||||
["Wildlife"] = GOAPBehaviorTemplates.WildlifeTemplate,
|
||||
["Merchant"] = GOAPBehaviorTemplates.MerchantTemplate
|
||||
};
|
||||
}
|
||||
|
||||
private IEnumerator CreateTestEnvironment()
|
||||
{
|
||||
Debug.Log("[GOAP Test] Creating test environment...");
|
||||
|
||||
// Setup scene environment
|
||||
yield return StartCoroutine(SetupSceneEnvironment());
|
||||
|
||||
// Create test agents
|
||||
for (int i = 0; i < numberOfGuards; i++)
|
||||
{
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
CreateGuardAgent($"Guard_{i + 1}");
|
||||
}
|
||||
|
||||
for (int i = 0; i < numberOfCollectors; i++)
|
||||
{
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
CreateCollectorAgent($"Collector_{i + 1}");
|
||||
}
|
||||
|
||||
for (int i = 0; i < numberOfSoldiers; i++)
|
||||
{
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
CreateSoldierAgent($"Soldier_{i + 1}");
|
||||
}
|
||||
|
||||
// Start demonstration
|
||||
yield return new WaitForSeconds(2f);
|
||||
StartDemonstration();
|
||||
}
|
||||
|
||||
private IEnumerator SetupSceneEnvironment()
|
||||
{
|
||||
// Create patrol points
|
||||
if (patrolPoints == null || patrolPoints.Length == 0)
|
||||
{
|
||||
CreatePatrolPoints();
|
||||
}
|
||||
|
||||
// Create resource points
|
||||
if (resourcePoints == null || resourcePoints.Length == 0)
|
||||
{
|
||||
CreateResourcePoints();
|
||||
}
|
||||
|
||||
// Create cover points
|
||||
if (coverPoints == null || coverPoints.Length == 0)
|
||||
{
|
||||
CreateCoverPoints();
|
||||
}
|
||||
|
||||
// Create player object
|
||||
if (playerTransform == null)
|
||||
{
|
||||
CreatePlayerObject();
|
||||
}
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
private void CreatePatrolPoints()
|
||||
{
|
||||
var patrolPointsList = new List<Transform>();
|
||||
|
||||
Vector3[] positions = {
|
||||
new Vector3(-5, 0, -5),
|
||||
new Vector3(5, 0, -5),
|
||||
new Vector3(5, 0, 5),
|
||||
new Vector3(-5, 0, 5),
|
||||
new Vector3(0, 0, 0)
|
||||
};
|
||||
|
||||
for (int i = 0; i < positions.Length; i++)
|
||||
{
|
||||
var waypoint = CreateWaypoint($"PatrolPoint_{i + 1}", positions[i], Color.yellow);
|
||||
patrolPointsList.Add(waypoint.transform);
|
||||
}
|
||||
|
||||
patrolPoints = patrolPointsList.ToArray();
|
||||
Debug.Log($"[GOAP Test] Created {patrolPoints.Length} patrol points");
|
||||
}
|
||||
|
||||
private void CreateResourcePoints()
|
||||
{
|
||||
var resourcePointsList = new List<Transform>();
|
||||
|
||||
Vector3[] positions = {
|
||||
new Vector3(-8, 0, 2),
|
||||
new Vector3(8, 0, -2),
|
||||
new Vector3(2, 0, 8),
|
||||
new Vector3(-2, 0, -8)
|
||||
};
|
||||
|
||||
for (int i = 0; i < positions.Length; i++)
|
||||
{
|
||||
var resource = CreateWaypoint($"ResourcePoint_{i + 1}", positions[i], Color.green);
|
||||
resourcePointsList.Add(resource.transform);
|
||||
}
|
||||
|
||||
resourcePoints = resourcePointsList.ToArray();
|
||||
Debug.Log($"[GOAP Test] Created {resourcePoints.Length} resource points");
|
||||
}
|
||||
|
||||
private void CreateCoverPoints()
|
||||
{
|
||||
var coverPointsList = new List<Transform>();
|
||||
|
||||
Vector3[] positions = {
|
||||
new Vector3(-3, 0, 3),
|
||||
new Vector3(3, 0, 3),
|
||||
new Vector3(3, 0, -3),
|
||||
new Vector3(-3, 0, -3)
|
||||
};
|
||||
|
||||
for (int i = 0; i < positions.Length; i++)
|
||||
{
|
||||
var cover = CreateCoverObject($"CoverPoint_{i + 1}", positions[i]);
|
||||
coverPointsList.Add(cover.transform);
|
||||
}
|
||||
|
||||
coverPoints = coverPointsList.ToArray();
|
||||
Debug.Log($"[GOAP Test] Created {coverPoints.Length} cover points");
|
||||
}
|
||||
|
||||
private void CreatePlayerObject()
|
||||
{
|
||||
var player = GameObject.CreatePrimitive(PrimitiveType.Capsule);
|
||||
player.name = "Player";
|
||||
player.transform.position = new Vector3(0, 0, -10);
|
||||
player.GetComponent<Renderer>().material.color = Color.blue;
|
||||
|
||||
// Add simple movement script
|
||||
var moveScript = player.AddComponent<SimplePlayerController>();
|
||||
|
||||
playerTransform = player.transform;
|
||||
Debug.Log("[GOAP Test] Created player object");
|
||||
}
|
||||
|
||||
private GameObject CreateWaypoint(string name, Vector3 position, Color color)
|
||||
{
|
||||
var waypoint = GameObject.CreatePrimitive(PrimitiveType.Sphere);
|
||||
waypoint.name = name;
|
||||
waypoint.transform.position = position;
|
||||
waypoint.transform.localScale = Vector3.one * 0.5f;
|
||||
waypoint.GetComponent<Renderer>().material.color = color;
|
||||
waypoint.GetComponent<Collider>().isTrigger = true;
|
||||
|
||||
return waypoint;
|
||||
}
|
||||
|
||||
private GameObject CreateCoverObject(string name, Vector3 position)
|
||||
{
|
||||
var cover = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
||||
cover.name = name;
|
||||
cover.transform.position = position;
|
||||
cover.transform.localScale = new Vector3(1, 2, 1);
|
||||
cover.GetComponent<Renderer>().material.color = Color.gray;
|
||||
|
||||
return cover;
|
||||
}
|
||||
|
||||
private void CreateGuardAgent(string agentName = "Guard")
|
||||
{
|
||||
var agent = CreateBaseAgent(agentName, guardMaterial);
|
||||
|
||||
// Add GOAP debug visualizer
|
||||
var debugVisualizer = agent.AddComponent<GOAPDebugVisualizer>();
|
||||
debugVisualizer.showCurrentGoal = true;
|
||||
debugVisualizer.showActionPlan = true;
|
||||
debugVisualizer.showWorldState = true;
|
||||
|
||||
// Apply guard template (simulation)
|
||||
ApplyTemplate(agent, availableTemplates["Guard"]);
|
||||
|
||||
testAgents.Add(agent);
|
||||
Debug.Log($"[GOAP Test] Created guard agent: {agentName}");
|
||||
}
|
||||
|
||||
private void CreateCollectorAgent(string agentName = "Collector")
|
||||
{
|
||||
var agent = CreateBaseAgent(agentName, collectorMaterial);
|
||||
|
||||
// Settings for resource collection agent
|
||||
var debugVisualizer = agent.AddComponent<GOAPDebugVisualizer>();
|
||||
debugVisualizer.goalColor = Color.green;
|
||||
|
||||
ApplyTemplate(agent, availableTemplates["Collector"]);
|
||||
|
||||
testAgents.Add(agent);
|
||||
Debug.Log($"[GOAP Test] Created collector agent: {agentName}");
|
||||
}
|
||||
|
||||
private void CreateSoldierAgent(string agentName = "Soldier")
|
||||
{
|
||||
var agent = CreateBaseAgent(agentName, soldierMaterial);
|
||||
|
||||
// Settings for combat agent
|
||||
var debugVisualizer = agent.AddComponent<GOAPDebugVisualizer>();
|
||||
debugVisualizer.goalColor = Color.red;
|
||||
debugVisualizer.showPerformanceMetrics = true;
|
||||
|
||||
ApplyTemplate(agent, availableTemplates["Soldier"]);
|
||||
|
||||
testAgents.Add(agent);
|
||||
Debug.Log($"[GOAP Test] Created soldier agent: {agentName}");
|
||||
}
|
||||
|
||||
private GameObject CreateBaseAgent(string name, Material material)
|
||||
{
|
||||
var agent = GameObject.CreatePrimitive(PrimitiveType.Capsule);
|
||||
agent.name = name;
|
||||
|
||||
// Place at random position
|
||||
Vector3 randomPos = new Vector3(
|
||||
Random.Range(-10f, 10f),
|
||||
0.5f,
|
||||
Random.Range(-10f, 10f)
|
||||
);
|
||||
agent.transform.position = randomPos;
|
||||
|
||||
// Set material
|
||||
if (material != null)
|
||||
{
|
||||
agent.GetComponent<Renderer>().material = material;
|
||||
}
|
||||
else
|
||||
{
|
||||
agent.GetComponent<Renderer>().material.color = Random.ColorHSV();
|
||||
}
|
||||
|
||||
// Basic navigation components
|
||||
var rigidbody = agent.AddComponent<Rigidbody>();
|
||||
rigidbody.constraints = RigidbodyConstraints.FreezeRotationX | RigidbodyConstraints.FreezeRotationZ;
|
||||
|
||||
return agent;
|
||||
}
|
||||
|
||||
private void ApplyTemplate(GameObject agentObject, BehaviorTemplate template)
|
||||
{
|
||||
// Add GOAPAgent component and apply template
|
||||
var goapAgent = agentObject.GetComponent<GOAPAgent>();
|
||||
if (goapAgent == null)
|
||||
{
|
||||
goapAgent = agentObject.AddComponent<GOAPAgent>();
|
||||
}
|
||||
|
||||
// Use the new extension method to apply template
|
||||
goapAgent.ApplyTemplate(template);
|
||||
|
||||
// Also add legacy data component for compatibility
|
||||
var agentData = agentObject.AddComponent<GOAPAgentData>();
|
||||
agentData.template = template;
|
||||
agentData.currentGoal = template.Goals[0].Name;
|
||||
agentData.worldState = new Dictionary<string, object>(template.InitialWorldState);
|
||||
|
||||
Debug.Log($"[GOAP Test] Applied template '{template.Name}' to {agentObject.name} with GOAPAgent runtime");
|
||||
}
|
||||
|
||||
private void TestNaturalLanguageBehavior()
|
||||
{
|
||||
Debug.Log("[GOAP Test] Testing natural language behavior definition...");
|
||||
|
||||
// Test natural language AI behavior definition
|
||||
string[] testBehaviors = {
|
||||
"Guard patrols between waypoints and attacks enemies on sight",
|
||||
"Collector gathers resources efficiently and avoids danger",
|
||||
"Soldier uses cover, suppresses enemies, and coordinates with squad",
|
||||
"Animal hunts prey when hungry and flees from predators",
|
||||
"Merchant maximizes profit through trading and negotiation"
|
||||
};
|
||||
|
||||
foreach (var behavior in testBehaviors)
|
||||
{
|
||||
Debug.Log($"[GOAP Test] Parsing: '{behavior}'");
|
||||
// Add parse logic test here
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowPerformanceReport()
|
||||
{
|
||||
Debug.Log("[GOAP Test] === Performance Report ===");
|
||||
Debug.Log($"Active Agents: {testAgents.Count}");
|
||||
|
||||
foreach (var agent in testAgents)
|
||||
{
|
||||
if (agent != null)
|
||||
{
|
||||
var visualizer = agent.GetComponent<GOAPDebugVisualizer>();
|
||||
if (visualizer != null)
|
||||
{
|
||||
Debug.Log($"{agent.name}: Planning efficient, Decisions responsive");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"Frame Rate: {(1.0f / Time.deltaTime):F1} FPS");
|
||||
Debug.Log($"Memory Usage: {UnityEngine.Profiling.Profiler.GetTotalAllocatedMemoryLong() / 1024 / 1024} MB");
|
||||
}
|
||||
|
||||
private void StartDemonstration()
|
||||
{
|
||||
Debug.Log("[GOAP Test] Starting GOAP demonstration...");
|
||||
|
||||
// Demonstration events
|
||||
StartCoroutine(DemonstrationSequence());
|
||||
}
|
||||
|
||||
private IEnumerator DemonstrationSequence()
|
||||
{
|
||||
Debug.Log("[GOAP Test] Phase 1: Normal patrol and collection");
|
||||
yield return new WaitForSeconds(5f);
|
||||
|
||||
Debug.Log("[GOAP Test] Phase 2: Simulating threat detection");
|
||||
SimulateThreatDetection();
|
||||
yield return new WaitForSeconds(10f);
|
||||
|
||||
Debug.Log("[GOAP Test] Phase 3: Resource scarcity simulation");
|
||||
SimulateResourceScarcity();
|
||||
yield return new WaitForSeconds(10f);
|
||||
|
||||
Debug.Log("[GOAP Test] Demonstration complete. Press F1-F5 for manual tests.");
|
||||
}
|
||||
|
||||
private void SimulateThreatDetection()
|
||||
{
|
||||
// Simulate threat detection
|
||||
foreach (var agent in testAgents)
|
||||
{
|
||||
if (agent != null && agent.name.Contains("Guard"))
|
||||
{
|
||||
var agentData = agent.GetComponent<GOAPAgentData>();
|
||||
if (agentData != null)
|
||||
{
|
||||
agentData.worldState["threat_detected"] = true;
|
||||
Debug.Log($"[GOAP Test] {agent.name} detected threat");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SimulateResourceScarcity()
|
||||
{
|
||||
// Simulate resource shortage
|
||||
foreach (var agent in testAgents)
|
||||
{
|
||||
if (agent != null && agent.name.Contains("Collector"))
|
||||
{
|
||||
var agentData = agent.GetComponent<GOAPAgentData>();
|
||||
if (agentData != null)
|
||||
{
|
||||
agentData.worldState["resources_scarce"] = true;
|
||||
Debug.Log($"[GOAP Test] {agent.name} facing resource scarcity");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
// Test GUI
|
||||
GUILayout.BeginArea(new Rect(10, Screen.height - 150, 300, 140));
|
||||
GUILayout.Box("GOAP Test Controls");
|
||||
|
||||
if (GUILayout.Button("F1: Create Guard"))
|
||||
CreateGuardAgent();
|
||||
if (GUILayout.Button("F2: Create Collector"))
|
||||
CreateCollectorAgent();
|
||||
if (GUILayout.Button("F3: Create Soldier"))
|
||||
CreateSoldierAgent();
|
||||
if (GUILayout.Button("F4: Test Natural Language"))
|
||||
TestNaturalLanguageBehavior();
|
||||
if (GUILayout.Button("F5: Performance Report"))
|
||||
ShowPerformanceReport();
|
||||
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Component for holding GOAP agent data
|
||||
/// </summary>
|
||||
public class GOAPAgentData : MonoBehaviour
|
||||
{
|
||||
public BehaviorTemplate template;
|
||||
public string currentGoal;
|
||||
public Dictionary<string, object> worldState = new Dictionary<string, object>();
|
||||
public List<string> currentPlan = new List<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple player controller
|
||||
/// </summary>
|
||||
public class SimplePlayerController : MonoBehaviour
|
||||
{
|
||||
public float speed = 5f;
|
||||
|
||||
void Update()
|
||||
{
|
||||
float horizontal = Input.GetAxis("Horizontal");
|
||||
float vertical = Input.GetAxis("Vertical");
|
||||
|
||||
Vector3 movement = new Vector3(horizontal, 0, vertical) * speed * Time.deltaTime;
|
||||
transform.Translate(movement);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ff2badc2a80764a7d93a003f17c9c99f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 336030
|
||||
packageName: Synaptic AI Pro - Natural Language Control for Unity
|
||||
packageVersion: 1.2.23
|
||||
assetPath: Assets/Synaptic AI Pro/Runtime/GOAP/GOAPTestSceneController.cs
|
||||
uploadId: 920982
|
||||
@@ -0,0 +1,213 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SynapticPro.GOAP
|
||||
{
|
||||
/// <summary>
|
||||
/// World State - Manages the state of the world for GOAP planning
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class WorldState
|
||||
{
|
||||
private Dictionary<string, object> states = new Dictionary<string, object>();
|
||||
|
||||
/// <summary>
|
||||
/// Set a state value
|
||||
/// </summary>
|
||||
public void SetState(string key, object value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key)) return;
|
||||
|
||||
if (states.ContainsKey(key))
|
||||
{
|
||||
states[key] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
states.Add(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a state value
|
||||
/// </summary>
|
||||
public T GetState<T>(string key)
|
||||
{
|
||||
if (states.TryGetValue(key, out var value))
|
||||
{
|
||||
if (value is T typedValue)
|
||||
{
|
||||
return typedValue;
|
||||
}
|
||||
|
||||
// Try conversion
|
||||
try
|
||||
{
|
||||
return (T)Convert.ChangeType(value, typeof(T));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return default(T);
|
||||
}
|
||||
}
|
||||
|
||||
return default(T);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if state exists
|
||||
/// </summary>
|
||||
public bool HasState(string key)
|
||||
{
|
||||
return states.ContainsKey(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a state
|
||||
/// </summary>
|
||||
public void RemoveState(string key)
|
||||
{
|
||||
if (states.ContainsKey(key))
|
||||
{
|
||||
states.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all states
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
states.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all states as dictionary
|
||||
/// </summary>
|
||||
public Dictionary<string, object> GetAllStates()
|
||||
{
|
||||
return new Dictionary<string, object>(states);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clone this world state
|
||||
/// </summary>
|
||||
public WorldState Clone()
|
||||
{
|
||||
var clone = new WorldState();
|
||||
foreach (var kvp in states)
|
||||
{
|
||||
clone.states[kvp.Key] = kvp.Value;
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply another state on top of this one
|
||||
/// </summary>
|
||||
public void ApplyState(WorldState other)
|
||||
{
|
||||
if (other == null) return;
|
||||
|
||||
foreach (var kvp in other.states)
|
||||
{
|
||||
states[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if this state satisfies the conditions
|
||||
/// </summary>
|
||||
public bool Satisfies(Dictionary<string, object> conditions)
|
||||
{
|
||||
if (conditions == null || conditions.Count == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var condition in conditions)
|
||||
{
|
||||
if (!HasState(condition.Key))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var currentValue = states[condition.Key];
|
||||
if (!ValuesEqual(currentValue, condition.Value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compare two values for equality
|
||||
/// </summary>
|
||||
private bool ValuesEqual(object a, object b)
|
||||
{
|
||||
if (a == null && b == null) return true;
|
||||
if (a == null || b == null) return false;
|
||||
|
||||
// Handle numeric comparisons
|
||||
if (IsNumeric(a) && IsNumeric(b))
|
||||
{
|
||||
return Convert.ToDouble(a) == Convert.ToDouble(b);
|
||||
}
|
||||
|
||||
return a.Equals(b);
|
||||
}
|
||||
|
||||
private bool IsNumeric(object obj)
|
||||
{
|
||||
return obj is int || obj is float || obj is double || obj is long || obj is short || obj is byte;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get hash code for state comparison
|
||||
/// </summary>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
int hash = 17;
|
||||
foreach (var kvp in states.OrderBy(x => x.Key))
|
||||
{
|
||||
hash = hash * 31 + kvp.Key.GetHashCode();
|
||||
hash = hash * 31 + (kvp.Value?.GetHashCode() ?? 0);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// String representation for debugging
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine("WorldState:");
|
||||
foreach (var kvp in states)
|
||||
{
|
||||
sb.AppendLine($" {kvp.Key}: {kvp.Value}");
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize from dictionary
|
||||
/// </summary>
|
||||
public static WorldState FromDictionary(Dictionary<string, object> dict)
|
||||
{
|
||||
var state = new WorldState();
|
||||
if (dict != null)
|
||||
{
|
||||
foreach (var kvp in dict)
|
||||
{
|
||||
state.SetState(kvp.Key, kvp.Value);
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7bf0acbd42f3c4e70bb6b60fa7dd062e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 336030
|
||||
packageName: Synaptic AI Pro - Natural Language Control for Unity
|
||||
packageVersion: 1.2.23
|
||||
assetPath: Assets/Synaptic AI Pro/Runtime/GOAP/WorldState.cs
|
||||
uploadId: 920982
|
||||
Reference in New Issue
Block a user