using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을 // ============================================================ // ProjectileImpactSound // // 역할: 투사체(돌, 화염병 등)가 충돌했을 때 // 대상(플레이어 / 바닥 / 벽)에 따라 다른 소리 재생 // // 사용법: // 1. 투사체 프리팹에 이 컴포넌트 Add Component // 2. Inspector에서 소리 슬롯 연결 // 3. 투사체의 OnCollisionEnter 또는 OnTriggerEnter에서 // HandleImpact(other.gameObject) 호출 // (또는 이 컴포넌트가 직접 감지) // // 충돌 대상 판별 순서: // Player Tag → Enemy Tag → Ground Layer → Wall/Default // ============================================================ [RequireComponent(typeof(AudioSource))] // 필수 컴포넌트를 지정할거에요 -> AudioSource를 public class ProjectileImpactSound : MonoBehaviour // 클래스를 선언할거에요 -> 투사체 충돌 사운드를 { // ───────────────────────────────────────────────────────── // 사운드 슬롯 // ───────────────────────────────────────────────────────── [Header("=== 충돌 대상별 소리 ===")] [Tooltip("플레이어에 맞았을 때 소리 (여러 개 = 랜덤)")] [SerializeField] private AudioClip[] hitPlayerSounds; // 배열을 선언할거에요 -> 플레이어 타격음들을 [Tooltip("적 몬스터에 맞았을 때 소리 (여러 개 = 랜덤)")] [SerializeField] private AudioClip[] hitEnemySounds; // 배열을 선언할거에요 -> 적 타격음들을 [Tooltip("바닥에 맞았을 때 소리 (여러 개 = 랜덤)")] [SerializeField] private AudioClip[] hitGroundSounds; // 배열을 선언할거에요 -> 바닥 충돌음들을 [Tooltip("벽/장애물에 맞았을 때 소리 (여러 개 = 랜덤)")] [SerializeField] private AudioClip[] hitWallSounds; // 배열을 선언할거에요 -> 벽 충돌음들을 [Tooltip("매칭 실패 시 기본 충돌음 (여러 개 = 랜덤)")] [SerializeField] private AudioClip[] defaultImpactSounds; // 배열을 선언할거에요 -> 기본 충돌음들을 [Header("=== 충돌 감지 설정 ===")] [Tooltip("바닥으로 판별할 LayerMask (Ground 레이어 등)")] [SerializeField] private LayerMask groundLayer; // 변수를 선언할거에요 -> 바닥 레이어를 [Tooltip("벽으로 판별할 LayerMask (Wall 레이어 등)")] [SerializeField] private LayerMask wallLayer; // 변수를 선언할거에요 -> 벽 레이어를 [Tooltip("충돌 감지 방식 선택\n" + "Collision: OnCollisionEnter (Rigidbody 있는 투사체)\n" + "Trigger: OnTriggerEnter (IsTrigger 켠 투사체)")] [SerializeField] private ImpactDetectMode detectMode = ImpactDetectMode.Collision; // 변수를 선언할거에요 -> 감지 방식을 [Tooltip("충돌 후 오브젝트 자동 제거 여부")] [SerializeField] private bool destroyOnImpact = false; // 변수를 선언할거에요 -> 충돌 후 제거 여부를 (투사체 자체 파괴는 투사체 스크립트에서) // ───────────────────────────────────────────────────────── // 감지 방식 열거형 // ───────────────────────────────────────────────────────── public enum ImpactDetectMode // 열거형을 선언할거에요 -> 충돌 감지 방식을 { Collision, // OnCollisionEnter 사용 Trigger, // OnTriggerEnter 사용 Manual, // 외부에서 HandleImpact() 직접 호출 } // ───────────────────────────────────────────────────────── // 내부 변수 // ───────────────────────────────────────────────────────── private AudioSource _audioSource; // 변수를 선언할거에요 -> AudioSource를 private bool _hasImpacted = false; // 변수를 선언할거에요 -> 이미 충돌 처리됐는지를 (중복 방지) // ───────────────────────────────────────────────────────── // 초기화 // ───────────────────────────────────────────────────────── private void Awake() // 함수를 실행할거에요 -> 초기화를 { _audioSource = GetComponent(); // 가져올거에요 -> AudioSource를 _audioSource.playOnAwake = false; // 끌거에요 -> 자동 재생을 } private void OnEnable() // 함수를 실행할거에요 -> 오브젝트 활성화 시 (풀 재사용 대비) { _hasImpacted = false; // 초기화할거에요 -> 충돌 상태를 } // ───────────────────────────────────────────────────────── // 충돌 감지 — Collision 방식 // ───────────────────────────────────────────────────────── private void OnCollisionEnter(Collision col) // 함수를 실행할거에요 -> 충돌 시 { if (detectMode != ImpactDetectMode.Collision) return; // 중단할거에요 -> Collision 모드 아니면 HandleImpact(col.gameObject, col.contacts[0].point); // 처리할거에요 -> 충돌 대상과 충돌 지점으로 } // ───────────────────────────────────────────────────────── // 충돌 감지 — Trigger 방식 // ───────────────────────────────────────────────────────── private void OnTriggerEnter(Collider other) // 함수를 실행할거에요 -> 트리거 진입 시 { if (detectMode != ImpactDetectMode.Trigger) return; // 중단할거에요 -> Trigger 모드 아니면 HandleImpact(other.gameObject, other.ClosestPoint(transform.position)); // 처리할거에요 -> 충돌 대상과 가장 가까운 지점으로 } // ───────────────────────────────────────────────────────── // 충돌 처리 — 외부에서도 호출 가능 (Manual 모드) // ───────────────────────────────────────────────────────── public void HandleImpact(GameObject hitObject, Vector3 hitPoint) // 함수를 선언할거에요 -> 충돌 처리를 (외부 호출 가능) { if (_hasImpacted) return; // 중단할거에요 -> 이미 처리됐으면 (중복 방지) _hasImpacted = true; // 설정할거에요 -> 처리됨으로 // 대상 판별 → 소리 선택 AudioClip[] clips = DetermineClips(hitObject); // 결정할거에요 -> 대상에 맞는 클립 배열을 PlayImpactSound(clips, hitPoint); // 재생할거에요 -> 충돌음을 } // ───────────────────────────────────────────────────────── // 충돌 대상 판별 // ───────────────────────────────────────────────────────── private AudioClip[] DetermineClips(GameObject hitObject) // 함수를 선언할거에요 -> 충돌 대상에 맞는 클립 배열을 반환하는 DetermineClips를 { // 1순위: 플레이어 if (hitObject.CompareTag("Player")) // 조건이 맞으면 실행할거에요 -> 플레이어이면 return hitPlayerSounds.Length > 0 ? hitPlayerSounds : defaultImpactSounds; // 반환할거에요 -> 플레이어 타격음을 // 2순위: 적 몬스터 if (hitObject.CompareTag("Enemy")) // 조건이 맞으면 실행할거에요 -> 적이면 return hitEnemySounds.Length > 0 ? hitEnemySounds : defaultImpactSounds; // 반환할거에요 -> 적 타격음을 // 3순위: 바닥 레이어 if (IsInLayerMask(hitObject.layer, groundLayer)) // 조건이 맞으면 실행할거에요 -> 바닥 레이어이면 return hitGroundSounds.Length > 0 ? hitGroundSounds : defaultImpactSounds; // 반환할거에요 -> 바닥 충돌음을 // 4순위: 벽 레이어 if (IsInLayerMask(hitObject.layer, wallLayer)) // 조건이 맞으면 실행할거에요 -> 벽 레이어이면 return hitWallSounds.Length > 0 ? hitWallSounds : defaultImpactSounds; // 반환할거에요 -> 벽 충돌음을 // 기본: defaultImpactSounds return defaultImpactSounds; // 반환할거에요 -> 기본 충돌음을 } private bool IsInLayerMask(int layer, LayerMask mask) // 함수를 선언할거에요 -> 레이어가 마스크에 포함됐는지 확인하는 IsInLayerMask를 { return mask == (mask | (1 << layer)); // 반환할거에요 -> 비트 연산으로 포함 여부를 } // ───────────────────────────────────────────────────────── // 충돌음 재생 // ───────────────────────────────────────────────────────── private void PlayImpactSound(AudioClip[] clips, Vector3 position) // 함수를 선언할거에요 -> 충돌음 재생을 { if (clips == null || clips.Length == 0) return; // 중단할거에요 -> 클립 없으면 int idx = UnityEngine.Random.Range(0, clips.Length); // 뽑을거에요 -> 랜덤 인덱스를 if (clips[idx] == null) return; // 중단할거에요 -> 클립 null이면 // AudioSource.PlayClipAtPoint: 오브젝트가 사라져도 소리는 끝까지 재생됨 // 3D 위치에서 재생 → 거리에 따라 자연스럽게 들림 AudioSource.PlayClipAtPoint(clips[idx], position); // 재생할거에요 -> 충돌 위치에서 소리를 } }