diff --git a/Unity/AIForOceans/Assets/Audio/no/no_1.mp3 b/Unity/AIForOceans/Assets/Audio/no/no_1.mp3 new file mode 100755 index 00000000..375e976d Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/no/no_1.mp3 differ diff --git a/Unity/AIForOceans/Assets/Audio/no/no_10.mp3 b/Unity/AIForOceans/Assets/Audio/no/no_10.mp3 new file mode 100755 index 00000000..8378b2d2 Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/no/no_10.mp3 differ diff --git a/Unity/AIForOceans/Assets/Audio/no/no_2.mp3 b/Unity/AIForOceans/Assets/Audio/no/no_2.mp3 new file mode 100755 index 00000000..6977fd9a Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/no/no_2.mp3 differ diff --git a/Unity/AIForOceans/Assets/Audio/no/no_3.mp3 b/Unity/AIForOceans/Assets/Audio/no/no_3.mp3 new file mode 100755 index 00000000..356a17e2 Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/no/no_3.mp3 differ diff --git a/Unity/AIForOceans/Assets/Audio/no/no_4.mp3 b/Unity/AIForOceans/Assets/Audio/no/no_4.mp3 new file mode 100755 index 00000000..e1a783a0 Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/no/no_4.mp3 differ diff --git a/Unity/AIForOceans/Assets/Audio/no/no_5.mp3 b/Unity/AIForOceans/Assets/Audio/no/no_5.mp3 new file mode 100755 index 00000000..f4c2f5a8 Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/no/no_5.mp3 differ diff --git a/Unity/AIForOceans/Assets/Audio/no/no_6.mp3 b/Unity/AIForOceans/Assets/Audio/no/no_6.mp3 new file mode 100755 index 00000000..de5a0232 Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/no/no_6.mp3 differ diff --git a/Unity/AIForOceans/Assets/Audio/no/no_7.mp3 b/Unity/AIForOceans/Assets/Audio/no/no_7.mp3 new file mode 100755 index 00000000..535b0877 Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/no/no_7.mp3 differ diff --git a/Unity/AIForOceans/Assets/Audio/no/no_8.mp3 b/Unity/AIForOceans/Assets/Audio/no/no_8.mp3 new file mode 100755 index 00000000..4ee4e973 Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/no/no_8.mp3 differ diff --git a/Unity/AIForOceans/Assets/Audio/no/no_9.mp3 b/Unity/AIForOceans/Assets/Audio/no/no_9.mp3 new file mode 100755 index 00000000..1f4c2dbd Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/no/no_9.mp3 differ diff --git a/Unity/AIForOceans/Assets/Audio/other/other_1.mp3 b/Unity/AIForOceans/Assets/Audio/other/other_1.mp3 new file mode 100755 index 00000000..6a7740a3 Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/other/other_1.mp3 differ diff --git a/Unity/AIForOceans/Assets/Audio/other/other_2.mp3 b/Unity/AIForOceans/Assets/Audio/other/other_2.mp3 new file mode 100755 index 00000000..e8f7a126 Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/other/other_2.mp3 differ diff --git a/Unity/AIForOceans/Assets/Audio/other/other_3.mp3 b/Unity/AIForOceans/Assets/Audio/other/other_3.mp3 new file mode 100755 index 00000000..e71cb300 Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/other/other_3.mp3 differ diff --git a/Unity/AIForOceans/Assets/Audio/other/other_4.mp3 b/Unity/AIForOceans/Assets/Audio/other/other_4.mp3 new file mode 100755 index 00000000..d134990b Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/other/other_4.mp3 differ diff --git a/Unity/AIForOceans/Assets/Audio/sortno/sortno_1.mp3 b/Unity/AIForOceans/Assets/Audio/sortno/sortno_1.mp3 new file mode 100644 index 00000000..57ee4828 Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/sortno/sortno_1.mp3 differ diff --git a/Unity/AIForOceans/Assets/Audio/sortyes/sortyes_1.mp3 b/Unity/AIForOceans/Assets/Audio/sortyes/sortyes_1.mp3 new file mode 100644 index 00000000..78846689 Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/sortyes/sortyes_1.mp3 differ diff --git a/Unity/AIForOceans/Assets/Audio/sortyes/sortyes_2.mp3 b/Unity/AIForOceans/Assets/Audio/sortyes/sortyes_2.mp3 new file mode 100644 index 00000000..d09b48e9 Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/sortyes/sortyes_2.mp3 differ diff --git a/Unity/AIForOceans/Assets/Audio/sortyes/sortyes_3.mp3 b/Unity/AIForOceans/Assets/Audio/sortyes/sortyes_3.mp3 new file mode 100644 index 00000000..b3dd4864 Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/sortyes/sortyes_3.mp3 differ diff --git a/Unity/AIForOceans/Assets/Audio/yes/yes_1.mp3 b/Unity/AIForOceans/Assets/Audio/yes/yes_1.mp3 new file mode 100755 index 00000000..4d4c2cc3 Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/yes/yes_1.mp3 differ diff --git a/Unity/AIForOceans/Assets/Audio/yes/yes_10.mp3 b/Unity/AIForOceans/Assets/Audio/yes/yes_10.mp3 new file mode 100755 index 00000000..7f16f99c Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/yes/yes_10.mp3 differ diff --git a/Unity/AIForOceans/Assets/Audio/yes/yes_2.mp3 b/Unity/AIForOceans/Assets/Audio/yes/yes_2.mp3 new file mode 100755 index 00000000..139879bd Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/yes/yes_2.mp3 differ diff --git a/Unity/AIForOceans/Assets/Audio/yes/yes_3.mp3 b/Unity/AIForOceans/Assets/Audio/yes/yes_3.mp3 new file mode 100755 index 00000000..95d78457 Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/yes/yes_3.mp3 differ diff --git a/Unity/AIForOceans/Assets/Audio/yes/yes_4.mp3 b/Unity/AIForOceans/Assets/Audio/yes/yes_4.mp3 new file mode 100755 index 00000000..32d6d5f1 Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/yes/yes_4.mp3 differ diff --git a/Unity/AIForOceans/Assets/Audio/yes/yes_5.mp3 b/Unity/AIForOceans/Assets/Audio/yes/yes_5.mp3 new file mode 100755 index 00000000..3ff711ef Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/yes/yes_5.mp3 differ diff --git a/Unity/AIForOceans/Assets/Audio/yes/yes_6.mp3 b/Unity/AIForOceans/Assets/Audio/yes/yes_6.mp3 new file mode 100755 index 00000000..b66727d8 Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/yes/yes_6.mp3 differ diff --git a/Unity/AIForOceans/Assets/Audio/yes/yes_7.mp3 b/Unity/AIForOceans/Assets/Audio/yes/yes_7.mp3 new file mode 100755 index 00000000..ae47edf3 Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/yes/yes_7.mp3 differ diff --git a/Unity/AIForOceans/Assets/Audio/yes/yes_8.mp3 b/Unity/AIForOceans/Assets/Audio/yes/yes_8.mp3 new file mode 100755 index 00000000..41c6fead Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/yes/yes_8.mp3 differ diff --git a/Unity/AIForOceans/Assets/Audio/yes/yes_9.mp3 b/Unity/AIForOceans/Assets/Audio/yes/yes_9.mp3 new file mode 100755 index 00000000..7719a4e4 Binary files /dev/null and b/Unity/AIForOceans/Assets/Audio/yes/yes_9.mp3 differ diff --git a/Unity/AIForOceans/Assets/Scripts/AIForOceans.asmdef b/Unity/AIForOceans/Assets/Scripts/AIForOceans.asmdef new file mode 100644 index 00000000..58aa4992 --- /dev/null +++ b/Unity/AIForOceans/Assets/Scripts/AIForOceans.asmdef @@ -0,0 +1,16 @@ +{ + "name": "AIForOceans", + "rootNamespace": "AIForOceans", + "references": [ + "Unity.TextMeshPro" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Unity/AIForOceans/Assets/Scripts/Animation/FishAnimator.cs b/Unity/AIForOceans/Assets/Scripts/Animation/FishAnimator.cs new file mode 100644 index 00000000..90602146 --- /dev/null +++ b/Unity/AIForOceans/Assets/Scripts/Animation/FishAnimator.cs @@ -0,0 +1,165 @@ +using UnityEngine; + +namespace AIForOceans +{ + /// + /// Handles fish animation - bobbing, movement, scanning pause + /// Mirrors the original renderer.js animation logic + /// + public class FishAnimator : MonoBehaviour + { + [Header("Animation Settings")] + public float bobAmplitude = 10f; + public float bobFrequency = 2f; + public float moveSpeed = 100f; + + [Header("Scan Settings")] + public bool isPaused = false; + public float scanPauseDuration = 1.5f; + + // Internal state + private float startTime; + private float pauseStartTime; + private float totalPauseTime; + private Vector2 startPosition; + private float bobPhase; + + // S-curve animation for scanning + private bool isScanning = false; + private float scanStartTime; + private float scanProgress; + + private void Start() + { + startTime = Time.time; + startPosition = transform.position; + } + + /// + /// Initialize with ocean object data + /// + public void Initialize(OceanObjectData data) + { + bobPhase = data.bobPhase; + moveSpeed = data.speed; + startPosition = data.position; + transform.position = startPosition; + } + + private void Update() + { + if (!isPaused) + { + UpdateMovement(); + } + + UpdateBobbing(); + + if (isScanning) + { + UpdateScanAnimation(); + } + } + + /// + /// Update horizontal movement + /// + private void UpdateMovement() + { + float elapsed = Time.time - startTime - totalPauseTime; + float xOffset = elapsed * moveSpeed; + + Vector2 pos = transform.position; + pos.x = startPosition.x + xOffset; + transform.position = pos; + } + + /// + /// Update bobbing animation (sine wave) + /// + private void UpdateBobbing() + { + float time = Time.time; + float bobOffset = Mathf.Sin(time * bobFrequency + bobPhase) * bobAmplitude; + + Vector2 pos = transform.position; + pos.y = startPosition.y + bobOffset; + transform.position = pos; + } + + /// + /// Start pause for scanning + /// + public void StartScan() + { + isPaused = true; + pauseStartTime = Time.time; + isScanning = true; + scanStartTime = Time.time; + scanProgress = 0f; + } + + /// + /// End scanning and resume movement + /// + public void EndScan() + { + isPaused = false; + totalPauseTime += Time.time - pauseStartTime; + isScanning = false; + } + + /// + /// Update scanning S-curve animation + /// + private void UpdateScanAnimation() + { + float elapsed = Time.time - scanStartTime; + scanProgress = Mathf.Clamp01(elapsed / scanPauseDuration); + + // S-curve easing: slow at start and end, fast in middle + float easedProgress = SCurve(scanProgress); + + // Can be used to animate scan overlay or other effects + // The original uses this to pause fish in center while AI analyzes + } + + /// + /// S-curve (sigmoid-like) easing function + /// + public static float SCurve(float t) + { + // Hermite interpolation (smooth step) + return t * t * (3f - 2f * t); + } + + /// + /// More pronounced S-curve + /// + public static float SCurveSteep(float t) + { + // Quintic smooth step + return t * t * t * (t * (t * 6f - 15f) + 10f); + } + + /// + /// Check if fish has moved off screen + /// + public bool IsOffScreen(float screenWidth) + { + return transform.position.x > screenWidth + 100f; + } + + /// + /// Reset to start position + /// + public void Reset() + { + startTime = Time.time; + totalPauseTime = 0f; + transform.position = startPosition; + isPaused = false; + isScanning = false; + } + } +} diff --git a/Unity/AIForOceans/Assets/Scripts/Animation/FishRenderer.cs b/Unity/AIForOceans/Assets/Scripts/Animation/FishRenderer.cs new file mode 100644 index 00000000..ff05bd33 --- /dev/null +++ b/Unity/AIForOceans/Assets/Scripts/Animation/FishRenderer.cs @@ -0,0 +1,160 @@ +using UnityEngine; + +namespace AIForOceans +{ + /// + /// Renders a procedurally generated fish from sprite components + /// + public class FishRenderer : MonoBehaviour + { + [Header("Sprite Renderers")] + public SpriteRenderer bodyRenderer; + public SpriteRenderer eyesRenderer; + public SpriteRenderer mouthRenderer; + public SpriteRenderer dorsalFinRenderer; + public SpriteRenderer pectoralFinFrontRenderer; + public SpriteRenderer pectoralFinBackRenderer; + public SpriteRenderer tailFinRenderer; + public SpriteRenderer scalesRenderer; + + [Header("Sprite Arrays (set in inspector or load from Resources)")] + public Sprite[] bodySprites; + public Sprite[] eyesSprites; + public Sprite[] mouthSprites; + public Sprite[] dorsalFinSprites; + public Sprite[] pectoralFinSprites; + public Sprite[] tailFinSprites; + public Sprite[] scalesSprites; + + // Current fish data + private OceanObjectData currentData; + + /// + /// Set up the fish visual from data + /// + public void SetupFish(OceanObjectData data) + { + currentData = data; + + if (data.objectType != GameConstants.OceanObjectType.Fish) + { + Debug.LogWarning("FishRenderer.SetupFish called with non-fish object"); + return; + } + + // Set sprites + SetSpriteIfValid(bodyRenderer, bodySprites, data.bodyIndex); + SetSpriteIfValid(eyesRenderer, eyesSprites, data.eyesIndex); + SetSpriteIfValid(mouthRenderer, mouthSprites, data.mouthIndex); + SetSpriteIfValid(dorsalFinRenderer, dorsalFinSprites, data.dorsalFinIndex); + SetSpriteIfValid(pectoralFinFrontRenderer, pectoralFinSprites, data.pectoralFinIndex); + SetSpriteIfValid(pectoralFinBackRenderer, pectoralFinSprites, data.pectoralFinIndex); + SetSpriteIfValid(tailFinRenderer, tailFinSprites, data.tailFinIndex); + SetSpriteIfValid(scalesRenderer, scalesSprites, data.scalesIndex); + + // Apply colors + ApplyColors(data); + } + + /// + /// Set sprite if index is valid + /// + private void SetSpriteIfValid(SpriteRenderer renderer, Sprite[] sprites, int index) + { + if (renderer == null || sprites == null || sprites.Length == 0) + return; + + int safeIndex = Mathf.Clamp(index, 0, sprites.Length - 1); + renderer.sprite = sprites[safeIndex]; + } + + /// + /// Apply color tinting to fish parts + /// + private void ApplyColors(OceanObjectData data) + { + // Body uses primary color + if (bodyRenderer != null) + bodyRenderer.color = data.primaryColor; + + // Scales use secondary color + if (scalesRenderer != null) + scalesRenderer.color = data.secondaryColor; + + // Fins use fin color + Color finColor = data.finColor; + if (dorsalFinRenderer != null) + dorsalFinRenderer.color = finColor; + if (pectoralFinFrontRenderer != null) + pectoralFinFrontRenderer.color = finColor; + if (pectoralFinBackRenderer != null) + pectoralFinBackRenderer.color = finColor; + if (tailFinRenderer != null) + tailFinRenderer.color = finColor; + + // Eyes and mouth stay neutral (white/original) + if (eyesRenderer != null) + eyesRenderer.color = Color.white; + if (mouthRenderer != null) + mouthRenderer.color = Color.white; + } + + /// + /// Set sorting order for all parts + /// + public void SetSortingOrder(int baseOrder) + { + // Back to front ordering + if (tailFinRenderer != null) + tailFinRenderer.sortingOrder = baseOrder; + if (pectoralFinBackRenderer != null) + pectoralFinBackRenderer.sortingOrder = baseOrder + 1; + if (dorsalFinRenderer != null) + dorsalFinRenderer.sortingOrder = baseOrder + 2; + if (bodyRenderer != null) + bodyRenderer.sortingOrder = baseOrder + 3; + if (scalesRenderer != null) + scalesRenderer.sortingOrder = baseOrder + 4; + if (pectoralFinFrontRenderer != null) + pectoralFinFrontRenderer.sortingOrder = baseOrder + 5; + if (eyesRenderer != null) + eyesRenderer.sortingOrder = baseOrder + 6; + if (mouthRenderer != null) + mouthRenderer.sortingOrder = baseOrder + 7; + } + + /// + /// Flip the fish horizontally + /// + public void SetFlipped(bool flipped) + { + Vector3 scale = transform.localScale; + scale.x = flipped ? -Mathf.Abs(scale.x) : Mathf.Abs(scale.x); + transform.localScale = scale; + } + + /// + /// Show/hide the fish + /// + public void SetVisible(bool visible) + { + gameObject.SetActive(visible); + } + + /// + /// Apply a highlight effect (for selection) + /// + public void SetHighlighted(bool highlighted) + { + float brightness = highlighted ? 1.2f : 1f; + + // Brighten all parts slightly + if (bodyRenderer != null) + { + Color c = currentData.primaryColor * brightness; + c.a = 1f; + bodyRenderer.color = c; + } + } + } +} diff --git a/Unity/AIForOceans/Assets/Scripts/Audio/SoundManager.cs b/Unity/AIForOceans/Assets/Scripts/Audio/SoundManager.cs new file mode 100644 index 00000000..1ee54196 --- /dev/null +++ b/Unity/AIForOceans/Assets/Scripts/Audio/SoundManager.cs @@ -0,0 +1,219 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace AIForOceans +{ + /// + /// Manages all game audio - sound effects and music + /// + public class SoundManager : MonoBehaviour + { + public static SoundManager Instance { get; private set; } + + [Header("Audio Sources")] + public AudioSource sfxSource; + public AudioSource musicSource; + + [Header("Sound Clips")] + public AudioClip[] yesClips; + public AudioClip[] noClips; + public AudioClip[] sortYesClips; + public AudioClip[] sortNoClips; + public AudioClip buttonClick; + public AudioClip scanSound; + public AudioClip resultSound; + + [Header("Settings")] + [Range(0f, 1f)] + public float sfxVolume = 1f; + [Range(0f, 1f)] + public float musicVolume = 0.5f; + + private Dictionary soundCategories; + + private void Awake() + { + // Singleton + if (Instance != null && Instance != this) + { + Destroy(gameObject); + return; + } + Instance = this; + DontDestroyOnLoad(gameObject); + + // Set up audio sources if not assigned + if (sfxSource == null) + { + sfxSource = gameObject.AddComponent(); + sfxSource.playOnAwake = false; + } + + if (musicSource == null) + { + musicSource = gameObject.AddComponent(); + musicSource.playOnAwake = false; + musicSource.loop = true; + } + + // Organize sounds into categories + soundCategories = new Dictionary + { + { "yes", yesClips }, + { "no", noClips }, + { "sortyes", sortYesClips }, + { "sortno", sortNoClips } + }; + } + + /// + /// Play a random sound from a category + /// + public void PlayRandomFromCategory(string category) + { + if (soundCategories.TryGetValue(category.ToLower(), out AudioClip[] clips)) + { + if (clips != null && clips.Length > 0) + { + int index = Random.Range(0, clips.Length); + PlaySFX(clips[index]); + } + } + } + + /// + /// Play a specific sound effect + /// + public void PlaySFX(AudioClip clip) + { + if (clip != null && sfxSource != null) + { + sfxSource.PlayOneShot(clip, sfxVolume); + } + } + + /// + /// Play button click sound + /// + public void PlayButtonClick() + { + PlaySFX(buttonClick); + } + + /// + /// Play yes training sound + /// + public void PlayYes() + { + PlayRandomFromCategory("yes"); + } + + /// + /// Play no training sound + /// + public void PlayNo() + { + PlayRandomFromCategory("no"); + } + + /// + /// Play sort yes sound (during prediction) + /// + public void PlaySortYes() + { + PlayRandomFromCategory("sortyes"); + } + + /// + /// Play sort no sound (during prediction) + /// + public void PlaySortNo() + { + PlayRandomFromCategory("sortno"); + } + + /// + /// Play scan/analyze sound + /// + public void PlayScan() + { + PlaySFX(scanSound); + } + + /// + /// Play result reveal sound + /// + public void PlayResult() + { + PlaySFX(resultSound); + } + + /// + /// Set SFX volume + /// + public void SetSFXVolume(float volume) + { + sfxVolume = Mathf.Clamp01(volume); + } + + /// + /// Set music volume + /// + public void SetMusicVolume(float volume) + { + musicVolume = Mathf.Clamp01(volume); + if (musicSource != null) + { + musicSource.volume = musicVolume; + } + } + + /// + /// Play background music + /// + public void PlayMusic(AudioClip music) + { + if (musicSource != null && music != null) + { + musicSource.clip = music; + musicSource.volume = musicVolume; + musicSource.Play(); + } + } + + /// + /// Stop background music + /// + public void StopMusic() + { + if (musicSource != null) + { + musicSource.Stop(); + } + } + + /// + /// Fade out music over time + /// + public void FadeOutMusic(float duration) + { + StartCoroutine(FadeOutCoroutine(duration)); + } + + private System.Collections.IEnumerator FadeOutCoroutine(float duration) + { + float startVolume = musicSource.volume; + float elapsed = 0f; + + while (elapsed < duration) + { + elapsed += Time.deltaTime; + musicSource.volume = Mathf.Lerp(startVolume, 0f, elapsed / duration); + yield return null; + } + + musicSource.Stop(); + musicSource.volume = startVolume; + } + } +} diff --git a/Unity/AIForOceans/Assets/Scripts/Classification/ITrainer.cs b/Unity/AIForOceans/Assets/Scripts/Classification/ITrainer.cs new file mode 100644 index 00000000..61e37840 --- /dev/null +++ b/Unity/AIForOceans/Assets/Scripts/Classification/ITrainer.cs @@ -0,0 +1,33 @@ +namespace AIForOceans +{ + /// + /// Interface for all classifiers (naive implementations) + /// + public interface ITrainer + { + /// + /// Add a training example + /// + void AddExample(OceanObjectData data, GameConstants.ClassLabel label); + + /// + /// Train the model (optional for some classifiers) + /// + void Train(); + + /// + /// Make a prediction + /// + (bool prediction, float confidence) Predict(OceanObjectData data); + + /// + /// Get count of examples for a class + /// + int GetExampleCount(GameConstants.ClassLabel label); + + /// + /// Clear all training data + /// + void ClearAll(); + } +} diff --git a/Unity/AIForOceans/Assets/Scripts/Classification/NaiveKNNClassifier.cs b/Unity/AIForOceans/Assets/Scripts/Classification/NaiveKNNClassifier.cs new file mode 100644 index 00000000..2796e6f5 --- /dev/null +++ b/Unity/AIForOceans/Assets/Scripts/Classification/NaiveKNNClassifier.cs @@ -0,0 +1,138 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace AIForOceans +{ + /// + /// Naive K-Nearest Neighbors classifier using attribute matching + /// Replaces TensorFlow.js KNN without actual ML + /// + public class NaiveKNNClassifier : ITrainer + { + private List yesExamples = new List(); + private List noExamples = new List(); + private int k = 3; // Number of neighbors to consider + + private struct TrainingExample + { + public OceanObjectData data; + public float[] features; + } + + public void AddExample(OceanObjectData data, GameConstants.ClassLabel label) + { + var example = new TrainingExample + { + data = data, + features = data.ToFeatureVector() + }; + + if (label == GameConstants.ClassLabel.Yes) + { + yesExamples.Add(example); + } + else + { + noExamples.Add(example); + } + } + + public void Train() + { + // KNN doesn't need explicit training - it's instance-based + } + + public (bool prediction, float confidence) Predict(OceanObjectData data) + { + if (yesExamples.Count == 0 && noExamples.Count == 0) + { + return (true, 0.5f); // No training data, random guess + } + + float[] queryFeatures = data.ToFeatureVector(); + + // Calculate distances to all examples + var distances = new List<(float distance, bool isYes)>(); + + foreach (var example in yesExamples) + { + float dist = CalculateDistance(queryFeatures, example.features); + distances.Add((dist, true)); + } + + foreach (var example in noExamples) + { + float dist = CalculateDistance(queryFeatures, example.features); + distances.Add((dist, false)); + } + + // Sort by distance and take k nearest + var nearest = distances + .OrderBy(d => d.distance) + .Take(Mathf.Min(k, distances.Count)) + .ToList(); + + // Count votes + int yesVotes = nearest.Count(n => n.isYes); + int noVotes = nearest.Count - yesVotes; + + bool prediction = yesVotes >= noVotes; + float confidence = Mathf.Max(yesVotes, noVotes) / (float)nearest.Count; + + // Adjust confidence based on distance + if (nearest.Count > 0) + { + float avgDistance = nearest.Average(n => n.distance); + // Lower distance = higher confidence + confidence *= Mathf.Clamp01(1f - avgDistance); + } + + return (prediction, confidence); + } + + /// + /// Calculate Euclidean distance between feature vectors + /// + private float CalculateDistance(float[] a, float[] b) + { + if (a.Length != b.Length) + { + // Handle different vector lengths + int minLen = Mathf.Min(a.Length, b.Length); + float sum = 0; + for (int i = 0; i < minLen; i++) + { + sum += Mathf.Pow(a[i] - b[i], 2); + } + return Mathf.Sqrt(sum); + } + + float distance = 0; + for (int i = 0; i < a.Length; i++) + { + distance += Mathf.Pow(a[i] - b[i], 2); + } + return Mathf.Sqrt(distance); + } + + public int GetExampleCount(GameConstants.ClassLabel label) + { + return label == GameConstants.ClassLabel.Yes ? yesExamples.Count : noExamples.Count; + } + + public void ClearAll() + { + yesExamples.Clear(); + noExamples.Clear(); + } + + /// + /// Set the number of neighbors to consider + /// + public void SetK(int newK) + { + k = Mathf.Max(1, newK); + } + } +} diff --git a/Unity/AIForOceans/Assets/Scripts/Classification/NaiveSVMClassifier.cs b/Unity/AIForOceans/Assets/Scripts/Classification/NaiveSVMClassifier.cs new file mode 100644 index 00000000..d4fc8297 --- /dev/null +++ b/Unity/AIForOceans/Assets/Scripts/Classification/NaiveSVMClassifier.cs @@ -0,0 +1,247 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace AIForOceans +{ + /// + /// Naive SVM-like classifier using weighted attribute matching + /// Replaces TensorFlow SVM without actual ML + /// Used for word modes (short/long) where we classify by attributes + /// + public class NaiveSVMClassifier : ITrainer + { + private List yesExamples = new List(); + private List noExamples = new List(); + + // Learned weights for each feature (after training) + private float[] featureWeights; + private float bias = 0f; + private bool isTrained = false; + + private struct TrainingExample + { + public OceanObjectData data; + public float[] features; + } + + public void AddExample(OceanObjectData data, GameConstants.ClassLabel label) + { + var example = new TrainingExample + { + data = data, + features = data.ToFeatureVector() + }; + + if (label == GameConstants.ClassLabel.Yes) + { + yesExamples.Add(example); + } + else + { + noExamples.Add(example); + } + + isTrained = false; // Need to retrain + } + + /// + /// Train the classifier by computing feature weights + /// This is a simplified version - real SVM uses optimization + /// + public void Train() + { + if (yesExamples.Count == 0 || noExamples.Count == 0) + { + // Can't train without both classes + isTrained = false; + return; + } + + int featureCount = yesExamples[0].features.Length; + featureWeights = new float[featureCount]; + + // Calculate mean feature values for each class + float[] yesMean = CalculateMean(yesExamples); + float[] noMean = CalculateMean(noExamples); + + // Weights are the difference between means + // Features that differ more between classes get higher weight + for (int i = 0; i < featureCount; i++) + { + featureWeights[i] = yesMean[i] - noMean[i]; + } + + // Normalize weights + float weightSum = featureWeights.Sum(w => Mathf.Abs(w)); + if (weightSum > 0) + { + for (int i = 0; i < featureCount; i++) + { + featureWeights[i] /= weightSum; + } + } + + // Calculate bias as the midpoint + float yesScore = CalculateScore(yesMean); + float noScore = CalculateScore(noMean); + bias = -(yesScore + noScore) / 2f; + + isTrained = true; + } + + public (bool prediction, float confidence) Predict(OceanObjectData data) + { + if (!isTrained) + { + // Fall back to simple similarity if not trained + return PredictBySimilarity(data); + } + + float[] features = data.ToFeatureVector(); + float score = CalculateScore(features) + bias; + + // Positive score = Yes, Negative score = No + bool prediction = score >= 0; + + // Confidence based on distance from decision boundary + float confidence = Mathf.Clamp01(Mathf.Abs(score) * 2f); + + return (prediction, confidence); + } + + /// + /// Calculate weighted score for features + /// + private float CalculateScore(float[] features) + { + float score = 0; + int len = Mathf.Min(features.Length, featureWeights.Length); + + for (int i = 0; i < len; i++) + { + score += features[i] * featureWeights[i]; + } + + return score; + } + + /// + /// Calculate mean feature vector + /// + private float[] CalculateMean(List examples) + { + if (examples.Count == 0) + return new float[0]; + + int featureCount = examples[0].features.Length; + float[] mean = new float[featureCount]; + + foreach (var example in examples) + { + for (int i = 0; i < featureCount; i++) + { + mean[i] += example.features[i]; + } + } + + for (int i = 0; i < featureCount; i++) + { + mean[i] /= examples.Count; + } + + return mean; + } + + /// + /// Fallback prediction using simple similarity + /// + private (bool prediction, float confidence) PredictBySimilarity(OceanObjectData data) + { + if (yesExamples.Count == 0 && noExamples.Count == 0) + { + return (true, 0.5f); + } + + float[] queryFeatures = data.ToFeatureVector(); + + float yesAvgDist = yesExamples.Count > 0 + ? yesExamples.Average(e => CalculateDistance(queryFeatures, e.features)) + : float.MaxValue; + + float noAvgDist = noExamples.Count > 0 + ? noExamples.Average(e => CalculateDistance(queryFeatures, e.features)) + : float.MaxValue; + + bool prediction = yesAvgDist <= noAvgDist; + float totalDist = yesAvgDist + noAvgDist; + float confidence = totalDist > 0 + ? Mathf.Abs(yesAvgDist - noAvgDist) / totalDist + : 0.5f; + + return (prediction, confidence); + } + + /// + /// Calculate Euclidean distance + /// + private float CalculateDistance(float[] a, float[] b) + { + int minLen = Mathf.Min(a.Length, b.Length); + float sum = 0; + + for (int i = 0; i < minLen; i++) + { + sum += Mathf.Pow(a[i] - b[i], 2); + } + + return Mathf.Sqrt(sum); + } + + public int GetExampleCount(GameConstants.ClassLabel label) + { + return label == GameConstants.ClassLabel.Yes ? yesExamples.Count : noExamples.Count; + } + + public void ClearAll() + { + yesExamples.Clear(); + noExamples.Clear(); + featureWeights = null; + bias = 0f; + isTrained = false; + } + + /// + /// Get feature weights for visualization (like original SVM) + /// + public float[] GetFeatureWeights() + { + return featureWeights ?? new float[0]; + } + + /// + /// Get feature importance for a specific prediction + /// + public Dictionary GetFeatureImportance() + { + if (featureWeights == null) + return new Dictionary(); + + var importance = new Dictionary + { + { "body", Mathf.Abs(featureWeights[0]) }, + { "eyes", Mathf.Abs(featureWeights[1]) }, + { "mouth", Mathf.Abs(featureWeights[2]) }, + { "dorsalFin", Mathf.Abs(featureWeights[3]) }, + { "pectoralFin", Mathf.Abs(featureWeights[4]) }, + { "tailFin", Mathf.Abs(featureWeights[5]) }, + { "primaryColor", (Mathf.Abs(featureWeights[6]) + Mathf.Abs(featureWeights[7]) + Mathf.Abs(featureWeights[8])) / 3f }, + { "secondaryColor", (Mathf.Abs(featureWeights[9]) + Mathf.Abs(featureWeights[10]) + Mathf.Abs(featureWeights[11])) / 3f }, + { "finColor", (Mathf.Abs(featureWeights[12]) + Mathf.Abs(featureWeights[13]) + Mathf.Abs(featureWeights[14])) / 3f } + }; + + return importance; + } + } +} diff --git a/Unity/AIForOceans/Assets/Scripts/Core/GameConstants.cs b/Unity/AIForOceans/Assets/Scripts/Core/GameConstants.cs new file mode 100644 index 00000000..9a9082e7 --- /dev/null +++ b/Unity/AIForOceans/Assets/Scripts/Core/GameConstants.cs @@ -0,0 +1,109 @@ +using UnityEngine; + +namespace AIForOceans +{ + /// + /// Game-wide constants matching the original JavaScript implementation + /// + public static class GameConstants + { + // App Modes (Game Levels) + public enum AppMode + { + FishVTrash, // Basic fish vs trash classification + CreaturesVTrashDemo, // Demo showing AI bias + CreaturesVTrash, // Fish + creatures vs trash + Short, // 6 adjectives, SVM + Long // 15 adjectives, SVM + } + + // Scene/Mode states + public enum GameScene + { + Loading, + Words, + Training, + Predicting, + Pond, + IntermediateLoading + } + + // Object types in the ocean + public enum OceanObjectType + { + Fish, + Creature, + Trash + } + + // Classification labels + public enum ClassLabel + { + Yes = 1, + No = 0 + } + + // Fish component counts (matching original) + public const int BODY_COUNT = 19; + public const int EYES_COUNT = 18; + public const int MOUTH_COUNT = 15; + public const int DORSAL_FIN_COUNT = 17; + public const int PECTORAL_FIN_COUNT = 17; + public const int TAIL_FIN_COUNT = 17; + + // Creature types + public static readonly string[] CREATURE_TYPES = new string[] + { + "Crab", "Dolphin", "Jellyfish", "Octopus", "Otter", + "Seahorse", "Snail", "Starfish", "Turtle", "Whale" + }; + + // Trash types + public static readonly string[] TRASH_TYPES = new string[] + { + "6-pack-rings", "Apple", "Banana", "Battery", "Bottle", + "Bulb", "Can", "Coffee", "Fork", "Laundry", "Sock", + "Soda", "Tire", "Wing" + }; + + // Word choices for Short mode (6 adjectives) + public static readonly string[] SHORT_WORDS = new string[] + { + "blue", "orange", "striped", "spotted", "long", "round" + }; + + // Word choices for Long mode (15 adjectives) + public static readonly string[] LONG_WORDS = new string[] + { + "blue", "orange", "striped", "spotted", "long", "round", + "green", "purple", "scary", "friendly", "big", "small", + "fast", "slow", "colorful" + }; + + // Animation constants + public const float BOB_AMPLITUDE = 10f; + public const float BOB_FREQUENCY = 2f; + public const float FISH_SPEED = 100f; + public const float SCAN_PAUSE_DURATION = 1.5f; + + // Training counts per mode + public const int FISH_V_TRASH_TRAINING_COUNT = 6; + public const int CREATURES_V_TRASH_TRAINING_COUNT = 8; + public const int SHORT_TRAINING_COUNT = 30; + public const int LONG_TRAINING_COUNT = 100; + + // UI constants + public const float CANVAS_WIDTH = 400f; + public const float CANVAS_HEIGHT = 400f; + public const float UI_SCALE_FACTOR = 1f; + + // Prediction confidence thresholds + public const float HIGH_CONFIDENCE_THRESHOLD = 0.7f; + public const float LOW_CONFIDENCE_THRESHOLD = 0.3f; + + // Colors for prediction frames + public static readonly Color GREEN_FRAME = new Color(0.2f, 0.8f, 0.2f); // Correct/Yes + public static readonly Color RED_FRAME = new Color(0.8f, 0.2f, 0.2f); // Wrong/No + public static readonly Color BLUE_FRAME = new Color(0.2f, 0.4f, 0.8f); // Uncertain + } +} diff --git a/Unity/AIForOceans/Assets/Scripts/Core/GameInitializer.cs b/Unity/AIForOceans/Assets/Scripts/Core/GameInitializer.cs new file mode 100644 index 00000000..78a313f3 --- /dev/null +++ b/Unity/AIForOceans/Assets/Scripts/Core/GameInitializer.cs @@ -0,0 +1,52 @@ +using UnityEngine; + +namespace AIForOceans +{ + /// + /// Initializes the game on startup + /// + public class GameInitializer : MonoBehaviour + { + [Header("Managers")] + public GameObject gameManagerPrefab; + public GameObject soundManagerPrefab; + public GameObject localizationPrefab; + + [Header("Initial Settings")] + public GameConstants.AppMode defaultMode = GameConstants.AppMode.FishVTrash; + public bool autoStartGame = false; + + private void Awake() + { + // Ensure managers exist + EnsureManagerExists(gameManagerPrefab); + EnsureManagerExists(soundManagerPrefab); + EnsureManagerExists(localizationPrefab); + } + + private void Start() + { + if (autoStartGame && GameManager.Instance != null) + { + GameManager.Instance.StartGame(defaultMode); + } + } + + private void EnsureManagerExists(GameObject prefab) where T : MonoBehaviour + { + if (FindFirstObjectByType() == null) + { + if (prefab != null) + { + Instantiate(prefab); + } + else + { + // Create empty manager + var obj = new GameObject(typeof(T).Name); + obj.AddComponent(); + } + } + } + } +} diff --git a/Unity/AIForOceans/Assets/Scripts/Core/GameManager.cs b/Unity/AIForOceans/Assets/Scripts/Core/GameManager.cs new file mode 100644 index 00000000..8e3929be --- /dev/null +++ b/Unity/AIForOceans/Assets/Scripts/Core/GameManager.cs @@ -0,0 +1,256 @@ +using System; +using UnityEngine; + +namespace AIForOceans +{ + /// + /// Main game manager singleton - handles state and scene transitions + /// + public class GameManager : MonoBehaviour + { + public static GameManager Instance { get; private set; } + + // Events for state changes + public event Action OnSceneChanged; + public event Action OnStateChanged; + public event Action OnTrainingExampleAdded; + public event Action OnPredictionMade; + + // Current game state + public GameState State { get; private set; } + + // Scene controller reference + private SceneController sceneController; + + private void Awake() + { + // Singleton pattern + if (Instance != null && Instance != this) + { + Destroy(gameObject); + return; + } + Instance = this; + DontDestroyOnLoad(gameObject); + + // Initialize state + State = new GameState(); + } + + private void Start() + { + sceneController = GetComponent(); + if (sceneController == null) + { + sceneController = gameObject.AddComponent(); + } + } + + /// + /// Start a new game with the specified mode + /// + public void StartGame(GameConstants.AppMode mode) + { + State.Reset(); + State.appMode = mode; + State.totalTrainingCount = State.GetTrainingCountForMode(); + + // Initialize appropriate trainer + InitializeTrainer(); + + // Generate initial fish data + GenerateFishData(); + + // Start with loading or words scene + if (mode == GameConstants.AppMode.Short || mode == GameConstants.AppMode.Long) + { + ChangeScene(GameConstants.GameScene.Words); + } + else if (mode == GameConstants.AppMode.CreaturesVTrashDemo) + { + // Demo skips training + ChangeScene(GameConstants.GameScene.Predicting); + } + else + { + ChangeScene(GameConstants.GameScene.Training); + } + + OnStateChanged?.Invoke(State); + } + + /// + /// Initialize the appropriate trainer for current mode + /// + private void InitializeTrainer() + { + if (State.appMode == GameConstants.AppMode.Short || + State.appMode == GameConstants.AppMode.Long) + { + State.trainer = new NaiveSVMClassifier(); + } + else + { + State.trainer = new NaiveKNNClassifier(); + } + } + + /// + /// Generate fish/objects for the current mode + /// + private void GenerateFishData() + { + State.fishData.Clear(); + + int count = State.totalTrainingCount + 20; // Training + prediction items + + bool includeFish = true; + bool includeCreatures = State.appMode == GameConstants.AppMode.CreaturesVTrash || + State.appMode == GameConstants.AppMode.CreaturesVTrashDemo; + bool includeTrash = State.appMode != GameConstants.AppMode.Short && + State.appMode != GameConstants.AppMode.Long; + + var generator = new OceanGenerator(); + State.fishData = generator.GenerateOcean(count, includeFish, includeCreatures, includeTrash); + } + + /// + /// Change to a new scene + /// + public void ChangeScene(GameConstants.GameScene newScene) + { + State.currentScene = newScene; + OnSceneChanged?.Invoke(newScene); + OnStateChanged?.Invoke(State); + } + + /// + /// Add a training example + /// + public void AddTrainingExample(OceanObjectData data, bool isYes) + { + State.trainer.AddExample(data, isYes ? GameConstants.ClassLabel.Yes : GameConstants.ClassLabel.No); + State.trainingIndex++; + + OnTrainingExampleAdded?.Invoke(data, isYes); + OnStateChanged?.Invoke(State); + + // Check if training is complete + if (State.trainingIndex >= State.totalTrainingCount) + { + OnTrainingComplete(); + } + } + + /// + /// Called when training is complete + /// + private void OnTrainingComplete() + { + // Train the model (for SVM) + State.trainer.Train(); + + // Move to prediction phase + if (State.appMode == GameConstants.AppMode.Short || + State.appMode == GameConstants.AppMode.Long) + { + ChangeScene(GameConstants.GameScene.IntermediateLoading); + } + else + { + ChangeScene(GameConstants.GameScene.Predicting); + } + } + + /// + /// Make a prediction on an object + /// + public PredictionResult MakePrediction(OceanObjectData data) + { + var (prediction, confidence) = State.trainer.Predict(data); + var result = new PredictionResult(data, prediction, confidence); + + // Determine if correct based on mode + result.isCorrect = DetermineCorrectness(data, prediction); + + State.pondResults.Add(result); + OnPredictionMade?.Invoke(result); + + return result; + } + + /// + /// Determine if a prediction is correct + /// + private bool DetermineCorrectness(OceanObjectData data, bool prediction) + { + switch (State.appMode) + { + case GameConstants.AppMode.FishVTrash: + // Yes = Fish, No = Trash + bool isFishOrCreature = data.objectType != GameConstants.OceanObjectType.Trash; + return prediction == isFishOrCreature; + + case GameConstants.AppMode.CreaturesVTrash: + case GameConstants.AppMode.CreaturesVTrashDemo: + // Yes = Fish/Creature, No = Trash + bool isNotTrash = data.objectType != GameConstants.OceanObjectType.Trash; + return prediction == isNotTrash; + + case GameConstants.AppMode.Short: + case GameConstants.AppMode.Long: + // Subjective - based on selected word + // This would need attribute checking + return true; // Simplified for now + + default: + return true; + } + } + + /// + /// Select a word for Short/Long modes + /// + public void SelectWord(string word) + { + State.selectedWord = word; + ChangeScene(GameConstants.GameScene.Training); + OnStateChanged?.Invoke(State); + } + + /// + /// Move to pond/results scene + /// + public void ShowResults() + { + ChangeScene(GameConstants.GameScene.Pond); + } + + /// + /// Select a fish in the pond to view details + /// + public void SelectPondFish(OceanObjectData fish) + { + State.selectedPondFish = fish; + OnStateChanged?.Invoke(State); + } + + /// + /// Toggle guide visibility + /// + public void ToggleGuide(bool show) + { + State.guideShowing = show; + OnStateChanged?.Invoke(State); + } + + /// + /// Advance to next guide + /// + public void NextGuide() + { + State.guideIndex++; + OnStateChanged?.Invoke(State); + } + } +} diff --git a/Unity/AIForOceans/Assets/Scripts/Core/GameState.cs b/Unity/AIForOceans/Assets/Scripts/Core/GameState.cs new file mode 100644 index 00000000..8e2e1755 --- /dev/null +++ b/Unity/AIForOceans/Assets/Scripts/Core/GameState.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace AIForOceans +{ + /// + /// Central game state - mirrors the original state.js + /// + [Serializable] + public class GameState + { + // Current mode and scene + public GameConstants.AppMode appMode = GameConstants.AppMode.FishVTrash; + public GameConstants.GameScene currentScene = GameConstants.GameScene.Loading; + + // Training state + public int trainingIndex = 0; + public int totalTrainingCount = 0; + public string selectedWord = ""; + + // Fish/object data + public List fishData = new List(); + public List pondResults = new List(); + + // Animation state + public bool isRunning = false; + public bool isPaused = false; + public float currentTime = 0f; + + // UI state + public bool guideShowing = false; + public int guideIndex = 0; + public OceanObjectData selectedPondFish = null; + + // Trainer reference (set at runtime) + [NonSerialized] + public ITrainer trainer; + + /// + /// Reset state for a new game + /// + public void Reset() + { + trainingIndex = 0; + fishData.Clear(); + pondResults.Clear(); + isRunning = false; + isPaused = false; + currentTime = 0f; + guideShowing = false; + guideIndex = 0; + selectedPondFish = null; + trainer?.ClearAll(); + } + + /// + /// Get training count for current mode + /// + public int GetTrainingCountForMode() + { + return appMode switch + { + GameConstants.AppMode.FishVTrash => GameConstants.FISH_V_TRASH_TRAINING_COUNT, + GameConstants.AppMode.CreaturesVTrashDemo => 0, // No training in demo + GameConstants.AppMode.CreaturesVTrash => GameConstants.CREATURES_V_TRASH_TRAINING_COUNT, + GameConstants.AppMode.Short => GameConstants.SHORT_TRAINING_COUNT, + GameConstants.AppMode.Long => GameConstants.LONG_TRAINING_COUNT, + _ => 6 + }; + } + } + + /// + /// Result of a prediction + /// + [Serializable] + public class PredictionResult + { + public OceanObjectData objectData; + public bool prediction; + public float confidence; + public bool isCorrect; + + public PredictionResult(OceanObjectData data, bool pred, float conf) + { + objectData = data; + prediction = pred; + confidence = conf; + isCorrect = false; // Set based on context + } + } +} diff --git a/Unity/AIForOceans/Assets/Scripts/Core/SceneController.cs b/Unity/AIForOceans/Assets/Scripts/Core/SceneController.cs new file mode 100644 index 00000000..d885dde6 --- /dev/null +++ b/Unity/AIForOceans/Assets/Scripts/Core/SceneController.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections; +using UnityEngine; + +namespace AIForOceans +{ + /// + /// Handles scene/mode transitions and UI management + /// + public class SceneController : MonoBehaviour + { + // References to scene UI containers (set in inspector or found at runtime) + public GameObject loadingUI; + public GameObject wordsUI; + public GameObject trainingUI; + public GameObject predictingUI; + public GameObject pondUI; + public GameObject intermediateLoadingUI; + + // Current active scene + private GameConstants.GameScene currentScene; + + private void Start() + { + // Subscribe to scene changes + if (GameManager.Instance != null) + { + GameManager.Instance.OnSceneChanged += HandleSceneChange; + } + } + + private void OnDestroy() + { + if (GameManager.Instance != null) + { + GameManager.Instance.OnSceneChanged -= HandleSceneChange; + } + } + + /// + /// Handle scene change events + /// + private void HandleSceneChange(GameConstants.GameScene newScene) + { + StartCoroutine(TransitionToScene(newScene)); + } + + /// + /// Transition to a new scene with optional fade + /// + private IEnumerator TransitionToScene(GameConstants.GameScene newScene) + { + // Hide current scene + HideAllScenes(); + + // Small delay for transition effect + yield return new WaitForSeconds(0.1f); + + // Show new scene + currentScene = newScene; + ShowScene(newScene); + + // Initialize scene-specific logic + InitializeScene(newScene); + } + + /// + /// Hide all scene UIs + /// + private void HideAllScenes() + { + SetActiveIfNotNull(loadingUI, false); + SetActiveIfNotNull(wordsUI, false); + SetActiveIfNotNull(trainingUI, false); + SetActiveIfNotNull(predictingUI, false); + SetActiveIfNotNull(pondUI, false); + SetActiveIfNotNull(intermediateLoadingUI, false); + } + + /// + /// Show specific scene UI + /// + private void ShowScene(GameConstants.GameScene scene) + { + switch (scene) + { + case GameConstants.GameScene.Loading: + SetActiveIfNotNull(loadingUI, true); + break; + case GameConstants.GameScene.Words: + SetActiveIfNotNull(wordsUI, true); + break; + case GameConstants.GameScene.Training: + SetActiveIfNotNull(trainingUI, true); + break; + case GameConstants.GameScene.Predicting: + SetActiveIfNotNull(predictingUI, true); + break; + case GameConstants.GameScene.Pond: + SetActiveIfNotNull(pondUI, true); + break; + case GameConstants.GameScene.IntermediateLoading: + SetActiveIfNotNull(intermediateLoadingUI, true); + break; + } + } + + /// + /// Initialize scene-specific behavior + /// + private void InitializeScene(GameConstants.GameScene scene) + { + switch (scene) + { + case GameConstants.GameScene.Loading: + StartCoroutine(HandleLoading()); + break; + case GameConstants.GameScene.IntermediateLoading: + StartCoroutine(HandleIntermediateLoading()); + break; + case GameConstants.GameScene.Predicting: + // Start prediction animation + break; + } + } + + /// + /// Handle initial loading + /// + private IEnumerator HandleLoading() + { + // Simulate asset loading + yield return new WaitForSeconds(1f); + + // Move to next appropriate scene + var state = GameManager.Instance.State; + if (state.appMode == GameConstants.AppMode.Short || + state.appMode == GameConstants.AppMode.Long) + { + GameManager.Instance.ChangeScene(GameConstants.GameScene.Words); + } + else + { + GameManager.Instance.ChangeScene(GameConstants.GameScene.Training); + } + } + + /// + /// Handle intermediate loading (SVM training) + /// + private IEnumerator HandleIntermediateLoading() + { + // Show loading while "training" happens + yield return new WaitForSeconds(1.5f); + + // Move to prediction + GameManager.Instance.ChangeScene(GameConstants.GameScene.Predicting); + } + + private void SetActiveIfNotNull(GameObject obj, bool active) + { + if (obj != null) + { + obj.SetActive(active); + } + } + } +} diff --git a/Unity/AIForOceans/Assets/Scripts/Data/CreatureRenderer.cs b/Unity/AIForOceans/Assets/Scripts/Data/CreatureRenderer.cs new file mode 100644 index 00000000..5bf8237e --- /dev/null +++ b/Unity/AIForOceans/Assets/Scripts/Data/CreatureRenderer.cs @@ -0,0 +1,80 @@ +using UnityEngine; + +namespace AIForOceans +{ + /// + /// Renders sea creatures and trash items (single sprite) + /// + public class CreatureRenderer : MonoBehaviour + { + [Header("Sprite Renderer")] + public SpriteRenderer spriteRenderer; + + [Header("Creature Sprites")] + public Sprite[] creatureSprites; // Index matches CREATURE_TYPES + public Sprite[] trashSprites; // Index matches TRASH_TYPES + + private OceanObjectData currentData; + + /// + /// Set up the visual from data + /// + public void Setup(OceanObjectData data) + { + currentData = data; + + if (spriteRenderer == null) + { + spriteRenderer = GetComponent(); + } + + if (spriteRenderer == null) return; + + if (data.objectType == GameConstants.OceanObjectType.Creature) + { + SetCreatureSprite(data.subType); + } + else if (data.objectType == GameConstants.OceanObjectType.Trash) + { + SetTrashSprite(data.subType); + } + } + + private void SetCreatureSprite(string creatureType) + { + int index = System.Array.IndexOf(GameConstants.CREATURE_TYPES, creatureType); + if (index >= 0 && creatureSprites != null && index < creatureSprites.Length) + { + spriteRenderer.sprite = creatureSprites[index]; + } + } + + private void SetTrashSprite(string trashType) + { + int index = System.Array.IndexOf(GameConstants.TRASH_TYPES, trashType); + if (index >= 0 && trashSprites != null && index < trashSprites.Length) + { + spriteRenderer.sprite = trashSprites[index]; + } + } + + /// + /// Set visibility + /// + public void SetVisible(bool visible) + { + gameObject.SetActive(visible); + } + + /// + /// Set sorting order + /// + public void SetSortingOrder(int order) + { + if (spriteRenderer != null) + { + spriteRenderer.sortingOrder = order; + } + } + } +} diff --git a/Unity/AIForOceans/Assets/Scripts/Data/OceanGenerator.cs b/Unity/AIForOceans/Assets/Scripts/Data/OceanGenerator.cs new file mode 100644 index 00000000..b34b96cf --- /dev/null +++ b/Unity/AIForOceans/Assets/Scripts/Data/OceanGenerator.cs @@ -0,0 +1,143 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace AIForOceans +{ + /// + /// Generates random ocean objects for the game + /// + public class OceanGenerator + { + /// + /// Generate a collection of ocean objects + /// + public List GenerateOcean( + int count, + bool includeFish = true, + bool includeCreatures = false, + bool includeTrash = true) + { + var objects = new List(); + + for (int i = 0; i < count; i++) + { + OceanObjectData obj = GenerateRandomObject(i, includeFish, includeCreatures, includeTrash); + objects.Add(obj); + } + + // Shuffle the list + ShuffleList(objects); + + return objects; + } + + /// + /// Generate a single random object based on allowed types + /// + private OceanObjectData GenerateRandomObject( + int id, + bool includeFish, + bool includeCreatures, + bool includeTrash) + { + // Build list of possible types + var possibleTypes = new List(); + + if (includeFish) + possibleTypes.Add(GameConstants.OceanObjectType.Fish); + if (includeCreatures) + possibleTypes.Add(GameConstants.OceanObjectType.Creature); + if (includeTrash) + possibleTypes.Add(GameConstants.OceanObjectType.Trash); + + if (possibleTypes.Count == 0) + { + // Default to fish if nothing selected + return OceanObjectData.CreateRandomFish(id); + } + + // Weight fish higher for better game balance + if (includeFish && (includeCreatures || includeTrash)) + { + // 60% fish, rest split between others + float roll = Random.value; + if (roll < 0.6f) + { + return OceanObjectData.CreateRandomFish(id); + } + else if (includeCreatures && includeTrash) + { + if (roll < 0.8f) + return OceanObjectData.CreateCreature(id); + else + return OceanObjectData.CreateTrash(id); + } + else if (includeCreatures) + { + return OceanObjectData.CreateCreature(id); + } + else + { + return OceanObjectData.CreateTrash(id); + } + } + + // Equal distribution + var selectedType = possibleTypes[Random.Range(0, possibleTypes.Count)]; + + return selectedType switch + { + GameConstants.OceanObjectType.Fish => OceanObjectData.CreateRandomFish(id), + GameConstants.OceanObjectType.Creature => OceanObjectData.CreateCreature(id), + GameConstants.OceanObjectType.Trash => OceanObjectData.CreateTrash(id), + _ => OceanObjectData.CreateRandomFish(id) + }; + } + + /// + /// Generate objects specifically for training + /// + public List GenerateTrainingSet( + int count, + GameConstants.AppMode mode) + { + bool includeFish = true; + bool includeCreatures = mode == GameConstants.AppMode.CreaturesVTrash || + mode == GameConstants.AppMode.CreaturesVTrashDemo; + bool includeTrash = mode != GameConstants.AppMode.Short && + mode != GameConstants.AppMode.Long; + + // For word modes, only fish + if (mode == GameConstants.AppMode.Short || mode == GameConstants.AppMode.Long) + { + includeCreatures = false; + includeTrash = false; + } + + return GenerateOcean(count, includeFish, includeCreatures, includeTrash); + } + + /// + /// Generate objects for prediction phase + /// + public List GeneratePredictionSet( + int count, + GameConstants.AppMode mode) + { + // Similar to training but may have different distribution + return GenerateTrainingSet(count, mode); + } + + /// + /// Fisher-Yates shuffle + /// + private void ShuffleList(List list) + { + for (int i = list.Count - 1; i > 0; i--) + { + int j = Random.Range(0, i + 1); + (list[i], list[j]) = (list[j], list[i]); + } + } + } +} diff --git a/Unity/AIForOceans/Assets/Scripts/Data/OceanObjectData.cs b/Unity/AIForOceans/Assets/Scripts/Data/OceanObjectData.cs new file mode 100644 index 00000000..1506b767 --- /dev/null +++ b/Unity/AIForOceans/Assets/Scripts/Data/OceanObjectData.cs @@ -0,0 +1,219 @@ +using System; +using UnityEngine; + +namespace AIForOceans +{ + /// + /// Data structure for any ocean object (fish, creature, trash) + /// + [Serializable] + public class OceanObjectData + { + // Basic info + public int id; + public GameConstants.OceanObjectType objectType; + public string name; + + // Fish-specific components (indices into sprite arrays) + public int bodyIndex; + public int eyesIndex; + public int mouthIndex; + public int dorsalFinIndex; + public int pectoralFinIndex; + public int tailFinIndex; + public int scalesIndex; + + // Colors + public Color primaryColor; + public Color secondaryColor; + public Color finColor; + + // For creatures/trash - the specific type name + public string subType; + + // Position and animation + public Vector2 position; + public float bobPhase; + public float speed; + + // Classification label (set during training) + public GameConstants.ClassLabel? label; + + /// + /// Create a random fish + /// + public static OceanObjectData CreateRandomFish(int id) + { + var fish = new OceanObjectData + { + id = id, + objectType = GameConstants.OceanObjectType.Fish, + name = $"Fish_{id}", + + // Random component indices + bodyIndex = UnityEngine.Random.Range(0, GameConstants.BODY_COUNT), + eyesIndex = UnityEngine.Random.Range(0, GameConstants.EYES_COUNT), + mouthIndex = UnityEngine.Random.Range(0, GameConstants.MOUTH_COUNT), + dorsalFinIndex = UnityEngine.Random.Range(0, GameConstants.DORSAL_FIN_COUNT), + pectoralFinIndex = UnityEngine.Random.Range(0, GameConstants.PECTORAL_FIN_COUNT), + tailFinIndex = UnityEngine.Random.Range(0, GameConstants.TAIL_FIN_COUNT), + scalesIndex = UnityEngine.Random.Range(0, 3), // Fewer scale options + + // Random colors + primaryColor = GetRandomFishColor(), + secondaryColor = GetRandomFishColor(), + finColor = GetRandomFishColor(), + + // Animation + bobPhase = UnityEngine.Random.Range(0f, Mathf.PI * 2f), + speed = GameConstants.FISH_SPEED * UnityEngine.Random.Range(0.8f, 1.2f) + }; + + return fish; + } + + /// + /// Create a creature + /// + public static OceanObjectData CreateCreature(int id, string creatureType = null) + { + if (string.IsNullOrEmpty(creatureType)) + { + int index = UnityEngine.Random.Range(0, GameConstants.CREATURE_TYPES.Length); + creatureType = GameConstants.CREATURE_TYPES[index]; + } + + return new OceanObjectData + { + id = id, + objectType = GameConstants.OceanObjectType.Creature, + name = creatureType, + subType = creatureType, + bobPhase = UnityEngine.Random.Range(0f, Mathf.PI * 2f), + speed = GameConstants.FISH_SPEED * UnityEngine.Random.Range(0.6f, 1.0f) + }; + } + + /// + /// Create trash + /// + public static OceanObjectData CreateTrash(int id, string trashType = null) + { + if (string.IsNullOrEmpty(trashType)) + { + int index = UnityEngine.Random.Range(0, GameConstants.TRASH_TYPES.Length); + trashType = GameConstants.TRASH_TYPES[index]; + } + + return new OceanObjectData + { + id = id, + objectType = GameConstants.OceanObjectType.Trash, + name = trashType, + subType = trashType, + bobPhase = UnityEngine.Random.Range(0f, Mathf.PI * 2f), + speed = GameConstants.FISH_SPEED * UnityEngine.Random.Range(0.4f, 0.8f) + }; + } + + /// + /// Get a random fish-appropriate color + /// + private static Color GetRandomFishColor() + { + // Predefined fish color palettes + Color[] fishColors = new Color[] + { + new Color(0.2f, 0.6f, 1.0f), // Blue + new Color(1.0f, 0.6f, 0.2f), // Orange + new Color(0.2f, 0.8f, 0.4f), // Green + new Color(0.8f, 0.2f, 0.8f), // Purple + new Color(1.0f, 1.0f, 0.2f), // Yellow + new Color(1.0f, 0.4f, 0.4f), // Red/Pink + new Color(0.4f, 0.8f, 0.8f), // Cyan + new Color(0.6f, 0.4f, 0.2f), // Brown + }; + + return fishColors[UnityEngine.Random.Range(0, fishColors.Length)]; + } + + /// + /// Convert to feature vector for classification + /// + public float[] ToFeatureVector() + { + if (objectType == GameConstants.OceanObjectType.Fish) + { + // Feature vector includes all fish attributes + return new float[] + { + bodyIndex / (float)GameConstants.BODY_COUNT, + eyesIndex / (float)GameConstants.EYES_COUNT, + mouthIndex / (float)GameConstants.MOUTH_COUNT, + dorsalFinIndex / (float)GameConstants.DORSAL_FIN_COUNT, + pectoralFinIndex / (float)GameConstants.PECTORAL_FIN_COUNT, + tailFinIndex / (float)GameConstants.TAIL_FIN_COUNT, + primaryColor.r, + primaryColor.g, + primaryColor.b, + secondaryColor.r, + secondaryColor.g, + secondaryColor.b, + finColor.r, + finColor.g, + finColor.b + }; + } + else + { + // For creatures/trash, use a simpler encoding + float typeValue = objectType == GameConstants.OceanObjectType.Creature ? 0.5f : 1.0f; + return new float[] { typeValue, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + } + } + + /// + /// Check if fish has a specific attribute (for word modes) + /// + public bool HasAttribute(string attribute) + { + if (objectType != GameConstants.OceanObjectType.Fish) + return false; + + attribute = attribute.ToLower(); + + return attribute switch + { + "blue" => IsColorDominant(Color.blue), + "orange" => IsColorDominant(new Color(1f, 0.5f, 0f)), + "green" => IsColorDominant(Color.green), + "purple" => IsColorDominant(new Color(0.5f, 0f, 0.5f)), + "yellow" => IsColorDominant(Color.yellow), + "red" => IsColorDominant(Color.red), + "striped" => scalesIndex == 1, + "spotted" => scalesIndex == 2, + "long" => bodyIndex >= GameConstants.BODY_COUNT / 2, + "round" => bodyIndex < GameConstants.BODY_COUNT / 2, + "big" => bodyIndex >= GameConstants.BODY_COUNT * 2 / 3, + "small" => bodyIndex < GameConstants.BODY_COUNT / 3, + _ => false + }; + } + + private bool IsColorDominant(Color targetColor) + { + float threshold = 0.4f; + return ColorDistance(primaryColor, targetColor) < threshold || + ColorDistance(secondaryColor, targetColor) < threshold; + } + + private float ColorDistance(Color a, Color b) + { + return Mathf.Sqrt( + Mathf.Pow(a.r - b.r, 2) + + Mathf.Pow(a.g - b.g, 2) + + Mathf.Pow(a.b - b.b, 2) + ); + } + } +} diff --git a/Unity/AIForOceans/Assets/Scripts/Scenes/PredictionSceneController.cs b/Unity/AIForOceans/Assets/Scripts/Scenes/PredictionSceneController.cs new file mode 100644 index 00000000..bda47818 --- /dev/null +++ b/Unity/AIForOceans/Assets/Scripts/Scenes/PredictionSceneController.cs @@ -0,0 +1,199 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace AIForOceans +{ + /// + /// Controls the prediction scene flow + /// + public class PredictionSceneController : MonoBehaviour + { + [Header("References")] + public FishRenderer fishRenderer; + public FishAnimator fishAnimator; + public AIBot aiBot; + public PredictionUI predictionUI; + + [Header("Animation Settings")] + public Vector2 spawnPosition = new Vector2(-200f, 0f); + public Vector2 scanPosition = new Vector2(0f, 0f); + public Vector2 exitPosition = new Vector2(200f, 0f); + public float swimSpeed = 100f; + public float scanDuration = 1.5f; + public float delayBetweenFish = 0.5f; + + [Header("Prediction Settings")] + public int fishToPredictCount = 20; + + private Queue fishQueue; + private bool isRunning = false; + private int predictedCount = 0; + + private void OnEnable() + { + StartPredictions(); + } + + private void OnDisable() + { + StopPredictions(); + } + + /// + /// Start the prediction sequence + /// + public void StartPredictions() + { + if (isRunning) return; + + var state = GameManager.Instance.State; + + // Get fish for prediction (skip training fish) + fishQueue = new Queue(); + int startIndex = state.totalTrainingCount; + + for (int i = startIndex; i < state.fishData.Count && i < startIndex + fishToPredictCount; i++) + { + fishQueue.Enqueue(state.fishData[i]); + } + + predictedCount = 0; + isRunning = true; + + StartCoroutine(RunPredictionLoop()); + } + + /// + /// Stop predictions + /// + public void StopPredictions() + { + isRunning = false; + StopAllCoroutines(); + } + + /// + /// Main prediction loop + /// + private IEnumerator RunPredictionLoop() + { + while (isRunning && fishQueue.Count > 0) + { + OceanObjectData fish = fishQueue.Dequeue(); + yield return StartCoroutine(ProcessFish(fish)); + yield return new WaitForSeconds(delayBetweenFish); + } + + // All predictions complete + isRunning = false; + OnAllPredictionsComplete(); + } + + /// + /// Process a single fish through prediction + /// + private IEnumerator ProcessFish(OceanObjectData fish) + { + // Set up fish visual + if (fishRenderer != null && fish.objectType == GameConstants.OceanObjectType.Fish) + { + fishRenderer.SetupFish(fish); + } + + // Swim in from left + yield return StartCoroutine(SwimTo(spawnPosition, scanPosition, 1f)); + + // AI scans the fish + if (aiBot != null) + { + aiBot.SetExpression(AIBot.Expression.Scanning); + } + + if (predictionUI != null) + { + predictionUI.ShowScanning(); + } + + // Play scan sound + if (SoundManager.Instance != null) + { + SoundManager.Instance.PlayScan(); + } + + // Wait for scan + yield return new WaitForSeconds(scanDuration); + + // Make prediction + PredictionResult result = GameManager.Instance.MakePrediction(fish); + predictedCount++; + + // AI reacts + if (aiBot != null) + { + aiBot.SetExpression(result.prediction ? AIBot.Expression.Yes : AIBot.Expression.No); + } + + // Brief pause to show result + yield return new WaitForSeconds(0.5f); + + // Swim out + yield return StartCoroutine(SwimTo(scanPosition, exitPosition, 0.5f)); + } + + /// + /// Animate swimming from one position to another + /// + private IEnumerator SwimTo(Vector2 from, Vector2 to, float duration) + { + if (fishRenderer == null) yield break; + + Transform fishTransform = fishRenderer.transform; + fishTransform.position = from; + + float elapsed = 0f; + + while (elapsed < duration) + { + elapsed += Time.deltaTime; + float t = FishAnimator.SCurve(elapsed / duration); + fishTransform.position = Vector2.Lerp(from, to, t); + yield return null; + } + + fishTransform.position = to; + } + + /// + /// Called when all predictions are complete + /// + private void OnAllPredictionsComplete() + { + // Hide prediction UI + if (predictionUI != null) + { + predictionUI.Hide(); + } + + // Move to pond scene + GameManager.Instance.ShowResults(); + } + + /// + /// Skip to results immediately + /// + public void SkipToResults() + { + StopPredictions(); + + // Make remaining predictions instantly + while (fishQueue != null && fishQueue.Count > 0) + { + OceanObjectData fish = fishQueue.Dequeue(); + GameManager.Instance.MakePrediction(fish); + } + + OnAllPredictionsComplete(); + } + } +} diff --git a/Unity/AIForOceans/Assets/Scripts/Scenes/TrainingSceneController.cs b/Unity/AIForOceans/Assets/Scripts/Scenes/TrainingSceneController.cs new file mode 100644 index 00000000..547e2f41 --- /dev/null +++ b/Unity/AIForOceans/Assets/Scripts/Scenes/TrainingSceneController.cs @@ -0,0 +1,186 @@ +using System.Collections; +using UnityEngine; + +namespace AIForOceans +{ + /// + /// Controls the training scene flow + /// + public class TrainingSceneController : MonoBehaviour + { + [Header("References")] + public FishRenderer currentFishRenderer; + public FishAnimator currentFishAnimator; + public AIBot aiBot; + public TrainingUI trainingUI; + + [Header("Spawn Settings")] + public Vector2 spawnPosition = new Vector2(-200f, 0f); + public Vector2 centerPosition = new Vector2(0f, 0f); + public float fishEnterDuration = 1f; + + private OceanObjectData currentFish; + private bool isAnimating = false; + + private void OnEnable() + { + // Subscribe to events + if (GameManager.Instance != null) + { + GameManager.Instance.OnTrainingExampleAdded += OnExampleAdded; + } + + // Show first fish + ShowNextFish(); + } + + private void OnDisable() + { + if (GameManager.Instance != null) + { + GameManager.Instance.OnTrainingExampleAdded -= OnExampleAdded; + } + } + + /// + /// Show the next fish in the training sequence + /// + private void ShowNextFish() + { + var state = GameManager.Instance.State; + + if (state.trainingIndex >= state.fishData.Count) + { + Debug.LogWarning("No more fish to show"); + return; + } + + currentFish = state.fishData[state.trainingIndex]; + + // Set up the fish renderer + if (currentFishRenderer != null) + { + if (currentFish.objectType == GameConstants.OceanObjectType.Fish) + { + currentFishRenderer.SetupFish(currentFish); + } + // For creatures/trash, would need different rendering + } + + // Animate fish entering + StartCoroutine(AnimateFishEnter()); + } + + /// + /// Animate fish swimming in from left + /// + private IEnumerator AnimateFishEnter() + { + isAnimating = true; + + if (currentFishRenderer == null) + { + isAnimating = false; + yield break; + } + + Transform fishTransform = currentFishRenderer.transform; + fishTransform.position = spawnPosition; + + float elapsed = 0f; + + while (elapsed < fishEnterDuration) + { + elapsed += Time.deltaTime; + float t = elapsed / fishEnterDuration; + + // Ease in-out + float easedT = FishAnimator.SCurve(t); + + fishTransform.position = Vector2.Lerp(spawnPosition, centerPosition, easedT); + yield return null; + } + + fishTransform.position = centerPosition; + isAnimating = false; + + // AI bot looks at fish + if (aiBot != null) + { + aiBot.SetExpression(AIBot.Expression.Thinking); + } + } + + /// + /// Called when user labels current fish + /// + private void OnExampleAdded(OceanObjectData data, bool isYes) + { + // AI bot reacts + if (aiBot != null) + { + aiBot.SetExpression(isYes ? AIBot.Expression.Yes : AIBot.Expression.No); + } + + // Animate fish exit and show next + StartCoroutine(AnimateFishExitAndShowNext(isYes)); + } + + /// + /// Animate fish exiting and bring in next + /// + private IEnumerator AnimateFishExitAndShowNext(bool wasYes) + { + if (currentFishRenderer == null) + yield break; + + isAnimating = true; + + Transform fishTransform = currentFishRenderer.transform; + Vector2 exitPosition = new Vector2(200f, 0f); + + float elapsed = 0f; + float exitDuration = 0.5f; + + while (elapsed < exitDuration) + { + elapsed += Time.deltaTime; + float t = elapsed / exitDuration; + float easedT = FishAnimator.SCurve(t); + + fishTransform.position = Vector2.Lerp(centerPosition, exitPosition, easedT); + yield return null; + } + + // Brief pause + yield return new WaitForSeconds(0.2f); + + // Check if more fish to show + var state = GameManager.Instance.State; + if (state.trainingIndex < state.totalTrainingCount && + state.trainingIndex < state.fishData.Count) + { + ShowNextFish(); + } + + isAnimating = false; + } + + /// + /// Skip current animation (for fast users) + /// + public void SkipAnimation() + { + if (isAnimating) + { + StopAllCoroutines(); + isAnimating = false; + + if (currentFishRenderer != null) + { + currentFishRenderer.transform.position = centerPosition; + } + } + } + } +} diff --git a/Unity/AIForOceans/Assets/Scripts/UI/AIBot.cs b/Unity/AIForOceans/Assets/Scripts/UI/AIBot.cs new file mode 100644 index 00000000..60cdfe78 --- /dev/null +++ b/Unity/AIForOceans/Assets/Scripts/UI/AIBot.cs @@ -0,0 +1,153 @@ +using System.Collections; +using UnityEngine; +using UnityEngine.UI; + +namespace AIForOceans +{ + /// + /// AI Bot character with different expression states + /// + public class AIBot : MonoBehaviour + { + public enum Expression + { + Neutral, + Thinking, + Yes, + No, + Scanning, + Closed + } + + [Header("Sprite References")] + public Image bodyImage; + public Image headImage; + public Image expressionImage; + public Image scanOverlayImage; + + [Header("Expression Sprites")] + public Sprite neutralSprite; + public Sprite thinkingSprite; + public Sprite yesSprite; + public Sprite noSprite; + public Sprite scanningSprite; + public Sprite closedSprite; + + [Header("Animation")] + public float expressionDuration = 1f; + public float bobAmplitude = 5f; + public float bobFrequency = 1f; + + private Expression currentExpression = Expression.Neutral; + private Coroutine expressionCoroutine; + private Vector3 basePosition; + + private void Start() + { + basePosition = transform.position; + SetExpression(Expression.Neutral); + + // Hide scan overlay initially + if (scanOverlayImage != null) + { + scanOverlayImage.enabled = false; + } + } + + private void Update() + { + // Gentle bobbing animation + float bobOffset = Mathf.Sin(Time.time * bobFrequency) * bobAmplitude; + transform.position = basePosition + new Vector3(0f, bobOffset, 0f); + } + + /// + /// Set the bot's expression + /// + public void SetExpression(Expression expression) + { + currentExpression = expression; + + // Update expression sprite + if (expressionImage != null) + { + expressionImage.sprite = expression switch + { + Expression.Neutral => neutralSprite, + Expression.Thinking => thinkingSprite, + Expression.Yes => yesSprite, + Expression.No => noSprite, + Expression.Scanning => scanningSprite, + Expression.Closed => closedSprite, + _ => neutralSprite + }; + } + + // Show/hide scan overlay + if (scanOverlayImage != null) + { + scanOverlayImage.enabled = expression == Expression.Scanning; + } + + // Auto-return to neutral for reactions + if (expression == Expression.Yes || expression == Expression.No) + { + if (expressionCoroutine != null) + { + StopCoroutine(expressionCoroutine); + } + expressionCoroutine = StartCoroutine(ReturnToNeutral()); + } + } + + /// + /// Return to neutral after showing expression + /// + private IEnumerator ReturnToNeutral() + { + yield return new WaitForSeconds(expressionDuration); + + if (currentExpression == Expression.Yes || currentExpression == Expression.No) + { + SetExpression(Expression.Neutral); + } + } + + /// + /// Play a celebration animation + /// + public void Celebrate() + { + StartCoroutine(CelebrationAnimation()); + } + + private IEnumerator CelebrationAnimation() + { + SetExpression(Expression.Yes); + + // Bounce animation + float duration = 0.5f; + float elapsed = 0f; + Vector3 startPos = basePosition; + + while (elapsed < duration) + { + elapsed += Time.deltaTime; + float t = elapsed / duration; + float bounce = Mathf.Sin(t * Mathf.PI * 3) * 10f * (1f - t); + transform.position = startPos + new Vector3(0f, bounce, 0f); + yield return null; + } + + transform.position = startPos; + } + + /// + /// Get current expression + /// + public Expression GetExpression() + { + return currentExpression; + } + } +} diff --git a/Unity/AIForOceans/Assets/Scripts/UI/ConfirmationDialog.cs b/Unity/AIForOceans/Assets/Scripts/UI/ConfirmationDialog.cs new file mode 100644 index 00000000..65e72637 --- /dev/null +++ b/Unity/AIForOceans/Assets/Scripts/UI/ConfirmationDialog.cs @@ -0,0 +1,133 @@ +using System; +using UnityEngine; +using UnityEngine.UI; +using TMPro; + +namespace AIForOceans +{ + /// + /// Reusable confirmation dialog + /// + public class ConfirmationDialog : MonoBehaviour + { + [Header("UI Elements")] + public GameObject dialogPanel; + public TextMeshProUGUI titleText; + public TextMeshProUGUI messageText; + public Button confirmButton; + public Button cancelButton; + public TextMeshProUGUI confirmButtonText; + public TextMeshProUGUI cancelButtonText; + + // Callbacks + private Action onConfirm; + private Action onCancel; + + private void Start() + { + // Set up button listeners + if (confirmButton != null) + { + confirmButton.onClick.AddListener(OnConfirmClicked); + } + + if (cancelButton != null) + { + cancelButton.onClick.AddListener(OnCancelClicked); + } + + // Hide by default + Hide(); + } + + /// + /// Show the dialog with custom content + /// + public void Show( + string title, + string message, + string confirmText = "OK", + string cancelText = "Cancel", + Action onConfirmCallback = null, + Action onCancelCallback = null) + { + if (dialogPanel != null) + { + dialogPanel.SetActive(true); + } + + if (titleText != null) + { + titleText.text = title; + } + + if (messageText != null) + { + messageText.text = message; + } + + if (confirmButtonText != null) + { + confirmButtonText.text = confirmText; + } + + if (cancelButtonText != null) + { + cancelButtonText.text = cancelText; + } + + onConfirm = onConfirmCallback; + onCancel = onCancelCallback; + + // Show/hide cancel button based on callback + if (cancelButton != null) + { + cancelButton.gameObject.SetActive(onCancel != null || !string.IsNullOrEmpty(cancelText)); + } + } + + /// + /// Show a simple alert (OK only) + /// + public void ShowAlert(string title, string message, Action onOk = null) + { + Show(title, message, "OK", "", onOk, null); + } + + /// + /// Hide the dialog + /// + public void Hide() + { + if (dialogPanel != null) + { + dialogPanel.SetActive(false); + } + + onConfirm = null; + onCancel = null; + } + + private void OnConfirmClicked() + { + if (SoundManager.Instance != null) + { + SoundManager.Instance.PlayButtonClick(); + } + + onConfirm?.Invoke(); + Hide(); + } + + private void OnCancelClicked() + { + if (SoundManager.Instance != null) + { + SoundManager.Instance.PlayButtonClick(); + } + + onCancel?.Invoke(); + Hide(); + } + } +} diff --git a/Unity/AIForOceans/Assets/Scripts/UI/GameButton.cs b/Unity/AIForOceans/Assets/Scripts/UI/GameButton.cs new file mode 100644 index 00000000..b16ea7b5 --- /dev/null +++ b/Unity/AIForOceans/Assets/Scripts/UI/GameButton.cs @@ -0,0 +1,166 @@ +using System; +using UnityEngine; +using UnityEngine.UI; +using UnityEngine.EventSystems; + +namespace AIForOceans +{ + /// + /// Custom button with states and feedback + /// + public class GameButton : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler, IPointerDownHandler, IPointerUpHandler + { + [Header("Button Settings")] + public string buttonId; + public bool playSound = true; + + [Header("Visual Settings")] + public Color normalColor = Color.white; + public Color hoverColor = new Color(0.9f, 0.9f, 0.9f); + public Color pressedColor = new Color(0.7f, 0.7f, 0.7f); + public Color disabledColor = new Color(0.5f, 0.5f, 0.5f, 0.5f); + + [Header("Animation")] + public float scaleOnPress = 0.95f; + public float animationSpeed = 10f; + + // Events + public event Action OnClicked; + public event Action OnPressed; + public event Action OnReleased; + + // Components + private Image image; + private Button button; + private RectTransform rectTransform; + + // State + private bool isHovered = false; + private bool isPressed = false; + private bool isInteractable = true; + private Vector3 originalScale; + private Vector3 targetScale; + + private void Awake() + { + image = GetComponent(); + button = GetComponent