using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
///
/// 발사된 화살 발사체 (파티클 프리팹에 부착됨)
/// SphereCast 기반 정밀 충돌 감지 (기존 100% 유지)
/// [NEW] 속성 데미지(DoT) 시스템 추가
///
public class PlayerArrow : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerArrow를
{
[Header("--- 정밀 피격 판정 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 정밀 피격 판정 설정 --- 을
[SerializeField] private float raycastDistance = 0.8f; // 변수를 선언할거에요 -> 레이캐스트 거리인 raycastDistance를
[SerializeField] private float raycastRadius = 0.08f; // 변수를 선언할거에요 -> 레이캐스트 반지름인 raycastRadius를
[SerializeField] private LayerMask hitLayers; // 변수를 선언할거에요 -> 충돌 레이어인 hitLayers를
[SerializeField] private Transform arrowTip; // 변수를 선언할거에요 -> 화살 끝부분 위치인 arrowTip을
[Header("--- 디버그 시각화 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 디버그 시각화 --- 를
[SerializeField] private bool showDebugRay = true; // 변수를 선언할거에요 -> 디버그 레이 표시 여부인 showDebugRay를
// 화살 스탯
private float damage; // 변수를 선언할거에요 -> 데미지 damage를
private float speed; // 변수를 선언할거에요 -> 속도 speed를
private float range; // 변수를 선언할거에요 -> 사거리 range를
private Vector3 startPos; // 변수를 선언할거에요 -> 시작 위치 startPos를
private Vector3 shootDirection; // 변수를 선언할거에요 -> 발사 방향 shootDirection을
private bool isFired = false; // 변수를 초기화할거에요 -> 발사 여부 isFired를 거짓으로
private bool hasHit = false; // 변수를 초기화할거에요 -> 충돌 여부 hasHit을 거짓으로
// [NEW] 속성 데미지 시스템
private ArrowElementType elementType = ArrowElementType.None; // 변수를 초기화할거에요 -> 속성 타입 elementType을 없음으로
private float elementDamage = 0f; // 변수를 초기화할거에요 -> 속성 데미지 elementDamage를 0으로
private float elementDuration = 0f; // 변수를 초기화할거에요 -> 속성 지속 시간 elementDuration을 0으로
// Raycast 추적용
private Vector3 previousPosition; // 변수를 선언할거에요 -> 이전 위치 previousPosition을
private Rigidbody rb; // 변수를 선언할거에요 -> 물리 컴포넌트 rb를
private void Awake() // 함수를 실행할거에요 -> 스크립트 시작 시 Awake를
{
rb = GetComponent(); // 컴포넌트를 가져올거에요 -> 리지드바디를
}
private void Start() // 함수를 실행할거에요 -> 활성화 시 Start를
{
if (arrowTip == null) // 조건이 맞으면 실행할거에요 -> 화살 팁이 설정되지 않았다면
{
Transform tip = transform.Find("ArrowTip"); // 찾을거에요 -> 자식 중 ArrowTip을
if (tip == null) tip = transform.Find("Tip"); // 못 찾으면 찾을거에요 -> Tip을
arrowTip = tip ?? transform; // 그래도 없으면 할당할거에요 -> 자기 자신의 위치를
}
if (hitLayers.value == 0) // 조건이 맞으면 실행할거에요 -> 충돌 레이어가 설정되지 않았다면
{
hitLayers = LayerMask.GetMask("Enemy", "EnemyHitBox", "Wall", "Ground", "Default"); // 값을 넣을거에요 -> 기본 충돌 레이어들을
}
}
///
/// [MODIFIED] 초기화 — 속성 정보 포함 (7개 파라미터)
/// PlayerAttack.OnShootArrow()에서 호출됩니다.
///
public void Initialize(
float dmg,
float arrowSpeed,
float maxRange,
Vector3 direction,
ArrowElementType element,
float elemDmg,
float elemDur) // 함수를 선언할거에요 -> 화살을 초기화하는 Initialize를
{
this.damage = dmg; // 값을 저장할거에요 -> 데미지를
this.speed = arrowSpeed; // 값을 저장할거에요 -> 속도를
this.range = maxRange; // 값을 저장할거에요 -> 사거리를
this.shootDirection = direction.normalized; // 값을 저장할거에요 -> 정규화된 발사 방향을
this.startPos = transform.position; // 값을 저장할거에요 -> 시작 위치를
this.previousPosition = transform.position; // 값을 저장할거에요 -> 이전 위치를 (레이캐스트용)
this.isFired = true; // 상태를 바꿀거에요 -> 발사됨 상태로
// [NEW] 속성 정보 저장
this.elementType = element; // 값을 저장할거에요 -> 속성 타입을
this.elementDamage = elemDmg; // 값을 저장할거에요 -> 속성 데미지를
this.elementDuration = elemDur; // 값을 저장할거에요 -> 속성 지속 시간을
// 발사 방향으로 회전
if (shootDirection != Vector3.zero) // 조건이 맞으면 실행할거에요 -> 방향이 유효하다면
{
transform.rotation = Quaternion.LookRotation(shootDirection); // 회전시킬거에요 -> 발사 방향을 보도록
}
// Rigidbody 설정
if (rb == null) rb = GetComponent(); // 컴포넌트를 가져올거에요 -> 없다면 리지드바디를
if (rb != null) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면
{
rb.useGravity = false; // 설정을 바꿀거에요 -> 중력을 끄기로
rb.isKinematic = false; // 설정을 바꿀거에요 -> 물리 연산을 켜기로
rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic; // 설정을 바꿀거에요 -> 연속 충돌 감지로
rb.velocity = shootDirection * speed; // 값을 넣을거에요 -> 방향과 속도를 곱해 물리 속도로
}
Destroy(gameObject, 5f); // 파괴할거에요 -> 5초 뒤에 안전장치로
}
///
/// [하위 호환성] 방향 없는 3-파라미터 버전
///
public void Initialize(float dmg, float arrowSpeed, float maxRange) // 함수를 선언할거에요 -> 구버전 호환용 초기화 함수를
{
Initialize(dmg, arrowSpeed, maxRange, transform.forward,
ArrowElementType.None, 0f, 0f); // 실행할거에요 -> 신버전 초기화 함수를 기본값으로
}
private void Update() // 함수를 실행할거에요 -> 매 프레임마다 Update를
{
if (!isFired || hasHit) return; // 조건이 맞으면 중단할거에요 -> 발사되지 않았거나 이미 맞았다면
// 사거리 체크
float traveledDistance = Vector3.Distance(startPos, transform.position); // 거리를 계산할거에요 -> 이동 거리를
if (traveledDistance >= range) // 조건이 맞으면 실행할거에요 -> 사거리를 넘었다면
{
Destroy(gameObject); // 파괴할거에요 -> 화살을
return; // 중단할거에요 -> 함수를
}
// 정밀 충돌 감지
CheckPrecisionCollision(); // 함수를 실행할거에요 -> 정밀 충돌 감지 함수를
previousPosition = transform.position; // 값을 저장할거에요 -> 현재 위치를 이전 위치로
}
///
/// SphereCast 기반 정밀 충돌 (기존 로직 100% 유지)
///
private void CheckPrecisionCollision() // 함수를 선언할거에요 -> 정밀 충돌을 체크하는 CheckPrecisionCollision을
{
Vector3 tipPosition = arrowTip != null ? arrowTip.position : transform.position; // 위치를 결정할거에요 -> 팁 위치 혹은 내 위치를
Vector3 direction = rb != null && rb.velocity.magnitude > 0.1f
? rb.velocity.normalized
: transform.forward; // 방향을 결정할거에요 -> 물리 속도 방향 혹은 정면을
float frameDistance = Vector3.Distance(previousPosition, transform.position); // 거리를 계산할거에요 -> 프레임 간 이동 거리를
float checkDistance = Mathf.Max(frameDistance, raycastDistance); // 값을 결정할거에요 -> 이동 거리와 최소 거리 중 큰 값을
RaycastHit hit; // 변수를 선언할거에요 -> 충돌 정보를 담을 hit을
bool didHit = Physics.SphereCast(
tipPosition,
raycastRadius,
direction,
out hit,
checkDistance,
hitLayers
); // 실행할거에요 -> 구체 캐스트를 쏴서 충돌 여부를 확인하는 것을
if (showDebugRay) // 조건이 맞으면 실행할거에요 -> 디버그 모드라면
{
Debug.DrawRay(tipPosition, direction * checkDistance, didHit ? Color.red : Color.green, 0.1f); // 선을 그릴거에요 -> 충돌 시 빨강, 아니면 초록으로
}
if (didHit) // 조건이 맞으면 실행할거에요 -> 충돌했다면
{
HandleHit(hit.collider, hit.point); // 실행할거에요 -> 충돌 처리 함수 HandleHit을
}
}
///
/// [MODIFIED] 충돌 처리 — 속성 데미지 적용 추가
///
private void HandleHit(Collider hitCollider, Vector3 hitPoint) // 함수를 선언할거에요 -> 충돌 결과를 처리하는 HandleHit을
{
if (hasHit) return; // 조건이 맞으면 중단할거에요 -> 이미 충돌 처리되었다면
hasHit = true; // 상태를 바꿀거에요 -> 충돌 처리됨으로
// 적 감지 (Tag 또는 Layer)
bool isEnemy = hitCollider.CompareTag("Enemy"); // 조건을 확인할거에요 -> 적 태그인지
// EnemyHitBox 레이어 체크 (레이어가 존재하는 경우에만)
int enemyHitBoxLayerIndex = LayerMask.NameToLayer("EnemyHitBox"); // 값을 가져올거에요 -> 레이어 인덱스를
if (!isEnemy && enemyHitBoxLayerIndex != -1) // 조건이 맞으면 실행할거에요 -> 적 태그가 아니고 레이어가 유효하다면
{
isEnemy = hitCollider.gameObject.layer == enemyHitBoxLayerIndex; // 조건을 갱신할거에요 -> 해당 레이어인지 확인해서
}
if (isEnemy) // 조건이 맞으면 실행할거에요 -> 적이라면
{
// MonsterClass를 찾아 데미지 적용
MonsterClass monster = hitCollider.GetComponentInParent(); // 컴포넌트를 찾을거에요 -> 부모의 몬스터 클래스를
if (monster == null) monster = hitCollider.GetComponent(); // 없으면 찾을거에요 -> 자신의 몬스터 클래스를
if (monster != null) // 조건이 맞으면 실행할거에요 -> 몬스터 스크립트가 있다면
{
// 1. 기본 데미지 적용
monster.TakeDamage(damage); // 실행할거에요 -> 데미지 입히기 함수를
Debug.Log($"적 명중! 기본데미지: {damage}"); // 로그를 출력할거에요 -> 명중 메시지를
// 2. [NEW] 속성 효과 적용
ApplyElementEffect(monster); // 실행할거에요 -> 속성 효과 적용 함수를
}
Destroy(gameObject, 0.05f); // 파괴할거에요 -> 화살을 잠시 뒤
}
else if (hitCollider.CompareTag("Wall") || hitCollider.CompareTag("Ground")) // 조건이 틀리면 실행할거에요 -> 벽이나 땅이라면
{
Debug.Log($"벽/바닥 충돌! 위치: {hitPoint}"); // 로그를 출력할거에요 -> 충돌 위치를
Destroy(gameObject, 0.05f); // 파괴할거에요 -> 화살을
}
else // 조건이 틀리면 실행할거에요 -> 그 외의 충돌이라면
{
// 기타 충돌 (Unknown)
Destroy(gameObject, 0.05f); // 파괴할거에요 -> 화살을
}
}
///
/// [NEW] 속성 효과 적용
/// MonsterClass에 ApplyStatusEffect 메서드가 있어야 합니다.
///
private void ApplyElementEffect(MonsterClass monster) // 함수를 선언할거에요 -> 속성 효과를 적용하는 ApplyElementEffect를
{
if (elementType == ArrowElementType.None || elementDamage <= 0f) return; // 조건이 맞으면 중단할거에요 -> 속성이 없거나 데미지가 없다면
// MonsterClass에 ApplyStatusEffect가 있는지 확인
switch (elementType) // 분기할거에요 -> 속성 타입에 따라
{
case ArrowElementType.Fire: // 조건이 맞으면 실행할거에요 -> 불 속성이라면
monster.ApplyStatusEffect(StatusEffectType.Burn, elementDamage, elementDuration); // 실행할거에요 -> 화상 효과 적용을
Debug.Log($"화염 효과! {elementDamage} 데미지 x {elementDuration}초"); // 로그를 출력할거에요 -> 효과 설명을
break;
case ArrowElementType.Ice: // 조건이 맞으면 실행할거에요 -> 얼음 속성이라면
monster.ApplyStatusEffect(StatusEffectType.Slow, elementDamage, elementDuration); // 실행할거에요 -> 슬로우 효과 적용을
Debug.Log($"빙결 효과! 슬로우 {elementDuration}초"); // 로그를 출력할거에요 -> 효과 설명을
break;
case ArrowElementType.Poison: // 조건이 맞으면 실행할거에요 -> 독 속성이라면
monster.ApplyStatusEffect(StatusEffectType.Poison, elementDamage, elementDuration); // 실행할거에요 -> 독 효과 적용을
Debug.Log($"독 효과! {elementDamage} 데미지 x {elementDuration}초"); // 로그를 출력할거에요 -> 효과 설명을
break;
case ArrowElementType.Lightning: // 조건이 맞으면 실행할거에요 -> 번개 속성이라면
monster.ApplyStatusEffect(StatusEffectType.Shock, elementDamage, elementDuration); // 실행할거에요 -> 감전(스턴) 효과 적용을
Debug.Log($"감전 효과! {elementDamage} 범위 데미지"); // 로그를 출력할거에요 -> 효과 설명을
break;
}
}
///
/// 백업 충돌 감지 (OnTriggerEnter) — 기존 로직 유지
///
private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 트리거 충돌 시 OnTriggerEnter를
{
if (hasHit) return; // 조건이 맞으면 중단할거에요 -> 이미 맞았다면
bool isEnemy = other.CompareTag("Enemy"); // 조건을 확인할거에요 -> 적 태그인지
int enemyHitBoxLayerIndex = LayerMask.NameToLayer("EnemyHitBox"); // 값을 가져올거에요 -> 히트박스 레이어 인덱스를
if (!isEnemy && enemyHitBoxLayerIndex != -1) // 조건이 맞으면 실행할거에요 -> 적 태그가 아니고 레이어가 유효하다면
{
isEnemy = other.gameObject.layer == enemyHitBoxLayerIndex; // 조건을 갱신할거에요 -> 레이어 확인으로
}
if (isEnemy) // 조건이 맞으면 실행할거에요 -> 적이라면
{
HandleHit(other, other.ClosestPoint(transform.position)); // 실행할거에요 -> 충돌 처리 함수를
}
else if (other.CompareTag("Wall") || other.CompareTag("Ground")) // 조건이 틀리면 실행할거에요 -> 벽이나 땅이라면
{
hasHit = true; // 상태를 바꿀거에요 -> 충돌 처리됨으로
Destroy(gameObject, 0.05f); // 파괴할거에요 -> 화살을
}
}
///
/// Gizmo 디버그 시각화 — 기존 로직 유지 + 속성 색상 추가
///
private void OnDrawGizmos() // 함수를 실행할거에요 -> 기즈모를 그리는 OnDrawGizmos를
{
if (!Application.isPlaying || !isFired || !showDebugRay) return; // 조건이 맞으면 중단할거에요 -> 실행 중이 아니거나 발사 전이거나 디버그가 꺼졌다면
Vector3 tipPosition = arrowTip != null ? arrowTip.position : transform.position; // 위치를 결정할거에요 -> 팁 위치 혹은 내 위치로
Vector3 direction = rb != null && rb.velocity.magnitude > 0.1f
? rb.velocity.normalized
: transform.forward; // 방향을 결정할거에요 -> 물리 속도 방향 혹은 정면으로
Gizmos.color = hasHit ? Color.red : Color.green; // 색상을 설정할거에요 -> 맞았으면 빨강, 아니면 초록으로
Gizmos.DrawWireSphere(tipPosition, raycastRadius); // 그림을 그릴거에요 -> 팁 위치에 구체를
Gizmos.DrawWireSphere(tipPosition + direction * raycastDistance, raycastRadius); // 그림을 그릴거에요 -> 예상 도달 위치에 구체를
Gizmos.color = Color.yellow; // 색상을 설정할거에요 -> 노란색으로
Gizmos.DrawLine(tipPosition, tipPosition + direction * raycastDistance); // 선을 그릴거에요 -> 궤적을 표시하는
// [NEW] 속성별 색상 표시
switch (elementType) // 분기할거에요 -> 속성 타입에 따라
{
case ArrowElementType.Fire: Gizmos.color = Color.red; break; // 색상을 바꿀거에요 -> 빨강으로
case ArrowElementType.Ice: Gizmos.color = Color.cyan; break; // 색상을 바꿀거에요 -> 청록으로
case ArrowElementType.Poison: Gizmos.color = Color.green; break; // 색상을 바꿀거에요 -> 초록으로
case ArrowElementType.Lightning: Gizmos.color = Color.yellow; break; // 색상을 바꿀거에요 -> 노랑으로
}
if (elementType != ArrowElementType.None) // 조건이 맞으면 실행할거에요 -> 속성이 있다면
{
Gizmos.DrawWireSphere(transform.position, 0.5f); // 그림을 그릴거에요 -> 속성 표시용 구체를
}
}
}