2026-02-04 14:06:25 +00:00
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using UnityEngine.AI;
|
|
|
|
|
|
using System.Collections;
|
|
|
|
|
|
|
2026-02-05 15:42:48 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 통합 원거리 몬스터 (키 작은 플레이어 머리 조준 기능 추가)
|
|
|
|
|
|
/// 파일 이름을 반드시 [ UniversalRangedMonster.cs ] 로 맞춰주세요!
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class UniversalRangedMonster : MonsterClass
|
2026-02-04 14:06:25 +00:00
|
|
|
|
{
|
2026-02-05 15:42:48 +00:00
|
|
|
|
public enum AttackStyle { Straight, Lob }
|
|
|
|
|
|
|
|
|
|
|
|
[Header("=== 🏹 공격 스타일 선택 ===")]
|
|
|
|
|
|
[SerializeField] private AttackStyle attackStyle = AttackStyle.Straight;
|
|
|
|
|
|
|
|
|
|
|
|
[Header("공통 설정")]
|
|
|
|
|
|
[SerializeField] private GameObject projectilePrefab;
|
|
|
|
|
|
[SerializeField] private Transform firePoint;
|
2026-02-04 14:06:25 +00:00
|
|
|
|
[SerializeField] private float attackRange = 10f;
|
|
|
|
|
|
[SerializeField] private float attackDelay = 2f;
|
2026-02-05 15:42:48 +00:00
|
|
|
|
[SerializeField] private float detectRange = 15f;
|
2026-02-04 14:06:25 +00:00
|
|
|
|
|
2026-02-05 15:42:48 +00:00
|
|
|
|
[Header("🔹 직선 발사 설정 (활/총)")]
|
|
|
|
|
|
[SerializeField] private float projectileSpeed = 20f;
|
|
|
|
|
|
[SerializeField] private float minDistance = 5f;
|
|
|
|
|
|
|
|
|
|
|
|
[Header("🔸 곡사 투척 설정 (돌/폭탄)")]
|
|
|
|
|
|
[Tooltip("체크하면 거리/높이를 계산해서 정확히 맞춥니다.")]
|
|
|
|
|
|
[SerializeField] private bool usePreciseLob = true;
|
|
|
|
|
|
|
|
|
|
|
|
[Tooltip("던지는 각도 (45도가 최대 사거리)")]
|
|
|
|
|
|
[Range(15f, 75f)][SerializeField] private float launchAngle = 45f;
|
2026-02-04 14:06:25 +00:00
|
|
|
|
|
2026-02-05 15:42:48 +00:00
|
|
|
|
// ⭐ [추가됨] 조준 높이 보정 (키 작은 플레이어 해결용)
|
|
|
|
|
|
[Tooltip("0이면 발가락, 1.0이면 명치, 1.5면 머리를 노립니다.")]
|
|
|
|
|
|
[SerializeField] private float aimHeight = 1.2f;
|
|
|
|
|
|
|
|
|
|
|
|
[Header("🏃♂️ 도망 설정")]
|
|
|
|
|
|
[SerializeField] private float fleeDistance = 5f;
|
|
|
|
|
|
|
|
|
|
|
|
[Header("애니메이션 & 기타")]
|
|
|
|
|
|
[SerializeField] private float throwForce = 15f; // (정밀 모드 꺼졌을 때 사용)
|
|
|
|
|
|
[SerializeField] private float throwUpward = 5f; // (정밀 모드 꺼졌을 때 사용)
|
|
|
|
|
|
[SerializeField] private float reloadTime = 2.0f;
|
|
|
|
|
|
[SerializeField] private GameObject handModel;
|
|
|
|
|
|
[SerializeField] private bool aimAtPlayer = true;
|
|
|
|
|
|
|
|
|
|
|
|
[SerializeField] private string attackAnim = "Monster_Attack";
|
|
|
|
|
|
[SerializeField] private string walkAnim = "Monster_Walk";
|
2026-02-04 14:06:25 +00:00
|
|
|
|
[SerializeField] private float patrolRadius = 5f;
|
|
|
|
|
|
[SerializeField] private float patrolInterval = 3f;
|
|
|
|
|
|
|
|
|
|
|
|
private float lastAttackTime;
|
2026-02-05 15:42:48 +00:00
|
|
|
|
private float nextPatrolTime;
|
|
|
|
|
|
// private bool isPlayerInZone;
|
|
|
|
|
|
private bool isReloading = false;
|
2026-02-04 14:06:25 +00:00
|
|
|
|
|
|
|
|
|
|
protected override void Init()
|
|
|
|
|
|
{
|
2026-02-05 15:42:48 +00:00
|
|
|
|
if (agent != null) { agent.stoppingDistance = attackRange * 0.8f; agent.speed = 3.5f; }
|
2026-02-04 14:06:25 +00:00
|
|
|
|
if (animator != null) animator.applyRootMotion = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected override void ExecuteAILogic()
|
|
|
|
|
|
{
|
2026-02-05 15:42:48 +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
|
|
|
|
|
|
|
|
|
|
float dist = Vector3.Distance(transform.position, playerTransform.position);
|
|
|
|
|
|
|
2026-02-05 15:42:48 +00:00
|
|
|
|
if (dist <= detectRange) HandleCombat(dist);
|
2026-02-04 14:06:25 +00:00
|
|
|
|
else
|
|
|
|
|
|
{
|
2026-02-05 15:42:48 +00:00
|
|
|
|
if (agent.hasPath && agent.destination == playerTransform.position) { agent.ResetPath(); nextPatrolTime = 0; }
|
2026-02-04 14:06:25 +00:00
|
|
|
|
Patrol();
|
|
|
|
|
|
UpdateMovementAnimation();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 15:42:48 +00:00
|
|
|
|
void HandleCombat(float dist)
|
|
|
|
|
|
{
|
|
|
|
|
|
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-04 14:06:25 +00:00
|
|
|
|
void TryAttack()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Time.time < lastAttackTime + attackDelay) return;
|
|
|
|
|
|
lastAttackTime = Time.time;
|
|
|
|
|
|
isAttacking = true;
|
|
|
|
|
|
|
|
|
|
|
|
if (agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; }
|
|
|
|
|
|
|
2026-02-05 15:42:48 +00:00
|
|
|
|
Vector3 lookDir = (playerTransform.position - transform.position).normalized;
|
|
|
|
|
|
lookDir.y = 0;
|
|
|
|
|
|
if (lookDir != Vector3.zero) transform.rotation = Quaternion.LookRotation(lookDir);
|
|
|
|
|
|
|
|
|
|
|
|
animator.Play(attackAnim);
|
2026-02-04 14:06:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 15:42:48 +00:00
|
|
|
|
public override void OnAttackStart()
|
2026-02-04 14:06:25 +00:00
|
|
|
|
{
|
2026-02-05 15:42:48 +00:00
|
|
|
|
if (!projectilePrefab || !firePoint) return;
|
|
|
|
|
|
|
|
|
|
|
|
GameObject obj = Instantiate(projectilePrefab, firePoint.position, transform.rotation);
|
2026-02-04 14:06:25 +00:00
|
|
|
|
|
2026-02-05 15:42:48 +00:00
|
|
|
|
if (obj.TryGetComponent<Projectile>(out var proj))
|
|
|
|
|
|
{
|
|
|
|
|
|
float speed = (attackStyle == AttackStyle.Straight) ? projectileSpeed : 0f;
|
|
|
|
|
|
proj.Initialize(transform.forward, speed, attackDamage);
|
|
|
|
|
|
}
|
2026-02-04 14:06:25 +00:00
|
|
|
|
|
2026-02-05 15:42:48 +00:00
|
|
|
|
if (attackStyle == AttackStyle.Lob)
|
2026-02-04 14:06:25 +00:00
|
|
|
|
{
|
2026-02-05 15:42:48 +00:00
|
|
|
|
Rigidbody rb = obj.GetComponent<Rigidbody>();
|
|
|
|
|
|
if (rb)
|
2026-02-04 14:06:25 +00:00
|
|
|
|
{
|
2026-02-05 15:42:48 +00:00
|
|
|
|
rb.useGravity = true;
|
|
|
|
|
|
|
|
|
|
|
|
// ⭐ 목표 지점 설정 (플레이어 위치 + 높이 보정)
|
|
|
|
|
|
Vector3 targetPos = playerTransform.position + Vector3.up * aimHeight;
|
|
|
|
|
|
|
|
|
|
|
|
if (usePreciseLob)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 보정된 위치(targetPos)를 기준으로 탄도 계산
|
|
|
|
|
|
Vector3 velocity = CalculateLobVelocity(firePoint.position, targetPos, launchAngle);
|
|
|
|
|
|
|
|
|
|
|
|
if (!float.IsNaN(velocity.x))
|
|
|
|
|
|
{
|
|
|
|
|
|
rb.velocity = velocity;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// 계산 실패 시 백업
|
|
|
|
|
|
rb.AddForce(transform.forward * throwForce + Vector3.up * throwUpward, ForceMode.Impulse);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
Vector3 dir = aimAtPlayer ? (targetPos - firePoint.position).normalized : transform.forward;
|
|
|
|
|
|
// dir.y = 0; // 높이 조준을 위해 y제거 주석 처리 (원하면 해제)
|
|
|
|
|
|
rb.AddForce(dir * throwForce + Vector3.up * throwUpward, ForceMode.Impulse);
|
|
|
|
|
|
}
|
2026-02-04 14:06:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 15:42:48 +00:00
|
|
|
|
if (handModel != null) { handModel.SetActive(false); StartCoroutine(ReloadRoutine()); }
|
|
|
|
|
|
}
|
2026-02-04 14:06:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 15:42:48 +00:00
|
|
|
|
Vector3 CalculateLobVelocity(Vector3 origin, Vector3 target, float angle)
|
2026-02-04 14:06:25 +00:00
|
|
|
|
{
|
2026-02-05 15:42:48 +00:00
|
|
|
|
Vector3 dir = target - origin;
|
|
|
|
|
|
float height = dir.y;
|
|
|
|
|
|
dir.y = 0;
|
|
|
|
|
|
float dist = dir.magnitude;
|
2026-02-04 14:06:25 +00:00
|
|
|
|
|
2026-02-05 15:42:48 +00:00
|
|
|
|
float a = angle * Mathf.Deg2Rad;
|
2026-02-04 14:06:25 +00:00
|
|
|
|
|
2026-02-05 15:42:48 +00:00
|
|
|
|
dir.y = dist * Mathf.Tan(a);
|
|
|
|
|
|
dist += height / Mathf.Tan(a);
|
2026-02-04 14:06:25 +00:00
|
|
|
|
|
2026-02-05 15:42:48 +00:00
|
|
|
|
float gravity = Physics.gravity.magnitude;
|
|
|
|
|
|
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-05 15:42:48 +00:00
|
|
|
|
if (velocitySq <= 0) return Vector3.zero;
|
2026-02-04 14:06:25 +00:00
|
|
|
|
|
2026-02-05 15:42:48 +00:00
|
|
|
|
float velocity = Mathf.Sqrt(velocitySq);
|
|
|
|
|
|
return dir.normalized * velocity;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ... (나머지 함수들은 기존과 동일) ...
|
|
|
|
|
|
void RetreatFromPlayer()
|
|
|
|
|
|
{
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
IEnumerator ReloadRoutine()
|
|
|
|
|
|
{
|
|
|
|
|
|
isReloading = true;
|
|
|
|
|
|
yield return new WaitForSeconds(reloadTime);
|
|
|
|
|
|
if (handModel != null) handModel.SetActive(true);
|
|
|
|
|
|
isReloading = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 15:42:48 +00:00
|
|
|
|
public override void OnAttackEnd()
|
2026-02-04 14:06:25 +00:00
|
|
|
|
{
|
2026-02-05 15:42:48 +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-05 15:42:48 +00:00
|
|
|
|
void Patrol()
|
|
|
|
|
|
{
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void UpdateMovementAnimation()
|
2026-02-04 14:06:25 +00:00
|
|
|
|
{
|
2026-02-05 15:42:48 +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
|
|
|
|
}
|
2026-02-05 15:42:48 +00:00
|
|
|
|
|
|
|
|
|
|
// private void OnTriggerEnter(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = true; }
|
|
|
|
|
|
//private void OnTriggerExit(Collider other) { if (other.CompareTag("Player")) isPlayerInZone = false; }
|
2026-02-04 14:06:25 +00:00
|
|
|
|
}
|