// ============================================================================ // CardSelectionEffect.cs // 카드 UI에 부착하여 선택 시 무지개 테두리 효과를 재생하는 컴포넌트 // 단일 책임: 개별 카드의 선택/해제 비주얼 이펙트만 담당 // ============================================================================ // ⚠️ 기존 LevelUpUIManager + CardUI 시스템과 통합 버전 // - Time.unscaledDeltaTime 사용 → Time.timeScale = 0 에서도 동작 // - Instantiate로 매번 새로 생성되는 카드 프리팹에 대응 // ============================================================================ using UnityEngine; // Unity 엔진 핵심 네임스페이스 using UnityEngine.UI; // UI 시스템 (Image, Canvas 등) using System; // Action 이벤트 사용을 위한 네임스페이스 /// /// 카드 하나에 부착되어 선택 시 무지개 테두리 효과를 제어하는 컴포넌트 /// Image 컴포넌트의 머티리얼을 런타임에 인스턴스화하여 개별 제어 /// public class CardSelectionEffect : MonoBehaviour { // ======================================================================= // 인스펙터 노출 필드 (SerializeField로 캡슐화 유지) // ======================================================================= [Header("=== 이펙트 설정 ===")] [SerializeField] private float _fadeInDuration = 0.3f; // 선택 시 이펙트가 나타나는 시간 (초) [SerializeField] private float _fadeOutDuration = 0.2f; // 해제 시 이펙트가 사라지는 시간 (초) [SerializeField] private float _selectedScale = 1.08f; // 선택 시 카드가 커지는 스케일 비율 [SerializeField] private float _scaleSpeed = 8.0f; // 스케일 변화 속도 (Lerp 계수) [Header("=== 셰이더 파라미터 ===")] [SerializeField] private float _borderWidth = 0.05f; // 무지개 테두리의 두께 [SerializeField] private float _rotationSpeed = 1.0f; // 무지개색 회전 속도 [SerializeField] private float _glowIntensity = 1.5f; // 글로우 빛의 강도 [SerializeField] private float _glowWidth = 0.03f; // 글로우가 퍼지는 범위 // ======================================================================= // 셰이더 프로퍼티 ID 캐싱 (매 프레임 문자열 해싱 방지 → GC 최적화) // ======================================================================= private static readonly int PROP_IS_SELECTED = Shader.PropertyToID("_IsSelected"); // 선택 상태 프로퍼티 ID private static readonly int PROP_SELECTION_ALPHA = Shader.PropertyToID("_SelectionAlpha"); // 선택 알파 프로퍼티 ID private static readonly int PROP_BORDER_WIDTH = Shader.PropertyToID("_BorderWidth"); // 테두리 두께 프로퍼티 ID private static readonly int PROP_SPEED = Shader.PropertyToID("_Speed"); // 회전 속도 프로퍼티 ID private static readonly int PROP_GLOW_INTENSITY = Shader.PropertyToID("_GlowIntensity"); // 글로우 강도 프로퍼티 ID private static readonly int PROP_GLOW_WIDTH = Shader.PropertyToID("_GlowWidth"); // 글로우 범위 프로퍼티 ID // ======================================================================= // 내부 상태 변수 // ======================================================================= private Image _cardImage; // 이 카드의 Image 컴포넌트 (캐싱) private Material _materialInstance; // 이 카드 전용 머티리얼 인스턴스 (다른 카드와 독립) private bool _isSelected; // 현재 선택 상태 여부 private float _currentAlpha; // 현재 이펙트 알파값 (0: 꺼짐, 1: 완전히 켜짐) private float _targetAlpha; // 목표 이펙트 알파값 (선택 시 1, 해제 시 0) private Vector3 _originalScale; // 카드의 원본 스케일 (초기값 보존) private Vector3 _targetScale; // 목표 스케일 (선택 시 확대, 해제 시 원본) private bool _isInitialized; // 초기화 완료 여부 (중복 초기화 방지) // ======================================================================= // 외부 이벤트: 카드가 선택되었을 때 외부에서 구독할 수 있는 콜백 // ======================================================================= public event Action OnCardSelected; // 이 카드가 선택되면 발화되는 이벤트 // ======================================================================= // 읽기 전용 프로퍼티 // ======================================================================= public bool IsSelected => _isSelected; // 외부에서 선택 상태를 읽을 수 있는 프로퍼티 // ======================================================================= // Unity 라이프사이클: Awake - 가장 먼저 호출, 컴포넌트 캐싱 // ======================================================================= private void Awake() { CacheComponents(); // 필요한 컴포넌트를 미리 캐싱 } /// /// 필요한 컴포넌트를 캐싱하고 머티리얼 인스턴스를 생성 /// Awake에서 한 번만 호출하여 런타임 GetComponent 호출을 제거 /// private void CacheComponents() { if (_isInitialized) return; // 이미 초기화되었으면 중복 실행 방지 _cardImage = GetComponent(); // Image 컴포넌트 캐싱 if (_cardImage == null) // Image가 없으면 경고 후 중단 { Debug.LogWarning($"[CardSelectionEffect] {gameObject.name}에 Image 컴포넌트가 없습니다."); // 어떤 오브젝트에서 문제인지 로그 return; // 초기화 실패 → 이후 로직 안전하게 스킵 } if (_cardImage.material != null) // 원본 머티리얼이 있으면 { _materialInstance = Instantiate(_cardImage.material); // 머티리얼을 복제하여 인스턴스 생성 (다른 카드와 독립) _cardImage.material = _materialInstance; // 복제된 인스턴스를 이 카드에 할당 } _originalScale = transform.localScale; // 원본 스케일 저장 (나중에 복원용) _targetScale = _originalScale; // 초기 목표 스케일 = 원본 InitializeShaderProperties(); // 셰이더 파라미터 초기값 설정 _isInitialized = true; // 초기화 완료 플래그 설정 } /// /// 셰이더 프로퍼티의 초기값을 설정 /// 선택되지 않은 상태로 시작하고, 인스펙터에서 지정한 값들을 적용 /// private void InitializeShaderProperties() { if (_materialInstance == null) return; // 머티리얼이 없으면 안전하게 리턴 _materialInstance.SetFloat(PROP_IS_SELECTED, 0f); // 초기 상태: 미선택 _materialInstance.SetFloat(PROP_SELECTION_ALPHA, 0f); // 초기 알파: 완전 투명 _materialInstance.SetFloat(PROP_BORDER_WIDTH, _borderWidth); // 테두리 두께 적용 _materialInstance.SetFloat(PROP_SPEED, _rotationSpeed); // 회전 속도 적용 _materialInstance.SetFloat(PROP_GLOW_INTENSITY, _glowIntensity); // 글로우 강도 적용 _materialInstance.SetFloat(PROP_GLOW_WIDTH, _glowWidth); // 글로우 범위 적용 } // ======================================================================= // Unity 라이프사이클: Update - 매 프레임 이펙트 상태 갱신 // ⚠️ Time.unscaledDeltaTime 사용 → Time.timeScale = 0 에서도 동작 // ======================================================================= private void Update() { if (!_isInitialized) return; // 초기화 안 됐으면 스킵 (방어 코드) UpdateEffectAlpha(); // 이펙트 알파 페이드 인/아웃 처리 UpdateScale(); // 카드 스케일 부드러운 변화 처리 } /// /// 이펙트 알파값을 목표값을 향해 부드럽게 보간 /// 선택 시 0→1로 페이드 인, 해제 시 1→0으로 페이드 아웃 /// ⚠️ Time.unscaledDeltaTime 사용 → 게임 일시정지(Time.timeScale=0) 중에도 이펙트 재생 /// private void UpdateEffectAlpha() { if (Mathf.Approximately(_currentAlpha, _targetAlpha)) return; // 이미 목표에 도달했으면 불필요한 연산 스킵 // 선택/해제에 따라 다른 속도로 페이드 처리 float fadeSpeed = _targetAlpha > _currentAlpha // 목표가 현재보다 크면 (페이드 인) ? 1f / _fadeInDuration // 페이드 인 속도 계산 : 1f / _fadeOutDuration; // 페이드 아웃 속도 계산 _currentAlpha = Mathf.MoveTowards( // 일정 속도로 목표를 향해 이동 _currentAlpha, // 현재 알파 _targetAlpha, // 목표 알파 fadeSpeed * Time.unscaledDeltaTime // ⚠️ unscaledDeltaTime → timeScale=0에서도 동작 ); ApplyAlphaToMaterial(); // 변경된 알파를 머티리얼에 적용 } /// /// 카드 스케일을 목표 스케일을 향해 부드럽게 보간 /// ⚠️ Time.unscaledDeltaTime 사용 → 게임 일시정지 중에도 스케일 애니메이션 재생 /// private void UpdateScale() { // 현재 스케일과 목표 스케일이 거의 같으면 스킵 if (Vector3.SqrMagnitude(transform.localScale - _targetScale) < 0.0001f) return; // SqrMagnitude: 루트 연산 없이 거리 비교 (성능 최적화) transform.localScale = Vector3.Lerp( // 현재 → 목표로 부드럽게 보간 transform.localScale, // 현재 스케일 _targetScale, // 목표 스케일 _scaleSpeed * Time.unscaledDeltaTime // ⚠️ unscaledDeltaTime → timeScale=0에서도 동작 ); } /// /// 현재 알파값을 셰이더 머티리얼에 적용 /// 알파가 0이면 셰이더에서 이펙트를 완전히 스킵하므로 성능에 유리 /// private void ApplyAlphaToMaterial() { if (_materialInstance == null) return; // 널 체크 (방어적 프로그래밍) _materialInstance.SetFloat(PROP_SELECTION_ALPHA, _currentAlpha); // 셰이더에 알파값 전달 _materialInstance.SetFloat(PROP_IS_SELECTED, // 선택 상태 전달 _currentAlpha > 0.01f ? 1f : 0f); // 알파가 거의 0이면 미선택 처리 (셰이더 early exit) } // ======================================================================= // 공개 API: 외부에서 선택/해제를 제어하는 메서드들 // ======================================================================= /// /// 이 카드를 선택 상태로 전환 /// 무지개 테두리 페이드 인 + 스케일 확대 + 이벤트 발화 /// CardUI.SetSelected(true)에서 호출됨 /// public void Select() { if (!_isInitialized) CacheComponents(); // 혹시 아직 초기화 안 됐으면 지금 실행 (Instantiate 직후 호출 대비) if (_isSelected) return; // 이미 선택된 상태면 중복 처리 방지 _isSelected = true; // 선택 상태 플래그 설정 _targetAlpha = 1f; // 이펙트 알파 목표를 1로 (페이드 인) _targetScale = _originalScale * _selectedScale; // 목표 스케일을 확대 비율로 설정 OnCardSelected?.Invoke(this); // 선택 이벤트 발화 (구독자에게 알림) } /// /// 이 카드를 해제 상태로 전환 /// 무지개 테두리 페이드 아웃 + 스케일 원본 복귀 /// CardUI.SetSelected(false)에서 호출됨 /// public void Deselect() { if (!_isSelected) return; // 이미 해제된 상태면 중복 처리 방지 _isSelected = false; // 선택 상태 플래그 해제 _targetAlpha = 0f; // 이펙트 알파 목표를 0으로 (페이드 아웃) _targetScale = _originalScale; // 목표 스케일을 원본으로 복귀 } /// /// 이펙트를 즉시 초기화 (페이드 없이 바로 꺼짐) /// CardUI.Setup()에서 호출되어 카드 재사용 시 깨끗한 상태 보장 /// public void ResetEffect() { if (!_isInitialized) CacheComponents(); // 초기화 안 됐으면 실행 (Instantiate 직후 대비) _isSelected = false; // 선택 상태 해제 _currentAlpha = 0f; // 알파를 즉시 0으로 _targetAlpha = 0f; // 목표 알파도 0으로 _targetScale = _originalScale; // 스케일을 원본으로 transform.localScale = _originalScale; // 스케일 즉시 적용 (보간 없이) ApplyAlphaToMaterial(); // 머티리얼에 즉시 반영 } // ======================================================================= // Unity 라이프사이클: OnDestroy - 메모리 누수 방지 // ======================================================================= private void OnDestroy() { if (_materialInstance != null) // 인스턴스화된 머티리얼이 있으면 { Destroy(_materialInstance); // 명시적으로 파괴하여 메모리 해제 _materialInstance = null; // 참조 정리 (댕글링 포인터 방지) } } }