2023. 7. 15. 09:17ㆍSWU_프로젝트/GURU - unity
에너미 폴리싱
오늘 내가 학습할 내용은 위와 같다.
목표 : 에너미의 외형을 좀비 캐릭터로 교체하고 싶다.
순서
1. 에셋 스토어에서 원하는 형태의 모델링 데이터를 임포트한다.
2. 개발 환경에 맞춰 모델링 데이터의 임포트 설정을 완료한다.
3. 임포트된 모델링 사용한다.
좀비 모델링 적용하기
- 에셋 스토어 우측의 카테고리 선택에서 3D 항목 하위의 Characters 항목에 체크 표시하기
- 무료 에셋만 검색하기 위해 Pricing 항목은 Free Assets로 선택
- 에셋 검색 결과 목록에서 Zombie 에셋을 선택한 후 에셋을 다운로드 및 프로젝트에 임포트하기
- Zombie1.FBX 파일을 선택하면 인스펙터 뷰에 임포트 셋팅 항목이 출력
- 임포트 셋팅 값이 그림과 같은지 확인하고 하이어라키 뷰로 드래그해서 넣어 씬에 배치한다.
- 좀비 오브젝트를 Enemy 오브젝트 쪽으로 드래그해 자식 오브젝트로 등록한다.
- 좀비 오브젝트의 Transform 데이터를 리셋해준다.
- 좀비 오브젝트가 공중에 뜬 상태임을 확인한다.
- 캡슐 오브젝트와 좀비 오브젝트의 피봇이 서로 다르기 때문에 생기는 현상이다.
이를 해결하기 위해서,
- 캡슐의 높이가 2미터이므로 좀비 오브젝트의 Y축 포지션 값을 -1로 변경한다.
- 캡슐이 보이지 않도록 메시 렌더러 컴포넌트를 비활성화시킨다.
- 좀비 외곽 형태에 맞게 캐릭터 콘트롤러의 지름 조정한다.
모델링 교체가 완료된 모습이다.
목표 : 에너미의 각 상태에 맞는 애니메이션을 적용하고 싶다.
순서 :
1. 대기 상태에서의 애니메이션 적용하기
2. 이동 및 복귀 상태에서의 애니메이션 적용하기
3. 공격 및 공격 대기 상태에서의 애니메이션 적용하기
4. 피격 상태에서의 애니메이션 적용하기
5. 죽음 상태에서의 애니메이션 적용하기
애니메이션 리타겟팅
- 좀비 모델링을 선택한 후 임포트 세팅의 [Rig] 탭을 클릭한다.
- 애니메이션 타입이 휴머노이드로 되어 있는지와 [Configure…] 버튼 좌측에 체크 표시를 확인한다.
모델링 데이터가 성공적으로 휴머노이드 타입으로 전환되었다면 프로젝트 뷰에 아바타(Avatar) 파일이 자동으로 생성된다.
애니메이터 콘트롤러 설정
- 애니메이션 관련 파일들을 따로 관리하기 위해 프로젝트 뷰에 새 폴더를 만들고 이름은 ‘Animations’로 변경한다.
- [+]버튼 – [Animator Controller]를 선택해 새 애니메이터 콘트롤러를 생성해준다.
- 애니메이터 콘트롤러의 이름을 ‘Zombie Anim’으로 변경한다.
새로 생성한 애니메이터 콘트롤러를 드래그해 좀비 오브젝트의 애니메이터 컴포넌트의 Controller 항목에 추가한다.
비주얼 스크립팅
- 애니메이터 콘트롤러를 추가한 상태에서 Zombie Anim을 더블 클릭하면 씬 뷰가 있었던 곳에 애니메이터 뷰가 생성된다.
- Entry, Any State, Exit의 세 가지 상태 박스를 마우스로 드래그해서 그림과 같이 정리해준다.
엔트리 상태 옆에 마우스 커서를 올려놓고 마우스 오른쪽 버튼을 클릭해 생성된 목록 박스에서 [Create state – Empty]를 선택해 빈 상태 박스를 생성해준다.
- 프로젝트 뷰에서 Z_Idle.anim 파일을 선택
- Z_Idle.anim 파일을 앞에서 만든 New State의 Motion 항목에 드래그해 넣어주고 New State의 이름을 ‘Idle’로 변경한다.
유니티 에디터를 플레이하여 좀비 모델링의 대기 애니메이션이 실행되는 것을 확인한다.
- 애니메이터 뷰에서 마우스 오른쪽 버튼을 클릭을 한 후 [Create state–Empty]를 선택해 빈 상태 박스를 생성한다.
- 생성된 상태 박스의 이름을 ‘Move’로 변경해준다.
- 프로젝트 뷰에서 Z_Run_InPlace 를 Move 상자의 Motion 항목으로 드래그해서 넣어 추가해준다.
- Idle 상태 박스에 마우스 커서를 올려놓은 상태에서 마우스 오른쪽 버튼을 클릭한 후 생성된 선택 목록에서 [Make Transition] 을 선택한다.
- 마우스 커서에 선이 연결돼 있는 상태로 다음 상태인 Move 상태 박스를 클릭한다.
- 상태를 전환할 수 있게 연결된 선을 트랜지션(Transition)이라고 한다 .
- 트랜지션을 호출하기 위한 파라미터가 필요
- 파라미터 창의 우측 상단에 있는 [+] 버튼을 클릭
- 코드로 트랜지션을 호출할 때 특정한 값을 조건으로 하지는 않을 것이므로 파라미터 종류는 Trigger로 선택한다.
- 파라미터의 이름은 ‘IdleToMove’로 변경한다.
- Idle -> Move 트랜지션을 선택
- 인스펙터 뷰의 하단의 Conditions에서 [+] 버튼을 클릭하고 빈 항목이 생성되면 앞에서 만든 IdleToMove로 지정
- Move 상자를 마우스로 선택하고 왼쪽으로 드래그한다.
- 타임라인 쪽의 시작 핸들도 0%가 될 때까지 좌측으로 드래그한다.
- 하단의 미리 보기를 플레이해서 동작이 더 많이 섞여 플레이되는 것을 확인한다.
에너미 폴리싱 (애니메이션)
애니메이션 트랜지션 호출하기(대기 → 공격 이동)
public class EnemyFSM: MonoBehaviour
{
. . . (생략) . . .
// 애니메이터 변수
Animator anim;
void Start()
{
. . . (생략) . . .
// 자식 오브젝트로부터 애니메이터 변수 받아오기
anim = transform.GetComponentInChildren<Animator>();
}
. . . (생략) . . .
}
- EnemyFSM.cs 스크립트에서 애니메이션 트랜지션을 호출할 준비하기
- 애니메이터 컨트롤러를 제어하기 위해 애니메이터 컴포넌트 받기
public class EnemyFSM: MonoBehaviour
{
. . . (생략) . . .
void Idle()
{
// 만일, 플레이어와의 거리가 액션 시작 범위 이내라면 Move 상태로 전환한다.
if (Vector3.Distance(transform.position, player.position) < findDistance)
{
m_State = EnemyState.Move;
print("상태 전환: Idle -> Move");
// 이동 애니메이션으로 전환하기
anim.SetTrigger("IdleToMove");
}
}
. . . (생략) . . .
}
- Idle( ) 함수 마지막 부분에서 상태 전환할 때 애니메이션도 같이 전환하는 코드를 추가
- 스크립트에서 트리거 파라미터를 호출할 때는 SetTrigger( ) 함수를 사용
- 에너미가 플레이어 쪽으로 방향 전환을 하지 않고 그냥 이동하는 문제 확인한다.
- 대기 애니메이션에서 이동 애니메이션으로 바로 전환되지 않는 문제 확인한다.
public class EnemyFSM: MonoBehaviour
{
. . . (생략) . . .
void Move()
{
. . . (생략) . . .
// 만일, 플레이어와의 거리가 공격 범위 밖이라면 플레이어를 향해 이동한다.
else if (Vector3.Distance(transform.position, player.position) > attackDistance)
{
// 이동 방향 설정
Vector3 dir = (player.position - transform.position).normalized;
// 캐릭터 콘트롤러를 이용해 이동하기
cc.Move(dir * moveSpeed * Time.deltaTime);
// 플레이어를 향해 방향을 전환한다.
transform.forward = dir;
}
. . . (생략) . . .
}
}
- 대기 애니메이션에서 이동 애니메이션으로의 전환이 즉시 이루어지도록 트랜지션을 수정하기
- 중앙 부분의 Has Exit Time 항목에 체크 표시를 해제
이동 애니메이션으로의 즉시 전환과 이동 방향으로 회전 여부를 확인한다.
애니메이션 트랜지션 호출하기(복귀 이동 → 대기)
Move 상태 박스에서 Idle 상태 상자 쪽으로 트랜지션을 연결
- 복귀 트랜지션에 사용할 새 트리거 파라미터를 추가
- 파라미터의 이름은 MoveToIdle로 설정
- [Has Exit Time] 항목의 체크 표시를 해제
. . . (생략) . . .
void Return()
{
// 만일, 초기 위치에서의 거리가 0.1f 이상이라면 초기 위치 쪽으로 이동한다.
if (Vector3.Distance(transform.position, originPos) > 0.1f)
{
. . . (생략) . . .
// 복귀 지점으로 방향을 전환한다.
transform.forward = dir;
}
// 그렇지 않다면, 자신의 위치를 초기 위치로 조정하고 현재 상태를 대기 상태로 전환한다.
else
{
. . . (생략) . . .
// 대기 애니메이션으로 전환하는 트랜지션을 호출한다.
anim.SetTrigger("MoveToIdle");
}
}
. . . (생략)
public class EnemyFSM : MonoBehaviour
{
. . . (생략) . . .
// 초기 위치 저장용 변수
Vector3 originPos;
Quaternion originRot;
. . . (생략) . . .
void Start()
{
. . . (생략) . . .
// 자신의 초기 위치와 회전 값을 저장하기
originPos = transform.position;
originRot = transform.rotation;
}
}
- 복귀 위치로 되돌아왔을 때는 처음에 배치돼 있었던 방향을 바라보도록 하기 위해 위치와 방향을 저장할 변수를 선언
- Start( ) 함수에 생성하면서의 초기 위치 값과 초기 회전 값을 저장
void Return()
{
. . . (생략) . . .
else
{
// 위치 값과 회전 값을 초기 상태로 변환한다.
transform.position = originPos;
transform.rotation = originRot;
// hp를 다시 회복한다.
hp = maxHp;
m_State = EnemyState.Idle;
print("상태 전환: Return -> Idle");
// 대기 애니메이션으로 전환하는 트랜지션을 호출한다.
anim.SetTrigger("MoveToIdle");
}
}
복귀 이동으로 초기 위치에 도달하면 위치 값과 회전 값을 저장한 값으로 변경
애니메이션 트랜지션 호출하기(공격 이동 → 공격)
- 공격은 공격 동작과 다음 공격 동작까지 재생될 대기 동작이 필요
- 공격 상태 박스와 공격 대기 상태 박스 추가
각 상태 박스에 공격 애니메이션 클립과 공격 대기 애니메이션 클립 추가
- 새 트리거 파라미터를 추가
- 파라미터의 이름은 ‘MoveToAttackDelay’로 변경
- Move 상태 박스에서 Attack Delay 상태 박스로 트랜지션을 연결
- 트랜지션의 [Conditions] 항목에 새로 만든 파라미터를 추가
- [Has Exit Time] 항목의 체크 표시도 해제
. . . (생략) . . .
void Move()
{
. . . (생략) . . .
// 그렇지 않다면 현재 상태를 공격으로 전환한다.
else
{
m_State = EnemyState.Attack;
print("상태 전환: Move -> Attack");
// 누적 시간을 공격 딜레이 시간만큼 미리 진행시켜 놓는다.
currentTime = attackDelay;
// 공격 대기 애니메이션 플레이
anim.SetTrigger("MoveToAttackDelay");
}
}
. . . (생략) .
공격 대기 상태에서 일정한 시간마다 공격 애니메이션을 실시하고 공격 대기 상태로 되돌아올 수 있도록 양쪽 뱡향으로 트랜지션을 연결한다.
- Attack Delay → Attack 트랜지션의 [Conditions] 항목에 StartAttack 파라미터를 추가한다.
- [Has Exit Time] 항목의 체크 표시를 해제한다.
. . . (생략) . . .
void Attack()
{
// 만일, 플레이어가 공격 범위 이내에 있다면 플레이어를 공격한다.
if (Vector3.Distance(transform.position, player.position) < attackDistance)
{
// 일정한 시간마다 플레이어를 공격한다.
currentTime += Time.deltaTime;
if (currentTime > attackDelay)
{
player.GetComponent<PlayerMove>().DamageAction(attackPower);
print("공격");
currentTime = 0;
// 공격 애니메이션 플레이
anim.SetTrigger("StartAttack");
}
}
. . . (생략) . . .
}
. . . (생략) .
- 공격 애니메이션이 끝나면 다른 조건 없이 공격 대기 애니메이션으로 돌아오도록 설정
- [Conditions] 항목에 어떠한 파라미터도 추가하지 않은 상태로 유지
- [Has Exit Time] 항목 체크
공격 애니메이션이 반복되지 않도록 Z_Attack 애니메이션 클립의 Loop Time 항목에 체크를 해제
애니메이션 트랜지션 호출하기(공격, 공격 대기 → 이동)
- 공격 중 플레이어가 공격 범위 밖으로 나가면 다시 추격하도록 트랜지션 추가
- 공격과 공격 대기 둘 다 이동 상태 쪽으로 트랜지션을 연결
- 스크립트에서 재추격 트랜지션을 호출할 수 있도록 ‘AttackToMove’라는 이름으로 파라미터를 추가
- Attack Delay → Move 트랜지션과 Attack → Move 트랜지션 둘 다 Conditions 항목에 AttackToMove 파라미터를 추가
- [Has Exit Time] 항목의 체크 표시를 해제
. . . (생략) . . .
void Attack()
{
. . . (생략) . . .
// 그렇지 않다면, 현재 상태를 이동으로 전환한다(재추격 실시).
else
{
m_State = EnemyState.Move;
print("상태 전환: Attack -> Move");
currentTime = 0;
// 이동 애니메이션 플레이
anim.SetTrigger("AttackToMove");
}
}
. . . (생략) . . .
공격 및 재 추격 애니메이션 적용 확인하기
서브 스테이트 머신 추가하기
- 추후에 많은 state들이 추가될 예정이라면 가독성을 높이기 위해 이러한 것들을 하나의 상태 영역으로 묶는게 좋음
- 애니메이터 뷰에서 마우스 오른쪽 버튼을 클릭한 후 [Create Sub-State Machine]을 선택
- 서브 스테이트가 생성되면 이름을 ‘Attack Process’로 변경
- 공격 스테이트와 공격 대기 스테이트를 모두 선택한 채 새로 생성한 서브 스테이트 쪽으로 드래그
- 공격 스테이트와 공격 대기 스테이트가 사라지고, 서브 스테이트 머신이 이동 스테이트와 연결된 것을 확인
애니메이션 이벤트 키 추가
- 좀비가 손을 휘두르기도 전에 플레이어가 먼저 피격당하는 싱크 문제 확인
- 싱크를 맞추기 위해 애니메이션 클립의 타임라인에서 특정한 프레임에 이벤트 실행을 위한 키를 설정 가능
- 하이어라키 뷰에서 에너미를 선택한 채 유니티 에디터 상단의 [Window–Animation–Animation]을 선택하면 애니메이션 뷰가 생성
- Z_Attack 클립을 선택하고 타임라인 쪽의 흰색 선을 마우스로 드래그해 좀비의 공격이 타격하는 시점까지 이동
- 10프레임에 흰색 선을 맞춰 놓은 상태에서 애니메이션 클립 콤보 박스의 우측에 있는 3개의 버튼 중 가장 오른쪽 버튼을 클릭(이벤트 키 추가)
- 이벤트 함수를 만들기 위해 ‘HitEvent’라는 이름으로 새로운 C# 스크립트를 생성
- 생성된 스크립트를 애니메이터 콤포넌트가 있는 좀비 모델링 오브젝트에 드래그 해서 넣어준다.
. . . (생략) . . .
void Attack()
{
// 만일, 플레이어가 공격 범위 이내에 있다면 플레이어를 공격한다.
if (Vector3.Distance(transform.position, player.position) < attackDistance)
{
// 일정한 시간마다 플레이어를 공격한다.
currentTime += Time.deltaTime;
if (currentTime > attackDelay)
{
// player.GetComponent<PlayerMove>().DamageAction(attackPower);
print("공격");
currentTime = 0;
// 공격 애니메이션 플레이
anim.SetTrigger("StartAttack");
}
}
. . . (생략) . . .
}
. . . (생략) . . .
공격시에 바로 공격 애니메이션이 실행되지 않도록 플레이어 스크립트의 데미지 처리 함수를 실행하는 라인을 주석 처리
. . . (생략) . . .
void Attack()
{
. . . (생략) . . .
}
// 플레이어의 스크립트의 데미지 처리 함수를 실행하기
public void AttackAction()
{
player.GetComponent<PlayerMove>().DamageAction(attackPower);
}
. . . (생략) . .
주석 처리된 코드를 HitEvent.cs 스크립트에서 접근할 수 있도록 Attack() 함수 아래에 AttackAction() 함수를 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class HitEvent : MonoBehaviour
{
// 에너미 스크립트 컴포넌트를 사용하기 위한 변수
public EnemyFSM efsm;
// 플레이어에게 데미지를 입히기 위한 이벤트 함수
public void PlayerHit()
{
efsm.AttackAction();
}
}
- 에너미 스크립트에 접근할 수 있도록 public 변수를 선언
- 이벤트 키에서 호출할 수 있도록 PlayerHit( ) 함수를 생성
HitEvent 컴포넌트의 efsm 항목에 Enemy 게임 오브젝트를 드래그해서 넣어 EnemyFSM 컴포넌트를 할당한다.
Z_Attack 클립에 생성했던 이벤트 키를 선택하고 Function 콤보 박스에 PlayerHit( ) 함수를 선택
공격 애니메이션과 이벤트 함수의 싱크 확인하기
애니메이션 트랜지션 호출하기(모든 상태 → 피격)
- [Create State – Empty]를 선택해 새 스테이트를 생성하고 이름을 ‘Damaged’로 변경
- Any State로부터 트랜지션을 생성해 Damaged 스테이트로 연결
- 깃허브에서 ZombieHit.zip 파일을 다운로드 및 임포트
- 인스펙터 뷰의 [Rig] 탭을 보면 Animation Type을 Humanoid로 변경
ZombieHit.anim 파일을 드래그해 피격 스테이트의 Motion 항목에 추가
‘Damaged’라는 이름으로 트리거 파라미터를 생성
Any State는 어떤 애니메이션 상태에서 전환될지 알 수 없기 때문에 Has Exit Time에 체크 표시를 반드시 해제
- Damage 상태 박스에서 Move 상태 박스로 트랜지션을 연결
- [Has Exit Time] 항목에 체크
- 애니메이션이 자동 전환되도록 [Conditions] 항목은 비워둠
. . . (생략) . . .
// 데미지 실행 함수
public void HitEnemy(int hitPower)
{
. . . (생략) . . .
// 에너미의 체력이 0보다 크면 피격 상태로 전환한다.
if (hp > 0)
{
m_State = EnemyState.Damaged;
print("상태 전환: Any state -> Damaged");
// 피격 애니메이션을 플레이한다.
anim.SetTrigger("Damaged");
Damaged();
}
. . . (생략) . . .
}
. . . (생략) . . .
EnemyFSM.cs 스크립트에서 피격 때 실행되는 HitEnemy( ) 함수 하단의 상태 전환 코드 다음 부분에 피격 애니메이션을 호출하는 코드를 추가
- 피격 애니메이션이 너무 길어서 피격 애니메이션이 종료되기도 전에 이동 상태로 전환되는 문제 발생
- Damaged 클립의 [Speed] 항목을 2배로 변경
. . . (생략) . . .
// 데미지 처리용 코루틴 함수
IEnumerator DamageProcess()
{
// 피격 모션 시간만큼 기다린다.
yield return new WaitForSeconds(1.0f);
// 현재 상태를 이동 상태로 전환한다.
m_State = EnemyState.Move;
print("상태 전환: Damaged -> Move");
}
. . . (생략) . . .
DamageProcess( ) 함수의 상태 전환을 위한 대기 시간을 0.5초에서 1초로 변경
애니메이션 트랜지션 호출하기(모든 상태 → 죽음)
- Die라는 이름으로 새로운 상태 박스를 생성
- Any State 상태 박스로부터 Die 상태 박스로 트랜지션을 연결
- Die 상태 박스의 Motion 항목에 Z_FallingForward.anim 파일을 추가
- 호출용 트리거 파라미터를 생성하고 이름을 ‘Die’로 변경
- Any State → Die 트랜지션을 선택한 후 Conditions에 Die 파라미터를 추가
. . . (생략) . . .
// 데미지 실행 함수
public void HitEnemy(int hitPower)
{
. . . (생략) . . .
// 그렇지 않다면, 죽음 상태로 전환한다.
else
{
m_State = EnemyState.Die;
print("상태 전환: Any state -> Die");
// 죽음 애니메이션을 플레이한다.
anim.SetTrigger("Die");
Die();
}
. . . (생략) . . .
}
. . . (생략) . . .
HitEnemy() 함수에서 죽음 상태로 전환하는 코드 하단에 죽음 애니메이션 파라미터를 호출하는 코드를 추가
게임을 플레이하면서 전체적인 에너미의 애니메이션 동작 확인한다.
'SWU_프로젝트 > GURU - unity' 카테고리의 다른 글
guru_unity 해커톤 _ 게임 기획서 제작(1) (0) | 2023.07.20 |
---|---|
GURU_unity_4주차 FPS 게임 제작(6) (0) | 2023.07.15 |
GURU_unity_3주차 FPS 게임 제작(3) (0) | 2023.07.15 |
GURU_unity_3주차 FPS 게임 제작(2) (2) | 2023.07.10 |
GURU_unity_2주차 FPS 게임 제작(1) (2) | 2023.07.07 |