Trash maze MVP (#79)
Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com> Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Reviewed-on: #79
This commit is contained in:
9
Assets/Scripts/Minigames/TrashMaze.meta
Normal file
9
Assets/Scripts/Minigames/TrashMaze.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
9
Assets/Scripts/Minigames/TrashMaze/Core.meta
Normal file
9
Assets/Scripts/Minigames/TrashMaze/Core.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
91
Assets/Scripts/Minigames/TrashMaze/Core/PulverController.cs
Normal file
91
Assets/Scripts/Minigames/TrashMaze/Core/PulverController.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using Core;
|
||||
using UnityEngine;
|
||||
using Input;
|
||||
using AppleHills.Core.Settings;
|
||||
|
||||
namespace Minigames.TrashMaze.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Controls Pulver character movement in the Trash Maze.
|
||||
/// Inherits from BasePlayerMovementController for tap-to-move and hold-to-move.
|
||||
/// Updates global shader properties for vision radius system.
|
||||
/// </summary>
|
||||
public class PulverController : BasePlayerMovementController
|
||||
{
|
||||
public static PulverController Instance { get; private set; }
|
||||
|
||||
// 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 : 8f;
|
||||
|
||||
internal override void OnManagedAwake()
|
||||
{
|
||||
// Singleton pattern
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Logging.Warning("[PulverController] Duplicate instance detected. Destroying duplicate.");
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
|
||||
base.OnManagedAwake();
|
||||
|
||||
Logging.Debug("[PulverController] Initialized");
|
||||
}
|
||||
|
||||
protected override void LoadSettings()
|
||||
{
|
||||
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()
|
||||
{
|
||||
base.Update(); // Call base for movement and animation
|
||||
|
||||
// Update global shader properties for vision system
|
||||
UpdateShaderGlobals();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates global shader properties used by visibility shaders
|
||||
/// </summary>
|
||||
private void UpdateShaderGlobals()
|
||||
{
|
||||
Shader.SetGlobalVector(PlayerWorldPosID, transform.position);
|
||||
Shader.SetGlobalFloat(VisionRadiusID, _visionRadius);
|
||||
}
|
||||
|
||||
internal override void OnManagedDestroy()
|
||||
{
|
||||
base.OnManagedDestroy();
|
||||
|
||||
if (Instance == this)
|
||||
{
|
||||
Instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set vision radius at runtime
|
||||
/// </summary>
|
||||
public void SetVisionRadius(float radius)
|
||||
{
|
||||
_visionRadius = Mathf.Max(0.1f, radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
168
Assets/Scripts/Minigames/TrashMaze/Core/TrashMazeController.cs
Normal file
168
Assets/Scripts/Minigames/TrashMaze/Core/TrashMazeController.cs
Normal file
@@ -0,0 +1,168 @@
|
||||
using Core;
|
||||
using Core.Lifecycle;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Minigames.TrashMaze.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Main controller for the Trash Maze minigame.
|
||||
/// Initializes the vision system and manages game flow.
|
||||
/// </summary>
|
||||
public class TrashMazeController : ManagedBehaviour
|
||||
{
|
||||
public static TrashMazeController Instance { get; private set; }
|
||||
|
||||
[Header("Player")]
|
||||
[SerializeField] private PulverController pulverPrefab;
|
||||
[SerializeField] private Transform startPosition;
|
||||
|
||||
[Header("Background")]
|
||||
[Tooltip("Background sprite renderer - world size and center are inferred from its bounds")]
|
||||
[SerializeField] private SpriteRenderer backgroundRenderer;
|
||||
|
||||
[Header("Exit")]
|
||||
[SerializeField] private Transform exitPosition;
|
||||
|
||||
// Cached shader property IDs for performance
|
||||
private static readonly int WorldSizeID = Shader.PropertyToID("_WorldSize");
|
||||
private static readonly int WorldCenterID = Shader.PropertyToID("_WorldCenter");
|
||||
|
||||
private PulverController _pulverInstance;
|
||||
private bool _mazeCompleted;
|
||||
|
||||
internal override void OnManagedAwake()
|
||||
{
|
||||
base.OnManagedAwake();
|
||||
|
||||
// Singleton pattern
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Logging.Warning("[TrashMazeController] Duplicate instance detected. Destroying duplicate.");
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
internal override void OnManagedStart()
|
||||
{
|
||||
base.OnManagedStart();
|
||||
|
||||
Logging.Debug("[TrashMazeController] Initializing Trash Maze");
|
||||
|
||||
InitializeMaze();
|
||||
}
|
||||
|
||||
private void InitializeMaze()
|
||||
{
|
||||
// Infer world bounds from background renderer and set shader globals
|
||||
ApplyBackgroundBoundsToShader();
|
||||
|
||||
// Spawn player
|
||||
SpawnPulver();
|
||||
|
||||
Logging.Debug("[TrashMazeController] Trash Maze initialized");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Automatically infers world size and center from the background sprite renderer bounds
|
||||
/// and applies them to shader global properties.
|
||||
/// This handles scaled, rotated, or transformed backgrounds correctly.
|
||||
/// </summary>
|
||||
private void ApplyBackgroundBoundsToShader()
|
||||
{
|
||||
if (backgroundRenderer == null)
|
||||
{
|
||||
Logging.Error("[TrashMazeController] Background renderer not assigned! World bounds cannot be inferred.");
|
||||
Logging.Warning("[TrashMazeController] Using fallback bounds: size=(100,100), center=(0,0)");
|
||||
|
||||
// Fallback values
|
||||
Shader.SetGlobalVector(WorldSizeID, new Vector4(100f, 100f, 0f, 0f));
|
||||
Shader.SetGlobalVector(WorldCenterID, new Vector4(0f, 0f, 0f, 0f));
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the material instance (avoid modifying shared asset)
|
||||
Material material = backgroundRenderer.material;
|
||||
if (material == null)
|
||||
{
|
||||
Logging.Warning("[TrashMazeController] Background material missing on renderer.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Use renderer.bounds (world-space, accounts for scale/rotation/parent transforms)
|
||||
Bounds bounds = backgroundRenderer.bounds;
|
||||
Vector3 worldSize = bounds.size;
|
||||
Vector3 worldCenter = bounds.center;
|
||||
|
||||
// Apply to shader globals (used by both background and object shaders)
|
||||
Shader.SetGlobalVector(WorldSizeID, new Vector4(worldSize.x, worldSize.y, 0f, 0f));
|
||||
Shader.SetGlobalVector(WorldCenterID, new Vector4(worldCenter.x, worldCenter.y, 0f, 0f));
|
||||
|
||||
// Also apply directly to background material for its shader
|
||||
material.SetVector(WorldSizeID, new Vector4(worldSize.x, worldSize.y, 0f, 0f));
|
||||
material.SetVector(WorldCenterID, new Vector4(worldCenter.x, worldCenter.y, 0f, 0f));
|
||||
|
||||
Logging.Debug($"[TrashMazeController] World bounds inferred from background: " +
|
||||
$"Size=({worldSize.x:F2}, {worldSize.y:F2}), Center=({worldCenter.x:F2}, {worldCenter.y:F2})");
|
||||
}
|
||||
|
||||
private void SpawnPulver()
|
||||
{
|
||||
if (pulverPrefab == null)
|
||||
{
|
||||
Logging.Error("[TrashMazeController] Pulver prefab not assigned!");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 spawnPosition = startPosition != null ? startPosition.position : Vector3.zero;
|
||||
_pulverInstance = Instantiate(pulverPrefab, spawnPosition, Quaternion.identity);
|
||||
|
||||
Logging.Debug($"[TrashMazeController] Pulver spawned at {spawnPosition}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when player reaches the maze exit
|
||||
/// </summary>
|
||||
public void OnExitReached()
|
||||
{
|
||||
if (_mazeCompleted)
|
||||
{
|
||||
Logging.Debug("[TrashMazeController] Maze already completed");
|
||||
return;
|
||||
}
|
||||
|
||||
_mazeCompleted = true;
|
||||
|
||||
Logging.Debug("[TrashMazeController] Maze completed! Player reached exit.");
|
||||
|
||||
// TODO: Trigger completion events
|
||||
// - Award booster packs collected
|
||||
// - Open gate for Trafalgar
|
||||
// - Switch control to Trafalgar
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when player collects a booster pack
|
||||
/// </summary>
|
||||
public void OnBoosterPackCollected()
|
||||
{
|
||||
Logging.Debug("[TrashMazeController] Booster pack collected");
|
||||
|
||||
// TODO: Integrate with card album system
|
||||
// CardAlbum.AddBoosterPack();
|
||||
}
|
||||
|
||||
internal override void OnManagedDestroy()
|
||||
{
|
||||
base.OnManagedDestroy();
|
||||
|
||||
if (Instance == this)
|
||||
{
|
||||
Instance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
9
Assets/Scripts/Minigames/TrashMaze/Objects.meta
Normal file
9
Assets/Scripts/Minigames/TrashMaze/Objects.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
3
Assets/Scripts/Minigames/TrashMaze/Objects/Editor.meta
Normal file
3
Assets/Scripts/Minigames/TrashMaze/Objects/Editor.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3a935f5e791c46df8920c2c33f1c24c0
|
||||
timeCreated: 1765361215
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1d081993ee424269bf8eae99db36a54c
|
||||
timeCreated: 1765361215
|
||||
559
Assets/Scripts/Minigames/TrashMaze/Objects/RevealableObject.cs
Normal file
559
Assets/Scripts/Minigames/TrashMaze/Objects/RevealableObject.cs
Normal file
@@ -0,0 +1,559 @@
|
||||
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.
|
||||
/// </summary>
|
||||
[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;
|
||||
|
||||
private SpriteRenderer _spriteRenderer;
|
||||
private Material _instanceMaterial;
|
||||
private bool _hasBeenRevealed = false;
|
||||
private bool _isCollected = false;
|
||||
|
||||
// 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>();
|
||||
|
||||
if (_spriteRenderer.material == null)
|
||||
{
|
||||
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.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()
|
||||
{
|
||||
if (PulverController.Instance == null)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// BINARY MODE: Start tracking
|
||||
// ========================================
|
||||
|
||||
private void StartBinaryModeTracking()
|
||||
{
|
||||
_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;
|
||||
|
||||
// NO vision tracking coroutine - Progressive shader does per-pixel distance checks using global _PlayerWorldPos
|
||||
|
||||
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)
|
||||
{
|
||||
// Calculate distance to player
|
||||
float distance = Vector2.Distance(transform.position, PulverController.PlayerPosition);
|
||||
bool isInRadius = distance < PulverController.VisionRadius;
|
||||
|
||||
// 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
|
||||
if (other.CompareTag("Player") && _hasBeenRevealed && !_isCollected)
|
||||
{
|
||||
HandleInteraction();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleInteraction()
|
||||
{
|
||||
if (isBoosterPack)
|
||||
{
|
||||
CollectBoosterPack();
|
||||
}
|
||||
else if (isExit)
|
||||
{
|
||||
ActivateExit();
|
||||
}
|
||||
}
|
||||
|
||||
private void CollectBoosterPack()
|
||||
{
|
||||
_isCollected = true;
|
||||
|
||||
Logging.Debug($"[RevealableObject] Booster pack collected: {gameObject.name}");
|
||||
|
||||
// Notify controller
|
||||
if (TrashMazeController.Instance != null)
|
||||
{
|
||||
TrashMazeController.Instance.OnBoosterPackCollected();
|
||||
}
|
||||
|
||||
// Destroy object
|
||||
Destroy(gameObject);
|
||||
}
|
||||
|
||||
private void ActivateExit()
|
||||
{
|
||||
Logging.Debug($"[RevealableObject] Exit activated: {gameObject.name}");
|
||||
|
||||
// Notify controller
|
||||
if (TrashMazeController.Instance != null)
|
||||
{
|
||||
TrashMazeController.Instance.OnExitReached();
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
Destroy(_instanceMaterial);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if object is currently visible to player
|
||||
/// </summary>
|
||||
public bool IsVisible()
|
||||
{
|
||||
float distance = Vector2.Distance(transform.position, PulverController.PlayerPosition);
|
||||
return distance < PulverController.VisionRadius;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if object has been revealed at any point
|
||||
/// </summary>
|
||||
public bool HasBeenRevealed()
|
||||
{
|
||||
return _hasBeenRevealed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force reveal the object (for debugging or special cases)
|
||||
/// </summary>
|
||||
public void ForceReveal()
|
||||
{
|
||||
_hasBeenRevealed = true;
|
||||
if (_instanceMaterial != null)
|
||||
{
|
||||
_instanceMaterial.SetFloat(IsRevealedID, 1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
Reference in New Issue
Block a user