2026-02-12 15:23:25 +00:00
using UnityEngine ; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using UnityEngine.AI ; // 길찾기(내비게이션) 기능을 불러올거에요 -> UnityEngine.AI를
using System.Collections ; // 코루틴 기능을 사용할거에요 -> System.Collections를
2026-02-04 14:06:25 +00:00
2026-02-05 15:42:48 +00:00
/// <summary>
/// 통합 원거리 몬스터 (키 작은 플레이어 머리 조준 기능 추가)
/// 파일 이름을 반드시 [ UniversalRangedMonster.cs ] 로 맞춰주세요!
/// </summary>
2026-02-12 15:23:25 +00:00
public class UniversalRangedMonster : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 UniversalRangedMonster를
2026-02-04 14:06:25 +00:00
{
2026-02-12 15:23:25 +00:00
public enum AttackStyle { Straight , Lob } // 열거형을 정의할거에요 -> 공격 방식(직선, 곡사)을 정의하는 AttackStyle을
2026-02-05 15:42:48 +00:00
2026-02-12 15:23:25 +00:00
[Header("=== 🏹 공격 스타일 선택 ===")] // 인스펙터 창에 제목을 표시할거에요 -> === 🏹 공격 스타일 선택 === 을
[SerializeField] private AttackStyle attackStyle = AttackStyle . Straight ; // 변수를 선언할거에요 -> 공격 스타일(기본 직선)을 attackStyle에
2026-02-05 15:42:48 +00:00
2026-02-12 15:23:25 +00:00
[Header("공통 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 공통 설정 을
[SerializeField] private GameObject projectilePrefab ; // 변수를 선언할거에요 -> 발사체 프리팹을 projectilePrefab에
[SerializeField] private Transform firePoint ; // 변수를 선언할거에요 -> 발사 위치를 firePoint에
[SerializeField] private float attackRange = 10f ; // 변수를 선언할거에요 -> 공격 사거리(10.0)를 attackRange에
[SerializeField] private float attackDelay = 2f ; // 변수를 선언할거에요 -> 공격 딜레이(2.0초)를 attackDelay에
[SerializeField] private float detectRange = 15f ; // 변수를 선언할거에요 -> 인식 거리(15.0)를 detectRange에
2026-02-04 14:06:25 +00:00
2026-02-12 15:23:25 +00:00
[Header("🔹 직선 발사 설정 (활/총)")] // 인스펙터 창에 제목을 표시할거에요 -> 🔹 직선 발사 설정 (활/총) 을
[SerializeField] private float projectileSpeed = 20f ; // 변수를 선언할거에요 -> 투사체 속도(20.0)를 projectileSpeed에
[SerializeField] private float minDistance = 5f ; // 변수를 선언할거에요 -> 최소 거리(도망가는 거리)를 minDistance에
2026-02-05 15:42:48 +00:00
2026-02-12 15:23:25 +00:00
[Header("🔸 곡사 투척 설정 (돌/폭탄)")] // 인스펙터 창에 제목을 표시할거에요 -> 🔸 곡사 투척 설정 (돌/폭탄) 을
[Tooltip("체크하면 거리/높이를 계산해서 정확히 맞춥니다.")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
[SerializeField] private bool usePreciseLob = true ; // 변수를 선언할거에요 -> 정밀 곡사 사용 여부를 usePreciseLob에
2026-02-05 15:42:48 +00:00
2026-02-12 15:23:25 +00:00
[Tooltip("던지는 각도 (45도가 최대 사거리)")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
[Range(15f, 75f)] [ SerializeField ] private float launchAngle = 45f ; // 변수를 선언할거에요 -> 발사 각도(45도)를 launchAngle에
2026-02-04 14:06:25 +00:00
2026-02-05 15:42:48 +00:00
// ⭐ [추가됨] 조준 높이 보정 (키 작은 플레이어 해결용)
2026-02-12 15:23:25 +00:00
[Tooltip("0이면 발가락, 1.0이면 명치, 1.5면 머리를 노립니다.")] // 마우스를 올리면 설명을 보여줄거에요 -> 툴팁 내용을
[SerializeField] private float aimHeight = 1.2f ; // 변수를 선언할거에요 -> 조준 높이 오프셋(1.2)을 aimHeight에
[Header("🏃♂️ 도망 설정")] // 인스펙터 창에 제목을 표시할거에요 -> 🏃♂️ 도망 설정 을
[SerializeField] private float fleeDistance = 5f ; // 변수를 선언할거에요 -> 도망가는 거리(5.0)를 fleeDistance에
[Header("애니메이션 & 기타")] // 인스펙터 창에 제목을 표시할거에요 -> 애니메이션 & 기타 를
[SerializeField] private float throwForce = 15f ; // 변수를 선언할거에요 -> 기본 투척 힘을 throwForce에
[SerializeField] private float throwUpward = 5f ; // 변수를 선언할거에요 -> 기본 상향 힘을 throwUpward에
[SerializeField] private float reloadTime = 2.0f ; // 변수를 선언할거에요 -> 장전 시간(2.0초)을 reloadTime에
[SerializeField] private GameObject handModel ; // 변수를 선언할거에요 -> 손에 든 무기 모델을 handModel에
[SerializeField] private bool aimAtPlayer = true ; // 변수를 선언할거에요 -> 플레이어 조준 여부를 aimAtPlayer에
[SerializeField] private string attackAnim = "Monster_Attack" ; // 변수를 선언할거에요 -> 공격 애니메이션 이름을 attackAnim에
[SerializeField] private string walkAnim = "Monster_Walk" ; // 변수를 선언할거에요 -> 걷기 애니메이션 이름을 walkAnim에
[SerializeField] private float patrolRadius = 5f ; // 변수를 선언할거에요 -> 순찰 반경(5.0)을 patrolRadius에
[SerializeField] private float patrolInterval = 3f ; // 변수를 선언할거에요 -> 순찰 간격(3.0초)을 patrolInterval에
private float lastAttackTime ; // 변수를 선언할거에요 -> 마지막 공격 시간을 lastAttackTime에
private float nextPatrolTime ; // 변수를 선언할거에요 -> 다음 순찰 시간을 nextPatrolTime에
private bool isReloading = false ; // 변수를 선언할거에요 -> 장전 중 여부를 isReloading에
protected override void Init ( ) // 함수를 덮어씌워 실행할거에요 -> 초기화 로직인 Init을
2026-02-04 14:06:25 +00:00
{
2026-02-12 15:23:25 +00:00
if ( agent ! = null ) { agent . stoppingDistance = attackRange * 0.8f ; agent . speed = 3.5f ; } // 조건이 맞으면 설정할거에요 -> 정지 거리를 사거리의 80%로, 속도를 3.5로
if ( animator ! = null ) animator . applyRootMotion = false ; // 조건이 맞으면 설정할거에요 -> 애니메이션 이동을 끄기로
2026-02-04 14:06:25 +00:00
}
2026-02-12 15:23:25 +00:00
protected override void ExecuteAILogic ( ) // 함수를 덮어씌워 실행할거에요 -> AI 행동 로직인 ExecuteAILogic을
2026-02-04 14:06:25 +00:00
{
2026-02-12 15:23:25 +00:00
if ( isHit | | isAttacking | | isResting | | isReloading ) return ; // 조건이 맞으면 중단할거에요 -> 피격, 공격, 휴식, 장전 중이라면
if ( isAttacking & & animator . GetCurrentAnimatorStateInfo ( 0 ) . IsName ( Monster_Idle ) ) OnAttackEnd ( ) ; // 조건이 맞으면 실행할거에요 -> 공격 중인데 모션이 대기라면 강제로 공격 종료를
2026-02-04 14:06:25 +00:00
2026-02-12 15:23:25 +00:00
float dist = Vector3 . Distance ( transform . position , playerTransform . position ) ; // 거리를 계산할거에요 -> 플레이어와의 거리를
2026-02-04 14:06:25 +00:00
2026-02-12 15:23:25 +00:00
if ( dist < = detectRange ) HandleCombat ( dist ) ; // 조건이 맞으면 실행할거에요 -> 감지 거리 이내라면 전투 처리(HandleCombat)를
else // 조건이 틀리면 실행할거에요 -> 멀리 있다면
2026-02-04 14:06:25 +00:00
{
2026-02-12 15:23:25 +00:00
if ( agent . hasPath & & agent . destination = = playerTransform . position ) { agent . ResetPath ( ) ; nextPatrolTime = 0 ; } // 조건이 맞으면 실행할거에요 -> 추격 중이었다면 경로를 취소하고 순찰을 준비하기를
Patrol ( ) ; // 함수를 실행할거에요 -> 순찰을 도는 Patrol을
UpdateMovementAnimation ( ) ; // 함수를 실행할거에요 -> 애니메이션 갱신을
2026-02-04 14:06:25 +00:00
}
}
2026-02-12 15:23:25 +00:00
void HandleCombat ( float dist ) // 함수를 선언할거에요 -> 거리별 전투 행동을 정하는 HandleCombat을
2026-02-05 15:42:48 +00:00
{
2026-02-12 15:23:25 +00:00
if ( attackStyle = = AttackStyle . Straight & & dist < minDistance ) RetreatFromPlayer ( ) ; // 조건이 맞으면 실행할거에요 -> 직선 공격 타입이고 너무 가까우면 도망가기를
else if ( dist < = attackRange ) TryAttack ( ) ; // 조건이 맞으면 실행할거에요 -> 사거리 안이라면 공격 시도를
else { if ( agent . isOnNavMesh ) { agent . isStopped = false ; agent . SetDestination ( playerTransform . position ) ; UpdateMovementAnimation ( ) ; } } // 그 외엔 실행할거에요 -> 플레이어를 향해 추격하기를
2026-02-05 15:42:48 +00:00
}
2026-02-12 15:23:25 +00:00
void TryAttack ( ) // 함수를 선언할거에요 -> 공격을 수행하는 TryAttack을
2026-02-04 14:06:25 +00:00
{
2026-02-12 15:23:25 +00:00
if ( Time . time < lastAttackTime + attackDelay ) return ; // 조건이 맞으면 중단할거에요 -> 쿨타임이 안 지났다면
lastAttackTime = Time . time ; // 값을 저장할거에요 -> 현재 시간을 마지막 공격 시간으로
isAttacking = true ; // 상태를 바꿀거에요 -> 공격 중 상태를 참(true)으로
2026-02-04 14:06:25 +00:00
2026-02-12 15:23:25 +00:00
if ( agent . isOnNavMesh ) { agent . isStopped = true ; agent . velocity = Vector3 . zero ; } // 조건이 맞으면 실행할거에요 -> 이동을 멈추고 제자리에 서기를
2026-02-04 14:06:25 +00:00
2026-02-12 15:23:25 +00:00
Vector3 lookDir = ( playerTransform . position - transform . position ) . normalized ; // 벡터를 계산할거에요 -> 플레이어를 바라보는 방향을
lookDir . y = 0 ; // 값을 바꿀거에요 -> 높이 차이를 무시하게
if ( lookDir ! = Vector3 . zero ) transform . rotation = Quaternion . LookRotation ( lookDir ) ; // 회전시킬거에요 -> 플레이어 쪽을 보도록
2026-02-05 15:42:48 +00:00
2026-02-12 15:23:25 +00:00
animator . Play ( attackAnim ) ; // 재생할거에요 -> 공격 애니메이션을
2026-02-04 14:06:25 +00:00
}
2026-02-12 15:23:25 +00:00
public override void OnAttackStart ( ) // 함수를 덮어씌워 실행할거에요 -> 공격 타이밍(애니메이션 이벤트)에 호출될 OnAttackStart를
2026-02-04 14:06:25 +00:00
{
2026-02-12 15:23:25 +00:00
if ( ! projectilePrefab | | ! firePoint ) return ; // 조건이 맞으면 중단할거에요 -> 프리팹이나 발사 위치가 없다면
2026-02-05 15:42:48 +00:00
2026-02-12 15:23:25 +00:00
GameObject obj = Instantiate ( projectilePrefab , firePoint . position , transform . rotation ) ; // 생성할거에요 -> 투사체를 발사 위치에
2026-02-04 14:06:25 +00:00
2026-02-12 15:23:25 +00:00
if ( obj . TryGetComponent < Projectile > ( out var proj ) ) // 조건이 맞으면 실행할거에요 -> 투사체 스크립트가 있다면
2026-02-05 15:42:48 +00:00
{
2026-02-12 15:23:25 +00:00
float speed = ( attackStyle = = AttackStyle . Straight ) ? projectileSpeed : 0f ; // 값을 결정할거에요 -> 직선이면 속도를, 곡사면 0(물리)을
proj . Initialize ( transform . forward , speed , attackDamage ) ; // 초기화할거에요 -> 방향, 속도, 데미지 정보를 전달해서
2026-02-05 15:42:48 +00:00
}
2026-02-04 14:06:25 +00:00
2026-02-12 15:23:25 +00:00
if ( attackStyle = = AttackStyle . Lob ) // 조건이 맞으면 실행할거에요 -> 곡사 공격 타입이라면
2026-02-04 14:06:25 +00:00
{
2026-02-12 15:23:25 +00:00
Rigidbody rb = obj . GetComponent < Rigidbody > ( ) ; // 컴포넌트를 가져올거에요 -> 투사체의 리지드바디를
if ( rb ) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면
2026-02-04 14:06:25 +00:00
{
2026-02-12 15:23:25 +00:00
rb . useGravity = true ; // 기능을 켤거에요 -> 중력 영향을 받도록
2026-02-05 15:42:48 +00:00
// ⭐ 목표 지점 설정 (플레이어 위치 + 높이 보정)
2026-02-12 15:23:25 +00:00
Vector3 targetPos = playerTransform . position + Vector3 . up * aimHeight ; // 위치를 계산할거에요 -> 플레이어 위치에 조준 높이를 더해서
2026-02-05 15:42:48 +00:00
2026-02-12 15:23:25 +00:00
if ( usePreciseLob ) // 조건이 맞으면 실행할거에요 -> 정밀 곡사를 사용한다면
2026-02-05 15:42:48 +00:00
{
// 보정된 위치(targetPos)를 기준으로 탄도 계산
2026-02-12 15:23:25 +00:00
Vector3 velocity = CalculateLobVelocity ( firePoint . position , targetPos , launchAngle ) ; // 계산할거에요 -> 목표에 도달하기 위한 물리 속도를
2026-02-05 15:42:48 +00:00
2026-02-12 15:23:25 +00:00
if ( ! float . IsNaN ( velocity . x ) ) // 조건이 맞으면 실행할거에요 -> 계산 결과가 유효하다면
2026-02-05 15:42:48 +00:00
{
2026-02-12 15:23:25 +00:00
rb . velocity = velocity ; // 값을 넣을거에요 -> 계산된 속도를 리지드바디에
2026-02-05 15:42:48 +00:00
}
2026-02-12 15:23:25 +00:00
else // 조건이 틀리면 실행할거에요 -> 계산 실패(도달 불가)라면
2026-02-05 15:42:48 +00:00
{
// 계산 실패 시 백업
2026-02-12 15:23:25 +00:00
rb . AddForce ( transform . forward * throwForce + Vector3 . up * throwUpward , ForceMode . Impulse ) ; // 힘을 가할거에요 -> 기본 힘으로 던지기를
2026-02-05 15:42:48 +00:00
}
}
2026-02-12 15:23:25 +00:00
else // 조건이 틀리면 실행할거에요 -> 정밀 곡사가 아니라면
2026-02-05 15:42:48 +00:00
{
2026-02-12 15:23:25 +00:00
Vector3 dir = aimAtPlayer ? ( targetPos - firePoint . position ) . normalized : transform . forward ; // 방향을 결정할거에요 -> 타겟 방향 혹은 정면으로
rb . AddForce ( dir * throwForce + Vector3 . up * throwUpward , ForceMode . Impulse ) ; // 힘을 가할거에요 -> 방향과 힘을 적용해서 던지기를
2026-02-05 15:42:48 +00:00
}
2026-02-04 14:06:25 +00:00
}
2026-02-12 15:23:25 +00:00
if ( handModel ! = null ) { handModel . SetActive ( false ) ; StartCoroutine ( ReloadRoutine ( ) ) ; } // 조건이 맞으면 실행할거에요 -> 손에 있는 무기를 숨기고 장전 코루틴을 시작하기를
2026-02-05 15:42:48 +00:00
}
2026-02-04 14:06:25 +00:00
}
2026-02-12 15:23:25 +00:00
Vector3 CalculateLobVelocity ( Vector3 origin , Vector3 target , float angle ) // 함수를 선언할거에요 -> 곡사 탄도 속도를 계산하는 CalculateLobVelocity를
2026-02-04 14:06:25 +00:00
{
2026-02-12 15:23:25 +00:00
Vector3 dir = target - origin ; // 벡터를 계산할거에요 -> 목표까지의 거리 벡터를
float height = dir . y ; // 값을 저장할거에요 -> 높이 차이를
dir . y = 0 ; // 값을 바꿀거에요 -> 수평 거리 계산을 위해 y를 0으로
float dist = dir . magnitude ; // 값을 저장할거에요 -> 수평 거리를
2026-02-04 14:06:25 +00:00
2026-02-12 15:23:25 +00:00
float a = angle * Mathf . Deg2Rad ; // 값을 변환할거에요 -> 각도를 라디안으로
2026-02-04 14:06:25 +00:00
2026-02-12 15:23:25 +00:00
dir . y = dist * Mathf . Tan ( a ) ; // 값을 계산할거에요 -> 탄젠트를 이용해 높이를
dist + = height / Mathf . Tan ( a ) ; // 값을 보정할거에요 -> 높이 차이에 따른 거리 보정을
2026-02-04 14:06:25 +00:00
2026-02-12 15:23:25 +00:00
float gravity = Physics . gravity . magnitude ; // 값을 가져올거에요 -> 중력 가속도를
// 물리 공식: v^2 = (g * x^2) / (2 * cos^2(a) * (x * tan(a) - y))
float velocitySq = ( gravity * dist * dist ) / ( 2 * Mathf . Pow ( Mathf . Cos ( a ) , 2 ) * ( dist * Mathf . Tan ( a ) - height ) ) ; // 값을 계산할거에요 -> 필요한 속도의 제곱을
2026-02-04 14:06:25 +00:00
2026-02-12 15:23:25 +00:00
if ( velocitySq < = 0 ) return Vector3 . zero ; // 조건이 맞으면 반환할거에요 -> 계산 불가라면 0 벡터를
2026-02-04 14:06:25 +00:00
2026-02-12 15:23:25 +00:00
float velocity = Mathf . Sqrt ( velocitySq ) ; // 값을 계산할거에요 -> 제곱근을 구해서 실제 속도를
return dir . normalized * velocity ; // 반환할거에요 -> 방향 벡터에 속도를 곱해서
2026-02-05 15:42:48 +00:00
}
2026-02-12 15:23:25 +00:00
void RetreatFromPlayer ( ) // 함수를 선언할거에요 -> 플레이어로부터 도망가는 RetreatFromPlayer를
2026-02-05 15:42:48 +00:00
{
2026-02-12 15:23:25 +00:00
if ( agent . pathPending | | ( agent . remainingDistance > 0.5f & & agent . velocity . magnitude > 0.1f ) ) { UpdateMovementAnimation ( ) ; return ; } // 조건이 맞으면 중단할거에요 -> 이미 이동 중이라면
Vector3 dir = ( transform . position - playerTransform . position ) . normalized ; // 벡터를 계산할거에요 -> 플레이어 반대 방향을
Vector3 pos = transform . position + dir * fleeDistance ; // 위치를 계산할거에요 -> 도망갈 목표 지점을
if ( NavMesh . SamplePosition ( pos , out NavMeshHit hit , 5f , NavMesh . AllAreas ) ) { if ( agent . isOnNavMesh ) { agent . isStopped = false ; agent . SetDestination ( hit . position ) ; } } // 조건이 맞으면 실행할거에요 -> 유효한 위치라면 그곳으로 이동하기를
UpdateMovementAnimation ( ) ; // 함수를 실행할거에요 -> 이동 애니메이션 갱신을
2026-02-04 14:06:25 +00:00
}
2026-02-12 15:23:25 +00:00
IEnumerator ReloadRoutine ( ) // 코루틴 함수를 선언할거에요 -> 무기 장전(재생성)을 처리하는 ReloadRoutine을
2026-02-04 14:06:25 +00:00
{
2026-02-12 15:23:25 +00:00
isReloading = true ; // 상태를 바꿀거에요 -> 장전 중 상태를 참으로
yield return new WaitForSeconds ( reloadTime ) ; // 기다릴거에요 -> 장전 시간만큼
if ( handModel ! = null ) handModel . SetActive ( true ) ; // 조건이 맞으면 실행할거에요 -> 손에 있는 무기를 다시 보이게
isReloading = false ; // 상태를 바꿀거에요 -> 장전 중 상태를 거짓으로
2026-02-04 14:06:25 +00:00
}
2026-02-12 15:23:25 +00:00
public override void OnAttackEnd ( ) // 함수를 덮어씌워 실행할거에요 -> 공격 종료 처리를 하는 OnAttackEnd를
2026-02-04 14:06:25 +00:00
{
2026-02-12 15:23:25 +00:00
isAttacking = false ; // 상태를 바꿀거에요 -> 공격 중 상태를 거짓으로
if ( animator ! = null ) animator . Play ( Monster_Idle ) ; // 조건이 맞으면 실행할거에요 -> 대기 애니메이션으로 복귀를
if ( ! isDead & & ! isHit ) StartCoroutine ( RestAfterAttack ( ) ) ; // 조건이 맞으면 실행할거에요 -> 살아있다면 휴식 코루틴 시작을
2026-02-04 14:06:25 +00:00
}
2026-02-12 15:23:25 +00:00
void Patrol ( ) // 함수를 선언할거에요 -> 순찰 로직 Patrol을
2026-02-05 15:42:48 +00:00
{
2026-02-12 15:23:25 +00:00
if ( Time . time < nextPatrolTime ) { if ( agent . velocity . magnitude < 0.1f ) animator . Play ( Monster_Idle ) ; return ; } // 조건이 맞으면 중단할거에요 -> 대기 시간이라면 대기 모션 재생 후 리턴
Vector3 randomPoint = transform . position + new Vector3 ( Random . Range ( - patrolRadius , patrolRadius ) , 0 , Random . Range ( - patrolRadius , patrolRadius ) ) ; // 벡터를 계산할거에요 -> 랜덤 순찰 지점을
if ( NavMesh . SamplePosition ( randomPoint , out NavMeshHit hit , 3f , NavMesh . AllAreas ) ) { if ( agent . isOnNavMesh ) { agent . isStopped = false ; agent . SetDestination ( hit . position ) ; } } // 조건이 맞으면 실행할거에요 -> 유효한 위치라면 이동하기를
nextPatrolTime = Time . time + patrolInterval ; // 값을 갱신할거에요 -> 다음 순찰 시간을
2026-02-05 15:42:48 +00:00
}
2026-02-12 15:23:25 +00:00
void UpdateMovementAnimation ( ) // 함수를 선언할거에요 -> 이동 애니메이션 갱신 함수를
2026-02-04 14:06:25 +00:00
{
2026-02-12 15:23:25 +00:00
if ( isAttacking | | isHit | | isResting ) return ; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면
if ( agent . velocity . magnitude > 0.1f ) animator . Play ( walkAnim ) ; else animator . Play ( Monster_Idle ) ; // 조건에 따라 재생할거에요 -> 걷기 또는 대기 애니메이션을
2026-02-04 14:06:25 +00:00
}
}