GURU_unity_3주차 FPS 게임 제작(3)

2023. 7. 15. 09:15SWU_프로젝트/GURU - unity

에너미 제작

 

 

오늘은 에너미를 제작해볼 것이다.

목표 

: 에너미의 상태를 구조화해 FSM으로 동작하게 하고 싶다.

순서

1. 에너미의 행동을 상태별로 분리해 다이어그램 만들기
2. 에너미 몸체 생성하기
3. 에너미 스크립트에 다이어그램에 작성한 각각의 상태 선언하기
4. 각 상태가 전환될 수 있도록 switch문 구성하기

 

에너미 행동 상태 다이어그램 제작

 

 

에너미 오브젝트 만들기

  • 하이어라키 뷰에서 [+] 버튼 – [3D Object] – [Capsule]을 선택해 캡슐 오브젝트를 생성하고, 이름은 ‘Enemy’로 변경해준다.
  • Mat_Enemy라는 이름으로 Material을 생성하고 색상을 붉은색으로 지정해준다.

 

 

  • 에너미 오브젝트의 Mesh Renderer 컴포넌트에 있는 Materials에 드래그해서 넣어 머티리얼을 교체해준다. 
  • 충돌을 쉽게 처리하기 위해 캐릭터 콘트롤러 컴포넌트를 추가하고 캡슐 콜라이더는 제거 (Remove Component) 해준다.

 

public class EnemyFSM : MonoBehaviour
{
// 에너미 상태 상수
enum EnemyState
{
Idle,
Move,
Attack,
Return,
Damaged,
Die
}
// 에너미 상태 변수
EnemyState m_State;
. . . (생략) . . .
}
  • 상태 다이어그램에 있는 6가지 상태를 enum 상수로 선언
  • 에너미의 현재 상태를 저장하기 위한 상태 변수도 선언
. . . (생략) . . .
void Update()
{
// 현재 상태를 체크해 해당 상태별로 정해진 기능을 수행하게 하고 싶다.
switch(m_State)
{
case EnemyState.Idle:
Idle();
break;
case EnemyState.Move:
Move();
break;
case EnemyState.Attack:
Attack();
break;
case EnemyState.Return:
Return();
break;
case EnemyState.Damaged:
Damaged();
break;
case EnemyState.Die:
Die();
break;
}
}

매 프레임마다 현재 에너미가 어떤 상태인지를 체크하고 그 상태에 맞는 기능을 실행하는 조건식을 실행한다.

 

목표 :

에너미의 각 상태마다 함수를 구현해보고 싶다.

순서:

1. 대기(Idle) 상태의 기능 구현하기
2. 이동(Move) 상태의 기능 구현하기
3. 공격(Attack) 상태의 기능 구현하기
4. 반환(Return) 상태의 기능 구현하기
5. 피격(Damaged) 상태의 기능 구현하기
6. 죽음(Die) 상태의 기능 구현하기

 

대기 상태 함수 호출

 

. . . (생략) . . .
void Update()
{
// 현재 상태를 체크해 해당 상태별로 정해진 기능을 수행하게 하고 싶다.
switch(m_State)
{
case EnemyState.Idle:
Idle();
break;
case EnemyState.Move:
//Move();
break;
case EnemyState.Attack:
//Attack();
break;
case EnemyState.Return:
//Return();
break;
case EnemyState.Damaged:
//Damaged();
break;
case EnemyState.Die:
//Die();
break;
}
}

 

Idle() 함수 외 주석 처리한다.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyFSM : MonoBehaviour
{
. . . (생략) . . .
// 에너미 상태 변수
EnemyState m_State;
// 플레이어 발견 범위
public float findDistance = 8f;
// 플레이어 트랜스폼
Transform player;
. . . (생략) . . .
}
  • 플레이어와 에너미와의 거리에 따라 대기 상태 → 이동 상태로 전환
  • 플레이어의 트랜스폼 변수와 지정 거리 변수를 선언
. . . (생략) . . .
void Start()
{
// 최초의 에너미 상태는 대기로 한다.
m_State = EnemyState.Idle;
// 플레이어의 트랜스폼 컴포넌트 받아오기
player = GameObject.Find("Player").transform;
}
. . . (생략) . . .
void Idle()
{
// 만일, 플레이어와의 거리가 액션 시작 범위 이내라면 Move 상태로 전환한다.
if(Vector3.Distance(transform.position, player.position) < findDistance)
{
m_State = EnemyState.Move;
print("상태 전환: Idle -> Move");
}
}
  • 에너미 생성 시 최초의 상태는 대기 상태로 지정한다.
  • 플레이어와의 거리를 재기 위해서 씬에 배치된 플레이어의 트랜스폼 컴포넌트를 검색하고 저장한다.
  • 플레이어와의 거리가 지정한 범위 미만이 되면 상태를 이동 상태로 전환한다.

 

 

이동 상태 함수 호출

 

. . . (생략) . . .
void Update()
{
// 현재 상태를 체크해 해당 상태별로 정해진 기능을 수행하게 하고 싶다.
switch(m_State)
{
case EnemyState.Idle:
Idle();
break;
case EnemyState.Move:
Move();
break;
case EnemyState.Attack:
//Attack();
break;
case EnemyState.Return:
//Return();
break;
case EnemyState.Damaged:
//Damaged();
break;
case EnemyState.Die:
//Die();
break;
}
}

• Move() 함수 주석 해제

 

public class EnemyFSM : MonoBehaviour
{
. . . (생략) . . .
// 공격 가능 범위
public float attackDistance = 2f;
// 이동 속도
public float moveSpeed = 5f;
// 캐릭터 콘트롤러 컴포넌트
CharacterController cc;
. . . (생략) . . .
void Start()
{
. . . (생략) . . .
// 캐릭터 콘트롤러 컴포넌트 받아오기
cc = GetComponent<CharacterController>();
}
. . . (생략) . . .
}

 

  • 에너미를 이동 기능을 구현하기 위해 이동 속도와 캐릭터 콘트롤러 컴포넌트 변수를 선언한다.
  • 캐릭터 콘트롤러 컴포넌트를 캐싱한다.

 

. . . (생략) . . .
void Move()
{
// 만일, 플레이어와의 거리가 공격 범위 밖이라면 플레이어를 향해 이동한다.
if(Vector3.Distance(transform.position, player.position) > attackDistance)
{
// 이동 방향 설정
Vector3 dir = (player.position - transform.position).normalized;
// 캐릭터 콘트롤러를 이용해 이동하기
cc.Move(dir * moveSpeed * Time.deltaTime);
}
// 그렇지 않다면, 현재 상태를 공격(Attack)으로 전환한다.
else
{
m_State = EnemyState.Attack;
print("상태 전환: Move -> Attack");
}
}

공격 범위 안에 들어왔을 때(상태 전환)와 공격 범위 안에 들어오지 않았을 때(이동)의 두 가지 경우로 나눠 처리한다.

 

 

공격 상태 함수 호출

 

. . . (생략) . . .
void Update()
{
// 현재 상태를 체크해 해당 상태별로 정해진 기능을 수행하게 하고 싶다.
switch(m_State)
{
case EnemyState.Idle:
Idle();
break;
case EnemyState.Move:
Move();
break;
case EnemyState.Attack:
Attack();
break;
case EnemyState.Return:
//Return();
break;
case EnemyState.Damaged:
//Damaged();
break;
case EnemyState.Die:
//Die();
break;
}
}

 

Attck() 함수 주석 해제한다.

 

public class EnemyFSM : MonoBehaviour
{
. . . (생략) . . .
// 누적 시간
float currentTime = 0;
// 공격 딜레이 시간
float attackDelay = 2f;
. . . (생략) . . .
void Attack()
{
// 만일, 플레이어가 공격 범위 이내에 있다면 플레이어를 공격한다.
if (Vector3.Distance(transform.position, player.position) < attackDistance)
{
}
// 그렇지 않다면, 현재 상태를 이동(Move)으로 전환한다(재추격 실시).
else
{
}
}
}

 

  • 시간 누적용 변수 및 공격 딜레이 시간 변수 추가한다.
  • 공격 함수는 플레이어가 공격 범위 안에 있을 때와 벗어났을 때의 행동으로 나누어서 구현한다.
. . . (생략) . . .
void Attack()
{
// 만일, 플레이어가 공격 범위 이내에 있다면 플레이어를 공격한다.
if (Vector3.Distance(transform.position, player.position) < attackDistance)
{
// 일정한 시간마다 플레이어를 공격한다.
currentTime += Time.deltaTime;
if(currentTime > attackDelay)
{
print("공격");
currentTime = 0;
}
}
// 그렇지 않다면, 현재 상태를 이동으로 전환한다(재추격 실시).
else
{
m_State = EnemyState.Move;
print("상태 전환: Attack -> Move");
currentTime = 0;
}
}

 

  • 일정한 시간마다 공격할 수 있도록 경과 시간을 누적하고 경과 시간이 공격 딜레이 시간을 넘어가면 경과 시간을 초기화한다.
  • 공격 중이라도 플레이어가 공격 범위를 넘어가면 경과 시간을 초기화하고 현재 상태를 이동 상태로 변환한다.
. . . (생략) . . .
void Move()
{
. . . (생략) . . .
// 그렇지 않다면, 현재 상태를 공격으로 전환한다.
else
{
m_State = EnemyState.Attack;
print("상태 전환: Move -> Attack");
// 누적 시간을 공격 딜레이 시간만큼 미리 진행시켜 놓는다.
currentTime = attackDelay;
}
}

첫 공격이 늦게 시작하는 문제를 조정하기 위해 현재 상태를 공격 상태로 전환할 때 누적 시간 변수에 미리 공격 딜레이 시간만큼을 저장한다.

 

 

. . . (생략) . . .
// 플레이어 체력 변수
public int hp = 20;
. . . (생략) . . .
// 플레이어의 피격 함수
public void DamageAction(int damage)
{
// 에너미의 공격력만큼 플레이어의 체력을 깎는다.
hp -= damage;
}

 

  • 플레이어의 PlayerMove.cs 스크립트에서 데미지 처리용 public 함수를 생성한다.
  • 에너미마다 공격력이 다를 수 있으므로 공격자(에너미)의 공격력을 파라미터로 전달한다.
. . . (생략) . . .
// 에너미 공격력
public int attackPower = 3;
. . . (생략) . . .
void Attack()
{
// 만일, 플레이어가 공격 범위 이내에 있다면 플레이어를 공격한다.
if (Vector3.Distance(transform.position, player.position) < attackDistance)
{
// 일정한 시간마다 플레이어를 공격한다.
currentTime += Time.deltaTime;
if (currentTime > attackDelay)
{
player.GetComponent<PlayerMove>().DamageAction(attackPower);
print("공격");
currentTime = 0;
}
}
. . . (생략) . . .
}

 

  • 에너미의 공격력을 파라미터로 넘기기 위해 공격력 변수를 전역 변수로 선언한다.
  • 에너미의 공격 함수에서 공격 시에 공격 대상(플레이어)의 데미지 함수를 실행한다.

 

복귀 상태 함수 호출

 

. . . (생략) . . .
Void Update()
{
// 현재 상태를 체크해 해당 상태별로 정해진 기능을 수행하게 하고 싶다.
switch(m_State)
{
case EnemyState.Idle:
Idle();
break;
case EnemyState.Move:
Move();
break;
case EnemyState.Attack:
Attack();
break;
case EnemyState.Return:
Return();
break;
case EnemyState.Damaged:
//Damaged();
break;
case EnemyState.Die:
//Die();
break;
}
}

Return() 함수 주석 해제한다.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyFSM : MonoBehaviour
{
. . . (생략) . . .
// 초기 위치 저장용 변수
Vector3 originPos;
// 이동 가능 범위
public float moveDistance = 20f;
// 에너미의 체력
public int hp = 15;
// 에너미의 최대 체력
int maxHp = 15;
void Start()
{
. . . (생략) . . .
// 자신의 초기 위치 저장하기
originPos = transform.position;
}
. . . (생략) . . .
}

복귀 위치를 지정하기 위해 생성과 동시에 초기 위치를 저장한다.

 

. . . (생략) . . .
void Move()
{
// 만일 현재 위치가 초기 위치에서 이동 가능 범위를 넘어간다면...
if(Vector3.Distance(transform.position, originPos) > moveDistance)
{
// 현재 상태를 복귀(Return)로 전환한다.
m_State = EnemyState.Return;
print("상태 전환: Move -> Return");
}
// 만일, 플레이어와의 거리가 공격 범위 밖이라면 플레이어를 향해 이동한다.
else if (Vector3.Distance(transform.position, player.position) > attackDistance)
{
. . . (생략) . . .
}
}
. . . (생략) . . .

이동 함수에서 이동 중에 초기 위치로부터 이동 가능 범위를 넘어가면 현재 상태를 복귀 상태로 전환한다.

 

. . . (생략) . . .
void Return()
{
// 만일, 초기 위치에서의 거리가 0.1f 이상이라면 초기 위치 쪽으로 이동한다.
if(Vector3.Distance(transform.position, originPos) > 0.1f)
{
Vector3 dir = (originPos - transform.position).normalized;
cc.Move(dir * moveSpeed * Time.deltaTime);
}
// 그렇지 않다면, 자신의 위치를 초기 위치로 조정하고 현재 상태를 대기로 전환한다.
else
{
transform.position = originPos;
// hp를 다시 회복한다.
hp = maxHp;
m_State = EnemyState.Idle;
print("상태 전환: Return -> Idle");
}
}
. . . (생략) . . .

 

피격 상태 함수 구현

 

→ 코루틴 함수의 프로세스

 

 

. . . (생략) . . .
void Damaged()
{
// 피격 상태를 처리하기 위한 코루틴을 실행한다.
StartCoroutine(DamageProcess());
}
// 데미지 처리용 코루틴 함수
IEnumerator DamageProcess()
{
// 피격 모션 시간만큼 기다린다.
yield return new WaitForSeconds(0.5f);
// 현재 상태를 이동 상태로 전환한다.
m_State = EnemyState.Move;
print("상태 전환: Damaged -> Move");
}

 

  • 피격 함수에는 코루틴 함수를 실행해주는 구문을 작성하고 실제 피격 행동 절차를 처리하는 코루틴 함수는 ‘DamageProcess’라는 이름으로 별도로 생성한다.
  • 데미지 코루틴 함수에서는 피격모션이 이뤄질 시간(0.5초)이 경과되면 현재 상태를 다시 이동 상태로 전환된다.

 

코루틴 함수에서 함수에서 yield return 키워드로 넘겨줄 수 있는 데이터 타입 목록

리턴 데이터 대기 시간
yield return null 다음 프레임까지 대기한다.
yield return new WaitForSeconds(float) 지정된 시간(초) 동안 대기한다.
yield return new WaitForFixedUpdate() 다음 고정(물리) 프레임까지 대기한다.
yield return new WaitForEndOfFrame() 모든 렌더링이 끝날 때까지 대기한다.
yield return StartCoroutine(string) 특정 코루틴 함수가 끝날 때까지 대기한다.
yield return new WWW(string) 웹 통신 작업이 끝날 때까지 대기한다.
yield return new AsyncOperation 비동기 씬 로드가 끝날 때까지 대기한다

 

. . . (생략) . . .
// 데미지 실행 함수
public void HitEnemy(int hitPower)
{
// 플레이어의 공격력만큼 에너미의 체력을 감소시킨다.
hp -= hitPower;
// 에너미의 체력이 0보다 크면 피격 상태로 전환한다.
if (hp > 0)
{
m_State = EnemyState.Damaged;
print("상태 전환: Any state -> Damaged");
Damaged();
}
}

 

  • 플레이어가 총을 맞췄을 때 에너미의 상태를 데미지 상태로 전환시킬 수 있도록 HitEnemy 함수를 생성한다.
  • 데미지 처리를 위해 에너미에게 체력(HP) 변수를 선언한다.
  • 에너미의 체력 상태에 따라 피격 상태로 전환한다.
. . . (생략) . . .
// 발사 무기 공격력
public int weaponPower = 5;
. . . (생략) . . .

플레이어가 데미지를 줄 수 있도록 PlayerFire.cs 스크립트에 공격력 변수를 추가

 

에너미 식별을 위해 에너미에게 레이어(Layer)를 설정한다.

 

. . . (생략) . . .
// 레이를 발사하고, 만일 부딪힌 물체가 있으면...
if (Physics.Raycast(ray, out hitInfo))
{
// 만일 레이에 부딪힌 대상의 레이어가 ‘Enemy’라면 데미지 함수를 실행한다.
if (hitInfo.transform.gameObject.layer == LayerMask.NameToLayer("Enemy"))
{
EnemyFSM eFSM = hitInfo.transform.GetComponent<EnemyFSM>();
eFSM.HitEnemy(weaponPower);
}
// 그렇지 않다면, 레이에 부딪힌 지점에 피격 이펙트를 플레이한다.
else
{
// 피격 이펙트의 위치를 레이가 부딪힌 지점으로 이동시킨다.
bulletEffect.transform.position = hitInfo.point;
. . . (생략) . . .
}
}
. . . (생략) . . .

복귀 중이거나 사망 상태에서도 계속 피격이 실행되는 문제 발생한다.

 

. . . (생략) . . .
// 데미지 실행 함수
public void HitEnemy(int hitPower)
{
// 만일, 이미 피격 상태이거나 사망 상태 또는 복귀 상태라면 아무런 처리도 하지 않고 함수를 종료한다.
if (m_State == EnemyState.Damaged || m_State == EnemyState.Die || m_State == EnemyState.Return)
{
return;
}
// 플레이어의 공격력만큼 에너미의 체력을 감소시킨다.
hp -= hitPower;
. . . (생략) . . .
}
. . . (생략) . . .

 

피격, 사망, 복귀 상태에서는 데미지 함수가 실행되지 않도록 예외 처리를 추가한다.

 

 

사망 상태 함수 구현

 

// 에너미의 체력
public int hp = 15;
. . . (생략) . . .
// 데미지 실행 함수
public void HitEnemy(int hitPower)
{
// 플레이어의 공격력만큼 에너미의 체력을 감소시킨다.
hp -= hitPower;
// 에너미의 체력이 0보다 크면 피격 상태로 전환한다.
if (hp > 0)
{
m_State = EnemyState.Damaged;
print("상태 전환: Any state -> Damaged");
Damaged();
}
// 그렇지 않다면 죽음 상태로 전환한다.
else
{
m_State = EnemyState.Die;
print("상태 전환: Any state -> Die");
Die();
}
}

 

에너미의 체력이 0이 되면 죽음 상태로 전환한다.

 

. . . (생략) . . .
// 죽음 상태 함수
void Die()
{
// 진행 중인 피격 코루틴을 중지한다.
StopAllCoroutines();
// 죽음 상태를 처리하기 위한 코루틴을 실행한다.
StartCoroutine(DieProcess());
}
IEnumerator DieProcess()
{
// 캐릭터 콘트롤러 컴포넌트를 비활성화시킨다.
cc.enabled = false;
// 2초 동안 기다린 후에 자기 자신을 제거한다.
yield return new WaitForSeconds(2f);
print("소멸!");
D

 

  • 이미 진행 중인 피격 코루틴이 사망 상태에서 실행되지 않도록 모든 피격 코루틴을 중지한다.
  • 일정 시간 뒤에 자신을 제거하도록 코루틴으로 실행한다.