[Add] Synaptic AI Pro
https://assetstore.unity.com/packages/tools/generative-ai/synaptic-ai-pro-natural-language-control-for-unity-336030
This commit is contained in:
@@ -0,0 +1,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
|
||||
@@ -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:
|
||||
Binary file not shown.
@@ -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
|
||||
Reference in New Issue
Block a user