Working materials with two modes - binary and continuous

This commit is contained in:
Michal Pikulski
2025-12-10 12:12:30 +01:00
parent b3ba4c35f3
commit 5de62563cf
16 changed files with 791 additions and 760 deletions

View File

@@ -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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6e053220514a0c64883d9484863533fe
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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

View File

@@ -452,7 +452,6 @@ namespace UI.CardSystem
if (cardData == null) return null;
var allSlots = FindObjectsByType<AlbumCardSlot>(FindObjectsSortMode.None);
foreach (var slot in allSlots)
{
if (slot.TargetCardDefinition != null &&

View File

@@ -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);
}
}
}

View File

@@ -49,6 +49,7 @@ namespace AppleHills.Core.Settings
float FollowUpdateInterval { get; }
float FollowerSpeedMultiplier { get; }
float HeldIconDisplayHeight { get; }
float TrashMazeVisionRadius { get; }
}
/// <summary>

View File

@@ -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<IPlayerMovementConfigs>();
_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
/// </summary>
public void SetVisionRadius(float radius)
{
visionRadius = Mathf.Max(0.1f, radius);
_visionRadius = Mathf.Max(0.1f, radius);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3a935f5e791c46df8920c2c33f1c24c0
timeCreated: 1765361215

View File

@@ -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;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1d081993ee424269bf8eae99db36a54c
timeCreated: 1765361215

View File

@@ -1,9 +1,20 @@
using Core;
using Minigames.TrashMaze.Core;
using UnityEngine;
using System.Collections;
using AppleHills.Core.Settings;
namespace Minigames.TrashMaze.Objects
{
/// <summary>
/// Reveal mode for object visibility
/// </summary>
public enum RevealMode
{
Binary, // Simple on/off reveal (current system)
Progressive // Pixel-by-pixel progressive reveal with stamp texture
}
/// <summary>
/// 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<SpriteRenderer>();
// 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();
}
/// <summary>
/// Initialize progressive reveal system with dynamic texture sizing
/// </summary>
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<IPlayerMovementConfigs>();
_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
// ========================================
/// <summary>
/// Binary mode coroutine - tracks player distance and updates vision/reveal flags
/// Runs continuously, checks every 0.1s for performance
/// </summary>
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
// ========================================
/// <summary>
/// Called when player starts moving - begin stamping if near object
/// </summary>
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})");
}
}
/// <summary>
/// Called when player stops moving - stop stamping
/// </summary>
private void OnPlayerMovementStopped()
{
if (_stampCoroutine != null)
{
StopCoroutine(_stampCoroutine);
_stampCoroutine = null;
}
}
/// <summary>
/// Coroutine that stamps reveal texture while player is moving and near object
/// </summary>
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);
}
}
/// <summary>
/// Stamp the player's current position onto the reveal texture
/// Direct CPU-based stamping - calculates circle-rectangle intersection in world space
/// </summary>
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)
{

View File

@@ -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"
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 732fa975ac924d89bb0078279d2cdb0b
timeCreated: 1765358086

View File

@@ -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
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 33afb80a55e64e53b8552498ad61acfa
timeCreated: 1765358067

View File

@@ -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<IPlayerMovementConfigs>();
_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<IPlayerFollowerSettings>();
// After
var configs = GameManager.GetSettingsObject<IPlayerMovementConfigs>();
_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<IPlayerFollowerSettings>(playerSettings);
// After
ServiceLocator.Register<IPlayerMovementConfigs>(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<IPlayerMovementConfigs>();
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<IPlayerMovementConfigs>();
_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<T>("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.**