Projext/Assets/02_Scripts/Systems/Scene/DungeonPortal.cs
hydrozen e989d20668 카툰 쉐이더 추가 + 중복 스크립트 수정 + 전체 업데이트
- ToonPostProcess.shader: 횃불 고딕 스타일 후처리 쉐이더 (Built-in RP)
- ToonCameraEffect.cs: 카메라 자동 부착 후처리 스크립트
- 중복 UI 스크립트 제거 (MenuIntroController, ToggleCustom)
- 씬, 프리팹, 애니메이션 등 전체 업데이트

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:31:16 +09:00

227 lines
14 KiB
C#

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 초기 설정을
}
/// <summary>
/// 상호작용 UI의 CanvasGroup 캐싱 + 초기 비활성화
/// </summary>
private void SetupInteractionUI() // 함수를 정의할거에요 -> UI 초기 설정을
{
if (interactionUI == null) return; // 중단할거에요 -> UI가 없으면 (UI 없이도 작동)
_uiInitialLocalPos = interactionUI.transform.localPosition; // 저장할거에요 -> UI의 초기 로컬 위치를
_uiCanvasGroup = interactionUI.GetComponent<CanvasGroup>(); // 가져올거에요 -> CanvasGroup을
if (_uiCanvasGroup == null) // 조건이 맞으면 실행할거에요 -> 없으면
{
_uiCanvasGroup = interactionUI.AddComponent<CanvasGroup>(); // 추가할거에요 -> 자동으로
}
_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; // 설정할거에요 -> 플레이어가 범위 밖으로 나감
}
// ─────────────────────────────────────────────────────────────
// 씬 전환 실행
// ─────────────────────────────────────────────────────────────
/// <summary>
/// 던전 씬으로 전환합니다.
/// SceneLoader(페이드) 우선, 없으면 직접 로드합니다.
///
/// [핵심] 플레이어 오브젝트는 DontDestroyOnLoadMarker 덕분에
/// 씬 전환 후에도 파괴되지 않고 모든 상태가 유지됩니다.
/// </summary>
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<Collider>(); // 가져올거에요 -> 콜라이더를
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
}
}