Carrot
본문 바로가기
Unity/멋쟁이사자처럼 부트캠프

[멋쟁이사자처럼부트캠프] 유니티 게임 개발 5기(45일차) - FPS 게임 (2)

by 독기품은토끼 2025. 7. 21.
✅ 오늘의 학습 목표
1. 3D FPS 게임 구현 (2)

 

1. 적 오브젝트

1. FSM

using UnityEngine;

public class EnemyFSM : MonoBehaviour
{
    private enum EnemyState { Idle, Move, Attack, Return, Damaged, Die }
    private EnemyState m_State;

    private Transform player;
    private CharacterController cc;

    public float findDistance = 8f;
    public float attackDistance = 3f;
    public float moveSpeed = 5f;

    private float currentTime = 0f;
    private float attackDelay = 2f;

    public int attackPower = 3;
    public int hp = 15;

    private Vector3 originPos;
    public float moveDistance = 20f;

    void Start()
    {
        m_State = EnemyState.Idle;
        player = GameObject.Find("Player").transform;
        cc = GetComponent<CharacterController>();
        originPos = transform.position;

        Cursor.visible = false;
        Cursor.lockState = CursorLockMode.Locked;
    }

    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;
        }
    }

    private void Idle()
    {
        if (Vector3.Distance(transform.position, player.position) < findDistance)
        {
            m_State = EnemyState.Move;
            Debug.Log("상태 전환 : Idle -> Move");
        }
    }

    private void Move()
    {
        if (Vector3.Distance(transform.position, originPos) > moveDistance)
        {
            m_State = EnemyState.Return;
            Debug.Log("상태 전환 : Move -> Return");
        }
        else if (Vector3.Distance(transform.position, player.position) > attackDistance)
        {
            Vector3 dir = (player.position - transform.position).normalized; // 플레이어 쪽으로 가는 방향 구하기
            cc.Move(dir * moveSpeed * Time.deltaTime);
        }
        else
        {
            currentTime = attackDelay;
            m_State = EnemyState.Attack;
            Debug.Log("상태 전환 : Move -> Attack");
        }
    }

    private void Attack()
    {
        if (Vector3.Distance(transform.position, player.position) < attackDistance)
        {
            currentTime += Time.deltaTime;
            if (currentTime > attackDelay)
            {
                currentTime = 0f;
                player.GetComponent<FPSPlayerMove>().DamageAction(attackPower);
                Debug.Log("공격!");
            }
        }
        else
        {
            currentTime = 0f;
            m_State = EnemyState.Move;
            Debug.Log("상태 전환 : Attack -> Move");
        }
    }

    private void Return()
    {
        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 = 15;
            m_State = EnemyState.Idle;
            Debug.Log("상태 전환 : Return -> Idle");
        }
    }

    public void HitEnemy(int hitPower)
    {
        if (m_State == EnemyState.Damaged || m_State == EnemyState.Die || m_State == EnemyState.Return)
            return;

        hp -= hitPower;

        if (hp > 0)
        {
            m_State = EnemyState.Damaged;
            Debug.Log("상태 전환 : Any State -> Damaged");
            Damaged();
        }
        else
        {
            m_State = EnemyState.Die;
            Debug.Log("상태 전환 : Any State -> Die");
            Die();
        }
    }

    private void Damaged()
    {
        StartCoroutine(DamageProcess());
    }

    IEnumerator DamageProcess()
    {
        yield return new WaitForSeconds(0.5f);

        m_State = EnemyState.Move;
        Debug.Log("상태 전환 : Damged -> Move");
    }

    private void Die()
    {
        StopAllCoroutines();

        StartCoroutine(DieProcess());
    }

    IEnumerator DieProcess()
    {
        cc.enabled = false;

        yield return new WaitForSeconds(2f);
        Debug.Log("Enemy 소멸");
        Destroy(gameObject);
    }
}
public class FPSPlayerMove : MonoBehaviour
{
    public int hp = 20;

    public void DamageAction(int damage)
    {
        hp -= damage;
    }
}
using UnityEngine;

public class FPSPlayerFire : MonoBehaviour
{
    public int weaponPower = 5;
    
    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            if (Physics.Raycast(ray, out hitInfo))
            {
                if (hitInfo.transform.gameObject.layer == LayerMask.NameToLayer("Enemy")) // Enemy 감지
                {
                    EnemyFSM eFSM = hitInfo.transform.GetComponent<EnemyFSM>();
                    eFSM.HitEnemy(weaponPower);
                }
                else // Enemy가 아닌 경우 피격 이펙트 플레이
                {
                    bulletEffect.transform.position = hitInfo.point; // 피격 이펙트가 부딪힌 대상의 위치로 이동
                    bulletEffect.transform.forward = hitInfo.normal;
                    ps.Play(); // 피격 이펙트 플레이
                }
            }
        }
    }
}

 

2. 체력바

using UnityEngine.UI;

public class FPSPlayerMove : MonoBehaviour
{
    private int maxHp = 20;
    public Slider hpSlider;

    public void DamageAction(int damage)
    {
        hp -= damage;
        hpSlider.value = (float)hp / (float)maxHp;

        if (hp > 0)
        {
            StartCoroutine(PlayHitEffect());
        }
    }
}
using UnityEngine.UI;

public class EnemyFSM : MonoBehaviour
{
    public int maxHp = 15;
    public Slider hpSlider;

    void Update()
    {
        hpSlider.value = (float)hp / (float)maxHp;
    }
}
using UnityEngine;

public class Billboard : MonoBehaviour
{
    public Transform target;

    void Update()
    {
        transform.forward = Camera.main.transform.forward;
    }
}

 

3. 피격 이펙트

using System.Collections;

public class FPSPlayerMove : MonoBehaviour
{
    public GameObject hitEffect;

    public void DamageAction(int damage)
    {
        if (hp > 0)
        {
            StartCoroutine(PlayHitEffect());
        }
    }

    IEnumerator PlayHitEffect()
    {
        Debug.Log("히트이펙트");
        hitEffect.SetActive(true);
        
        yield return new WaitForSeconds(0.3f);
        hitEffect.SetActive(false);
    }
}

 

4. 게임 매니저

using System.Collections;
using TMPro;
using UnityEngine;

public class FPSGameManager : Singleton<FPSGameManager>
{
    public enum GameState { Ready, Run, GameOver }
    public GameState gState;

    public GameObject gameLabel;
    private TextMeshProUGUI gameText;

    private FPSPlayerMove player;

    void Start()
    {
        gState = GameState.Ready;
        gameText = gameLabel.GetComponent<TextMeshProUGUI>();

        gameText.text = "Ready...";
        gameText.color = new Color32(255, 185, 0, 255);

        player = GameObject.Find("Player").GetComponent<FPSPlayerMove>();

        StartCoroutine(ReadyToStart());
    }

    void Update()
    {
        if (player.hp <= 0)
        {
            gameLabel.SetActive(true);
            gameText.text = "Game Over";
            gameText.color = new Color32(255, 0, 0, 255);

            gState = GameState.GameOver;
        }
    }

    IEnumerator ReadyToStart()
    {
        yield return new WaitForSeconds(2f);
        gameText.text = "Go!";

        yield return new WaitForSeconds(0.5f);
        gameLabel.SetActive(false);
        gState = GameState.Run;
    }
}
public class FPSPlayerMove : MonoBehaviour
{
    void Update()
    {
        if (FPSGameManager.Instance.gState != FPSGameManager.GameState.Run)
            return;
    }
}

// PlayerFire, PlayerRotate, CamRotate에도 조건문 추가해주기

 

 

2. Asset 적용

🥕 예행 작업
1. Asset 다운로드
 

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

 

1. 애니메이션

public class EnemyFSM : MonoBehaviour
{
    private Animator anim;

    private Vector3 originPos;
    private Quaternion originRot;

    void Start()
    {
        originPos = transform.position;
        originRot = transform.rotation;

        anim = transform.GetComponentInChildren<Animator>();
    }

    private void Idle()
    {
        if (Vector3.Distance(transform.position, player.position) < findDistance)
        {
            anim.SetTrigger("IdleToMove");
            m_State = EnemyState.Move;
            Debug.Log("상태 전환 : Idle -> Move");
        }
    }
    
        private void Move()
    {
        if (Vector3.Distance(transform.position, originPos) > moveDistance)
        {
            m_State = EnemyState.Return;
            Debug.Log("상태 전환 : Move -> Return");
        }
        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
        {
            currentTime = attackDelay;
            m_State = EnemyState.Attack;
            Debug.Log("상태 전환 : Move -> Attack");
        }
    }

    private 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;

            hp = 15;
            m_State = EnemyState.Idle;
            Debug.Log("상태 전환 : Return -> Idle");

            anim.SetTrigger("MoveToIdle");
        }
    }
}

 

2. 결과