using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을 // ══════════════════════════════════════════════════════════════ // HealthAltar — 체력 회복 제단 // ══════════════════════════════════════════════════════════════ // 아이템과 달리 "구조물"이므로 플레이어가 통과하면 안 돼요. // // [핵심 원리] // 제단은 Item 레이어에 있으면 Player와 물리 충돌이 꺼져있어서 통과돼요. // (Physics → Layer Collision Matrix에서 Item ↔ Player 충돌이 꺼짐) // // 해결: Awake에서 자동으로 레이어를 분리해요 // ① 메인 오브젝트 → Environment 레이어 (Player와 충돌 ON = 통과 불가) // ② 자식 "AltarTrigger" → Item 레이어 + isTrigger (OverlapSphere 감지용) // // 이렇게 하면 플레이어가 제단을 뚫고 지나갈 수 없으면서도 // F키 상호작용(Physics.OverlapSphere + itemLayer)으로 감지는 정상 작동해요. // ══════════════════════════════════════════════════════════════ public class HealthAltar : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 HealthAltar를 { [Header("--- 회복 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 회복 설정 --- 을 [SerializeField] private float healAmount = 50f; // 변수를 선언할거에요 -> 회복량인 healAmount를 [SerializeField] private float interactRange = 3.5f; // 변수를 선언할거에요 -> 상호작용 거리인 interactRange를 [Header("--- 시각 효과 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 시각 효과 --- 를 [SerializeField] private ParticleSystem healEffect; // 변수를 선언할거에요 -> 회복 이펙트인 healEffect를 [Header("--- 충돌 설정 ---")] [Tooltip("체크하면 Awake에서 레이어와 Collider를 자동 보정해요\n" + "메인 → Environment 레이어 (물리 충돌, 통과 불가)\n" + "자식 → Item 레이어 + 트리거 (상호작용 감지용)")] [SerializeField] private bool autoFixCollider = true; // 변수를 선언할거에요 -> 콜라이더 자동 보정 여부를 // ══════════════════════════════════════════════════════════ // 초기화 — 레이어 + 콜라이더 자동 보정 // ══════════════════════════════════════════════════════════ private void Awake() // 함수를 실행할거에요 -> 게임 시작 시 최초 1회 { if (!autoFixCollider) return; // 중단할거에요 -> 자동 보정 꺼져있으면 // ── 원래 레이어를 기억 (자식 트리거에 사용) ────────────── int originalLayer = gameObject.layer; // 저장할거에요 -> 현재 레이어를 (Item 레이어일 것) // ── 메인 오브젝트를 Environment 레이어로 변경 ───────── // Item ↔ Player는 Physics Matrix에서 충돌이 꺼져있어서 통과돼요 // Environment ↔ Player는 충돌이 켜져있어서 막아줘요 int envLayer = LayerMask.NameToLayer("Environment"); // 가져올거에요 -> Environment 레이어 번호를 if (envLayer != -1) // 조건이 맞으면 실행할거에요 -> Environment 레이어가 존재하면 { gameObject.layer = envLayer; // 변경할거에요 -> 메인 오브젝트를 Environment 레이어로 Debug.Log($"[HealthAltar] 레이어 변경: {LayerMask.LayerToName(originalLayer)} → Environment (통과 방지)"); // 로그를 찍을거에요 } else // Environment 레이어가 없으면 { // Default 레이어로 폴백 (Default ↔ Player도 충돌 켜져있음) gameObject.layer = 0; // 변경할거에요 -> Default 레이어로 (0번) Debug.LogWarning("[HealthAltar] Environment 레이어를 찾을 수 없어서 Default로 설정했어요!"); // 경고를 찍을거에요 } // ── 메인 Collider를 물리 충돌용으로 확인 ───────────── // isTrigger = false여야 물리 충돌이 작동해요 Collider mainCol = GetComponent(); // 가져올거에요 -> 이 오브젝트의 메인 콜라이더를 if (mainCol != null) // 조건이 맞으면 실행할거에요 -> 콜라이더가 있으면 { if (mainCol.isTrigger) // 조건이 맞으면 실행할거에요 -> 트리거로 설정돼있으면 { mainCol.isTrigger = false; // 끌거에요 -> 트리거를 (물리 충돌 활성화 = 통과 불가) Debug.Log("[HealthAltar] 메인 Collider를 isTrigger=false로 변경 (통과 방지)"); // 로그를 찍을거에요 } } else // 콜라이더가 아예 없으면 { mainCol = gameObject.AddComponent(); // 추가할거에요 -> BoxCollider를 mainCol.isTrigger = false; // 끌거에요 -> 트리거를 (물리 충돌 = 통과 불가) Debug.LogWarning("[HealthAltar] Collider가 없어서 BoxCollider를 자동 추가했어요!"); // 경고를 찍을거에요 } // ── 상호작용 감지용 트리거 자식 오브젝트 생성 ───────── // PlayerInteraction의 OverlapSphere는 itemLayer만 감지하므로 // 트리거 자식은 반드시 원래(Item) 레이어를 유지해야 해요! EnsureInteractionTrigger(mainCol, originalLayer); // 실행할거에요 -> 트리거 자식 생성/확인을 } /// /// 상호작용 감지용 트리거 자식 오브젝트를 생성하거나 이미 있으면 무시해요. /// 핵심: 자식은 원래 레이어(Item)를 유지해야 OverlapSphere(itemLayer)에 걸려요! /// private void EnsureInteractionTrigger(Collider mainCol, int itemLayer) // 함수를 선언할거에요 -> 트리거 자식을 확인/생성하는 { // 이미 자식 중에 "AltarTrigger"가 있으면 중복 생성 방지 Transform existingTrigger = transform.Find("AltarTrigger"); // 찾을거에요 -> 기존 트리거 자식을 if (existingTrigger != null) return; // 중단할거에요 -> 이미 있으면 (중복 방지) // 새 자식 오브젝트 생성 GameObject triggerObj = new GameObject("AltarTrigger"); // 생성할거에요 -> 트리거 전용 자식 오브젝트를 triggerObj.transform.SetParent(transform); // 붙일거에요 -> 이 오브젝트의 자식으로 triggerObj.transform.localPosition = Vector3.zero; // 설정할거에요 -> 위치를 부모와 동일하게 triggerObj.transform.localRotation = Quaternion.identity; // 설정할거에요 -> 회전을 부모와 동일하게 triggerObj.transform.localScale = Vector3.one; // 설정할거에요 -> 스케일을 1로 // ⭐ 핵심: 자식은 원래 레이어(Item)를 유지! // 부모는 Environment로 바꿨지만, 자식은 Item이어야 // PlayerInteraction의 OverlapSphere(itemLayer)에서 감지돼요 triggerObj.layer = itemLayer; // 설정할거에요 -> 원래(Item) 레이어로 // 트리거 콜라이더 추가 (메인 콜라이더 크기 복사) if (mainCol is BoxCollider box) // 조건이 맞으면 실행할거에요 -> 메인이 BoxCollider면 { BoxCollider triggerBox = triggerObj.AddComponent(); // 추가할거에요 -> BoxCollider를 triggerBox.center = box.center; // 복사할거에요 -> 중심점을 triggerBox.size = box.size * 1.1f; // 설정할거에요 -> 약간 크게 (상호작용 여유) triggerBox.isTrigger = true; // 켤거에요 -> 트리거로 (감지 전용, 물리 충돌 X) } else if (mainCol is SphereCollider sphere) // 조건이 맞으면 실행할거에요 -> SphereCollider면 { SphereCollider triggerSphere = triggerObj.AddComponent(); // 추가할거에요 -> SphereCollider를 triggerSphere.center = sphere.center; // 복사할거에요 -> 중심점을 triggerSphere.radius = sphere.radius * 1.1f; // 설정할거에요 -> 약간 크게 triggerSphere.isTrigger = true; // 켤거에요 -> 트리거로 } else if (mainCol is CapsuleCollider capsule) // 조건이 맞으면 실행할거에요 -> CapsuleCollider면 { CapsuleCollider triggerCapsule = triggerObj.AddComponent(); // 추가할거에요 -> CapsuleCollider를 triggerCapsule.center = capsule.center; // 복사할거에요 -> 중심점을 triggerCapsule.radius = capsule.radius * 1.1f; // 설정할거에요 -> 약간 크게 triggerCapsule.height = capsule.height; // 복사할거에요 -> 높이를 triggerCapsule.direction = capsule.direction; // 복사할거에요 -> 방향을 triggerCapsule.isTrigger = true; // 켤거에요 -> 트리거로 } else // 그 외 콜라이더 타입이면 { BoxCollider fallbackTrigger = triggerObj.AddComponent(); // 추가할거에요 -> 기본 BoxCollider를 fallbackTrigger.size = Vector3.one; // 설정할거에요 -> 기본 크기로 fallbackTrigger.isTrigger = true; // 켤거에요 -> 트리거로 } Debug.Log($"[HealthAltar] 트리거 자식 생성 완료 (레이어: {LayerMask.LayerToName(itemLayer)})"); // 로그를 찍을거에요 } // ══════════════════════════════════════════════════════════ // 상호작용 (PlayerInteraction에서 호출) // ══════════════════════════════════════════════════════════ // ⭐ [핵심] PlayerInteraction에서 던져주는 'PlayerHealth' 자료형을 직접 받습니다! public void Use(PlayerHealth playerHealth) // 함수를 선언할거에요 -> 제단을 사용하는 Use를 { if (playerHealth == null) return; // 조건이 맞으면 중단할거에요 -> 플레이어 체력 스크립트가 없다면 // 플레이어의 Transform 정보 추출 Transform interactor = playerHealth.transform; // 값을 가져올거에요 -> 상호작용하는 대상의 위치 정보를 // 실제 몸통 중심점과의 거리 계산 float distance = Vector3.Distance(transform.position, interactor.position); // 거리를 계산할거에요 -> 제단과 플레이어 사이의 거리를 if (distance <= interactRange) // 조건이 맞으면 실행할거에요 -> 거리가 상호작용 범위 이내라면 { ApplyHeal(playerHealth); // 함수를 실행할거에요 -> 회복 적용 함수 ApplyHeal을 } else // 조건이 틀리면 실행할거에요 -> 거리가 너무 멀다면 { Debug.Log($"[제단] 너무 멉니다! (거리: {distance:F1} / 제한: {interactRange})"); // 로그를 출력할거에요 -> 거리 부족 메시지를 } } // ══════════════════════════════════════════════════════════ // 회복 적용 // ══════════════════════════════════════════════════════════ private void ApplyHeal(PlayerHealth targetHealth) // 함수를 선언할거에요 -> 실제로 체력을 회복시키는 ApplyHeal을 { targetHealth.Heal(healAmount); // 함수를 실행할거에요 -> 플레이어의 회복 함수인 Heal을 if (healEffect != null) // 조건이 맞으면 실행할거에요 -> 이펙트가 설정되어 있다면 { healEffect.transform.position = targetHealth.transform.position; // 위치를 옮길거에요 -> 플레이어 위치로 healEffect.Play(); // 실행할거에요 -> 이펙트 재생을 } Debug.Log($"[제단] {targetHealth.gameObject.name} 상호작용 성공! {healAmount} HP 회복."); // 로그를 출력할거에요 -> 회복 성공 메시지를 } // ══════════════════════════════════════════════════════════ // 에디터 기즈모 // ══════════════════════════════════════════════════════════ private void OnDrawGizmosSelected() // 함수를 실행할거에요 -> 선택 시 기즈모를 그리는 OnDrawGizmosSelected를 { Gizmos.color = Color.cyan; // 색상을 설정할거에요 -> 하늘색으로 Gizmos.DrawWireSphere(transform.position, interactRange); // 그림을 그릴거에요 -> 상호작용 범위를 표시하는 원을 } }