248 lines
14 KiB
C#
248 lines
14 KiB
C#
// ============================================================================
|
|
// CardSelectionEffect.cs
|
|
// 카드 UI에 부착하여 선택 시 무지개 테두리 효과를 재생하는 컴포넌트
|
|
// 단일 책임: 개별 카드의 선택/해제 비주얼 이펙트만 담당
|
|
// ============================================================================
|
|
// ⚠️ 기존 LevelUpUIManager + CardUI 시스템과 통합 버전
|
|
// - Time.unscaledDeltaTime 사용 → Time.timeScale = 0 에서도 동작
|
|
// - Instantiate로 매번 새로 생성되는 카드 프리팹에 대응
|
|
// ============================================================================
|
|
using UnityEngine; // Unity 엔진 핵심 네임스페이스
|
|
using UnityEngine.UI; // UI 시스템 (Image, Canvas 등)
|
|
using System; // Action 이벤트 사용을 위한 네임스페이스
|
|
|
|
/// <summary>
|
|
/// 카드 하나에 부착되어 선택 시 무지개 테두리 효과를 제어하는 컴포넌트
|
|
/// Image 컴포넌트의 머티리얼을 런타임에 인스턴스화하여 개별 제어
|
|
/// </summary>
|
|
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<CardSelectionEffect> OnCardSelected; // 이 카드가 선택되면 발화되는 이벤트
|
|
|
|
// =======================================================================
|
|
// 읽기 전용 프로퍼티
|
|
// =======================================================================
|
|
public bool IsSelected => _isSelected; // 외부에서 선택 상태를 읽을 수 있는 프로퍼티
|
|
|
|
// =======================================================================
|
|
// Unity 라이프사이클: Awake - 가장 먼저 호출, 컴포넌트 캐싱
|
|
// =======================================================================
|
|
private void Awake()
|
|
{
|
|
CacheComponents(); // 필요한 컴포넌트를 미리 캐싱
|
|
}
|
|
|
|
/// <summary>
|
|
/// 필요한 컴포넌트를 캐싱하고 머티리얼 인스턴스를 생성
|
|
/// Awake에서 한 번만 호출하여 런타임 GetComponent 호출을 제거
|
|
/// </summary>
|
|
private void CacheComponents()
|
|
{
|
|
if (_isInitialized) return; // 이미 초기화되었으면 중복 실행 방지
|
|
|
|
_cardImage = GetComponent<Image>(); // 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; // 초기화 완료 플래그 설정
|
|
}
|
|
|
|
/// <summary>
|
|
/// 셰이더 프로퍼티의 초기값을 설정
|
|
/// 선택되지 않은 상태로 시작하고, 인스펙터에서 지정한 값들을 적용
|
|
/// </summary>
|
|
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(); // 카드 스케일 부드러운 변화 처리
|
|
}
|
|
|
|
/// <summary>
|
|
/// 이펙트 알파값을 목표값을 향해 부드럽게 보간
|
|
/// 선택 시 0→1로 페이드 인, 해제 시 1→0으로 페이드 아웃
|
|
/// ⚠️ Time.unscaledDeltaTime 사용 → 게임 일시정지(Time.timeScale=0) 중에도 이펙트 재생
|
|
/// </summary>
|
|
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(); // 변경된 알파를 머티리얼에 적용
|
|
}
|
|
|
|
/// <summary>
|
|
/// 카드 스케일을 목표 스케일을 향해 부드럽게 보간
|
|
/// ⚠️ Time.unscaledDeltaTime 사용 → 게임 일시정지 중에도 스케일 애니메이션 재생
|
|
/// </summary>
|
|
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에서도 동작
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 알파값을 셰이더 머티리얼에 적용
|
|
/// 알파가 0이면 셰이더에서 이펙트를 완전히 스킵하므로 성능에 유리
|
|
/// </summary>
|
|
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: 외부에서 선택/해제를 제어하는 메서드들
|
|
// =======================================================================
|
|
|
|
/// <summary>
|
|
/// 이 카드를 선택 상태로 전환
|
|
/// 무지개 테두리 페이드 인 + 스케일 확대 + 이벤트 발화
|
|
/// CardUI.SetSelected(true)에서 호출됨
|
|
/// </summary>
|
|
public void Select()
|
|
{
|
|
if (!_isInitialized) CacheComponents(); // 혹시 아직 초기화 안 됐으면 지금 실행 (Instantiate 직후 호출 대비)
|
|
if (_isSelected) return; // 이미 선택된 상태면 중복 처리 방지
|
|
|
|
_isSelected = true; // 선택 상태 플래그 설정
|
|
_targetAlpha = 1f; // 이펙트 알파 목표를 1로 (페이드 인)
|
|
_targetScale = _originalScale * _selectedScale; // 목표 스케일을 확대 비율로 설정
|
|
|
|
OnCardSelected?.Invoke(this); // 선택 이벤트 발화 (구독자에게 알림)
|
|
}
|
|
|
|
/// <summary>
|
|
/// 이 카드를 해제 상태로 전환
|
|
/// 무지개 테두리 페이드 아웃 + 스케일 원본 복귀
|
|
/// CardUI.SetSelected(false)에서 호출됨
|
|
/// </summary>
|
|
public void Deselect()
|
|
{
|
|
if (!_isSelected) return; // 이미 해제된 상태면 중복 처리 방지
|
|
|
|
_isSelected = false; // 선택 상태 플래그 해제
|
|
_targetAlpha = 0f; // 이펙트 알파 목표를 0으로 (페이드 아웃)
|
|
_targetScale = _originalScale; // 목표 스케일을 원본으로 복귀
|
|
}
|
|
|
|
/// <summary>
|
|
/// 이펙트를 즉시 초기화 (페이드 없이 바로 꺼짐)
|
|
/// CardUI.Setup()에서 호출되어 카드 재사용 시 깨끗한 상태 보장
|
|
/// </summary>
|
|
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; // 참조 정리 (댕글링 포인터 방지)
|
|
}
|
|
}
|
|
}
|