OldBlueWater/BlueWater/Assets/02.Scripts/Ai/PirateShipAi.cs
NTG 86f9d2607e Closes #213 #214 해적선 Ai 추가 및 항해 씬 취합
+ 해적선 Ai 추가
  ㄴ 패트롤, 추격, 공격 등의 패턴 적용
+ Cannon 클래스 분리
  ㄴ 캐논 자체의 기능만 남기고, Player는 CannonController와 연결해서 사용
+ Player, Pirate 용 cannon projectile 분리
+ New input system 네이밍 변경
  ㄴ ToggleCannon -> ToggleLaunchMode
  ㄴ FireCannon -> LaunchCannon
+ 해적선 Ai에 Rayfire(파괴) 기능 테스트용 추가
2024-03-05 12:47:17 +09:00

364 lines
12 KiB
C#

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<Patrol>();
cannon = transform.Find("Cannon")?.GetComponent<Cannon>();
}
private void InitStart()
{
ai = GetComponent<IAstarAI>();
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<IDamageable>()?.TakeDamage(power);
cannon.SetActivePredictLine(false);
if (marker)
{
Destroy(marker);
}
}
public void SetCurrentHp(float value) => CurrentHp = value;
#endregion
}
}