부트캠프

공격 패턴 시스템 구현

noyyo 2023. 11. 3. 21:46

저번부터 쭉 구현중인 시스템의 기본적인 로직을 오늘 얼추 뼈대를 잡게 되었다.

여러 문제에 많이 부딪히고 있는데 오늘 부딪힌 가장 큰 문제는 애니메이션의 상태를 판별하는 것이다.

 

여러가지의 공격 패턴이 존재하게 될 것이고 공격마다 각각 해당하는 애니메이션 클립을 재생하게 될 텐데 애니메이션이 끝났는지 판단하는 방법을 찾기가 어려웠다.

 

결국 튜터님께 자문을 구했고 StateMachine Behaviour라는 클래스를 상속받아서 애니메이터에서 상태 변화에 따라 이벤트를 실행할 수 있는 방법에 대해 들었지만 지금 구현중인 시스템과는 조금 알맞지 않았다.

시스템은 어떤 애니메이션 클립을 실행할지 모르고 SetTrigger를 통해서 트리거를 세팅시켜줄 뿐인데 StateMachine Behaviour를 통해 애니메이션을 관리하려면 관리가 필요한 모든 Animation 클립에 직접 행동을 추가시켜줘야 했다.

때문에 결국 깔끔하진 않지만 다른 방법을 쓰기로 했다.

Animator의 GetCurrentStateInfo 메소드를 통해서 현재 상태의 normalizedTime을 가져와서 저장하고 normalizedTime이 갑자기 작아지면 상태가 바뀌었다고 알 수 있으므로 해당 방법을 통해서 애니메이션이 끝났는지 확인하는 방식으로 구현했다.

 

그 외에도 여러가지 시스템의 구조적인 문제, 기획적인 문제등으로 머리를 싸매고 있지만 구현이 생각했던 것보다 너무 늦어지고 있기에 일단은 나중에 수정하는 것이 어렵지 않을 것 같은 문제들은 제쳐두고 핵심이 되는 로직들을 구현해서 아래와 같이 작성했다.

 

using System.Collections;
using System.Collections.Generic;
using Unity.IO.LowLevel.Unsafe;
using UnityEngine;

public enum ActionTypes
{
    None = -1,
    Melee,
    Target,
    RangedTargeted,
    LaunchProjectile,
    ChargedLaunchProjectile,
    AoE,
}
public enum AnimState
{
    NotStarted,
    Playing,
    Completed
}


[CreateAssetMenu(fileName = "AttackActionSO", menuName = "Characters/Enemy/AttackAction")]
public abstract class AttackAction : ScriptableObject
{
    [HideInInspector]
    public EnemyStateMachine StateMachine;
    [ReadOnly] // 해당 필드는 인스펙터에서 편집이 불가능합니다. 파생된 클래스에서 지정합니다.
    public ActionTypes ActionType;
    [SerializeField]
    public ActionConfig Config;
    [SerializeField]
    public ActionCondition Condition;
    protected float timeStarted { get; set; }
    protected float timeRunning { get { return (Time.time - timeStarted); } }
    // 액션이 완료되면 true로 세팅해줘야 합니다.
    protected bool isCompleted;
    protected bool isEffectStarted;
    protected bool isEffectEnded;
    protected float effectStartTime;
    protected bool hasRemainingEffect = false;
    // 애니메이션이 실행됐는지 여부를 확인하는 딕셔너리입니다.
    protected Dictionary<int, AnimState> animState = new Dictionary<int, AnimState>(2);
    protected int currentAnimHash;
    protected float currentAnimNormalizedTime;
    [HideInInspector]
    public float lastUsedTime;
    
    public virtual void OnAwake()
    {
        Config.InitializeAnimHash();
        InitializeAnimState();
    }
    public virtual void OnStart()
    {
        timeStarted = Time.time;
        isCompleted = false;
        isEffectStarted = false;
        isEffectEnded = false;
    }
    public virtual void OnUpdate()
    {
        if (isCompleted)
        {
            if (Config.ChainedAction != null)
            {
                StateMachine.CurrentAction = Config.ChainedAction;
                StateMachine.ChangeState(StateMachine.AttackState);
            }
            else
            {
                StateMachine.ChangeState(StateMachine.ChaseState);
            }
            return;
        }
        
        if (isEffectStarted && !isEffectEnded && Time.time - effectStartTime >= Config.EffectDurationSeconds)
        {
            OnEffectFinish();
        }
        CheckAnimationState();

    }
    public virtual void OnEnd()
    {
        if (isEffectStarted && !isEffectEnded)
        {
            hasRemainingEffect = true;
            StateMachine.AddActionInActive(this);
        }
        else
        {
            lastUsedTime = Time.time;
        }
    }
    protected virtual void OnEffectStart()
    {
        effectStartTime = Time.time;
        isEffectStarted = true;
    }
    protected virtual void OnEffectFinish()
    {
        isEffectEnded = true;
        if (hasRemainingEffect)
        {
            StateMachine.RemoveActionInActive(this);
            lastUsedTime = Time.time;
        }
    }
    protected virtual void ApplyAttack(IDamageable target)
    {
        target.TakeDamage(Config.DamageAmount);
        if (target.CanTakeAttackEffect)
        {
            MonoBehaviour targetObject;
            if (!(target is MonoBehaviour))
                return;
            targetObject = target as MonoBehaviour;
            switch (Config.AttackEffectType)
            {
                case AttackEffectTypes.None:
                    break;
                case AttackEffectTypes.KnockBack:
                    ApplyKnockBackOrAirborne(targetObject.gameObject, AttackEffectTypes.KnockBack);
                    break;
                case AttackEffectTypes.Airborne:
                    ApplyKnockBackOrAirborne(targetObject.gameObject, AttackEffectTypes.Airborne);
                    break;
                case AttackEffectTypes.Stun:
                    break;
            }
        }
    }
    private void ApplyKnockBackOrAirborne(GameObject target, AttackEffectTypes effectType)
    {
        if (target == null)
            return;
        Transform targetTransform = target.transform;
        Rigidbody rigidbody;
        if (target.TryGetComponent(out rigidbody))
        {
            Vector3 direction = Vector3.zero;
            switch (effectType)
            {
                case AttackEffectTypes.KnockBack:
                    direction = targetTransform.forward;
                    rigidbody.AddForce(direction.normalized * Config.AttackEffectValue);
                    break;
                case AttackEffectTypes.Airborne:
                    direction = Vector3.up;
                    rigidbody.AddForce(direction * Config.AttackEffectValue);
                    break;
                default:
                    return;
            }

        }
    }
    protected void InitializeAnimState()
    {
        animState.Add(Config.AnimTriggerHash1, AnimState.NotStarted);
        animState.Add(Config.AnimTriggerHash2, AnimState.NotStarted);
    }
    protected void StartAnimation(int animTriggerHash)
    {
        if (!animState.ContainsKey(animTriggerHash))
        {
            Debug.LogError("해당 애니메이션 해쉬가 없습니다.");
            return;
        }
            
        foreach (AnimState state in animState.Values)
        {
            if (state == AnimState.Playing)
            {
                Debug.LogError("이미 다른 애니메이션이 실행중입니다.");
                return;
            }
        }
        StateMachine.Animator.SetTrigger(animTriggerHash);
        animState[animTriggerHash] = AnimState.Playing;
        currentAnimHash = animTriggerHash;
    }
    protected void CheckAnimationState()
    {
        if (animState[currentAnimHash] == AnimState.Playing)
        {
            float normalizedTime = StateMachine.Animator.GetCurrentAnimatorStateInfo(0).normalizedTime;
            if (normalizedTime < currentAnimNormalizedTime)
            {
                animState[currentAnimHash] = AnimState.Completed;
                currentAnimNormalizedTime = 0f;
            }
            else
            {
                currentAnimNormalizedTime = normalizedTime;
            }
        }
    }
    
}

 

이제 AttackAction이 스크립터블 오브젝트를 상속받아 작성됐기에 생성하는 부분에서 모든 적들이 하나의 상태를 공유하는 사태를 막기 위해 적절하게 복사본을 생성하고 관리할 수 있도록 초기화 하고 StateMachine 내에서 잘 관리될 수 있도록 필요한 코드들을 추가하는 일이 남아있다.

 

해당 작업이 끝나고 해야할 것들은 Action을 선택하는 알고리즘을 작성하고 Action이 적절한 방향으로 실행될 수 있도록 적의 Transform을 조정하는 ChaseState의 수정이 남아있다.

'부트캠프' 카테고리의 다른 글

1차 통합  (1) 2023.11.09
적 공격 액션 시스템 완성  (0) 2023.11.08
적 공격 기믹 구상하기(feat 소통의 중요성)  (0) 2023.11.01
일정 영역을 넘어가면 돌아가는 적 AI  (0) 2023.10.27
적 AI구현  (0) 2023.10.26