2026-06-06 20:12:40 +07:00
parent de84b2bf48
commit 97ac0f71f5
13682 changed files with 1125938 additions and 0 deletions
@@ -0,0 +1,331 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace SynapticPro
{
/// <summary>
/// Component that controls the adaptive music system
/// Automatically manages intro→loop, crossfade transitions, beat synchronization, etc.
/// </summary>
public class AdaptiveMusicController : MonoBehaviour
{
[System.Serializable]
public class MusicSegment
{
public string name;
public AudioSource audioSource;
public float startTime;
public float endTime;
public float loopPoint;
public bool isLoop;
public float fadeInDuration = 0f;
public float fadeOutDuration = 0f;
public List<Transition> transitions = new List<Transition>();
}
[System.Serializable]
public class Transition
{
public string toSegment;
public TransitionType type = TransitionType.Crossfade;
public float duration = 1f;
}
public enum TransitionType
{
Immediate,
Crossfade,
OnBeat,
OnBar
}
[Header("Music Settings")]
public float bpm = 120f;
public int beatsPerBar = 4;
public bool playOnStart = true;
[Header("Segments")]
public List<MusicSegment> segments = new List<MusicSegment>();
private MusicSegment currentSegment;
private MusicSegment nextSegment;
private float currentTime;
private bool isTransitioning;
private Coroutine musicCoroutine;
void Start()
{
InitializeSegments();
if (playOnStart)
{
PlayFromBeginning();
}
}
void InitializeSegments()
{
// Automatically detect AudioSource from child objects
if (segments.Count == 0)
{
AudioSource[] sources = GetComponentsInChildren<AudioSource>();
foreach (var source in sources)
{
var segment = new MusicSegment
{
name = source.gameObject.name,
audioSource = source,
isLoop = source.loop
};
// Infer settings from segment name
if (segment.name.ToLower().Contains("intro"))
{
segment.isLoop = false;
segment.transitions.Add(new Transition
{
toSegment = "MainLoop",
type = TransitionType.Crossfade,
duration = 1.5f
});
}
else if (segment.name.ToLower().Contains("mainloop") || segment.name.ToLower().Contains("loop"))
{
segment.isLoop = true;
}
segments.Add(segment);
}
}
// Set end time from AudioClip length
foreach (var segment in segments)
{
if (segment.audioSource && segment.audioSource.clip)
{
if (segment.endTime <= 0)
{
segment.endTime = segment.audioSource.clip.length;
}
}
}
}
public void PlayFromBeginning()
{
if (musicCoroutine != null)
{
StopCoroutine(musicCoroutine);
}
musicCoroutine = StartCoroutine(MusicPlaybackCoroutine());
}
IEnumerator MusicPlaybackCoroutine()
{
// Find intro segment
var introSegment = segments.Find(s => s.name.ToLower().Contains("intro"));
if (introSegment == null && segments.Count > 0)
{
introSegment = segments[0];
}
if (introSegment == null)
{
Debug.LogWarning("No music segments found!");
yield break;
}
// Play intro
currentSegment = introSegment;
PlaySegment(currentSegment);
while (true)
{
currentTime = currentSegment.audioSource.time;
// Loop when reaching loop point
if (currentSegment.isLoop && currentSegment.loopPoint > 0 &&
currentTime >= currentSegment.loopPoint)
{
currentSegment.audioSource.time = currentSegment.startTime;
}
// Check transitions
if (!isTransitioning)
{
// Check for automatic transition
if (!currentSegment.isLoop && currentTime >= currentSegment.endTime - 2f)
{
// Find transition to next segment
if (currentSegment.transitions.Count > 0)
{
var transition = currentSegment.transitions[0];
var next = segments.Find(s => s.name == transition.toSegment);
if (next != null)
{
StartCoroutine(TransitionToSegment(next, transition));
}
}
}
}
yield return null;
}
}
void PlaySegment(MusicSegment segment)
{
if (segment.audioSource == null) return;
segment.audioSource.time = segment.startTime;
segment.audioSource.Play();
if (segment.fadeInDuration > 0)
{
StartCoroutine(FadeIn(segment.audioSource, segment.fadeInDuration));
}
else
{
segment.audioSource.volume = 1f;
}
}
IEnumerator TransitionToSegment(MusicSegment targetSegment, Transition transition)
{
if (isTransitioning) yield break;
isTransitioning = true;
nextSegment = targetSegment;
switch (transition.type)
{
case TransitionType.Immediate:
currentSegment.audioSource.Stop();
currentSegment = targetSegment;
PlaySegment(currentSegment);
break;
case TransitionType.Crossfade:
// Start next segment
PlaySegment(targetSegment);
targetSegment.audioSource.volume = 0;
// Crossfade
float elapsed = 0;
while (elapsed < transition.duration)
{
elapsed += Time.deltaTime;
float t = elapsed / transition.duration;
currentSegment.audioSource.volume = 1 - t;
targetSegment.audioSource.volume = t;
yield return null;
}
currentSegment.audioSource.Stop();
currentSegment = targetSegment;
break;
case TransitionType.OnBeat:
case TransitionType.OnBar:
// Wait until end of beat/bar
float beatDuration = 60f / bpm;
float barDuration = beatDuration * beatsPerBar;
float waitTime = transition.type == TransitionType.OnBeat ? beatDuration : barDuration;
// Wait for next beat/bar
float timeToWait = waitTime - (currentSegment.audioSource.time % waitTime);
yield return new WaitForSeconds(timeToWait);
// Crossfade transition
StartCoroutine(TransitionToSegment(targetSegment, new Transition
{
toSegment = targetSegment.name,
type = TransitionType.Crossfade,
duration = transition.duration
}));
yield break;
}
isTransitioning = false;
}
IEnumerator FadeIn(AudioSource source, float duration)
{
float elapsed = 0;
source.volume = 0;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
source.volume = elapsed / duration;
yield return null;
}
source.volume = 1f;
}
IEnumerator FadeOut(AudioSource source, float duration)
{
float elapsed = 0;
float startVolume = source.volume;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
source.volume = startVolume * (1 - elapsed / duration);
yield return null;
}
source.volume = 0;
source.Stop();
}
/// <summary>
/// Transition to specific segment
/// </summary>
public void TransitionTo(string segmentName, float transitionDuration = 2f)
{
var targetSegment = segments.Find(s => s.name == segmentName);
if (targetSegment != null && !isTransitioning)
{
StartCoroutine(TransitionToSegment(targetSegment, new Transition
{
toSegment = segmentName,
type = TransitionType.Crossfade,
duration = transitionDuration
}));
}
}
/// <summary>
/// Stop music
/// </summary>
public void StopMusic(float fadeOutDuration = 1f)
{
if (musicCoroutine != null)
{
StopCoroutine(musicCoroutine);
musicCoroutine = null;
}
foreach (var segment in segments)
{
if (segment.audioSource && segment.audioSource.isPlaying)
{
if (fadeOutDuration > 0)
{
StartCoroutine(FadeOut(segment.audioSource, fadeOutDuration));
}
else
{
segment.audioSource.Stop();
}
}
}
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 8c4f1e2878a70444b9a55aa57656a98b
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/AdaptiveMusicController.cs
uploadId: 920982
@@ -0,0 +1,241 @@
using System.Collections;
using UnityEngine;
namespace SynapticPro
{
/// <summary>
/// Revolutionary adaptive music system
/// Perfect implementation of intro+loop pattern
/// Music system generated from natural language in one shot
/// </summary>
public class AdaptiveMusicSystem : MonoBehaviour
{
[Header("Audio Settings")]
public AudioClip musicClip;
public float introDuration = 10f;
public float loopStartTime = 10f;
public float loopEndTime = -1f; // -1 means end of clip
[Header("Fade Settings")]
public float fadeInDuration = 0f;
public float fadeOutDuration = 2f;
public float volume = 0.8f;
[Header("Runtime Info")]
[SerializeField] private bool isPlaying = false;
[SerializeField] private bool hasPlayedIntro = false;
[SerializeField] private float currentTime = 0f;
private AudioSource audioSource;
private Coroutine musicCoroutine;
void Awake()
{
// Automatically create AudioSource
audioSource = gameObject.GetComponent<AudioSource>();
if (audioSource == null)
{
audioSource = gameObject.AddComponent<AudioSource>();
}
// Basic configuration
audioSource.playOnAwake = false;
audioSource.loop = false; // Manual loop control
audioSource.volume = 0f; // For fade-in
}
void Start()
{
if (musicClip != null)
{
PlayAdaptiveMusic();
}
}
/// <summary>
/// Start playing adaptive music
/// </summary>
public void PlayAdaptiveMusic()
{
if (musicClip == null)
{
Debug.LogWarning("[AdaptiveMusic] AudioClip is not assigned!");
return;
}
if (musicCoroutine != null)
{
StopCoroutine(musicCoroutine);
}
// Set default loop end time
if (loopEndTime <= 0 || loopEndTime > musicClip.length)
{
loopEndTime = musicClip.length;
}
musicCoroutine = StartCoroutine(AdaptiveMusicCoroutine());
}
/// <summary>
/// Stop music (with fade-out)
/// </summary>
public void StopMusic()
{
if (musicCoroutine != null)
{
StopCoroutine(musicCoroutine);
musicCoroutine = null;
}
StartCoroutine(FadeOut());
}
/// <summary>
/// Main loop for adaptive music
/// </summary>
private IEnumerator AdaptiveMusicCoroutine()
{
isPlaying = true;
hasPlayedIntro = false;
currentTime = 0f;
// Set AudioClip
audioSource.clip = musicClip;
audioSource.time = 0f;
audioSource.Play();
// Fade in
if (fadeInDuration > 0)
{
yield return StartCoroutine(FadeIn());
}
else
{
audioSource.volume = volume;
}
Debug.Log($"[AdaptiveMusic] Started playing: Intro({introDuration}s) -> Loop({loopStartTime}s-{loopEndTime}s)");
// Main loop
while (isPlaying)
{
currentTime = audioSource.time;
// Handle intro section
if (!hasPlayedIntro && currentTime >= introDuration)
{
hasPlayedIntro = true;
Debug.Log("[AdaptiveMusic] Intro finished, starting loop section");
}
// When loop point is reached
if (hasPlayedIntro && currentTime >= loopEndTime)
{
Debug.Log($"[AdaptiveMusic] Loop point reached, jumping to {loopStartTime}s");
audioSource.time = loopStartTime;
}
// When music ends (unexpected)
if (!audioSource.isPlaying)
{
Debug.LogWarning("[AdaptiveMusic] Audio stopped unexpectedly, restarting...");
audioSource.time = hasPlayedIntro ? loopStartTime : 0f;
audioSource.Play();
}
yield return null; // Wait until next frame
}
}
/// <summary>
/// Fade-in processing
/// </summary>
private IEnumerator FadeIn()
{
float elapsedTime = 0f;
audioSource.volume = 0f;
while (elapsedTime < fadeInDuration)
{
elapsedTime += Time.deltaTime;
float normalizedTime = elapsedTime / fadeInDuration;
audioSource.volume = Mathf.Lerp(0f, volume, normalizedTime);
yield return null;
}
audioSource.volume = volume;
}
/// <summary>
/// Fade-out processing
/// </summary>
private IEnumerator FadeOut()
{
float startVolume = audioSource.volume;
float elapsedTime = 0f;
while (elapsedTime < fadeOutDuration)
{
elapsedTime += Time.deltaTime;
float normalizedTime = elapsedTime / fadeOutDuration;
audioSource.volume = Mathf.Lerp(startVolume, 0f, normalizedTime);
yield return null;
}
audioSource.volume = 0f;
audioSource.Stop();
isPlaying = false;
}
/// <summary>
/// Dynamically update settings (can be changed during runtime)
/// </summary>
public void UpdateSettings(float newIntroDuration, float newLoopStart, float newLoopEnd = -1f)
{
introDuration = newIntroDuration;
loopStartTime = newLoopStart;
if (newLoopEnd > 0)
{
loopEndTime = newLoopEnd;
}
else if (musicClip != null)
{
loopEndTime = musicClip.length;
}
Debug.Log($"[AdaptiveMusic] Settings updated: Intro({introDuration}s) -> Loop({loopStartTime}s-{loopEndTime}s)");
}
/// <summary>
/// Display debug information
/// </summary>
void OnGUI()
{
if (!Application.isPlaying) return;
GUILayout.BeginArea(new Rect(10, 10, 300, 150));
GUILayout.Label("🎵 Adaptive Music System");
GUILayout.Label($"Playing: {isPlaying}");
GUILayout.Label($"Has Played Intro: {hasPlayedIntro}");
GUILayout.Label($"Current Time: {currentTime:F2}s");
GUILayout.Label($"Loop Range: {loopStartTime:F1}s - {loopEndTime:F1}s");
if (musicClip != null)
{
GUILayout.Label($"Clip Length: {musicClip.length:F2}s");
}
GUILayout.EndArea();
}
void OnDestroy()
{
if (musicCoroutine != null)
{
StopCoroutine(musicCoroutine);
}
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: f7c2ed473485749bca12716e0fd628d1
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/AdaptiveMusicSystem.cs
uploadId: 920982
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b000310b3323d42889d061c6c34cabf0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,327 @@
using System.Collections.Generic;
using UnityEngine;
namespace SynapticPro.BehaviorTree
{
/// <summary>
/// Selector (OR) - Returns Success if ANY child succeeds
/// Tries children in order until one succeeds
/// </summary>
public class BTSelector : BTComposite
{
public BTSelector(string name = "Selector") : base(name) { }
public override BTStatus Tick()
{
for (int i = currentChildIndex; i < children.Count; i++)
{
var status = children[i].Tick();
switch (status)
{
case BTStatus.Success:
currentChildIndex = 0;
IsRunning = false;
return BTStatus.Success;
case BTStatus.Running:
currentChildIndex = i;
IsRunning = true;
return BTStatus.Running;
case BTStatus.Failure:
// Try next child
continue;
}
}
// All children failed
currentChildIndex = 0;
IsRunning = false;
return BTStatus.Failure;
}
}
/// <summary>
/// Sequence (AND) - Returns Success only if ALL children succeed
/// Executes children in order, stops on first failure
/// </summary>
public class BTSequence : BTComposite
{
public BTSequence(string name = "Sequence") : base(name) { }
public override BTStatus Tick()
{
for (int i = currentChildIndex; i < children.Count; i++)
{
var status = children[i].Tick();
switch (status)
{
case BTStatus.Success:
// Continue to next child
continue;
case BTStatus.Running:
currentChildIndex = i;
IsRunning = true;
return BTStatus.Running;
case BTStatus.Failure:
currentChildIndex = 0;
IsRunning = false;
return BTStatus.Failure;
}
}
// All children succeeded
currentChildIndex = 0;
IsRunning = false;
return BTStatus.Success;
}
}
/// <summary>
/// Parallel - Runs all children simultaneously
/// Returns based on policy (RequireAll or RequireOne)
/// </summary>
public class BTParallel : BTComposite
{
public enum Policy
{
RequireAll, // Success only if ALL succeed
RequireOne // Success if ANY succeeds
}
public Policy SuccessPolicy { get; set; } = Policy.RequireAll;
public Policy FailurePolicy { get; set; } = Policy.RequireOne;
private List<BTStatus> childStatuses = new List<BTStatus>();
public BTParallel(string name = "Parallel") : base(name) { }
public BTParallel(Policy successPolicy, Policy failurePolicy, string name = "Parallel") : base(name)
{
SuccessPolicy = successPolicy;
FailurePolicy = failurePolicy;
}
public override BTStatus Tick()
{
// Initialize status tracking
while (childStatuses.Count < children.Count)
{
childStatuses.Add(BTStatus.Running);
}
int successCount = 0;
int failureCount = 0;
int runningCount = 0;
for (int i = 0; i < children.Count; i++)
{
// Skip already completed children
if (childStatuses[i] != BTStatus.Running)
{
if (childStatuses[i] == BTStatus.Success) successCount++;
else failureCount++;
continue;
}
var status = children[i].Tick();
childStatuses[i] = status;
switch (status)
{
case BTStatus.Success:
successCount++;
break;
case BTStatus.Failure:
failureCount++;
break;
case BTStatus.Running:
runningCount++;
break;
}
}
// Check failure policy
if (FailurePolicy == Policy.RequireOne && failureCount > 0)
{
Reset();
return BTStatus.Failure;
}
if (FailurePolicy == Policy.RequireAll && failureCount == children.Count)
{
Reset();
return BTStatus.Failure;
}
// Check success policy
if (SuccessPolicy == Policy.RequireOne && successCount > 0)
{
Reset();
return BTStatus.Success;
}
if (SuccessPolicy == Policy.RequireAll && successCount == children.Count)
{
Reset();
return BTStatus.Success;
}
// Still running
if (runningCount > 0)
{
IsRunning = true;
return BTStatus.Running;
}
// Default to failure
Reset();
return BTStatus.Failure;
}
public override void Reset()
{
base.Reset();
childStatuses.Clear();
}
}
/// <summary>
/// Random Selector - Randomly picks a child to execute
/// </summary>
public class BTRandomSelector : BTComposite
{
private List<int> shuffledIndices = new List<int>();
private int currentIndex = 0;
public BTRandomSelector(string name = "RandomSelector") : base(name) { }
public override BTStatus Tick()
{
// Shuffle on first tick
if (shuffledIndices.Count == 0)
{
ShuffleChildren();
}
for (int i = currentIndex; i < shuffledIndices.Count; i++)
{
var childIndex = shuffledIndices[i];
var status = children[childIndex].Tick();
switch (status)
{
case BTStatus.Success:
Reset();
return BTStatus.Success;
case BTStatus.Running:
currentIndex = i;
IsRunning = true;
return BTStatus.Running;
case BTStatus.Failure:
continue;
}
}
Reset();
return BTStatus.Failure;
}
private void ShuffleChildren()
{
shuffledIndices.Clear();
for (int i = 0; i < children.Count; i++)
{
shuffledIndices.Add(i);
}
// Fisher-Yates shuffle
for (int i = shuffledIndices.Count - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1);
var temp = shuffledIndices[i];
shuffledIndices[i] = shuffledIndices[j];
shuffledIndices[j] = temp;
}
}
public override void Reset()
{
base.Reset();
shuffledIndices.Clear();
currentIndex = 0;
}
}
/// <summary>
/// Random Sequence - Executes children in random order
/// </summary>
public class BTRandomSequence : BTComposite
{
private List<int> shuffledIndices = new List<int>();
private int currentIndex = 0;
public BTRandomSequence(string name = "RandomSequence") : base(name) { }
public override BTStatus Tick()
{
// Shuffle on first tick
if (shuffledIndices.Count == 0)
{
ShuffleChildren();
}
for (int i = currentIndex; i < shuffledIndices.Count; i++)
{
var childIndex = shuffledIndices[i];
var status = children[childIndex].Tick();
switch (status)
{
case BTStatus.Success:
continue;
case BTStatus.Running:
currentIndex = i;
IsRunning = true;
return BTStatus.Running;
case BTStatus.Failure:
Reset();
return BTStatus.Failure;
}
}
Reset();
return BTStatus.Success;
}
private void ShuffleChildren()
{
shuffledIndices.Clear();
for (int i = 0; i < children.Count; i++)
{
shuffledIndices.Add(i);
}
for (int i = shuffledIndices.Count - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1);
var temp = shuffledIndices[i];
shuffledIndices[i] = shuffledIndices[j];
shuffledIndices[j] = temp;
}
}
public override void Reset()
{
base.Reset();
shuffledIndices.Clear();
currentIndex = 0;
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 86de43f4d7cbe473fbd33222e6a6b693
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/BehaviorTree/BTComposites.cs
uploadId: 920982
@@ -0,0 +1,395 @@
using System;
using UnityEngine;
namespace SynapticPro.BehaviorTree
{
/// <summary>
/// Inverter - Inverts the result of child
/// Success -> Failure, Failure -> Success, Running -> Running
/// </summary>
public class BTInverter : BTDecorator
{
public BTInverter(string name = "Inverter") : base(name) { }
public override BTStatus Tick()
{
if (child == null) return BTStatus.Failure;
var status = child.Tick();
switch (status)
{
case BTStatus.Success:
return BTStatus.Failure;
case BTStatus.Failure:
return BTStatus.Success;
default:
IsRunning = true;
return BTStatus.Running;
}
}
}
/// <summary>
/// Succeeder - Always returns Success (ignores child result)
/// </summary>
public class BTSucceeder : BTDecorator
{
public BTSucceeder(string name = "Succeeder") : base(name) { }
public override BTStatus Tick()
{
if (child == null) return BTStatus.Success;
var status = child.Tick();
if (status == BTStatus.Running)
{
IsRunning = true;
return BTStatus.Running;
}
return BTStatus.Success;
}
}
/// <summary>
/// Failer - Always returns Failure (ignores child result)
/// </summary>
public class BTFailer : BTDecorator
{
public BTFailer(string name = "Failer") : base(name) { }
public override BTStatus Tick()
{
if (child == null) return BTStatus.Failure;
var status = child.Tick();
if (status == BTStatus.Running)
{
IsRunning = true;
return BTStatus.Running;
}
return BTStatus.Failure;
}
}
/// <summary>
/// Repeater - Repeats child execution N times or until condition
/// </summary>
public class BTRepeater : BTDecorator
{
public int RepeatCount { get; set; } = -1; // -1 = infinite
public bool RepeatUntilFail { get; set; } = false;
public bool RepeatUntilSuccess { get; set; } = false;
private int currentCount = 0;
public BTRepeater(int count = -1, string name = "Repeater") : base(name)
{
RepeatCount = count;
}
public override BTStatus Tick()
{
if (child == null) return BTStatus.Failure;
var status = child.Tick();
if (status == BTStatus.Running)
{
IsRunning = true;
return BTStatus.Running;
}
// Check termination conditions
if (RepeatUntilFail && status == BTStatus.Failure)
{
Reset();
return BTStatus.Success;
}
if (RepeatUntilSuccess && status == BTStatus.Success)
{
Reset();
return BTStatus.Success;
}
currentCount++;
// Check count limit
if (RepeatCount > 0 && currentCount >= RepeatCount)
{
Reset();
return status;
}
// Reset child and continue
child.Reset();
IsRunning = true;
return BTStatus.Running;
}
public override void Reset()
{
base.Reset();
currentCount = 0;
}
}
/// <summary>
/// Cooldown - Prevents child execution for a duration after completion
/// </summary>
public class BTCooldown : BTDecorator
{
public float CooldownTime { get; set; } = 1f;
private float lastExecutionTime = float.MinValue;
private bool childCompleted = false;
public BTCooldown(float cooldown, string name = "Cooldown") : base(name)
{
CooldownTime = cooldown;
}
public override BTStatus Tick()
{
if (child == null) return BTStatus.Failure;
// Check cooldown
float currentTime = Time.time;
if (currentTime - lastExecutionTime < CooldownTime)
{
return BTStatus.Failure;
}
var status = child.Tick();
if (status == BTStatus.Running)
{
IsRunning = true;
return BTStatus.Running;
}
// Child completed, start cooldown
lastExecutionTime = currentTime;
IsRunning = false;
return status;
}
public override void Reset()
{
base.Reset();
// Don't reset lastExecutionTime - cooldown persists
}
/// <summary>
/// Force reset cooldown
/// </summary>
public void ResetCooldown()
{
lastExecutionTime = float.MinValue;
}
}
/// <summary>
/// Timeout - Fails if child takes too long
/// </summary>
public class BTTimeout : BTDecorator
{
public float TimeoutDuration { get; set; } = 5f;
private float startTime;
private bool started = false;
public BTTimeout(float timeout, string name = "Timeout") : base(name)
{
TimeoutDuration = timeout;
}
public override BTStatus Tick()
{
if (child == null) return BTStatus.Failure;
if (!started)
{
startTime = Time.time;
started = true;
}
// Check timeout
if (Time.time - startTime >= TimeoutDuration)
{
child.Abort();
Reset();
return BTStatus.Failure;
}
var status = child.Tick();
if (status != BTStatus.Running)
{
Reset();
}
else
{
IsRunning = true;
}
return status;
}
public override void Reset()
{
base.Reset();
started = false;
}
}
/// <summary>
/// Condition Decorator - Only executes child if condition is true
/// </summary>
public class BTConditional : BTDecorator
{
private Func<BTContext, bool> condition;
public BTConditional(Func<BTContext, bool> condition, string name = "Conditional") : base(name)
{
this.condition = condition;
}
public override BTStatus Tick()
{
if (condition == null || !condition(Context))
{
return BTStatus.Failure;
}
if (child == null) return BTStatus.Success;
var status = child.Tick();
IsRunning = status == BTStatus.Running;
return status;
}
}
/// <summary>
/// Retry - Retries child on failure
/// </summary>
public class BTRetry : BTDecorator
{
public int MaxRetries { get; set; } = 3;
private int currentRetries = 0;
public BTRetry(int maxRetries = 3, string name = "Retry") : base(name)
{
MaxRetries = maxRetries;
}
public override BTStatus Tick()
{
if (child == null) return BTStatus.Failure;
var status = child.Tick();
switch (status)
{
case BTStatus.Success:
Reset();
return BTStatus.Success;
case BTStatus.Running:
IsRunning = true;
return BTStatus.Running;
case BTStatus.Failure:
currentRetries++;
if (currentRetries >= MaxRetries)
{
Reset();
return BTStatus.Failure;
}
child.Reset();
IsRunning = true;
return BTStatus.Running;
}
return BTStatus.Failure;
}
public override void Reset()
{
base.Reset();
currentRetries = 0;
}
}
/// <summary>
/// Delay - Waits before executing child
/// </summary>
public class BTDelay : BTDecorator
{
public float DelayTime { get; set; } = 1f;
private float startTime;
private bool waiting = false;
private bool delayComplete = false;
public BTDelay(float delay, string name = "Delay") : base(name)
{
DelayTime = delay;
}
public override BTStatus Tick()
{
if (!waiting && !delayComplete)
{
startTime = Time.time;
waiting = true;
}
if (waiting)
{
if (Time.time - startTime >= DelayTime)
{
waiting = false;
delayComplete = true;
}
else
{
IsRunning = true;
return BTStatus.Running;
}
}
if (child == null)
{
Reset();
return BTStatus.Success;
}
var status = child.Tick();
if (status != BTStatus.Running)
{
Reset();
}
else
{
IsRunning = true;
}
return status;
}
public override void Reset()
{
base.Reset();
waiting = false;
delayComplete = false;
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 58ef143ab804043b2beb2a2ca8bfd85d
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/BehaviorTree/BTDecorators.cs
uploadId: 920982
@@ -0,0 +1,356 @@
using System;
using UnityEngine;
namespace SynapticPro.BehaviorTree
{
/// <summary>
/// Action node - Executes a function
/// </summary>
public class BTAction : BTNode
{
private Func<BTContext, BTStatus> action;
private Action<BTContext> onStart;
private Action<BTContext> onEnd;
public BTAction(Func<BTContext, BTStatus> action, string name = "Action") : base(name)
{
this.action = action;
}
public BTAction(Action<BTContext> simpleAction, string name = "Action") : base(name)
{
this.action = (ctx) =>
{
simpleAction?.Invoke(ctx);
return BTStatus.Success;
};
}
/// <summary>
/// Set callback for when action starts
/// </summary>
public BTAction OnStart(Action<BTContext> callback)
{
onStart = callback;
return this;
}
/// <summary>
/// Set callback for when action ends
/// </summary>
public BTAction OnEnd(Action<BTContext> callback)
{
onEnd = callback;
return this;
}
public override BTStatus Tick()
{
if (action == null) return BTStatus.Failure;
if (!IsRunning)
{
onStart?.Invoke(Context);
}
var status = action(Context);
IsRunning = status == BTStatus.Running;
if (status != BTStatus.Running)
{
onEnd?.Invoke(Context);
}
return status;
}
public override void Abort()
{
base.Abort();
onEnd?.Invoke(Context);
}
}
/// <summary>
/// Condition node - Checks a condition
/// </summary>
public class BTCondition : BTNode
{
private Func<BTContext, bool> condition;
public BTCondition(Func<BTContext, bool> condition, string name = "Condition") : base(name)
{
this.condition = condition;
}
public override BTStatus Tick()
{
if (condition == null) return BTStatus.Failure;
return condition(Context) ? BTStatus.Success : BTStatus.Failure;
}
}
/// <summary>
/// Wait node - Waits for specified duration
/// </summary>
public class BTWait : BTNode
{
public float Duration { get; set; }
private float startTime;
private bool started = false;
public BTWait(float duration, string name = "Wait") : base(name)
{
Duration = duration;
}
public override BTStatus Tick()
{
if (!started)
{
startTime = Time.time;
started = true;
}
if (Time.time - startTime >= Duration)
{
Reset();
return BTStatus.Success;
}
IsRunning = true;
return BTStatus.Running;
}
public override void Reset()
{
base.Reset();
started = false;
}
}
/// <summary>
/// Log node - Logs a message (for debugging)
/// </summary>
public class BTLog : BTNode
{
private string message;
private Func<BTContext, string> dynamicMessage;
public BTLog(string message, string name = "Log") : base(name)
{
this.message = message;
}
public BTLog(Func<BTContext, string> messageFunc, string name = "Log") : base(name)
{
this.dynamicMessage = messageFunc;
}
public override BTStatus Tick()
{
string msg = dynamicMessage != null ? dynamicMessage(Context) : message;
Debug.Log($"[BT] {msg}");
return BTStatus.Success;
}
}
/// <summary>
/// Set Value node - Sets a value in context
/// </summary>
public class BTSetValue<T> : BTNode
{
private string key;
private T value;
private Func<BTContext, T> valueFunc;
public BTSetValue(string key, T value, string name = "SetValue") : base(name)
{
this.key = key;
this.value = value;
}
public BTSetValue(string key, Func<BTContext, T> valueFunc, string name = "SetValue") : base(name)
{
this.key = key;
this.valueFunc = valueFunc;
}
public override BTStatus Tick()
{
T val = valueFunc != null ? valueFunc(Context) : value;
Context.Set(key, val);
return BTStatus.Success;
}
}
/// <summary>
/// Check Value node - Checks a value in context
/// </summary>
public class BTCheckValue<T> : BTNode where T : IEquatable<T>
{
private string key;
private T expectedValue;
private Func<T, T, bool> comparer;
public BTCheckValue(string key, T expectedValue, string name = "CheckValue") : base(name)
{
this.key = key;
this.expectedValue = expectedValue;
}
public BTCheckValue(string key, T expectedValue, Func<T, T, bool> comparer, string name = "CheckValue") : base(name)
{
this.key = key;
this.expectedValue = expectedValue;
this.comparer = comparer;
}
public override BTStatus Tick()
{
var value = Context.Get<T>(key);
bool match;
if (comparer != null)
{
match = comparer(value, expectedValue);
}
else
{
match = value != null && value.Equals(expectedValue);
}
return match ? BTStatus.Success : BTStatus.Failure;
}
}
/// <summary>
/// Move To node - Moves towards a target position
/// </summary>
public class BTMoveTo : BTNode
{
public float Speed { get; set; } = 5f;
public float StoppingDistance { get; set; } = 0.5f;
private Vector3 targetPosition;
private Func<BTContext, Vector3> targetFunc;
public BTMoveTo(Vector3 target, string name = "MoveTo") : base(name)
{
targetPosition = target;
}
public BTMoveTo(Func<BTContext, Vector3> targetFunc, string name = "MoveTo") : base(name)
{
this.targetFunc = targetFunc;
}
public override BTStatus Tick()
{
if (Context?.Transform == null) return BTStatus.Failure;
Vector3 target = targetFunc != null ? targetFunc(Context) : targetPosition;
Vector3 currentPos = Context.Transform.position;
float distance = Vector3.Distance(currentPos, target);
if (distance <= StoppingDistance)
{
IsRunning = false;
return BTStatus.Success;
}
// Move towards target
Vector3 direction = (target - currentPos).normalized;
Context.Transform.position += direction * Speed * Time.deltaTime;
// Face direction
if (direction != Vector3.zero)
{
direction.y = 0;
if (direction.sqrMagnitude > 0.001f)
{
Context.Transform.forward = direction;
}
}
IsRunning = true;
return BTStatus.Running;
}
}
/// <summary>
/// Look At node - Rotates to face a target
/// </summary>
public class BTLookAt : BTNode
{
public float RotationSpeed { get; set; } = 5f;
public float Tolerance { get; set; } = 5f; // degrees
private Transform target;
private Func<BTContext, Transform> targetFunc;
public BTLookAt(Transform target, string name = "LookAt") : base(name)
{
this.target = target;
}
public BTLookAt(Func<BTContext, Transform> targetFunc, string name = "LookAt") : base(name)
{
this.targetFunc = targetFunc;
}
public override BTStatus Tick()
{
if (Context?.Transform == null) return BTStatus.Failure;
Transform lookTarget = targetFunc != null ? targetFunc(Context) : target;
if (lookTarget == null) return BTStatus.Failure;
Vector3 direction = lookTarget.position - Context.Transform.position;
direction.y = 0;
if (direction.sqrMagnitude < 0.001f)
{
return BTStatus.Success;
}
Quaternion targetRotation = Quaternion.LookRotation(direction);
float angle = Quaternion.Angle(Context.Transform.rotation, targetRotation);
if (angle <= Tolerance)
{
return BTStatus.Success;
}
Context.Transform.rotation = Quaternion.Slerp(
Context.Transform.rotation,
targetRotation,
RotationSpeed * Time.deltaTime
);
IsRunning = true;
return BTStatus.Running;
}
}
/// <summary>
/// Random Success node - Randomly returns success or failure
/// </summary>
public class BTRandomSuccess : BTNode
{
public float SuccessChance { get; set; } = 0.5f;
public BTRandomSuccess(float chance = 0.5f, string name = "RandomSuccess") : base(name)
{
SuccessChance = Mathf.Clamp01(chance);
}
public override BTStatus Tick()
{
return UnityEngine.Random.value < SuccessChance ? BTStatus.Success : BTStatus.Failure;
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 3f5de2b2723dd433bac886ded1d38724
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/BehaviorTree/BTLeaves.cs
uploadId: 920982
@@ -0,0 +1,252 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace SynapticPro.BehaviorTree
{
/// <summary>
/// Node execution status
/// </summary>
public enum BTStatus
{
Success,
Failure,
Running
}
/// <summary>
/// Base class for all Behavior Tree nodes
/// </summary>
public abstract class BTNode
{
public string Name { get; set; }
public BTNode Parent { get; protected set; }
public bool IsRunning { get; protected set; }
/// <summary>
/// Context data shared across the tree
/// </summary>
public BTContext Context { get; set; }
protected BTNode(string name = "Node")
{
Name = name;
}
/// <summary>
/// Execute this node
/// </summary>
public abstract BTStatus Tick();
/// <summary>
/// Reset node state
/// </summary>
public virtual void Reset()
{
IsRunning = false;
}
/// <summary>
/// Called when node is aborted
/// </summary>
public virtual void Abort()
{
IsRunning = false;
}
/// <summary>
/// Set parent node
/// </summary>
internal void SetParent(BTNode parent)
{
Parent = parent;
}
}
/// <summary>
/// Base class for nodes with children
/// </summary>
public abstract class BTComposite : BTNode
{
protected List<BTNode> children = new List<BTNode>();
protected int currentChildIndex = 0;
public IReadOnlyList<BTNode> Children => children;
protected BTComposite(string name = "Composite") : base(name) { }
/// <summary>
/// Add a child node
/// </summary>
public BTComposite AddChild(BTNode child)
{
if (child != null)
{
child.SetParent(this);
child.Context = Context;
children.Add(child);
}
return this;
}
/// <summary>
/// Add multiple children
/// </summary>
public BTComposite AddChildren(params BTNode[] nodes)
{
foreach (var node in nodes)
{
AddChild(node);
}
return this;
}
/// <summary>
/// Remove a child node
/// </summary>
public void RemoveChild(BTNode child)
{
if (child != null)
{
child.SetParent(null);
children.Remove(child);
}
}
/// <summary>
/// Clear all children
/// </summary>
public void ClearChildren()
{
foreach (var child in children)
{
child.SetParent(null);
}
children.Clear();
currentChildIndex = 0;
}
public override void Reset()
{
base.Reset();
currentChildIndex = 0;
foreach (var child in children)
{
child.Reset();
}
}
public override void Abort()
{
base.Abort();
foreach (var child in children)
{
child.Abort();
}
}
}
/// <summary>
/// Base class for decorator nodes (single child)
/// </summary>
public abstract class BTDecorator : BTNode
{
protected BTNode child;
public BTNode Child => child;
protected BTDecorator(string name = "Decorator") : base(name) { }
/// <summary>
/// Set the child node
/// </summary>
public BTDecorator SetChild(BTNode node)
{
if (child != null)
{
child.SetParent(null);
}
child = node;
if (child != null)
{
child.SetParent(this);
child.Context = Context;
}
return this;
}
public override void Reset()
{
base.Reset();
child?.Reset();
}
public override void Abort()
{
base.Abort();
child?.Abort();
}
}
/// <summary>
/// Shared context data for the behavior tree
/// </summary>
public class BTContext
{
private Dictionary<string, object> data = new Dictionary<string, object>();
/// <summary>
/// The GameObject this tree is attached to
/// </summary>
public GameObject GameObject { get; set; }
/// <summary>
/// The Transform of the GameObject
/// </summary>
public Transform Transform => GameObject?.transform;
/// <summary>
/// Set a value in the context
/// </summary>
public void Set<T>(string key, T value)
{
data[key] = value;
}
/// <summary>
/// Get a value from the context
/// </summary>
public T Get<T>(string key, T defaultValue = default)
{
if (data.TryGetValue(key, out var value) && value is T typedValue)
{
return typedValue;
}
return defaultValue;
}
/// <summary>
/// Check if a key exists
/// </summary>
public bool Has(string key)
{
return data.ContainsKey(key);
}
/// <summary>
/// Remove a value
/// </summary>
public void Remove(string key)
{
data.Remove(key);
}
/// <summary>
/// Clear all data
/// </summary>
public void Clear()
{
data.Clear();
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: cf24ecfaec86e41f3a4b25d6f3979fa4
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/BehaviorTree/BTNode.cs
uploadId: 920982
@@ -0,0 +1,443 @@
using System;
using UnityEngine;
using UnityEngine.Events;
namespace SynapticPro.BehaviorTree
{
/// <summary>
/// MonoBehaviour that runs a Behavior Tree
/// </summary>
public class BehaviorTreeRunner : MonoBehaviour
{
[Header("Execution Settings")]
[SerializeField] private bool autoStart = true;
[SerializeField] private float tickInterval = 0f; // 0 = every frame
[SerializeField] private bool pauseWhenDisabled = true;
[Header("Debug")]
[SerializeField] private bool debugMode = false;
[SerializeField] private string currentNodeName = "";
[Header("Events")]
public UnityEvent OnTreeStarted;
public UnityEvent OnTreeCompleted;
public UnityEvent OnTreeSucceeded;
public UnityEvent OnTreeFailed;
// Runtime
private BTNode rootNode;
private BTContext context;
private float lastTickTime;
private bool isRunning;
private BTStatus lastStatus = BTStatus.Running;
/// <summary>
/// The root node of the tree
/// </summary>
public BTNode RootNode => rootNode;
/// <summary>
/// The shared context
/// </summary>
public BTContext Context => context;
/// <summary>
/// Whether the tree is currently running
/// </summary>
public bool IsRunning => isRunning;
/// <summary>
/// Last tick status
/// </summary>
public BTStatus LastStatus => lastStatus;
private void Awake()
{
context = new BTContext { GameObject = gameObject };
}
private void Start()
{
if (autoStart && rootNode != null)
{
StartTree();
}
}
private void Update()
{
if (!isRunning || rootNode == null) return;
// Check tick interval
if (tickInterval > 0 && Time.time - lastTickTime < tickInterval)
{
return;
}
lastTickTime = Time.time;
// Tick the tree
lastStatus = rootNode.Tick();
if (debugMode && rootNode != null)
{
currentNodeName = GetCurrentNodeName(rootNode);
}
// Handle completion
if (lastStatus != BTStatus.Running)
{
OnTreeCompleted?.Invoke();
if (lastStatus == BTStatus.Success)
{
OnTreeSucceeded?.Invoke();
if (debugMode) Debug.Log($"[BT] Tree completed with Success");
}
else
{
OnTreeFailed?.Invoke();
if (debugMode) Debug.Log($"[BT] Tree completed with Failure");
}
// Reset for next run
rootNode.Reset();
}
}
private void OnEnable()
{
if (pauseWhenDisabled && rootNode != null)
{
// Resume
}
}
private void OnDisable()
{
if (pauseWhenDisabled && rootNode != null)
{
// Pause (just stop ticking, state preserved)
}
}
/// <summary>
/// Set the root node of the tree
/// </summary>
public void SetTree(BTNode root)
{
rootNode = root;
if (rootNode != null)
{
PropagateContext(rootNode, context);
}
}
/// <summary>
/// Start running the tree
/// </summary>
public void StartTree()
{
if (rootNode == null)
{
Debug.LogWarning("[BT] Cannot start tree: root node is null");
return;
}
isRunning = true;
lastTickTime = Time.time;
lastStatus = BTStatus.Running;
OnTreeStarted?.Invoke();
if (debugMode) Debug.Log("[BT] Tree started");
}
/// <summary>
/// Stop the tree
/// </summary>
public void StopTree()
{
if (!isRunning) return;
isRunning = false;
rootNode?.Abort();
rootNode?.Reset();
if (debugMode) Debug.Log("[BT] Tree stopped");
}
/// <summary>
/// Pause the tree
/// </summary>
public void PauseTree()
{
isRunning = false;
if (debugMode) Debug.Log("[BT] Tree paused");
}
/// <summary>
/// Resume the tree
/// </summary>
public void ResumeTree()
{
isRunning = true;
if (debugMode) Debug.Log("[BT] Tree resumed");
}
/// <summary>
/// Reset and restart the tree
/// </summary>
public void RestartTree()
{
StopTree();
StartTree();
}
/// <summary>
/// Set a value in the context
/// </summary>
public void SetContextValue<T>(string key, T value)
{
context?.Set(key, value);
}
/// <summary>
/// Get a value from the context
/// </summary>
public T GetContextValue<T>(string key, T defaultValue = default)
{
return context != null ? context.Get(key, defaultValue) : defaultValue;
}
/// <summary>
/// Propagate context to all nodes
/// </summary>
private void PropagateContext(BTNode node, BTContext ctx)
{
if (node == null) return;
node.Context = ctx;
if (node is BTComposite composite)
{
foreach (var child in composite.Children)
{
PropagateContext(child, ctx);
}
}
else if (node is BTDecorator decorator)
{
PropagateContext(decorator.Child, ctx);
}
}
/// <summary>
/// Get the name of currently executing node
/// </summary>
private string GetCurrentNodeName(BTNode node)
{
if (node == null) return "";
if (node.IsRunning)
{
if (node is BTComposite composite)
{
foreach (var child in composite.Children)
{
string childName = GetCurrentNodeName(child);
if (!string.IsNullOrEmpty(childName))
{
return childName;
}
}
}
else if (node is BTDecorator decorator)
{
string childName = GetCurrentNodeName(decorator.Child);
if (!string.IsNullOrEmpty(childName))
{
return childName;
}
}
return node.Name;
}
return "";
}
private void OnDrawGizmosSelected()
{
if (!debugMode || rootNode == null) return;
// Draw current node info
Vector3 pos = transform.position + Vector3.up * 2f;
#if UNITY_EDITOR
UnityEditor.Handles.Label(pos, $"BT: {currentNodeName}\nStatus: {lastStatus}");
#endif
}
}
/// <summary>
/// Builder for creating Behavior Trees fluently
/// </summary>
public class BehaviorTreeBuilder
{
private BTNode root;
private BTNode current;
/// <summary>
/// Start with a Selector
/// </summary>
public BehaviorTreeBuilder Selector(string name = "Selector")
{
var node = new BTSelector(name);
SetNode(node);
return this;
}
/// <summary>
/// Start with a Sequence
/// </summary>
public BehaviorTreeBuilder Sequence(string name = "Sequence")
{
var node = new BTSequence(name);
SetNode(node);
return this;
}
/// <summary>
/// Start with a Parallel
/// </summary>
public BehaviorTreeBuilder Parallel(string name = "Parallel")
{
var node = new BTParallel(name);
SetNode(node);
return this;
}
/// <summary>
/// Add an Action
/// </summary>
public BehaviorTreeBuilder Action(Func<BTContext, BTStatus> action, string name = "Action")
{
var node = new BTAction(action, name);
AddToCurrentComposite(node);
return this;
}
/// <summary>
/// Add a simple Action (auto-succeeds)
/// </summary>
public BehaviorTreeBuilder Do(Action<BTContext> action, string name = "Action")
{
var node = new BTAction(action, name);
AddToCurrentComposite(node);
return this;
}
/// <summary>
/// Add a Condition
/// </summary>
public BehaviorTreeBuilder Condition(Func<BTContext, bool> condition, string name = "Condition")
{
var node = new BTCondition(condition, name);
AddToCurrentComposite(node);
return this;
}
/// <summary>
/// Add a Wait
/// </summary>
public BehaviorTreeBuilder Wait(float duration, string name = "Wait")
{
var node = new BTWait(duration, name);
AddToCurrentComposite(node);
return this;
}
/// <summary>
/// Add an Inverter decorator
/// </summary>
public BehaviorTreeBuilder Invert()
{
// This should wrap the next node added
// For simplicity, we'll handle this differently
return this;
}
/// <summary>
/// Build the tree
/// </summary>
public BTNode Build()
{
return root;
}
/// <summary>
/// Build and assign to runner
/// </summary>
public void BuildAndRun(BehaviorTreeRunner runner)
{
runner.SetTree(Build());
runner.StartTree();
}
private void SetNode(BTNode node)
{
if (root == null)
{
root = node;
}
else if (current is BTComposite composite)
{
composite.AddChild(node);
}
current = node;
}
private void AddToCurrentComposite(BTNode node)
{
if (current is BTComposite composite)
{
composite.AddChild(node);
}
else if (root == null)
{
root = node;
current = node;
}
}
}
/// <summary>
/// Static helper for building trees
/// </summary>
public static class BT
{
public static BTSelector Selector(string name = "Selector") => new BTSelector(name);
public static BTSequence Sequence(string name = "Sequence") => new BTSequence(name);
public static BTParallel Parallel(string name = "Parallel") => new BTParallel(name);
public static BTRandomSelector RandomSelector(string name = "RandomSelector") => new BTRandomSelector(name);
public static BTRandomSequence RandomSequence(string name = "RandomSequence") => new BTRandomSequence(name);
public static BTInverter Inverter(string name = "Inverter") => new BTInverter(name);
public static BTSucceeder Succeeder(string name = "Succeeder") => new BTSucceeder(name);
public static BTFailer Failer(string name = "Failer") => new BTFailer(name);
public static BTRepeater Repeater(int count = -1, string name = "Repeater") => new BTRepeater(count, name);
public static BTCooldown Cooldown(float time, string name = "Cooldown") => new BTCooldown(time, name);
public static BTTimeout Timeout(float time, string name = "Timeout") => new BTTimeout(time, name);
public static BTRetry Retry(int count = 3, string name = "Retry") => new BTRetry(count, name);
public static BTDelay Delay(float time, string name = "Delay") => new BTDelay(time, name);
public static BTAction Action(Func<BTContext, BTStatus> action, string name = "Action") => new BTAction(action, name);
public static BTAction Action(Action<BTContext> action, string name = "Action") => new BTAction(action, name);
public static BTCondition Condition(Func<BTContext, bool> condition, string name = "Condition") => new BTCondition(condition, name);
public static BTWait Wait(float duration, string name = "Wait") => new BTWait(duration, name);
public static BTLog Log(string message, string name = "Log") => new BTLog(message, name);
public static BTMoveTo MoveTo(Vector3 target, string name = "MoveTo") => new BTMoveTo(target, name);
public static BTMoveTo MoveTo(Func<BTContext, Vector3> targetFunc, string name = "MoveTo") => new BTMoveTo(targetFunc, name);
public static BTRandomSuccess RandomSuccess(float chance = 0.5f, string name = "RandomSuccess") => new BTRandomSuccess(chance, name);
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: fb8445612d6634f2dbe123206811c1b6
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/BehaviorTree/BehaviorTreeRunner.cs
uploadId: 920982
@@ -0,0 +1,267 @@
using UnityEngine;
using System.Collections;
namespace SynapticPro
{
/// <summary>
/// Controller for Synaptic DissolvePro shader
/// Provides easy animation control and particle system integration
/// </summary>
[ExecuteAlways]
public class DissolveController : MonoBehaviour
{
[Header("Target")]
public Renderer targetRenderer;
public int materialIndex = 0;
[Header("Dissolve Settings")]
[Range(0f, 1f)]
public float dissolveAmount = 0f;
[Header("Direction")]
public DissolveDirection direction = DissolveDirection.Up;
public Vector3 customDirection = Vector3.up;
public Transform directionSource;
[Header("Animation")]
public float animationDuration = 2f;
public AnimationCurve dissolveCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
public bool autoReverse = false;
public float reverseDelay = 0.5f;
[Header("Particles")]
public ParticleSystem dissolveParticles;
public bool spawnParticlesOnEdge = true;
public float particleEmissionRate = 50f;
[Header("Audio")]
public AudioSource dissolveAudio;
public AudioClip dissolveSound;
[Header("Events")]
public UnityEngine.Events.UnityEvent onDissolveStart;
public UnityEngine.Events.UnityEvent onDissolveComplete;
public UnityEngine.Events.UnityEvent onAppearStart;
public UnityEngine.Events.UnityEvent onAppearComplete;
public enum DissolveDirection
{
Up,
Down,
Left,
Right,
Forward,
Back,
Spherical,
Custom
}
private Material material;
private Coroutine animationCoroutine;
private bool isAnimating = false;
// Shader property IDs
private static readonly int DissolveAmountID = Shader.PropertyToID("_DissolveAmount");
private static readonly int DissolveDirectionID = Shader.PropertyToID("_DissolveDirection");
private static readonly int DirectionalDissolveID = Shader.PropertyToID("_DirectionalDissolve");
private void OnEnable()
{
SetupMaterial();
}
private void SetupMaterial()
{
if (targetRenderer == null)
targetRenderer = GetComponent<Renderer>();
if (targetRenderer != null && targetRenderer.sharedMaterials.Length > materialIndex)
{
// Use instance material in play mode, shared in edit mode
if (Application.isPlaying)
{
material = targetRenderer.materials[materialIndex];
}
else
{
material = targetRenderer.sharedMaterials[materialIndex];
}
}
}
private void Update()
{
if (material == null)
return;
// Update dissolve amount
material.SetFloat(DissolveAmountID, dissolveAmount);
// Update direction
Vector3 dir = GetDissolveDirection();
material.SetVector(DissolveDirectionID, new Vector4(dir.x, dir.y, dir.z, 0));
material.SetFloat(DirectionalDissolveID, direction == DissolveDirection.Spherical ? 0f : 1f);
// Update particles
UpdateParticles();
}
private Vector3 GetDissolveDirection()
{
switch (direction)
{
case DissolveDirection.Up: return Vector3.up;
case DissolveDirection.Down: return Vector3.down;
case DissolveDirection.Left: return Vector3.left;
case DissolveDirection.Right: return Vector3.right;
case DissolveDirection.Forward: return Vector3.forward;
case DissolveDirection.Back: return Vector3.back;
case DissolveDirection.Spherical: return Vector3.zero;
case DissolveDirection.Custom:
if (directionSource != null)
return (directionSource.position - transform.position).normalized;
return customDirection.normalized;
default: return Vector3.up;
}
}
private void UpdateParticles()
{
if (dissolveParticles == null || !spawnParticlesOnEdge)
return;
var emission = dissolveParticles.emission;
// Only emit particles while dissolving is active and in progress
if (isAnimating && dissolveAmount > 0.01f && dissolveAmount < 0.99f)
{
emission.rateOverTime = particleEmissionRate;
// Position particles at dissolve edge (approximate)
Vector3 edgePosition = transform.position + GetDissolveDirection() * dissolveAmount * 2f;
dissolveParticles.transform.position = edgePosition;
}
else
{
emission.rateOverTime = 0;
}
}
/// <summary>
/// Start dissolving the object
/// </summary>
public void Dissolve()
{
if (animationCoroutine != null)
StopCoroutine(animationCoroutine);
animationCoroutine = StartCoroutine(AnimateDissolve(0f, 1f, onDissolveStart, onDissolveComplete));
}
/// <summary>
/// Make the object appear (reverse dissolve)
/// </summary>
public void Appear()
{
if (animationCoroutine != null)
StopCoroutine(animationCoroutine);
animationCoroutine = StartCoroutine(AnimateDissolve(1f, 0f, onAppearStart, onAppearComplete));
}
/// <summary>
/// Toggle between dissolved and visible states
/// </summary>
public void Toggle()
{
if (dissolveAmount < 0.5f)
Dissolve();
else
Appear();
}
/// <summary>
/// Set dissolve amount instantly
/// </summary>
public void SetDissolveInstant(float amount)
{
if (animationCoroutine != null)
StopCoroutine(animationCoroutine);
dissolveAmount = Mathf.Clamp01(amount);
isAnimating = false;
}
private IEnumerator AnimateDissolve(float from, float to,
UnityEngine.Events.UnityEvent onStart, UnityEngine.Events.UnityEvent onComplete)
{
isAnimating = true;
onStart?.Invoke();
// Play audio
if (dissolveAudio != null && dissolveSound != null)
{
dissolveAudio.clip = dissolveSound;
dissolveAudio.Play();
}
float elapsed = 0f;
dissolveAmount = from;
while (elapsed < animationDuration)
{
elapsed += Time.deltaTime;
float t = elapsed / animationDuration;
float curveT = dissolveCurve.Evaluate(t);
dissolveAmount = Mathf.Lerp(from, to, curveT);
yield return null;
}
dissolveAmount = to;
isAnimating = false;
onComplete?.Invoke();
// Auto reverse
if (autoReverse)
{
yield return new WaitForSeconds(reverseDelay);
if (to > 0.5f)
Appear();
else
Dissolve();
}
}
/// <summary>
/// Trigger dissolve from damage or hit
/// </summary>
public void OnDamage(Vector3 hitPoint)
{
// Set direction from hit point
direction = DissolveDirection.Custom;
customDirection = (transform.position - hitPoint).normalized;
Dissolve();
}
#if UNITY_EDITOR
private void OnValidate()
{
SetupMaterial();
}
private void OnDrawGizmosSelected()
{
// Draw dissolve direction
Gizmos.color = Color.cyan;
Vector3 dir = GetDissolveDirection();
if (dir.magnitude > 0.01f)
{
Gizmos.DrawRay(transform.position, dir * 2f);
Gizmos.DrawWireSphere(transform.position + dir * dissolveAmount * 2f, 0.1f);
}
}
#endif
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 2e7cdf08fe32b4c2684b691945fe388c
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/DissolveController.cs
uploadId: 920982
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c4f7a2c7dfa7040b680c4b57ea573efa
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -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
@@ -0,0 +1,402 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
namespace SynapticPro
{
/// <summary>
/// GPU-based grass rendering system using Compute Shaders
/// Supports LOD, frustum culling, and massive instance counts (1M+)
/// </summary>
[ExecuteAlways]
public class GrassRenderer : MonoBehaviour
{
[System.Serializable]
public struct GrassInstance
{
public Vector3 position;
public Vector3 normal;
public Vector2 uv;
public float height;
public float width;
public float rotation;
public float stiffness;
public float windPhase;
public static int Size => sizeof(float) * 14;
}
[Header("Grass Mesh")]
public Mesh grassMesh;
public Material grassMaterial;
[Header("Terrain Source")]
public Terrain sourceTerrain;
public LayerMask groundLayers = -1;
[Header("Density Settings")]
[Range(0.1f, 5f)]
public float density = 1f;
[Range(0.5f, 3f)]
public float heightMin = 0.5f;
[Range(0.5f, 3f)]
public float heightMax = 1.5f;
[Range(0.1f, 1f)]
public float widthMin = 0.3f;
[Range(0.1f, 1f)]
public float widthMax = 0.5f;
[Header("Culling & LOD")]
public float maxRenderDistance = 100f;
public float lod0Distance = 20f;
public float lod1Distance = 50f;
public float lod2Distance = 80f;
[Range(0f, 1f)]
public float densityFalloff = 0.5f;
public float frustumCullMargin = 2f;
[Header("Wind")]
public Vector3 windDirection = new Vector3(1, 0, 0.5f);
[Range(0f, 2f)]
public float windStrength = 0.5f;
public float windSpeed = 1f;
public float windFrequency = 1f;
[Header("Compute Shader")]
public ComputeShader grassCompute;
// Buffers
private ComputeBuffer sourceBuffer;
private ComputeBuffer culledBuffer;
private ComputeBuffer argsBuffer;
private ComputeBuffer counterBuffer;
// Instance data
private GrassInstance[] instances;
private int totalInstances;
private int maxVisibleInstances;
// Kernel IDs
private int mainKernel;
private int clearKernel;
// Shader property IDs
private static readonly int ViewProjectionMatrixID = Shader.PropertyToID("_ViewProjectionMatrix");
private static readonly int ViewMatrixID = Shader.PropertyToID("_ViewMatrix");
private static readonly int CameraPositionID = Shader.PropertyToID("_CameraPosition");
private static readonly int CameraForwardID = Shader.PropertyToID("_CameraForward");
private static readonly int FrustumCullMarginID = Shader.PropertyToID("_FrustumCullMargin");
private static readonly int MaxRenderDistanceID = Shader.PropertyToID("_MaxRenderDistance");
private static readonly int LOD0DistanceID = Shader.PropertyToID("_LOD0Distance");
private static readonly int LOD1DistanceID = Shader.PropertyToID("_LOD1Distance");
private static readonly int LOD2DistanceID = Shader.PropertyToID("_LOD2Distance");
private static readonly int DensityFalloffID = Shader.PropertyToID("_DensityFalloff");
private static readonly int TimeID = Shader.PropertyToID("_Time");
private static readonly int WindDirectionID = Shader.PropertyToID("_WindDirection");
private static readonly int WindStrengthID = Shader.PropertyToID("_WindStrength");
private static readonly int WindSpeedID = Shader.PropertyToID("_WindSpeed");
private static readonly int WindFrequencyID = Shader.PropertyToID("_WindFrequency");
private static readonly int TotalInstancesID = Shader.PropertyToID("_TotalInstances");
private static readonly int MaxVisibleInstancesID = Shader.PropertyToID("_MaxVisibleInstances");
private static readonly int LODGroupID = Shader.PropertyToID("_LODGroup");
private static readonly int FrustumPlanesID = Shader.PropertyToID("_FrustumPlanes");
private Camera mainCamera;
private Plane[] frustumPlanes = new Plane[6];
private Vector4[] frustumPlaneVectors = new Vector4[6];
private bool isInitialized = false;
private void OnEnable()
{
Initialize();
}
private void OnDisable()
{
ReleaseBuffers();
}
private void Initialize()
{
if (grassCompute == null || grassMesh == null || grassMaterial == null)
{
Debug.LogWarning("[GrassRenderer] Missing required references.");
return;
}
mainCamera = Camera.main;
if (mainCamera == null)
{
Debug.LogWarning("[GrassRenderer] No main camera found.");
return;
}
// Get kernel IDs
mainKernel = grassCompute.FindKernel("CSMain");
clearKernel = grassCompute.FindKernel("CSClear");
// Generate grass instances
GenerateGrassInstances();
if (totalInstances == 0)
{
Debug.LogWarning("[GrassRenderer] No grass instances generated.");
return;
}
// Create buffers
CreateBuffers();
isInitialized = true;
Debug.Log($"[GrassRenderer] Initialized with {totalInstances} grass instances.");
}
private void GenerateGrassInstances()
{
List<GrassInstance> instanceList = new List<GrassInstance>();
Bounds bounds;
if (sourceTerrain != null)
{
bounds = sourceTerrain.terrainData.bounds;
bounds.center = sourceTerrain.transform.position + bounds.center;
}
else
{
// Use a default area around the object
bounds = new Bounds(transform.position, new Vector3(50, 10, 50));
}
float spacing = 1f / density;
int gridSizeX = Mathf.CeilToInt(bounds.size.x / spacing);
int gridSizeZ = Mathf.CeilToInt(bounds.size.z / spacing);
for (int x = 0; x < gridSizeX; x++)
{
for (int z = 0; z < gridSizeZ; z++)
{
// Add some randomness to position
float offsetX = Random.Range(-spacing * 0.4f, spacing * 0.4f);
float offsetZ = Random.Range(-spacing * 0.4f, spacing * 0.4f);
Vector3 position = new Vector3(
bounds.min.x + x * spacing + offsetX,
bounds.max.y + 10f,
bounds.min.z + z * spacing + offsetZ
);
// Raycast to find ground
if (Physics.Raycast(position, Vector3.down, out RaycastHit hit, bounds.size.y + 20f, groundLayers))
{
// Skip if on steep slopes
if (hit.normal.y < 0.5f)
continue;
GrassInstance instance = new GrassInstance
{
position = hit.point,
normal = hit.normal,
uv = new Vector2(
(hit.point.x - bounds.min.x) / bounds.size.x,
(hit.point.z - bounds.min.z) / bounds.size.z
),
height = Random.Range(heightMin, heightMax),
width = Random.Range(widthMin, widthMax),
rotation = Random.Range(0f, 360f),
stiffness = Random.Range(0f, 0.5f),
windPhase = Random.Range(0f, Mathf.PI * 2f)
};
instanceList.Add(instance);
}
}
}
instances = instanceList.ToArray();
totalInstances = instances.Length;
maxVisibleInstances = Mathf.Min(totalInstances, 500000); // Cap for performance
}
private void CreateBuffers()
{
ReleaseBuffers();
// Source instances buffer
sourceBuffer = new ComputeBuffer(totalInstances, GrassInstance.Size);
sourceBuffer.SetData(instances);
// Culled instances buffer (visible instances)
culledBuffer = new ComputeBuffer(maxVisibleInstances, GrassInstance.Size);
// Indirect args buffer
uint[] args = new uint[5] { grassMesh.GetIndexCount(0), 0, 0, 0, 0 };
argsBuffer = new ComputeBuffer(1, sizeof(uint) * 5, ComputeBufferType.IndirectArguments);
argsBuffer.SetData(args);
// Counter buffer
counterBuffer = new ComputeBuffer(1, sizeof(uint));
counterBuffer.SetData(new uint[] { 0 });
// Set buffers to compute shader
grassCompute.SetBuffer(mainKernel, "_SourceInstances", sourceBuffer);
grassCompute.SetBuffer(mainKernel, "_CulledInstances", culledBuffer);
grassCompute.SetBuffer(mainKernel, "_IndirectArgs", argsBuffer);
grassCompute.SetBuffer(mainKernel, "_InstanceCounter", counterBuffer);
grassCompute.SetBuffer(clearKernel, "_InstanceCounter", counterBuffer);
grassCompute.SetBuffer(clearKernel, "_IndirectArgs", argsBuffer);
// Set material buffer
grassMaterial.SetBuffer("_GrassInstances", culledBuffer);
}
private void ReleaseBuffers()
{
sourceBuffer?.Release();
culledBuffer?.Release();
argsBuffer?.Release();
counterBuffer?.Release();
sourceBuffer = null;
culledBuffer = null;
argsBuffer = null;
counterBuffer = null;
}
private void Update()
{
if (!isInitialized || mainCamera == null)
return;
// Update frustum planes
GeometryUtility.CalculateFrustumPlanes(mainCamera, frustumPlanes);
for (int i = 0; i < 6; i++)
{
frustumPlaneVectors[i] = new Vector4(
frustumPlanes[i].normal.x,
frustumPlanes[i].normal.y,
frustumPlanes[i].normal.z,
frustumPlanes[i].distance
);
}
// Set compute shader parameters
Matrix4x4 viewMatrix = mainCamera.worldToCameraMatrix;
Matrix4x4 projMatrix = mainCamera.projectionMatrix;
Matrix4x4 viewProjMatrix = projMatrix * viewMatrix;
grassCompute.SetMatrix(ViewProjectionMatrixID, viewProjMatrix);
grassCompute.SetMatrix(ViewMatrixID, viewMatrix);
grassCompute.SetVector(CameraPositionID, mainCamera.transform.position);
grassCompute.SetVector(CameraForwardID, mainCamera.transform.forward);
grassCompute.SetFloat(FrustumCullMarginID, frustumCullMargin);
grassCompute.SetFloat(MaxRenderDistanceID, maxRenderDistance);
grassCompute.SetFloat(LOD0DistanceID, lod0Distance);
grassCompute.SetFloat(LOD1DistanceID, lod1Distance);
grassCompute.SetFloat(LOD2DistanceID, lod2Distance);
grassCompute.SetFloat(DensityFalloffID, densityFalloff);
grassCompute.SetFloat(TimeID, Time.time);
grassCompute.SetVector(WindDirectionID, windDirection.normalized);
grassCompute.SetFloat(WindStrengthID, windStrength);
grassCompute.SetFloat(WindSpeedID, windSpeed);
grassCompute.SetFloat(WindFrequencyID, windFrequency);
grassCompute.SetInt(TotalInstancesID, totalInstances);
grassCompute.SetInt(MaxVisibleInstancesID, maxVisibleInstances);
grassCompute.SetVectorArray(FrustumPlanesID, frustumPlaneVectors);
// Clear counter
grassCompute.Dispatch(clearKernel, 1, 1, 1);
// Run culling for each LOD group
int threadGroups = Mathf.CeilToInt(totalInstances / 256f);
for (int lod = 0; lod < 3; lod++)
{
grassCompute.SetInt(LODGroupID, lod);
grassCompute.Dispatch(mainKernel, threadGroups, 1, 1);
}
// Update indirect args with actual count
uint[] count = new uint[1];
counterBuffer.GetData(count);
uint[] args = new uint[5];
argsBuffer.GetData(args);
args[1] = count[0];
argsBuffer.SetData(args);
// Update material properties
grassMaterial.SetVector("_WindDirection", windDirection.normalized);
grassMaterial.SetFloat("_WindStrength", windStrength);
grassMaterial.SetFloat("_WindSpeed", windSpeed);
}
private void LateUpdate()
{
if (!isInitialized)
return;
// Draw grass using GPU instancing
Bounds drawBounds = new Bounds(transform.position, Vector3.one * maxRenderDistance * 2f);
Graphics.DrawMeshInstancedIndirect(
grassMesh,
0,
grassMaterial,
drawBounds,
argsBuffer,
0,
null,
ShadowCastingMode.Off,
false,
gameObject.layer
);
}
private void OnDrawGizmosSelected()
{
if (sourceTerrain != null)
{
Gizmos.color = Color.green;
Bounds bounds = sourceTerrain.terrainData.bounds;
bounds.center = sourceTerrain.transform.position + bounds.center;
Gizmos.DrawWireCube(bounds.center, bounds.size);
}
// Draw LOD distances
Gizmos.color = new Color(0, 1, 0, 0.3f);
Gizmos.DrawWireSphere(transform.position, lod0Distance);
Gizmos.color = new Color(1, 1, 0, 0.3f);
Gizmos.DrawWireSphere(transform.position, lod1Distance);
Gizmos.color = new Color(1, 0, 0, 0.3f);
Gizmos.DrawWireSphere(transform.position, lod2Distance);
}
/// <summary>
/// Regenerate grass instances (call after terrain changes)
/// </summary>
public void Regenerate()
{
ReleaseBuffers();
GenerateGrassInstances();
if (totalInstances > 0)
{
CreateBuffers();
isInitialized = true;
}
}
/// <summary>
/// Add interaction point (player position)
/// </summary>
public void SetInteractionPoint(Vector3 position, float radius = 2f)
{
if (grassMaterial != null)
{
grassMaterial.SetVector("_InteractionPosition", new Vector4(position.x, position.y, position.z, radius));
}
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 0174f790ecb8b43e1b64c8794c7f2b81
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/GrassRenderer.cs
uploadId: 920982
@@ -0,0 +1,21 @@
{
"name": "Synaptic.MCP.Unity",
"rootNamespace": "SynapticPro",
"references": [
"Unity.Mathematics",
"Unity.Collections",
"Unity.Burst",
"Unity.TextMeshPro",
"Unity.Nuget.Newtonsoft-Json",
"UnityEngine.UI"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 6c92e72eb55f84d54a65e7008ce49439
AssemblyDefinitionImporter:
externalObjects: {}
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/NexusAI.MCP.Unity.asmdef
uploadId: 920982
@@ -0,0 +1,582 @@
using System;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
using System.IO;
using System.Linq;
#if UNITY_2019_1_OR_NEWER
using UnityEditor.Compilation;
#endif
#else
using System.IO;
using System.Linq;
#endif
namespace SynapticPro
{
/// <summary>
/// Unity Real-time Event Monitoring System
/// Monitors Play state, file changes, and compilation state in real-time
/// </summary>
#if UNITY_EDITOR
public class NexusEventMonitor : EditorWindow
#else
public class NexusEventMonitor
#endif
{
private static NexusEventMonitor instance;
public static NexusEventMonitor Instance
{
get
{
if (instance == null)
{
#if UNITY_EDITOR
instance = GetWindow<NexusEventMonitor>();
instance.titleContent = new GUIContent("Nexus Event Monitor");
#else
instance = new NexusEventMonitor();
#endif
instance.Initialize();
}
return instance;
}
}
// Monitoring state
private bool isMonitoringPlayState = false;
private bool isMonitoringFileChanges = false;
private bool isMonitoringCompile = false;
// Previous state
#if UNITY_EDITOR
private PlayModeStateChange lastPlayState;
#endif
private Dictionary<string, DateTime> lastFileModificationTimes = new Dictionary<string, DateTime>();
private List<string> monitoredFileExtensions = new List<string> { ".cs", ".js", ".ts", ".shader", ".hlsl" };
// Event buffer
private Queue<EventData> eventQueue = new Queue<EventData>();
private const int maxEventQueueSize = 100;
// Custom event subscriptions
private Dictionary<string, HashSet<string>> eventSubscriptions = new Dictionary<string, HashSet<string>>();
[Serializable]
public class EventData
{
public string type;
public string category;
public DateTime timestamp;
public Dictionary<string, object> data;
public string description;
}
public static event Action<EventData> OnEventDetected;
private void Initialize()
{
#if UNITY_EDITOR
// Subscribe to editor events
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
#if UNITY_2019_1_OR_NEWER
CompilationPipeline.compilationStarted += OnCompilationStarted;
CompilationPipeline.compilationFinished += OnCompilationFinished;
#endif
#endif // UNITY_EDITOR
// Initialize file watcher
InitializeFileWatcher();
Debug.Log("[Nexus Event Monitor] Initialized");
}
private void OnDestroy()
{
#if UNITY_EDITOR
// Unsubscribe from events
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
#if UNITY_2019_1_OR_NEWER
CompilationPipeline.compilationStarted -= OnCompilationStarted;
CompilationPipeline.compilationFinished -= OnCompilationFinished;
#endif
#endif // UNITY_EDITOR
}
#region Play State Monitoring
/// <summary>
/// Start/stop Play state monitoring
/// </summary>
public bool StartPlayStateMonitoring(bool enable = true)
{
isMonitoringPlayState = enable;
if (enable)
{
BroadcastEvent(new EventData
{
type = "play_state_monitoring",
category = "monitoring",
timestamp = DateTime.Now,
description = "Play state monitoring started",
data = new Dictionary<string, object>
{
#if UNITY_EDITOR
["current_state"] = EditorApplication.isPlaying ? "playing" : "stopped",
["is_paused"] = EditorApplication.isPaused
#else
["current_state"] = Application.isPlaying ? "playing" : "stopped",
["is_paused"] = false
#endif
}
});
}
return true;
}
#if UNITY_EDITOR
private void OnPlayModeStateChanged(PlayModeStateChange state)
{
if (!isMonitoringPlayState) return;
var eventData = new EventData
{
type = "play_state_changed",
category = "editor",
timestamp = DateTime.Now,
description = $"Play mode changed to {state}",
data = new Dictionary<string, object>
{
["previous_state"] = lastPlayState.ToString(),
["current_state"] = state.ToString(),
["is_playing"] = EditorApplication.isPlaying,
["is_paused"] = EditorApplication.isPaused,
["frame_count"] = Time.frameCount
}
};
BroadcastEvent(eventData);
lastPlayState = state;
}
#endif
#endregion
#region File Change Monitoring
private FileSystemWatcher fileWatcher;
private void InitializeFileWatcher()
{
try
{
string projectPath = Directory.GetParent(Application.dataPath).FullName;
fileWatcher = new FileSystemWatcher(projectPath)
{
IncludeSubdirectories = true,
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName
};
fileWatcher.Changed += OnFileChanged;
fileWatcher.Created += OnFileCreated;
fileWatcher.Deleted += OnFileDeleted;
fileWatcher.Renamed += OnFileRenamed;
}
catch (Exception e)
{
Debug.LogError($"[Nexus Event Monitor] Failed to initialize file watcher: {e.Message}");
}
}
/// <summary>
/// Start/stop file change monitoring
/// </summary>
public bool StartFileChangeMonitoring(bool enable = true)
{
isMonitoringFileChanges = enable;
if (fileWatcher != null)
{
fileWatcher.EnableRaisingEvents = enable;
}
if (enable)
{
BroadcastEvent(new EventData
{
type = "file_monitoring",
category = "monitoring",
timestamp = DateTime.Now,
description = "File change monitoring started",
data = new Dictionary<string, object>
{
["monitored_extensions"] = monitoredFileExtensions,
["project_path"] = Directory.GetParent(Application.dataPath).FullName
}
});
}
return true;
}
private void OnFileChanged(object sender, FileSystemEventArgs e)
{
if (!ShouldMonitorFile(e.FullPath)) return;
var eventData = new EventData
{
type = "file_changed",
category = "filesystem",
timestamp = DateTime.Now,
description = $"File modified: {Path.GetFileName(e.FullPath)}",
data = new Dictionary<string, object>
{
["file_path"] = e.FullPath,
["file_name"] = Path.GetFileName(e.FullPath),
["extension"] = Path.GetExtension(e.FullPath),
["relative_path"] = GetRelativePath(e.FullPath)
}
};
BroadcastEvent(eventData);
}
private void OnFileCreated(object sender, FileSystemEventArgs e)
{
if (!ShouldMonitorFile(e.FullPath)) return;
BroadcastEvent(new EventData
{
type = "file_created",
category = "filesystem",
timestamp = DateTime.Now,
description = $"File created: {Path.GetFileName(e.FullPath)}",
data = new Dictionary<string, object>
{
["file_path"] = e.FullPath,
["file_name"] = Path.GetFileName(e.FullPath),
["extension"] = Path.GetExtension(e.FullPath),
["relative_path"] = GetRelativePath(e.FullPath)
}
});
}
private void OnFileDeleted(object sender, FileSystemEventArgs e)
{
if (!ShouldMonitorFile(e.FullPath)) return;
BroadcastEvent(new EventData
{
type = "file_deleted",
category = "filesystem",
timestamp = DateTime.Now,
description = $"File deleted: {Path.GetFileName(e.FullPath)}",
data = new Dictionary<string, object>
{
["file_path"] = e.FullPath,
["file_name"] = Path.GetFileName(e.FullPath),
["extension"] = Path.GetExtension(e.FullPath),
["relative_path"] = GetRelativePath(e.FullPath)
}
});
}
private void OnFileRenamed(object sender, RenamedEventArgs e)
{
if (!ShouldMonitorFile(e.FullPath)) return;
BroadcastEvent(new EventData
{
type = "file_renamed",
category = "filesystem",
timestamp = DateTime.Now,
description = $"File renamed: {Path.GetFileName(e.OldFullPath)} → {Path.GetFileName(e.FullPath)}",
data = new Dictionary<string, object>
{
["old_path"] = e.OldFullPath,
["new_path"] = e.FullPath,
["old_name"] = Path.GetFileName(e.OldFullPath),
["new_name"] = Path.GetFileName(e.FullPath),
["relative_path"] = GetRelativePath(e.FullPath)
}
});
}
private bool ShouldMonitorFile(string filePath)
{
if (!isMonitoringFileChanges) return false;
string extension = Path.GetExtension(filePath).ToLower();
return monitoredFileExtensions.Contains(extension);
}
private string GetRelativePath(string fullPath)
{
string projectPath = Directory.GetParent(Application.dataPath).FullName;
if (fullPath.StartsWith(projectPath))
{
return fullPath.Substring(projectPath.Length + 1).Replace('\\', '/');
}
return fullPath;
}
#endregion
#region Compilation Monitoring
/// <summary>
/// Start/stop compilation state monitoring
/// </summary>
public bool StartCompileMonitoring(bool enable = true)
{
isMonitoringCompile = enable;
if (enable)
{
BroadcastEvent(new EventData
{
type = "compile_monitoring",
category = "monitoring",
timestamp = DateTime.Now,
description = "Compilation monitoring started",
data = new Dictionary<string, object>
{
#if UNITY_EDITOR
["is_compiling"] = EditorApplication.isCompiling
#else
["is_compiling"] = false
#endif
}
});
}
return true;
}
#if UNITY_EDITOR && UNITY_2019_1_OR_NEWER
private void OnCompilationStarted(object context)
{
if (!isMonitoringCompile) return;
BroadcastEvent(new EventData
{
type = "compilation_started",
category = "compiler",
timestamp = DateTime.Now,
description = "Script compilation started",
data = new Dictionary<string, object>
{
["context"] = context?.ToString(),
["assemblies_building"] = CompilationPipeline.GetAssemblies().Length
}
});
}
private void OnCompilationFinished(object context)
{
if (!isMonitoringCompile) return;
BroadcastEvent(new EventData
{
type = "compilation_finished",
category = "compiler",
timestamp = DateTime.Now,
description = "Script compilation finished",
data = new Dictionary<string, object>
{
["context"] = context?.ToString(),
["has_errors"] = EditorApplication.isCompiling == false && EditorUtility.scriptCompilationFailed
}
});
}
#endif
#endregion
#region Custom Event Subscription
/// <summary>
/// Subscribe to custom event
/// </summary>
public bool SubscribeToEvent(string eventType, string subscriberId)
{
if (!eventSubscriptions.ContainsKey(eventType))
{
eventSubscriptions[eventType] = new HashSet<string>();
}
eventSubscriptions[eventType].Add(subscriberId);
BroadcastEvent(new EventData
{
type = "event_subscription",
category = "monitoring",
timestamp = DateTime.Now,
description = $"Subscribed to event: {eventType}",
data = new Dictionary<string, object>
{
["event_type"] = eventType,
["subscriber_id"] = subscriberId,
["total_subscribers"] = eventSubscriptions[eventType].Count
}
});
return true;
}
/// <summary>
/// Unsubscribe from custom event
/// </summary>
public bool UnsubscribeFromEvent(string eventType, string subscriberId)
{
if (eventSubscriptions.ContainsKey(eventType))
{
eventSubscriptions[eventType].Remove(subscriberId);
if (eventSubscriptions[eventType].Count == 0)
{
eventSubscriptions.Remove(eventType);
}
return true;
}
return false;
}
/// <summary>
/// Fire custom event
/// </summary>
public void TriggerCustomEvent(string eventType, Dictionary<string, object> data, string description = "")
{
var eventData = new EventData
{
type = eventType,
category = "custom",
timestamp = DateTime.Now,
description = description.Length > 0 ? description : $"Custom event: {eventType}",
data = data ?? new Dictionary<string, object>()
};
BroadcastEvent(eventData);
}
#endregion
#region Event Broadcasting
private void BroadcastEvent(EventData eventData)
{
// Add to event queue
eventQueue.Enqueue(eventData);
while (eventQueue.Count > maxEventQueueSize)
{
eventQueue.Dequeue();
}
// Notify external event handlers
OnEventDetected?.Invoke(eventData);
// Send to MCP Client
if (NexusMCPClient.Instance != null)
{
var mcpMessage = new
{
type = "unity_event",
event_type = eventData.type,
category = eventData.category,
timestamp = eventData.timestamp.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"),
description = eventData.description,
data = eventData.data
};
// Send to MCP asynchronously
if (UnityMainThreadDispatcher.Exists())
{
UnityMainThreadDispatcher.Instance().Enqueue(() =>
{
NexusMCPClient.Instance.SendMessage(Newtonsoft.Json.JsonConvert.SerializeObject(mcpMessage));
});
}
}
Debug.Log($"[Nexus Event Monitor] {eventData.type}: {eventData.description}");
}
/// <summary>
/// Get recent event history
/// </summary>
public string GetRecentEvents(int count = 10)
{
var recentEvents = eventQueue.TakeLast(count).Reverse();
var result = new System.Text.StringBuilder();
result.AppendLine($"📊 Recent Events (Last {count}):");
foreach (var evt in recentEvents)
{
result.AppendLine($" [{evt.timestamp:HH:mm:ss}] {evt.category}/{evt.type}: {evt.description}");
}
return result.ToString();
}
/// <summary>
/// Get monitoring state
/// </summary>
public string GetMonitoringStatus()
{
return $@"📡 Nexus Event Monitor Status:
Play State Monitoring: {(isMonitoringPlayState ? "🟢 Active" : "🔴 Inactive")}
File Change Monitoring: {(isMonitoringFileChanges ? "🟢 Active" : "🔴 Inactive")}
Compile Monitoring: {(isMonitoringCompile ? "🟢 Active" : "🔴 Inactive")}
Custom Events: {eventSubscriptions.Count} subscriptions
Recent Events: {eventQueue.Count} in queue";
}
#endregion
#region GUI
#if UNITY_EDITOR
void OnGUI()
{
GUILayout.Label("Nexus Event Monitor", EditorStyles.boldLabel);
EditorGUILayout.Space();
// Display monitoring state
isMonitoringPlayState = EditorGUILayout.Toggle("Monitor Play State", isMonitoringPlayState);
isMonitoringFileChanges = EditorGUILayout.Toggle("Monitor File Changes", isMonitoringFileChanges);
isMonitoringCompile = EditorGUILayout.Toggle("Monitor Compilation", isMonitoringCompile);
EditorGUILayout.Space();
// Statistics information
EditorGUILayout.LabelField("Event Queue Size", eventQueue.Count.ToString());
EditorGUILayout.LabelField("Active Subscriptions", eventSubscriptions.Count.ToString());
EditorGUILayout.Space();
if (GUILayout.Button("Clear Event Queue"))
{
eventQueue.Clear();
}
if (GUILayout.Button("Test Custom Event"))
{
TriggerCustomEvent("test_event", new Dictionary<string, object>
{
["test_data"] = "Hello from Nexus!",
["timestamp"] = DateTime.Now
}, "Manual test event triggered");
}
}
#endif
#endregion
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 2f808c91c637e46edbc1587c1350d4cd
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/NexusEventMonitor.cs
uploadId: 920982
@@ -0,0 +1,883 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using Newtonsoft.Json;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace SynapticPro
{
/// <summary>
/// Nexus MCP Client - Connects to MCP server and handles AI communication
/// Supports multiple AI providers through MCP protocol
/// </summary>
public class NexusMCPClient : MonoBehaviour
{
private static NexusMCPClient instance;
public static NexusMCPClient Instance
{
get
{
if (instance == null)
{
if (Application.isPlaying)
{
// Runtime only: Create GameObject only in PlayMode
var go = new GameObject("NexusMCPClient_Runtime");
instance = go.AddComponent<NexusMCPClient>();
DontDestroyOnLoad(go);
}
else
{
// Return null in Editor mode (use NexusEditorMCPService)
return null;
}
}
return instance;
}
}
private ClientWebSocket webSocket;
private Queue<MCPMessage> messageQueue = new Queue<MCPMessage>();
private bool isConnected = false;
private string serverUrl = "ws://localhost:8090";
public event Action<string> OnMessageReceived;
public event Action OnConnected;
public event Action OnDisconnected;
public event Action<string> OnError;
[Serializable]
public class MCPMessage
{
public string type;
public string id;
public string provider;
public string content;
public Dictionary<string, object> parameters;
public string tool;
public string command;
public object data;
}
[Serializable]
public class MCPResponse
{
public string id;
public bool success;
public string content;
public string error;
public string provider;
public int tokensUsed;
}
private void Start()
{
Debug.Log($"[Nexus MCP] Starting MCP Client: {gameObject.name}");
// Monitor Play mode and Editor mode switching
#if UNITY_EDITOR
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
#endif
_ = Task.Run(async () => await ConnectToMCPServer());
}
private void OnDestroy()
{
Debug.Log($"[Nexus MCP] Destroying MCP Client: {gameObject.name}");
#if UNITY_EDITOR
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
#endif
DisconnectFromMCPServer();
// Clear instance
if (instance == this)
{
instance = null;
}
}
#if UNITY_EDITOR
private void OnPlayModeStateChanged(PlayModeStateChange state)
{
// Reconnect when switching between Play mode and Editor mode
switch (state)
{
case PlayModeStateChange.EnteredPlayMode:
case PlayModeStateChange.EnteredEditMode:
if (!isConnected)
{
Debug.Log($"[Nexus MCP] Reconnecting due to play mode change: {state}");
_ = Task.Run(async () => await ConnectToMCPServer());
}
break;
}
}
#endif
public async Task ConnectToMCPServer()
{
try
{
webSocket = new ClientWebSocket();
await webSocket.ConnectAsync(new Uri(serverUrl), CancellationToken.None);
isConnected = true;
OnConnected?.Invoke();
Debug.Log("[Nexus MCP] Connected to MCP Server");
// Start listening for messages
_ = Task.Run(async () => await ListenForMessages());
}
catch (Exception e)
{
Debug.LogError($"[Nexus MCP] Failed to connect: {e.Message}");
OnError?.Invoke(e.Message);
isConnected = false;
}
}
private async Task ListenForMessages()
{
Debug.Log("[Nexus MCP] Starting message listener");
var buffer = new byte[1024 * 4];
while (webSocket.State == WebSocketState.Open)
{
try
{
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Text)
{
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
// Queue message for main thread processing
var mcpMessage = JsonConvert.DeserializeObject<MCPMessage>(message);
messageQueue.Enqueue(mcpMessage);
}
}
catch (Exception e)
{
Debug.LogError($"[Nexus MCP] Listen error: {e.Message}");
break;
}
}
isConnected = false;
OnDisconnected?.Invoke();
}
private void Update()
{
// Process queued messages on main thread
if (messageQueue.Count > 0)
{
Debug.Log($"[Nexus MCP] Processing {messageQueue.Count} queued messages");
while (messageQueue.Count > 0)
{
var message = messageQueue.Dequeue();
ProcessMessage(message);
}
}
}
private void ProcessMessage(MCPMessage message)
{
Debug.Log($"[Nexus MCP] Processing message type: {message.type}, tool: {message.tool}, command: {message.command}");
switch (message.type)
{
case "unity_operation":
ExecuteUnityOperation(message);
break;
case "ai_response":
OnMessageReceived?.Invoke(message.content);
break;
case "tool_call":
HandleToolCall(message);
break;
default:
Debug.Log($"[Nexus MCP] Unknown message type: {message.type}");
break;
}
}
public async Task<string> SendChatMessage(string message, string provider = "claude")
{
if (!isConnected)
{
await ConnectToMCPServer();
if (!isConnected)
return "Failed to connect to MCP server";
}
var mcpMessage = new MCPMessage
{
type = "chat",
id = Guid.NewGuid().ToString(),
provider = provider,
content = message,
parameters = new Dictionary<string, object>()
};
try
{
var json = JsonConvert.SerializeObject(mcpMessage);
var buffer = Encoding.UTF8.GetBytes(json);
await webSocket.SendAsync(
new ArraySegment<byte>(buffer),
WebSocketMessageType.Text,
true,
CancellationToken.None
);
// Wait for response (simplified - in practice you'd use proper async pattern)
return await WaitForResponse(mcpMessage.id);
}
catch (Exception e)
{
Debug.LogError($"[Nexus MCP] Send error: {e.Message}");
return $"Error: {e.Message}";
}
}
public async Task<bool> ExecuteUnityTool(string toolName, Dictionary<string, object> parameters)
{
if (!isConnected)
return false;
var mcpMessage = new MCPMessage
{
type = "tool_call",
id = Guid.NewGuid().ToString(),
tool = toolName,
parameters = parameters
};
try
{
var json = JsonConvert.SerializeObject(mcpMessage);
var buffer = Encoding.UTF8.GetBytes(json);
await webSocket.SendAsync(
new ArraySegment<byte>(buffer),
WebSocketMessageType.Text,
true,
CancellationToken.None
);
return true;
}
catch (Exception e)
{
Debug.LogError($"[Nexus MCP] Tool call error: {e.Message}");
return false;
}
}
private async Task<string> WaitForResponse(string messageId)
{
// Simplified response waiting - in practice use proper async/await pattern
float timeout = 30f;
string response = null;
System.Action<string> responseHandler = (content) => response = content;
OnMessageReceived += responseHandler;
while (timeout > 0 && response == null)
{
await Task.Delay(100);
timeout -= 0.1f;
}
OnMessageReceived -= responseHandler;
return response ?? "Timeout waiting for response";
}
private void ExecuteUnityOperation(MCPMessage message)
{
Debug.Log($"[Nexus MCP] Executing Unity operation: {message.tool} with command: {message.command}");
// Must be executed on Unity main thread
if (UnityMainThreadDispatcher.Exists())
{
UnityMainThreadDispatcher.Instance().Enqueue(() => {
ExecuteUnityOperationOnMainThread(message);
});
}
else
{
// Execute directly if main thread dispatcher not available (risky)
ExecuteUnityOperationOnMainThread(message);
}
}
private void ExecuteUnityOperationOnMainThread(MCPMessage message)
{
try
{
Debug.Log($"[Nexus MCP] Executing on main thread: {message.tool}");
// Map MCP tool names to Unity operations
string operationType = message.command ?? message.tool ?? "";
// Convert tool name to existing operation type
operationType = ConvertMCPToolToOperation(operationType);
Debug.Log($"[Nexus MCP] Converted operation type: {operationType}");
var operation = new NexusUnityOperation
{
type = operationType,
parameters = new Dictionary<string, string>()
};
// Convert parameters
if (message.parameters != null)
{
foreach (var kvp in message.parameters)
{
if (kvp.Value != null)
{
// Handle nested objects
if (kvp.Value is Dictionary<string, object> dict)
{
// Handle structures like Vector3
if (dict.ContainsKey("x") && dict.ContainsKey("y") && dict.ContainsKey("z"))
{
operation.parameters[kvp.Key] = $"{dict["x"]},{dict["y"]},{dict["z"]}";
}
else if (dict.ContainsKey("x") && dict.ContainsKey("y"))
{
operation.parameters[kvp.Key] = $"{dict["x"]},{dict["y"]}";
}
else if (dict.ContainsKey("r") && dict.ContainsKey("g") && dict.ContainsKey("b"))
{
operation.parameters[kvp.Key] = $"{dict["r"]},{dict["g"]},{dict["b"]}";
}
else
{
// Save other objects as JSON strings
operation.parameters[kvp.Key] = JsonConvert.SerializeObject(dict);
}
}
else
{
operation.parameters[kvp.Key] = kvp.Value.ToString();
}
}
}
}
Debug.Log($"[Nexus MCP] About to execute operation with parameters: {operation.parameters.Count}");
foreach (var param in operation.parameters)
{
Debug.Log($"[Nexus MCP] Parameter: {param.Key} = {param.Value}");
}
// Execute Unity operation (already running on main thread)
string result;
bool success;
#if UNITY_EDITOR
// Only executable in Editor
try
{
// Use reflection to call Executor in Editor assembly
var executorType = System.Type.GetType("SynapticPro.NexusUnityExecutor, Synaptic.MCP.Unity.Editor");
if (executorType == null)
{
result = "Error: NexusUnityExecutor not found in Editor assembly";
success = false;
}
else
{
var executor = Activator.CreateInstance(executorType);
var executeMethod = executorType.GetMethod("ExecuteOperation");
if (executeMethod == null)
{
result = "Error: ExecuteOperation method not found";
success = false;
}
else
{
var task = (Task<string>)executeMethod.Invoke(executor, new object[] { operation });
result = task.Result;
success = !result.StartsWith("Error:") && !result.StartsWith("Failed:") && !result.Contains("Tool execution failed");
}
}
}
catch (Exception ex)
{
result = $"Exception during execution: {ex.Message}";
success = false;
Debug.LogError($"[Nexus MCP] Exception: {ex}");
}
#else
// Not executable at runtime
result = "MCP operations are only available in Unity Editor";
success = false;
#endif
Debug.Log($"[Nexus MCP] Operation result: {result}");
Debug.Log($"[Nexus MCP] Operation success: {success}");
// Send result to MCP server
_ = SendOperationResult(message.id, success, result);
// Output result to log
if (success)
{
Debug.Log($"[Nexus MCP] SUCCESS: {result}");
}
else
{
Debug.LogError($"[Nexus MCP] FAILED: {result}");
}
}
catch (Exception e)
{
Debug.LogError($"[Nexus MCP] Unity operation error: {e.Message}");
}
}
private void HandleToolCall(MCPMessage message)
{
// Handle all unity_* tools uniformly
if (message.tool.StartsWith("unity_"))
{
ExecuteUnityOperation(message);
}
else
{
Debug.LogWarning($"[Nexus MCP] Unknown tool: {message.tool}");
}
}
public bool IsConnected => IsConnectedToServer();
public void SetServerUrl(string url)
{
serverUrl = url;
}
// Unity-specific MCP tools
public async Task<bool> CreateGameObject(string name, Vector3 position = default)
{
var parameters = new Dictionary<string, object>
{
["type"] = "CREATE_GAMEOBJECT",
["parameters"] = new Dictionary<string, object>
{
["name"] = name,
["position"] = $"{position.x},{position.y},{position.z}"
}
};
return await ExecuteUnityTool("unity_create", parameters);
}
public async Task<bool> AddComponent(string targetName, string componentType)
{
var parameters = new Dictionary<string, object>
{
["type"] = "ADD_COMPONENT",
["parameters"] = new Dictionary<string, object>
{
["target"] = targetName,
["type"] = componentType
}
};
return await ExecuteUnityTool("unity_create", parameters);
}
public async Task<bool> CreateUI(string uiType, string name, Dictionary<string, string> properties = null)
{
var parameters = new Dictionary<string, object>
{
["type"] = "CREATE_UI",
["parameters"] = new Dictionary<string, object>
{
["type"] = uiType,
["name"] = name
}
};
if (properties != null)
{
foreach (var kvp in properties)
{
((Dictionary<string, object>)parameters["parameters"])[kvp.Key] = kvp.Value;
}
}
return await ExecuteUnityTool("unity_create", parameters);
}
private async Task SendOperationResult(string messageId, bool success, string result)
{
object structuredData = null;
string displayContent = result;
// Attempt to parse JSON and send as structured data
try
{
// If result is JSON, send as structured data
if (result.TrimStart().StartsWith("{") || result.TrimStart().StartsWith("["))
{
structuredData = JsonConvert.DeserializeObject(result);
displayContent = success ? "Retrieved structured data" : result;
}
}
catch (Exception e)
{
Debug.LogWarning($"[Nexus MCP] JSON parse failed: {e.Message}");
}
// Store result in content field according to MCP protocol
var response = new MCPMessage
{
type = "operation_result",
id = messageId,
content = result, // Return original result (JSON string) as is
data = new { success = success }
};
try
{
var json = JsonConvert.SerializeObject(response, Formatting.Indented);
var buffer = Encoding.UTF8.GetBytes(json);
await webSocket.SendAsync(
new ArraySegment<byte>(buffer),
WebSocketMessageType.Text,
true,
CancellationToken.None
);
Debug.Log($"[Nexus MCP] Sent operation result: {success}");
Debug.Log($"[Nexus MCP] Response JSON: {json}");
}
catch (Exception e)
{
Debug.LogError($"[Nexus MCP] Failed to send operation result: {e.Message}");
}
}
private string ConvertMCPToolToOperation(string mcpTool)
{
switch (mcpTool)
{
// GameObject operations
case "unity_create_gameobject":
case "create_gameobject":
return "CREATE_GAMEOBJECT";
case "unity_update_gameobject":
case "update_gameobject":
return "SET_PROPERTY";
case "unity_delete_gameobject":
case "delete_gameobject":
return "DELETE_GAMEOBJECT";
case "unity_set_transform":
case "set_transform":
return "SET_PROPERTY";
// Components
case "unity_add_component":
case "add_component":
return "ADD_COMPONENT";
case "unity_update_component":
case "update_component":
return "UPDATE_COMPONENT";
// Package management
case "unity_list_packages":
case "list_packages":
return "LIST_PACKAGES";
case "unity_install_package":
case "install_package":
return "INSTALL_PACKAGE";
case "unity_remove_package":
case "remove_package":
return "REMOVE_PACKAGE";
case "unity_check_package":
case "check_package":
return "CHECK_PACKAGE";
// UI
case "unity_create_ui":
case "create_ui":
return "CREATE_UI";
// Terrain
case "unity_create_terrain":
case "create_terrain":
return "CREATE_TERRAIN";
case "unity_modify_terrain":
case "modify_terrain":
return "MODIFY_TERRAIN";
// Camera
case "unity_setup_camera":
case "setup_camera":
return "SETUP_CAMERA";
// Cinemachine
case "unity_create_virtual_camera":
case "create_virtual_camera":
return "CREATE_VIRTUAL_CAMERA";
case "unity_create_freelook_camera":
case "create_freelook_camera":
return "CREATE_FREELOOK_CAMERA";
case "unity_setup_cinemachine_brain":
case "setup_cinemachine_brain":
return "SETUP_CINEMACHINE_BRAIN";
case "unity_update_virtual_camera":
case "update_virtual_camera":
return "UPDATE_VIRTUAL_CAMERA";
case "unity_create_dolly_track":
case "create_dolly_track":
return "CREATE_DOLLY_TRACK";
case "unity_add_collider_extension":
case "add_collider_extension":
return "ADD_COLLIDER_EXTENSION";
case "unity_add_confiner_extension":
case "add_confiner_extension":
return "ADD_CONFINER_EXTENSION";
case "unity_create_state_driven_camera":
case "create_state_driven_camera":
return "CREATE_STATE_DRIVEN_CAMERA";
case "unity_create_clear_shot_camera":
case "create_clear_shot_camera":
return "CREATE_CLEAR_SHOT_CAMERA";
case "unity_create_impulse_source":
case "create_impulse_source":
return "CREATE_IMPULSE_SOURCE";
case "unity_add_impulse_listener":
case "add_impulse_listener":
return "ADD_IMPULSE_LISTENER";
case "unity_create_blend_list_camera":
case "create_blend_list_camera":
return "CREATE_BLEND_LIST_CAMERA";
case "unity_create_target_group":
case "create_target_group":
return "CREATE_TARGET_GROUP";
case "unity_add_target_to_group":
case "add_target_to_group":
return "ADD_TARGET_TO_GROUP";
case "unity_set_camera_priority":
case "set_camera_priority":
return "SET_CAMERA_PRIORITY";
case "unity_set_camera_enabled":
case "set_camera_enabled":
return "SET_CAMERA_ENABLED";
case "unity_create_mixing_camera":
case "create_mixing_camera":
return "CREATE_MIXING_CAMERA";
case "unity_update_camera_target":
case "update_camera_target":
return "UPDATE_CAMERA_TARGET";
case "unity_update_brain_blend_settings":
case "update_brain_blend_settings":
return "UPDATE_BRAIN_BLEND_SETTINGS";
case "unity_get_active_camera_info":
case "get_active_camera_info":
return "GET_ACTIVE_CAMERA_INFO";
// Placement
case "unity_place_objects":
case "place_objects":
return "PLACE_OBJECTS";
// Lighting
case "unity_setup_lighting":
case "setup_lighting":
return "SETUP_LIGHTING";
// Material
case "unity_create_material":
case "create_material":
return "CREATE_MATERIAL";
// Prefab
case "unity_create_prefab":
case "create_prefab":
return "CREATE_PREFAB";
// Script
case "unity_create_script":
case "create_script":
return "CREATE_SCRIPT";
// Scene
case "unity_manage_scene":
case "manage_scene":
return "MANAGE_SCENE";
// Animation
case "unity_create_animation":
case "create_animation":
return "CREATE_ANIMATION";
// Physics
case "unity_setup_physics":
case "setup_physics":
return "SETUP_PHYSICS";
// Other
case "unity_search":
case "search_objects":
return "SEARCH_OBJECTS";
case "unity_console":
case "console_operation":
return "CONSOLE_OPERATION";
// Operation history / Undo/Redo
case "unity_get_operation_history":
return "GET_OPERATION_HISTORY";
case "unity_undo_operation":
return "UNDO_OPERATION";
case "unity_redo_operation":
return "REDO_OPERATION";
case "unity_create_checkpoint":
return "CREATE_CHECKPOINT";
case "unity_restore_checkpoint":
return "RESTORE_CHECKPOINT";
// Real-time event monitoring
case "unity_monitor_play_state":
return "MONITOR_PLAY_STATE";
case "unity_monitor_file_changes":
return "MONITOR_FILE_CHANGES";
case "unity_monitor_compile":
return "MONITOR_COMPILE";
case "unity_subscribe_events":
return "SUBSCRIBE_EVENTS";
case "unity_get_events":
return "GET_EVENTS";
case "unity_get_monitoring_status":
return "GET_MONITORING_STATUS";
// Project settings
case "unity_get_build_settings":
return "GET_BUILD_SETTINGS";
case "unity_get_player_settings":
return "GET_PLAYER_SETTINGS";
case "unity_get_quality_settings":
return "GET_QUALITY_SETTINGS";
case "unity_get_input_settings":
return "GET_INPUT_SETTINGS";
case "unity_get_physics_settings":
return "GET_PHYSICS_SETTINGS";
case "unity_get_project_summary":
return "GET_PROJECT_SUMMARY";
default:
// Strip unity_ prefix if present and convert to uppercase
if (mcpTool.StartsWith("unity_"))
return mcpTool.Substring(6).ToUpper();
return mcpTool.ToUpper();
}
}
/// <summary>
/// Disconnect from MCP server
/// </summary>
public void DisconnectFromMCPServer()
{
try
{
if (webSocket != null && isConnected)
{
isConnected = false;
webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Disconnecting", CancellationToken.None);
webSocket.Dispose();
webSocket = null;
OnDisconnected?.Invoke();
Debug.Log("[Nexus MCP] Disconnected from MCP Server");
}
}
catch (Exception e)
{
Debug.LogError($"[Nexus MCP] Error during disconnect: {e.Message}");
}
}
/// <summary>
/// Check connection status
/// </summary>
public bool IsConnectedToServer()
{
return isConnected && webSocket != null && webSocket.State == WebSocketState.Open;
}
/// <summary>
/// Retry connection
/// </summary>
public async void ReconnectToMCPServer()
{
DisconnectFromMCPServer();
await Task.Delay(1000); // Wait 1 second
await ConnectToMCPServer();
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: ca10f7dfd72d44babac35f8a775c32c7
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/NexusMCPClient.cs
uploadId: 920982
@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
namespace SynapticPro
{
/// <summary>
/// Data class representing Unity operations
/// Used for integration with MCP server and AI
/// </summary>
[Serializable]
public class NexusUnityOperation
{
public string id;
public string type;
public Dictionary<string, string> parameters;
public string code;
public string description;
public List<string> dependencies;
public OperationStatus status;
public enum OperationStatus
{
Pending,
Executing,
Completed,
Failed,
Skipped
}
public NexusUnityOperation()
{
id = Guid.NewGuid().ToString();
parameters = new Dictionary<string, string>();
dependencies = new List<string>();
status = OperationStatus.Pending;
}
public NexusUnityOperation(string operationType) : this()
{
type = operationType;
}
}
/// <summary>
/// Class representing operation results
/// </summary>
[Serializable]
public class NexusOperationResult
{
public bool success;
public string message;
public string operationId;
public object resultData;
public NexusOperationResult(string opId, bool isSuccess, string msg)
{
operationId = opId;
success = isSuccess;
message = msg;
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 4b7ad5776c96849b48c660e722897bb6
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/NexusOperation.cs
uploadId: 920982
@@ -0,0 +1,498 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Newtonsoft.Json;
namespace SynapticPro
{
/// <summary>
/// History management and Undo/Redo functionality for Unity operations
/// </summary>
public class NexusOperationHistory
{
private static NexusOperationHistory instance;
public static NexusOperationHistory Instance
{
get
{
if (instance == null)
{
instance = new NexusOperationHistory();
}
return instance;
}
}
private Stack<OperationRecord> undoStack = new Stack<OperationRecord>();
private Stack<OperationRecord> redoStack = new Stack<OperationRecord>();
private const int maxHistorySize = 50;
public event Action OnHistoryChanged;
[Serializable]
public class OperationRecord
{
public string id;
public string operationType;
public string description;
public DateTime timestamp;
public Dictionary<string, object> parameters;
public OperationState beforeState;
public OperationState afterState;
public bool canUndo;
public string error;
}
[Serializable]
public class OperationState
{
public List<GameObjectState> gameObjects = new List<GameObjectState>();
public Dictionary<string, string> globalState = new Dictionary<string, string>();
}
[Serializable]
public class GameObjectState
{
public string name;
public string path;
public bool active;
public Vector3 position;
public Quaternion rotation;
public Vector3 scale;
public List<ComponentState> components = new List<ComponentState>();
}
[Serializable]
public class ComponentState
{
public string type;
public Dictionary<string, object> properties = new Dictionary<string, object>();
}
/// <summary>
/// Record operation
/// </summary>
public void RecordOperation(string operationType, string description,
Dictionary<string, object> parameters, Action operation)
{
var record = new OperationRecord
{
id = Guid.NewGuid().ToString(),
operationType = operationType,
description = description,
timestamp = DateTime.Now,
parameters = parameters,
canUndo = true
};
// Capture state before operation
record.beforeState = CaptureState(parameters);
try
{
// Execute operation
operation?.Invoke();
// Capture state after operation
record.afterState = CaptureState(parameters);
// Add to history
AddToHistory(record);
Debug.Log($"[Operation History] Recorded: {description}");
}
catch (Exception e)
{
record.error = e.Message;
record.canUndo = false;
Debug.LogError($"[Operation History] Operation failed: {e.Message}");
throw;
}
}
/// <summary>
/// Capture current state
/// </summary>
private OperationState CaptureState(Dictionary<string, object> parameters)
{
var state = new OperationState();
// Identify target GameObject
if (parameters != null && parameters.ContainsKey("target"))
{
var targetName = parameters["target"].ToString();
var target = GameObject.Find(targetName);
if (target != null)
{
state.gameObjects.Add(CaptureGameObjectState(target));
}
}
// For newly created objects
if (parameters != null && parameters.ContainsKey("name"))
{
var name = parameters["name"].ToString();
var obj = GameObject.Find(name);
if (obj != null)
{
state.gameObjects.Add(CaptureGameObjectState(obj));
}
}
// Global state (scene settings, etc.)
state.globalState["activeScene"] = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name;
return state;
}
/// <summary>
/// Capture GameObject state
/// </summary>
private GameObjectState CaptureGameObjectState(GameObject obj)
{
var state = new GameObjectState
{
name = obj.name,
path = GetFullPath(obj),
active = obj.activeSelf,
position = obj.transform.position,
rotation = obj.transform.rotation,
scale = obj.transform.localScale
};
// Component state
foreach (var component in obj.GetComponents<Component>())
{
if (component == null || component is Transform) continue;
var compState = new ComponentState
{
type = component.GetType().FullName
};
// Save properties of major components
if (component is Rigidbody rb)
{
compState.properties["mass"] = rb.mass;
compState.properties["useGravity"] = rb.useGravity;
compState.properties["isKinematic"] = rb.isKinematic;
}
else if (component is Collider col)
{
compState.properties["isTrigger"] = col.isTrigger;
compState.properties["enabled"] = col.enabled;
}
else if (component is Renderer rend)
{
compState.properties["enabled"] = rend.enabled;
compState.properties["materialCount"] = rend.sharedMaterials.Length;
}
state.components.Add(compState);
}
return state;
}
/// <summary>
/// Add to history
/// </summary>
private void AddToHistory(OperationRecord record)
{
undoStack.Push(record);
redoStack.Clear(); // Clear the Redo stack when a new operation is added
// Delete old records if maximum history size is exceeded
while (undoStack.Count > maxHistorySize)
{
var oldRecords = undoStack.ToArray();
undoStack.Clear();
for (int i = 1; i < oldRecords.Length; i++)
{
undoStack.Push(oldRecords[i]);
}
}
OnHistoryChanged?.Invoke();
}
/// <summary>
/// Execute Undo
/// </summary>
public bool Undo()
{
if (undoStack.Count == 0) return false;
var record = undoStack.Pop();
if (!record.canUndo)
{
Debug.LogWarning($"[Operation History] Cannot undo: {record.description}");
return false;
}
try
{
// Restore state
RestoreState(record.beforeState);
// Add to Redo stack
redoStack.Push(record);
Debug.Log($"[Operation History] Undo: {record.description}");
OnHistoryChanged?.Invoke();
return true;
}
catch (Exception e)
{
Debug.LogError($"[Operation History] Undo failed: {e.Message}");
return false;
}
}
/// <summary>
/// Execute Redo
/// </summary>
public bool Redo()
{
if (redoStack.Count == 0) return false;
var record = redoStack.Pop();
try
{
// Restore state
RestoreState(record.afterState);
// Add to Undo stack
undoStack.Push(record);
Debug.Log($"[Operation History] Redo: {record.description}");
OnHistoryChanged?.Invoke();
return true;
}
catch (Exception e)
{
Debug.LogError($"[Operation History] Redo failed: {e.Message}");
return false;
}
}
/// <summary>
/// Restore state
/// </summary>
private void RestoreState(OperationState state)
{
if (state == null) return;
foreach (var objState in state.gameObjects)
{
var obj = GameObject.Find(objState.path);
if (obj != null)
{
// Restore Transform
obj.transform.position = objState.position;
obj.transform.rotation = objState.rotation;
obj.transform.localScale = objState.scale;
obj.SetActive(objState.active);
// Restore component state
foreach (var compState in objState.components)
{
var component = obj.GetComponent(compState.type);
if (component != null)
{
RestoreComponentState(component, compState.properties);
}
}
}
}
}
/// <summary>
/// Restore component state
/// </summary>
private void RestoreComponentState(Component component, Dictionary<string, object> properties)
{
if (component is Rigidbody rb && properties != null)
{
if (properties.ContainsKey("mass")) rb.mass = Convert.ToSingle(properties["mass"]);
if (properties.ContainsKey("useGravity")) rb.useGravity = Convert.ToBoolean(properties["useGravity"]);
if (properties.ContainsKey("isKinematic")) rb.isKinematic = Convert.ToBoolean(properties["isKinematic"]);
}
else if (component is Collider col && properties != null)
{
if (properties.ContainsKey("isTrigger")) col.isTrigger = Convert.ToBoolean(properties["isTrigger"]);
if (properties.ContainsKey("enabled")) col.enabled = Convert.ToBoolean(properties["enabled"]);
}
else if (component is Renderer rend && properties != null)
{
if (properties.ContainsKey("enabled")) rend.enabled = Convert.ToBoolean(properties["enabled"]);
}
}
/// <summary>
/// Clear history
/// </summary>
public void ClearHistory()
{
undoStack.Clear();
redoStack.Clear();
OnHistoryChanged?.Invoke();
}
/// <summary>
/// Get history information
/// </summary>
public string GetHistoryInfo()
{
var info = new System.Text.StringBuilder();
info.AppendLine($"📝 Operation History");
info.AppendLine($"Undo available: {undoStack.Count} operations");
info.AppendLine($"Redo available: {redoStack.Count} operations");
if (undoStack.Count > 0)
{
info.AppendLine("\nRecent operations:");
var recent = undoStack.ToArray();
for (int i = 0; i < Math.Min(5, recent.Length); i++)
{
var record = recent[i];
info.AppendLine($" - {record.description} ({record.timestamp:HH:mm:ss})");
}
}
return info.ToString();
}
/// <summary>
/// Export history in JSON format
/// </summary>
public string ExportHistory()
{
var history = new
{
undoStack = undoStack.ToArray(),
redoStack = redoStack.ToArray(),
exportTime = DateTime.Now
};
return JsonConvert.SerializeObject(history, Formatting.Indented);
}
private string GetFullPath(GameObject obj)
{
var path = obj.name;
var parent = obj.transform.parent;
while (parent != null)
{
path = parent.name + "/" + path;
parent = parent.parent;
}
return path;
}
public bool CanUndo => undoStack.Count > 0;
public bool CanRedo => redoStack.Count > 0;
// Checkpoint functionality
private Dictionary<string, CheckpointData> checkpoints = new Dictionary<string, CheckpointData>();
[Serializable]
public class CheckpointData
{
public string name;
public string description;
public DateTime timestamp;
public Stack<OperationRecord> undoStackSnapshot;
public Stack<OperationRecord> redoStackSnapshot;
}
/// <summary>
/// Create checkpoint
/// </summary>
public bool CreateCheckpoint(string name, string description = "")
{
try
{
var checkpoint = new CheckpointData
{
name = name,
description = description,
timestamp = DateTime.Now,
undoStackSnapshot = new Stack<OperationRecord>(undoStack.Reverse()),
redoStackSnapshot = new Stack<OperationRecord>(redoStack.Reverse())
};
checkpoints[name] = checkpoint;
Debug.Log($"[Operation History] Checkpoint '{name}' created");
return true;
}
catch (Exception e)
{
Debug.LogError($"[Operation History] Failed to create checkpoint: {e.Message}");
return false;
}
}
/// <summary>
/// Restore from checkpoint
/// </summary>
public bool RestoreCheckpoint(string name)
{
try
{
if (!checkpoints.ContainsKey(name))
{
Debug.LogWarning($"[Operation History] Checkpoint '{name}' not found");
return false;
}
var checkpoint = checkpoints[name];
// Restore stacks
undoStack = new Stack<OperationRecord>(checkpoint.undoStackSnapshot.Reverse());
redoStack = new Stack<OperationRecord>(checkpoint.redoStackSnapshot.Reverse());
OnHistoryChanged?.Invoke();
Debug.Log($"[Operation History] Restored to checkpoint '{name}' ({checkpoint.timestamp})");
return true;
}
catch (Exception e)
{
Debug.LogError($"[Operation History] Failed to restore checkpoint: {e.Message}");
return false;
}
}
/// <summary>
/// Get list of available checkpoints
/// </summary>
public string GetCheckpoints()
{
if (checkpoints.Count == 0)
return "No checkpoints available";
var result = new System.Text.StringBuilder();
result.AppendLine("📍 Available Checkpoints:");
foreach (var kvp in checkpoints.OrderByDescending(x => x.Value.timestamp))
{
var cp = kvp.Value;
result.AppendLine($" - {cp.name} ({cp.timestamp:yyyy-MM-dd HH:mm:ss})");
if (!string.IsNullOrEmpty(cp.description))
result.AppendLine($" {cp.description}");
}
return result.ToString();
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: b997204f24ea74cc1a2c78968bc82c77
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/NexusOperationHistory.cs
uploadId: 920982
@@ -0,0 +1,574 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using UnityEngine;
using UnityEngine.SceneManagement;
#if UNITY_EDITOR
using UnityEditor;
#endif
using System.Reflection;
using System.Linq;
namespace SynapticPro
{
/// <summary>
/// Real-time Execution State Monitoring Class
/// Monitors Unity Editor execution state, performance, and memory usage
/// </summary>
public static class NexusRuntimeMonitor
{
private static float lastFPS = 0f;
private static long lastGCMemory = 0L;
private static DateTime lastUpdateTime = DateTime.Now;
/// <summary>
/// Get Unity execution state information
/// </summary>
public static string GetRuntimeStatus(Dictionary<string, string> parameters)
{
try
{
var includePerformance = parameters.GetValueOrDefault("includePerformance", "true") == "true";
var includeMemory = parameters.GetValueOrDefault("includeMemory", "true") == "true";
var includeErrors = parameters.GetValueOrDefault("includeErrors", "true") == "true";
var status = new Dictionary<string, object>
{
["playMode"] = Application.isPlaying,
["isEditor"] = Application.isEditor,
["isPaused"] = GetEditorPausedState(),
["isCompiling"] = GetEditorCompilingState(),
["platform"] = Application.platform.ToString(),
["unityVersion"] = Application.unityVersion,
["timestamp"] = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
};
if (includePerformance)
{
status["performance"] = GetPerformanceData();
}
if (includeMemory)
{
status["memory"] = GetMemoryData();
}
if (includeErrors)
{
status["errors"] = GetErrorStatus();
}
return FormatStatusReport(status);
}
catch (Exception e)
{
return $"Error getting runtime status: {e.Message}";
}
}
/// <summary>
/// Get performance metrics
/// </summary>
public static string GetPerformanceMetrics(Dictionary<string, string> parameters)
{
try
{
var duration = float.Parse(parameters.GetValueOrDefault("duration", "5"));
var includeFrameTime = parameters.GetValueOrDefault("includeFrameTime", "true") == "true";
var includeGPUUsage = parameters.GetValueOrDefault("includeGPUUsage", "true") == "true";
var includeBatches = parameters.GetValueOrDefault("includeBatches", "true") == "true";
var metrics = new Dictionary<string, object>();
// FPS calculation
if (Application.isPlaying)
{
var currentFPS = 1.0f / Time.unscaledDeltaTime;
lastFPS = currentFPS;
metrics["fps"] = Math.Round(currentFPS, 1);
metrics["deltaTime"] = Math.Round(Time.unscaledDeltaTime * 1000, 2); // ms
}
else
{
metrics["fps"] = "Editor Mode";
metrics["deltaTime"] = "N/A";
}
if (includeFrameTime)
{
metrics["frameTime"] = Application.isPlaying ?
Math.Round(Time.unscaledDeltaTime * 1000, 2) : 0;
metrics["timeScale"] = Time.timeScale;
metrics["fixedDeltaTime"] = Time.fixedDeltaTime;
}
if (includeGPUUsage)
{
// Use Profiler API (Editor only)
if (Application.isEditor)
{
metrics["gpuMemory"] = SystemInfo.graphicsMemorySize + " MB";
metrics["graphicsDevice"] = SystemInfo.graphicsDeviceName;
metrics["graphicsAPI"] = SystemInfo.graphicsDeviceType.ToString();
}
}
if (includeBatches)
{
// Rendering statistics (game runtime only)
if (Application.isPlaying)
{
metrics["triangles"] = "Available in Game View";
metrics["batches"] = "Available in Game View";
}
else
{
metrics["triangles"] = "N/A (Editor Mode)";
metrics["batches"] = "N/A (Editor Mode)";
}
}
// System information
metrics["systemMemory"] = SystemInfo.systemMemorySize + " MB";
metrics["processorCount"] = SystemInfo.processorCount;
metrics["processorType"] = SystemInfo.processorType;
return FormatPerformanceReport(metrics);
}
catch (Exception e)
{
return $"Error getting performance metrics: {e.Message}";
}
}
/// <summary>
/// Get detailed memory usage
/// </summary>
public static string GetMemoryUsage(Dictionary<string, string> parameters)
{
try
{
var includeBreakdown = parameters.GetValueOrDefault("includeBreakdown", "true") == "true";
var includeGC = parameters.GetValueOrDefault("includeGC", "true") == "true";
var includeProfiler = parameters.GetValueOrDefault("includeProfiler", "false") == "true";
var memory = new Dictionary<string, object>();
// Basic memory information
var gcMemory = GC.GetTotalMemory(false);
memory["gcMemory"] = FormatBytes(gcMemory);
memory["gcGeneration0"] = GC.CollectionCount(0);
memory["gcGeneration1"] = GC.CollectionCount(1);
memory["gcGeneration2"] = GC.CollectionCount(2);
if (includeGC)
{
var memoryDiff = gcMemory - lastGCMemory;
memory["memoryDelta"] = FormatBytes(memoryDiff);
lastGCMemory = gcMemory;
}
// Unity-specific memory information
if (Application.isEditor)
{
memory["unityReserved"] = "Available via Profiler";
memory["gfxDriver"] = SystemInfo.graphicsMemorySize + " MB";
}
if (includeBreakdown)
{
// Memory usage by asset type (estimated)
var textures = Resources.FindObjectsOfTypeAll<Texture>();
var meshes = Resources.FindObjectsOfTypeAll<Mesh>();
var audioClips = Resources.FindObjectsOfTypeAll<AudioClip>();
memory["textureCount"] = textures.Length;
memory["meshCount"] = meshes.Length;
memory["audioClipCount"] = audioClips.Length;
// Estimated memory size
long textureMemory = 0;
foreach (var tex in textures)
{
if (tex != null)
{
textureMemory += UnityEngine.Profiling.Profiler.GetRuntimeMemorySizeLong(tex);
}
}
memory["textureMemory"] = FormatBytes(textureMemory);
}
if (includeProfiler && Application.isPlaying)
{
memory["profilerMemory"] = UnityEngine.Profiling.Profiler.GetTotalAllocatedMemoryLong();
memory["profilerReserved"] = UnityEngine.Profiling.Profiler.GetTotalReservedMemoryLong();
}
return FormatMemoryReport(memory);
}
catch (Exception e)
{
return $"Error getting memory usage: {e.Message}";
}
}
/// <summary>
/// Get current error status
/// </summary>
public static string GetErrorStatus(Dictionary<string, string> parameters)
{
try
{
var includeWarnings = parameters.GetValueOrDefault("includeWarnings", "true") == "true";
var includeStackTrace = parameters.GetValueOrDefault("includeStackTrace", "false") == "true";
var maxErrors = int.Parse(parameters.GetValueOrDefault("maxErrors", "20"));
var isCompiling = GetEditorCompilingState();
var hasCompileErrors = GetEditorCompileErrorState();
var errors = new Dictionary<string, object>
{
["isCompiling"] = isCompiling,
["hasCompileErrors"] = hasCompileErrors,
["timestamp"] = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
};
// Console log retrieval is limited, so only basic information
errors["status"] = isCompiling ? "Compiling..." :
hasCompileErrors ? "Compile Errors" : "No Errors";
return FormatErrorReport(errors);
}
catch (Exception e)
{
return $"Error getting error status: {e.Message}";
}
}
/// <summary>
/// Start monitoring Play state changes
/// </summary>
public static string MonitorPlayState(Dictionary<string, string> parameters)
{
try
{
var enableNotifications = parameters.GetValueOrDefault("enableNotifications", "true") == "true";
var includeTimestamp = parameters.GetValueOrDefault("includeTimestamp", "true") == "true";
if (enableNotifications)
{
#if UNITY_EDITOR
// Set up EditorApplication event handlers
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
#endif
}
var currentState = new Dictionary<string, object>
{
["isPlaying"] = Application.isPlaying,
#if UNITY_EDITOR
["isPaused"] = EditorApplication.isPaused,
["isCompiling"] = EditorApplication.isCompiling,
#else
["isPaused"] = false,
["isCompiling"] = false,
#endif
["monitoringEnabled"] = enableNotifications
};
if (includeTimestamp)
{
currentState["timestamp"] = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
}
return FormatPlayStateReport(currentState);
}
catch (Exception e)
{
return $"Error setting up play state monitoring: {e.Message}";
}
}
/// <summary>
/// Get build status
/// </summary>
public static string GetBuildStatus(Dictionary<string, string> parameters)
{
try
{
var includeSettings = parameters.GetValueOrDefault("includeSettings", "true") == "true";
var includeErrors = parameters.GetValueOrDefault("includeErrors", "true") == "true";
var buildInfo = new Dictionary<string, object>();
if (includeSettings)
{
#if UNITY_EDITOR
buildInfo["targetPlatform"] = EditorUserBuildSettings.activeBuildTarget.ToString();
buildInfo["developmentBuild"] = EditorUserBuildSettings.development;
buildInfo["scriptDebugging"] = EditorUserBuildSettings.allowDebugging;
buildInfo["buildAppBundle"] = EditorUserBuildSettings.buildAppBundle;
#else
buildInfo["targetPlatform"] = Application.platform.ToString();
buildInfo["developmentBuild"] = UnityEngine.Debug.isDebugBuild;
buildInfo["scriptDebugging"] = false;
buildInfo["buildAppBundle"] = false;
#endif
}
#if UNITY_EDITOR
// Scene information
var scenes = EditorBuildSettings.scenes;
buildInfo["sceneCount"] = scenes.Length;
buildInfo["enabledScenes"] = scenes.Count(s => s.enabled);
#else
buildInfo["sceneCount"] = SceneManager.sceneCountInBuildSettings;
buildInfo["enabledScenes"] = SceneManager.sceneCountInBuildSettings;
#endif
if (includeErrors)
{
#if UNITY_EDITOR
buildInfo["canBuild"] = !EditorApplication.isCompiling && !EditorUtility.scriptCompilationFailed;
buildInfo["compilationStatus"] = EditorApplication.isCompiling ? "Compiling" :
EditorUtility.scriptCompilationFailed ? "Error" : "Ready";
#else
buildInfo["canBuild"] = true;
buildInfo["compilationStatus"] = "Ready";
#endif
}
return FormatBuildReport(buildInfo);
}
catch (Exception e)
{
return $"Error getting build status: {e.Message}";
}
}
// ===== Helper Methods =====
#if UNITY_EDITOR
private static void OnPlayModeStateChanged(PlayModeStateChange state)
{
var message = $"Play Mode State Changed: {state} at {DateTime.Now:HH:mm:ss}";
UnityEngine.Debug.Log($"[Nexus Monitor] {message}");
// Real-time notification via WebSocket is also possible
// NexusWebSocketClient.Instance?.SendMessage(new { type = "play_state_changed", state = state.ToString() });
}
#endif
private static Dictionary<string, object> GetPerformanceData()
{
return new Dictionary<string, object>
{
["fps"] = Application.isPlaying ? Math.Round(1.0f / Time.unscaledDeltaTime, 1) : 0,
["frameTime"] = Application.isPlaying ? Math.Round(Time.unscaledDeltaTime * 1000, 2) : 0,
["timeScale"] = Time.timeScale
};
}
private static Dictionary<string, object> GetMemoryData()
{
var gcMemory = GC.GetTotalMemory(false);
return new Dictionary<string, object>
{
["gcMemory"] = FormatBytes(gcMemory),
["systemMemory"] = SystemInfo.systemMemorySize + " MB",
["graphicsMemory"] = SystemInfo.graphicsMemorySize + " MB"
};
}
private static Dictionary<string, object> GetErrorStatus()
{
var isCompiling = GetEditorCompilingState();
var hasErrors = GetEditorCompileErrorState();
return new Dictionary<string, object>
{
["isCompiling"] = isCompiling,
["hasErrors"] = hasErrors,
["status"] = isCompiling ? "Compiling" :
hasErrors ? "Errors" : "OK"
};
}
private static string FormatBytes(long bytes)
{
string[] suffixes = { "B", "KB", "MB", "GB" };
int counter = 0;
decimal number = (decimal)bytes;
while (Math.Round(number / 1024) >= 1)
{
number = number / 1024;
counter++;
}
return string.Format("{0:n1} {1}", number, suffixes[counter]);
}
private static string FormatStatusReport(Dictionary<string, object> status)
{
var report = "=== Unity Runtime Status ===\n";
report += $"Play Mode: {status["playMode"]}\n";
report += $"Paused: {status["isPaused"]}\n";
report += $"Compiling: {status["isCompiling"]}\n";
report += $"Platform: {status["platform"]}\n";
report += $"Unity Version: {status["unityVersion"]}\n";
report += $"Timestamp: {status["timestamp"]}\n";
if (status.ContainsKey("performance"))
{
var perf = (Dictionary<string, object>)status["performance"];
report += $"\n--- Performance ---\n";
report += $"FPS: {perf["fps"]}\n";
report += $"Frame Time: {perf["frameTime"]} ms\n";
report += $"Time Scale: {perf["timeScale"]}\n";
}
if (status.ContainsKey("memory"))
{
var mem = (Dictionary<string, object>)status["memory"];
report += $"\n--- Memory ---\n";
report += $"GC Memory: {mem["gcMemory"]}\n";
report += $"System Memory: {mem["systemMemory"]}\n";
report += $"Graphics Memory: {mem["graphicsMemory"]}\n";
}
if (status.ContainsKey("errors"))
{
var err = (Dictionary<string, object>)status["errors"];
report += $"\n--- Status ---\n";
report += $"Compilation: {err["status"]}\n";
}
return report;
}
private static string FormatPerformanceReport(Dictionary<string, object> metrics)
{
var report = "=== Performance Metrics ===\n";
foreach (var kvp in metrics)
{
report += $"{kvp.Key}: {kvp.Value}\n";
}
return report;
}
private static string FormatMemoryReport(Dictionary<string, object> memory)
{
var report = "=== Memory Usage ===\n";
foreach (var kvp in memory)
{
report += $"{kvp.Key}: {kvp.Value}\n";
}
return report;
}
private static string FormatErrorReport(Dictionary<string, object> errors)
{
var report = "=== Error Status ===\n";
foreach (var kvp in errors)
{
report += $"{kvp.Key}: {kvp.Value}\n";
}
return report;
}
private static string FormatPlayStateReport(Dictionary<string, object> state)
{
var report = "=== Play State Monitoring ===\n";
foreach (var kvp in state)
{
report += $"{kvp.Key}: {kvp.Value}\n";
}
return report;
}
private static string FormatBuildReport(Dictionary<string, object> buildInfo)
{
var report = "=== Build Status ===\n";
foreach (var kvp in buildInfo)
{
report += $"{kvp.Key}: {kvp.Value}\n";
}
return report;
}
/// <summary>
/// Safely access Editor-only APIs using Reflection
/// </summary>
private static bool GetEditorPausedState()
{
try
{
var editorAppType = Type.GetType("UnityEditor.EditorApplication, UnityEditor");
if (editorAppType != null)
{
var isPausedProperty = editorAppType.GetProperty("isPaused");
if (isPausedProperty != null)
{
return (bool)isPausedProperty.GetValue(null);
}
}
}
catch
{
// Return false if Editor API is not available
}
return false;
}
private static bool GetEditorCompilingState()
{
try
{
var editorAppType = Type.GetType("UnityEditor.EditorApplication, UnityEditor");
if (editorAppType != null)
{
var isCompilingProperty = editorAppType.GetProperty("isCompiling");
if (isCompilingProperty != null)
{
return (bool)isCompilingProperty.GetValue(null);
}
}
}
catch
{
// Return false if Editor API is not available
}
return false;
}
private static bool GetEditorCompileErrorState()
{
try
{
var editorUtilityType = Type.GetType("UnityEditor.EditorUtility, UnityEditor");
if (editorUtilityType != null)
{
var scriptCompilationFailedProperty = editorUtilityType.GetProperty("scriptCompilationFailed");
if (scriptCompilationFailedProperty != null)
{
return (bool)scriptCompilationFailedProperty.GetValue(null);
}
}
}
catch
{
// Return false if Editor API is not available
}
return false;
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 09ffb06a8ddce4b64abb75ae2bc7b4da
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/NexusRuntimeMonitor.cs
uploadId: 920982
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 5d19e3f2889d94749975b77ca74cd0dd
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/NexusSetupManager.cs
uploadId: 920982
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e28d9ecb6fdce48ec83abc30643b6326
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,40 @@
fileFormatVersion: 2
guid: 8f724b6962862454090b50e69e5e94bf
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 1
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
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/Plugins/Newtonsoft.Json.dll
uploadId: 920982
@@ -0,0 +1,398 @@
using UnityEngine;
using System.Collections.Generic;
namespace SynapticPro
{
/// <summary>
/// Controller for Synaptic ShieldPro shader
/// Manages hit effects, ripples, and shield state
/// </summary>
[ExecuteAlways]
public class ShieldController : MonoBehaviour
{
[System.Serializable]
public class HitRipple
{
public Vector3 worldPosition;
public float startTime;
public float strength;
public float speed;
public bool active;
}
[Header("Target")]
public Renderer targetRenderer;
public int materialIndex = 0;
[Header("Shield State")]
public bool shieldActive = true;
[Range(0f, 1f)]
public float shieldStrength = 1f;
[Range(0f, 1f)]
public float shieldOpacity = 0.5f;
[Header("Hit Ripples")]
public int maxRipples = 4;
public float rippleDuration = 1f;
public float rippleSpeed = 5f;
public float rippleStrength = 1f;
[Header("Hit Flash")]
public bool enableHitFlash = true;
public Color hitFlashColor = new Color(1f, 0.8f, 0.5f, 1f);
public float hitFlashDuration = 0.1f;
public float hitFlashIntensity = 2f;
[Header("Damage State")]
public bool enableDamageState = true;
[Range(0f, 1f)]
public float damageLevel = 0f;
public Color damagedColor = new Color(1f, 0.3f, 0.2f, 1f);
public float damageFlickerSpeed = 10f;
public float damageFlickerIntensity = 0.3f;
[Header("Break Effect")]
public float breakThreshold = 0.9f;
public ParticleSystem breakParticles;
public AudioSource breakAudio;
public AudioClip breakSound;
[Header("Regeneration")]
public bool enableRegeneration = true;
public float regenerationDelay = 3f;
public float regenerationRate = 0.2f;
[Header("Events")]
public UnityEngine.Events.UnityEvent onShieldHit;
public UnityEngine.Events.UnityEvent onShieldBreak;
public UnityEngine.Events.UnityEvent onShieldRestore;
private Material material;
private List<HitRipple> ripples = new List<HitRipple>();
private float lastHitTime;
private float hitFlashTimer;
private bool isFlashing;
private bool isBroken;
// Shader property IDs
private static readonly int ShieldStrengthID = Shader.PropertyToID("_ShieldStrength");
private static readonly int ShieldOpacityID = Shader.PropertyToID("_ShieldOpacity");
private static readonly int HitPositionID = Shader.PropertyToID("_HitPosition");
private static readonly int HitTimeID = Shader.PropertyToID("_HitTime");
private static readonly int RippleSpeedID = Shader.PropertyToID("_RippleSpeed");
private static readonly int RippleStrengthID = Shader.PropertyToID("_RippleStrength");
private static readonly int DamageLevelID = Shader.PropertyToID("_DamageLevel");
private static readonly int HitFlashColorID = Shader.PropertyToID("_HitFlashColor");
private static readonly int HitFlashIntensityID = Shader.PropertyToID("_HitFlashIntensity");
// For multiple ripples
private static readonly int RipplePositions1ID = Shader.PropertyToID("_RipplePosition1");
private static readonly int RipplePositions2ID = Shader.PropertyToID("_RipplePosition2");
private static readonly int RipplePositions3ID = Shader.PropertyToID("_RipplePosition3");
private static readonly int RipplePositions4ID = Shader.PropertyToID("_RipplePosition4");
private static readonly int RippleTimes1ID = Shader.PropertyToID("_RippleTime1");
private static readonly int RippleTimes2ID = Shader.PropertyToID("_RippleTime2");
private static readonly int RippleTimes3ID = Shader.PropertyToID("_RippleTime3");
private static readonly int RippleTimes4ID = Shader.PropertyToID("_RippleTime4");
private void OnEnable()
{
SetupMaterial();
InitializeRipples();
}
private void SetupMaterial()
{
if (targetRenderer == null)
targetRenderer = GetComponent<Renderer>();
if (targetRenderer != null && targetRenderer.sharedMaterials.Length > materialIndex)
{
if (Application.isPlaying)
{
material = targetRenderer.materials[materialIndex];
}
else
{
material = targetRenderer.sharedMaterials[materialIndex];
}
}
}
private void InitializeRipples()
{
ripples.Clear();
for (int i = 0; i < maxRipples; i++)
{
ripples.Add(new HitRipple());
}
}
private void Update()
{
if (material == null)
return;
// Update base shield properties
material.SetFloat(ShieldStrengthID, shieldActive ? shieldStrength : 0f);
material.SetFloat(ShieldOpacityID, shieldOpacity);
// Update ripples
UpdateRipples();
// Update hit flash
UpdateHitFlash();
// Update damage state
UpdateDamageState();
// Handle regeneration
UpdateRegeneration();
}
private void UpdateRipples()
{
float currentTime = Time.time;
// Deactivate expired ripples
foreach (var ripple in ripples)
{
if (ripple.active && currentTime - ripple.startTime > rippleDuration)
{
ripple.active = false;
}
}
// Send ripple data to shader
for (int i = 0; i < Mathf.Min(4, ripples.Count); i++)
{
var ripple = ripples[i];
Vector4 posStrength = ripple.active ?
new Vector4(ripple.worldPosition.x, ripple.worldPosition.y, ripple.worldPosition.z, ripple.strength) :
Vector4.zero;
float time = ripple.active ? currentTime - ripple.startTime : -1f;
switch (i)
{
case 0:
material.SetVector(RipplePositions1ID, posStrength);
material.SetFloat(RippleTimes1ID, time);
break;
case 1:
material.SetVector(RipplePositions2ID, posStrength);
material.SetFloat(RippleTimes2ID, time);
break;
case 2:
material.SetVector(RipplePositions3ID, posStrength);
material.SetFloat(RippleTimes3ID, time);
break;
case 3:
material.SetVector(RipplePositions4ID, posStrength);
material.SetFloat(RippleTimes4ID, time);
break;
}
}
material.SetFloat(RippleSpeedID, rippleSpeed);
material.SetFloat(RippleStrengthID, rippleStrength);
}
private void UpdateHitFlash()
{
if (!enableHitFlash || !isFlashing)
return;
hitFlashTimer -= Time.deltaTime;
if (hitFlashTimer <= 0)
{
isFlashing = false;
material.SetFloat(HitFlashIntensityID, 0f);
}
else
{
float flashAmount = (hitFlashTimer / hitFlashDuration) * hitFlashIntensity;
material.SetColor(HitFlashColorID, hitFlashColor);
material.SetFloat(HitFlashIntensityID, flashAmount);
}
}
private void UpdateDamageState()
{
if (!enableDamageState)
return;
material.SetFloat(DamageLevelID, damageLevel);
// Flickering when damaged
if (damageLevel > 0.5f)
{
float flicker = Mathf.Sin(Time.time * damageFlickerSpeed) * damageFlickerIntensity * damageLevel;
material.SetFloat(ShieldOpacityID, shieldOpacity + flicker);
}
}
private void UpdateRegeneration()
{
if (!enableRegeneration || !Application.isPlaying)
return;
if (isBroken)
{
// Wait for regeneration delay after break
if (Time.time - lastHitTime > regenerationDelay)
{
isBroken = false;
shieldActive = true;
damageLevel = 0.5f; // Start at half strength
onShieldRestore?.Invoke();
}
}
else if (damageLevel > 0 && Time.time - lastHitTime > regenerationDelay * 0.5f)
{
// Gradual regeneration
damageLevel = Mathf.Max(0, damageLevel - regenerationRate * Time.deltaTime);
}
}
/// <summary>
/// Register a hit on the shield
/// </summary>
public void OnHit(Vector3 worldPosition, float damage = 0.1f)
{
if (!shieldActive || isBroken)
return;
lastHitTime = Time.time;
// Add ripple
AddRipple(worldPosition);
// Trigger flash
if (enableHitFlash)
{
isFlashing = true;
hitFlashTimer = hitFlashDuration;
}
// Apply damage
if (enableDamageState)
{
damageLevel = Mathf.Min(1f, damageLevel + damage);
// Check for break
if (damageLevel >= breakThreshold)
{
BreakShield();
}
}
onShieldHit?.Invoke();
}
/// <summary>
/// Add a ripple effect at the specified world position
/// </summary>
public void AddRipple(Vector3 worldPosition)
{
// Find inactive ripple or oldest ripple
HitRipple targetRipple = null;
float oldestTime = float.MaxValue;
foreach (var ripple in ripples)
{
if (!ripple.active)
{
targetRipple = ripple;
break;
}
else if (ripple.startTime < oldestTime)
{
oldestTime = ripple.startTime;
targetRipple = ripple;
}
}
if (targetRipple != null)
{
targetRipple.worldPosition = worldPosition;
targetRipple.startTime = Time.time;
targetRipple.strength = rippleStrength;
targetRipple.speed = rippleSpeed;
targetRipple.active = true;
}
}
/// <summary>
/// Break the shield
/// </summary>
public void BreakShield()
{
if (isBroken)
return;
isBroken = true;
shieldActive = false;
damageLevel = 1f;
// Play break effects
if (breakParticles != null)
{
breakParticles.transform.position = transform.position;
breakParticles.Play();
}
if (breakAudio != null && breakSound != null)
{
breakAudio.clip = breakSound;
breakAudio.Play();
}
onShieldBreak?.Invoke();
}
/// <summary>
/// Instantly restore shield to full
/// </summary>
public void RestoreShield()
{
isBroken = false;
shieldActive = true;
damageLevel = 0f;
onShieldRestore?.Invoke();
}
/// <summary>
/// Set shield state
/// </summary>
public void SetShieldActive(bool active)
{
if (isBroken && active)
RestoreShield();
else
shieldActive = active;
}
#if UNITY_EDITOR
private void OnValidate()
{
SetupMaterial();
InitializeRipples();
}
private void OnDrawGizmosSelected()
{
// Draw active ripples
Gizmos.color = Color.cyan;
foreach (var ripple in ripples)
{
if (ripple.active)
{
float radius = (Time.time - ripple.startTime) * rippleSpeed;
Gizmos.DrawWireSphere(ripple.worldPosition, Mathf.Min(radius, 5f));
}
}
}
#endif
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: d022be34ead974ebabb1db008241e991
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/ShieldController.cs
uploadId: 920982
@@ -0,0 +1,99 @@
using UnityEngine;
namespace SynapticPro
{
/// <summary>
/// Makes a sky sphere follow the main camera position.
/// Used for landscape photo skybox effect.
/// </summary>
[AddComponentMenu("Synaptic Pro/Sky Sphere Camera Follow")]
public class SkySphereCameraFollow : MonoBehaviour
{
[Tooltip("Target camera to follow. If null, uses Camera.main")]
public Camera targetCamera;
[Tooltip("Offset from camera position")]
public Vector3 offset = Vector3.zero;
[Tooltip("Enable rotation sync with camera")]
public bool syncRotation = false;
[Tooltip("Only sync Y-axis rotation")]
public bool yAxisOnly = true;
private Transform _cameraTransform;
public void Initialize()
{
if (targetCamera == null)
{
targetCamera = Camera.main;
}
if (targetCamera != null)
{
_cameraTransform = targetCamera.transform;
}
}
private void Start()
{
Initialize();
}
private void LateUpdate()
{
if (_cameraTransform == null)
{
if (targetCamera != null)
{
_cameraTransform = targetCamera.transform;
}
else
{
targetCamera = Camera.main;
if (targetCamera != null)
{
_cameraTransform = targetCamera.transform;
}
}
if (_cameraTransform == null) return;
}
// Follow camera position
transform.position = _cameraTransform.position + offset;
// Optionally sync rotation
if (syncRotation)
{
if (yAxisOnly)
{
var euler = transform.eulerAngles;
euler.y = _cameraTransform.eulerAngles.y;
transform.eulerAngles = euler;
}
else
{
transform.rotation = _cameraTransform.rotation;
}
}
}
/// <summary>
/// Set the rotation offset of the sky sphere
/// </summary>
public void SetRotation(float yRotation)
{
transform.rotation = Quaternion.Euler(0, yRotation, 0);
}
/// <summary>
/// Set the scale (radius) of the sky sphere
/// </summary>
public void SetRadius(float radius)
{
transform.localScale = Vector3.one * radius;
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 24aa80d0fd2f34354af4a91e3b425cfe
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/SkySphereCameraFollow.cs
uploadId: 920982
@@ -0,0 +1,88 @@
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;
namespace SynapticPro
{
/// <summary>
/// Unity Main Thread Dispatcher
/// Enables execution on Unity main thread from other threads
/// </summary>
public class UnityMainThreadDispatcher : MonoBehaviour
{
private static readonly Queue<Action> _executionQueue = new Queue<Action>();
private static UnityMainThreadDispatcher _instance = null;
public static bool Exists()
{
return _instance != null;
}
public static UnityMainThreadDispatcher Instance()
{
if (!Exists())
{
throw new Exception("UnityMainThreadDispatcher could not find the UnityMainThreadDispatcher object.");
}
return _instance;
}
void Awake()
{
if (_instance == null)
{
_instance = this;
DontDestroyOnLoad(this.gameObject);
}
}
void OnDestroy()
{
_instance = null;
}
public void Update()
{
lock (_executionQueue)
{
while (_executionQueue.Count > 0)
{
_executionQueue.Dequeue().Invoke();
}
}
}
public void Enqueue(IEnumerator action)
{
lock (_executionQueue)
{
_executionQueue.Enqueue(() => {
StartCoroutine(action);
});
}
}
public void Enqueue(Action action)
{
Enqueue(ActionWrapper(action));
}
IEnumerator ActionWrapper(Action a)
{
a();
yield return null;
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Initialize()
{
if (_instance == null)
{
GameObject dispatcher = new GameObject("UnityMainThreadDispatcher");
_instance = dispatcher.AddComponent<UnityMainThreadDispatcher>();
}
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: c8a03acc262934b7faa5552e0798e29b
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/UnityMainThreadDispatcher.cs
uploadId: 920982
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9b9f602bd5fc341dabb19d0f864dca50
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,181 @@
using UnityEngine;
namespace Synaptic.Water
{
/// <summary>
/// Applies buoyancy forces to make objects float on the ocean
/// </summary>
[RequireComponent(typeof(Rigidbody))]
public class Buoyancy : MonoBehaviour
{
[Header("Buoyancy Settings")]
[Tooltip("Reference to the ocean system")]
public OceanSystem ocean;
[Tooltip("Buoyancy force multiplier")]
public float buoyancyForce = 10f;
[Tooltip("Water drag coefficient")]
public float waterDrag = 1f;
[Tooltip("Angular water drag")]
public float waterAngularDrag = 0.5f;
[Header("Float Points")]
[Tooltip("Points where buoyancy is sampled. If empty, uses object center.")]
public Transform[] floatPoints;
[Header("Wave Response")]
[Tooltip("How much the object responds to wave normal")]
public float waveAlignmentStrength = 0.5f;
[Tooltip("Maximum rotation speed when aligning to waves")]
public float maxAlignmentTorque = 5f;
private Rigidbody rb;
private float originalDrag;
private float originalAngularDrag;
private bool isInWater;
void Start()
{
rb = GetComponent<Rigidbody>();
originalDrag = rb.linearDamping;
originalAngularDrag = rb.angularDamping;
// Auto-find ocean if not set
if (ocean == null)
ocean = FindFirstObjectByType<OceanSystem>();
// Create default float points if none specified
if (floatPoints == null || floatPoints.Length == 0)
{
floatPoints = new Transform[] { transform };
}
}
void FixedUpdate()
{
if (ocean == null) return;
float submergedAmount = 0f;
Vector3 totalForce = Vector3.zero;
Vector3 averageWaveNormal = Vector3.zero;
int submergedPoints = 0;
foreach (Transform point in floatPoints)
{
if (point == null) continue;
Vector3 pointPos = point.position;
float waterHeight = ocean.GetWaveHeight(pointPos);
float depth = waterHeight - pointPos.y;
if (depth > 0)
{
// Point is underwater
submergedPoints++;
submergedAmount += Mathf.Clamp01(depth);
// Buoyancy force proportional to submersion depth
float forceMagnitude = buoyancyForce * Mathf.Clamp01(depth) * Physics.gravity.magnitude;
Vector3 force = Vector3.up * forceMagnitude;
// Apply force at float point position
rb.AddForceAtPosition(force, pointPos, ForceMode.Force);
totalForce += force;
// Sample wave normal
averageWaveNormal += ocean.GetWaveNormal(pointPos);
}
}
// Update water state
bool wasInWater = isInWater;
isInWater = submergedPoints > 0;
// Apply water drag when in water
if (isInWater)
{
float normalizedSubmersion = (float)submergedPoints / floatPoints.Length;
rb.linearDamping = Mathf.Lerp(originalDrag, waterDrag, normalizedSubmersion);
rb.angularDamping = Mathf.Lerp(originalAngularDrag, waterAngularDrag, normalizedSubmersion);
// Align to wave normal
if (waveAlignmentStrength > 0 && submergedPoints > 0)
{
averageWaveNormal = (averageWaveNormal / submergedPoints).normalized;
Vector3 currentUp = transform.up;
Vector3 targetUp = Vector3.Lerp(currentUp, averageWaveNormal, waveAlignmentStrength);
Quaternion targetRotation = Quaternion.FromToRotation(currentUp, targetUp) * transform.rotation;
Vector3 torque = CalculateAlignmentTorque(transform.rotation, targetRotation);
rb.AddTorque(Vector3.ClampMagnitude(torque, maxAlignmentTorque), ForceMode.Force);
}
}
else
{
rb.linearDamping = originalDrag;
rb.angularDamping = originalAngularDrag;
}
// Water entry/exit events
if (isInWater && !wasInWater)
{
OnWaterEnter();
}
else if (!isInWater && wasInWater)
{
OnWaterExit();
}
}
Vector3 CalculateAlignmentTorque(Quaternion current, Quaternion target)
{
Quaternion delta = target * Quaternion.Inverse(current);
delta.ToAngleAxis(out float angle, out Vector3 axis);
if (angle > 180f) angle -= 360f;
return axis * (angle * Mathf.Deg2Rad);
}
protected virtual void OnWaterEnter()
{
// Override for splash effects, sounds, etc.
}
protected virtual void OnWaterExit()
{
// Override for exit effects
}
/// <summary>
/// Check if object is currently in water
/// </summary>
public bool IsInWater => isInWater;
/// <summary>
/// Get current water height at object position
/// </summary>
public float GetWaterHeightAtPosition()
{
if (ocean == null) return 0f;
return ocean.GetWaveHeight(transform.position);
}
void OnDrawGizmosSelected()
{
if (floatPoints == null) return;
Gizmos.color = Color.cyan;
foreach (Transform point in floatPoints)
{
if (point != null)
{
Gizmos.DrawWireSphere(point.position, 0.2f);
}
}
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: ceebf94a476b54c36b1118cedb42bda6
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/Water/Buoyancy.cs
uploadId: 920982
@@ -0,0 +1,194 @@
using UnityEngine;
namespace Synaptic.Water
{
/// <summary>
/// Creates an infinite ocean plane that follows the camera
/// </summary>
[ExecuteAlways]
public class OceanSystem : MonoBehaviour
{
[Header("Ocean Settings")]
public Material oceanMaterial;
public int gridSize = 128;
public float tileSize = 100f;
public int tilesAroundCamera = 3;
[Header("LOD Settings")]
public bool useLOD = true;
public float lodDistance = 200f;
public int lodLevels = 3;
[Header("Camera")]
public Transform followCamera;
private MeshFilter meshFilter;
private MeshRenderer meshRenderer;
private Mesh oceanMesh;
private Vector3 lastCameraPosition;
void Start()
{
if (followCamera == null)
followCamera = Camera.main?.transform;
CreateOceanMesh();
}
void Update()
{
if (followCamera == null) return;
// Snap to grid position following camera
Vector3 camPos = followCamera.position;
float snapX = Mathf.Floor(camPos.x / tileSize) * tileSize;
float snapZ = Mathf.Floor(camPos.z / tileSize) * tileSize;
transform.position = new Vector3(snapX, transform.position.y, snapZ);
}
void CreateOceanMesh()
{
meshFilter = GetComponent<MeshFilter>();
if (meshFilter == null)
meshFilter = gameObject.AddComponent<MeshFilter>();
meshRenderer = GetComponent<MeshRenderer>();
if (meshRenderer == null)
meshRenderer = gameObject.AddComponent<MeshRenderer>();
oceanMesh = GenerateOceanMesh(gridSize, tileSize * tilesAroundCamera * 2);
meshFilter.sharedMesh = oceanMesh;
if (oceanMaterial != null)
meshRenderer.sharedMaterial = oceanMaterial;
}
Mesh GenerateOceanMesh(int resolution, float size)
{
Mesh mesh = new Mesh();
mesh.name = "OceanMesh";
mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
int vertCount = (resolution + 1) * (resolution + 1);
Vector3[] vertices = new Vector3[vertCount];
Vector2[] uvs = new Vector2[vertCount];
Vector3[] normals = new Vector3[vertCount];
float halfSize = size * 0.5f;
float step = size / resolution;
for (int z = 0; z <= resolution; z++)
{
for (int x = 0; x <= resolution; x++)
{
int i = z * (resolution + 1) + x;
float xPos = x * step - halfSize;
float zPos = z * step - halfSize;
vertices[i] = new Vector3(xPos, 0, zPos);
uvs[i] = new Vector2((float)x / resolution, (float)z / resolution);
normals[i] = Vector3.up;
}
}
int[] triangles = new int[resolution * resolution * 6];
int t = 0;
for (int z = 0; z < resolution; z++)
{
for (int x = 0; x < resolution; x++)
{
int i = z * (resolution + 1) + x;
triangles[t++] = i;
triangles[t++] = i + resolution + 1;
triangles[t++] = i + 1;
triangles[t++] = i + 1;
triangles[t++] = i + resolution + 1;
triangles[t++] = i + resolution + 2;
}
}
mesh.vertices = vertices;
mesh.uv = uvs;
mesh.normals = normals;
mesh.triangles = triangles;
mesh.RecalculateBounds();
return mesh;
}
/// <summary>
/// Get wave height at world position (for buoyancy)
/// </summary>
public float GetWaveHeight(Vector3 worldPos)
{
if (oceanMaterial == null) return transform.position.y;
float time = Time.time * oceanMaterial.GetFloat("_WaveSpeed");
float oceanScale = oceanMaterial.HasProperty("_OceanScale") ? oceanMaterial.GetFloat("_OceanScale") : 1f;
float waveHeight = oceanMaterial.HasProperty("_WaveHeight") ? oceanMaterial.GetFloat("_WaveHeight") : 1f;
Vector3 scaledPos = worldPos * oceanScale;
float height = transform.position.y;
// Sample waves A through H
height += SampleGerstnerWave(oceanMaterial.GetVector("_WaveA"), scaledPos, time * 0.8f) * waveHeight;
height += SampleGerstnerWave(oceanMaterial.GetVector("_WaveB"), scaledPos, time * 0.9f) * waveHeight;
height += SampleGerstnerWave(oceanMaterial.GetVector("_WaveC"), scaledPos, time) * waveHeight;
height += SampleGerstnerWave(oceanMaterial.GetVector("_WaveD"), scaledPos, time * 1.1f) * waveHeight;
if (oceanMaterial.HasProperty("_WaveE"))
{
height += SampleGerstnerWave(oceanMaterial.GetVector("_WaveE"), scaledPos, time * 1.2f) * waveHeight;
height += SampleGerstnerWave(oceanMaterial.GetVector("_WaveF"), scaledPos, time * 1.4f) * waveHeight;
height += SampleGerstnerWave(oceanMaterial.GetVector("_WaveG"), scaledPos, time * 1.6f) * waveHeight;
height += SampleGerstnerWave(oceanMaterial.GetVector("_WaveH"), scaledPos, time * 1.8f) * waveHeight;
}
return height;
}
float SampleGerstnerWave(Vector4 wave, Vector3 pos, float time)
{
float steepness = wave.z;
float wavelength = wave.w;
if (wavelength <= 0) return 0;
float k = 2f * Mathf.PI / wavelength;
float c = Mathf.Sqrt(9.8f / k);
Vector2 d = new Vector2(wave.x, wave.y).normalized;
float f = k * (Vector2.Dot(d, new Vector2(pos.x, pos.z)) - c * time);
float a = steepness / k;
return a * Mathf.Sin(f);
}
/// <summary>
/// Get wave normal at world position
/// </summary>
public Vector3 GetWaveNormal(Vector3 worldPos)
{
float delta = 0.1f;
float h = GetWaveHeight(worldPos);
float hX = GetWaveHeight(worldPos + Vector3.right * delta);
float hZ = GetWaveHeight(worldPos + Vector3.forward * delta);
Vector3 tangentX = new Vector3(delta, hX - h, 0).normalized;
Vector3 tangentZ = new Vector3(0, hZ - h, delta).normalized;
return Vector3.Cross(tangentZ, tangentX).normalized;
}
void OnValidate()
{
if (Application.isPlaying && oceanMesh != null)
{
CreateOceanMesh();
}
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: f02a1388f7cb640c0811bba5b3794e55
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/Water/OceanSystem.cs
uploadId: 920982
@@ -0,0 +1,370 @@
using UnityEngine;
using System.Collections.Generic;
namespace Synaptic
{
/// <summary>
/// Water surface component - attach to water plane
/// Provides water height queries and wave animation
/// </summary>
public class WaterSurface : MonoBehaviour
{
[Header("Wave Settings")]
public float waveSpeed = 1f;
public float waveStrength = 0.1f;
public float waveFrequency = 1f;
public Vector2 waveDirectionA = new Vector2(1, 0);
public Vector2 waveDirectionB = new Vector2(0, 1);
[Header("Physics")]
public float waterDensity = 1000f; // kg/m³ (water = 1000)
private static List<WaterSurface> activeSurfaces = new List<WaterSurface>();
public static WaterSurface GetWaterSurfaceAt(Vector3 position)
{
foreach (var surface in activeSurfaces)
{
if (surface.IsPointAboveWater(position))
{
return surface;
}
}
return null;
}
void OnEnable()
{
if (!activeSurfaces.Contains(this))
{
activeSurfaces.Add(this);
}
}
void OnDisable()
{
activeSurfaces.Remove(this);
}
/// <summary>
/// Get water height at world position using Gerstner waves
/// </summary>
public float GetWaterHeight(Vector3 worldPosition)
{
float baseHeight = transform.position.y;
float time = Time.time * waveSpeed;
// Gerstner wave calculation
float height = 0f;
height += GerstnerWaveHeight(worldPosition, waveDirectionA, waveStrength, waveFrequency * 10f, time);
height += GerstnerWaveHeight(worldPosition, waveDirectionB, waveStrength * 0.5f, waveFrequency * 7f, time * 1.3f);
height += GerstnerWaveHeight(worldPosition, new Vector2(0.7f, 0.7f), waveStrength * 0.3f, waveFrequency * 5f, time * 0.8f);
return baseHeight + height;
}
private float GerstnerWaveHeight(Vector3 position, Vector2 direction, float steepness, float wavelength, float time)
{
float k = 2f * Mathf.PI / wavelength;
float c = Mathf.Sqrt(9.8f / k);
Vector2 d = direction.normalized;
float f = k * (Vector2.Dot(d, new Vector2(position.x, position.z)) - c * time);
float a = steepness / k;
return a * Mathf.Sin(f);
}
/// <summary>
/// Check if a point is within the water area (XZ bounds)
/// </summary>
public bool IsPointAboveWater(Vector3 point)
{
// Simple bounds check using renderer bounds
var renderer = GetComponent<Renderer>();
if (renderer != null)
{
var bounds = renderer.bounds;
return point.x >= bounds.min.x && point.x <= bounds.max.x &&
point.z >= bounds.min.z && point.z <= bounds.max.z;
}
// Fallback to transform scale
Vector3 localPoint = transform.InverseTransformPoint(point);
return Mathf.Abs(localPoint.x) <= 0.5f && Mathf.Abs(localPoint.z) <= 0.5f;
}
/// <summary>
/// Get wave normal at position for physics calculations
/// </summary>
public Vector3 GetWaveNormal(Vector3 worldPosition)
{
float delta = 0.1f;
float h0 = GetWaterHeight(worldPosition);
float hx = GetWaterHeight(worldPosition + Vector3.right * delta);
float hz = GetWaterHeight(worldPosition + Vector3.forward * delta);
Vector3 tangentX = new Vector3(delta, hx - h0, 0).normalized;
Vector3 tangentZ = new Vector3(0, hz - h0, delta).normalized;
return Vector3.Cross(tangentZ, tangentX).normalized;
}
}
/// <summary>
/// Buoyancy component - makes objects float on water
/// Attach to any Rigidbody that should float
/// </summary>
[RequireComponent(typeof(Rigidbody))]
public class Buoyancy : MonoBehaviour
{
[Header("Buoyancy Settings")]
[Tooltip("Points where buoyancy force is applied")]
public Transform[] floatPoints;
[Tooltip("How much the object floats (1 = neutrally buoyant)")]
[Range(0f, 3f)]
public float buoyancyStrength = 1.5f;
[Tooltip("Underwater drag multiplier")]
public float underwaterDrag = 3f;
[Tooltip("Underwater angular drag multiplier")]
public float underwaterAngularDrag = 1f;
[Header("Effects")]
public bool createSplashOnEnter = true;
public GameObject splashPrefab;
public float splashThreshold = 2f; // Minimum velocity to create splash
private Rigidbody rb;
private float originalDrag;
private float originalAngularDrag;
private bool wasUnderwater = false;
private WaterSurface currentWater;
void Start()
{
rb = GetComponent<Rigidbody>();
originalDrag = rb.linearDamping;
originalAngularDrag = rb.angularDamping;
// Auto-generate float points if not set
if (floatPoints == null || floatPoints.Length == 0)
{
GenerateFloatPoints();
}
}
void GenerateFloatPoints()
{
var collider = GetComponent<Collider>();
if (collider != null)
{
var bounds = collider.bounds;
var points = new List<Transform>();
// Create 4 corner points + center
Vector3[] offsets = new Vector3[]
{
new Vector3(-0.4f, -0.5f, -0.4f),
new Vector3(0.4f, -0.5f, -0.4f),
new Vector3(-0.4f, -0.5f, 0.4f),
new Vector3(0.4f, -0.5f, 0.4f),
new Vector3(0, -0.5f, 0)
};
foreach (var offset in offsets)
{
var point = new GameObject("FloatPoint").transform;
point.parent = transform;
point.localPosition = Vector3.Scale(offset, bounds.size);
points.Add(point);
}
floatPoints = points.ToArray();
}
}
void FixedUpdate()
{
currentWater = WaterSurface.GetWaterSurfaceAt(transform.position);
if (currentWater == null)
{
// Reset drag when out of water
rb.linearDamping = originalDrag;
rb.angularDamping = originalAngularDrag;
wasUnderwater = false;
return;
}
bool isUnderwater = false;
int underwaterPoints = 0;
foreach (var point in floatPoints)
{
if (point == null) continue;
float waterHeight = currentWater.GetWaterHeight(point.position);
float depth = waterHeight - point.position.y;
if (depth > 0)
{
isUnderwater = true;
underwaterPoints++;
// Calculate buoyancy force
float displacementMultiplier = Mathf.Clamp01(depth / 0.5f);
float buoyancyForce = currentWater.waterDensity * Physics.gravity.magnitude * displacementMultiplier * buoyancyStrength;
// Apply force at float point
Vector3 force = Vector3.up * buoyancyForce / floatPoints.Length;
rb.AddForceAtPosition(force, point.position, ForceMode.Force);
// Add wave influence
Vector3 waveNormal = currentWater.GetWaveNormal(point.position);
rb.AddForceAtPosition(waveNormal * buoyancyForce * 0.1f, point.position, ForceMode.Force);
}
}
// Apply underwater drag
if (isUnderwater)
{
float submergedRatio = (float)underwaterPoints / floatPoints.Length;
rb.linearDamping = Mathf.Lerp(originalDrag, underwaterDrag, submergedRatio);
rb.angularDamping = Mathf.Lerp(originalAngularDrag, underwaterAngularDrag, submergedRatio);
}
else
{
rb.linearDamping = originalDrag;
rb.angularDamping = originalAngularDrag;
}
// Splash effect on water entry
if (createSplashOnEnter && isUnderwater && !wasUnderwater)
{
if (rb.linearVelocity.magnitude > splashThreshold)
{
CreateSplash();
}
}
wasUnderwater = isUnderwater;
}
void CreateSplash()
{
if (splashPrefab != null)
{
float waterHeight = currentWater.GetWaterHeight(transform.position);
Vector3 splashPos = new Vector3(transform.position.x, waterHeight, transform.position.z);
Instantiate(splashPrefab, splashPos, Quaternion.identity);
}
else
{
// Create simple particle splash
float waterHeight = currentWater.GetWaterHeight(transform.position);
Vector3 splashPos = new Vector3(transform.position.x, waterHeight, transform.position.z);
var splashGO = new GameObject("Splash");
splashGO.transform.position = splashPos;
var ps = splashGO.AddComponent<ParticleSystem>();
var main = ps.main;
main.startLifetime = 1f;
main.startSpeed = 3f;
main.startSize = 0.1f;
main.startColor = new Color(0.8f, 0.9f, 1f, 0.7f);
main.gravityModifier = 1f;
main.maxParticles = 50;
main.duration = 0.3f;
main.loop = false;
var emission = ps.emission;
emission.rateOverTime = 0;
emission.SetBurst(0, new ParticleSystem.Burst(0f, 30));
var shape = ps.shape;
shape.shapeType = ParticleSystemShapeType.Hemisphere;
shape.radius = 0.3f;
ps.Play();
Destroy(splashGO, 2f);
}
}
void OnDrawGizmosSelected()
{
if (floatPoints == null) return;
Gizmos.color = Color.cyan;
foreach (var point in floatPoints)
{
if (point != null)
{
Gizmos.DrawWireSphere(point.position, 0.1f);
}
}
}
}
/// <summary>
/// Water interaction trigger - creates ripples and splashes when objects enter
/// </summary>
[RequireComponent(typeof(Collider))]
public class WaterInteraction : MonoBehaviour
{
[Header("Ripple Settings")]
public bool createRipples = true;
public float rippleInterval = 0.5f;
public GameObject ripplePrefab;
[Header("Splash Settings")]
public bool createSplashes = true;
public float minSplashVelocity = 1f;
public GameObject splashPrefab;
private float lastRippleTime;
void OnTriggerEnter(Collider other)
{
if (!createSplashes) return;
var rb = other.GetComponent<Rigidbody>();
if (rb != null && rb.linearVelocity.magnitude > minSplashVelocity)
{
CreateSplashAt(other.ClosestPoint(transform.position), rb.linearVelocity.magnitude);
}
}
void OnTriggerStay(Collider other)
{
if (!createRipples) return;
if (Time.time - lastRippleTime < rippleInterval) return;
var rb = other.GetComponent<Rigidbody>();
if (rb != null && rb.linearVelocity.magnitude > 0.1f)
{
CreateRippleAt(other.ClosestPoint(transform.position));
lastRippleTime = Time.time;
}
}
void CreateSplashAt(Vector3 position, float intensity)
{
if (splashPrefab != null)
{
var splash = Instantiate(splashPrefab, position, Quaternion.identity);
Destroy(splash, 3f);
}
}
void CreateRippleAt(Vector3 position)
{
if (ripplePrefab != null)
{
var ripple = Instantiate(ripplePrefab, position, Quaternion.Euler(90, 0, 0));
Destroy(ripple, 2f);
}
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 48981e73f7f1045bd8addf534ba1a1e3
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/WaterPhysics.cs
uploadId: 920982