using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 using UnityEngine.SceneManagement; // 씬 전환 기능을 불러올거에요 -> SceneManager를 // ══════════════════════════════════════════════════════════════════ // DungeonPortal — 던전(옥소명) 씬 이동 포탈 // // [역할] // 메인 씬에서 던전 씬으로 이동하는 포탈입니다. // BossPortal과 동일한 구조이며, 역방향(던전→메인) 복귀도 지원합니다. // // [플레이어 상태 유지 원리] // ① Player 루트 오브젝트에 DontDestroyOnLoadMarker가 붙어 있으므로 // 씬 전환 시 플레이어가 파괴되지 않고 그대로 이동합니다. // ② 메인 씬에서 캐릭터를 변경하면(장비, 스탯 등) 오브젝트 자체가 // DontDestroyOnLoad 영역에 살아있으므로 모든 변경사항이 보존됩니다. // ③ 던전 씬에 PlayerSpawnPoint를 배치하면 자동으로 스폰 위치가 적용됩니다. // // [사용법] // 1. 메인 씬: 포탈 위치에 빈 오브젝트 → Collider(IsTrigger) + 이 스크립트 // 2. 던전 씬 "옥소명": PlayerSpawnPoint 배치 + DungeonPortal(복귀용) 배치 // 3. Build Settings에 "옥소명" 씬 등록 필수! // // [의존] // - DontDestroyOnLoadMarker (Player에 부착 필수) // - PlayerSpawnPoint (각 씬 스폰 위치) // - SceneLoader (페이드 전환, 없으면 직접 로드) // ══════════════════════════════════════════════════════════════════ public class DungeonPortal : MonoBehaviour // 클래스를 정의할거에요 -> 던전 포탈 컴포넌트를 { // ───────────────────────────────────────────────────────────── // Inspector 설정 // ───────────────────────────────────────────────────────────── [Header("=== 씬 설정 ===")] [Tooltip("이동할 던전 씬 이름 (Build Settings에 등록 필수)")] [SerializeField] private string targetSceneName = "옥소명"; // 변수를 선언할거에요 -> 이동할 던전 씬 이름을 [Header("=== 상호작용 ===")] [Tooltip("포탈 활성화 키")] [SerializeField] private KeyCode interactKey = KeyCode.F; // 변수를 선언할거에요 -> 상호작용 키를 (기본 F) [Tooltip("자동 이동 여부 (true: 트리거 진입만으로 즉시 이동)")] [SerializeField] private bool autoTransition = false; // 변수를 선언할거에요 -> 자동 전환 여부를 (false면 F키 필요) [Header("=== UI 연결 (선택) ===")] [Tooltip("트리거 범위 안에서 표시할 안내 UI")] [SerializeField] private GameObject interactionUI; // 변수를 선언할거에요 -> 상호작용 안내 UI를 [Header("=== UI 연출 ===")] [SerializeField] private float uiFadeSpeed = 5f; // 변수를 선언할거에요 -> UI 페이드 속도를 [SerializeField] private float uiFloatSpeed = 2f; // 변수를 선언할거에요 -> UI 둥실거리는 속도를 [SerializeField] private float uiFloatAmplitude = 0.15f; // 변수를 선언할거에요 -> UI 둥실거리는 폭을 [Header("=== 플레이어 태그 ===")] [SerializeField] private string playerTag = "Player"; // 변수를 선언할거에요 -> 플레이어 판별 태그를 // ───────────────────────────────────────────────────────────── // 내부 상태 (GC 없는 캐싱) // ───────────────────────────────────────────────────────────── private bool _isPlayerInside = false; // 변수를 초기화할거에요 -> 플레이어가 트리거 안에 있는지 여부를 private bool _isTransitioning = false; // 변수를 초기화할거에요 -> 씬 전환 중인지 여부를 (중복 전환 방지) private CanvasGroup _uiCanvasGroup; // 변수를 선언할거에요 -> UI 페이드용 CanvasGroup 캐시를 private Vector3 _uiInitialLocalPos; // 변수를 선언할거에요 -> UI 초기 위치를 (둥실거림 기준점) private float _currentUIAlpha = 0f; // 변수를 초기화할거에요 -> 현재 UI 투명도를 // ───────────────────────────────────────────────────────────── // 초기화 // ───────────────────────────────────────────────────────────── private void Awake() // Awake에서 초기화할거에요 -> 컴포넌트 캐싱을 { SetupInteractionUI(); // 실행할거에요 -> 상호작용 UI 초기 설정을 } /// /// 상호작용 UI의 CanvasGroup 캐싱 + 초기 비활성화 /// private void SetupInteractionUI() // 함수를 정의할거에요 -> UI 초기 설정을 { if (interactionUI == null) return; // 중단할거에요 -> UI가 없으면 (UI 없이도 작동) _uiInitialLocalPos = interactionUI.transform.localPosition; // 저장할거에요 -> UI의 초기 로컬 위치를 _uiCanvasGroup = interactionUI.GetComponent(); // 가져올거에요 -> CanvasGroup을 if (_uiCanvasGroup == null) // 조건이 맞으면 실행할거에요 -> 없으면 { _uiCanvasGroup = interactionUI.AddComponent(); // 추가할거에요 -> 자동으로 } _uiCanvasGroup.alpha = 0f; // 설정할거에요 -> 초기에 완전 투명으로 interactionUI.SetActive(true); // 활성화할거에요 -> alpha로 보이기 제어하므로 } // ───────────────────────────────────────────────────────────── // 매 프레임 처리 // ───────────────────────────────────────────────────────────── private void Update() // 매 프레임 실행할거에요 -> 입력 감지와 UI 업데이트를 { UpdateInteractionUI(); // 실행할거에요 -> UI 페이드/둥실거림 연출을 if (!_isPlayerInside) return; // 중단할거에요 -> 플레이어가 트리거 밖이면 if (_isTransitioning) return; // 중단할거에요 -> 이미 전환 중이면 // 자동 전환이 아닌 경우에만 키 입력 대기 if (!autoTransition && Input.GetKeyDown(interactKey)) // 조건이 맞으면 실행할거에요 -> F키를 눌렀으면 { StartSceneTransition(); // 실행할거에요 -> 씬 전환을 } } // ───────────────────────────────────────────────────────────── // 트리거 감지 // ───────────────────────────────────────────────────────────── private void OnTriggerEnter(Collider other) // 트리거 진입 시 실행할거에요 { if (other == null) return; // 방어할거에요 -> null 체크를 if (!other.CompareTag(playerTag)) return; // 중단할거에요 -> 플레이어가 아니면 _isPlayerInside = true; // 설정할거에요 -> 플레이어가 범위 안에 있음으로 Debug.Log($"[DungeonPortal] 플레이어 포탈 범위 진입 — 대상 씬: {targetSceneName}"); // 로그를 찍을거에요 // 자동 전환 모드이면 즉시 이동 if (autoTransition && !_isTransitioning) // 조건이 맞으면 실행할거에요 -> 자동 전환이고 전환 중이 아니면 { StartSceneTransition(); // 실행할거에요 -> 즉시 씬 전환을 } } private void OnTriggerExit(Collider other) // 트리거 이탈 시 실행할거에요 { if (other == null) return; // 방어할거에요 -> null 체크를 if (!other.CompareTag(playerTag)) return; // 중단할거에요 -> 플레이어가 아니면 _isPlayerInside = false; // 설정할거에요 -> 플레이어가 범위 밖으로 나감 } // ───────────────────────────────────────────────────────────── // 씬 전환 실행 // ───────────────────────────────────────────────────────────── /// /// 던전 씬으로 전환합니다. /// SceneLoader(페이드) 우선, 없으면 직접 로드합니다. /// /// [핵심] 플레이어 오브젝트는 DontDestroyOnLoadMarker 덕분에 /// 씬 전환 후에도 파괴되지 않고 모든 상태가 유지됩니다. /// private void StartSceneTransition() // 함수를 정의할거에요 -> 씬 전환 처리를 { // 씬 이름 유효성 검사 if (string.IsNullOrEmpty(targetSceneName)) // 조건이 맞으면 실행할거에요 -> 씬 이름이 비어있으면 { Debug.LogError("[DungeonPortal] targetSceneName이 비어있어요! Inspector에서 씬 이름을 설정해주세요."); // 에러를 찍을거에요 return; // 중단할거에요 } _isTransitioning = true; // 설정할거에요 -> 전환 중 플래그를 Debug.Log($"[DungeonPortal] 씬 전환 시작: {targetSceneName}"); // 로그를 찍을거에요 // SceneLoader 싱글톤이 있으면 페이드 전환 if (SceneLoader.Instance != null) // 조건이 맞으면 실행할거에요 -> SceneLoader가 있으면 { SceneLoader.Instance.LoadSceneWithFade(targetSceneName); // 실행할거에요 -> 페이드와 함께 로드를 } else // 없으면 직접 로드 { Debug.LogWarning("[DungeonPortal] SceneLoader를 찾을 수 없어요. 직접 씬 로드합니다."); // 경고를 찍을거에요 SceneManager.LoadScene(targetSceneName); // 실행할거에요 -> 동기 씬 로드를 } } // ───────────────────────────────────────────────────────────── // UI 연출 (페이드 + 둥실거림) // ───────────────────────────────────────────────────────────── private void UpdateInteractionUI() // 함수를 정의할거에요 -> UI 연출을 { if (_uiCanvasGroup == null) return; // 중단할거에요 -> UI가 없으면 // 페이드 인/아웃 float targetAlpha = _isPlayerInside ? 1f : 0f; // 계산할거에요 -> 목표 투명도를 _currentUIAlpha = Mathf.MoveTowards(_currentUIAlpha, targetAlpha, Time.deltaTime * uiFadeSpeed); // 보간할거에요 -> 부드럽게 _uiCanvasGroup.alpha = _currentUIAlpha; // 적용할거에요 -> UI에 // 둥실거림 (보이는 동안만) if (_currentUIAlpha > 0.01f && interactionUI != null) // 조건이 맞으면 실행할거에요 -> UI가 보이면 { float yOffset = Mathf.Sin(Time.time * uiFloatSpeed) * uiFloatAmplitude; // 계산할거에요 -> 사인파 오프셋을 interactionUI.transform.localPosition = _uiInitialLocalPos + new Vector3(0f, yOffset, 0f); // 적용할거에요 -> 위치를 } } // ───────────────────────────────────────────────────────────── // Gizmo (씬 뷰에서 포탈 범위 시각화) // ───────────────────────────────────────────────────────────── private void OnDrawGizmos() // 씬 뷰에서 그릴거에요 -> 포탈 범위 기즈모를 { // 포탈 색상: 던전은 초록, 복귀는 파랑으로 구분 Gizmos.color = new Color(0f, 0.8f, 0.4f, 0.3f); // 설정할거에요 -> 초록색 반투명으로 Collider col = GetComponent(); // 가져올거에요 -> 콜라이더를 if (col is BoxCollider box) // 박스 콜라이더면 { Gizmos.matrix = transform.localToWorldMatrix; // 로컬 좌표계 적용 Gizmos.DrawCube(box.center, box.size); // 박스 채움 Gizmos.color = new Color(0f, 1f, 0.5f, 0.8f); // 외곽선 색상 Gizmos.DrawWireCube(box.center, box.size); // 박스 외곽선 } else if (col is SphereCollider sphere) // 구형 콜라이더면 { Gizmos.DrawSphere(transform.position + sphere.center, sphere.radius); // 구 채움 Gizmos.DrawWireSphere(transform.position + sphere.center, sphere.radius); // 구 외곽선 } // 포탈 위에 대상 씬 이름 표시 #if UNITY_EDITOR UnityEditor.Handles.Label(transform.position + Vector3.up * 2f, // 위치 설정 $"→ {targetSceneName}", // 라벨 텍스트 new GUIStyle { // 스타일 설정 normal = { textColor = Color.green }, // 초록 텍스트 fontSize = 14, // 글자 크기 fontStyle = FontStyle.Bold // 볼드 }); #endif } }