Unity Engine/Unity 3D.

[유니티 3D] Enemy 상태패턴 작성

Muru 2023. 10. 22. 19:02

임의로 넣은 랜덤 값으로 적이 멈추고, 순찰하고, 추적하며 공격할수있도록 만들어보겠습니다.

상태패턴을 작용할것이라

Enemy.cs 그리고 내가 원하는 상태 enum { IDLE, MOVE, ATTACK, DEATH} 등을 적고 원하는 패턴에 대한 스크립트를 만들어서 작성을하는데... IDLE.cs, MOVE.cs, ATTACK.cs 등... 근데 여러 스크립트를 짜기엔 대형 프로젝트는 아닌지라 한 스크립트에 작성을 했습니다.

제가원하는 몬스터의 패턴은 다음과 같습니다

    public enum State
    {
        IDLE, //멈춤
        PATROL, //순찰
        CHASE, //추적
        ATTACK, //공격
        EVADE, //회피
        DEATH //사망
    }
    
//현 상태를 알수있도록
public State currentState
들어가는 상태, 현재 상태, 빠져나오는 상태 작성을위해

private void ChangeState(State newState)
{
	if (currentState != newState)
    {
    	//진행중이던 상태 종료
    	ExitState(currentState);
        //현재 상태
        currentState = newState;
        //새로 들어갈 상태 실행
        EnterState(newState);
    }
}
private void ExitState(State state)
    {
        switch (currentState)
        {
            case State.IDLE:
                ExitIdle();
                break;
            case State.PATROL:
                ExitPatrol();
                break;
            case State.CHASE:
                ExitChase();
                break;
            case State.ATTACK:
                ExitAttack();
                break;
            case State.EVADE:
                ExitEvade();
                break;
            case State.DEATH:
                ExitDeath();
                break;
        }
    }
    
    private void EnterState(State state)
    {
        Debug.Log(" 현재 상태 : " + currentState);

        switch (currentState)
        {
            case State.IDLE:
                EnterIdle();
                break;
                
			//...생략
            
            case State.DEATH:
                EnterDeath();
                break;
        }
    }

    private void ExecuteState()
    {
        switch (currentState)
        {
            case State.IDLE:
                ExcuteIdle();
                break;
                
           //...생략
           
            case State.DEATH:
                ExcuteDeath();
                break;
        }
    }
private void EnterIdle()
{

}

private ExcuteIdle()
{

}

private ExitIdle()
{

}

//... 나머지도 같은식으로

이런식으로 모든 패턴에 대한 메서드를 Alt + Enter로 싹다 생성해준다.

 


순찰 실행 로직

순찰할 지점을 정했다면, 그 방향을 바라보며 이동합니다.

//컴포넌트
private Animator animator;
private Transform playerTransform;

//순찰을 위한 코드
[SerializeField] private float speed = 3f;	//이동속도
[SerializeField] private float patrolRadius = 5f;   //순찰 반경
[SerializeField] private float patrolDuration = 5f; //한 위치에 멈춰있을 시간
[SerializeField] private float detectionRadius = 8f;  //플레이어 감지 거리
private Vector3 nextPatrolPoint;	//순찰 지점
//초기화
private void Awake()
{
    ChangeState(State.PATROL();
    animator = GetComponent<Animator>();
    playerTransform = GameObject.FindGameObjectWithTag("Player").transform;
}

//순찰 실행
private void ExcutePatrol()
{
	animator.SetFloat("Speed",  1f);	//블렌드 트리를 사용해 1이면 Walk 애니메이션 실행
    
    //플레이어를 찾는거리
    float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.Position)
	
    //다음 움직일 지점 찾기
    float distanceToPatrolPoint = Vector3.Distance(transform.position, nextPatrolPoint);

	//감지 거리 내에 있다면 추적상태로 전환
	if (distanceToPlayer <= detectionRadius)
    {
        ChangeState(State.CHASE);
        return;
    }
    
     //목표지점에 거의 도달했다면 코루틴 실행
     if (distanceToPatrolPoint <= 0.1f)
     {
       StartCoroutine(StayForAWhile());
       return;
     }
     
     else
     {
     	//바라볼 방향 구하기
     	Vector3 moveDirecion = (nextPatrolPoint - transform.position).normalized;
        //회전이 적용될 x y z 값
        Quaternion lookRotation = Quaternion.LookRotation(new Vector3(moveDirection.x, 0f, moveDirection.z));
        //부드럽게 회전시키기
        transform.rotation = Quaternion.Slerp(transform.rotation, lookRotation, speed * Time.detlaTime);
     	//nextPatrol에서 구한 순찰지점거리로 이동
        transform.position = Vector3.MoveTowards(transform.position, nextPatrolPoint, speed * Time.deltaTime);
     }
 
}
    //순찰지점은 y값을 제외한 나머지 랜덤으로 이동
    private void ChooseNextPatrolPoint()
    {
        float randomX = UnityEngine.Random.Range(-patrolRadius, patrolRadius);
        float randomZ = UnityEngine.Random.Range(-patrolRadius, patrolRadius);

        nextPatrolPoint = transform.position + new Vector3(randomX, 0f, randomZ);
    }
//순찰 지점 도착시 잠깐 쉬어주기
    private IEnumerator StayForAWhile()
    {
        Debug.Log("Stay...");
        
        //IDLE상태로 바꿔 잠깐 멈춰주고, 순찰 휴식후 다음 지점찾아 다시 순찰
        ChangeState(State.IDLE);
        yield return new WaitForSeconds(patrolDuration);
        ChooseNextPatrolPoint();   
        ChangeState(State.PATROL);
    }

추적 실행 로직

추적 상태로 전환시, 플레이어를 바라보며 이동합니다.

private vodi ExcuteChase()
{
	animator.SetFloat("Speed", 1.5f);
    
    //플레이어 발견시 쳐다보도록함. (플레이어 위치 - 내 위치)
    Vector3 directionToPlayer = (playerTransform.position - transform.position).normalized;
    //회전 방향 설정
    Quaternion lookRotation = Quaternion.LookRotation(new Vector3(directionToPlayer.x, 0f, directionToPlayer.z)
    //부드럽게 회전시키기
    transform.rotation = Quaternion.Slerp(transform.rotation, lookRotation, enemyStats.speed * Time.deltaTime);

	//플레이어 추적하기
    float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.transform.position);
     transform.position = Vector3.MoveTowards(transform.position, playerTransform.transform.position, speed * Time.deltaTime);

    //추적중에 공격 사거리내로 들어오면 공격실행을 위해 공격상태로 전환
    if (distanceToPlayer <= 2f)
    {
    	ChangeState(State.ATTACK);
        return;
    }
    
    //추적중에 플레이어가 멀어졌다면 기존 순찰모드로 돌아감
    if (distanceToPlayer > detectionRadius)
    {
    	ChangeState(State.PATROL);
        return;
    }
}

공격 실행 로직

추적 중에 공격 범위에 들어와 공격이 실행되는 상태

[SerializeField] private float attackInterval = 1.5f;    //공격 간격
private Coroutine attackRoutine;    //현재 공격을 코루틴에 저장한다.
private bool isAttacking = false;
private void ExcuteAttack()
{
	//enemy와 플레이어 사이의 거리
	float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.transform.position);

	//공격 범위 내에 들어오면서, 조건 충족시
	if (distanceToPlayer <= 2f && !isAttacking)
    {
    	//공격중엔 움직이지않음
    	animator.SetFloat("Speed", 0f);
        //공격 실행하는 코루틴을 저장함 (나중에 공격 취소 등을 위하여)
        attackRoutine = StateCoroutine(AttackPlayer());
		isAttacking = true;        
    }
    //공격 상태인데 공격 범위에 벗어나버리면 공격 중지
	else if (distanceToPlayer > 2f && isAttacking)
    {
    	StopCoroutine(attackRoutine);
        animator.SetBool("isAttacking", false);	//파라미터 이름 임의로
        isAttacking = false;
        
        ChangeState(State.CHASE);
    }
}
    //공격 코루틴 실행중
    private IEnumerator AttackPlayer()
    {
        while (true)
        {
            //공격 범위 내에 들어와있다면 공격 실행
            animator.SetBool("isAttacking", true);
            //공격 재사용 대기 시간
            yield return new WaitForSeconds(attackInterval);
            //공격 애니메이션 종료
            animator.SetBool("isAttacking", false);

            //애니메이션 길이 (공격 간격)을 고려하여 살짝 대기
            yield return new WaitForSeconds(attackInterval - 1f);
        }
    }