using DG.Tweening; using DG.Tweening.Core; using DG.Tweening.Plugins.Options; using IVDataFormat; using Spine; using Spine.Unity; using System; using System.Collections; using System.Collections.Generic; using System.Numerics; using UnityEngine; using AnimationState = Spine.AnimationState; using Vector2 = UnityEngine.Vector2; using Vector3 = UnityEngine.Vector3; // 전투용 캐릭터/몬스터에서 상속받는 부모 클래스. public class CreatureBase : Entity, IBattleEntity { protected static readonly Vector3 V3_MonSizeNormal = new Vector3(0.75f, 0.75f, 0.75f); protected static readonly Vector3 V3_MonSizeElite = new Vector3(0.8f, 0.8f, 0.8f); protected static readonly Vector3 V3_MonSizeBoss = new Vector3(1f, 1f, 1f); protected TweenerCore twcSummon = null; protected TweenerCore twcUnsummon = null; protected BattleMgr battleMgr => BattleMgr.Instance; protected int iIndex = -1; protected bool bInitNeed = true; protected bool bLoading = false; protected bool bSummonNeed = false; bool _bFriendly = true; protected bool bFriendly { get => _bFriendly; set { if(_bFriendly != value) { _bFriendly = value; ClearTags(); if(_bFriendly) { AddTags(EntityTag.Character, EntityTag.Player); TargetFilter = Bitset32.GetMask(EntityTag.Enemy); } else { AddTags(EntityTag.Character, EntityTag.Enemy); TargetFilter = Bitset32.GetMask(EntityTag.Player); } } } } public Bitset32.Mask TargetFilter; protected bool bCoward = false; protected int isDamaged = 0; #region Battle State public enum eState { idle, move, attack, damage, die, skill, ceremony, warp_in, warp_out, attack2, attack3, rush, } public enum eCreatureClass { charEnemy = -2, character = -1, normal = 0, elite = 1, boss = 2, bigboss = 3, summon = 10 } /// /// 몬스터 위치별 번호 /// public enum eDgCreatureClass { DgN1, DgN2, DgN3, DgN4, DgN5, DgE1, DgBs, DgAwBs } public bool IsInit { get; private set; } protected ParticleSystem ptcHit; [SerializeField] protected SkeletonAnimation skAnim; Dictionary animEventHandlers = new(); AnimationState _anState; protected AnimationState anState { get => _anState; set { if(_anState != null) _anState.Event -= HandleAnimEvent; KeyValuePair[] handlerPairs = { new KeyValuePair(nameof(AttackDamage), AttackDamage), new KeyValuePair(nameof(CheckAttackState), CheckAttackState), new KeyValuePair(nameof(CheckDamageState), CheckDamageState), new KeyValuePair(nameof(CheckDieState), CheckDieState), new KeyValuePair(nameof(SkillEffect), SkillEffect), new KeyValuePair(nameof(CheckSkillState), CheckSkillState), new KeyValuePair(nameof(CheckCeremonyState), CheckCeremonyState), new KeyValuePair(nameof(CheckWarp_outState), CheckWarp_outState), }; _anState = value; animEventHandlers.Clear(); for(int i = 0; i < handlerPairs.Length; ++i) { var eventData = skAnim.Skeleton.Data.FindEvent(handlerPairs[i].Key); if (eventData != null) animEventHandlers.Add(eventData, handlerPairs[i].Value); } _anState.Event += HandleAnimEvent; } } protected HpBar hpBar; [SerializeField] protected eState animState = eState.idle; [SerializeField] protected eCreatureClass creatureClass = eCreatureClass.normal; protected IBattleEntity target; protected Transform targetTransform => target != null ? (target as Entity).transform : null; protected Vector2 targetPos => target != null ? (target as Entity).transform.position : Vector2.zero; protected float sqrtDistToTarget => target != null ? ((Vector2)transform.position - targetPos).sqrMagnitude : 9999f; protected float useSkillRange = 0f; protected bool bCanMove = true; protected bool bCanAttack = true; protected bool bDead = false; public bool IsDead => bDead; protected bool bSummon = false; protected bool bAttackable = false; public bool IsLookRight => LookDirection.x > 0; [SerializeField] protected float fAttackTick = 0f; protected float fAttackTime = 1.2f; protected float fSearchTick = 0f; protected const float F_SearchTime = 0.1f; protected BoxCollider areaSize; protected Vector3 minArea; protected Vector3 maxArea; [Range(0.1f, 1.0f)] protected float atkSpeed = 1.0f; [Range(1.0f, 4.0f)] protected float motionSpeed = 1.0f; protected Stack stackSkillAvail; #endregion Battle State #region Status protected float attackRange = 7f; protected float recogRange = 10f; // 플레이어 인식범위 protected float enemySlowSpeed = 0.2f; // 적군 농땡이 속도 protected eMonsterType eType; protected bool alertPlayer = false; protected BigInteger maxAtk = 0L; protected float fCrtDam = dConst.RateMaxFloat; protected int iCrtRate = 0; protected float fMov = dConst.RateMaxFloat; protected float fSpd = dConst.RateMaxFloat; protected BigInteger currentAtk = 1; protected BigInteger maxHp = 10; protected BigInteger currentHp = 10; protected float fCrtDamCalc = dConst.RateMaxFloat; protected int iCrtRateCalc = 0; protected float moveSpeed = 3f; protected float fSpdCalc = dConst.RateMaxFloat; #endregion Status #region Debuff public class cDamage { public int sec; public float time; public BigInteger atk; public float crtdam; public int crtrate; public int idnum; } // 둔화. protected float fDecMov = 0f; protected float fDecMovTime = 0f; // 출혈. protected int iDamageSecCnt = 0; protected cDamage[] damageSecs = new cDamage[2] { new cDamage(), new cDamage() }; // 기절. protected bool bStun = false; protected float fStunTime = 0f; // 기절/넉백 쿨타임. protected float fStunPushCool = 0f; #endregion Debuff protected int backAllowed = 1; [SerializeField] float backtime = 0f; protected bool isdash = false; protected virtual void Start() { Init(); if (bInitNeed && !bLoading) { bInitNeed = false; anState = skAnim.AnimationState; } _bFriendly = true; ClearTags(); AddTags(EntityTag.Character, EntityTag.Player); TargetFilter = Bitset32.GetMask((int)EntityTag.Enemy); } protected void Update() { SetAnimTimeScale(); if (battleMgr.BattlePause || bSummon || bDead) return; float ftimedec = Time.deltaTime; #region Debuff // 둔화. if (fDecMovTime > 0f) { fDecMovTime -= ftimedec; if (fDecMovTime <= 0f) { fDecMov = 0f; moveSpeed = fMov * 12f / dConst.RateMaxFloat; } } // 출혈. if (iDamageSecCnt > 0) { for (int i = 0; i < damageSecs.Length; i++) { if (damageSecs[i].time <= 0f) continue; damageSecs[i].time -= ftimedec; if (damageSecs[i].time <= 0f) { GetDamage(damageSecs[i].atk, damageSecs[i].crtdam, damageSecs[i].crtrate, damageSecs[i].idnum); iDamageSecCnt--; continue; } int isec = (int)damageSecs[i].time; if (isec < damageSecs[i].sec) { damageSecs[i].sec = isec; GetDamage(damageSecs[i].atk, damageSecs[i].crtdam, damageSecs[i].crtrate, damageSecs[i].idnum); } } } // 기절. if (bStun) { fStunTime -= ftimedec; if (fStunTime <= 0f) { bStun = false; } } else if (fStunPushCool > 0f) { fStunPushCool -= ftimedec; } #endregion Debuff if (bStun) return; // 유저가 터치하면 이동 if (bFriendly && VirtualPad.isTouchScreen()) { if (animState != eState.move) { ChangeStateForce(eState.move); } } // 아니라면 AI로 동작 else { switch (animState) { case eState.idle: // 공격 대기 시간 체크. if (fAttackTick < fAttackTime) { fAttackTick += Time.deltaTime; if (fAttackTick >= fAttackTime) bAttackable = true; } // 목표 재설정 대기 시간 체크. if (fSearchTick < F_SearchTime) fSearchTick += Time.deltaTime; else SearchTarget(); // 현재 상태가 idle이고 공격 범위 안에 있으며 공격력이 0이 아니면 공격 체크. if (animState == eState.idle && maxAtk > 0 && sqrtDistToTarget <= attackRange * attackRange) { CheckAttack(); } break; case eState.move: // 공격 대기 시간 체크. if (fAttackTick < fAttackTime) { fAttackTick += Time.deltaTime; if (fAttackTick >= fAttackTime) bAttackable = true; } // 목표 재설정 대기 시간 체크. if (fSearchTick < F_SearchTime) fSearchTick += Time.deltaTime; else SearchTarget(); // 현재 상태가 move이고 이동 이후 공격 범위 안에 있으며 공격력이 0이 아니면 공격 체크. if (animState == eState.move) { MoveToTarget(); if(maxAtk > 0 && sqrtDistToTarget <= attackRange * attackRange) { CheckAttack(); } } break; // 스킬 중에 구르는 모션이 필요한게 있음. 아직 파악 불가. case eState.rush: backAllowed--; // 공격 대기 시간 체크. if (fAttackTick < fAttackTime) { fAttackTick += Time.deltaTime; if (fAttackTick >= fAttackTime) bAttackable = true; } //1f 가 시간 backtime += Time.deltaTime; if (backtime > 0.4f) { CheckAttack(); backtime = 0; isdash = false; } break; } } } #region Base & Init // 컴포넌트 세팅. public void SetHpbar(HpBar hpbar) { hpBar = hpbar; } // 피아 지정. public void SetFriendly(bool bfriendly) { bFriendly = bfriendly; } // 크리쳐 종류, 인덱스 지정. public void SetClassIndex(eCreatureClass cls, int index) { creatureClass = cls; iIndex = index; switch (creatureClass) { case eCreatureClass.normal: skAnim.transform.localScale = V3_MonSizeNormal; break; case eCreatureClass.elite: skAnim.transform.localScale = V3_MonSizeElite; break; case eCreatureClass.boss: skAnim.transform.localScale = V3_MonSizeBoss; break; } } // 초기화. public void Init() { if(IsInit) return; IsInit = true; ptcHit = transform.Find("hit").GetComponent(); InitTween(); stackSkillAvail = new Stack(); } // 트윈 초기화. protected virtual void InitTween() { twcSummon = transform.DOScale(Global.V3_1, 0.8f).ChangeStartValue(Global.V3_001).SetEase(Ease.OutQuad).OnComplete(SummonEnd).SetAutoKill(false).Pause(); twcUnsummon = transform.DOScale(Global.V3_1, 2.0f).SetEase(Ease.InQuad).OnComplete(UnsummonEnd).SetAutoKill(false).Pause(); } #endregion Base & Init #region Stat // 스탯 세팅. public void SetStatus(BigInteger atk, BigInteger hp, float fcrtdam, int icrtrate, float fmov, float fspd, float frange, bool bhpcurkeep = false, bool isrunaway = false, int igroup = 1) { maxAtk = atk; currentAtk = maxAtk; maxHp = hp; if (!bhpcurkeep) currentHp = maxHp; fCrtDam = fcrtdam; iCrtRate = icrtrate; fMov = fmov; fSpd = fspd; eType = (eMonsterType)igroup; fCrtDamCalc = fCrtDam; iCrtRateCalc = iCrtRate; fSpdCalc = fSpd; moveSpeed = fMov * 12f / dConst.RateMaxFloat; if (fDecMov > 0f) moveSpeed *= (1f - fDecMov); attackRange = frange / dConst.RateMaxFloat; float foffset = dConst.RateMaxFloat / fSpdCalc; atkSpeed = Mathf.Lerp(0.1f, 0.75f, foffset); // 공속이 높을 수록 0.1로 motionSpeed = Mathf.Lerp(4.0f, 1.5f, foffset); // 공속이 높을 수록 4로 fAttackTime = 0.5f * atkSpeed; bCoward = isrunaway; areaSize = BattleMgr.Instance.GetBgSize(); minArea = areaSize.bounds.min; maxArea = areaSize.bounds.max; } public eCreatureClass GetCreatureClass() { return creatureClass; } public BigInteger GetAtk() { return currentAtk; } public float GetCrtDam() { return fCrtDamCalc; } public int GetCrtRate() { return iCrtRateCalc; } public eMonsterType GetMonType() { return eType; } #endregion Stat #region Transform Control // 타겟쪽으로 이동(1프레임). protected virtual void MoveToTarget() { } // 타겟쪽에게 대시 이동(1프레임). public void FrontToTarget(float fdt) { isdash = true; ChangeState(eState.rush); ChangeLookDirection(targetTransform); Vector3 moveVec = (targetPos - (Vector2)transform.position).normalized * fdt;//5 가 거리 transform.DOMove(transform.position + moveVec, 0.8f).SetEase(Ease.OutQuart).OnComplete(InitPos);//1f 가 시간 } public void InitPos() { MoveToTarget(); } public virtual void MoveToRush(int dir, float fdt, bool bright) { Vector3 dirVec; switch (dir) { case 0: dirVec = Vector3.up; break; case 1: dirVec = Vector3.Lerp(Vector3.up, Vector3.right, 0.33f).normalized; break; case 2: dirVec = Vector3.Lerp(Vector3.up, Vector3.right, 0.66f).normalized; break; case 3: dirVec = Vector3.right; break; case 4: dirVec = Vector3.Lerp(Vector3.right, Vector3.down, 0.33f).normalized; break; case 5: dirVec = Vector3.Lerp(Vector3.right, Vector3.down, 0.66f).normalized; break; case 6: dirVec = Vector3.down; break; case 7: dirVec = Vector3.Lerp(Vector3.down, Vector3.left, 0.33f).normalized; break; case 8: dirVec = Vector3.Lerp(Vector3.down, Vector3.left, 0.66f).normalized; break; case 9: dirVec = Vector3.left; break; case 10: dirVec = Vector3.Lerp(Vector3.left, Vector3.up, 0.33f).normalized; break; case 11: dirVec = Vector3.Lerp(Vector3.left, Vector3.up, 0.66f).normalized; break; default: dirVec = (targetPos - (Vector2)transform.position).normalized; break; } dirVec = bright ? dirVec : new Vector3(dirVec.x * -1, dirVec.y, dirVec.z); Vector3 moveVec = dirVec * fdt;//5 가 거리 transform.DOMove(transform.position + moveVec, 0.2f).SetEase(Ease.OutQuart);//1f 가 시간 } #endregion Transform Control #region Object Control public bool IsBattleAvail() => gameObject.activeSelf && !bDead && !bSummon; // 애니메이션 로드 대기중. public void SetLoading() { bLoading = true; } public virtual void Summon(Vector3 v3pos, bool bfrontright) { transform.position = v3pos; ChangeLookDirection(new Vector2(bfrontright ? 1f : -1f, 0f)); skAnim.skeleton.A = 1.0f; Summon(); } public virtual void Summon() { // 애니메이션 로드 대기중이면 소환 안함. 로드 완료되는 타이밍에 소환. if (bLoading) { bSummonNeed = true; return; } bDead = false; bSummon = true; bAttackable = false; fAttackTick = 0f; target = null; gameObject.SetActive(true); twcSummon.Restart(); // 애니메이션이 다른 애니메이션으로 변경되면 새로 로드해야 함. if (bInitNeed) { bInitNeed = false; anState = skAnim.AnimationState; } ChangeStateForce(eState.idle); } public void SummonEnd() { bSummon = false; ChangeStateForce(eState.idle); fSearchTick = F_SearchTime; } public virtual void Unsummon() { bSummon = true; bAttackable = false; twcUnsummon.Restart(); } public void UnsummonEnd() { ChangeState(eState.idle); gameObject.SetActive(false); } #endregion Object Control #region Skill & Attack // 사용 가능한 스킬 추가. public void AddSkillAvail(int idx) { stackSkillAvail.Push(idx); } // 사용 가능한 스킬 모두 제거. public void ClearSkillAvail() { stackSkillAvail.Clear(); } public virtual void CheckSkill() { if (bStun) return; // 스킬이 사용 가능할 경우. if (stackSkillAvail.Count > 0 && target != null) { //스킬 사용부분 int skillid = stackSkillAvail.Peek(); int skillused = battleMgr.UseSkill(bFriendly, skillid, this, target, fCrtDamCalc, iCrtRateCalc, currentAtk); if (skillused != 0) // 스킬 사용함 or 사용할 수 없는 스킬. stackSkillAvail.Pop(); } } public virtual void CheckAttack() { if (bStun) return; // 공격 가능(공격 속도 참조한 공격 시간 경과됨). if (bAttackable) { ChangeLookDirection(targetTransform); ChangeState(eState.attack); } else { ChangeState(eState.idle); } } #endregion Skill & Attack #region Damage & Heal & Die // 피격. public void GetDamage(BigInteger atk, float crtdamamage, int crtrate, int dnum = -1) { if (BattleMgr.Instance.BattlePause || bSummon || bDead) return; bool isCritical = CheckCritical(crtrate); if(isCritical) atk = GetCriticalDamage(atk, crtdamamage, crtrate); if (currentHp >= maxHp) currentHp = maxHp; if (dnum != -1) BattleMgr.SSetTotalDamage(atk, dnum); currentHp -= atk; if (currentHp <= 0) { hpBar.ShowHpBar(0f); battleMgr.AddPvpDamage(bFriendly, atk, 0f); ChangeState(eState.die); } else { float fractionalPart = (float)(currentHp * 10000 / maxHp) / 10000f; hpBar.ShowHpBar(fractionalPart); battleMgr.AddPvpDamage(bFriendly, atk, fractionalPart); if (eType == eMonsterType.SmallObject) { ChangeState(eState.damage); } else { StartCoroutine(DamagedRed()); } } Damage dmg = new Damage { value = atk, isCritical = isCritical, }; OnDamaged(dmg); } protected virtual void OnDamaged(Damage damage) { } public static bool CheckCritical(int crtRate) { if (crtRate <= 0) return false; int actualRate = crtRate % dConst.RateMax; return UnityEngine.Random.Range(0, dConst.RateMax) < actualRate; } public static BigInteger GetCriticalDamage(BigInteger atk, float crtDamage, int crtRate) { // 치명타 확률 오버한만큼 데미지 추가 ??? int crtLevel = crtRate / dConst.RateMax; crtLevel = crtLevel < 1 ? 1 : crtLevel; //@ fcrtdam 현재는 치명타 데미지 증가 스탯을 사용하지 않음. 추후 기획으로 추가되면 적용. return atk * (1 + crtLevel); } IEnumerator DamagedRed() { Skeleton sk = skAnim.skeleton; sk.SetColor(Color.red); yield return new WaitForSeconds(0.2f); sk.SetColor(Color.white); } // 회복. public void GetHeal(float frate) { if (BattleMgr.Instance.BattlePause || bSummon || bDead) return; BigInteger birate = new BigInteger(frate); BigInteger biadd = maxHp * birate / dConst.RateMaxBi; currentHp += biadd; if (currentHp > maxHp) currentHp = maxHp; } // 사망시 처리. protected virtual void OnDie() { bDead = true; bAttackable = false; SetIsDamaged(0); twcUnsummon.Restart(); } public int GetIsDamaged() { return isDamaged; } public void SetIsDamaged(int idamcnt) { isDamaged = idamcnt; } #endregion Damage & Heal & Die #region Debuff // 초기화. public void ResetDebuff() { fDecMov = 0f; fDecMovTime = 0f; damageSecs[0].time = 0; damageSecs[1].time = 0; bStun = false; fStunTime = 0f; fStunPushCool = 0f; } // 둔화(이동속도 감소). public void SetDecMove(float ftime, float fvalue) { if (BattleMgr.Instance.BattlePause || bSummon || bDead) return; if (fvalue < fDecMov) return; fDecMov = fvalue; fDecMovTime = ftime; moveSpeed = fMov * 12f / dConst.RateMaxFloat; if (fDecMov > 0f) moveSpeed *= (1f - fDecMov); } // 출혈(초당 데미지). public virtual void SetDamageSec(float ftime, BigInteger fatk, float fcrtdam, int icrtrate, int idnum = -1) { if (BattleMgr.Instance.BattlePause || bSummon || bDead) return; if (ftime < 1f) { return; } int index = -1; for (int i = 0; i < damageSecs.Length; i++) { if (damageSecs[i].time <= 0f) { index = i; break; } } if (index < 0) return; damageSecs[index].time = ftime - 0.05f; damageSecs[index].sec = (int)ftime; damageSecs[index].atk = fatk; damageSecs[index].crtdam = fcrtdam; damageSecs[index].crtrate = icrtrate; damageSecs[index].idnum = idnum; iDamageSecCnt++; } // 기절. public void SetStun(float ftime) { if (BattleMgr.Instance.BattlePause || bSummon || bDead) return; if (fStunPushCool > 0f) return; if (!bStun) { bStun = true; ChangeStateForce(eState.idle); if (creatureClass == eCreatureClass.boss) fStunPushCool = 10f; } fStunTime = ftime; } // 넉백. public void PushFrame(float fpushforce) { if (BattleMgr.Instance.BattlePause || bSummon || bDead || fStunPushCool > 0f) return; if (creatureClass == eCreatureClass.boss) fStunPushCool = 10f; } // 넉백(강제). protected void PushFrameForce(float fpushforce) { Vector3 v3moveto = transform.position; fpushforce *= 0.5f; if (IsLookRight) { v3moveto.x -= 10; } else { v3moveto.x += 10; } Vector3 v3framepos = Vector3.MoveTowards(transform.position, v3moveto, fpushforce); Vector3 v3movedir = v3framepos - transform.position; v3movedir.y = 0f; v3movedir.z = 0f; if (gameObject.activeSelf) { transform.DOLocalMoveX(transform.position.x + v3movedir.x, 0.5f).SetEase(Ease.OutExpo); } } // 끌어당기기 public void SetPull(BigInteger bivalue, Transform trf) { float pullValue = (int)bivalue / dConst.RateMaxFloat; Vector3 v3framepos = Vector3.Lerp(transform.position, trf.position, pullValue); if (gameObject.activeSelf) { transform.DOLocalMove(v3framepos, 0.5f).SetEase(Ease.Linear); } } #endregion Debuff #region Target & Animation // 타겟 위치 찾기. protected virtual void SearchTarget() { } // 애니메이션 이벤트. 스파인 애니메이션에서 호출됨. void HandleAnimEvent(TrackEntry trackEntry, Spine.Event e) { if(animEventHandlers.TryGetValue(e.Data, out Action action)) action(); } // 캐릭터 상태(애니메이션) 변경. public void ChangeState(eState changestate) { if (changestate == animState && changestate != eState.attack && changestate != eState.skill) return; animState = changestate; switch (animState) { case eState.idle: anState.SetAnimation(0, "idle", true); break; case eState.move: anState.SetAnimation(0, "move", true); break; case eState.attack: case eState.attack2: case eState.attack3: bAttackable = false; fAttackTick = 0f; anState.SetAnimation(0, animState.ToString(), false); break; case eState.die: anState.SetAnimation(0, "die", false); OnDie(); break; case eState.skill: bAttackable = false; fAttackTick = 0f; anState.SetAnimation(0, "attack", false); break; case eState.ceremony: anState.SetAnimation(0, "ceremony", false); break; case eState.warp_in: anState.SetAnimation(0, "warp_in", false); break; case eState.warp_out: anState.SetAnimation(0, "warp_out", false); break; case eState.rush: anState.SetAnimation(0, "rush", false); break; case eState.damage: anState.SetAnimation(0, "damage", false); break; } } // 캐릭터 상태(애니메이션) 변경. public void ChangeStateForce(eState changestate) { if (anState == null) return; animState = changestate; switch (animState) { case eState.idle: anState.SetAnimation(0, "idle", true); break; case eState.move: anState.SetAnimation(0, "move", true); break; case eState.attack: bAttackable = false; fAttackTick = 0f; anState.SetAnimation(0, animState.ToString(), false); break; case eState.die: anState.SetAnimation(0, "die", false); OnDie(); break; case eState.skill: bAttackable = false; fAttackTick = 0f; anState.SetAnimation(0, "attack", false); break; case eState.ceremony: anState.SetAnimation(0, "ceremony", false); break; case eState.warp_in: anState.SetAnimation(0, "warp_in", false); break; case eState.warp_out: anState.SetAnimation(0, "warp_out", false); break; case eState.rush: anState.SetAnimation(0, "rush", false); break; case eState.damage: anState.SetAnimation(0, "damage", false); break; } } //애니메이션 속도 초가화 public void SetAnimTimeScale() { if (animState == eState.attack || animState == eState.attack2) skAnim.timeScale = 1.0f * motionSpeed; else skAnim.timeScale = 1.0f; } #endregion Target & Animation #region Ani Event // 공격 애니메이션 마지막 프레임. public void CheckAttackState() { SearchTarget(); } // 스킬 애니메이션 마지막 프레임. public void CheckSkillState() { SearchTarget(); } // 피해받음 애니메이션 마지막 프레임. public void CheckDamageState() { ChangeState(eState.idle); } // 사망 애니메이션 마지막 프레임. public void CheckDieState() { } // 세레머니 애니메이션 마지막 프레임. public void CheckCeremonyState() { ChangeState(eState.idle); } public void CheckWarp_outState() { ChangeState(eState.idle); } // 공격 애니메이션 중간 키(데미지 타이밍). public virtual void AttackDamage() { } // 스킬 애니메이션 중간 키. public virtual void SkillEffect() { } #endregion Ani Event }