AStarHeightmapGrid / Assets / PlatypusIdeas / AirPath / Runtime / Visualization / PathVisualizationManager.cs
PathVisualizationManager.cs
Raw
using System.Collections;
using System.Collections.Generic;
using PlatypusIdeas.AirPath.Runtime.Configuration;
using PlatypusIdeas.AirPath.Runtime.Core.Terrain;
using PlatypusIdeas.AirPath.Runtime.Events;
using UnityEngine;

namespace PlatypusIdeas.AirPath.Runtime.Visualization {
    /// <summary>
    /// Manages all path visualization including line renderer and grid cells
    /// </summary>
    public class PathVisualizationManager : MonoBehaviour {
        [Header("Configuration")]
        [SerializeField] private VisualizationConfiguration visualizationConfig;
        
        [Header("Visualization Components")]
        [SerializeField] private TerrainController terrainController;
        
        [Header("Runtime Settings")]
        [SerializeField] private bool debugMode = false;
        
        // Visualization components
        private LineRenderer _pathLineRenderer;
        private List<Vector2Int> _coloredCells = new();
        private Coroutine _pathAnimationCoroutine;
        
        private bool _isValidated = false;
        
        private void OnValidate() {
            ValidateConfiguration();
        }
        
        private void Start() {
            if (!ValidateConfiguration()) {
                enabled = false;
                return;
            }
            
            InitializeComponents();
            SubscribeToEvents();
        }
        
        /// <summary>
        /// Validates all required configurations and dependencies
        /// </summary>
        /// <returns>True if all validations pass, false otherwise</returns>
        private bool ValidateConfiguration() {
            bool isValid = true;
            string errorPrefix = $"[{gameObject.name}] PathVisualizationManager:";
            
            // Check required configuration
            if (visualizationConfig == null) {
                Debug.LogError($"{errorPrefix} VisualizationConfiguration must be assigned!", this);
                isValid = false;
                _isValidated = false;
                return false; // Early return as nothing else can be validated
            }
            
            // Check terrain reference (warning only, some features will be limited)
            if (terrainController == null) {
                Debug.LogWarning($"{errorPrefix} TerrainController is not assigned. Cell coloring features will be disabled.", this);
            }
            
            // Validate visualization dependencies based on configuration settings
            if (visualizationConfig.ShowPathLine) {
                if (visualizationConfig.PathLineRendererPrefab == null) {
                    Debug.LogError($"{errorPrefix} PathLineRendererPrefab must be assigned in VisualizationConfiguration when ShowPathLine is enabled!", this);
                    isValid = false;
                } else {
                    // Check if prefab has LineRenderer component
                    var lineRenderer = visualizationConfig.PathLineRendererPrefab.GetComponent<LineRenderer>();
                    if (lineRenderer == null) {
                        Debug.LogError($"{errorPrefix} PathLineRendererPrefab must have a LineRenderer component!", this);
                        isValid = false;
                    }
                }
                
                if (visualizationConfig.PathMaterial == null) {
                    Debug.LogWarning($"{errorPrefix} PathMaterial is not assigned. Line renderer will use default material.", this);
                }
            }
            
            if (visualizationConfig.ShowPathCellColors && terrainController == null) {
                Debug.LogWarning($"{errorPrefix} ShowPathCellColors is enabled but TerrainController is not assigned. This feature will be disabled.", this);
            }
            
            _isValidated = isValid;
            return isValid;
        }
        
        /// <summary>
        /// Checks if the component is properly configured and ready to use
        /// </summary>
        public bool IsConfigured => _isValidated && visualizationConfig != null;
        
        /// <summary>
        /// Checks if cell coloring features are available
        /// </summary>
        private bool CanUseTerrainFeatures => IsConfigured && terrainController != null;
        
        private void OnDestroy() {
            UnsubscribeFromEvents();
            CleanupVisualization();
        }
        
        private void InitializeComponents() {
            if (!IsConfigured) return;
            
            if (visualizationConfig.ShowPathLine) {
                SetupLineRenderer();
            }
        }
        
        private void SubscribeToEvents() {
            this.Subscribe<PathCalculatedEvent>(OnPathCalculated);
            this.Subscribe<PathRequestedEvent>(OnPathRequested);
            this.Subscribe<ConfigurationChangedEvent>(OnConfigurationChanged);
            this.Subscribe<PathfindingModeChangedEvent>(OnModeChanged);
            this.Subscribe<VisualizationUpdateEvent>(OnVisualizationUpdate);
        }
        
        private void UnsubscribeFromEvents() {
            EventBusExtensions.Unsubscribe<PathCalculatedEvent>(OnPathCalculated);
            EventBusExtensions.Unsubscribe<PathRequestedEvent>(OnPathRequested);
            EventBusExtensions.Unsubscribe<ConfigurationChangedEvent>(OnConfigurationChanged);
            EventBusExtensions.Unsubscribe<PathfindingModeChangedEvent>(OnModeChanged);
            EventBusExtensions.Unsubscribe<VisualizationUpdateEvent>(OnVisualizationUpdate);
        }
        
        private void OnConfigurationChanged(ConfigurationChangedEvent evt) {
            if (evt.Configuration is VisualizationConfiguration visConfig) {
                // Only update if it's the same configuration instance we're using
                if (visConfig == visualizationConfig) {
                    // Re-validate with new configuration
                    if (ValidateConfiguration()) {
                        UpdateVisualizationSettings();
                    }
                }
            }
        }
        
        private void OnModeChanged(PathfindingModeChangedEvent evt) {
            // Clear previous visualization when mode changes
            ClearPathVisualization();
        }
        
        private void OnPathRequested(PathRequestedEvent evt) {
            if (!IsConfigured) return;
            
            // Clear previous visualization
            ClearPathVisualization();
            
            // Visualize start/end cells if enabled and terrain is available
            if (visualizationConfig.ShowPathCellColors && CanUseTerrainFeatures) {
                SetCellColor(evt.Request.StartGridPos, visualizationConfig.StartCellColor);
                SetCellColor(evt.Request.EndGridPos, visualizationConfig.EndCellColor);
            }
        }
        
        private void OnPathCalculated(PathCalculatedEvent evt) {
            if (!IsConfigured) return;
            
            if (!evt.Success) {
                Debug.Log("[PathVisualization] Path calculation failed, no visualization needed");
                return;
            }
            
            // Visualize path line
            if (visualizationConfig.ShowPathLine && evt.WorldPath != null) {
                VisualizePath(evt.WorldPath, visualizationConfig.PathLineHeight);
            }
            
            // Animate path cells if enabled and terrain is available
            if (visualizationConfig.ShowPathCellColors && CanUseTerrainFeatures && evt.WorldPath != null) {
                if (_pathAnimationCoroutine != null) {
                    StopCoroutine(_pathAnimationCoroutine);
                }
                _pathAnimationCoroutine = StartCoroutine(AnimatePathCells(evt.WorldPath));
            }
            
            // Publish visualization complete event
            EventBusExtensions.Publish(new VisualizationUpdateEvent(
                VisualizationUpdateEvent.UpdateType.PathLineUpdated,
                evt.WorldPath,
                this
            ));
        }
        
        private void OnVisualizationUpdate(VisualizationUpdateEvent evt) {
            switch (evt.Type) {
                case VisualizationUpdateEvent.UpdateType.VisualizationCleared:
                    ClearPathVisualization();
                    break;
                case VisualizationUpdateEvent.UpdateType.DebugVisualizationToggled:
                    debugMode = (bool)evt.Data;
                    break;
            }
        }
        
        private void SetupLineRenderer() {
            if (!IsConfigured || visualizationConfig.PathLineRendererPrefab == null) {
                return; // Already validated, just safety check
            }
            
            var lineObj = Instantiate(visualizationConfig.PathLineRendererPrefab, transform);
            lineObj.name = "PathLineRenderer";
            _pathLineRenderer = lineObj.GetComponent<LineRenderer>();
            
            ConfigureLineRenderer();
        }
        
        private void ConfigureLineRenderer() {
            if (_pathLineRenderer == null || !IsConfigured) return;
            
            if (visualizationConfig.PathMaterial != null) {
                _pathLineRenderer.material = visualizationConfig.PathMaterial;
            }
            
            _pathLineRenderer.startColor = visualizationConfig.PathColor;
            _pathLineRenderer.endColor = visualizationConfig.PathColor;
            
            if (visualizationConfig.PathLineWidthCurve != null && visualizationConfig.PathLineWidthCurve.keys.Length > 0) {
                _pathLineRenderer.widthCurve = visualizationConfig.PathLineWidthCurve;
            } else {
                _pathLineRenderer.startWidth = visualizationConfig.PathLineWidth;
                _pathLineRenderer.endWidth = visualizationConfig.PathLineWidth * 0.25f;
            }
            
            _pathLineRenderer.positionCount = 0;
            _pathLineRenderer.gameObject.SetActive(visualizationConfig.ShowPathLine);
        }
        
        private void VisualizePath(List<Vector3> worldPath, float heightOffset) {
            if (_pathLineRenderer == null || worldPath == null || !IsConfigured || !visualizationConfig.ShowPathLine) return;
            
            _pathLineRenderer.positionCount = worldPath.Count;
            
            for (int i = 0; i < worldPath.Count; i++) {
                var linePos = worldPath[i];
                linePos.y += heightOffset;
                _pathLineRenderer.SetPosition(i, linePos);
            }
            
            _pathLineRenderer.gameObject.SetActive(true);
        }
        
        private IEnumerator AnimatePathCells(List<Vector3> worldPath) {
            if (!CanUseTerrainFeatures) yield break;
            
            foreach (var worldPos in worldPath) {
                var gridPos = WorldToGridPosition(worldPos);
                SetCellColor(gridPos, visualizationConfig.PathColor);
                yield return new WaitForSeconds(0.02f);
            }
        }
        
        private void SetCellColor(Vector2Int gridPos, Color color) {
            if (!CanUseTerrainFeatures) return;
            
            terrainController.SetColor(gridPos, color);
            
            if (!_coloredCells.Contains(gridPos)) {
                _coloredCells.Add(gridPos);
            }
        }
        
        private void ClearPathVisualization() {
            // Clear colored cells
            if (CanUseTerrainFeatures && Application.isPlaying) {
                foreach (var cell in _coloredCells) {
                    try {
                        terrainController.ResetCellColor(cell);
                    } catch (MissingReferenceException) {
                        // Handle destroyed objects gracefully
                    }
                }
            }
            _coloredCells.Clear();
            
            // Clear path line
            if (_pathLineRenderer != null) {
                _pathLineRenderer.positionCount = 0;
                _pathLineRenderer.gameObject.SetActive(false);
            }
            
            // Stop animation if running
            if (_pathAnimationCoroutine != null) {
                StopCoroutine(_pathAnimationCoroutine);
                _pathAnimationCoroutine = null;
            }
        }
        
        private void UpdateVisualizationSettings() {
            if (!IsConfigured) return;
            
            // Update line renderer
            if (_pathLineRenderer != null) {
                _pathLineRenderer.gameObject.SetActive(visualizationConfig.ShowPathLine);
                ConfigureLineRenderer();
            } else if (visualizationConfig.ShowPathLine && !_pathLineRenderer) {
                SetupLineRenderer();
            }
            
            // Clear colors if disabled
            if (!visualizationConfig.ShowPathCellColors && _coloredCells.Count > 0) {
                ClearPathVisualization();
            }
        }
        
        private Vector2Int WorldToGridPosition(Vector3 worldPos) {
            if (!CanUseTerrainFeatures) return Vector2Int.zero;
            
            var localPos = worldPos - terrainController.transform.position;
            return new Vector2Int(
                Mathf.FloorToInt(localPos.x / terrainController.CellSize),
                Mathf.FloorToInt(localPos.z / terrainController.CellSize)
            );
        }
        
        private Vector3 GridToWorldPosition(Vector2Int gridPos, float yOffset = 0) {
            if (!CanUseTerrainFeatures) return Vector3.zero;
            
            return terrainController.transform.position + new Vector3(
                gridPos.x * terrainController.CellSize + terrainController.CellSize / 2f,
                yOffset,
                gridPos.y * terrainController.CellSize + terrainController.CellSize / 2f
            );
        }
        
        private void CleanupVisualization() {
            ClearPathVisualization();
            
            if (_pathLineRenderer != null) {
                if (Application.isPlaying) {
                    Destroy(_pathLineRenderer.gameObject);
                } else {
                    DestroyImmediate(_pathLineRenderer.gameObject);
                }
            }
        }
        
        private void OnDrawGizmos() {
            if (!debugMode || !IsConfigured || !visualizationConfig.ShowDebugGizmos) return;
            
            // Draw debug visualization if needed
            if (_pathLineRenderer != null && _pathLineRenderer.positionCount > 0) {
                Gizmos.color = visualizationConfig.DebugPathColor;
                for (int i = 0; i < _pathLineRenderer.positionCount - 1; i++) {
                    Gizmos.DrawLine(_pathLineRenderer.GetPosition(i), _pathLineRenderer.GetPosition(i + 1));
                }
            }
        }
    }
}