OldBlueWater/BlueWater/Assets/02.Scripts/Character/Crewmate/Crewmate.cs

560 lines
19 KiB
C#
Raw Normal View History

2023-10-17 07:31:10 +00:00
using System;
using System.Collections;
using BehaviorDesigner.Runtime;
2023-10-17 07:31:10 +00:00
using Sirenix.OdinInspector;
using UnityEngine;
2023-10-17 07:31:10 +00:00
using UnityEngine.AI;
using UnityEngine.InputSystem;
using UnityEngine.UI;
// ReSharper disable once CheckNamespace
namespace BlueWaterProject
{
[RequireComponent(typeof(PlayerInput))]
public abstract class Crewmate : BaseCharacter, IDamageable, IAnimatorBridge, IAiView, INormalAttack, IInIslandPlayer
{
2023-10-17 07:31:10 +00:00
#region Properties and variables
// DrawGizmos
2023-10-17 07:31:10 +00:00
[Title("DrawGizmos")]
[Tooltip("전체 Gizmos 그리기 여부")]
[SerializeField] private bool isDrawGizmos = true;
[ShowIf("@isDrawGizmos")]
[Tooltip("타겟 인식 범위 그리기 여부")]
[SerializeField] private bool isDrawViewRange = true;
[ShowIf("@isDrawGizmos")]
[Tooltip("이동제한 범위 그리기 여부")]
[SerializeField] private bool isDrawDefenseRange = true;
[ShowIf("@isDrawGizmos")]
[Tooltip("타겟과의 상태 그리기 여부\n빨간색 = 공격 범위 밖\n파란색 = 공격 범위 안")]
[SerializeField] private bool isDrawTargetRange = true;
// Stat
2023-10-17 07:31:10 +00:00
[field: Title("Stat")]
[field: Tooltip("최대 체력 설정")]
[field: SerializeField] public float MaxHp { get; private set; } = 100f;
[field: Tooltip("현재 체력")]
[field: SerializeField] public float CurrentHp { get; private set; }
[field: Tooltip("이동 속도 설정")]
[field: SerializeField] public float MoveSpd { get; set; } = 5f;
2023-10-17 07:31:10 +00:00
[field: Tooltip("공격력 설정")]
[field: SerializeField] public float Atk { get; private set; } = 10f;
[field: Tooltip("공격 속도(다음 공격 주기)\nAtkCooldown = 2f (2초마다 1번 공격)")]
[field: SerializeField] public float AtkCooldown { get; private set; } = 1f;
[field: Tooltip("공격 사거리 설정")]
[field: SerializeField] public float AtkRange { get; set; } = 1.5f;
[field: Tooltip("이동 제한 범위 설정")]
[field: SerializeField] public float DefenseRange { get; private set; } = 20f;
[field: Tooltip("Idle 상태에서 랜덤으로 이동 여부")]
[field: SerializeField] public bool IsRandomMove { get; set; }
[field: ShowIf("@IsRandomMove")]
[field: Tooltip("Idle 상태에서 이동하는 범위 설정")]
[field: SerializeField] public float RandomMoveRange { get; set; }
// HpSlider
[Title("HpSlider")]
[SerializeField] private bool useHpSlider = true;
[ShowIf("@useHpSlider")]
[Required("HpSlider 프리팹을 넣어주세요.")]
[SerializeField] private GameObject hpSliderPrefab;
[ShowIf("@useHpSlider")]
[SerializeField] private Vector3 hpSliderOffset = Vector3.up;
[ShowIf("@useHpSlider")]
[DisableIf("@true")]
[SerializeField] private Slider hpSlider;
2023-10-17 07:31:10 +00:00
// Data
2023-10-17 07:31:10 +00:00
[field: Title("Data")]
[field: DisableIf("@true")]
[field: SerializeField] public Vector3 DefensePos { get; set; }
[field: DisableIf("@true")]
[field: SerializeField] public bool IsCombated { get; set; }
[field: DisableIf("@true")]
[field: SerializeField] public bool BeAttackedInIdle { get; set; }
[field: DisableIf("@true")]
[field: SerializeField] public bool UseRigidbody { get; set; }
[DisableIf("@true")]
[SerializeField] private bool beAttacked;
[DisableIf("@true")]
[SerializeField] protected bool isAttacking;
// 일반 변수
public int CrewmatePrefabIndex { get; set; } = -1;
protected Vector2 movementInput;
2023-10-17 07:31:10 +00:00
protected bool usedNormalAttackCoroutine;
protected WaitForSeconds waitAtkCooldown;
// 컴포넌트
public GameObject GameObject => gameObject;
public Transform Transform => transform;
public Rigidbody Rb { get; set; }
public Collider MyCollider { get; set; }
2023-10-17 07:31:10 +00:00
public NavMeshAgent Agent { get; set; }
private BehaviorTree bt;
private Transform unitRoot;
protected Animator myAnimator;
private Canvas worldSpaceCanvas;
2023-10-17 07:31:10 +00:00
// Hash
protected static readonly int RunStateHash = Animator.StringToHash("RunState");
protected static readonly int AttackHash = Animator.StringToHash("Attack");
protected static readonly int AttackStateHash = Animator.StringToHash("AttackState");
protected static readonly int NormalStateHash = Animator.StringToHash("NormalState");
protected static readonly int DieHash = Animator.StringToHash("Die");
2023-10-17 07:31:10 +00:00
// Const
private static readonly WaitForSeconds BeAttackedWaitTime = new(0.3f);
#endregion
#region abstract
protected abstract IEnumerator NormalAttackCoroutine();
#endregion
#region Unity built-in methods
protected override void Awake()
{
base.Awake();
Rb = GetComponent<Rigidbody>();
2023-10-17 07:31:10 +00:00
MyCollider = GetComponent<Collider>();
Agent = GetComponent<NavMeshAgent>();
bt = GetComponent<BehaviorTree>();
2023-10-17 07:31:10 +00:00
unitRoot = transform.Find("UnitRoot");
if (unitRoot == null)
2023-10-17 07:31:10 +00:00
{
print("UnitRoot를 찾을 수 없습니다.");
}
else
{
myAnimator = unitRoot.GetComponent<Animator>();
if (myAnimator == null)
{
print("myAnimator를 찾을 수 없습니다.");
}
}
worldSpaceCanvas = GameObject.Find("WorldSpaceCanvas")?.GetComponent<Canvas>();
if (worldSpaceCanvas == null)
{
print("WorldSpaceCanvas 찾을 수 없습니다.");
}
else
{
if (useHpSlider)
{
hpSlider = Instantiate(hpSliderPrefab, worldSpaceCanvas.transform).GetComponent<Slider>();
hpSlider.gameObject.name = gameObject.name + " HpSlider";
hpSlider.transform.rotation = unitRoot.transform.rotation;
}
}
2023-10-17 07:31:10 +00:00
}
protected override void Start()
{
base.Start();
TargetLayer = LayerMask.GetMask("Enemy");
waitAtkCooldown = new WaitForSeconds(AtkCooldown);
Agent.updateRotation = false;
SetAgentSpeed(MoveSpd);
hpSlider.maxValue = MaxHp;
2023-10-17 07:31:10 +00:00
SetCurrentHp(MaxHp);
}
protected override void Update()
{
switch (useHpSlider)
{
case true when CurrentHp > 0 && CurrentHp < MaxHp:
{
if (!hpSlider.gameObject.activeSelf)
{
hpSlider.gameObject.SetActive(true);
}
var localOffset = unitRoot.TransformPoint(hpSliderOffset);
hpSlider.transform.position = localOffset;
break;
}
case true when CurrentHp <= 0 || CurrentHp >= MaxHp:
{
if (hpSlider.gameObject.activeSelf)
{
hpSlider.gameObject.SetActive(false);
}
break;
}
}
if (CurrentHp <= 0) return;
2023-10-17 07:31:10 +00:00
if (GameManager.Inst.CurrentInIslandPlayer.GameObject == gameObject)
2023-10-17 07:31:10 +00:00
{
// 움직이는 경우
if (movementInput.x != 0 || movementInput.y != 0)
2023-10-17 07:31:10 +00:00
{
// Rigidbody 사용
if (!UseRigidbody)
{
UseRigidbodyMovement();
}
if (!beAttacked)
{
myAnimator.SetFloat(RunStateHash, 0.5f);
}
2023-10-17 07:31:10 +00:00
}
// 멈춰있는 경우
else
2023-10-17 07:31:10 +00:00
{
// NavMeshAgent 사용
if (UseRigidbody)
{
UseAgentMovement();
}
if (Agent.velocity.x != 0 || Agent.velocity.z != 0)
{
myAnimator.SetFloat(RunStateHash, 0.5f);
}
else if (!beAttacked)
{
myAnimator.SetFloat(RunStateHash, 0f);
}
2023-10-17 07:31:10 +00:00
}
}
else
2023-10-17 07:31:10 +00:00
{
if (GameManager.Inst.CurrentInIslandPlayer.GameObject && GameManager.Inst.CurrentInIslandPlayer.UseRigidbody && !Target)
2023-10-17 07:31:10 +00:00
{
if (!UseRigidbody)
{
UseRigidbodyMovement();
}
2023-10-17 07:31:10 +00:00
if (!beAttacked)
{
myAnimator.SetFloat(RunStateHash, 0.5f);
}
2023-10-17 07:31:10 +00:00
}
else if (GameManager.Inst.CurrentInIslandPlayer.GameObject && !GameManager.Inst.CurrentInIslandPlayer.UseRigidbody)
2023-10-17 07:31:10 +00:00
{
if (UseRigidbody)
{
UseAgentMovement();
}
if (Agent.velocity.x != 0 || Agent.velocity.z != 0)
{
myAnimator.SetFloat(RunStateHash, 0.5f);
}
else if (!beAttacked)
{
myAnimator.SetFloat(RunStateHash, 0f);
}
2023-10-17 07:31:10 +00:00
}
}
var localScale = transform.localScale;
if (UseRigidbody)
{
localScale.x = Rb.velocity.x switch
2023-10-17 07:31:10 +00:00
{
> 0 => Mathf.Abs(localScale.x),
< 0 => -Mathf.Abs(localScale.x),
_ => localScale.x
};
}
else
{
if (Agent.velocity.x != 0)
{
localScale.x = Agent.velocity.x switch
{
> 0 => Mathf.Abs(localScale.x),
< 0 => -Mathf.Abs(localScale.x),
_ => localScale.x
};
}
else
{
if (Target)
{
var targetToDistanceX = Target.bounds.center.x - MyCollider.bounds.center.x;
localScale.x = targetToDistanceX switch
{
> 0 => Mathf.Abs(localScale.x),
< 0 => -Mathf.Abs(localScale.x),
_ => localScale.x
};
}
}
}
transform.localScale = localScale;
}
protected override void FixedUpdate()
{
if (CurrentHp <= 0) return;
if (UseRigidbody)
{
// var movement = GameManager.Inst.InIslandPlayer.Rb.velocity * (MoveSpd / GameManager.Inst.InIslandPlayer.MoveSpd);
// rb.velocity = new Vector3(movement.x, 0, movement.z);
if (GameManager.Inst.CurrentInIslandPlayer.GameObject == gameObject)
{
var localMovement = new Vector3(movementInput.x, 0, movementInput.y);
var worldDirection = transform.TransformDirection(localMovement);
var movement = worldDirection * MoveSpd;
Rb.velocity = new Vector3(movement.x, 0, movement.z);
}
else
{
var predictedPos = GameManager.Inst.CurrentInIslandPlayer.Rb.position + GameManager.Inst.CurrentInIslandPlayer.Rb.velocity;
var moveDir = (predictedPos - transform.position).normalized;
Rb.velocity = new Vector3(moveDir.x, 0, moveDir.z) * MoveSpd;
}
}
}
2023-10-17 07:31:10 +00:00
#endregion
#region Interfaces
//IDamageable
public void TakeDamage(float attackerPower, Vector3? attackPos = null)
{
2023-10-17 07:31:10 +00:00
IsCombated = true;
if (!Target)
{
BeAttackedInIdle = true;
bt.SendEvent("BeAttackedInIdle", attackPos);
}
2023-10-17 07:31:10 +00:00
var changeHp = Mathf.Max(CurrentHp - attackerPower, 0);
SetCurrentHp(changeHp);
// 죽었는지 체크
if (changeHp == 0f)
{
Die();
2023-10-17 07:31:10 +00:00
return;
}
StartCoroutine(nameof(BeAttacked));
}
public void Die()
{
myAnimator.SetTrigger(DieHash);
MyCollider.enabled = false;
if (Agent.enabled)
{
Agent.isStopped = true;
}
else
{
Rb.isKinematic = true;
}
Agent.enabled = false;
if (GameManager.Inst.CurrentInIslandPlayer == (IInIslandPlayer)this)
{
foreach (var crewmate in GameManager.Inst.CurrentCrewmateList)
{
if (crewmate == null || !crewmate.gameObject.activeSelf || crewmate.CurrentHp <= 0) continue;
GameManager.Inst.SetCurrentInIslandPlayer(crewmate);
return;
}
// 게임 종료
var overlayCanvas = GameObject.Find("OverlayCanvas");
overlayCanvas.transform.Find("RestartPopUp").gameObject.SetActive(true);
return;
}
Destroy(hpSlider.gameObject, 2f);
Destroy(gameObject, 2f);
}
2023-10-17 07:31:10 +00:00
// IAnimatorBridge
public virtual void AttackTiming()
{
if (!Target) return;
var myCenterPos = MyCollider.bounds.center;
var targetDir = (Target.bounds.center - myCenterPos).normalized;
if (!Physics.Raycast(MyCollider.bounds.center, targetDir, out var hit, AtkRange, TargetLayer)) return;
var iDamageable = hit.transform.GetComponent<IDamageable>();
iDamageable.TakeDamage(Atk);
}
public void SetIsAttacking(int boolValue) => isAttacking = boolValue == 1;
// IAiView
[field: Title("IAiView")]
[field: SerializeField] public float ViewRadius { get; set; } = 15f;
[field: SerializeField] public Collider[] Targets { get; set; } = new Collider[MAX_COLLIDERS];
[field: SerializeField] public Collider Target { get; set; }
[field: SerializeField] public LayerMask TargetLayer { get; set; }
private const int MAX_COLLIDERS = 30;
public void FindNearestTargetInRange(Vector3 centerPos, bool targetIsTrigger = true)
2023-10-17 07:31:10 +00:00
{
Array.Clear(Targets, 0, MAX_COLLIDERS);
var numResults = Physics.OverlapSphereNonAlloc(centerPos, ViewRadius, Targets, TargetLayer,
2023-10-17 07:31:10 +00:00
targetIsTrigger ? QueryTriggerInteraction.Collide : QueryTriggerInteraction.Ignore);
if (numResults <= 0)
{
SetTarget(null);
return;
}
var nearestDistance = ViewRadius * ViewRadius;
Collider nearestTargetCollider = null;
for (var i = 0; i < numResults; i++)
{
var distanceSqrToTarget = (centerPos - Targets[i].bounds.center).sqrMagnitude;
2023-10-17 07:31:10 +00:00
if (distanceSqrToTarget >= nearestDistance) continue;
nearestDistance = distanceSqrToTarget;
nearestTargetCollider = Targets[i];
}
SetTarget(nearestTargetCollider);
}
public void SetTarget(Collider value)
{
Target = value;
if (value != null)
{
IsCombated = true;
BeAttackedInIdle = false;
}
}
public bool IsTargetWithinRange(Vector3 centerPos, float range)
2023-10-17 07:31:10 +00:00
{
var inRange = Vector3.Distance(centerPos, Target.bounds.center) <= AtkRange;
return inRange;
2023-10-17 07:31:10 +00:00
}
public bool GoOutOfBounds()
{
var defensePosInRange = Vector3.Distance(transform.position, DefensePos) <= DefenseRange;
return !defensePosInRange;
}
public void MoveTarget(Vector3 targetPos, float speed, float stopDistance = float.MaxValue)
2023-10-17 07:31:10 +00:00
{
if (Vector3.Distance(Agent.destination, targetPos) < 0.1f) return;
SetAgentSpeed(speed);
2023-10-17 07:31:10 +00:00
Agent.stoppingDistance = stopDistance;
Agent.isStopped = false;
Agent.SetDestination(targetPos);
}
// INormalAttack
public void NormalAttack()
{
StartCoroutine(nameof(NormalAttackCoroutine));
}
public void StopNormalAttackCoroutine() => StopCoroutine(nameof(NormalAttackCoroutine));
public bool GetUsedNormalAttackCoroutine() => usedNormalAttackCoroutine;
#endregion
#region Player input system
public void OnMove(InputValue value)
{
if (CurrentHp <= 0) return;
movementInput = value.Get<Vector2>();
}
2023-10-17 07:31:10 +00:00
#endregion
#region Custom methods
private void UseRigidbodyMovement()
{
UseRigidbody = true;
Rb.isKinematic = false;
2023-10-17 07:31:10 +00:00
Agent.enabled = false;
}
private void UseAgentMovement()
{
UseRigidbody = false;
Rb.isKinematic = true;
2023-10-17 07:31:10 +00:00
Agent.enabled = true;
if (Target) return;
MoveTarget(GameManager.Inst.CurrentInIslandPlayer.Transform.position, MoveSpd, GlobalValue.MAXIMUM_STOP_DISTANCE);
2023-10-17 07:31:10 +00:00
}
private IEnumerator BeAttacked()
{
beAttacked = true;
myAnimator.SetFloat(RunStateHash, 1f);
yield return BeAttackedWaitTime;
beAttacked = false;
}
private void SetCurrentHp(float value)
{
CurrentHp = value;
if (useHpSlider)
{
hpSlider.value = value;
}
}
private void SetAgentSpeed(float value) => Agent.speed = value;
2023-10-17 07:31:10 +00:00
#endregion
}
}