오늘은 어제 계획했던 적의 AI 구현과 전투에 더해 적들이 소환되는 필드를 구현하였다.

 

먼저 오늘 개발한 것들에 대해 정리하기 전에 구현한 적의 행동 패턴에 대해서 설명하겠다.

적이 종류별로 소환되는 일정한 구역이 있고, 해당 범위 내에 적이 일정 수만큼 스폰된다. (기본값 5) 적은 지역 안에서 랜덤한 시간 주기로(10~30초) 조금씩 움직인다. 적은 플레이어의 공격을 받으면 전투 상태에 돌입하여 플레이어를 따라온다. 플레이어가 지역 밖으로 나가면 추격을 중지하고 원래 위치로 되돌아간다. 이미 전투 상태에 돌입한 적은 시간이 지나도 전투 상태가 유지된다.

 

 

적의 AI 구현에는 이전에 강의에서 배울 때 구현했던 EnemyController와 IdleState, MoveState, AttackState, DeadState, MoveToWaypointState 등을 사용했다. MoveState는 명칭이 명확하지 않은 것 같아 MoveToTarget으로 이름을 변경했다. 그리고 기존에 구현했던 기능은 주변에 적을 발견하면 쫓아오는 것이었기 때문에, 위에 서술한 내용대로 AI를 구성하기 위해서 내부 코드들을 조금씩 수정해주었다.

 

public Transform Target
    {
        get
        {
            if (target.GetComponent<PlayerCharacterController>().IsAlive && _enemyAreaController.IsPlayerInArea && isInBattle)
            {
                return target;
            }
            else return null;
        }
    }

우선 Target을 찾는 방법이 조금 달라졌다. 플레이어가 살아있어야함은 물론이고, 플레이어가 지역 내에 있어야한다. 또, 현재 전투 상태에 돌입한 상태여야 Player를 Target으로 반환하도록 하였다.

public override void Update(float deltaTime)
    {
        // if searched target
        // change to move state

        if (context.Target)
        {
            if (context.IsAvailableAttack)
            {
                // check attack cool time
                // and transition to attack state
                stateMachine.ChangeState<AttackState>();
            }
            else
            {
                stateMachine.ChangeState<MoveToTargetState>();
            }
        }
        else if (_isPatrol)
        {
            if (stateMachine.ElapsedTimeInState > _idleTime)
            {
                context.SetPatrolPosition();
            }
            if (Vector3.Distance(context.transform.position, context.patrolPosition) > _agent.stoppingDistance)
            {
                stateMachine.ChangeState<MoveToWaypointState>();
            }            
        }
    }

그리고 가장 크게 바뀐 것은 IdleState인 것 같다. 타겟을 찾았을 때의 동작은 동일하지만, 타겟을 찾지 못했을 경우, 패트롤을 하는지 확인하고, IdleState에서 일정 시간이 지나면 PatrolPosition을 설정해준다. 그리고 patrolPosition 위치에 있지 않을 경우, MoveToWaypointState로 전환하여 patrolPosition까지 이동한다.

 

몬스터가 움직이는 반경은 중심으로부터 15까지, 적을 감지하는 범위는 20까지로 설정하여 적의 감지범위 밖에서 적을 공격할 수 없도록 하였다.

 

public void TakeDamage(int damage, GameObject hitEffectPrefab, Transform attackFrom)
    {
        isInBattle = true;
        target = attackFrom.transform;

        if (!IsAlive)
        {
            return;
        }

        _currentHP -= damage;

        if (_healthBar)
        {
            _healthBar.Value = _currentHP;
        }

        if (hitEffectPrefab)
        {
            Instantiate(hitEffectPrefab, hitPoint);
        }

        if (IsAlive)
        {
            if (stateMachine.CurrentState is not AttackState)
            {
                animator.SetTrigger(_hitTriggerHash);
            }
        }
        else
        {
            if (_healthBar != null)
            {
                _healthBar.enabled = false;
            }

            stateMachine.ChangeState<DeadState>();
        }
    }

그리고 TakeDamage의 기능도 수정해주었다. attackFrom이라는 매개변수를 추가하여 어떤 상대로부터 공격받았는지를 저장하도록 하였다. 또, AttackState가 아닐 때만 피격 모션을 재생하도록 하였다. 이 부분을 없애면 연속으로 공격을 받을 때 공격을 하지 못하고 계속해서 피격당하는 굶지마의 거미 같은 몬스터를 구현할 수 있을 것 같다.

 

using SingletonPattern;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyAreaController : MonoBehaviour
{
    #region Variables

    private Transform _player;
    public GameObject MonsterPrefab;

    public int numberOfEnemy = 5;

    public float checkPlayerRange = 20;
    public float enemyMoveRange = 15;

    #endregion Variables

    #region Properties

    public float DistanceToPlayer => Vector3.Distance(transform.position, _player.transform.position);

    public bool IsPlayerInArea
    {
        get
        {
            if (checkPlayerRange > DistanceToPlayer) return true;
            else return false;
        }
    }

    #endregion Properties

    #region Unity Methods

    void Start()
    {
        _player = PlayerCharacterController.Instance.transform;

        StartCoroutine(CheckEnemyCoroutine());
    }

#if UNITY_EDITOR
    private void OnDrawGizmos()
    {
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(transform.position, checkPlayerRange);
    }
#endif

    #endregion Unity Methods

    private IEnumerator CheckEnemyCoroutine()
    {
        for (int i = 0; i < numberOfEnemy; ++i)
        {
            SpawnEnemy();
        }

        while(true)
        {
            while (transform.childCount < numberOfEnemy)
            {
                yield return new WaitForSeconds(1f);
                SpawnEnemy();
            }
            yield return new WaitForSeconds(5f);
        }
    }

    private void SpawnEnemy()
    {
        float randomX = Random.Range(-10, 10f);
        float randomZ = Random.Range(-10, 10f);
        float randomRotation = Random.Range(0, 360);

        Vector3 spawnPosition = new Vector3(randomX, MonsterPrefab.transform.position.y, randomZ) + transform.position;
        Quaternion spawnRotation = Quaternion.Euler(0, randomRotation, 0);

        GameObject enemy = Instantiate(MonsterPrefab, spawnPosition, spawnRotation, transform);
        enemy.GetComponent<EnemyController>().patrolPosition = spawnPosition;
    }

    public bool CheckEnemyMoveRange(Vector3 position)
    {
        return Vector3.Distance(position, transform.position) < enemyMoveRange;
    }
}

적이 소환되는 구역은 이런식으로 구현하였다. 중심부를 기준으로 거리를 측정하여 원형 범위로 적이 소환되고 움직일 수 있도록 하였다. 위에 언급했듯, 적이 움직일 수 있는 범위보다 지역 내 플레이어 감지 범위를 넓게 하여 플레이어가 감지범위 밖에서 때릴 수 없도록 하였다.

또, 5초마다 범위 내의 몬스터 수를 체크하도록 하여 성능에 부담을 적게 주도록 하였다.

 

이외에도 여기저기서 수정한 부분들이 많긴 한데, 여기저기 흩어져있어서 정리하기가 어려운 것 같다.

 

오늘까지 구현된 부분은 다음과 같다.

 

뭔가 전반적인 전투가 갖춰지긴 했으나.. UI라든가 공격 효과, 효과음 등이 없어서 전투가 상당히 밋밋해보이는 것 같다. 공격 버튼 효과도 아직 구현하지 못했다. 내일 계획은 Inventory와 Item을 구현하는 것인데, 우선 이것들을 먼저 구현해야겠다. UI라든가 효과 등은 부가적인 부분이고, 핵심적인 기능들을 구현하는 것이 우선이라고 생각한다. 스크롤형 인벤토리를 구현하는 것이나, 장비의 소켓, 장비를 클릭하여 장착하거나 판매하는 것 등... 이전 구현 때와는 달라진 부분들이 상당히 많아서 구현이 조금 어려울 수도 있을 것 같다. 적을 처치하면 일정 확률로 캐릭터의 인벤토리에 아이템이 추가되는 기능도 추가해야할 것 같다. UI작업 등 해야할 것들이 상당히 많아서 하루보다 더 걸릴 수 있을 것 같다. 먼저 Inventory와 Item의 기본 구현을 마무리하고, UI라든지, 효과음, 공격 효과, 버튼 효과 등의 작업을 해야할 것 같다.

+ Recent posts