다양한 기록

[Fortress Craft] Boss Monster - Frost Lizard AI 본문

유니티 엔진/Fortress Craft

[Fortress Craft] Boss Monster - Frost Lizard AI

라구넹 2025. 2. 6. 01:30

 

Monster_FrostLizardMain

Monster_FrostLizardController

 

두 가지 클래스를 통해 AI가 작동

 

Main : 다음에 어떤 행동을 할 지 정하는 클래스

Controller : 실제 해당 행동에 대한 구체적인 구현

 

Main에서 다음 스테이트 결정하는 건 코루틴으로 구현

Controller에서 AI 설정은 FIxedUpdateNetwork에서 구현됨

 

이유: 컨트롤러의 경우 해당 행동에서 속도 설정 같은 게 필요한 경우가 있음

⇒ 한 번 스테이트 정하고 끝나지 않는 행동이 있어서 컨트롤러에서는 코루틴으로 구현 X

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Fusion;

namespace Agit.FortressCraft
{
    public class Monster_FrostLizardMain : NetworkBehaviour
    {
        [SerializeField] private Monster_FrostLizardController monsterCtrl;
        [SerializeField] private int idle = 20;
        [SerializeField] private int walk = 20;
        [SerializeField] private int breath = 20;
        [SerializeField] private int tailAttack = 20;
        [SerializeField] private int slam = 20;

        private int sum;


        private int num;
        private int burstActualValue = 0;
        private Dictionary<Monster_FrostLizardState, float> delayDict;
        private Monster_FrostLizardState prevState = Monster_FrostLizardState.IDLE;
        private Monster_FrostLizardState nextState = Monster_FrostLizardState.IDLE;

        public override void Spawned()
        {
            delayDict = new Dictionary<Monster_FrostLizardState, float>();
            delayDict.Add(Monster_FrostLizardState.IDLE, 2.0f);
            delayDict.Add(Monster_FrostLizardState.WALK, 2.0f);
            delayDict.Add(Monster_FrostLizardState.BREATH, 3.0f);
            delayDict.Add(Monster_FrostLizardState.TAILATTACK, 2.0f);
            delayDict.Add(Monster_FrostLizardState.SLAM, 3.0f);

            sum = idle + walk + breath + tailAttack + slam;

            StartCoroutine(SetMonsterState());
        }

        public IEnumerator SetMonsterState()
        {
            while( true )
            {
                prevState = nextState;
                nextState = Monster_FrostLizardState.NON;

                /*
                // 스테이트 강제 설정 ----------------------------------------------------

                if (nextState != Monster_FrostLizardState.NON)
                {
                    monsterCtrl.setState(nextState, delayDict[nextState]);
                    return;
                }
                */

                // 스테이트 일반 설정 ----------------------------------------------------
                num = Random.Range(0, sum);

                int currentSum = idle;

                if (num < currentSum)
                {
                    //Debug.Log("Monster - Idle");
                    nextState = Monster_FrostLizardState.IDLE;
                }
                else if (num < (currentSum += walk)) // 걷기 
                {
                    //Debug.Log("Monster - Walk");
                    nextState = Monster_FrostLizardState.WALK;
                }
                else if (num < (currentSum += breath))
                {
                    //Debug.Log("Monster - Breath");
                    nextState = Monster_FrostLizardState.BREATH;
                }
                else if( num < (currentSum += tailAttack) )
                {
                    nextState = Monster_FrostLizardState.TAILATTACK;
                }
                else if( num < (currentSum += slam) )
                {
                    nextState = Monster_FrostLizardState.SLAM;
                }

                if( prevState == nextState && nextState == Monster_FrostLizardState.BREATH )
                {
                    continue;
                }

                monsterCtrl.SetState(nextState, delayDict[nextState]);

                yield return new WaitForSeconds(delayDict[nextState]);
            }
        }
    }
}

Main에서 코루틴으로 관리하는 형태

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Fusion;



namespace Agit.FortressCraft
{
    public enum Monster_FrostLizardState
    {
        IDLE,
        WALK,
        BREATH,
        TAILATTACK,
        SLAM,
        NON
    }

    public class Monster_FrostLizardController : MonsterController
    {
        private Monster_FrostLizardState state = Monster_FrostLizardState.NON;
        private MonsterAttackCollider attackCollider;
        

        public override void Spawned()
        {
            base.Spawned();
            HP = hpMax;
            attackCollider = GetComponentInChildren<MonsterAttackCollider>();
        }

        // FixedUpdateNetwork는 Athority 있는 거에서만 돌아서 FixedUpdate에서 처리 필요 
        private void FixedUpdate()
        {
            if(Runner.IsSharedModeMasterClient)
            {
                rb.velocity = Vector2.zero;
                MonsterAI();
            }
        }

        public void SetState(Monster_FrostLizardState state, float nextDelay)
        {
            if (!timeCheck()) return;

            acted = false;
            startTime = Time.fixedTime;
            this.state = state;
            this.nextDelay = nextDelay;
        }

        public override void MonsterAI()
        {
            if (HP <= 0.0f) return;
            
            switch (state)
            {
                case Monster_FrostLizardState.IDLE:
                    ActionIdle();
                    break;
                case Monster_FrostLizardState.WALK:
                    ActionWalk();
                    break;
                case Monster_FrostLizardState.BREATH:
                    ActionBreath();
                    break;
                case Monster_FrostLizardState.TAILATTACK:
                    ActionTailAttack();
                    break;
                case Monster_FrostLizardState.SLAM:
                    ActionSlam();
                    break;

            }
        }

        private void ActionIdle()
        {
            if (acted) return;
            animator.SetTrigger("Idle");
            acted = true;
        }

        private void ActionWalk()
        {
            if (!acted)
            {
                dir = Vector2.zero;
                Transform Target = null;
                Collider2D[] cols = Physics2D.OverlapCircleAll(new Vector2(transform.position.x, transform.position.y), 4.0f);

                // 적 탐색 
                foreach (Collider2D col in cols)
                {
                    if (col.tag.StartsWith("Unit") && !col.CompareTag("Unit_Monster"))
                    {
                        Target = col.transform;
                        break;
                    }
                }

                if( Target != null )
                {
                    dir = (Target.position - transform.position).normalized;
                }
                else
                {
                    // 타겟이 없을 시 랜덤 단위 벡터 
                    float angle = Random.Range(0f, Mathf.PI * 2);
                    dir = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle));
                }

                animator.SetTrigger("Walk");
                acted = true;
            }

            if( dir.x > 0.001f || dir.y > 0.001f )
            {
                rb.velocity = dir * movingWeight;
            }

            if( rb.velocity.x > 0 )
            {
                rb.transform.localScale = new Vector3( Mathf.Abs( transform.localScale.x) * -1.0f,
                                                    transform.localScale.y, transform.localScale.z);
            }
            else
            {
                rb.transform.localScale = new Vector3(Mathf.Abs(transform.localScale.x),
                                                    transform.localScale.y, transform.localScale.z);
            }
        }

        private void ActionBreath()
        {
            if (acted) return;

            attackCollider.Damage = damage * 3.0f;
            animator.SetTrigger("Breath");
            acted = true;
        }

        private void ActionTailAttack()
        {
            if (acted) return;

            attackCollider.Damage = damage;
            animator.SetTrigger("TailAttack");
            acted = true;
        }

        private void ActionSlam()
        {
            if (acted) return;

            attackCollider.Damage = damage;
            animator.SetTrigger("Slam");
            acted = true;
        }
    }
}

그밖에 자세한 구현은 Controller에서 진행

 

 

플레이 예시 영상