+ 해적선 Ai 추가 ㄴ 패트롤, 추격, 공격 등의 패턴 적용 + Cannon 클래스 분리 ㄴ 캐논 자체의 기능만 남기고, Player는 CannonController와 연결해서 사용 + Player, Pirate 용 cannon projectile 분리 + New input system 네이밍 변경 ㄴ ToggleCannon -> ToggleLaunchMode ㄴ FireCannon -> LaunchCannon + 해적선 Ai에 Rayfire(파괴) 기능 테스트용 추가
364 lines
12 KiB
C#
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
|
|
}
|
|
} |