[패스트캠퍼스 수강 후기] 올인원 패키지 : 유니티 포트폴리오 완성 100% 환급 챌린지 12회차 미션 시작합니다.


저번 시간에 이어 FOV 마무리하고, 동적 AI 캐릭터 구현하기입니다.




FieldOfView를 Editor 기능으로 확장하여 적 캐릭터의 시야 반지름이 원으로 표시되는 것을 확인할 수 있고, View Angle값을 변경하여 시야각을 조절하면 Editor 상에서 Realtime으로 변하며 확인할 수 있습니다.

이제 SearchEnemy() 관련 부분을 수정합니다.


EnemyController_New.cs

public class EnemyController_New : MonoBehaviour
{
    protected StateMachine_New<EnemyController_New> stateMachine;
    public StateMachine_new<EnemyController_New> StateMachine => stateMachine;
    
    private FieldOfView_New fov; // FOV 코드 추가.
    
    // 기존 코드 제거. -> FOV에서 처리하므로 불필요.
    //public LayerMask targetMask;
    //public Transform target;
    //public float viewRadius;
    public float attackRange;
    public Transform Target => fov?.NearestTarget; // FieldOfView_New.cs에 public Transform NearestTarget => nearestTarget; 추가해줍니다.
    
    private void Start()
    {
        stateMachine = new StateMachine_New<EnemyController_New>(this, new IdleState_New());
        stateMachine.AddState(new MoveState_New());
        stateMachine.AddState(new AttackState_New());
        
        fov = GetComponent<FieldOfView_New>();
    }
    
    private void Update()
    {
        stateMachine.Update(Time.deltaTime);
    }
    
    public bool IsAvailableAttack
    {
        get
        {
            if (!Target) // target -> Target property
            {
                return false;
            }
            
            float distance = Vector3.Distance(transform.position, Target.position); // target -> Target
            return (distance <= attackRange);
        }
    }
    
    public Transform SearchEnemy()
    {
        return Target; // FOV에서 검색된 Target return

        // FOV의 Target 사용으로 불필요.
        //target = null;
        //Collider[] targetInViewRadius = Physics.OverlapSphere(transform.position, viewRadius, targetMask);
        //if (targetInViewRadius.Length > 0)
        //{
        //    target = targetInViewRadius[0].transform;
        //}
    }
}


여기까지 구현을 하면 적을 찾는 루틴 SearchEnemy() 함수의 기존 직접 적을 찾는 코딩 방식에서, Editor에 설정된 FOV를 이용하여 적을 찾는 방식으로 변경하여 구현을 할 수 있게 되었습니다.

대단합니다. 결국 기존에는 저러한 값들이 바뀌면 항상 프로그램 소스 코드를 무조건 변경해야 했고, 관련 코드들도 모두 수정하는 일이 빈번할텐데.. FOV 컴포넌트를 추가하여 Editor 상에서도 실시간 수정을 할 수가 있게 되고, 어떻게 보면 다른 컴포넌트들에서도 FOV 컴포넌트를 공통으로 사용하게 될 것이므로 코드의 집중화까지 되는 효과가 있다고 할 수 있겠습니다.

FieldOfView_New.cs의 Update()시 매번 FindVisibleTargets()를 호출하는 것은 부하가 많이 걸리므로 delay를 사용하는 방식으로 조금 수정합니다.


FieldOfView_New.cs 수정 부분들

public float delay = 0.2f;

void Start()
{
    StartCoroutine("FindTargetsWithDelay", delay); // delay 시간으로 자동호출되는 함수 등록.
}

void Update()
{
    //FindVisibleTargets(); // Update시마다 매번 호출하지 않고 Start()에서 자동호출되는 함수 등록.
}

// delay 시간을 가지고 FindVisibleTargets()을 호출하는 함수.
IEnumerator FindTargetsWithDelay(float delay)
{
    while(true)
    {
        yield return new WaitForSeconds(delay);
        FindVisibleTargets();
    }
}

 



Update()는 매 Frame마다 호출되므로 너무 빈번하게 호출됩니다. FindTargetsWithDelay() 함수를 구현하여 0.2초마다 호출되는 함수 방식을 구현하였습니다.

기존 코드들을 정리해 줍니다. target => fov?.Target을 사용하는 형태로.. OnDrawGizmos()와 같은 불필요해진 함수들도 삭제를 합니다.




Unity에서 BarbarianWarrior_FOV (적 FOV)의 설정을 해줍니다.
+ View Radius = 5
+ View Angle = 90
+ Delay = 0.2
+ Target Mask = Player
+ Obstacle Mask = Ground, Wall
  => Obstacle Mask에 Ground와 Wall을 설정해 줌으로써 해당 GameObject들에서는 시야가 무시되도록 처리한 것입니다.
  



현재까지 상태에서 구동을 해보면 위의 화면과 같이 실행이 됩니다.
기존과 다르게 적의 반경 내에 있다고 하더라도 시야각에 들어가지 않으면 적이 MoveState로 Transition 되지 않기 때문에 캐릭터를 공격하러 오지 않는 것을 확인할 수 있습니다.

이런 것들을 이용하면 적의 뒤에서 공격하는 게임 형식이라던지, 잠입 방식의 게임 방식도 응용하여 개발이 가능해집니다.



이제 FSM을 좀더 확장하여 캐릭터가 2지점 사이를 오가는 Patrol 기능을 구현해 보도록 하겠습니다.



패트롤중 적발견하고 공격거리 이내면 Attack State로 Transition하고,
패트롤중 적발견하고 공격거리 밖이면 Move State로 Transition하고,
패트롤중 적을 발견하지 못하면 랜덤하게 Idle State로 Transition하는 방식을 취할 예정입니다.

Idle, Attack, Move State는 기존 코드를 사용하며, Patrol State만 추가로 구현을 하면 됩니다. 단지 Patrol 상태에 따른 Random Idle 처리를 위해 Idle State는 약간 변경을 해주어야 합니다.

Patrol State를 추가하는 루틴을 공부하는 이유는 기존 Attack - Idle - Move 상태만 처리하던 시스템에서 Patrol 상태만 추가를 해줌으로서 쉽게 캐릭터의 상태를 추가하고 제어할 수 있다는 것을 보여주기 위함입니다.

와우.. 상태 State 개념... 대박 좋은 거 같습니다. 어떻게 저런 아이디어를 생각해내고 구현해 낼 수 있는지.. ㅎㅎ 이건 게임이 아니라 다른 개발 프로젝트에서도 충분히 응용할 수 있고.. 꼭 그렇게 해야할 것 같은 중압감을 느낄 정도네요 ㅠ.,ㅜ;

 

 



Patrol waypoint를 구현하기 위해 Unity 상에 Sphere 2개를 놓았고, 해당 위치를 반복하며 이동하는 기능을 "MoveToWaypoint_New" 스크립트 컴포넌트를 추가하여 구현합니다.


MoveToWaypoint_New.cs

public class MoveToWaypoints : StateMachine_New<EnemyController_New>
{
    private Animator animator;
    private CharacterController controller;
    private NavMeshAgent agent;
    protected int hashMove = Animator.StringToHash("Move");
    protected int hashMoveSpeed = Animator.StringToHash("MoveSpeed");
    
    public override void OnInitialized()
    {
        animator = context.GetComponent<Animator>();
        controller = context.GetComponent<CharacterController>();
        agent = context.GetComponent<NavMeshAgent>();
    }
    
    public override void OnEnter()
    {
        if (context.targetWaypoint == null)
            context.FindNextWaypoint(); // Patrol 위치 설정
         
        if (context.targetWaypoint)
        {
            agent?.SetDestination(context.targetWaypoint.position);
            animator?.SetBool(hashMove, true);
        }
    }
   
    public override void Update(float deltaTime)
    {
        Transform enemy = context.SearchEnemy();
        if (enemy)
        {
            if (context.IsAvailableAttack)
                statemachine.ChangeState<AttackState_New>();
            else
                stateMachine.ChangeState<MoveState_New>();
        }
        else
        {
            // pathPending : NavMeshAgent가 이동해야할 경로가 존재하는지 체크
            if (!agent.pathPending && (agent.remainingDistance <= agent.stoppingDistance))
            {
                // 이동해야할 경로도 없고, 도착지점에 도착했다면 다음 목표지점 검색
                Transform nextDest = context.FindNextWaypoint();
                if (nextDest)
                {
                    agent.SetDestination(nextDest.position);
                }
                stateMachine.ChangeState<IdleState_New>(); // 잠시 Idle 상태로 Transition
            }
            else
            {
                // 경로가 남았다면 이동
                controller.Move(agent.velocity * deltaTime);
                animator.SetFloat(hashMoveSpeed, agent.velocity.magnitude / agent.speed, .1f, deltaTime);
            }
        }
    }
    
    public override void OnExit()
    {
        animator?.SetBool(hashMove, false);
        agent.ResetPath();
    }
}



EnemyController_New.cs 수정 부분들

public Transform[] waypoints; // Unity상의 Patrol 위치점들
[HideInInspector]
public Transform targetWaypoint = null;
private int waypointIndex = 0;

public Transform FindNextWaypoint()
{
    targetWaypoint = null;
    if (waypoints.Length > 0)
    {
        targetWaypoint = waypoints[waypointIndex];
    }
    
    waypointIndex = (waypointIndex + 1) % waypoints.Length; // Index Cycling..
}



IdleState_New.cs 수정 부분들

bool isPatrol = false;
private float minIdleTime = 0.0f;
private float maxIdleTime = 3.0f;
private float idleTime = 0.0f;

public override void OnEnter()
{
    animator?.SetBool(hashMove, false);
    animator?.SetFloat(hashMoveSpeed, 0);
    controller?.Move(Vector3.zero);
    
    if (isPatrol)
        idleTime = Random.Range(minIdleTime, maxIdleTime);
}

public override void Update(float deltaTime)
{
    Transform enemy = context.SearchEnemy();
    if (enemy)
    {
        if (context.IsAvailableAttack) stateMachine.ChangeState<AttackState_New>();
        else stateMachine.ChangeState<MoveState_New>();
    }
    else if (isPatrol && stateMachine.ElapsedTimeInState > idleTime)
    {
        stateMachine.ChangeState<MoveToWaypoints>();
    }
}




위와 같은 FSM 상태 구현은 나중에 디아블로 게임 제작시 구현할 체력이 떨어졌을 때 특정 지점으로 회피하였다가 다시 상태가 변경되어 다른 루틴을 구현하는 방식으로 활용될 것입니다.

캐릭터 AI를 위한 FSM 모델을 구현하고 이를 토대로 여러 상태를 가진 캐릭터 구현이 완료되었습니다.

 

<위의 코드들은 제가 보면서 주요한 함수, 코드를 확인하기 위해 타이핑한 용도로, 전체 소스코드가 아님에 주의해 주세요. 전체 코드는 교육 수강을 하면 완벽하게 받으실 수가 있답니다 ^^>


패스트캠퍼스 - 올인원 패키지 : 유니티 포트폴리오 완성 bit.ly/2R561g0

 

유니티 게임 포트폴리오 완성 올인원 패키지 Online. | 패스트캠퍼스

게임 콘텐츠 프로그래머로 취업하고 싶다면, 포트폴리오 완성은 필수! '디아블로'와 '배틀그라운드' 게임을 따라 만들어 보며, 프로그래머 면접에 나오는 핵심 개념까지 모두 잡아 보세요!

www.fastcampus.co.kr

 

+ Recent posts