Super Knight : Enter the Dungeon
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.

615 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;
}
}
}