본문 바로가기
게임 프로그래밍/게임개발 중급

게임개발 중급(31) - 3D 게임 만들기(총정리)

by jyppro 2023. 5. 22.

3D 게임 만들기(총정리)

예제로서 필요한 기본기능에 대한 설명은 모두 마친것 같으니 여태 했던 내용을 총정리하겠습니다. 최종적으로 만들어진 내용물만 포함되어 있습니다.

 

하이어라키 창과 프로젝트 창

하이어라키-창과-프로젝트-창
제작에 사용된 오브젝트 및 폴더

 

ACAUTIL, FreeButtonSet, OArielG 폴더는 UI에 사용된 에셋폴더들입니다.

 

Material

머티리얼
색상에 사용된 머티리얼

오브젝트의 구분을 위해 색상을 적용시킨 머티리얼 입니다.

 

Prefabs

프리팹
사용된 프리팹

이번에 만든 프리팹은 아이템 하나밖에 없습니다.

 

Scripts

스크립트
사용된 스크립트들

이번에는 총 5개의 스크립트가 사용되었습니다. 생각보다 많이 사용되지는 않은 것 같습니다. 스크립트를 하나하나씩 살펴보겠습니다.

 

PlayerController

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

public class PlayerController : MonoBehaviour
{
    private bool isMovingForward = false;
    private bool isMovingBackward = false;
    private bool isMovingLeft = false;
    private bool isMovingRight = false;
    Rigidbody rigid;
    float jumpForce = 5.0f;
    float walkForce = 5.0f;
    float forwardForce = 1.0f;
    int key = 0;
    public AudioSource AS;
    public ParticleSystem PS;

    void Start()
    {
        this.rigid = GetComponent<Rigidbody>();
        AS = GetComponent<AudioSource>();
        PS = GetComponent<ParticleSystem>();
    }

    void Update()
    {
        //점프
        if(Input.GetKeyDown(KeyCode.Space) && this.rigid.velocity.y == 0f){
            rigid.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
        }

        //F키, B키로 움직이기
        if(Input.GetKey(KeyCode.F)) // 앞
        {
            key = 1;
            transform.Translate(0f,0f,forwardForce*key*Time.deltaTime);
        }
        else if(Input.GetKey(KeyCode.B)) // 뒤
        {
            key = -1;
            transform.Translate(0f,0f,forwardForce*key*Time.deltaTime);
        }

        //방향키로 움직이기
        if(Input.GetKey(KeyCode.RightArrow)){ // 오른쪽
            key = 1;
            this.rigid.AddForce(transform.right * key * this.walkForce);
        }
        if(Input.GetKey(KeyCode.LeftArrow)){ // 왼쪽
            key = -1;
            this.rigid.AddForce(transform.right * key * this.walkForce);
        }
        if(Input.GetKey(KeyCode.UpArrow)){ // 앞
            key = 1;
            this.rigid.AddForce(transform.forward * key * this.walkForce);
        }
        if(Input.GetKey(KeyCode.DownArrow)){ // 뒤
            key = -1;
            this.rigid.AddForce(transform.forward * key * this.walkForce);
        }

        //4방향 UI로 움직이기
        if (isMovingForward)
        {
            MoveForward();
        }
        else if (isMovingBackward)
        {
            MoveBackward();
        }
        else if (isMovingLeft)
        {
            MoveLeft();
        }
        else if (isMovingRight)
        {
            MoveRight();
        }
    }

    //아이템 먹으면 점수 올라가는 기능
    void OnTriggerEnter(Collider other)
    {
        if(other.CompareTag("Item"))
        {
        AS.Play();
        PS.Play();
        // 감독 스크립트에 플레이어와 아이템이 충돌했다고 전달한다 
        GameObject director = GameObject.Find("GameDirector");
        director.GetComponent<GameDirector>().DecreaseHp();
        director.GetComponent<GameDirector>().StageChange();
        director.GetComponent<GameDirector>().ScoreUp();
        other.gameObject.SetActive(false);
        GetComponent<ItemManager>().RespawnItem(other.gameObject);
        }
    }

    public void ForwardButtonDown()
    {
        isMovingForward = true;
    }

    public void ForwardButtonUp()
    {
        isMovingForward = false;
    }

    public void BackwardButtonDown()
    {
        isMovingBackward = true;
    }

    public void BackwardButtonUp()
    {
        isMovingBackward = false;
    }

    public void LeftButtonDown()
    {
        isMovingLeft = true;
    }

    public void LeftButtonUp()
    {
        isMovingLeft = false;
    }

    public void RightButtonDown()
    {
        isMovingRight = true;
    }

    public void RightButtonUp()
    {
        isMovingRight = false;
    }

    private void MoveForward()
    {
        key = 1;
        transform.Translate(0f,0f,walkForce*key*Time.deltaTime);
    }

    private void MoveBackward()
    {
        key = -1;
        transform.Translate(0f,0f,walkForce*key*Time.deltaTime);
    }

    private void MoveLeft()
    {
        key = -1;
        transform.Translate(walkForce*key*Time.deltaTime,0f,0f);
    }

    private void MoveRight()
    {
        key = 1;
        transform.Translate(walkForce*key*Time.deltaTime,0f,0f);
    }
}

모든 스크립트의 기본이 되는 플레이어 컨트롤러 입니다. 충돌처리를 위한 콜라이더와 리지드 바디, 그리고 소리와 파티클을 만들어 주는 오디오 소스, 파티클 시스템이 연결되어 있고, 방향키를 이용한 움직임, 특정 키를 사용한 움직임, 점프, UI를 사용한 움직임 등 여러가지 움직임을 만들었습니다. UI컨트롤은 다른 스크립트로 빼서 연결시켜주었습니다.

 

GameDirector

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; // UI를 사용하므로 잊지 않고 추가한다

public class GameDirector : MonoBehaviour
{
    GameObject GaugeUI;
    GameObject StageUI;
    GameObject ScoreBoardUI;
    GameObject RestartBtnUI;
    int stageLevel = 1;
    int score = 0;

    void Start()
    {
        this.GaugeUI = GameObject.Find("Gauge");
        this.StageUI = GameObject.Find("Stage");
        this.ScoreBoardUI = GameObject.Find("Score");
        this.RestartBtnUI = GameObject.Find("RestartButton");
        this.RestartBtnUI.SetActive(false);
    }

    public void DecreaseHp()
    {
        if(this.stageLevel == 1)
        {
            this.GaugeUI.GetComponent<Image>().fillAmount -= 0.1f; // 10개먹으면 다음 스테이지
        }
        else if(this.stageLevel == 2)
        {
            this.GaugeUI.GetComponent<Image>().fillAmount -= 0.05f; // 20개 먹으면 다음 스테이지
        }
        else
        {
            this.GaugeUI.GetComponent<Image>().fillAmount -= 0.025f; // 40개 먹으면 게임 클리어
        }
        
    }

    public void StageChange()
    {
        if(this.GaugeUI.GetComponent<Image>().fillAmount == 0f)
        {
            if(this.stageLevel >= 1)
            {
                this.stageLevel++;
                this.GaugeUI.GetComponent<Image>().fillAmount = 1f;
                this.StageUI.GetComponent<Text>().text = "Stage : " + stageLevel.ToString();
            }
            
            if (this.stageLevel == 4)
            {
                this.RestartBtnUI.SetActive(true);
                this.StageUI.GetComponent<Text>().text = "Game Clear!";
                this.StageUI.GetComponent<Text>().color = Color.green;
                Time.timeScale = 0f;
            }
        }
    }

    public void ScoreUp()
    {
        score += 10;
        if(score >= 0)
        {
            this.ScoreBoardUI.GetComponent<Text>().text = "Score: " + score.ToString();
        }
    }

    public void ResetBtn()
    {
        this.stageLevel = 1;
        this.score = 0;
        this.StageUI.GetComponent<Text>().text = "Stage : " + stageLevel.ToString();
        this.StageUI.GetComponent<Text>().color = Color.black;
        this.ScoreBoardUI.GetComponent<Text>().text = "Score: " + score.ToString();
        this.GaugeUI.GetComponent<Image>().fillAmount = 1f;
        this.RestartBtnUI.SetActive(false);
        Time.timeScale = 1f;
    }
}

게임의 전체적인 시스템을 관리하는 디렉터입니다. 점수, 게이지, 스테이지, 재시작 버튼 등 모든 UI를 한번에 관리하는 시스템 관리자 개념입니다.

 

ItemManager

using UnityEngine;

public class ItemManager : MonoBehaviour
{
    public GameObject itemPrefab;
    public int itemCount = 10;

    private void Start()
    {
        SpawnItems();
    }

    private void SpawnItems()
    {
        for (int i = 0; i < itemCount; i++)
        {
            Vector3 spawnPosition = GetRandomSpawnPosition();
            Instantiate(itemPrefab, spawnPosition, Quaternion.identity);
        }
    }

    public void RespawnItem(GameObject item)
    {
        StartCoroutine(RespawnAfterDelay(item));
    }

    private System.Collections.IEnumerator RespawnAfterDelay(GameObject item)
    {
        yield return new WaitForSeconds(3f);
        Vector3 spawnPosition = GetRandomSpawnPosition();
        item.transform.position = spawnPosition;
        item.SetActive(true);
    }

    private Vector3 GetRandomSpawnPosition()
    {
        // 아이템이 생성될 위치를 랜덤하게 결정합니다.
        // 원하는 위치 범위에 맞게 수정해주세요.
        float x = Random.Range(-3f, 3f);
        float y = Random.Range(0f, 2f);
        float z = Random.Range(-3f, 3f);

        return new Vector3(x, y, z);
    }
}

예제게임을 진행하는 데 꼭 필요한 아이템을 컨트롤 하는 아이템 매니저 입니다. 게임이 시작할 때 아이템을 랜덤으로 생성하는 스폰과, 플레이어가 아이템을 먹으면 일정시간이 지난 후 랜덤위치에 아이템이 다시 생성되는 리스폰의 역할을 하고 있는 스크립트 입니다.

 

PlayerBoundary

using UnityEngine;

public class PlayerBoundary : MonoBehaviour
{
    private Vector3 originalPosition;

    private void Start()
    {
        originalPosition = transform.position;
    }

    private void OnTriggerExit(Collider other)
    {
        if (other.CompareTag("Boundary"))
        {
            // 경계를 벗어난 경우 원래 위치로 돌아옴
            transform.position = originalPosition;
        }
    }
}

플레이어가 주어진 맵을 벗어났을 때, 원위치로 복귀시켜주는 스크립트 입니다. 해당 작업을 해주지 않으면 플레이어가 게임을 진행하던 중 맵을 벗어나거나 이상한 공간에 빠지는 일이 벌어졌을 때, 더 이상 게임을 진행할 수 없게 됩니다.

 

Restart

using UnityEngine;
using UnityEngine.SceneManagement;

public class Restart : MonoBehaviour
{
    public void OnRestartButtonClick()
    {
        GameObject director = GameObject.Find("GameDirector");
        director.GetComponent<GameDirector>().ResetBtn();
    }
}

재시작버튼에 OnClick()으로 연결해 주는 스크립트입니다. UI를 관리하는 게임 디렉터에 정의된 함수를 사용하여 모든 수치를 초기화 시켜 게임을 처음상태로 돌려줍니다.

 

게임 플레이

게임을 시작하면, 10개의 아이템이 맵 안에서 랜덤하게 생성되고, 해당 아이템을 플레이어가 움직여서 먹을 수 있습니다. 아이템을 먹으면 점수가 10점씩 올라가며, 스테이지 별로 정해진 수치만큼 먹어야 다음 스테이지로 넘어갈 수 있습니다. 게이지를 통해 다음 스테이지까지 얼마나 남았는지 확인이 가능합니다. 스테이지는 총 3스테이지까지 구성이 되어있으며, 3스테이지를 클리어하고 4스테이지에 도달하면 게임을 클리어하게 됩니다. 게임은 멈추게 되고, 재시작 버튼이 등장합니다. 재시작 버튼을 누르면 모든 상태가 초기화되어 게임을 처음 플레이하는 것과 같은 상태가 됩니다.

 

게임화면

게임플레이-화면
게임플레이 화면

게임을 플레이하는 화면입니다. (파란색 원통 : 플레이어, 원 : 아이템, 바닥과 벽 : 맵, 방향UI, 점수, 스테이지, 게이지UI)

게임 플레이 예시를 위해 스테이지별 수치를 10개씩으로 조정하였습니다. 28개의 아이템을 먹어 3스테이지에서 2개의 아이템만 먹으면 게임을 클리어하게 됩니다.

 

<NEXT>

이제 예제로서의 게임은 2D와 3D를 전부 다루었습니다. 모든 게임에서 사용될 만한 기본적인 방식에 대해서 주로 다뤘습니다. 움직임이나 아이템, 충돌 등 게임이라면 필수적으로 들어갈만한 내용입니다. 이제부터는 조금 더 응용할 수 있는 내용에 대해서 다루어볼까 합니다. 다음에도 3D 게임으로 찾아뵙겠습니다. 감사합니다.