From 5de62563cf482561624bea10593b666cf48d696c Mon Sep 17 00:00:00 2001 From: Michal Pikulski Date: Wed, 10 Dec 2025 12:12:30 +0100 Subject: [PATCH] Working materials with two modes - binary and continuous --- .../MazeObjectProgressiveTemplate.mat | 59 ++ .../MazeObjectProgressiveTemplate.mat.meta | 8 + .../MichalTesting_TrashMaze.unity | 14 +- .../CardSystem/UI/Pages/AlbumViewPage.cs | 1 - .../Core/Settings/PlayerFollowerSettings.cs | 5 + .../Core/Settings/SettingsInterfaces.cs | 1 + .../TrashMaze/Core/PulverController.cs | 16 +- .../Minigames/TrashMaze/Objects/Editor.meta | 3 + .../Objects/Editor/RevealableObjectEditor.cs | 99 +++ .../Editor/RevealableObjectEditor.cs.meta | 3 + .../TrashMaze/Objects/RevealableObject.cs | 448 ++++++++++- .../ObjectVisibilityProgressive.shader | 106 +++ .../ObjectVisibilityProgressive.shader.meta | 3 + Assets/Shaders/TrashMaze/RevealStamp.shader | 64 ++ .../Shaders/TrashMaze/RevealStamp.shader.meta | 3 + ...actoring_summary_movement_and_trashmaze.md | 718 ------------------ 16 files changed, 791 insertions(+), 760 deletions(-) create mode 100644 Assets/Art/Materials/TrashMaze/MazeObjectProgressiveTemplate.mat create mode 100644 Assets/Art/Materials/TrashMaze/MazeObjectProgressiveTemplate.mat.meta create mode 100644 Assets/Scripts/Minigames/TrashMaze/Objects/Editor.meta create mode 100644 Assets/Scripts/Minigames/TrashMaze/Objects/Editor/RevealableObjectEditor.cs create mode 100644 Assets/Scripts/Minigames/TrashMaze/Objects/Editor/RevealableObjectEditor.cs.meta create mode 100644 Assets/Shaders/TrashMaze/ObjectVisibilityProgressive.shader create mode 100644 Assets/Shaders/TrashMaze/ObjectVisibilityProgressive.shader.meta create mode 100644 Assets/Shaders/TrashMaze/RevealStamp.shader create mode 100644 Assets/Shaders/TrashMaze/RevealStamp.shader.meta delete mode 100644 docs/refactoring_summary_movement_and_trashmaze.md diff --git a/Assets/Art/Materials/TrashMaze/MazeObjectProgressiveTemplate.mat b/Assets/Art/Materials/TrashMaze/MazeObjectProgressiveTemplate.mat new file mode 100644 index 00000000..0aea4403 --- /dev/null +++ b/Assets/Art/Materials/TrashMaze/MazeObjectProgressiveTemplate.mat @@ -0,0 +1,59 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!21 &2100000 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: MazeObjectProgressiveTemplate + m_Shader: {fileID: 4800000, guid: 732fa975ac924d89bb0078279d2cdb0b, type: 3} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _AlphaTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MainTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _MaskTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _NormalMap: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _OutlineTex: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + - _RevealMask: + m_Texture: {fileID: 0} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _EnableExternalAlpha: 0 + - _IsInVision: 0 + - _ZWrite: 0 + m_Colors: + - _Color: {r: 1, g: 1, b: 1, a: 1} + - _RendererColor: {r: 1, g: 1, b: 1, a: 1} + m_BuildTextureStacks: [] + m_AllowLocking: 1 diff --git a/Assets/Art/Materials/TrashMaze/MazeObjectProgressiveTemplate.mat.meta b/Assets/Art/Materials/TrashMaze/MazeObjectProgressiveTemplate.mat.meta new file mode 100644 index 00000000..3502b15d --- /dev/null +++ b/Assets/Art/Materials/TrashMaze/MazeObjectProgressiveTemplate.mat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6e053220514a0c64883d9484863533fe +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 2100000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scenes/TestingStuff/MichalTesting_TrashMaze.unity b/Assets/Scenes/TestingStuff/MichalTesting_TrashMaze.unity index a6ac9f72..206fdfb6 100644 --- a/Assets/Scenes/TestingStuff/MichalTesting_TrashMaze.unity +++ b/Assets/Scenes/TestingStuff/MichalTesting_TrashMaze.unity @@ -504,7 +504,7 @@ PrefabInstance: m_Modifications: - target: {fileID: 6259373434446242904, guid: 07f826f001311e04984c3efc9ee2b897, type: 3} propertyPath: m_Name - value: RevealableObject (2) + value: RevealableObject_Full_2 objectReference: {fileID: 0} - target: {fileID: 6487644332527623320, guid: 07f826f001311e04984c3efc9ee2b897, type: 3} propertyPath: m_SortingOrder @@ -565,7 +565,7 @@ PrefabInstance: m_Modifications: - target: {fileID: 6259373434446242904, guid: 07f826f001311e04984c3efc9ee2b897, type: 3} propertyPath: m_Name - value: RevealableObject (1) + value: RevealableObject_Full objectReference: {fileID: 0} - target: {fileID: 6487644332527623320, guid: 07f826f001311e04984c3efc9ee2b897, type: 3} propertyPath: m_SortingOrder @@ -1035,14 +1035,22 @@ PrefabInstance: serializedVersion: 3 m_TransformParent: {fileID: 0} m_Modifications: + - target: {fileID: 397845239581813408, guid: 07f826f001311e04984c3efc9ee2b897, type: 3} + propertyPath: revealMode + value: 1 + objectReference: {fileID: 0} - target: {fileID: 6259373434446242904, guid: 07f826f001311e04984c3efc9ee2b897, type: 3} propertyPath: m_Name - value: RevealableObject + value: RevealableObject_Partial objectReference: {fileID: 0} - target: {fileID: 6487644332527623320, guid: 07f826f001311e04984c3efc9ee2b897, type: 3} propertyPath: m_SortingOrder value: 5 objectReference: {fileID: 0} + - target: {fileID: 6487644332527623320, guid: 07f826f001311e04984c3efc9ee2b897, type: 3} + propertyPath: 'm_Materials.Array.data[0]' + value: + objectReference: {fileID: 2100000, guid: 6e053220514a0c64883d9484863533fe, type: 2} - target: {fileID: 7983424933738472089, guid: 07f826f001311e04984c3efc9ee2b897, type: 3} propertyPath: m_LocalPosition.x value: 0 diff --git a/Assets/Scripts/CardSystem/UI/Pages/AlbumViewPage.cs b/Assets/Scripts/CardSystem/UI/Pages/AlbumViewPage.cs index e303c85d..7ce03503 100644 --- a/Assets/Scripts/CardSystem/UI/Pages/AlbumViewPage.cs +++ b/Assets/Scripts/CardSystem/UI/Pages/AlbumViewPage.cs @@ -452,7 +452,6 @@ namespace UI.CardSystem if (cardData == null) return null; var allSlots = FindObjectsByType(FindObjectsSortMode.None); - foreach (var slot in allSlots) { if (slot.TargetCardDefinition != null && diff --git a/Assets/Scripts/Core/Settings/PlayerFollowerSettings.cs b/Assets/Scripts/Core/Settings/PlayerFollowerSettings.cs index 73c26d40..60e8f8ec 100644 --- a/Assets/Scripts/Core/Settings/PlayerFollowerSettings.cs +++ b/Assets/Scripts/Core/Settings/PlayerFollowerSettings.cs @@ -72,6 +72,9 @@ namespace AppleHills.Core.Settings [SerializeField] private float followUpdateInterval = 0.1f; [SerializeField] private float followerSpeedMultiplier = 1.2f; [SerializeField] private float heldIconDisplayHeight = 2.0f; + + [Header("Trash Maze Vision")] + [SerializeField] private float trashMazeVisionRadius = 8f; public float FollowDistance => followDistance; public float ManualMoveSmooth => manualMoveSmooth; @@ -81,6 +84,7 @@ namespace AppleHills.Core.Settings public float FollowUpdateInterval => followUpdateInterval; public float FollowerSpeedMultiplier => followerSpeedMultiplier; public float HeldIconDisplayHeight => heldIconDisplayHeight; + public float TrashMazeVisionRadius => trashMazeVisionRadius; public void Validate() { @@ -92,6 +96,7 @@ namespace AppleHills.Core.Settings followUpdateInterval = Mathf.Max(0.01f, followUpdateInterval); followerSpeedMultiplier = Mathf.Max(0.1f, followerSpeedMultiplier); heldIconDisplayHeight = Mathf.Max(0f, heldIconDisplayHeight); + trashMazeVisionRadius = Mathf.Max(1f, trashMazeVisionRadius); } } } diff --git a/Assets/Scripts/Core/Settings/SettingsInterfaces.cs b/Assets/Scripts/Core/Settings/SettingsInterfaces.cs index b34da0b2..ffb4ec92 100644 --- a/Assets/Scripts/Core/Settings/SettingsInterfaces.cs +++ b/Assets/Scripts/Core/Settings/SettingsInterfaces.cs @@ -49,6 +49,7 @@ namespace AppleHills.Core.Settings float FollowUpdateInterval { get; } float FollowerSpeedMultiplier { get; } float HeldIconDisplayHeight { get; } + float TrashMazeVisionRadius { get; } } /// diff --git a/Assets/Scripts/Minigames/TrashMaze/Core/PulverController.cs b/Assets/Scripts/Minigames/TrashMaze/Core/PulverController.cs index 8348b127..43598a9a 100644 --- a/Assets/Scripts/Minigames/TrashMaze/Core/PulverController.cs +++ b/Assets/Scripts/Minigames/TrashMaze/Core/PulverController.cs @@ -14,16 +14,16 @@ namespace Minigames.TrashMaze.Core { public static PulverController Instance { get; private set; } - [Header("Vision")] - [SerializeField] private float visionRadius = 3f; - // Cached shader property IDs for performance private static readonly int PlayerWorldPosID = Shader.PropertyToID("_PlayerWorldPos"); private static readonly int VisionRadiusID = Shader.PropertyToID("_VisionRadius"); + // Vision radius loaded from settings + private float _visionRadius; + // Public accessors for other systems public static Vector2 PlayerPosition => Instance != null ? Instance.transform.position : Vector2.zero; - public static float VisionRadius => Instance != null ? Instance.visionRadius : 3f; + public static float VisionRadius => Instance != null ? Instance._visionRadius : 8f; internal override void OnManagedAwake() { @@ -46,6 +46,10 @@ namespace Minigames.TrashMaze.Core { var configs = GameManager.GetSettingsObject(); _movementSettings = configs.TrashMazeMovement; + + // Load vision radius from follower settings + _visionRadius = configs.FollowerMovement.TrashMazeVisionRadius; + Logging.Debug($"[PulverController] Loaded vision radius from settings: {_visionRadius}"); } protected override void Update() @@ -62,7 +66,7 @@ namespace Minigames.TrashMaze.Core private void UpdateShaderGlobals() { Shader.SetGlobalVector(PlayerWorldPosID, transform.position); - Shader.SetGlobalFloat(VisionRadiusID, visionRadius); + Shader.SetGlobalFloat(VisionRadiusID, _visionRadius); } internal override void OnManagedDestroy() @@ -80,7 +84,7 @@ namespace Minigames.TrashMaze.Core /// public void SetVisionRadius(float radius) { - visionRadius = Mathf.Max(0.1f, radius); + _visionRadius = Mathf.Max(0.1f, radius); } } } diff --git a/Assets/Scripts/Minigames/TrashMaze/Objects/Editor.meta b/Assets/Scripts/Minigames/TrashMaze/Objects/Editor.meta new file mode 100644 index 00000000..81c056a8 --- /dev/null +++ b/Assets/Scripts/Minigames/TrashMaze/Objects/Editor.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3a935f5e791c46df8920c2c33f1c24c0 +timeCreated: 1765361215 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/TrashMaze/Objects/Editor/RevealableObjectEditor.cs b/Assets/Scripts/Minigames/TrashMaze/Objects/Editor/RevealableObjectEditor.cs new file mode 100644 index 00000000..befd7ac0 --- /dev/null +++ b/Assets/Scripts/Minigames/TrashMaze/Objects/Editor/RevealableObjectEditor.cs @@ -0,0 +1,99 @@ +using UnityEngine; +using UnityEditor; +using Minigames.TrashMaze.Objects; + +namespace Minigames.TrashMaze.Editor +{ + [CustomEditor(typeof(RevealableObject))] + public class RevealableObjectEditor : UnityEditor.Editor + { + private RenderTexture _cachedStampTexture; + private Texture2D _previewTexture; + + public override void OnInspectorGUI() + { + DrawDefaultInspector(); + + // Only show debug info in play mode + if (!Application.isPlaying) + { + return; + } + + EditorGUILayout.Space(10); + EditorGUILayout.LabelField("Progressive Reveal Debug (Play Mode Only)", EditorStyles.boldLabel); + EditorGUILayout.HelpBox("Shows the current stamp texture for Progressive reveal mode.", MessageType.Info); + + RevealableObject revealableObject = (RevealableObject)target; + + // Use reflection to get private _revealStampTexture field + var field = typeof(RevealableObject).GetField("_revealStampTexture", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + + if (field != null) + { + RenderTexture stampTexture = field.GetValue(revealableObject) as RenderTexture; + + if (stampTexture != null) + { + // Display stamp texture info + EditorGUILayout.LabelField("Stamp Texture:", $"{stampTexture.width}x{stampTexture.height}"); + + // Show preview of stamp texture + GUILayout.Label("Reveal Mask Preview (White = Revealed):"); + + // Create preview texture if needed + if (_cachedStampTexture != stampTexture || _previewTexture == null) + { + _cachedStampTexture = stampTexture; + + if (_previewTexture != null) + { + DestroyImmediate(_previewTexture); + } + + _previewTexture = new Texture2D(stampTexture.width, stampTexture.height, TextureFormat.R8, false); + _previewTexture.filterMode = FilterMode.Point; + } + + // Copy RenderTexture to Texture2D for preview + RenderTexture.active = stampTexture; + _previewTexture.ReadPixels(new Rect(0, 0, stampTexture.width, stampTexture.height), 0, 0); + _previewTexture.Apply(); + RenderTexture.active = null; + + // Display preview with fixed size + float previewSize = 256f; + float aspectRatio = (float)stampTexture.height / stampTexture.width; + Rect previewRect = GUILayoutUtility.GetRect(previewSize, previewSize * aspectRatio); + EditorGUI.DrawPreviewTexture(previewRect, _previewTexture, null, ScaleMode.ScaleToFit); + + // Auto-refresh in play mode + if (Application.isPlaying) + { + Repaint(); + } + } + else + { + EditorGUILayout.HelpBox("No stamp texture found. Make sure object is in Progressive reveal mode.", MessageType.Warning); + } + } + else + { + EditorGUILayout.HelpBox("Could not access stamp texture via reflection.", MessageType.Error); + } + } + + private void OnDisable() + { + // Clean up preview texture + if (_previewTexture != null) + { + DestroyImmediate(_previewTexture); + _previewTexture = null; + } + } + } +} + diff --git a/Assets/Scripts/Minigames/TrashMaze/Objects/Editor/RevealableObjectEditor.cs.meta b/Assets/Scripts/Minigames/TrashMaze/Objects/Editor/RevealableObjectEditor.cs.meta new file mode 100644 index 00000000..f338d84d --- /dev/null +++ b/Assets/Scripts/Minigames/TrashMaze/Objects/Editor/RevealableObjectEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1d081993ee424269bf8eae99db36a54c +timeCreated: 1765361215 \ No newline at end of file diff --git a/Assets/Scripts/Minigames/TrashMaze/Objects/RevealableObject.cs b/Assets/Scripts/Minigames/TrashMaze/Objects/RevealableObject.cs index 91659fd2..f75eeb1a 100644 --- a/Assets/Scripts/Minigames/TrashMaze/Objects/RevealableObject.cs +++ b/Assets/Scripts/Minigames/TrashMaze/Objects/RevealableObject.cs @@ -1,9 +1,20 @@ using Core; using Minigames.TrashMaze.Core; using UnityEngine; +using System.Collections; +using AppleHills.Core.Settings; namespace Minigames.TrashMaze.Objects { + /// + /// Reveal mode for object visibility + /// + public enum RevealMode + { + Binary, // Simple on/off reveal (current system) + Progressive // Pixel-by-pixel progressive reveal with stamp texture + } + /// /// Component for objects that need reveal memory (obstacles, booster packs, treasures). /// Tracks if object has been revealed and updates material properties accordingly. @@ -11,10 +22,17 @@ namespace Minigames.TrashMaze.Objects [RequireComponent(typeof(SpriteRenderer))] public class RevealableObject : MonoBehaviour { + [Header("Reveal Settings")] + [SerializeField] private RevealMode revealMode = RevealMode.Binary; + [Header("Textures")] [SerializeField] private Sprite normalSprite; [SerializeField] private Sprite outlineSprite; + [Header("Progressive Reveal Settings")] + [SerializeField, Range(0.5f, 3f)] private float stampWorldRadius = 1.5f; + [Tooltip("Size of each stamp in world units. Smaller = more gradual reveal. Should be smaller than vision radius.")] + [Header("Object Type")] [SerializeField] private bool isBoosterPack = false; [SerializeField] private bool isExit = false; @@ -27,64 +45,403 @@ namespace Minigames.TrashMaze.Objects // Material property IDs (cached for performance) private static readonly int IsRevealedID = Shader.PropertyToID("_IsRevealed"); private static readonly int IsInVisionID = Shader.PropertyToID("_IsInVision"); + private static readonly int RevealMaskID = Shader.PropertyToID("_RevealMask"); + + // Progressive reveal system + private RenderTexture _revealStampTexture; + private Coroutine _stampCoroutine; + private Bounds _objectBounds; + private float _activationDistance; + + // Binary reveal system + private Coroutine _binaryRevealCoroutine; private void Awake() { _spriteRenderer = GetComponent(); - // Create instance material so each object has its own properties - if (_spriteRenderer.material != null) + if (_spriteRenderer.material == null) { - _instanceMaterial = new Material(_spriteRenderer.material); - _spriteRenderer.material = _instanceMaterial; + Logging.Error($"[RevealableObject] No material assigned to {gameObject.name}"); + return; + } + + // Create instance material + _instanceMaterial = new Material(_spriteRenderer.material); + _spriteRenderer.material = _instanceMaterial; + + // Call mode-specific initialization - COMPLETELY SEPARATE + if (revealMode == RevealMode.Binary) + { + InitializeBinaryMode(); + } + else if (revealMode == RevealMode.Progressive) + { + InitializeProgressiveMode(); + } + } + + // ======================================== + // BINARY MODE INITIALIZATION + // ======================================== + + private void InitializeBinaryMode() + { + // Validate Binary shader + string shaderName = _instanceMaterial.shader.name; + if (!shaderName.Contains("ObjectVisibility") || shaderName.Contains("Progressive")) + { + Logging.Error($"[RevealableObject] {gameObject.name} Binary mode needs shader 'TrashMaze/ObjectVisibility', currently: {shaderName}"); + } + + // Set initial Binary mode properties + _instanceMaterial.SetFloat(IsRevealedID, 0f); + _instanceMaterial.SetFloat(IsInVisionID, 0f); + + // Set textures + if (normalSprite != null) + { + _instanceMaterial.SetTexture("_MainTex", normalSprite.texture); + } + if (outlineSprite != null) + { + _instanceMaterial.SetTexture("_OutlineTex", outlineSprite.texture); + } + + Logging.Debug($"[RevealableObject] {gameObject.name} Binary mode initialized"); + } + + // ======================================== + // PROGRESSIVE MODE INITIALIZATION + // ======================================== + + private void InitializeProgressiveMode() + { + // Validate Progressive shader + string shaderName = _instanceMaterial.shader.name; + if (!shaderName.Contains("ObjectVisibilityProgressive")) + { + Logging.Error($"[RevealableObject] {gameObject.name} Progressive mode needs shader 'TrashMaze/ObjectVisibilityProgressive', currently: {shaderName}"); + } + + // Initialize progressive reveal system + InitializeProgressiveReveal(); + } + + /// + /// Initialize progressive reveal system with dynamic texture sizing + /// + private void InitializeProgressiveReveal() + { + // Get object bounds for UV calculations + _objectBounds = _spriteRenderer.bounds; + + // Load activation distance from settings (use vision radius from follower settings) + var configs = GameManager.GetSettingsObject(); + _activationDistance = configs.FollowerMovement.TrashMazeVisionRadius; + + Logging.Debug($"[RevealableObject] {gameObject.name} loaded activation distance from settings: {_activationDistance}"); + + // Dynamically determine texture size from sprite + int textureWidth = 128; + int textureHeight = 128; + + if (normalSprite != null && normalSprite.texture != null) + { + // Use sprite's texture resolution (match 1:1 for pixel-perfect) + textureWidth = normalSprite.texture.width; + textureHeight = normalSprite.texture.height; } else { - Logging.Error($"[RevealableObject] No material assigned to {gameObject.name}"); + Logging.Warning($"[RevealableObject] {gameObject.name} in Progressive mode but normalSprite not assigned! Using default 128x128 texture. Assign Normal Sprite in inspector!"); } + + // Create reveal stamp texture (R8 format = 8-bit grayscale, minimal memory) + _revealStampTexture = new RenderTexture(textureWidth, textureHeight, 0, RenderTextureFormat.R8); + _revealStampTexture.filterMode = FilterMode.Point; // Sharp edges for binary reveals + _revealStampTexture.wrapMode = TextureWrapMode.Clamp; + _revealStampTexture.Create(); // Explicitly create the texture + + // Clear to black (nothing revealed initially) + RenderTexture previousActive = RenderTexture.active; + RenderTexture.active = _revealStampTexture; + GL.Clear(false, true, Color.black); + RenderTexture.active = previousActive; + + Logging.Debug($"[RevealableObject] {gameObject.name} cleared reveal texture to black - should be invisible initially"); + + // Set Progressive shader properties + _instanceMaterial.SetTexture(RevealMaskID, _revealStampTexture); + + // Progressive shader uses global _PlayerWorldPos and _VisionRadius (set by PulverController) + // No need to set _IsInVision - shader does per-pixel distance checks + + // Set textures + if (normalSprite != null) + { + _instanceMaterial.SetTexture("_MainTex", normalSprite.texture); + } + if (outlineSprite != null) + { + _instanceMaterial.SetTexture("_OutlineTex", outlineSprite.texture); + } + + Logging.Debug($"[RevealableObject] {gameObject.name} Progressive mode initialized: {textureWidth}x{textureHeight} reveal texture"); } private void Start() { - // Initialize material properties - if (_instanceMaterial != null) + if (PulverController.Instance == null) { - _instanceMaterial.SetFloat(IsRevealedID, 0f); - _instanceMaterial.SetFloat(IsInVisionID, 0f); - - // Set textures if provided - if (normalSprite != null) - { - _instanceMaterial.SetTexture("_MainTex", normalSprite.texture); - } - if (outlineSprite != null) - { - _instanceMaterial.SetTexture("_OutlineTex", outlineSprite.texture); - } + Logging.Error($"[RevealableObject] {gameObject.name} cannot start - PulverController not found!"); + return; + } + + // Start mode-specific runtime logic - COMPLETELY SEPARATE + if (revealMode == RevealMode.Binary) + { + StartBinaryModeTracking(); + } + else if (revealMode == RevealMode.Progressive) + { + StartProgressiveModeTracking(); } } - private void Update() + // ======================================== + // BINARY MODE: Start tracking + // ======================================== + + private void StartBinaryModeTracking() { - if (_isCollected || _instanceMaterial == null) return; + _binaryRevealCoroutine = StartCoroutine(BinaryRevealTrackingCoroutine()); + Logging.Debug($"[RevealableObject] {gameObject.name} Binary mode tracking started"); + } + + // ======================================== + // PROGRESSIVE MODE: Start tracking + // ======================================== + + private void StartProgressiveModeTracking() + { + // Subscribe to movement events for stamping + PulverController.Instance.OnMovementStarted += OnPlayerMovementStarted; + PulverController.Instance.OnMovementStopped += OnPlayerMovementStopped; - // Calculate distance to player - float distance = Vector2.Distance(transform.position, PulverController.PlayerPosition); - bool isInRadius = distance < PulverController.VisionRadius; + // NO vision tracking coroutine - Progressive shader does per-pixel distance checks using global _PlayerWorldPos - // Update real-time vision flag - _instanceMaterial.SetFloat(IsInVisionID, isInRadius ? 1f : 0f); - - // Set revealed flag (once true, stays true) - if (isInRadius && !_hasBeenRevealed) + Logging.Debug($"[RevealableObject] {gameObject.name} Progressive mode tracking started"); + } + + + // ======================================== + // BINARY MODE: Vision-based reveal coroutine + // ======================================== + + /// + /// Binary mode coroutine - tracks player distance and updates vision/reveal flags + /// Runs continuously, checks every 0.1s for performance + /// + private IEnumerator BinaryRevealTrackingCoroutine() + { + while (!_isCollected && _instanceMaterial != null) { - _hasBeenRevealed = true; - _instanceMaterial.SetFloat(IsRevealedID, 1f); + // Calculate distance to player + float distance = Vector2.Distance(transform.position, PulverController.PlayerPosition); + bool isInRadius = distance < PulverController.VisionRadius; - Logging.Debug($"[RevealableObject] {gameObject.name} revealed!"); + // Set real-time vision flag (controls shader color vs outline) + _instanceMaterial.SetFloat(IsInVisionID, isInRadius ? 1f : 0f); + + // Set reveal flag (once revealed, stays revealed) + if (isInRadius && !_hasBeenRevealed) + { + _hasBeenRevealed = true; + _instanceMaterial.SetFloat(IsRevealedID, 1f); + Logging.Debug($"[RevealableObject] {gameObject.name} revealed!"); + } + + // Wait before next check (reduces CPU load) + yield return new WaitForSeconds(0.1f); } } + // ======================================== + // PROGRESSIVE MODE: Event-based stamp reveal + // ======================================== + + /// + /// Called when player starts moving - begin stamping if near object + /// + private void OnPlayerMovementStarted() + { + if (_isCollected || revealMode != RevealMode.Progressive) return; + + // Check if player's vision circle could overlap with object bounds + // Use closest point on bounds to check distance + Vector2 playerPos = PulverController.PlayerPosition; + Vector2 closestPoint = _objectBounds.ClosestPoint(playerPos); + float distanceToBounds = Vector2.Distance(playerPos, closestPoint); + + // Start stamping if vision radius could reach the object + // Add padding to account for vision radius overlap + float activationThreshold = _activationDistance + _objectBounds.extents.magnitude; + + if (distanceToBounds < activationThreshold && _stampCoroutine == null) + { + _stampCoroutine = StartCoroutine(StampRevealCoroutine()); + Logging.Debug($"[RevealableObject] {gameObject.name} started stamping coroutine (distanceToBounds: {distanceToBounds:F2}, threshold: {activationThreshold:F2})"); + } + } + + /// + /// Called when player stops moving - stop stamping + /// + private void OnPlayerMovementStopped() + { + if (_stampCoroutine != null) + { + StopCoroutine(_stampCoroutine); + _stampCoroutine = null; + } + } + + /// + /// Coroutine that stamps reveal texture while player is moving and near object + /// + private IEnumerator StampRevealCoroutine() + { + while (!_isCollected) + { + // Check if player's vision circle overlaps with object bounds + Vector2 playerPos = PulverController.PlayerPosition; + Vector2 closestPoint = _objectBounds.ClosestPoint(playerPos); + float distanceToBounds = Vector2.Distance(playerPos, closestPoint); + + // Calculate activation threshold with padding + float activationThreshold = _activationDistance + _objectBounds.extents.magnitude; + + // If player moved too far away, stop stamping + if (distanceToBounds >= activationThreshold) + { + Logging.Debug($"[RevealableObject] {gameObject.name} stopping stamping coroutine (too far)"); + _stampCoroutine = null; + yield break; + } + + // Stamp if player's vision radius reaches any part of the object + if (distanceToBounds < PulverController.VisionRadius) + { + StampPlayerPosition(); + } + + // Wait before next stamp (reduces GPU writes) + yield return new WaitForSeconds(0.1f); + } + } + + /// + /// Stamp the player's current position onto the reveal texture + /// Direct CPU-based stamping - calculates circle-rectangle intersection in world space + /// + private void StampPlayerPosition() + { + if (_revealStampTexture == null) + { + Logging.Warning($"[RevealableObject] {gameObject.name} cannot stamp: texture is null"); + return; + } + + // Get player position and vision radius in world space + Vector2 playerWorldPos = PulverController.PlayerPosition; + float visionRadius = PulverController.VisionRadius; + + // Get object bounds in world space + Vector2 boundsMin = _objectBounds.min; + Vector2 boundsSize = _objectBounds.size; + + // Get texture dimensions + int texWidth = _revealStampTexture.width; + int texHeight = _revealStampTexture.height; + + // Calculate the bounding box of the circle in world space + Vector2 circleMin = playerWorldPos - Vector2.one * visionRadius; + Vector2 circleMax = playerWorldPos + Vector2.one * visionRadius; + + // Calculate intersection of circle bounding box with object bounds + Vector2 intersectMin = new Vector2( + Mathf.Max(circleMin.x, boundsMin.x), + Mathf.Max(circleMin.y, boundsMin.y) + ); + Vector2 intersectMax = new Vector2( + Mathf.Min(circleMax.x, boundsMin.x + boundsSize.x), + Mathf.Min(circleMax.y, boundsMin.y + boundsSize.y) + ); + + // Check if there's any intersection + if (intersectMin.x >= intersectMax.x || intersectMin.y >= intersectMax.y) + { + return; // No intersection, nothing to stamp + } + + // Convert world space intersection to texture pixel coordinates + int pixelMinX = Mathf.FloorToInt((intersectMin.x - boundsMin.x) / boundsSize.x * texWidth); + int pixelMaxX = Mathf.CeilToInt((intersectMax.x - boundsMin.x) / boundsSize.x * texWidth); + int pixelMinY = Mathf.FloorToInt((intersectMin.y - boundsMin.y) / boundsSize.y * texHeight); + int pixelMaxY = Mathf.CeilToInt((intersectMax.y - boundsMin.y) / boundsSize.y * texHeight); + + // Clamp to texture bounds + pixelMinX = Mathf.Max(0, pixelMinX); + pixelMaxX = Mathf.Min(texWidth, pixelMaxX); + pixelMinY = Mathf.Max(0, pixelMinY); + pixelMaxY = Mathf.Min(texHeight, pixelMaxY); + + // Read current texture data + RenderTexture.active = _revealStampTexture; + Texture2D tempTex = new Texture2D(texWidth, texHeight, TextureFormat.R8, false); + tempTex.ReadPixels(new Rect(0, 0, texWidth, texHeight), 0, 0); + tempTex.Apply(); + + // Stamp pixels within the circle + bool anyPixelStamped = false; + float radiusSquared = visionRadius * visionRadius; + + for (int py = pixelMinY; py < pixelMaxY; py++) + { + for (int px = pixelMinX; px < pixelMaxX; px++) + { + // Convert pixel coordinates back to world space + float worldX = boundsMin.x + (px / (float)texWidth) * boundsSize.x; + float worldY = boundsMin.y + (py / (float)texHeight) * boundsSize.y; + + // Check if this pixel is within the circle + float dx = worldX - playerWorldPos.x; + float dy = worldY - playerWorldPos.y; + float distSquared = dx * dx + dy * dy; + + if (distSquared <= radiusSquared) + { + // Stamp this pixel (set to white) + tempTex.SetPixel(px, py, Color.white); + anyPixelStamped = true; + } + } + } + + if (anyPixelStamped) + { + // Upload modified texture back to GPU + tempTex.Apply(); + Graphics.CopyTexture(tempTex, _revealStampTexture); + + Logging.Debug($"[RevealableObject] {gameObject.name} stamped pixels at world pos ({playerWorldPos.x:F2}, {playerWorldPos.y:F2}), radius {visionRadius:F2}"); + } + + RenderTexture.active = null; + Destroy(tempTex); + } + private void OnTriggerEnter2D(Collider2D other) { // Check if player is interacting @@ -135,6 +492,33 @@ namespace Minigames.TrashMaze.Objects private void OnDestroy() { + // Stop Binary mode coroutine + if (_binaryRevealCoroutine != null) + { + StopCoroutine(_binaryRevealCoroutine); + } + + // Unsubscribe from Progressive mode movement events + if (revealMode == RevealMode.Progressive && PulverController.Instance != null) + { + PulverController.Instance.OnMovementStarted -= OnPlayerMovementStarted; + PulverController.Instance.OnMovementStopped -= OnPlayerMovementStopped; + } + + // Stop Progressive mode stamping coroutine + if (_stampCoroutine != null) + { + StopCoroutine(_stampCoroutine); + } + + // Clean up progressive reveal resources + if (_revealStampTexture != null) + { + _revealStampTexture.Release(); + Destroy(_revealStampTexture); + } + + // Clean up instance material if (_instanceMaterial != null) { diff --git a/Assets/Shaders/TrashMaze/ObjectVisibilityProgressive.shader b/Assets/Shaders/TrashMaze/ObjectVisibilityProgressive.shader new file mode 100644 index 00000000..91c2dd08 --- /dev/null +++ b/Assets/Shaders/TrashMaze/ObjectVisibilityProgressive.shader @@ -0,0 +1,106 @@ +Shader "TrashMaze/ObjectVisibilityProgressive" +{ + Properties + { + _MainTex ("Normal Texture (Color)", 2D) = "white" {} + _OutlineTex ("Outline Texture (White)", 2D) = "white" {} + _RevealMask ("Reveal Mask", 2D) = "black" {} + [PerRendererData] _IsInVision ("Is In Vision", Float) = 0 + } + + SubShader + { + Tags + { + "Queue"="Transparent" + "RenderType"="Transparent" + "IgnoreProjector"="True" + } + + Blend SrcAlpha OneMinusSrcAlpha + ZWrite Off + Cull Off + + Pass + { + HLSLPROGRAM + #pragma vertex vert + #pragma fragment frag + #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" + + struct Attributes + { + float4 positionOS : POSITION; + float2 uv : TEXCOORD0; + }; + + struct Varyings + { + float2 uv : TEXCOORD0; + float4 positionCS : SV_POSITION; + float3 positionWS : TEXCOORD1; + }; + + TEXTURE2D(_MainTex); + SAMPLER(sampler_MainTex); + TEXTURE2D(_OutlineTex); + SAMPLER(sampler_OutlineTex); + TEXTURE2D(_RevealMask); + SAMPLER(sampler_RevealMask); + float4 _MainTex_ST; + + // Global shader properties (set by PulverController) + float2 _PlayerWorldPos; + float _VisionRadius; + + Varyings vert(Attributes input) + { + Varyings output; + VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz); + output.positionCS = vertexInput.positionCS; + output.positionWS = vertexInput.positionWS; + output.uv = TRANSFORM_TEX(input.uv, _MainTex); + return output; + } + + half4 frag(Varyings input) : SV_Target + { + // Sample reveal mask (0 = not revealed, 1 = revealed) + float revealAmount = SAMPLE_TEXTURE2D(_RevealMask, sampler_RevealMask, input.uv).r; + + // Binary decision: is this pixel revealed? + bool isRevealed = revealAmount > 0.5; + + // If pixel was never revealed, hide it completely + if (!isRevealed) + { + return half4(0, 0, 0, 0); + } + + // Calculate per-pixel distance to player in world space + float2 pixelWorldPos = input.positionWS.xy; + float distanceToPlayer = distance(pixelWorldPos, _PlayerWorldPos); + + // Three-state logic per pixel: + // 1. Never revealed -> hide (handled above) + // 2. Revealed + currently in player vision radius -> show color + // 3. Revealed + outside player vision radius -> show outline + + if (distanceToPlayer < _VisionRadius) + { + // Pixel is revealed AND currently in vision - show color + return SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv); + } + else + { + // Pixel is revealed but NOT currently in vision - show outline + return SAMPLE_TEXTURE2D(_OutlineTex, sampler_OutlineTex, input.uv); + } + } + ENDHLSL + } + } + + FallBack "Transparent/Diffuse" +} + diff --git a/Assets/Shaders/TrashMaze/ObjectVisibilityProgressive.shader.meta b/Assets/Shaders/TrashMaze/ObjectVisibilityProgressive.shader.meta new file mode 100644 index 00000000..888bdade --- /dev/null +++ b/Assets/Shaders/TrashMaze/ObjectVisibilityProgressive.shader.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 732fa975ac924d89bb0078279d2cdb0b +timeCreated: 1765358086 \ No newline at end of file diff --git a/Assets/Shaders/TrashMaze/RevealStamp.shader b/Assets/Shaders/TrashMaze/RevealStamp.shader new file mode 100644 index 00000000..4c2506c1 --- /dev/null +++ b/Assets/Shaders/TrashMaze/RevealStamp.shader @@ -0,0 +1,64 @@ +Shader "Hidden/TrashMaze/RevealStamp" +{ + Properties + { + _StampPos ("Stamp Position", Vector) = (0.5, 0.5, 0, 0) + _StampRadius ("Stamp Radius", Float) = 0.2 + } + + SubShader + { + Tags { "Queue"="Overlay" "RenderType"="Opaque" } + + // Additive blend to accumulate stamps + Blend One One + ZTest Always + ZWrite Off + Cull Off + + Pass + { + HLSLPROGRAM + #pragma vertex vert + #pragma fragment frag + #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" + + struct Attributes + { + float4 positionOS : POSITION; + float2 uv : TEXCOORD0; + }; + + struct Varyings + { + float4 positionCS : SV_POSITION; + float2 uv : TEXCOORD0; + }; + + float4 _StampPos; + float _StampRadius; + + Varyings vert(Attributes input) + { + Varyings output; + output.positionCS = TransformObjectToHClip(input.positionOS.xyz); + output.uv = input.uv; + return output; + } + + half4 frag(Varyings input) : SV_Target + { + // Calculate distance from stamp center + float dist = distance(input.uv, _StampPos.xy); + + // Binary circle: 1.0 inside radius, 0.0 outside + float alpha = dist < _StampRadius ? 1.0 : 0.0; + + // Return white with calculated alpha (additive blend accumulates) + return half4(alpha, alpha, alpha, alpha); + } + ENDHLSL + } + } +} + diff --git a/Assets/Shaders/TrashMaze/RevealStamp.shader.meta b/Assets/Shaders/TrashMaze/RevealStamp.shader.meta new file mode 100644 index 00000000..6edb3cd6 --- /dev/null +++ b/Assets/Shaders/TrashMaze/RevealStamp.shader.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 33afb80a55e64e53b8552498ad61acfa +timeCreated: 1765358067 \ No newline at end of file diff --git a/docs/refactoring_summary_movement_and_trashmaze.md b/docs/refactoring_summary_movement_and_trashmaze.md deleted file mode 100644 index 44e4efc8..00000000 --- a/docs/refactoring_summary_movement_and_trashmaze.md +++ /dev/null @@ -1,718 +0,0 @@ -# Movement System Refactoring & Trash Maze Visibility System - Implementation Summary - -**Date:** December 8, 2025 -**Scope:** Player movement architecture refactoring + Trash Maze minigame visibility system - ---- - -## 🎯 Overview - -This refactoring addressed technical debt in the movement system and implemented a new fog-of-war visibility system for the Trash Maze minigame. The work involved: - -1. **Splitting settings interfaces** to separate player movement from follower behavior -2. **Creating a reusable base controller** for all player movement implementations -3. **Refactoring existing controllers** to use the new base class -4. **Implementing Trash Maze visibility system** with per-object reveal memory and URP shaders - ---- - -## 📊 Changes Summary - -**Statistics:** -- **19 files changed** -- **1,139 insertions**, 1,556 deletions (net: -417 lines) -- **8 new files created** (5 C#, 2 shaders, 1 meta) -- **11 files modified** - ---- - -## 🔧 Part 1: Movement System Refactoring - -### Problem Statement - -**Technical Debt Identified:** -- `IPlayerFollowerSettings` interface mixed player movement properties with follower-specific properties -- Player movement code duplicated between `PlayerTouchController` and would be needed again for `PulverController` -- No clean way to have different movement configurations for different contexts (overworld vs minigames) -- FollowerController incorrectly depended on player movement settings - -### Solution Architecture - -Created a **container pattern** with separate settings interfaces: - -``` -IPlayerMovementConfigs (container) -├── DefaultPlayerMovement: IPlayerMovementSettings → Used by PlayerTouchController -├── TrashMazeMovement: IPlayerMovementSettings → Used by PulverController -└── FollowerMovement: IFollowerSettings → Used by FollowerController -``` - ---- - -## 📝 Detailed Changes - -### 1. Settings Interfaces Split - -**File:** `Assets/Scripts/Core/Settings/SettingsInterfaces.cs` - -**Changes:** -- ✅ Kept `IPlayerMovementSettings` - player-only properties (MoveSpeed, MaxAcceleration, etc.) -- ✅ Created `IPlayerMovementConfigs` - container holding three separate configurations -- ✅ Created `IFollowerSettings` - follower-only properties (FollowDistance, ThresholdFar, etc.) -- ❌ Removed `IPlayerFollowerSettings` - was mixing concerns - -**New Interface Structure:** -```csharp -public interface IPlayerMovementSettings -{ - float MoveSpeed { get; } - float MaxAcceleration { get; } - float StopDistance { get; } - bool UseRigidbody { get; } - HoldMovementMode DefaultHoldMovementMode { get; } -} - -public interface IPlayerMovementConfigs -{ - IPlayerMovementSettings DefaultPlayerMovement { get; } - IPlayerMovementSettings TrashMazeMovement { get; } - IFollowerSettings FollowerMovement { get; } -} - -public interface IFollowerSettings -{ - float FollowDistance { get; } - float ManualMoveSmooth { get; } - // ... 6 more follower-specific properties -} -``` - ---- - -### 2. Settings Implementation Updated - -**File:** `Assets/Scripts/Core/Settings/PlayerFollowerSettings.cs` - -**Changes:** -- Changed from implementing `IPlayerFollowerSettings` to implementing `IPlayerMovementConfigs` -- Created three serializable nested data classes: - - `PlayerMovementSettingsData` - implements `IPlayerMovementSettings` - - `FollowerSettingsData` - implements `IFollowerSettings` -- Now exposes three separate configurations through properties - -**Before:** -```csharp -public class PlayerFollowerSettings : BaseSettings, IPlayerFollowerSettings -{ - [SerializeField] private float moveSpeed = 5f; - [SerializeField] private float followDistance = 1.5f; - // ... all properties mixed together -} -``` - -**After:** -```csharp -public class PlayerFollowerSettings : BaseSettings, IPlayerMovementConfigs -{ - [SerializeField] private PlayerMovementSettingsData defaultPlayerMovement; - [SerializeField] private PlayerMovementSettingsData trashMazeMovement; - [SerializeField] private FollowerSettingsData followerMovement; - - public IPlayerMovementSettings DefaultPlayerMovement => defaultPlayerMovement; - public IPlayerMovementSettings TrashMazeMovement => trashMazeMovement; - public IFollowerSettings FollowerMovement => followerMovement; -} -``` - -**Benefits:** -- Designer can configure player movement separately for overworld vs trash maze -- Follower settings completely separated -- Each configuration validates independently - ---- - -### 3. Base Player Movement Controller Created - -**File:** `Assets/Scripts/Input/BasePlayerMovementController.cs` ✨ **NEW FILE** - -**Purpose:** Abstract base class providing all common player movement functionality - -**Features:** -- ✅ Tap-to-move (pathfinding) -- ✅ Hold-to-move (direct or pathfinding modes) -- ✅ Collision simulation with obstacle avoidance -- ✅ Animation updates (Speed, DirX, DirY blend tree parameters) -- ✅ Movement state tracking with events (OnMovementStarted/Stopped) -- ✅ Abstract `LoadSettings()` method for derived classes to provide specific settings - -**Key Components:** -```csharp -public abstract class BasePlayerMovementController : ManagedBehaviour, ITouchInputConsumer -{ - protected IPlayerMovementSettings _movementSettings; - protected abstract void LoadSettings(); // Derived classes implement - - // Common functionality - public virtual void OnTap(Vector2 worldPosition) { /* pathfinding logic */ } - public virtual void OnHoldStart(Vector2 worldPosition) { /* hold logic */ } - protected virtual void MoveDirectlyTo(Vector2 worldPosition) { /* direct movement */ } - protected virtual Vector3 AdjustVelocityForObstacles() { /* collision */ } - protected virtual void UpdateAnimation() { /* animator updates */ } -} -``` - -**Statistics:** -- **330 lines** of reusable movement logic -- Eliminates duplication across all player controllers - ---- - -### 4. PlayerTouchController Refactored - -**File:** `Assets/Scripts/Input/PlayerTouchController.cs` - -**Changes:** -- Changed from `ManagedBehaviour, ITouchInputConsumer` to extending `BasePlayerMovementController` -- **Removed 376 lines** of duplicate movement code (now in base class) -- Kept only PlayerTouchController-specific features: - - `MoveToAndNotify()` - Used by systems like Pickup.cs - - `InterruptMoveTo()` - Cancel movement operations - - Save/load system integration -- Implements `LoadSettings()` to get `DefaultPlayerMovement` configuration - -**Before:** -```csharp -public class PlayerTouchController : ManagedBehaviour, ITouchInputConsumer -{ - // 400+ lines of movement logic + MoveToAndNotify -} -``` - -**After:** -```csharp -public class PlayerTouchController : BasePlayerMovementController -{ - protected override void LoadSettings() - { - var configs = GameManager.GetSettingsObject(); - _movementSettings = configs.DefaultPlayerMovement; - } - - // Only ~100 lines for MoveToAndNotify + overrides -} -``` - -**Code Reduction:** 376 lines removed, functionality unchanged - ---- - -### 5. FollowerController Updated - -**File:** `Assets/Scripts/Movement/FollowerController.cs` - -**Changes:** -- Changed from `IPlayerFollowerSettings` to `IFollowerSettings` -- Updated settings loading: - -```csharp -// Before -_settings = GameManager.GetSettingsObject(); - -// After -var configs = GameManager.GetSettingsObject(); -_settings = configs.FollowerMovement; -``` - -- All existing `_settings.PropertyName` calls unchanged (already follower-only) -- Added public `IsHolding` property to base controller for follower to access - ---- - -### 6. GameManager Updated - -**File:** `Assets/Scripts/Core/GameManager.cs` - -**Changes:** -```csharp -// Before -ServiceLocator.Register(playerSettings); - -// After -ServiceLocator.Register(playerSettings); -``` - ---- - -### 7. ItemSlot Fixed - -**File:** `Assets/Scripts/Interactions/ItemSlot.cs` - -**Changes:** -- Removed unused `IPlayerFollowerSettings` field -- Fixed one usage that needed `HeldIconDisplayHeight`: - -```csharp -// Before -float desiredHeight = playerFollowerSettings?.HeldIconDisplayHeight ?? 2.0f; - -// After -var configs = GameManager.GetSettingsObject(); -float desiredHeight = configs?.FollowerMovement?.HeldIconDisplayHeight ?? 2.0f; -``` - ---- - -## 🎮 Part 2: Trash Maze Visibility System - -### Problem Statement - -Implement a fog-of-war visibility system where: -- Pulver moves through a dark maze with a circular "light" radius -- Background shows lit/unlit versions based on distance -- Objects (obstacles, treasures) are hidden until revealed -- Revealed objects show white outline when outside light radius (permanent memory) - -### Solution Architecture - -**Per-Object Memory Approach:** -- Background uses simple distance-based shader (no memory) -- Objects use per-object bool flag for reveal memory -- Two separate URP/HLSL shaders -- No global RenderTexture needed (saves ~1MB GPU memory) - ---- - -## 📝 Trash Maze Implementation - -### 1. PulverController Created - -**File:** `Assets/Scripts/Minigames/TrashMaze/Core/PulverController.cs` ✨ **NEW FILE** - -**Purpose:** Player controller for trash maze with vision system - -**Features:** -- Extends `BasePlayerMovementController` - gets all movement logic -- Implements `LoadSettings()` to use `TrashMazeMovement` configuration -- Adds shader update logic in `Update()` override -- Updates global shader properties: - - `_PlayerWorldPos` - Pulver's position - - `_VisionRadius` - Size of vision circle -- Manages vision radius configuration - -**Code:** -```csharp -public class PulverController : BasePlayerMovementController -{ - [SerializeField] private float visionRadius = 3f; - - protected override void LoadSettings() - { - var configs = GameManager.GetSettingsObject(); - _movementSettings = configs.TrashMazeMovement; - } - - protected override void Update() - { - base.Update(); // Movement & animation - UpdateShaderGlobals(); // Vision system - } - - private void UpdateShaderGlobals() - { - Shader.SetGlobalVector(PlayerWorldPosID, transform.position); - Shader.SetGlobalFloat(VisionRadiusID, visionRadius); - } -} -``` - -**Statistics:** 87 lines - ---- - -### 2. TrashMazeController Created - -**File:** `Assets/Scripts/Minigames/TrashMaze/Core/TrashMazeController.cs` ✨ **NEW FILE** - -**Purpose:** Main coordinator for trash maze minigame - -**Responsibilities:** -- Initializes vision system (sets world bounds shader globals) -- Spawns Pulver at start position -- Handles exit interaction -- Handles booster pack collection events (ready for card album integration) - -**Code:** -```csharp -public class TrashMazeController : ManagedBehaviour -{ - [SerializeField] private PulverController pulverPrefab; - [SerializeField] private Transform startPosition; - [SerializeField] private Vector2 worldSize = new Vector2(100f, 100f); - [SerializeField] private Vector2 worldCenter = Vector2.zero; - - internal override void OnManagedStart() - { - // Set global shader properties for world bounds - Shader.SetGlobalVector("_WorldSize", worldSize); - Shader.SetGlobalVector("_WorldCenter", worldCenter); - - SpawnPulver(); - } - - public void OnExitReached() { /* Maze completion */ } - public void OnBoosterPackCollected() { /* Card collection */ } -} -``` - -**Statistics:** 122 lines - ---- - -### 3. RevealableObject Component Created - -**File:** `Assets/Scripts/Minigames/TrashMaze/Objects/RevealableObject.cs` ✨ **NEW FILE** - -**Purpose:** Per-object visibility memory for obstacles, booster packs, treasures - -**How It Works:** -```csharp -public class RevealableObject : MonoBehaviour -{ - private Material _instanceMaterial; // Unique material per object - private bool _hasBeenRevealed = false; // Permanent memory - - private void Update() - { - // Check distance to player - float distance = Vector2.Distance(transform.position, PulverController.PlayerPosition); - bool isInRadius = distance < PulverController.VisionRadius; - - // Update material properties - _instanceMaterial.SetFloat("_IsInVision", isInRadius ? 1f : 0f); - - if (isInRadius && !_hasBeenRevealed) - { - _hasBeenRevealed = true; - _instanceMaterial.SetFloat("_IsRevealed", 1f); // Persists forever - } - } -} -``` - -**Features:** -- Creates instance material automatically (per-object state) -- Tracks reveal state with simple bool -- Updates shader properties each frame -- Handles interaction for booster packs and exit -- Memory: ~12 bytes per object (vs 1MB for global texture approach) - -**Statistics:** 175 lines - ---- - -### 4. BackgroundVisibility Shader Created - -**File:** `Assets/Shaders/TrashMaze/BackgroundVisibility.shader` ✨ **NEW FILE** - -**Purpose:** Simple distance-based texture swap for maze background - -**Type:** URP/HLSL shader (Universal Render Pipeline compatible) - -**Inputs:** -- `_LitTex` - Full-color maze texture -- `_UnlitTex` - Dark/desaturated maze texture -- `_TransitionSoftness` - Smooth blend zone size - -**Global Properties (from PulverController):** -- `_PlayerWorldPos` - Player position -- `_VisionRadius` - Vision circle radius - -**Logic:** -```hlsl -// Calculate distance from pixel to player -float dist = distance(input.positionWS.xy, _PlayerWorldPos.xy); - -// Smooth transition between lit and unlit -float t = smoothstep(_VisionRadius - _TransitionSoftness, _VisionRadius, dist); - -// Blend textures -half4 litColor = SAMPLE_TEXTURE2D(_LitTex, sampler_LitTex, input.uv); -half4 unlitColor = SAMPLE_TEXTURE2D(_UnlitTex, sampler_UnlitTex, input.uv); - -return lerp(litColor, unlitColor, t); -``` - -**Features:** -- Real-time distance calculation (no memory) -- Smooth transition with configurable softness -- Uses URP shader library functions -- Opaque render queue - -**Statistics:** 82 lines - ---- - -### 5. ObjectVisibility Shader Created - -**File:** `Assets/Shaders/TrashMaze/ObjectVisibility.shader` ✨ **NEW FILE** - -**Purpose:** 3-state visibility with per-object memory - -**Type:** URP/HLSL shader (Universal Render Pipeline compatible) - -**Inputs:** -- `_MainTex` - Normal colored texture -- `_OutlineTex` - White outline/silhouette texture -- `_IsRevealed` - Per-instance property (0 or 1, set by RevealableObject) -- `_IsInVision` - Per-instance property (0 or 1, updated each frame) - -**Logic:** -```hlsl -if (_IsInVision > 0.5) -{ - // Inside vision radius - show color - return SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv); -} -else if (_IsRevealed > 0.5) -{ - // Revealed but outside vision - show outline - return SAMPLE_TEXTURE2D(_OutlineTex, sampler_OutlineTex, input.uv); -} -else -{ - // Never revealed - transparent (hidden) - return half4(0, 0, 0, 0); -} -``` - -**Features:** -- Three distinct states (hidden/color/outline) -- Per-material properties (not global) -- Transparent render queue for proper blending -- Automatic partial reveals (per-pixel logic) - -**Statistics:** 91 lines - ---- - -## 🎨 Asset Requirements - -### For Trash Maze to Function: - -**Materials Needed:** -1. **MazeBackground.mat** - Uses `BackgroundVisibility` shader - - Assign lit texture (color maze) - - Assign unlit texture (dark maze) - -2. **MazeObject.mat** - Uses `ObjectVisibility` shader - - Will be instanced per object automatically - - Each object assigns its own normal + outline textures - -**Textures Needed:** -- Background: 2 versions (lit + unlit) of maze texture -- Per Object: Normal sprite + white outline/silhouette version - -**Outline Generation:** -Manual or automated approach to create white silhouette from colored sprites - ---- - -## 📊 Performance Characteristics - -### Movement Refactoring: -- **Memory:** No change -- **CPU:** Slightly improved (less duplicate code paths) -- **Maintainability:** Significantly improved (single source of truth) - -### Trash Maze Visibility: -- **Memory:** ~12 bytes per object (vs 1MB for RenderTexture approach) - - 100 objects = 1.2 KB - - 1000 objects = 12 KB -- **CPU:** ~0.2ms per frame for 100 objects - - Distance checks: 100 × 0.001ms = 0.1ms - - Material updates: 100 × 0.001ms = 0.1ms -- **GPU:** Minimal (standard sprite rendering) - - Background: 1 draw call - - Objects: N draw calls (standard) -- **Target:** 60 FPS with 100-200 objects - ---- - -## ✅ Benefits Achieved - -### Refactoring Benefits: - -1. **Clean Separation of Concerns** - - Player movement ≠ Follower movement - - Each system uses exactly what it needs - - No accidental coupling - -2. **Code Reusability** - - 330 lines of movement logic now reusable - - Any new player controller can inherit from base - - PulverController implementation: only 87 lines - -3. **Flexible Configuration** - - Different movement configs for different contexts - - Designer-friendly (three clear settings groups) - - No code changes needed to adjust behavior - -4. **Type Safety** - - Can't accidentally use follower settings in player controller - - Compiler enforces correct usage - - Clear interface contracts - -### Trash Maze Benefits: - -1. **Memory Efficient** - - Per-object approach: 12 KB for 1000 objects - - RenderTexture approach would be: 1 MB - - Savings: ~99% memory reduction - -2. **Simple & Maintainable** - - Easy to debug individual objects - - Inspector-visible state - - No complex UV coordinate math - -3. **Scalable** - - Works with hundreds of objects - - No frame drops - - GPU-efficient shaders - -4. **Designer-Friendly** - - Vision radius configurable per-minigame - - Smooth transition configurable - - Clear material setup - ---- - -## 🧪 Testing Checklist - -### Movement System: -- [x] PlayerTouchController compiles without errors -- [x] PulverController compiles without errors -- [x] FollowerController compiles without errors -- [ ] PlayerTouchController movement works in overworld -- [ ] MoveToAndNotify still works (Pickup.cs integration) -- [ ] Follower follows player correctly -- [ ] Settings Editor shows three separate configs - -### Trash Maze Visibility: -- [ ] PulverController spawns and moves with tap/hold -- [ ] Background switches lit/unlit based on distance -- [ ] Objects invisible until Pulver approaches -- [ ] Objects show color when in vision radius -- [ ] Objects show outline after revealed -- [ ] Outline persists when Pulver moves away -- [ ] Booster pack collection works -- [ ] Exit interaction works -- [ ] 60 FPS stable with 100+ objects - ---- - -## 📚 Files Reference - -### Created Files (8 new): -1. `Assets/Scripts/Input/BasePlayerMovementController.cs` - Base movement class (330 lines) -2. `Assets/Scripts/Minigames/TrashMaze/Core/PulverController.cs` - Trash maze player (87 lines) -3. `Assets/Scripts/Minigames/TrashMaze/Core/TrashMazeController.cs` - Minigame coordinator (122 lines) -4. `Assets/Scripts/Minigames/TrashMaze/Objects/RevealableObject.cs` - Per-object memory (175 lines) -5. `Assets/Shaders/TrashMaze/BackgroundVisibility.shader` - Distance-based shader (82 lines) -6. `Assets/Shaders/TrashMaze/ObjectVisibility.shader` - 3-state shader (91 lines) -7. `Assets/Shaders/TrashMaze.meta` - Folder metadata -8. `Assets/Scripts/Minigames/TrashMaze/` - Folder structure + metas - -### Modified Files (11): -1. `Assets/Scripts/Core/Settings/SettingsInterfaces.cs` - Split interfaces -2. `Assets/Scripts/Core/Settings/PlayerFollowerSettings.cs` - Container implementation -3. `Assets/Scripts/Core/GameManager.cs` - Register new interface -4. `Assets/Scripts/Input/PlayerTouchController.cs` - Refactored to use base (-376 lines) -5. `Assets/Scripts/Movement/FollowerController.cs` - Use IFollowerSettings -6. `Assets/Scripts/Interactions/ItemSlot.cs` - Remove unused settings -7. Various `.meta` files - Unity-generated metadata - ---- - -## 🎯 Next Steps - -### Immediate (Unity Setup): -1. Open Unity and verify compilation -2. Check Settings Editor - should show three configs now -3. Create trash maze test scene -4. Create materials for BackgroundVisibility and ObjectVisibility shaders -5. Setup Pulver prefab with PulverController component -6. Test basic visibility system - -### Short-term (MVP): -1. Create outline textures for maze sprites -2. Setup maze background with lit/unlit textures -3. Add obstacles with RevealableObject component -4. Add booster packs with collection logic -5. Add maze exit with interaction -6. Test full gameplay loop - -### Future Enhancements: -1. Settings integration (ITrashMazeSettings interface) -2. Save/load reveal state (optional persistence) -3. Soft vision edge (shader smoothstep tuning) -4. Vision radius visualization (debug gizmo) -5. Audio feedback on reveal -6. Particle effects on collection -7. Smooth outline fade transitions - ---- - -## 🔍 Technical Notes - -### Why Container Pattern? - -We considered several approaches: -1. ❌ **Named settings lookup** - `GetSettingsObject("name")` - Not supported by existing system -2. ❌ **Separate interfaces** - ITrashMazeSettings - Would break base controller abstraction -3. ❌ **Prefixed properties** - DefaultMoveSpeed, TrashMazeMoveSpeed - Pollutes interface -4. ✅ **Container pattern** - One interface with multiple configs - Clean, flexible, type-safe - -### Why Per-Object Memory? - -We considered two approaches: -1. **Global RenderTexture** - 1MB texture tracking all reveals - - Pros: Automatic partial reveals, pixel-perfect memory - - Cons: 1MB GPU memory, complex UV math, Graphics.Blit overhead -2. ✅ **Per-Object Bool** - Simple flag per object - - Pros: 12 KB for 1000 objects, simple logic, easy debugging - - Cons: Object-based not pixel-based (acceptable for this use case) - -### Why URP Shaders? - -Project uses Universal Render Pipeline: -- `AppleHillsRenderPipeline.asset` -- `UniversalRenderPipelineGlobalSettings.asset` - -Built-in pipeline shaders (`UnityCG.cginc`, `CGPROGRAM`) don't work in URP. -Required conversion to HLSL with URP shader library includes. - ---- - -## 📖 Related Documentation - -- **StatueDressup Pattern:** `docs/wip/statue_dressup_complete_summary.md` - Similar minigame pattern -- **ManagedBehaviour:** Core lifecycle system used throughout -- **Settings System:** ScriptableObject-based configuration pattern -- **Input System:** ITouchInputConsumer interface for touch/tap input - ---- - -## ✨ Summary - -This refactoring successfully: -1. ✅ Eliminated technical debt in movement system -2. ✅ Created reusable base controller (330 lines of shared logic) -3. ✅ Separated player and follower concerns cleanly -4. ✅ Implemented trash maze visibility system (per-object memory) -5. ✅ Created URP-compatible shaders (background + objects) -6. ✅ Net reduction of 417 lines of code -7. ✅ Zero compilation errors -8. ✅ Maintained all existing functionality - -**The system is now more maintainable, more flexible, and ready for the trash maze minigame.** -