주녘공부일지

[마법사의 길] 캐릭터, 몬스터 AI 상태 관리 본문

GameDevelopment/[Unity] Project

[마법사의 길] 캐릭터, 몬스터 AI 상태 관리

주녘 2024. 9. 9. 16:10

게임 소개 영상



 

 

https://github.com/godgjwnsgur7/Project_IH

 

GitHub - godgjwnsgur7/Project_IH

Contribute to godgjwnsgur7/Project_IH development by creating an account on GitHub.

github.com

유한 상태 기계 (FSM)을 기반으로 한 플레이어, 몬스터 AI 상태 관리

1) 플레이어 상태 관리 ( Condition, Exit, Enter )

protected EPlayerState _playerState = EPlayerState.None;
public virtual EPlayerState PlayerState
{
    get { return _playerState; }
    protected set
    {
        if (_playerState == value)
            return;

        bool isChangeState = true;
        switch (value)
        {
            case EPlayerState.Idle: isChangeState = IdleStateCondition(); break;
            case EPlayerState.Move: isChangeState = MoveStateCondition(); break;
            case EPlayerState.Jump: isChangeState = JumpStateCondition(); break;
            case EPlayerState.Fall: isChangeState = FallStateCondition(); break;
            case EPlayerState.Land: isChangeState = LandStateCondition(); break;
            case EPlayerState.Dash: isChangeState = DashStateCondition(); break;
            case EPlayerState.Attack: isChangeState = AttackStateCondition(); break;
            case EPlayerState.Skill1:
            case EPlayerState.Skill2:
            case EPlayerState.Skill3:
            case EPlayerState.Skill4: isChangeState = SkillStateCondition(); break;
            case EPlayerState.Hit: isChangeState = HitStateCondition(); break;
            case EPlayerState.Down: isChangeState = DownStateCondition(); break;
            case EPlayerState.DownLand: isChangeState = DownLandStateCondition(); break;
            case EPlayerState.GetUp: isChangeState = GetUpStateCondition(); break;
            case EPlayerState.Dead: isChangeState = DeadStateCondition(); break;
        }

        if (isChangeState == false)
            return;

        switch (_playerState)
        {
            case EPlayerState.Idle: IdleStateExit(); break;
            case EPlayerState.Move: MoveStateExit(); break;
            case EPlayerState.Jump: JumpStateExit(); break;
            case EPlayerState.Fall: FallStateExit(); break;
            case EPlayerState.Land: LandStateExit(); break;
            case EPlayerState.Dash: DashStateExit(); break;
            case EPlayerState.Attack: AttackStateExit(); break;
            case EPlayerState.Skill1:
            case EPlayerState.Skill2:
            case EPlayerState.Skill3:
            case EPlayerState.Skill4: SkillStateExit(); break;
            case EPlayerState.Guard: GuardStateExit(); break;
            case EPlayerState.Block: BlockStateExit(); break;
            case EPlayerState.Hit: HitStateExit(); break;
            case EPlayerState.Down: DownStateExit(); break;
            case EPlayerState.DownLand: DownLandStateExit(); break;
            case EPlayerState.GetUp: GetUpStateExit(); break;
            case EPlayerState.Dead: DeadStateExit(); break;
        }

        _playerState = value;
        PlayAnimation(value);

        switch (value)
        {
            case EPlayerState.Idle: IdleStateEnter(); break;
            case EPlayerState.Move: MoveStateEnter(); break;
            case EPlayerState.Jump: JumpStateEnter(); break;
            case EPlayerState.Fall: FallStateEnter(); break;
            case EPlayerState.Land: LandStateEnter(); break;
            case EPlayerState.Dash: DashStateEnter(); break;
            case EPlayerState.Attack: AttackStateEnter(); break;
            case EPlayerState.Skill1:
            case EPlayerState.Skill2:
            case EPlayerState.Skill3:
            case EPlayerState.Skill4: SkillStateEnter(); break;
            case EPlayerState.Guard: GuardStateEnter(); break;
            case EPlayerState.Block: BlockStateEnter(); break;
            case EPlayerState.Hit: HitStateEnter(); break;
            case EPlayerState.Down: DownStateEnter(); break;
            case EPlayerState.DownLand: DownLandStateEnter(); break;
            case EPlayerState.GetUp: GetUpStateEnter(); break;
            case EPlayerState.Dead: DeadStateEnter(); break;
        }
    }
}

2) 플레이어 인풋 컨트롤러 ( Update )

- 플레이어가 입력 값을 받아 동작이 가능한 상태를 나타내며, 상태가 변경됨에 따른 동작을 수행

private _isPlayerInputControll;
public bool IsPlayerInputControll
{
    get { return _isPlayerInputControll; }
    protected set
    {
        if (_isPlayerInputControll == value)
            return;

        _isPlayerInputControll = value;
        ConnectInputActions(value);

        if (_isPlayerInputControll)
        {
            // 강제 모션 변환
            PlayAnimation(EPlayerState.Idle);
            IdleStateEnter();

            if (coPlayerStateController == null)
                coPlayerStateController = StartCoroutine(CoPlayerStateController());
        }
    }
}
    
Coroutine coPlayerStateController = null;
protected IEnumerator CoPlayerStateController()
{
    while (IsPlayerInputControll)
    {
        switch (PlayerState)
        {
            case EPlayerState.Idle: UpdateIdleState(); break;
            case EPlayerState.Move: UpdateMoveState(); break;
            case EPlayerState.Jump: UpdateJumpState(); break;
            case EPlayerState.Fall: UpdateFallState(); break;
            case EPlayerState.Land: UpdateLandState(); break;
            case EPlayerState.Dash: UpdateDashState(); break;
            case EPlayerState.Attack: UpdateAttackState(); break;
            case EPlayerState.Skill1:
            case EPlayerState.Skill2:
            case EPlayerState.Skill3:
            case EPlayerState.Skill4: UpdateSkillState(); break;
            case EPlayerState.Guard: UpdateGuardState(); break;
            case EPlayerState.Block: UpdateBlockState(); break;
            case EPlayerState.Hit: UpdateHitState(); break;
            case EPlayerState.Down: UpdateDownState(); break;
            case EPlayerState.DownLand: UpdateDownLandState(); break;
            case EPlayerState.GetUp: UpdateGetUpState(); break;
        }

        yield return null;
    }

    coPlayerStateController = null;
}

Ex) Attack State

- Condition : Attack 모션은 착지 상태에서만 가능

- Enter : Attack 상태에서는 플레이어 조작을 할 수 없음

- Update : Attack 모션이 종료되면 입력되고 있는 값에 따라 상태를 전이 ( Idle, Move )

 protected virtual bool AttackStateCondition()
 {
     if (CreatureFoot.IsLandingGround == false)
         return false;

     return true;
 }

 protected virtual void AttackStateEnter()
 {
     isPlayerStateLock = true;
     InitRigidVelocityX();

 }

 protected virtual void UpdateAttackState()
 {
     if(IsEndCurrentState(EPlayerState.Attack))
     {
         isPlayerStateLock = false;
         PlayerState = EPlayerState.Move;
         PlayerState = EPlayerState.Idle;
     }
 }

 protected virtual void AttackStateExit()
 {

 }
 
 public void OnAttackTarget(IHitEvent attackTarget)
{
    attackTarget.OnHit(new AttackParam(PlayerInfo.Damage));
}

2) 몬스터 상태 관리, AI 

- 각 상태에 따라 코루틴으로 실행되는 Update문에서 상태 전이에 대한 정의를 함

 

public float UpdateAITick { get; protected set; } = 0.0f;
[SerializeField, ReadOnly] BaseObject ChaseTarget;
[SerializeField, ReadOnly] BaseObject AttackTarget;

protected IEnumerator CoUpdateAI()
{
    while (true)
    {
        switch (MonsterState)
        {
            case ENormalMonsterState.None:
                MonsterState = ENormalMonsterState.Idle; 
                break;
            case ENormalMonsterState.Idle: UpdateIdleState(); break;
            case ENormalMonsterState.Patrol: UpdatePatrolState(); break;
            case ENormalMonsterState.Chase: UpdateChaseState(); break;
            case ENormalMonsterState.Attack: UpdateAttackState(); break;
            case ENormalMonsterState.Hit: UpdateHitState(); break;
            case ENormalMonsterState.Dead: UpdateDeadState(); break;
        }

        if (UpdateAITick > 0)
            yield return new WaitForSeconds(UpdateAITick);
        else
            yield return null;
    }
}

3) 현재 애니메이션의 상태 제어를 위한 로직

- 애니메이션 상태 확인, 특정 모션이 끝났는지 확인 등

protected void PlayAnimation(EPlayerState state)
{
    if (animator == null)
        return;

    animator.Play(state.ToString());
}

public bool IsState(EPlayerState state)
{
    if (animator == null)
        return false;

    return IsState(animator.GetCurrentAnimatorStateInfo(0), state);
}

protected bool IsState(AnimatorStateInfo stateInfo, EPlayerState state)
{
    return stateInfo.IsName(state.ToString());
}

public bool IsEndCurrentState(EPlayerState state)
{
    if (animator == null)
    {
        Debug.LogWarning("animator is Null");
        return false;
    }

    // 다른 애니메이션 재생 중
    if (!IsState(state))
        return false;

    return IsEndState(animator.GetCurrentAnimatorStateInfo(0));
}

 protected bool IsEndState(AnimatorStateInfo stateInfo)
 {
     return stateInfo.normalizedTime >= 1.0f;
 }