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); // 그림을 그릴거에요 -> 속성 표시용 구체를 } } }