Projext/Assets/02_Scripts/Player/Interaction/Heal/HealthAltar.cs

194 lines
14 KiB
C#
Raw Normal View History

using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
// ══════════════════════════════════════════════════════════════
// HealthAltar — 체력 회복 제단
// ══════════════════════════════════════════════════════════════
// 아이템과 달리 "구조물"이므로 플레이어가 통과하면 안 돼요.
//
// [핵심 원리]
// 제단은 Item 레이어에 있으면 Player와 물리 충돌이 꺼져있어서 통과돼요.
// (Physics → Layer Collision Matrix에서 Item ↔ Player 충돌이 꺼짐)
//
// 해결: Awake에서 자동으로 레이어를 분리해요
// ① 메인 오브젝트 → Environment 레이어 (Player와 충돌 ON = 통과 불가)
// ② 자식 "AltarTrigger" → Item 레이어 + isTrigger (OverlapSphere 감지용)
//
// 이렇게 하면 플레이어가 제단을 뚫고 지나갈 수 없으면서도
// F키 상호작용(Physics.OverlapSphere + itemLayer)으로 감지는 정상 작동해요.
// ══════════════════════════════════════════════════════════════
2026-02-02 08:30:23 +00:00
2026-02-12 15:23:25 +00:00
public class HealthAltar : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 HealthAltar를
2026-02-02 08:30:23 +00:00
{
2026-02-12 15:23:25 +00:00
[Header("--- 회복 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 회복 설정 --- 을
[SerializeField] private float healAmount = 50f; // 변수를 선언할거에요 -> 회복량인 healAmount를
[SerializeField] private float interactRange = 3.5f; // 변수를 선언할거에요 -> 상호작용 거리인 interactRange를
2026-02-02 08:30:23 +00:00
2026-02-12 15:23:25 +00:00
[Header("--- 시각 효과 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 시각 효과 --- 를
[SerializeField] private ParticleSystem healEffect; // 변수를 선언할거에요 -> 회복 이펙트인 healEffect를
2026-02-02 08:30:23 +00:00
[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<Collider>(); // 가져올거에요 -> 이 오브젝트의 메인 콜라이더를
if (mainCol != null) // 조건이 맞으면 실행할거에요 -> 콜라이더가 있으면
{
if (mainCol.isTrigger) // 조건이 맞으면 실행할거에요 -> 트리거로 설정돼있으면
{
mainCol.isTrigger = false; // 끌거에요 -> 트리거를 (물리 충돌 활성화 = 통과 불가)
Debug.Log("[HealthAltar] 메인 Collider를 isTrigger=false로 변경 (통과 방지)"); // 로그를 찍을거에요
}
}
else // 콜라이더가 아예 없으면
{
mainCol = gameObject.AddComponent<BoxCollider>(); // 추가할거에요 -> BoxCollider를
mainCol.isTrigger = false; // 끌거에요 -> 트리거를 (물리 충돌 = 통과 불가)
Debug.LogWarning("[HealthAltar] Collider가 없어서 BoxCollider를 자동 추가했어요!"); // 경고를 찍을거에요
}
// ── 상호작용 감지용 트리거 자식 오브젝트 생성 ─────────
// PlayerInteraction의 OverlapSphere는 itemLayer만 감지하므로
// 트리거 자식은 반드시 원래(Item) 레이어를 유지해야 해요!
EnsureInteractionTrigger(mainCol, originalLayer); // 실행할거에요 -> 트리거 자식 생성/확인을
}
/// <summary>
/// 상호작용 감지용 트리거 자식 오브젝트를 생성하거나 이미 있으면 무시해요.
/// 핵심: 자식은 원래 레이어(Item)를 유지해야 OverlapSphere(itemLayer)에 걸려요!
/// </summary>
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>(); // 추가할거에요 -> 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>(); // 추가할거에요 -> 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>(); // 추가할거에요 -> 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>(); // 추가할거에요 -> 기본 BoxCollider를
fallbackTrigger.size = Vector3.one; // 설정할거에요 -> 기본 크기로
fallbackTrigger.isTrigger = true; // 켤거에요 -> 트리거로
}
Debug.Log($"[HealthAltar] 트리거 자식 생성 완료 (레이어: {LayerMask.LayerToName(itemLayer)})"); // 로그를 찍을거에요
}
// ══════════════════════════════════════════════════════════
// 상호작용 (PlayerInteraction에서 호출)
// ══════════════════════════════════════════════════════════
// ⭐ [핵심] PlayerInteraction에서 던져주는 'PlayerHealth' 자료형을 직접 받습니다!
2026-02-12 15:23:25 +00:00
public void Use(PlayerHealth playerHealth) // 함수를 선언할거에요 -> 제단을 사용하는 Use를
2026-02-02 08:30:23 +00:00
{
2026-02-12 15:23:25 +00:00
if (playerHealth == null) return; // 조건이 맞으면 중단할거에요 -> 플레이어 체력 스크립트가 없다면
2026-02-02 08:30:23 +00:00
// 플레이어의 Transform 정보 추출
2026-02-12 15:23:25 +00:00
Transform interactor = playerHealth.transform; // 값을 가져올거에요 -> 상호작용하는 대상의 위치 정보를
2026-02-02 08:30:23 +00:00
// 실제 몸통 중심점과의 거리 계산
2026-02-12 15:23:25 +00:00
float distance = Vector3.Distance(transform.position, interactor.position); // 거리를 계산할거에요 -> 제단과 플레이어 사이의 거리를
2026-02-02 08:30:23 +00:00
2026-02-12 15:23:25 +00:00
if (distance <= interactRange) // 조건이 맞으면 실행할거에요 -> 거리가 상호작용 범위 이내라면
2026-02-02 08:30:23 +00:00
{
ApplyHeal(playerHealth); // 함수를 실행할거에요 -> 회복 적용 함수 ApplyHeal을
2026-02-02 08:30:23 +00:00
}
2026-02-12 15:23:25 +00:00
else // 조건이 틀리면 실행할거에요 -> 거리가 너무 멀다면
2026-02-02 08:30:23 +00:00
{
2026-02-12 15:23:25 +00:00
Debug.Log($"[제단] 너무 멉니다! (거리: {distance:F1} / 제한: {interactRange})"); // 로그를 출력할거에요 -> 거리 부족 메시지를
2026-02-02 08:30:23 +00:00
}
}
// ══════════════════════════════════════════════════════════
// 회복 적용
// ══════════════════════════════════════════════════════════
2026-02-12 15:23:25 +00:00
private void ApplyHeal(PlayerHealth targetHealth) // 함수를 선언할거에요 -> 실제로 체력을 회복시키는 ApplyHeal을
2026-02-02 08:30:23 +00:00
{
targetHealth.Heal(healAmount); // 함수를 실행할거에요 -> 플레이어의 회복 함수인 Heal을
2026-02-02 08:30:23 +00:00
2026-02-12 15:23:25 +00:00
if (healEffect != null) // 조건이 맞으면 실행할거에요 -> 이펙트가 설정되어 있다면
2026-02-02 08:30:23 +00:00
{
2026-02-12 15:23:25 +00:00
healEffect.transform.position = targetHealth.transform.position; // 위치를 옮길거에요 -> 플레이어 위치로
healEffect.Play(); // 실행할거에요 -> 이펙트 재생을
2026-02-02 08:30:23 +00:00
}
2026-02-12 15:23:25 +00:00
Debug.Log($"[제단] {targetHealth.gameObject.name} 상호작용 성공! {healAmount} HP 회복."); // 로그를 출력할거에요 -> 회복 성공 메시지를
2026-02-02 08:30:23 +00:00
}
// ══════════════════════════════════════════════════════════
// 에디터 기즈모
// ══════════════════════════════════════════════════════════
2026-02-12 15:23:25 +00:00
private void OnDrawGizmosSelected() // 함수를 실행할거에요 -> 선택 시 기즈모를 그리는 OnDrawGizmosSelected를
2026-02-02 08:30:23 +00:00
{
2026-02-12 15:23:25 +00:00
Gizmos.color = Color.cyan; // 색상을 설정할거에요 -> 하늘색으로
Gizmos.DrawWireSphere(transform.position, interactRange); // 그림을 그릴거에요 -> 상호작용 범위를 표시하는 원을
2026-02-02 08:30:23 +00:00
}
}