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

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

by 독기품은토끼 2025. 7. 25.
✅ 오늘의 학습 목표
1. 수류탄 데미지 적용
2. 옵션 UI 생성
3. 오프닝 화면 구현

1. 수류탄 데미지 적용

저번 수업에서 수류탄 리소스 적용까지 진행했다.

이번에는 수류탄을 던졌을 때, 폭발 범위 내에 있는 적들의 체력이 감소되도록 코드를 수정해 보겠다.

public class BombAction : MonoBehaviour
{
    public int attackPower = 10;
    public float explosinRadius = 5f;

    private void OnCollisionEnter(Collision collision)
    {
        Collider[] cols = Physics.OverlapSphere(transform.position, explosinRadius, 1 << 9);

        for (int i = 0; i < cols.Length; i++)
        {
            cols[i].GetComponent<EnemyFSM>().HitEnemy(attackPower);
        }
    }
}

 

수류탄 특성상 폭발 반경 내에 있는 모든 적 오브젝트가 데미지를 받아야 한다.
이럴 때 사용하는 함수가 바로 Overlap 계열 함수이다.

 

▶ Overlap 함수

OverlapSphere 함수는 특정 지점을 중심으로 하는 반지름 영역 안에 있는 모든 오브젝트의 Collider 컴포넌트를 감지하여 배열로 반환한다.
이 함수는 해당 프레임에 한 번 실행되며, 반환된 배열을 통해 범위 내 오브젝트에 데미지를 적용할 수 있다.

Overlap 함수에는 Sphere(구), Box(박스), Capsule(캡슐) 형태가 있으며, 상황에 따라 사용할 수 있다.

 

🚨 쉬프트 연산자

추가로 코드를 보면 1 << 9 와 같이 쉬프트 연산자가 사용된 것을 볼 수 있다.
이는 유니티의 레이어 시스템이 비트 마스크 방식으로 되어 있기 때문이다.

각 비트는 하나의 레이어를 의미하며, 예를 들어 9번 레이어에 해당하는 오브젝트만 감지하려면 1 << 9로 설정해 해당 비트만 켜진 레이어 마스크를 만든다.
이렇게 하면 폭발 반경 내에서 9번 레이어에 해당하는 적 오브젝트만 데미지를 받게 된다.

 

 

 

2. 옵션 UI 제작

 

우선 톱니바퀴 모양의 버튼을 우측 상단에 만들어주고

옵션창을 위와 같이 만들어주었다.

 

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

    public GameObject gameOption;

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

        yield return new WaitForSeconds(0.5f);
        gameLabel.SetActive(false);
        gState = GameState.Run;
    }

    public void OpenOptionWindow()
    {
        gameOption.SetActive(true);
        Time.timeScale = 0f;
        gState = GameState.Pause;
    }

    public void CloseOptionWindow()
    {
        gameOption.SetActive(false);
        Time.timeScale = 1f;
        gState = GameState.Run;
    }

    public void RestartGame()
    {
        Time.timeScale += 1f;
        SceneManager.LoadScene(1);
    }

    public void QuitGame()
    {
        Application.Quit();
    }
}

 

GameManager 스크립트에 옵션 창에서 사용하는 "계속하기", "다시하기", "게임 종료" 기능이 각각 함수로 구현해 주었다.

  • OpenOptionWindow()는 ESC 키 등으로 옵션 창을 열 때 호출되며(구현 x), Time.timeScale = 0f를 통해 게임을 일시정지 상태로 만들고 GameState를 Pause로 변경한다.
  • CloseOptionWindow()는 "계속하기" 버튼에 연결되어 있으며, 옵션 창을 닫고 Time.timeScale = 1f로 되돌려 게임을 다시 실행 상태로 전환시킨다.
  • RestartGame()은 "다시하기" 버튼에서 사용되며, 씬을 다시 로드해 게임을 처음부터 시작하게 해 준다.
  • QuitGame()은 게임을 완전히 종료하는 기능으로, Application.Quit()을 호출하여 실행 중인 게임을 종료시킨다.

 

 

public class FPSGameManager : Singleton<FPSGameManager>
{
    void Update()
    {
        if (player.hp <= 0)
        {
            player.GetComponentInChildren<Animator>().SetFloat("MoveMotion", 0f);

            gameLabel.SetActive(true);
            gameText.text = "Game Over";
            gameText.color = new Color32(255, 0, 0, 255);

            Transform buttons = gameText.transform.GetChild(0);
            buttons.gameObject.SetActive(true);

            gState = GameState.GameOver;
        }
    }
}

 

게임이 종료되었을 때에도 동일한 작동을 하는 버튼을 만들어주었다.

 

3. 로그인 화면

이번에는 간단한 로그인 기능을 구현해주려고 한다.

🥕 예행 작업
1. Scene 생성 (LoginScene)

 

UI는 위와 같이 만들어주었고,

아이디 입력과 생성, 비밀번호 체크 기능을 통해 로그인 후 게임 메인 씬으로 이동하는 코드를 구현해 주겠다.

using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

public class LoginManager : MonoBehaviour
{
    public TMP_InputField id;
    public TMP_InputField password;

    public TextMeshProUGUI notify;

    void Start()
    {
        notify.text = "";
    }

    public void SaveUserData()
    {
        if (!CheckInput(id.text, password.text))
            return;

        if (!PlayerPrefs.HasKey(id.text)) // 현재 저장된 데이터 중에 동일한 id가 있는지 확인
        {
            PlayerPrefs.SetString(id.text, password.text);
            notify.text = "아이디 생성이 완료되었습니다.";
        }
        else // 입력한 ID가 이미 존재 한다면
            notify.text = "이미 존재하는 아이디입니다.";
    }

    public void CheckUserData()
    {
        if (!CheckInput(id.text, password.text))
            return;

        string pass = PlayerPrefs.GetString(id.text); // 아이디(Key)에 저장된 패스워드(Value)를 가져오는 기능

        if (password.text == pass)
            SceneManager.LoadScene(1);
        else
            notify.text = "입력하신 아이디와 패스워드가 일치하지 않습니다.";
    }

    private bool CheckInput(string id, string pwd) // 입력의 유무를 확인하는 기능
    {
        if (id == "" || pwd == "")
        {
            notify.text = "아이디 또는 패스워드를 입력해주세요.";

            return false;
        }
        else
            return true;
    }
}

 

🚨 PlayerPrefs()

Unity에서는 간단한 데이터 저장에 PlayerPrefs를 사용할 수 있다.
입력한 아이디가 기존에 존재하지 않을 경우 해당 아이디를 Key로, 비밀번호를 Value로 저장한다.

 

 

4. 로딩 화면

로그인 화면에서 게임 메인 씬으로 이동할 때, 씬이 바로 전환되는 대신 로딩 중임을 보여주는 화면을 추가해 주겠다.

 

이때 사용하는 전환 방식은 비동기 씬 전환 (Async Load) 방식이다.

UI는 슬라이더(Slider)를 통해 로딩 진행률을 시각적으로 보여주고 텍스트(TextMeshPro)를 이용해 현재 로딩 퍼센트를 표시하도록 구성했다.

using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

public class LoadingNextScene : MonoBehaviour
{

    public int sceneNumber = 2;
    public Slider loadingBar;
    public TextMeshProUGUI loadingText;

    void Start()
    {
        StartCoroutine(TransitionNextScene(sceneNumber));
    }

    IEnumerator TransitionNextScene(int num)
    {
        AsyncOperation ao = SceneManager.LoadSceneAsync(num);

        ao.allowSceneActivation = false; // 로드가 완료되어도 새로운 씬으로 이동 X

        while (!ao.isDone)
        {
            loadingBar.value = ao.progress;
            loadingText.text = $"{ao.progress * 100f}%";

            if (ao.progress >= 0.9f)
                ao.allowSceneActivation = true;

            yield return null;
        }
    }
}

 

▶ 비동기 방식으로 씬을 로드하는 이유

기존에 많이 쓰는 SceneManager.LoadScene()은 동기 방식이라 새로운 씬이 로드될 때까지 게임이 완전히 멈춘다.
이러면 음악, 애니메이션, UI 등이 중단되어 사용자 입장에서 부자연스럽게 느껴질 수 있다.

반면, LoadSceneAsync()는 백그라운드에서 씬을 로드하면서도 기존 UI나 애니메이션, 사운드가 끊기지 않고 유지되기 때문에
자연스럽고 부드러운 전환 효과를 만들 수 있다.

 

🚨 ao.allowSceneActivation = false

이 구문은 씬이 로드가 완료되어도 자동으로 씬 전환이 되지 않도록 막는 기능이다.

Unity에서는 LoadSceneAsync()를 사용할 경우, 내부적으로 ao.progress가 최대 0.9f까지만 올라가고,
allowSceneActivation이 true로 설정되는 순간 마지막 0.1f를 채우며 씬을 전환한다.

이 값을 false로 설정해 두면, 씬이 메모리에 로딩되더라도 슬라이더 애니메이션이 끝나기 전 또는 사용자가 전환을 원할 때까지 기다릴 수 있다.

즉, 로딩 퍼센트가 100%가 되기 전에 씬이 먼저 넘어가는 걸 막기 위해 로딩 완료 타이밍을 직접 제어할 수 있도록 해주는 중요한 설정이다.

 

 

 

6. 무기 모드 UI

기존에는 키보드의 1번, 2번 키를 누르면 일반 모드와 스나이퍼 모드로 무기가 전환되도록 구현해 뒀었다.

이번에는 여기에 UI 요소를 연동해서 사용자가 모드를 변경했을 때 시각적으로도 확실한 전환 느낌을 받을 수 있도록 개선해 주겠다.

public class FPSPlayerFire : MonoBehaviour
{
    public GameObject weapon01;
    public GameObject weapon02;
    public GameObject weapon01_R;
    public GameObject weapon02_R;

    public GameObject crosshair01;
    public GameObject crosshair02;
    
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Alpha1))
        {
            wMode = WeaponMode.Normal;
            Camera.main.fieldOfView = 60f;
            wModeText.text = "Normal Mode";

            weapon01.SetActive(true);
            weapon02.SetActive(false);
            crosshair01.SetActive(true);
            crosshair02.SetActive(false);
            weapon01_R.SetActive(true);
            weapon02_R.SetActive(false);
        }
        else if (Input.GetKeyDown(KeyCode.Alpha2))
        {
            wMode = WeaponMode.Sniper;
            wModeText.text = "Sniper Mode";

            weapon01.SetActive(false);
            weapon02.SetActive(true);
            crosshair01.SetActive(false);
            crosshair02.SetActive(true);
            weapon01_R.SetActive(false);
            weapon02_R.SetActive(true);
        }
    }
}

 

  • 무기 GameObject 활성화/비활성화
    • 일반 모드 → weapon01만 켜고 weapon02는 끔
    • 스나이퍼 모드 → 반대로 설정
  • 무기 오른쪽 키 전환
    • weapon01_R, weapon02_R로 구분해서 수류탄, 줌 In/Out 표현
  • 크로스헤어 UI 전환
    • 일반 모드일 땐 십자선 형태의 crosshair01 사용
    • 스나이퍼 모드일 땐 조준경 형태의 crosshair02 표시