You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
614 lines
20 KiB
614 lines
20 KiB
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; |
|
/// <summary> |
|
/// AI 의 행동을 제한 하거나 해제합니다. |
|
/// </summary> |
|
/// <param name="LOCK"></param> |
|
void AgentEnabled(bool LOCK) |
|
{ |
|
this.LOCK = LOCK; |
|
MoveStop(LOCK);//중지 요청 |
|
} |
|
[HideInInspector] |
|
public bool isMove = false; |
|
/// <summary> |
|
/// AI 의 이동 상태를 적용합니다. |
|
/// </summary> |
|
/// <param name="IDLE">대기 모션으로 전환</param> |
|
/// <param name="FORCE">강제 재생</param> |
|
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; |
|
/// <summary> |
|
/// AI 의 초기화를 진해합니다. |
|
/// </summary> |
|
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); |
|
} |
|
/// <summary> |
|
/// AI 가 생성 되었을때 리스폰 위치로 이동시키기 위해 사용됩니다. |
|
/// NaviMeshAgent 사용시 Transform.position 으로는 이동시키면 캐릭터가 이상동작을함 |
|
/// </summary> |
|
/// <param name="pos">이동 좌표</param> |
|
public void OnWarp(Vector3 pos) |
|
{ |
|
agent.Warp(pos); |
|
} |
|
|
|
[Header("모션")] |
|
public Motion motion; |
|
/// <summary> |
|
/// 공격 방향 또는 이동 방향 을 바라봅니다. |
|
/// </summary> |
|
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; |
|
/// <summary> |
|
/// 데미지를 입었으며 히트모션을 실행합니다. |
|
/// </summary> |
|
/// <param name="attacker">공격자</param> |
|
/// <param name="dir">공격 방향</param> |
|
/// <param name="knockBackAddRate">넉백 추가 비율</param> |
|
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; |
|
/// <summary> |
|
/// HP 가 0 이 되었으며 죽는 모션을 실행합니다. |
|
/// </summary> |
|
/// <param name="attacker">공격자</param> |
|
/// <param name="dir">공격 방향</param> |
|
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;//기본 넉백값 |
|
/// <summary> |
|
/// 캐릭터를 넉백시킵니다. |
|
/// </summary> |
|
/// <param name="dir">넉백 방향</param> |
|
/// <param name="knockBackAddRate">추가 넉백 비율</param> |
|
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; |
|
/// <summary> |
|
/// AI 가 누군가를 추적할지 설정합니다. |
|
/// </summary> |
|
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<SkillData> skillsTriggerRun; |
|
EventManager.ObjectCreate objectCreate = new EventManager.ObjectCreate(); |
|
/// <summary> |
|
/// AI 가 이동을 시작합니다. |
|
/// </summary> |
|
/// <param name="position">타겟의 좌표</param> |
|
/// <param name="dir">이동 방향</param> |
|
/// <param name="CHASE">true = 추적, false = 탈주, 대기</param> |
|
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<HitCheck>(); |
|
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; |
|
} |
|
/// <summary> |
|
/// 추적 레이더(Rader.cs) 에 적 캐릭터가 탐지 되어 이를 실행합니다. |
|
/// </summary> |
|
/// <param name="target">추적대상</param> |
|
/// <param name="ENTER">true = 감지, false = 소실</param> |
|
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; |
|
/// <summary> |
|
/// 레이더-근거리공격(Rader.cs) |
|
/// 공격 범위 내에 캐릭터가 감지되어 이를 실행합니다. |
|
/// </summary> |
|
/// <param name="target">공격대상</param> |
|
/// <param name="ENTER">true = 감지, false = 소실</param> |
|
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; |
|
/// <summary> |
|
/// 레이더-원거리공격(Rader.cs) |
|
/// 공격 범위 내에 캐릭터가 감지되어 이를 실행합니다. |
|
/// </summary> |
|
/// <param name="target">공격대상</param> |
|
/// <param name="ENTER">true = 감지, false = 소실</param> |
|
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; |
|
} |
|
} |
|
/// <summary> |
|
/// AI 가 공격을 실행합니다. |
|
/// </summary> |
|
/// <returns></returns> |
|
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; |
|
} |
|
/// <summary> |
|
/// 공격 모션 종료, 무기 콜리더 비활성화 |
|
/// </summary> |
|
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; |
|
/// <summary> |
|
/// 공격 모션의 이벤트가 발생하여 이를 처리합니다. |
|
/// </summary> |
|
/// <param name="data"></param> |
|
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; |
|
/// <summary> |
|
/// 레이더-방어(Rader.cs) |
|
/// 공격 범위 내에 적의 투사체 혹은 무기가 감지되어 이를 처리합니다. |
|
/// </summary> |
|
/// <param name="target"></param> |
|
/// <param name="ENTER"></param> |
|
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; |
|
} |
|
} |
|
}
|
|
|