using System; using System.Collections; using BehaviorDesigner.Runtime; using Pathfinding; using RayFire; using Sirenix.OdinInspector; using UnityEngine; using UnityEngine.Serialization; // ReSharper disable once CheckNamespace namespace BlueWaterProject { public class PirateShipAi : MonoBehaviour, IDamageable { /*********************************************************************** * Variables ***********************************************************************/ #region Variables // 컴포넌트 [TitleGroup("컴포넌트"), BoxGroup("컴포넌트/컴포넌트", ShowLabel = false)] [Required, SerializeField] private Patrol patrol; [BoxGroup("컴포넌트/컴포넌트", ShowLabel = false)] [Required, SerializeField] private Cannon cannon; [BoxGroup("컴포넌트/컴포넌트", ShowLabel = false)] [SerializeField] private RayfireRigid[] rayfireRigids; // 배의 기본 설정 [field: TitleGroup("배의 기본 설정")] // AI [BoxGroup("배의 기본 설정/AI")] [Tooltip("타겟 감지 거리"), Range(0f, 200f)] [SerializeField] private float viewRadius = 100f; [BoxGroup("배의 기본 설정/AI")] [Tooltip("타겟의 정면 방향으로 Offset만큼의 위치를 목표 지점으로 설정"), Range(0f, 100f)] [SerializeField] private float targetForwardOffset = 50f; [BoxGroup("배의 기본 설정/AI")] [Tooltip("타겟과 유지할 거리"), Range(0f, 100f)] [SerializeField] private float targetMaintainDistance = 40f; [BoxGroup("배의 기본 설정/AI")] [Tooltip("타겟을 놓치기 시작한 거리"), Range(0f, 200f)] [SerializeField] private float missDistance = 100f; [BoxGroup("배의 기본 설정/AI")] [Tooltip("타겟을 놓치기 시작한 거리"), Range(0f, 20f)] [SerializeField] private float missedTargetTime = 5f; [BoxGroup("배의 기본 설정/AI")] [Tooltip("타겟을 완전히 놓친 후, 기다리는 시간"), Range(0f, 10f)] [SerializeField] private float returnPatrolWaitTime = 3f; [BoxGroup("배의 기본 설정/AI")] [Tooltip("타겟과 유지할 거리"), Range(0f, 100f)] [SerializeField] private float cannonLaunchDistance = 60f; // 체력 [field: BoxGroup("배의 기본 설정/체력")] [field: Tooltip("배의 최대 체력"), Range(1f, 1000f)] [field: SerializeField] public float MaxHp { get; private set; } = 100f; [field: BoxGroup("배의 기본 설정/체력")] [field: Tooltip("배의 현재 체력")] [field: SerializeField] public float CurrentHp { get; private set; } // 기타 [BoxGroup("배의 기본 설정/기타")] [SerializeField] private bool isDrawingGizmos = true; [BoxGroup("배의 기본 설정/기타")] [SerializeField] private LayerMask targetLayer; [field: SerializeField] public Collider Target { get; private set; } private IAstarAI ai; private Collider[] hitColliders; private WaitForSeconds rescanTime = new(1f); private Coroutine findNearestTargetCoroutine; private Coroutine patrolCoroutine; private Coroutine chaseCoroutine; private Coroutine missedTargetCoroutine; private const int MAX_HIT_SIZE = 5; #endregion /*********************************************************************** * Unity Events ***********************************************************************/ #region Unity Events private void OnDrawGizmosSelected() { if (!isDrawingGizmos) return; var centerPosition = transform.position; Gizmos.color = Color.red; Gizmos.DrawWireSphere(centerPosition, viewRadius); } private void Start() { InitStart(); } private void Update() { RotateCannon(); } #endregion /*********************************************************************** * Init Methods ***********************************************************************/ #region Init Methods [Button("셋팅 초기화")] private void Init() { patrol = GetComponent(); cannon = transform.Find("Cannon")?.GetComponent(); } private void InitStart() { ai = GetComponent(); hitColliders = new Collider[MAX_HIT_SIZE]; SetCurrentHp(MaxHp); findNearestTargetCoroutine = StartCoroutine(nameof(FindNearestTargetCoroutine)); if (!Target) { patrolCoroutine = StartCoroutine(nameof(PatrolCoroutine)); } } #endregion /*********************************************************************** * Interfaces ***********************************************************************/ #region Interfaces public void TakeDamage(float attackerPower, Vector3? attackPos = null) { var changeHp = Mathf.Max(CurrentHp - attackerPower, 0f); SetCurrentHp(changeHp); if (CurrentHp == 0f) { Die(); } } public void Die() { if (ai != null) { ai.isStopped = true; } foreach (var element in rayfireRigids) { if (element) { element.Demolish(); } } } public float GetCurrentHp() => CurrentHp; #endregion /*********************************************************************** * Methods ***********************************************************************/ #region Methods private IEnumerator FindNearestTargetCoroutine() { while (true) { var centerPos = transform.position; var hitSize = Physics.OverlapSphereNonAlloc(centerPos, viewRadius, hitColliders, targetLayer, QueryTriggerInteraction.Ignore); if (hitSize <= 0) { Target = null; yield return rescanTime; continue; } var nearestDistance = float.PositiveInfinity; Collider nearestTargetCollider = null; for (var i = 0; i < hitSize; i++) { var distance = Vector3.Distance(centerPos, hitColliders[i].transform.position); if (distance >= nearestDistance) continue; nearestDistance = distance; nearestTargetCollider = hitColliders[i]; } Target = nearestTargetCollider; if (Target != null) { if (patrolCoroutine != null) { StopCoroutine(patrolCoroutine); patrolCoroutine = null; } chaseCoroutine ??= StartCoroutine(nameof(ChaseCoroutine)); findNearestTargetCoroutine = null; yield break; } yield return rescanTime; } } private IEnumerator PatrolCoroutine() { if (patrol.WayPoints.Length <= 0) yield break; var movePoint = patrol.GetCurrentWayPoint(); SetDestination(movePoint); while (ai.pathPending) { yield return null; } while (!ai.reachedDestination) { SetDestination(movePoint); yield return null; } var elapsedTime = 0f; var currentWaitTime = patrol.GetCurrentWayPointInfo().WaitTime; while (elapsedTime < currentWaitTime) { elapsedTime += Time.deltaTime; yield return null; } patrol.SetMovePoint(); patrolCoroutine = StartCoroutine(nameof(PatrolCoroutine)); } private IEnumerator ChaseCoroutine() { while (Target) { var targetTransform = Target.transform; var toTarget = targetTransform.position - transform.position; toTarget.y = 0f; var targetDistance = toTarget.magnitude; var targetDirection = toTarget.normalized; if (targetDistance > missDistance) { missedTargetCoroutine ??= StartCoroutine(nameof(MissedTargetCoroutine)); } var movePosition = targetTransform.position + targetTransform.forward * targetForwardOffset; if (targetDistance < targetMaintainDistance) { var crossDirection = Vector3.Cross(targetDirection, Vector3.up).normalized; movePosition = targetTransform.position + crossDirection * targetMaintainDistance; } if (targetDistance < cannonLaunchDistance && !cannon.IsReloading) { cannon.LaunchAtTarget(Target); } SetDestination(movePosition); yield return null; } chaseCoroutine = null; } private IEnumerator MissedTargetCoroutine() { var elapsedTime = 0f; while (elapsedTime <= missedTargetTime) { elapsedTime += Time.deltaTime; var targetDistance = Vector3.Distance(transform.position, Target.transform.position); if (targetDistance <= missDistance) { missedTargetCoroutine = null; yield break; } yield return null; } Target = null; ai.isStopped = true; yield return new WaitForSeconds(returnPatrolWaitTime); missedTargetCoroutine = null; findNearestTargetCoroutine ??= StartCoroutine(nameof(FindNearestTargetCoroutine)); patrolCoroutine ??= StartCoroutine(nameof(PatrolCoroutine)); } private void SetDestination(Vector3 position) { if (ai == null) return; if (ai.isStopped) { ai.isStopped = false; } ai.destination = position; } private void RotateCannon() { if (!Target) return; var directionToMouse = (Target.transform.position - transform.position).normalized; directionToMouse.y = 0f; var lookRotation = Quaternion.LookRotation(directionToMouse); var cannonRotationDirection = Quaternion.Euler(0f, lookRotation.eulerAngles.y, 0f); cannon.transform.rotation = cannonRotationDirection; } public void HitAction(RaycastHit hit, float power, GameObject marker = null) { hit.transform.GetComponent()?.TakeDamage(power); cannon.SetActivePredictLine(false); if (marker) { Destroy(marker); } } public void SetCurrentHp(float value) => CurrentHp = value; #endregion } }