using System.Collections.Generic; using UnityEngine; using Sirenix.OdinInspector; using QualityLevel = HighlightPlus.QualityLevel; using HighlightPlus; namespace DDD { public enum InteractionOutlineType { None, Focused, Available, Unavailable, Objective } [System.Serializable] public struct InteractionOutlineData { public Color Color; [Range(0.1f, 5f)] public float Width; [Range(0f, 1f)] public float Opacity; } [RequireComponent(typeof(HighlightEffect))] [RequireComponent(typeof(RestaurantInteractionComponent))] [AddComponentMenu("DDD/Interaction/InteractableHighlight")] public class InteractableHighlight : MonoBehaviour { [Title("Outline Styles")] [SerializeField] private InteractionOutlineData _availableStyle = new() {Color = Color.white, Width = 1f, Opacity = 1f}; [SerializeField] private InteractionOutlineData _focusedStyle = new() {Color = Color.yellow, Width = 1f, Opacity = 1f}; [SerializeField] private InteractionOutlineData _unavailableStyle = new() {Color = Color.gray, Width = 0.5f, Opacity = 1f}; [SerializeField] private InteractionOutlineData _objectiveStyle = new() {Color = Color.cyan, Width = 1f, Opacity = 1f}; [Title("Animation Settings")] [SerializeField, Range(0.5f, 10f)] private float _breathingSpeed = 2f; [SerializeField, Range(0.1f, 1f)] private float _breathingRange = 0.3f; [SerializeField] private bool _enableBreathingEffect = true; [Title("Highlight Component Settings")] [SerializeField, Range(0f, 1f)] private float _alphaCutOff = 0.5f; [SerializeField] private bool _combineMeshes = true; [SerializeField] private bool _constantWidth = true; [SerializeField] private QualityLevel _outlineQuality = QualityLevel.Highest; [SerializeField] private bool _outlineIndependent = true; [SerializeField, Range(0, 4)] private int _outlineBlurPasses = 1; [SerializeField, Range(1f, 20f)] private float _outlineSharpness = 8f; [Title("Debug")] [SerializeField, ReadOnly] private InteractionOutlineType _currentOutlineType; [SerializeField, ReadOnly] private float _currentOpacityMultiplier = 1f; // 런타임에 생성되는 딕셔너리 private Dictionary _outlineData; // Private runtime variables private float _opacityMultiply = 1.0f; private float _breathingTime; private HighlightEffect _highlightComponent; private RestaurantInteractionComponent _interactionComponent; private IInteractor _interactor; private InteractionOutlineType _lastAppliedType = InteractionOutlineType.None; private void Awake() { // OutlineData 딕셔너리 초기화 InitializeOutlineData(); // Cache HighlightEffect _highlightComponent = GetComponent(); _interactionComponent = GetComponent(); // HighlightEffect 설정 적용 ApplyHighlightSettings(); } private void InitializeOutlineData() { _outlineData = new Dictionary { {InteractionOutlineType.Available, _availableStyle}, {InteractionOutlineType.Focused, _focusedStyle}, {InteractionOutlineType.Unavailable, _unavailableStyle}, {InteractionOutlineType.Objective, _objectiveStyle}, {InteractionOutlineType.None, new InteractionOutlineData() {Color = Color.clear, Width = 0.5f, Opacity = 0f}} }; } private void ApplyHighlightSettings() { if (!_highlightComponent) return; _highlightComponent.alphaCutOff = _alphaCutOff; _highlightComponent.combineMeshes = _combineMeshes; _highlightComponent.constantWidth = _constantWidth; _highlightComponent.outlineQuality = _outlineQuality; _highlightComponent.outlineIndependent = _outlineIndependent; _highlightComponent.outlineBlurPasses = _outlineBlurPasses; _highlightComponent.outlineSharpness = _outlineSharpness; } private void Update() { FetchPlayerInteractorComponent(); var currentType = GetCurrentOutlineType(); _currentOutlineType = currentType; // 디버그용 if (_enableBreathingEffect) { UpdateBreathingEffect(currentType); } else { _opacityMultiply = 1f; _breathingTime = 0f; } _currentOpacityMultiplier = _opacityMultiply; // 디버그용 ApplyOutlineType(currentType); } private void UpdateBreathingEffect(InteractionOutlineType type) { if (type == InteractionOutlineType.Available || type == InteractionOutlineType.Focused) { _breathingTime += Time.deltaTime; float sin01 = (Mathf.Sin(_breathingTime * _breathingSpeed) + 1) / 2; _opacityMultiply = 1f - _breathingRange + sin01 * _breathingRange; } else { _opacityMultiply = 1f; _breathingTime = 0f; } } private void FetchPlayerInteractorComponent() { if (_interactor == null) { var player = PlayerManager.Instance.GetPlayer(); _interactor = player?.GetComponent(); } } private InteractionOutlineType GetCurrentOutlineType() { // interaction이 null이거나 컴포넌트가 비활성화된 경우 if (!_interactionComponent || !_interactionComponent.enabled) return InteractionOutlineType.None; // IInteractable 인터페이스로 캐스팅하여 상태 확인 if (_interactionComponent is not IInteractable interactable) return InteractionOutlineType.None; if (_interactionComponent.IsInteractionHidden()) { return InteractionOutlineType.None; } try { // 상호작용 불가능한 경우 if (CanExecuteInteraction() == false) return InteractionOutlineType.Unavailable; // TODO: 여기에 추가 상태 로직을 구현 // - isObjective 등의 퀘스트 상태를 체크 // 플레이어가 현재 이 오브젝트를 포커스 중인지 확인 if (IsPlayerFocusing()) { return InteractionOutlineType.Focused; } // 기본적으로 상호작용 가능한 상태 return InteractionOutlineType.Available; } catch { return InteractionOutlineType.Unavailable; } } private void ApplyOutlineType(InteractionOutlineType type) { // 같은 타입이면 불필요한 프로퍼티 세팅을 피함 if (_lastAppliedType == type && _enableBreathingEffect == false) return; _lastAppliedType = type; if (!_highlightComponent) return; // OutlineData에서 해당 타입의 스타일 가져오기 if (!_outlineData.TryGetValue(type, out var data)) { // 데이터가 없으면 None 타입 적용 data = _outlineData[InteractionOutlineType.None]; } // HighlightEffect에 적용 if (type == InteractionOutlineType.None) { _highlightComponent.highlighted = false; _highlightComponent.outline = 0; } else { _highlightComponent.highlighted = true; Color color = data.Color; // color.a = data.Opacity * _opacityMultiply; color *= _opacityMultiply; _highlightComponent.outlineColor = color; _highlightComponent.outlineWidth = data.Width; _highlightComponent.outline = 1; } } private bool IsPlayerFocusing() { return _interactor?.GetFocusedInteractable() == _interactionComponent; } private bool CanExecuteInteraction() { if (_interactionComponent.CanInteract() == false) { return false; } if (_interactor == null) { return false; } return _interactor.CanInteractTo(_interactionComponent); } // 런타임에 스타일 변경을 위한 메서드들 [Button("Refresh Outline Data")] private void RefreshOutlineData() { InitializeOutlineData(); } [Button("Apply Highlight Settings")] private void ApplyHighlightSettingsButton() { ApplyHighlightSettings(); } } }