using System; using System.Collections; using BehaviorDesigner.Runtime; using Sirenix.OdinInspector; using UnityEngine; 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 { #region Properties and variables // DrawGizmos [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 [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; [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; // Data [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; protected bool usedNormalAttackCoroutine; protected WaitForSeconds waitAtkCooldown; // 컴포넌트 public GameObject GameObject => gameObject; public Transform Transform => transform; public Rigidbody Rb { get; set; } public Collider MyCollider { get; set; } public NavMeshAgent Agent { get; set; } private BehaviorTree bt; private Transform unitRoot; protected Animator myAnimator; private Canvas worldSpaceCanvas; // 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"); // 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(); MyCollider = GetComponent(); Agent = GetComponent(); bt = GetComponent(); unitRoot = transform.Find("UnitRoot"); if (unitRoot == null) { print("UnitRoot를 찾을 수 없습니다."); } else { myAnimator = unitRoot.GetComponent(); if (myAnimator == null) { print("myAnimator를 찾을 수 없습니다."); } } worldSpaceCanvas = GameObject.Find("WorldSpaceCanvas")?.GetComponent(); if (worldSpaceCanvas == null) { print("WorldSpaceCanvas 찾을 수 없습니다."); } else { if (useHpSlider) { hpSlider = Instantiate(hpSliderPrefab, worldSpaceCanvas.transform).GetComponent(); hpSlider.gameObject.name = gameObject.name + " HpSlider"; hpSlider.transform.rotation = unitRoot.transform.rotation; } } } protected override void Start() { base.Start(); TargetLayer = LayerMask.GetMask("Enemy"); waitAtkCooldown = new WaitForSeconds(AtkCooldown); Agent.updateRotation = false; SetAgentSpeed(MoveSpd); hpSlider.maxValue = MaxHp; 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; if (GameManager.Inst.CurrentInIslandPlayer.GameObject == gameObject) { // 움직이는 경우 if (movementInput.x != 0 || movementInput.y != 0) { // Rigidbody 사용 if (!UseRigidbody) { UseRigidbodyMovement(); } if (!beAttacked) { myAnimator.SetFloat(RunStateHash, 0.5f); } } // 멈춰있는 경우 else { // 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); } } } else { if (GameManager.Inst.CurrentInIslandPlayer.GameObject && GameManager.Inst.CurrentInIslandPlayer.UseRigidbody && !Target) { if (!UseRigidbody) { UseRigidbodyMovement(); } if (!beAttacked) { myAnimator.SetFloat(RunStateHash, 0.5f); } } else if (GameManager.Inst.CurrentInIslandPlayer.GameObject && !GameManager.Inst.CurrentInIslandPlayer.UseRigidbody) { if (UseRigidbody) { UseAgentMovement(); } if (Agent.velocity.x != 0 || Agent.velocity.z != 0) { myAnimator.SetFloat(RunStateHash, 0.5f); } else if (!beAttacked) { myAnimator.SetFloat(RunStateHash, 0f); } } } var localScale = transform.localScale; if (UseRigidbody) { localScale.x = Rb.velocity.x switch { > 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; } } } #endregion #region Interfaces //IDamageable public void TakeDamage(float attackerPower, Vector3? attackPos = null) { IsCombated = true; if (!Target) { BeAttackedInIdle = true; bt.SendEvent("BeAttackedInIdle", attackPos); } var changeHp = Mathf.Max(CurrentHp - attackerPower, 0); SetCurrentHp(changeHp); // 죽었는지 체크 if (changeHp == 0f) { Die(); 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); } // 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.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) { Array.Clear(Targets, 0, MAX_COLLIDERS); var numResults = Physics.OverlapSphereNonAlloc(centerPos, ViewRadius, Targets, TargetLayer, 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; 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) { var inRange = Vector3.Distance(centerPos, Target.bounds.center) <= AtkRange; return inRange; } public bool GoOutOfBounds() { var defensePosInRange = Vector3.Distance(transform.position, DefensePos) <= DefenseRange; return !defensePosInRange; } public void MoveTarget(Vector3 targetPos, float speed, float stopDistance = float.MaxValue) { if (Vector3.Distance(Agent.destination, targetPos) < 0.1f) return; SetAgentSpeed(speed); 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(); } #endregion #region Custom methods private void UseRigidbodyMovement() { UseRigidbody = true; Rb.isKinematic = false; Agent.enabled = false; } private void UseAgentMovement() { UseRigidbody = false; Rb.isKinematic = true; Agent.enabled = true; if (Target) return; MoveTarget(GameManager.Inst.CurrentInIslandPlayer.Transform.position, MoveSpd, GlobalValue.MAXIMUM_STOP_DISTANCE); } 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; #endregion } }