using DG.Tweening; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class CharacterAI : MonoBehaviour { public NavMeshAgent agent; private void Start() { agent.updateRotation = false; character.OnDie += OnDie; character.OnHit += OnHit; character.OnResurrection += OnResurrection; } private void OnEnable() { OnInitialized();//초기화 시작 timeAtk_Last = Time.realtimeSinceStartup; InvokeRepeating("BehaviorChase", Random.Range(0, chaseTime), chaseTime); } private void OnDisable() { CancelInvoke(); agent.enabled = false; } public bool LOCK = false; /// /// AI 의 행동을 제한 하거나 해제합니다. /// /// void AgentEnabled(bool LOCK) { this.LOCK = LOCK; MoveStop(LOCK);//중지 요청 } [HideInInspector] public bool isMove = false; /// /// AI 의 이동 상태를 적용합니다. /// /// 대기 모션으로 전환 /// 강제 재생 void MoveStop(bool IDLE = false, bool FORCE = false) { isMove = false; if (IDLE) motion.Play(Motion.Type.Idle, FORCE); if (agent.enabled) { agent.velocity = Vector3.zero; agent.isStopped = true; } } [Header("초기화")] public Character character; public HitCheck hitCheck;//플레이어 충돌 시 데미지를 주기위한 오브젝트 public Radar radarChase; public Radar radarAttack; int layerEnemy = 0; CharacterData data; /// /// AI 의 초기화를 진해합니다. /// private void OnInitialized() { if (data == null) { data = DataManager.Instance.dataMonster.DataGet(character.NAME); rateHitMotion = data.hitMotionSkip; knockBack = data.knockBack; chaseTime = data.behaviorTime; RATE_CHASE = data.behaviorChase; RATE_ESCAPE = data.behaviorEscape; RATE_IDLE = data.behaviorIdle; } layerEnemy = LayerMask.NameToLayer($"Hit{(character.iff == IFF.Our ? IFF.Enemy : IFF.Our)}"); radarAttack.gameObject.layer = radarChase.gameObject.layer = layerEnemy; hitCheck.Initialized(character, layerEnemy, int.MaxValue, null); //hitCheck.Activetion(false, 0); hitCheck.Activetion(true, int.MaxValue); if (weapon != null) { weapon.Initialized(character, layerEnemy, int.MaxValue, null); weapon.Activetion(false, 0); } attackTarget = chaseTarget = null; radarChase.gameObject.SetActive(true); radarAttack.gameObject.SetActive(true); } /// /// AI 가 생성 되었을때 리스폰 위치로 이동시키기 위해 사용됩니다. /// NaviMeshAgent 사용시 Transform.position 으로는 이동시키면 캐릭터가 이상동작을함 /// /// 이동 좌표 public void OnWarp(Vector3 pos) { agent.Warp(pos); } [Header("모션")] public Motion motion; /// /// 공격 방향 또는 이동 방향 을 바라봅니다. /// void Look() { if (dirFire.x != 0 || dirMove.x != 0) { float x = dirFire.x != 0 ? -dirFire.x : -dirMove.x; motion.LookSet(x < 0 ? -1 : 1); } } [Header("히트모션을 무시할 확률")] public int rateHitMotion = 300; /// /// 데미지를 입었으며 히트모션을 실행합니다. /// /// 공격자 /// 공격 방향 /// 넉백 추가 비율 public void OnHit(Character attacker, Vector3 dir, float knockBackAddRate, bool MOTION_FORCE, System.Action OnEnd) { if (attacker != null && attacker.iff != character.iff) chaseTarget = attacker.transform; if (chaseTarget != null) motion.LookSet((transform.position - chaseTarget.position).normalized.x < 0 ? -1 : 1); agent.velocity = Vector3.zero; KnockBack(dir, knockBackAddRate); if (!MOTION_FORCE && (motion.Check_ATK() || motion.Check_Appear() || motion.isSkill || Random.Range(0, 1000) < rateHitMotion))//모션을 실행하지 않는 경우 { OnEnd(); } else { AgentEnabled(true);//히트 모션 시작, 행동 정지 motion.Play(Motion.Type.Hit, false, () => { agent.velocity = Vector3.zero; AgentEnabled(false);//히트 모션 종료, 행동 시작 if (attacker != null) { if (!character.STUN && attacker.iff == IFF.Our) TargetSet(attacker.transform.position, ref dirMove, true);//히트 모션 종료 } OnEnd(); }); } } public GameObject prefabEffDeath; public SoundPlayer spDie; /// /// HP 가 0 이 되었으며 죽는 모션을 실행합니다. /// /// 공격자 /// 공격 방향 public void OnDie(Character attacker, Vector3 dir) { CancelInvoke(); hitCheck.gameObject.SetActive(false); AgentEnabled(true);//죽었을 경우, 행동 정지 motion.Play(Motion.Type.Die, true, () => { if (prefabEffDeath != null) character.map.ObjectCreate(prefabEffDeath, null, Vector3.one, transform.position); if (spDie != null) SoundManager.Instance.EffectPlay(spDie.gameObject); character.Hide_Start(); }); } public void OnResurrection(Character character) { InvokeRepeating("BehaviorChase", Random.Range(0, chaseTime), chaseTime); } [Space(5)] public float knockBack = 0.5f;//기본 넉백값 /// /// 캐릭터를 넉백시킵니다. /// /// 넉백 방향 /// 추가 넉백 비율 void KnockBack(Vector3 dir, float knockBackAddRate) { dir.y = 0; agent.velocity = dir * (knockBack * knockBackAddRate); } [Space(5)] public bool LOCK_UPDOWN = false; [Header("이동 패턴")] public Transform chaseTarget; Vector3 dirMove; [Range(0.1f, 10f)] public float chaseTime = 1f; public float radiusBehavior = 10f; [Space(5)] public int RATE_CHASE = 500; public int RATE_ESCAPE = 500; public int RATE_IDLE = 500; int RATE_TOTAL; Vector3 posMove = Vector3.zero; /// /// AI 가 누군가를 추적할지 설정합니다. /// void BehaviorChase() { if (!gameObject.activeInHierarchy) { CancelInvoke(); return; } if (character.STUN || LOCK) return; if (chaseTarget != null && !chaseTarget.gameObject.activeInHierarchy)//추적대상 상실 chaseTarget = null; if (attackTarget != null && !attackTarget.gameObject.activeInHierarchy)//공격대상 상실 { attackTarget = null; targetMelee = targetRange = null; } if (BehaviorAttack()) ;//공격 대상이 주위에 있고 공격함 else { if(chaseTarget == null && attackTarget == null)//대상이 없을때 { RATE_TOTAL = RATE_ESCAPE + RATE_IDLE + 1; if (Random.Range(0, RATE_TOTAL) < RATE_ESCAPE) { TargetSet(Vector3.zero, ref dirMove, false);//배회 return; } } else { if (attackTarget != null) { RATE_TOTAL = RATE_ESCAPE + RATE_IDLE + 1; if (Random.Range(0, RATE_TOTAL) < RATE_ESCAPE) { TargetSet(Vector3.zero, ref dirMove, false);//탈주 return; } } else { RATE_TOTAL = RATE_CHASE + RATE_ESCAPE + RATE_IDLE + 1; int RATE = Random.Range(0, RATE_TOTAL); if (RATE < RATE_CHASE) { Vector3 posMove = chaseTarget.position; if (LOCK_UPDOWN) posMove.x = 0; TargetSet(posMove, ref dirMove, true);//추적 return; } else if (RATE < RATE_CHASE + RATE_ESCAPE) { TargetSet(Vector3.zero, ref dirMove, false);//탈주 return; } } } MoveStop(true);//대기 } } private void FixedUpdate() { //if (isMove && (agent.velocity.sqrMagnitude < 0.04f && agent.remainingDistance < 0.5f)) if (isMove && (agent.enabled && agent.remainingDistance < 0.5f)) MoveStop(true);//목표 지점 도착 } public void OnMoveSpeedChanged(float spd) { agent.speed = character.CalculateMoveSpeed(); } List skillsTriggerRun; EventManager.ObjectCreate objectCreate = new EventManager.ObjectCreate(); /// /// AI 가 이동을 시작합니다. /// /// 타겟의 좌표 /// 이동 방향 /// true = 추적, false = 탈주, 대기 public void TargetSet(Vector3 position, ref Vector3 dir, bool CHASE = false) { if (isMove && !CHASE) return; if (!agent.enabled) agent.enabled = true; position = CHASE ? position : Util.PointCreate(transform.position, radiusBehavior);//이동좌표 설정 agent.SetDestination(position); agent.isStopped = false;//이동 시작 dir = (position - transform.position); Look(); if (spWalk != null) { motion.Play(Motion.Type.Run, false, null, data => { SoundManager.Instance.EffectPlay(spWalk.gameObject);//보스, 중보스 의 경우 이벤트가 걸려있을수 있음 if (SHAKE) character.map.tiltCamera.Shaking(0.1f, 0.1f); }); } else if((skillsTriggerRun = skill.SkillList_Trigger("Run")) != null) { motion.Play(Motion.Type.Run, false, null, data => { for (int i = 0; i < skillsTriggerRun.Count; i++) { if (skillsTriggerRun[i].Activate("Run")) { switch (skillsTriggerRun[i].work) { case "CreatePoison": { float damage = character.HP * skillsTriggerRun[i].value; character.Damage_Apply((int)damage, null, Vector3.zero, 0, true, null); objectCreate.prefab = PrefabManager.Instance.PrefabGet($"Effect/StatusEffect Poison Activate"); objectCreate.target = transform; objectCreate.scale = Vector3.one; objectCreate.pos = Vector3.zero; EventManager.Instance.Invoke("PrefabCreateLocal", objectCreate); objectCreate.create.transform.SetParent(null); objectCreate.create.transform.eulerAngles = Vector3.zero; Vector3 pos = objectCreate.create.transform.position; pos.y = 1; objectCreate.create.transform.position = pos; HitCheck hit = objectCreate.create.GetComponent(); hit.damageType = DamageType.RateHP; hit.damageValue = skillsTriggerRun[i].value; hit.Initialized(character, LayerMask.NameToLayer($"Hit{(character.iff == IFF.Our ? IFF.Enemy : IFF.Our)}"), 9999, null); } break; } } } }); } else { motion.Play(Motion.Type.Run); } agent.speed = character.CalculateMoveSpeed(); isMove = true; } /// /// 추적 레이더(Rader.cs) 에 적 캐릭터가 탐지 되어 이를 실행합니다. /// /// 추적대상 /// true = 감지, false = 소실 public void TargetChase(Transform target, bool ENTER) { if (ENTER) { chaseTarget = target; if (!character.STUN || !LOCK) { MoveStop(false); BehaviorChase(); } } else chaseTarget = chaseTarget == target ? null : chaseTarget; } [Header("공격 패턴")] public Transform attackTarget; public HitCheck weapon; public Fire fire; float timeAtk_Last = 0; Vector3 dirFire; Transform targetMelee; /// /// 레이더-근거리공격(Rader.cs) /// 공격 범위 내에 캐릭터가 감지되어 이를 실행합니다. /// /// 공격대상 /// true = 감지, false = 소실 public void Target_Melee(Transform target, bool ENTER) { if (ENTER) { targetMelee = attackTarget = target; if (!character.STUN || !LOCK) BehaviorAttack();//근거리 } else { targetMelee = attackTarget = attackTarget == target ? null : attackTarget; } } Transform targetRange; /// /// 레이더-원거리공격(Rader.cs) /// 공격 범위 내에 캐릭터가 감지되어 이를 실행합니다. /// /// 공격대상 /// true = 감지, false = 소실 public void Target_Range(Transform target, bool ENTER) { if (ENTER) { targetRange = attackTarget = target; if (!character.STUN || !LOCK) BehaviorAttack();//원거리 } else { targetRange = attackTarget = attackTarget == target ? null : attackTarget; } } /// /// AI 가 공격을 실행합니다. /// /// bool BehaviorAttack() { if (attackTarget == null) return false; bool SKILL = skill.Active("Attack", attackTarget, active => { MoveStop(true); AgentEnabled(false);//스킬 발동이 종료됐을 경우, 행동 시작 motion.isSkill = false; }); if (SKILL) { AgentEnabled(true);//스킬 발동이 시작됐을 경우, 행동 정지 motion.isSkill = true; return SKILL; } if (!SKILL && attackTarget != null) { if (character.CalculateAttackDelay() < Time.realtimeSinceStartup - timeAtk_Last) { if (weapon != null && targetMelee != null) { dirMove = (attackTarget.position - transform.position).normalized; Look();//근거리공격 대상을 바라봄 AgentEnabled(true);//근거리, 공격 모션 시작, 행동 종료 //motion.typeBefore = Motion.Type.Attack; motion.Play(Motion.Type.Attack, false, () => { timeAtk_Last = Time.realtimeSinceStartup; weapon.Activetion(false, 0); AgentEnabled(false);//근거리, 공격 모션 종료, 행동 시작 MoveStop(true); }, Attack_Event); return true; } if (fire != null && targetRange != null) { dirFire = attackTarget == null ? dirMove : (attackTarget.position - transform.position).normalized; if (LOCK_UPDOWN) { dirFire.x = dirFire.x < 0 ? -1 : 1; dirFire.z = 0; dirFire.y = 0; } Look();//원거리공격 대상을 바라봄 AgentEnabled(true);//원거리, 공격 모션 시작, 행동 정지 Vector3 dir = dirFire; //motion.typeBefore = Motion.Type.Attack; motion.Play(Motion.Type.Attack, false, () => { timeAtk_Last = Time.realtimeSinceStartup; dirFire = Vector3.zero; AgentEnabled(false);//원거리, 공격 모션 종료, 행동 시작 MoveStop(true); }, data => { fire.OnFire(dir); }); return true; } } } return false; } /// /// 공격 모션 종료, 무기 콜리더 비활성화 /// void Attack_End() { timeAtk_Last = Time.realtimeSinceStartup; weapon.Activetion(false, 0); AgentEnabled(false);//근거리, 공격 모션 종료, 행동 시작 } [Space(10)] public bool SHAKE = false; public SoundPlayer spSmash; public SoundPlayer spWalk; TiltCamera tiltCamera; /// /// 공격 모션의 이벤트가 발생하여 이를 처리합니다. /// /// void Attack_Event(Spine.Event data) { switch (data.Data.Name) { case "On": { weapon.Activetion(true, 1);//무기의 콜리더 활성화 } break; case "Off": { if (SHAKE) { if (spSmash != null) SoundManager.Instance.EffectPlay(spSmash.gameObject); character.map.tiltCamera.Shaking(0.25f, 0.25f); } weapon.Activetion(false, 0);//무기의 콜리더 비활성화 } break; } } [Header("스킬 패턴")] public Skill skill; /// /// 레이더-방어(Rader.cs) /// 공격 범위 내에 적의 투사체 혹은 무기가 감지되어 이를 처리합니다. /// /// /// public void OnDefense(Transform target, bool ENTER) { if (character.STUN || LOCK || !ENTER) return; //스킬 Trigger:Defense 가 있는지 확인 bool SKILL = skill.Active("Defense", null, active => { MoveStop(true); AgentEnabled(false);//스킬:Defense 상태 발동이 종료됐을 경우, 행동 시작 motion.isSkill = false; }); if (SKILL) { Vector3 dir = (transform.position - target.position).normalized; motion.LookSet(dir.x < 0 ? -1 : 1); AgentEnabled(true);//스킬:Defense 상태 발동이 시작됐을 경우, 행동 정지 motion.isSkill = true; } } }