2026-02-12 15:23:25 +00:00
using UnityEngine ; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using System.Collections ; // 코루틴 기능을 사용할거에요 -> System.Collections를
2026-02-06 04:20:12 +00:00
2026-02-09 14:49:44 +00:00
/// <summary>
/// 발사된 화살 발사체 (파티클 프리팹에 부착됨)
/// SphereCast 기반 정밀 충돌 감지 (기존 100% 유지)
2026-02-21 06:14:24 +00:00
/// [UPGRADED] 속성 데미지를 인스펙터에서 속성별로 개별 조정 가능
2026-02-09 14:49:44 +00:00
/// </summary>
2026-02-12 15:23:25 +00:00
public class PlayerArrow : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerArrow를
2026-02-06 04:20:12 +00:00
{
2026-02-12 15:23:25 +00:00
[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을
2026-02-09 14:49:44 +00:00
2026-02-21 06:14:24 +00:00
[Header("--- 기본 화살 데미지 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 기본 화살 데미지 --- 를
[Tooltip("기본 화살 데미지 (속성과 무관한 물리 데미지)")]
[SerializeField] private float baseDamage = 10f ; // 변수를 선언할거에요 -> 기본 물리 데미지인 baseDamage를
[Header("--- 🔥 불 속성 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> 불 속성 섹션을
[SerializeField]
private ElementEffectData fireEffect = new ElementEffectData // 변수를 선언할거에요 -> 불 속성 데이터인 fireEffect를
{
bonusDamage = 5f , // 기본값을 설정할거에요 -> 즉발 추가 데미지를 5로
dotDamagePerTick = 3f , // 기본값을 설정할거에요 -> 틱당 화상 데미지를 3으로
dotDuration = 4f , // 기본값을 설정할거에요 -> 화상 지속시간을 4초로
dotTickInterval = 0.5f , // 기본값을 설정할거에요 -> 틱 간격을 0.5초로
effectStrength = 1f // 기본값을 설정할거에요 -> 효과 강도를 1로 (불은 순수 데미지라 참고용)
} ;
[Header("--- ❄️ 얼음 속성 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> 얼음 속성 섹션을
[SerializeField]
private ElementEffectData iceEffect = new ElementEffectData // 변수를 선언할거에요 -> 얼음 속성 데이터인 iceEffect를
{
bonusDamage = 2f , // 기본값을 설정할거에요 -> 즉발 추가 데미지를 2로
dotDamagePerTick = 0f , // 기본값을 설정할거에요 -> 틱 데미지를 0으로 (얼음은 슬로우 위주)
dotDuration = 3f , // 기본값을 설정할거에요 -> 슬로우 지속시간을 3초로
dotTickInterval = 1f , // 기본값을 설정할거에요 -> 틱 간격을 1초로
effectStrength = 0.5f // 기본값을 설정할거에요 -> 슬로우 비율을 0.5 (50% 감속)로
} ;
[Header("--- ☠️ 독 속성 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> 독 속성 섹션을
[SerializeField]
private ElementEffectData poisonEffect = new ElementEffectData // 변수를 선언할거에요 -> 독 속성 데이터인 poisonEffect를
{
bonusDamage = 1f , // 기본값을 설정할거에요 -> 즉발 추가 데미지를 1로
dotDamagePerTick = 2f , // 기본값을 설정할거에요 -> 틱당 독 데미지를 2로
dotDuration = 6f , // 기본값을 설정할거에요 -> 독 지속시간을 6초로
dotTickInterval = 1f , // 기본값을 설정할거에요 -> 틱 간격을 1초로
effectStrength = 1f // 기본값을 설정할거에요 -> 효과 강도를 1로
} ;
[Header("--- ⚡ 전기 속성 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> 전기 속성 섹션을
[SerializeField]
private ElementEffectData lightningEffect = new ElementEffectData // 변수를 선언할거에요 -> 전기 속성 데이터인 lightningEffect를
{
bonusDamage = 8f , // 기본값을 설정할거에요 -> 즉발 추가 데미지를 8로
dotDamagePerTick = 0f , // 기본값을 설정할거에요 -> 틱 데미지를 0으로 (전기는 스턴 위주)
dotDuration = 1.5f , // 기본값을 설정할거에요 -> 스턴 지속시간을 1.5초로
dotTickInterval = 1f , // 기본값을 설정할거에요 -> 틱 간격을 1초로
effectStrength = 1.5f // 기본값을 설정할거에요 -> 스턴 지속시간을 1.5초로
} ;
2026-02-12 15:23:25 +00:00
[Header("--- 디버그 시각화 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 디버그 시각화 --- 를
[SerializeField] private bool showDebugRay = true ; // 변수를 선언할거에요 -> 디버그 레이 표시 여부인 showDebugRay를
2026-02-09 14:49:44 +00:00
2026-02-21 06:14:24 +00:00
// 화살 런타임 스탯 (Initialize에서 설정됨)
private float damage ; // 변수를 선언할거에요 -> 최종 데미지 damage를
2026-02-12 15:23:25 +00:00
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을 거짓으로
2026-02-06 04:20:12 +00:00
2026-02-21 06:14:24 +00:00
// 현재 적용 중인 속성 (Initialize에서 설정됨)
private ArrowElementType currentElement = ArrowElementType . None ; // 변수를 초기화할거에요 -> 현재 속성을 없음으로
2026-02-09 14:49:44 +00:00
2026-02-25 10:39:20 +00:00
// [NEW] 사운드 참조
private PlayerSoundFX _soundFX ; // 변수를 선언할거에요 -> 사운드 스크립트 캐싱용을
2026-02-09 14:49:44 +00:00
// Raycast 추적용
2026-02-12 15:23:25 +00:00
private Vector3 previousPosition ; // 변수를 선언할거에요 -> 이전 위치 previousPosition을
private Rigidbody rb ; // 변수를 선언할거에요 -> 물리 컴포넌트 rb를
2026-02-09 14:49:44 +00:00
2026-02-27 00:44:52 +00:00
private AudioSource _arrowAudioSource ; // 변수를 선언할거에요 -> 화살 자체의 AudioSource를 (날아가는 소리용)
2026-02-12 15:23:25 +00:00
private void Awake ( ) // 함수를 실행할거에요 -> 스크립트 시작 시 Awake를
2026-02-09 14:49:44 +00:00
{
2026-02-12 15:23:25 +00:00
rb = GetComponent < Rigidbody > ( ) ; // 컴포넌트를 가져올거에요 -> 리지드바디를
2026-02-27 00:44:52 +00:00
_arrowAudioSource = GetComponent < AudioSource > ( ) ; // 가져올거에요 -> 화살 오브젝트의 AudioSource를 (날아가는 소리)
2026-02-09 14:49:44 +00:00
}
2026-02-12 15:23:25 +00:00
private void Start ( ) // 함수를 실행할거에요 -> 활성화 시 Start를
2026-02-09 14:49:44 +00:00
{
2026-02-25 10:39:20 +00:00
_soundFX = FindObjectOfType < PlayerSoundFX > ( ) ; // [NEW] 가져올거에요 -> 씬의 사운드 스크립트를 캐싱
2026-02-12 15:23:25 +00:00
if ( arrowTip = = null ) // 조건이 맞으면 실행할거에요 -> 화살 팁이 설정되지 않았다면
2026-02-09 14:49:44 +00:00
{
2026-02-12 15:23:25 +00:00
Transform tip = transform . Find ( "ArrowTip" ) ; // 찾을거에요 -> 자식 중 ArrowTip을
if ( tip = = null ) tip = transform . Find ( "Tip" ) ; // 못 찾으면 찾을거에요 -> Tip을
arrowTip = tip ? ? transform ; // 그래도 없으면 할당할거에요 -> 자기 자신의 위치를
2026-02-09 14:49:44 +00:00
}
2026-02-12 15:23:25 +00:00
if ( hitLayers . value = = 0 ) // 조건이 맞으면 실행할거에요 -> 충돌 레이어가 설정되지 않았다면
2026-02-09 14:49:44 +00:00
{
2026-02-22 13:37:34 +00:00
hitLayers = LayerMask . GetMask ( "Enemy" , "EnemyHitBox" , "Wall" , "Ground" ) ; // 값을 넣을거에요 -> 기본 충돌 레이어들을 (Default 제외)
2026-02-09 14:49:44 +00:00
}
}
/// <summary>
2026-02-21 06:14:24 +00:00
/// [UPGRADED] 초기화 — 속성 타입만 받고, 세부 수치는 인스펙터 설정값 사용
2026-02-09 14:49:44 +00:00
/// PlayerAttack.OnShootArrow()에서 호출됩니다.
/// </summary>
public void Initialize (
float dmg ,
float arrowSpeed ,
float maxRange ,
Vector3 direction ,
2026-02-21 06:14:24 +00:00
ArrowElementType element ) // 함수를 선언할거에요 -> 화살을 초기화하는 Initialize를 (5개 파라미터)
2026-02-06 04:20:12 +00:00
{
2026-02-12 15:23:25 +00:00
this . speed = arrowSpeed ; // 값을 저장할거에요 -> 속도를
this . range = maxRange ; // 값을 저장할거에요 -> 사거리를
this . shootDirection = direction . normalized ; // 값을 저장할거에요 -> 정규화된 발사 방향을
this . startPos = transform . position ; // 값을 저장할거에요 -> 시작 위치를
this . previousPosition = transform . position ; // 값을 저장할거에요 -> 이전 위치를 (레이캐스트용)
this . isFired = true ; // 상태를 바꿀거에요 -> 발사됨 상태로
2026-02-21 06:14:24 +00:00
this . currentElement = element ; // 값을 저장할거에요 -> 현재 속성 타입을
2026-02-06 04:20:12 +00:00
2026-02-21 06:14:24 +00:00
// [UPGRADED] 기본 데미지 = 외부에서 받은 dmg + 속성 즉발 보너스 데미지
ElementEffectData effectData = GetElementData ( element ) ; // 데이터를 가져올거에요 -> 현재 속성에 맞는 설정값을
float bonusDmg = ( effectData ! = null & & effectData . enabled ) ? effectData . bonusDamage : 0f ; // 값을 계산할거에요 -> 속성 추가 즉발 데미지를
this . damage = dmg + bonusDmg ; // 값을 저장할거에요 -> 기본 데미지 + 속성 보너스를 최종 데미지로
Debug . Log ( $"[Arrow] 초기화 — 기본:{dmg} + 속성보너스:{bonusDmg} = 최종:{this.damage} | 속성:{element}" ) ; // 로그를 출력할거에요 -> 데미지 계산 내역을
2026-02-09 14:49:44 +00:00
// 발사 방향으로 회전
2026-02-12 15:23:25 +00:00
if ( shootDirection ! = Vector3 . zero ) // 조건이 맞으면 실행할거에요 -> 방향이 유효하다면
2026-02-09 14:49:44 +00:00
{
2026-02-12 15:23:25 +00:00
transform . rotation = Quaternion . LookRotation ( shootDirection ) ; // 회전시킬거에요 -> 발사 방향을 보도록
2026-02-09 14:49:44 +00:00
}
// Rigidbody 설정
2026-02-12 15:23:25 +00:00
if ( rb = = null ) rb = GetComponent < Rigidbody > ( ) ; // 컴포넌트를 가져올거에요 -> 없다면 리지드바디를
2026-02-09 14:49:44 +00:00
2026-02-12 15:23:25 +00:00
if ( rb ! = null ) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면
2026-02-06 04:20:12 +00:00
{
2026-02-12 15:23:25 +00:00
rb . useGravity = false ; // 설정을 바꿀거에요 -> 중력을 끄기로
rb . isKinematic = false ; // 설정을 바꿀거에요 -> 물리 연산을 켜기로
rb . collisionDetectionMode = CollisionDetectionMode . ContinuousDynamic ; // 설정을 바꿀거에요 -> 연속 충돌 감지로
rb . velocity = shootDirection * speed ; // 값을 넣을거에요 -> 방향과 속도를 곱해 물리 속도로
2026-02-06 04:20:12 +00:00
}
2026-02-12 15:23:25 +00:00
Destroy ( gameObject , 5f ) ; // 파괴할거에요 -> 5초 뒤에 안전장치로
2026-02-06 04:20:12 +00:00
}
2026-02-21 06:14:24 +00:00
/// <summary>
/// [하위 호환성] 기존 7-파라미터 버전도 유지 (외부에서 직접 수치를 넘기는 경우)
/// </summary>
public void Initialize (
float dmg ,
float arrowSpeed ,
float maxRange ,
Vector3 direction ,
ArrowElementType element ,
float elemDmg ,
float elemDur ) // 함수를 선언할거에요 -> 구버전 호환용 7-파라미터 초기화 함수를
{
Initialize ( dmg , arrowSpeed , maxRange , direction , element ) ; // 실행할거에요 -> 새 5-파라미터 초기화를 (인스펙터 값 사용)
}
2026-02-09 14:49:44 +00:00
/// <summary>
/// [하위 호환성] 방향 없는 3-파라미터 버전
/// </summary>
2026-02-21 06:14:24 +00:00
public void Initialize ( float dmg , float arrowSpeed , float maxRange ) // 함수를 선언할거에요 -> 구버전 호환용 3-파라미터 초기화 함수를
{
Initialize ( dmg , arrowSpeed , maxRange , transform . forward , ArrowElementType . None ) ; // 실행할거에요 -> 신버전 초기화를 기본값으로
}
/// <summary>
/// [NEW] 속성 타입에 맞는 인스펙터 설정 데이터를 반환
/// </summary>
private ElementEffectData GetElementData ( ArrowElementType element ) // 함수를 선언할거에요 -> 속성 데이터를 가져오는 GetElementData를
2026-02-09 14:49:44 +00:00
{
2026-02-21 06:14:24 +00:00
switch ( element ) // 분기할거에요 -> 속성 타입에 따라
{
case ArrowElementType . Fire : return fireEffect ; // 반환할거에요 -> 불 속성 설정값을
case ArrowElementType . Ice : return iceEffect ; // 반환할거에요 -> 얼음 속성 설정값을
case ArrowElementType . Poison : return poisonEffect ; // 반환할거에요 -> 독 속성 설정값을
case ArrowElementType . Lightning : return lightningEffect ; // 반환할거에요 -> 전기 속성 설정값을
default : return null ; // 반환할거에요 -> 없음 (일반 화살)
}
2026-02-09 14:49:44 +00:00
}
2026-02-12 15:23:25 +00:00
private void Update ( ) // 함수를 실행할거에요 -> 매 프레임마다 Update를
2026-02-06 04:20:12 +00:00
{
2026-02-12 15:23:25 +00:00
if ( ! isFired | | hasHit ) return ; // 조건이 맞으면 중단할거에요 -> 발사되지 않았거나 이미 맞았다면
2026-02-09 14:49:44 +00:00
// 사거리 체크
2026-02-12 15:23:25 +00:00
float traveledDistance = Vector3 . Distance ( startPos , transform . position ) ; // 거리를 계산할거에요 -> 이동 거리를
if ( traveledDistance > = range ) // 조건이 맞으면 실행할거에요 -> 사거리를 넘었다면
2026-02-06 04:20:12 +00:00
{
2026-02-12 15:23:25 +00:00
Destroy ( gameObject ) ; // 파괴할거에요 -> 화살을
return ; // 중단할거에요 -> 함수를
2026-02-06 04:20:12 +00:00
}
2026-02-09 14:49:44 +00:00
// 정밀 충돌 감지
2026-02-12 15:23:25 +00:00
CheckPrecisionCollision ( ) ; // 함수를 실행할거에요 -> 정밀 충돌 감지 함수를
2026-02-09 14:49:44 +00:00
2026-02-12 15:23:25 +00:00
previousPosition = transform . position ; // 값을 저장할거에요 -> 현재 위치를 이전 위치로
2026-02-06 04:20:12 +00:00
}
2026-02-09 14:49:44 +00:00
/// <summary>
/// SphereCast 기반 정밀 충돌 (기존 로직 100% 유지)
/// </summary>
2026-02-12 15:23:25 +00:00
private void CheckPrecisionCollision ( ) // 함수를 선언할거에요 -> 정밀 충돌을 체크하는 CheckPrecisionCollision을
2026-02-09 14:49:44 +00:00
{
2026-02-12 15:23:25 +00:00
Vector3 tipPosition = arrowTip ! = null ? arrowTip . position : transform . position ; // 위치를 결정할거에요 -> 팁 위치 혹은 내 위치를
2026-02-09 14:49:44 +00:00
Vector3 direction = rb ! = null & & rb . velocity . magnitude > 0.1f
? rb . velocity . normalized
2026-02-12 15:23:25 +00:00
: transform . forward ; // 방향을 결정할거에요 -> 물리 속도 방향 혹은 정면을
2026-02-09 14:49:44 +00:00
2026-02-12 15:23:25 +00:00
float frameDistance = Vector3 . Distance ( previousPosition , transform . position ) ; // 거리를 계산할거에요 -> 프레임 간 이동 거리를
float checkDistance = Mathf . Max ( frameDistance , raycastDistance ) ; // 값을 결정할거에요 -> 이동 거리와 최소 거리 중 큰 값을
2026-02-09 14:49:44 +00:00
2026-02-12 15:23:25 +00:00
RaycastHit hit ; // 변수를 선언할거에요 -> 충돌 정보를 담을 hit을
2026-02-09 14:49:44 +00:00
bool didHit = Physics . SphereCast (
tipPosition ,
raycastRadius ,
direction ,
out hit ,
checkDistance ,
hitLayers
2026-02-12 15:23:25 +00:00
) ; // 실행할거에요 -> 구체 캐스트를 쏴서 충돌 여부를 확인하는 것을
2026-02-09 14:49:44 +00:00
2026-02-12 15:23:25 +00:00
if ( showDebugRay ) // 조건이 맞으면 실행할거에요 -> 디버그 모드라면
2026-02-09 14:49:44 +00:00
{
2026-02-12 15:23:25 +00:00
Debug . DrawRay ( tipPosition , direction * checkDistance , didHit ? Color . red : Color . green , 0.1f ) ; // 선을 그릴거에요 -> 충돌 시 빨강, 아니면 초록으로
2026-02-09 14:49:44 +00:00
}
2026-02-12 15:23:25 +00:00
if ( didHit ) // 조건이 맞으면 실행할거에요 -> 충돌했다면
2026-02-09 14:49:44 +00:00
{
2026-02-12 15:23:25 +00:00
HandleHit ( hit . collider , hit . point ) ; // 실행할거에요 -> 충돌 처리 함수 HandleHit을
2026-02-09 14:49:44 +00:00
}
}
/// <summary>
2026-02-21 06:14:24 +00:00
/// [UPGRADED] 충돌 처리 — 인스펙터 설정값 기반 속성 데미지 적용
2026-02-09 14:49:44 +00:00
/// </summary>
2026-02-12 15:23:25 +00:00
private void HandleHit ( Collider hitCollider , Vector3 hitPoint ) // 함수를 선언할거에요 -> 충돌 결과를 처리하는 HandleHit을
2026-02-09 14:49:44 +00:00
{
2026-02-12 15:23:25 +00:00
if ( hasHit ) return ; // 조건이 맞으면 중단할거에요 -> 이미 충돌 처리되었다면
hasHit = true ; // 상태를 바꿀거에요 -> 충돌 처리됨으로
2026-02-09 14:49:44 +00:00
2026-02-27 00:44:52 +00:00
// ⭐ 날아가는 소리 즉시 정지 — 충돌 순간 끊기지 않게
// 임팩트 소리("팍")는 _soundFX.PlayArrowHitFlesh()에서 별도 재생됨
if ( _arrowAudioSource ! = null ) _arrowAudioSource . Stop ( ) ; // 정지할거에요 -> 날아가는 소리를
2026-02-09 14:49:44 +00:00
// 적 감지 (Tag 또는 Layer)
2026-02-12 15:23:25 +00:00
bool isEnemy = hitCollider . CompareTag ( "Enemy" ) ; // 조건을 확인할거에요 -> 적 태그인지
2026-02-09 14:49:44 +00:00
// EnemyHitBox 레이어 체크 (레이어가 존재하는 경우에만)
2026-02-12 15:23:25 +00:00
int enemyHitBoxLayerIndex = LayerMask . NameToLayer ( "EnemyHitBox" ) ; // 값을 가져올거에요 -> 레이어 인덱스를
if ( ! isEnemy & & enemyHitBoxLayerIndex ! = - 1 ) // 조건이 맞으면 실행할거에요 -> 적 태그가 아니고 레이어가 유효하다면
2026-02-09 14:49:44 +00:00
{
2026-02-12 15:23:25 +00:00
isEnemy = hitCollider . gameObject . layer = = enemyHitBoxLayerIndex ; // 조건을 갱신할거에요 -> 해당 레이어인지 확인해서
2026-02-09 14:49:44 +00:00
}
2026-02-22 13:37:34 +00:00
// [FIX] Enemy 레이어도 적으로 인식
int enemyLayerIndex = LayerMask . NameToLayer ( "Enemy" ) ; // 값을 가져올거에요 -> Enemy 레이어 인덱스를
if ( ! isEnemy & & enemyLayerIndex ! = - 1 ) // 조건이 맞으면 실행할거에요 -> 아직 적으로 판별 안 됐고 Enemy 레이어가 유효하다면
{
isEnemy = hitCollider . gameObject . layer = = enemyLayerIndex ; // 조건을 갱신할거에요 -> Enemy 레이어인지 확인해서
}
2026-02-12 15:23:25 +00:00
if ( isEnemy ) // 조건이 맞으면 실행할거에요 -> 적이라면
2026-02-09 14:49:44 +00:00
{
// MonsterClass를 찾아 데미지 적용
2026-02-12 15:23:25 +00:00
MonsterClass monster = hitCollider . GetComponentInParent < MonsterClass > ( ) ; // 컴포넌트를 찾을거에요 -> 부모의 몬스터 클래스를
if ( monster = = null ) monster = hitCollider . GetComponent < MonsterClass > ( ) ; // 없으면 찾을거에요 -> 자신의 몬스터 클래스를
2026-02-09 14:49:44 +00:00
2026-02-22 13:37:34 +00:00
// [FIX] MonsterClass가 없는 오브젝트(Sensor 등)는 무시 — 화살 통과
if ( monster = = null ) // 조건이 맞으면 실행할거에요 -> 몬스터 스크립트를 못 찾았다면
2026-02-09 14:49:44 +00:00
{
2026-02-22 13:37:34 +00:00
hasHit = false ; // 되돌릴거에요 -> 충돌 상태를 해제해서 다음 충돌 가능하게
return ; // 중단할거에요 -> 화살을 파괴하지 않고 통과시키기
}
2026-02-09 14:49:44 +00:00
2026-02-22 13:37:34 +00:00
// 1. 최종 데미지 적용 (기본 + 속성 보너스가 이미 합산됨)
monster . TakeDamage ( damage ) ; // 실행할거에요 -> 최종 데미지를 입히는 함수를
Debug . Log ( $"[Arrow] 적 명중! 최종 데미지: {damage} | 속성: {currentElement}" ) ; // 로그를 출력할거에요 -> 명중 메시지를
2026-02-25 10:39:20 +00:00
// [NEW] 적 명중 사운드
if ( _soundFX ! = null ) _soundFX . PlayArrowHitFlesh ( hitPoint ) ; // 실행할거에요 -> 적 명중 소리를
2026-02-22 13:37:34 +00:00
// 2. [UPGRADED] 속성 지속 효과(DoT) 적용 — 인스펙터 설정값 사용
ApplyElementEffect ( monster ) ; // 실행할거에요 -> 속성 효과 적용 함수를
// 3. [NEW] 타격 임팩트 (히트스톱 + 카메라 흔들림 + 넉백)
bool isStrongHit = currentElement ! = ArrowElementType . None ; // 판단할거에요 -> 속성 화살이면 강한 공격으로
if ( HitImpactManager . Instance ! = null ) // 조건이 맞으면 실행할거에요 -> 임팩트 매니저가 있다면
{
Vector3 hitDir = shootDirection ! = Vector3 . zero ? shootDirection : transform . forward ; // 방향을 결정할거에요 -> 화살 진행 방향을
HitImpactManager . Instance . TriggerHitImpact ( monster , hitDir , hitPoint , isStrongHit ) ; // 실행할거에요 -> 타격 임팩트를
2026-02-09 14:49:44 +00:00
}
2026-02-12 15:23:25 +00:00
Destroy ( gameObject , 0.05f ) ; // 파괴할거에요 -> 화살을 잠시 뒤
2026-02-09 14:49:44 +00:00
}
2026-02-12 15:23:25 +00:00
else if ( hitCollider . CompareTag ( "Wall" ) | | hitCollider . CompareTag ( "Ground" ) ) // 조건이 틀리면 실행할거에요 -> 벽이나 땅이라면
2026-02-09 14:49:44 +00:00
{
2026-02-12 15:23:25 +00:00
Debug . Log ( $"벽/바닥 충돌! 위치: {hitPoint}" ) ; // 로그를 출력할거에요 -> 충돌 위치를
2026-02-25 10:39:20 +00:00
if ( _soundFX ! = null ) _soundFX . PlayArrowHitWall ( hitPoint ) ; // [NEW] 실행할거에요 -> 벽 명중 소리를
2026-02-12 15:23:25 +00:00
Destroy ( gameObject , 0.05f ) ; // 파괴할거에요 -> 화살을
2026-02-09 14:49:44 +00:00
}
2026-02-12 15:23:25 +00:00
else // 조건이 틀리면 실행할거에요 -> 그 외의 충돌이라면
2026-02-09 14:49:44 +00:00
{
2026-02-22 13:37:34 +00:00
// [DEBUG] 뭐에 맞았는지 확인용 로그 — 문제 해결 후 삭제 가능
Debug . LogWarning ( $"[Arrow] 알 수 없는 충돌! 오브젝트: {hitCollider.gameObject.name}, 레이어: {LayerMask.LayerToName(hitCollider.gameObject.layer)}, 태그: {hitCollider.tag}, 위치: {hitPoint}" ) ; // 경고를 출력할거에요 -> 충돌 대상 상세 정보를
2026-02-12 15:23:25 +00:00
Destroy ( gameObject , 0.05f ) ; // 파괴할거에요 -> 화살을
2026-02-09 14:49:44 +00:00
}
}
/// <summary>
2026-02-21 06:14:24 +00:00
/// [UPGRADED] 속성 효과 적용 — 인스펙터에서 설정한 값을 직접 사용
/// MonsterClass에 ApplyStatusEffect(StatusEffectType, float dotDmg, float duration, float tickInterval, float strength)
/// 오버로드가 필요합니다.
2026-02-09 14:49:44 +00:00
/// </summary>
2026-02-12 15:23:25 +00:00
private void ApplyElementEffect ( MonsterClass monster ) // 함수를 선언할거에요 -> 속성 효과를 적용하는 ApplyElementEffect를
2026-02-09 14:49:44 +00:00
{
2026-02-21 06:14:24 +00:00
if ( currentElement = = ArrowElementType . None ) return ; // 조건이 맞으면 중단할거에요 -> 일반 화살이라면
2026-02-09 14:49:44 +00:00
2026-02-21 06:14:24 +00:00
ElementEffectData data = GetElementData ( currentElement ) ; // 데이터를 가져올거에요 -> 현재 속성의 인스펙터 설정값을
if ( data = = null | | ! data . enabled ) return ; // 조건이 맞으면 중단할거에요 -> 설정이 없거나 비활성이라면
switch ( currentElement ) // 분기할거에요 -> 속성 타입에 따라
2026-02-09 14:49:44 +00:00
{
2026-02-12 15:23:25 +00:00
case ArrowElementType . Fire : // 조건이 맞으면 실행할거에요 -> 불 속성이라면
2026-02-21 06:14:24 +00:00
monster . ApplyStatusEffect ( // 실행할거에요 -> 화상 상태이상을 적용하는 함수를
StatusEffectType . Burn , // 인자를 전달할거에요 -> 화상 타입을
data . dotDamagePerTick , // 인자를 전달할거에요 -> 틱당 데미지를 (인스펙터 값)
data . dotDuration , // 인자를 전달할거에요 -> 지속 시간을 (인스펙터 값)
data . dotTickInterval , // 인자를 전달할거에요 -> 틱 간격을 (인스펙터 값)
data . effectStrength // 인자를 전달할거에요 -> 효과 강도를 (인스펙터 값)
) ;
Debug . Log ( $"[Arrow] 🔥 화염! 틱당:{data.dotDamagePerTick} x {data.dotDuration}초 (간격:{data.dotTickInterval}초)" ) ; // 로그를 출력할거에요 -> 화상 효과 내역을
break ; // 분기를 종료할거에요
2026-02-09 14:49:44 +00:00
2026-02-12 15:23:25 +00:00
case ArrowElementType . Ice : // 조건이 맞으면 실행할거에요 -> 얼음 속성이라면
2026-02-21 06:14:24 +00:00
monster . ApplyStatusEffect ( // 실행할거에요 -> 슬로우 상태이상을 적용하는 함수를
StatusEffectType . Slow , // 인자를 전달할거에요 -> 슬로우 타입을
data . dotDamagePerTick , // 인자를 전달할거에요 -> 틱당 데미지를 (얼음은 보통 0)
data . dotDuration , // 인자를 전달할거에요 -> 슬로우 지속 시간을
data . dotTickInterval , // 인자를 전달할거에요 -> 틱 간격을
data . effectStrength // 인자를 전달할거에요 -> 슬로우 비율을 (0.5면 50% 감속)
) ;
Debug . Log ( $"[Arrow] ❄️ 빙결! 슬로우:{data.effectStrength * 100}% x {data.dotDuration}초" ) ; // 로그를 출력할거에요 -> 빙결 효과 내역을
break ; // 분기를 종료할거에요
2026-02-09 14:49:44 +00:00
2026-02-12 15:23:25 +00:00
case ArrowElementType . Poison : // 조건이 맞으면 실행할거에요 -> 독 속성이라면
2026-02-21 06:14:24 +00:00
monster . ApplyStatusEffect ( // 실행할거에요 -> 독 상태이상을 적용하는 함수를
StatusEffectType . Poison , // 인자를 전달할거에요 -> 독 타입을
data . dotDamagePerTick , // 인자를 전달할거에요 -> 틱당 독 데미지를
data . dotDuration , // 인자를 전달할거에요 -> 독 지속 시간을
data . dotTickInterval , // 인자를 전달할거에요 -> 틱 간격을
data . effectStrength // 인자를 전달할거에요 -> 효과 강도를
) ;
Debug . Log ( $"[Arrow] ☠️ 독! 틱당:{data.dotDamagePerTick} x {data.dotDuration}초 (간격:{data.dotTickInterval}초)" ) ; // 로그를 출력할거에요 -> 독 효과 내역을
break ; // 분기를 종료할거에요
case ArrowElementType . Lightning : // 조건이 맞으면 실행할거에요 -> 전기 속성이라면
monster . ApplyStatusEffect ( // 실행할거에요 -> 감전(스턴) 상태이상을 적용하는 함수를
StatusEffectType . Shock , // 인자를 전달할거에요 -> 감전 타입을
data . dotDamagePerTick , // 인자를 전달할거에요 -> 틱당 데미지를 (전기는 보통 0)
data . dotDuration , // 인자를 전달할거에요 -> 스턴 지속 시간을
data . dotTickInterval , // 인자를 전달할거에요 -> 틱 간격을
data . effectStrength // 인자를 전달할거에요 -> 스턴 시간을
) ;
Debug . Log ( $"[Arrow] ⚡ 감전! 스턴:{data.effectStrength}초 | 추가데미지:{data.bonusDamage}" ) ; // 로그를 출력할거에요 -> 감전 효과 내역을
break ; // 분기를 종료할거에요
2026-02-09 14:49:44 +00:00
}
}
/// <summary>
/// 백업 충돌 감지 (OnTriggerEnter) — 기존 로직 유지
/// </summary>
2026-02-12 15:23:25 +00:00
private void OnTriggerEnter ( Collider other ) // 함수를 실행할거에요 -> 트리거 충돌 시 OnTriggerEnter를
2026-02-06 04:20:12 +00:00
{
2026-02-12 15:23:25 +00:00
if ( hasHit ) return ; // 조건이 맞으면 중단할거에요 -> 이미 맞았다면
2026-02-09 14:49:44 +00:00
2026-02-12 15:23:25 +00:00
bool isEnemy = other . CompareTag ( "Enemy" ) ; // 조건을 확인할거에요 -> 적 태그인지
int enemyHitBoxLayerIndex = LayerMask . NameToLayer ( "EnemyHitBox" ) ; // 값을 가져올거에요 -> 히트박스 레이어 인덱스를
if ( ! isEnemy & & enemyHitBoxLayerIndex ! = - 1 ) // 조건이 맞으면 실행할거에요 -> 적 태그가 아니고 레이어가 유효하다면
2026-02-06 04:20:12 +00:00
{
2026-02-12 15:23:25 +00:00
isEnemy = other . gameObject . layer = = enemyHitBoxLayerIndex ; // 조건을 갱신할거에요 -> 레이어 확인으로
2026-02-09 14:49:44 +00:00
}
2026-02-12 15:23:25 +00:00
if ( isEnemy ) // 조건이 맞으면 실행할거에요 -> 적이라면
2026-02-09 14:49:44 +00:00
{
2026-02-12 15:23:25 +00:00
HandleHit ( other , other . ClosestPoint ( transform . position ) ) ; // 실행할거에요 -> 충돌 처리 함수를
2026-02-06 04:20:12 +00:00
}
2026-02-12 15:23:25 +00:00
else if ( other . CompareTag ( "Wall" ) | | other . CompareTag ( "Ground" ) ) // 조건이 틀리면 실행할거에요 -> 벽이나 땅이라면
2026-02-06 04:20:12 +00:00
{
2026-02-12 15:23:25 +00:00
hasHit = true ; // 상태를 바꿀거에요 -> 충돌 처리됨으로
Destroy ( gameObject , 0.05f ) ; // 파괴할거에요 -> 화살을
2026-02-09 14:49:44 +00:00
}
}
2026-02-21 06:14:24 +00:00
/// <summary>
/// [PUBLIC] 외부에서 인스펙터 속성 데이터를 읽을 수 있는 접근자
/// UI 등에서 속성 정보를 표시할 때 사용
/// </summary>
public ElementEffectData GetCurrentElementData ( ) // 함수를 선언할거에요 -> 현재 속성 데이터를 반환하는 접근자를
{
return GetElementData ( currentElement ) ; // 반환할거에요 -> 현재 속성의 설정 데이터를
}
/// <summary>
/// [PUBLIC] 현재 속성 타입 반환
/// </summary>
public ArrowElementType GetCurrentElement ( ) // 함수를 선언할거에요 -> 현재 속성 타입을 반환하는 접근자를
{
return currentElement ; // 반환할거에요 -> 현재 속성 타입을
}
2026-02-09 14:49:44 +00:00
/// <summary>
/// Gizmo 디버그 시각화 — 기존 로직 유지 + 속성 색상 추가
/// </summary>
2026-02-12 15:23:25 +00:00
private void OnDrawGizmos ( ) // 함수를 실행할거에요 -> 기즈모를 그리는 OnDrawGizmos를
2026-02-09 14:49:44 +00:00
{
2026-02-12 15:23:25 +00:00
if ( ! Application . isPlaying | | ! isFired | | ! showDebugRay ) return ; // 조건이 맞으면 중단할거에요 -> 실행 중이 아니거나 발사 전이거나 디버그가 꺼졌다면
2026-02-09 14:49:44 +00:00
2026-02-12 15:23:25 +00:00
Vector3 tipPosition = arrowTip ! = null ? arrowTip . position : transform . position ; // 위치를 결정할거에요 -> 팁 위치 혹은 내 위치로
2026-02-09 14:49:44 +00:00
Vector3 direction = rb ! = null & & rb . velocity . magnitude > 0.1f
? rb . velocity . normalized
2026-02-12 15:23:25 +00:00
: transform . forward ; // 방향을 결정할거에요 -> 물리 속도 방향 혹은 정면으로
2026-02-09 14:49:44 +00:00
2026-02-12 15:23:25 +00:00
Gizmos . color = hasHit ? Color . red : Color . green ; // 색상을 설정할거에요 -> 맞았으면 빨강, 아니면 초록으로
Gizmos . DrawWireSphere ( tipPosition , raycastRadius ) ; // 그림을 그릴거에요 -> 팁 위치에 구체를
Gizmos . DrawWireSphere ( tipPosition + direction * raycastDistance , raycastRadius ) ; // 그림을 그릴거에요 -> 예상 도달 위치에 구체를
2026-02-09 14:49:44 +00:00
2026-02-12 15:23:25 +00:00
Gizmos . color = Color . yellow ; // 색상을 설정할거에요 -> 노란색으로
Gizmos . DrawLine ( tipPosition , tipPosition + direction * raycastDistance ) ; // 선을 그릴거에요 -> 궤적을 표시하는
2026-02-09 14:49:44 +00:00
2026-02-21 06:14:24 +00:00
// 속성별 색상 표시
switch ( currentElement ) // 분기할거에요 -> 속성 타입에 따라
2026-02-09 14:49:44 +00:00
{
2026-02-12 15:23:25 +00:00
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 ; // 색상을 바꿀거에요 -> 노랑으로
2026-02-09 14:49:44 +00:00
}
2026-02-21 06:14:24 +00:00
if ( currentElement ! = ArrowElementType . None ) // 조건이 맞으면 실행할거에요 -> 속성이 있다면
2026-02-09 14:49:44 +00:00
{
2026-02-12 15:23:25 +00:00
Gizmos . DrawWireSphere ( transform . position , 0.5f ) ; // 그림을 그릴거에요 -> 속성 표시용 구체를
2026-02-06 04:20:12 +00:00
}
}
}