[SKKU DT] 13일차 -FPS 게임 만들기(6) -좀비 모델링 적용, 애니메이션 적용, 로비 씬

2023. 11. 15. 18:24SKKU DT

728x90
반응형

https://assetstore.unity.com/packages/3d/characters/humanoids/zombie-30232

 

Zombie | 3D 휴머노이드 | Unity Asset Store

Elevate your workflow with the Zombie asset from Pxltiger. Find this & other 휴머노이드 options on the Unity Asset Store.

assetstore.unity.com

무료 좀비 에셋 다운로드

 

 


 

 

좀비 프리팹을 Enemy 오브젝트의 Children으로 가져다 놓는다.

 

 


 

 

좀비의 애니메이션을 적용 (Idle에서 Move로)

Animator_Controller 생성

 

 

New State를 하나 만들어서 "Idle"로 이름을 바꾸고 [Motion] 자리에 좀비의 애니메이션 "Z_Idle"을 드래그앤 드랍한다.

 

 

제자리에서 움직이는 Z_Run_InPlace를 새로 만든 Move에 넣는다. 트랜지션 화살표 잇기

 

 

트리거 파라미터 생성(IdleToMove)

 

 

Idle -> Move 화살표 클릭하면 Inspector 창에서 [Conditions]에 조건을 넣을 수 있다.

 

 

호출했을 때 애니메이션이 바로 나오려면 [Has Exit Time]을 체크 해제하면 된다.

 


 

 

EnemyFSM 스크립트에서 자식의 컴포넌트를 가져오게 한다. Zombie1이 Enemy 안에 있다.

애니메이터 변수 생성

Animator Anim;

 

Start 함수에서 자식 오브젝트 컴포넌트 가져오기

anim = transform.GetComponentInChildren<Animator>();

 

EnemyFSM 스크립트의 Idle 함수에서 변수의 트리거 접근해서 바꿔주기

void Idle()
{
    //만약 플레이어와 적의 거리가 발견 범위 이내라면 Move 상태로 전환
    if(Vector3.Distance(transform.position, player.position) < findDistance)
    {
        m_State = EnemyState.Move;
        Debug.Log("발견! Idle -> Move");
        //이동 애니메이션을 전환
        anim.SetTrigger("IdleToMove");
    }
}

 

 


 

 

좀비가 나를 바라보며 다가오게 하기

EnemyFSM 스크립트에서 Move 함수 수정 -플레이어를 향해서 방향을 전환하게 수정(플레이어 - 적)

void Move()
{
    //만약 현재 위치와 초기 위치 거리가 이동 가능 범위보다 크면 복귀
    if(Vector3.Distance(transform.position, originPos) > moveDistance)
    {
        m_State = EnemyState.Return;
        Debug.Log("상태 전환");
    }
    //만약 플레이어와 적의 거리가 공격 범위보다 크다면 플레이어를 향해 이동
    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;
    }
    else
    {
        m_State = EnemyState.Attack;
        Debug.Log("공격!");
        //공격이 바로 안되므로 공격 딜레이 시간 만큼 미리 진행을 시켜놓아야 한다.
        currentTime = attackDelay;
    }
}

 

 


 

 

좀비의 애니메이션을 적용 (Move에서 Idle로) (Return)

MoveToIdle 트리거 생성, Move -> Idle 트랜지션 생성, Condition 부여, Has Exit Time 체크 해제

 

 

EnemyFSM 스크립트에서 Return 함수 수정, 초기 위치 방향으로 이동하게 수정

void Return()
{
    //오리지널 방향으로 돌아가기 특정 좌표값보다 범위로 나타내는게 에러가 덜 날 가능성이 높다.
    if(Vector3.Distance(transform.position, originPos) > 0.1f)
    {
        //방향 구하기 (목적지 - 내 위치)
        Vector3 dir = (originPos - transform.position).normalized;
        //이동하기
        cc.Move(dir * moveSpeed * Time.deltaTime);
        //복귀 지점 방향으로 전환
        transform.forward = dir;
    }
    else  //그렇지 않다면 적 위치를 초기 위치로 조정하고, 대기 상태로 전환한다.
    {
        transform.position = originPos;
        m_State = EnemyState.Idle;
        Debug.Log("상태 전환: Return -> Idle");
        //대기 애니메이션으로 전환
        anim.SetTrigger("MoveToIdle");
    }
}

돌아가면 기울어져서 멈춘다.

 

EnemyFSM에 회전 변수 추가

Quaternion originRot;

 

Start 함수에 초기 위치에 회전 추가 originRot

void Start()
{
    //플레이어 Transform Component 할당
    player = GameObject.Find("Player").transform;
    //최초의 적 상태를 대기로 설정
    m_State = EnemyState.Idle;
    //적 캐릭터 컨트롤러 컴포넌트 할당
    cc = GetComponent<CharacterController>();
    //적의 초기 위치 저장
    originPos = transform.position;
    originRot = transform.rotation;
    //나의 자식 오브젝트 컴포넌트를 가져와라 컴포넌트 할당
    anim = transform.GetComponentInChildren<Animator>();
}

 

Return함수 수정

void Return()
{
    //오리지널 방향으로 돌아가기 특정 좌표값보다 범위로 나타내는게 에러가 덜 날 가능성이 높다.
    if(Vector3.Distance(transform.position, originPos) > 0.1f)
    {
        //방향 구하기 (목적지 - 내 위치)
        Vector3 dir = (originPos - transform.position).normalized;
        //이동하기
        cc.Move(dir * moveSpeed * Time.deltaTime);
        //복귀 지점 방향으로 전환
        transform.forward = dir;
    }
    else  //그렇지 않다면 적 위치를 초기 위치로 조정하고, 대기 상태로 전환한다.
    {
        transform.position = originPos;
        transform.rotation = originRot;
        m_State = EnemyState.Idle;
        Debug.Log("상태 전환: Return -> Idle");
        //대기 애니메이션으로 전환
        anim.SetTrigger("MoveToIdle");
    }
}

 

 


 

 

좀비의 애니메이션을 적용 (Attack, Attack 대기)

Attack, Attack_Delay 스테이트 생성, 각각 Z_Attack, Z_Idle 애니메이션 넣기

 

 

Move -> Attack_Delay 트랜지션 연결, MoveToAttackDelay 조건 추가

 

EnemyFsm 스크립트의 Move 함수 수정

void Move()
{
    //만약 현재 위치와 초기 위치 거리가 이동 가능 범위보다 크면 복귀
    if(Vector3.Distance(transform.position, originPos) > moveDistance)
    {
        m_State = EnemyState.Return;
        Debug.Log("상태 전환");
    }
    //만약 플레이어와 적의 거리가 공격 범위보다 크다면 플레이어를 향해 이동
    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;
    }
    else
    {
        m_State = EnemyState.Attack;
        Debug.Log("공격!");
        //공격이 바로 안되므로 공격 딜레이 시간 만큼 미리 진행을 시켜놓아야 한다.
        currentTime = attackDelay;
        //공격 대기 애니메이션 실행
        anim.SetTrigger("MoveToAttackDelay");
    }
}

 

 

AttackDelay - > Attack 트랜지션에 StartAttack 트리거 추가, 조건 추가, Has Exit Time 체크 해제

EnemyFsm 스크립트의 Attack 함수 수정(anim.SetTrigger("StartAttack");)

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");
        }
    }
    //그렇지 않다면, Move();
    else
    {
        m_State = EnemyState.Move;
        Debug.Log("이동");
        currentTime = 0;
    }
}

**Attack -> AttackDelay는 별다는 트랜지션, 조건, 코드 수정 없이 돌아오기 때문에 건드릴 필요 없다.

 

 

Attack - [Loop Time] 끄기

 

 

Attack -> Move, Attack_Delay -> Move로 돌아오기

AttackToMove 파라미터 생성, AttackToMove 조건 추가, Has Exit Time 체크 해제

Attack_Delay -> Move도 마찬가지

 

EnemyFsm의 Attack 함수 수정(anim.SetTrigger("AttackToMove");)

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");
        }
    }
    //그렇지 않다면, Move();
    else
    {
        m_State = EnemyState.Move;
        Debug.Log("이동");
        currentTime = 0;
        //이동 애니메이션 실행
        anim.SetTrigger("AttackToMove");
    }
}

 

 


 

 

버그 수정(EnemyFSM 스크립트에서 DamageProcess() 코루틴 수정)

IEnumerator DamageProcess()
{
    //피격 모션만큼 기다림 애니메이션 고려
    yield return new WaitForSeconds(0.5f);
    //이동 상태 전환
    m_State = EnemyState.Move;
    //이동 애니메이션
    anim.SetTrigger("IdleToMove");
}

 

 

 


 

 

좀비의 애니메이션을 적용 (Die)

AnyState의 트랜지션을 Die와 연결, Die 파라미터 추가, Die 조건 추가

 

HitEnemy 함수 변경(anim.SetTrigger("Die");)

public void HitEnemy(int hitPower)
{
    //적이 피격 상태이면 플레이어를 때릴 수 없게
    if(m_State == EnemyState.Damaged)
    {
        return;
    }
    //플레이어의 공격력만큼 적 체력 감소
    hp -= hitPower;
    //적 체력이 0보다 크면 피격
    if(hp > 0)
    {
        m_State = EnemyState.Damaged;
        Damaged();  //코루틴 호출
        Debug.Log("적 체력 표시: " + hp);
    }
    //0보다 작으면 죽음
    else
    {
        m_State = EnemyState.Die;
        Debug.Log("죽음");
        //죽음 애니메이션
        anim.SetTrigger("Die");
        Die();
    }
    IEnumerator DieProcess()
    {
        //적 캐릭터 컨트롤러 컴포넌트 비활성화
        cc.enabled = false;
        //2초 대기
        yield return new WaitForSeconds(3f);
        //적 제거
        Destroy(gameObject);
    }
    void Die()
    {
        //실행 중인 이전 Coroutine 중지
        StopAllCoroutines();
        //죽음 Coroutine 실행
        StartCoroutine(DieProcess());
    }
}

 

 


 

 

Scene 전환하기

로비 씬 만들기

 

 

이미지와 버튼 추가

 

 

SceneChange 스크립트 생성 -namespace 사용

using UnityEngine.SceneManagement;

 

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneChange : MonoBehaviour
{ 
    }
    public void LoadBattleScene()
    {
        //파일 이름으로 씬 불러오기
        SceneManager.LoadScene("MainScene");
    }
}

 

 

EventSystem에 스크립트를 넣는다.

 

 

Button에 매핑한다.

 

 

[File] - [Build Settings]에서 씬을 추가해야 한다.

**맨 먼저 실행할 씬 부터 배치해야 한다.

 

 


 

 

ESC 버튼 만들기

UI 이미지 만들기

 

 

GameManager 스크립트에서 UI 패널 가져올 코드 만들기

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class GameManager : MonoBehaviour
{
    //게임 매니저 가져와서 gm 변수 설정
    public static GameManager gm;

    private void Awake()
    {
        if(gm == null)
        {
            gm = this;  //없으면 싱글톤 할당을 해서 적용한다.
        }
    }
    //게임 상태 상수. Ready라는 상태를 모든 스크립트가 알아야함
    public enum GameState
    {
        Ready,
        Go,
        Pause,
        GameOver
    }
    //GameState 변수 선언. gState에 따라서 효과 적용
    public GameState gState;
    //UI 오브젝트 변수 생성
    public GameObject gameLabel;
    //게임 상태 텍스트 가져오기. 컴포넌트 변수는 Start에서 할당을 해주어야 함.
    Text gameText;
    //UI 패널 가져오기
    public GameObject gameOption;
    void Start()
    {
        //초기 상태를 준비로 설정
        gState = GameState.Ready;
        gameText = gameLabel.GetComponent<Text>();
        gameText.text = "Ready";
        StartCoroutine(ReadyToStart());
    }
    void Update()
    {

    }
    IEnumerator ReadyToStart()
    {
        //2초 대기
        yield return new WaitForSeconds(2f);
        //Go
        gameText.text = "Go!";
        //대기
        yield return new WaitForSeconds(0.5f);
        //텍스트 비활성화
        gameLabel.SetActive(false);
        //상태 변경
        gState = GameState.Go;
    }
    public void OpenOption()
    {
        gameOption.SetActive(true);
        Time.timeScale = 0;
        gState = GameState.Pause;
    }
    public void CloseOption()
    {
        gameOption.SetActive(false);
        Time.timeScale = 1.0f;
        gState = GameState.Go;
    }
}

 

 

UI 이미지 만든 후 ButtonManager 스크립트 작성

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class ButtonManager : MonoBehaviour
{
    public void RestartGame()
    {
        Time.timeScale = 1.0f;
        //현재 열려있는 씬을 로드해라
        SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
    }
    public void QuitGame()
    {
        Application.Quit();
    }
}

 

 

버튼 달아준 후 GameManager와 ButtonManager로 함수 만들기

 

 

GameManager에 OpenOption, CloseOption 함수 추가

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class GameManager : MonoBehaviour
{
    //게임 매니저 가져와서 gm 변수 설정
    public static GameManager gm;

    private void Awake()
    {
        if(gm == null)
        {
            gm = this;  //없으면 싱글톤 할당을 해서 적용한다.
        }
    }
    //게임 상태 상수. Ready라는 상태를 모든 스크립트가 알아야함
    public enum GameState
    {
        Ready,
        Go,
        Pause,
        GameOver
    }
    //GameState 변수 선언. gState에 따라서 효과 적용
    public GameState gState;
    //UI 오브젝트 변수 생성
    public GameObject gameLabel;
    //게임 상태 텍스트 가져오기. 컴포넌트 변수는 Start에서 할당을 해주어야 함.
    Text gameText;
    //UI 패널 가져오기
    public GameObject gameOption;
    void Start()
    {
        //초기 상태를 준비로 설정
        gState = GameState.Ready;
        gameText = gameLabel.GetComponent<Text>();
        gameText.text = "Ready";
        StartCoroutine(ReadyToStart());
    }
    IEnumerator ReadyToStart()
    {
        //2초 대기
        yield return new WaitForSeconds(2f);
        //Go
        gameText.text = "Go!";
        //대기
        yield return new WaitForSeconds(0.5f);
        //텍스트 비활성화
        gameLabel.SetActive(false);
        //상태 변경
        gState = GameState.Go;
    }
    public void OpenOption()
    {
        gameOption.SetActive(true);
        Time.timeScale = 0;
        gState = GameState.Pause;
    }
    public void CloseOption()
    {
        gameOption.SetActive(false);
        Time.timeScale = 1.0f;
        gState = GameState.Go;
    }
}

 

ButtonManager 스크립트에 RestartGame, QuitGame 함수 추가

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class ButtonManager : MonoBehaviour
{
    public void RestartGame()
    {
        Time.timeScale = 1.0f;
        //현재 열려있는 씬을 로드해라
        SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
    }
    public void QuitGame()
    {
        Application.Quit();
    }
}

 

 


 

 

728x90
반응형