본문 바로가기
game as a service

[서평] 초보자를 위한 유니티 입문

by jessicahan96 2023. 4. 22.

한빛미디어 <나는 리뷰어다> 활동을 위해서 책을 제공받아 작성된 서평입니다.

 

https://www.hanbit.co.kr/store/books/look.php?p_code=B1399693424 

 

초보자를 위한 유니티 입문(개정2판)

유니티 초보자를 위한 최적의 입문서 따라 하다 보면 어느새 나도 게임 개발자!

www.hanbit.co.kr

 

서론

저는 지인 분들께 유니티 책 추천을 부탁받으면 제가 베타 리더를 했었던 <유니티 교과서>라는 책을 입문서로 추천하고, 수학의 정석처럼 두고두고 계속 보면서 참고할 만한 책으로는 개정판 컨텐츠 검수에 참여했었던 <레트로의 유니티 프로그래밍 에센스>라는 책을 추천해주고는 했었는데요 -! 올해는 전공과목에서도 배우고 있고, 절판된 책이긴 하지만 유니티짱이라는 오픈소스를 활용한 프로젝트 실습을 하기 위해 <유니티 5 3D 게임 제작 입문>이라는 책을 중고로 구매하여 학습하고 있었습니다. 이번에 한빛미디어 리뷰 활동 도서 리스트에 있어서 반가운 마음에 유니티 책을 고르긴 했지만 다른 책 및 유사한 프로그래밍 개념과 비교해서 설명해드리는 게 다른 독자 분들께도 도움이 될 것 같아서 그 점을 중심으로 리뷰해보겠습니다 - ! 이 책의 특장점을 꼽자면 우선, 이 책에서 자주 등장하는 병아리 이미지를 통해 유니티를 학습하는데 필요한 기본 개념이나 기능을 설명해 준 점입니다. 병아리 게임 오브젝트의 형태가 단순해서 더 쉽게, 직관적으로 학습 내용을 이해할 수 있었습니다. 다음으로는 코드에 대해 차근차근 설명해준다는 점입니다. 다른 책들도 마찬가지이겠지만 특히 스크립트 설명 때 코드를 먼저 제시한 후 뒤에 코드 블럭을 나누어 설명을 해주는 점이 좋았습니다. 덕분에 코드를 작성하고, 주석으로 관련 설명을 적으며 예제를 실습하였습니다.

본론으로 들어가서...! 리뷰

1장과 2장에서는 유니티를 시작하기 위한 준비 과정, 인터페이스와 사용 방법에 대한 설명을 제시합니다. 저는 그 중 Project-Scene-GameObject-Component 의 하이어라키를 시각화하여 유니티게임의 데이터구조를 설명하거나, 이 책에서 자주 등장하는 병아리 이미지를 통해 유니티를 학습하는데 필요한 기본 개념이나 기능을 설명해 준 점이 만족스러웠습니다. 병아리 게임 오브젝트의 형태가 단순해서, 더 쉽게, 직관적으로 학습 내용을 이해할 수 있었습니다.

 

 

다음으로 3장에서는 본격적인 실습 예제를 통해 유니티를 학습할 수 있습니다. 씬의 방향을 표시하는 기즈모를 다루며 오브젝트의 위치를 확인하거나 메인 카메라의 위치와 각도를 변경하며 유니티를 연습할 수 있습니다. 그리고 유니티에서 기본으로 제공하는 Cube나 Sphere를 활용하여 바닥과 벽, 공을 만들어 볼 수도 있습니다. 무엇보다 이 실습에서 가장 중요한 점은 오브젝트에 중력과 같은 물리동작을 적용하기 위한 리지드바디 컴포넌트를 적용(Attach)시켜 볼 수 있다는 점입니다. 한편으로, 공과 경사면의 충돌 판정을 위한 Collider, 탄성 등 오브젝트 끼리 접촉했을 때 마찰이나 반발이 일어나도록 물리동작을 설정하기 위한 Physic Material도 학습할 수 있습니다. 그러고 나서 Material을 오브젝트에 적용하여 공의 색상, 질감 등 외관을 변경하는 방법 또한 실습할 수 있습니다.

 

 

4장에서는 스프라이트 에디터 를 다루는 법, 웹 프로그래밍을 할 때 사용하는 CSS의 Z index 속성과 같이 오브젝트의 그리기 순서를 레이어 기능을 사용하여 변경할 수 있는 스프라이트 렌더러를 다루는 법을 배웁니다. 3장에서 배웠던 충돌 판정을 위한 콜라이더 중 2D 전용 콜라이더인 Box Collider 2D를 오브젝트에 적용해봅니다. 자동차(Player)와 포탑(CannonMuzzle)을 부모-자식 관계 설정(Parenting)통해 상대적인 위치로 좌표를 이동하는 법도 학습합니다. 그리고 PlayerController.cs 스크립트를 통해 플레이어를 제어합니다. (코드에 대한 설명은 코드 내부에 주석으로 상세히 적어두겠습니다.)

 

// 사용할 라이브러리를 선언합니다
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 클래스를 선언합니다
public class PlayerController : MonoBehaviour
{
    //변수를 선언합니다
    //유니티 스크립트는 함수 밖에서 public으로 선언한 변수가 속성이 되어 인스펙터 창에서 컴포넌트의 항목으로 직접 값을 변경 가능.
    public float speed = 8f;
    public float moveableRange = 5.5f;
    public float power = 1000f;
    public GameObject cannonBall;
    public Transform spawnPoint;
     
    //게임 플레이 중에 반복해서 호출되는 Update 함수 - 매 프레임마다 반복해서 실행되는 함수로, 오브젝트를 움직이는 처리 등 게임에서 반복적으로 실행되는 기능을 작성.
    //참고 - Start 함수는 게임을 실행했을 때 맨 처음 한 번만 실행되는 함수로. 일반적으로 게임 설정 등의 처리를 작성.
    void Update()
    {
        //플레이어를 움직이는 동작 처리
        transform.Translate(Input.GetAxisRaw(
            "Horizontal") * speed * Time.deltaTime, 0, 0);
        //플레이어의 이동 범위를 제한하는 처리
        transform.position = new Vector2(Mathf.Clamp(
            transform.position.x, -moveableRange, moveableRange),
            transform.position.y);

        //if조건문을 통해 키를 획득하면(스페이스 바가 입력되면)함수를 호출
        if (Input.GetKeyDown(KeyCode.Space))
        {
            //포탄을 발사하는 함수
            Shoot();
        }
    }

    void Shoot()
    {
        //인스턴스 생성
        GameObject newBullet =
            Instantiate(cannonBall, spawnPoint.position,
            Quaternion.identity) as GameObject;
        //인스턴스 이동
        newBullet.GetComponent<Rigidbody2D>().AddForce(
            Vector3.up * power);
    }
}

 

이후 이번 장의 가장 중요한 개념이기도 한 프리팹(Prefab)을 이용한 자동 오브젝트 생성법을 배웁니다. 프리팹은 도장을 찍듯이 오브젝트를 복제할 수 있는 유니티의 기능으로, 이를 통해 포탄(CannonBall)의 같은 설정(모양이나 충돌 판정)이 적용된 오브젝트인 인스턴스를 쉽게 복제할 수 있습니다. 인스턴스의 변경을 프리팹에 반영하는 Overrides의 개념도 학습합니다. 그리고 DestroyObj.cs 스크립트를 통해 포탄이 발사된 후 일정시간이 경과하면 사라지게 하는 스크립트를 작성합니다. (코드에 대한 설명은 코드 내부에 주석으로 상세히 적어두겠습니다.)

 

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

public class DestroyObj : MonoBehaviour
{
    //제거될 시간을 지정하는 변수 선언
    public float deleteTime = 2.0f;

    // Start is called before the first frame update
    void Start()
    {
        //유니티에서 제공하는 오브젝트를 제거하기 위한 함수
        Destroy(gameObject, deleteTime);
    }

    // Update is called once per frame
    void Update()
    {

    }
}

 

마지막으로 ChickGenerator.cs 스크립트를 통해 병아리 구슬을 생성할 스크립트를 작성합니다. (코드에 대한 설명은 코드 내부에 주석으로 상세히 적어두겠습니다.)

 

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

public class ChickGenerator : MonoBehaviour
{
    //병아리 구슬 프리팹을 할당할 변수를 선언
    public GameObject obj;
    public float interval = 3.0f;

    // Start is called before the first frame update
    void Start()
    {
        //일정 시간 간격으로 함수를 호출하는 함수
        InvokeRepeating("SpawnObj", 0.1f, interval);
    }

    // Update is called once per frame
    //병아리 구슬을 생성하는 함수
    void SpawnObj()
    {
        Instantiate(obj, transform.position, transform.rotation);
    }
}

 

 

5장에서는 4장에서 제작했던 게임에 점수나 시간 표시, 시작 버튼 등 게임에 필요한 UI(User Interfaces)를 만들어볼 수 있습니다. 다음으로 On Click 이벤트(버튼 클릭시 발생하는 이벤트) ButtonTest.cs 스크립트의 함수가 호출되도록 설정합니다. (코드에 대한 설명은 코드 내부에 주석으로 상세히 적어두겠습니다.)

 

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

public class ButtonTest : MonoBehaviour
{
    // 버튼 클릭 등의 이벤트에서 함수를 호출하기 위해 public으로 선언
    public void TestCall()
    {
        //콘솔 창에 메세지 출력
        Debug.Log("Hello Unity");
    }
}

 

다음으로 StartGame.cs 스크립트를 통 버튼을 클릭하면 지정한 씬으로 이동하는 동작을 처리합니다. (코드에 대한 설명은 코드 내부에 주석으로 상세히 적어두겠습니다.)

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//씬을 이동하기 위해 필요한 라이브러리 선언
using UnityEngine.SceneManagement;

public class StartGame : MonoBehaviour
{
    //버튼에서 호출할 함수 선언
    //외부에서 호출하려면 접근 지정자를 public으로 선언
    public void LoadingNewScene()
    {
        //씬을 호출하는 처리
        SceneManager.LoadScene("Main");
    }
}

 

 

6장에서는 3D 장애물 달리기 게임을 만듭니다. 에셋스토어에서 다운 받은 Standard Assets Package를 임포트한 후 Third PersonController 프리팹을 추가합니다.  오브젝트를 식별하기 위해 설정된 태그를 통해 스테이지의 시작 지점, 목표 지점, 낙하 판정 등에서 플레이어를 판별합니다.  이 프리팹을 쫓아다니는 Tracking Camera를 통해 게임 화면을 움직입니다. 플레이어를 방해하는 장애물을 배치 후, 리지드바디 컴포넌트로 물리적인 동작(상자 밀기)을 반영할지 설정할 수 있습니다. 에셋스토어의 무료 텍스처(Yughues Free Architectural Materials, Sky5X One)를 활용하여 스테이지를 꾸밀 수 있고, Spotlight와 Point Light를 사용하여 조명을 연출할 수 있습니다.

 

Spotlight를 활용하여 생생한 비주얼을 구현하는 방법은 아래의 유니티 공식 블로그 글을 참고하세요 - !

https://blog.unity.com/kr/engine-platform/spotlight-team-best-practices-making-believable-visuals-in-unity

 

Spotlight Team 베스트 프랙티스: Unity에서 생생한 비주얼 구현하기 | Unity Blog

저는 Spotlight Team의 구성원으로서 운 좋게도 매우 흥미로운 여러 프로젝트에 참여할 수 있었습니다. 유니티의 Spotlight Team은 고객과 협력하여 게임 프로젝트를 진행하며, 팀에서 제가 맡은 주요

blog.unity.com

 

이후 낙하 판정을 위해 임의로 Cube를 추가한 후 Mesh Renderer 컴포넌트를 제거하여 화면에 표시하지 않는 법을 학습합니다. 그러고 나서, 특정 영역에 플레이어가 닿으면 플레이어를 시작 지점으로 되돌리고 게임을 재시작하는 기능을 만들어봅니다. 이러한 기능을 구현하려면 스크립트에서 충돌 판정 처리를 작성해야 합니다.

 

일반적으로 오브젝트끼리 접촉했는지 판단할 때는 유니티가 오브젝트에 설정된 콜라이더라는 컴포넌트를 감시하여 자동으로 처리합니다. 플레이어가 바닥 위에 서 있을 수 있는 것도 이러한 기능이 정상적으로 동작하기 때문입니다. 이번 예제에서 만들 판정 영역처럼 자동으로 충돌 판정을 하지 않고 영역에 진입 여부만 판단하고 싶을 때는 콜라이더 컴포넌트의 Is Trigger를 체크합니다. 이렇게 하면 자동으로 처리되던 충돌 판정은 기능하지 않고 충돌 판정 영역에 오브젝트가 접촉했을 때의 처리를 스크립트로 작성할 수 있습니다. 이제 Out.cs 스크립트를 통 낙하 판정 영역에 플레이어가 닿으면 게임을 재시작합니다. (코드에 대한 설명은 코드 내부에 주석으로 상세히 적어두겠습니다.)

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//게임을 재시작하거나 씬과 관련된 동작을 처리할 때 사용하는 라이브러리 선언
using UnityEngine.SceneManagement;

public class Out : MonoBehaviour
{
    //충돌 판정 처리
    //판정 영역에 무엇인가 들어왔을 때 자동으로 실행
    //Player 태그가 설정된 오브젝트가 접촉했ㅇ르 때만 기능 처리
    void OnTriggerEnter(Collider col)
    {
        if (col.gameObject.tag == "Player")
        {
            //리셋처리
            SceneManager.LoadScene(
                SceneManager.GetActiveScene().name);
        }
    }
}

 

장애물 경기에는 목표 지점이 필요하므로, 낙하 판정 영역과 마찬가지로 목표 도달 판정 영역인 GoalArea를 만듭니다. 이후 목표지점에서 2가지 일(1. 목표 영역과 플레이어의 충돌 판정 2. 목표 지점에 도달했는지 판정)을 처리하기 위한 Goal.cs 스크립트를 작성합니다. (코드에 대한 설명은 코드 내부에 주석으로 상세히 적어두겠습니다.)

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

public class Goal : MonoBehaviour
{
    //목표 도달 판정을 위한 변수 선언
    public static bool goal;

    // 게임이 실행되고 제일 먼저 처리해야 할 일을 Start 함수 안에 작성
    // 변수 goal의 값을 false로 설정 -> 목표 지점에 도달하지 않았음을 의미
    void Start()
    {
        goal = false;
    }

    // 목표 지점에 닿았을 때의 처리
    void OnTriggerEnter(Collider col)
    {
        if (col.gameObject.tag == "Player")
        {
            // 변수 goal의 값을 true로 설정 -> 목표 지점에 도달했음을 의미
            goal = true;
        }
    }
}

 

다음으로, 출발하고 나서부터의 경과 시간을 플레이어가 알 수 있도록 화면에 타이머를 표시합니다. 먼저, Canvas 하위에 Text UI를 만듭니다. 다음으로 3가지 기능 ( 1. 시간 측정 2. TimerText의 텍스트 변경 3. 목표 지점에 도달하면 시간 측정 멈추기 ) 을 처리하는 Timer.cs 스크립트를 작성합니다. (코드에 대한 설명은 코드 내부에 주석으로 상세히 적어두겠습니다.)

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// UI 시스템을 이용하기 위해 using으로 Unity Engine.UI 라이브러리를 사용할 것을 선언
using UnityEngine.UI;

public class Timer : MonoBehaviour
{
    //경과 시간을 위한 변수 선언
    public static float time;

    //게임이 시작될 때의 처리: 변수 time을 0으로 초기화
    void Start()
    {
        time = 0;
    }

    // Update is called once per frame
    void Update()
    {
        if (Goal.goal == false)
        {
            //시간을 측정하는 처리
            //변수 time에 Time.deltaTime을 더한다
            //Time.deltaTime은 이전에 처리된 시점과 이번에 처리할 시점 사이에 경과된 시간
            time += Time.deltaTime;
        }
        //Float형 변수인 time의 소수점 이하를 버리고 Int형 값으로 변환
        int t = Mathf.FloorToInt(time);
        
        //TimerText 오브젝트에 설정된 Text 컴포넌트를 얻어서 변수 uiText에 담아둠
        Text uiText = GetComponent<Text>();
        
        // 변수 uiText에 담아둔 Text 컴포넌트의 값(text 속성값)을 변경하여 경과된 시간을 표시
        uiText.text = "Time:" + t;
    }
}

 

목표 지점에 도달하면 결과화면을 표시해야합니다. 결과 화면에는 경과 시간, 최고 기록, 재시작 버튼을 표시합니다. 또한 최고 기록을 위해 데이터를 저장하는 기능을 사용합니다. (결과화면의 텍스트나 버튼은 게임을 실행하고 목표 지점에 도달할 때까지는 Inspecter의 계층 창에서 오브젝트의 체크를 해제하여 화면에 표시하지 않고, 플레이어가 목표지점에 도달했을 때  GameResult.cs 스크립트로 표시될 수 있게 합니다. (코드에 대한 설명은 코드 내부에 주석으로 상세히 적어두겠습니다.)

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// UI 시스템을 이용할 수 있도록 라이브러리 선언
using UnityEngine.UI;
// 씬을 불러오기 위해 씬을 다루기 위한 라이브러리 선언
using UnityEngine.SceneManagement;

public class GameResult : MonoBehaviour
{
    //최고 기록을 담기 위한 변수 선언
    //highscore는 GameResult 스크립트에서만 사용할 것이므로 private로 선언
    private int highScore;
    
    //결과화면 UI 오브젝트의 변수 선언
    //목표 지점 도달시간 텍스트
    public Text resultTime;
    //최고기록 텍스트
    public Text bestTime;
    //결과 화면의 오브젝트를 묶어둔 빈 오브젝트 (그룹의 개념)
    public GameObject resultUI;

    // 게임을 시작할 때 Start 함수에서 최고 기록을 담아두는 highScore 변수에 초깃값을 설정
    void Start()
    {
        //playerPrefebs는 데이터를 저장해주는 클래스
        //playerPrefes.HasKey("HighScore")를 호출하면 저장된 데이터에 HighScore라는 항목이 있는지 판단.
        if (PlayerPrefs.HasKey("HighScore"))
        {
            //존재하면 highScore 변수에 해당 값을 설정함
            highScore = PlayerPrefs.GetInt("HighScore");
        }
        else
        {
            //HighScore가 존재하지 않으면 999로 초기화
            highScore = 999;
        }
    }

    //목표지점에 도달했을 때의 처리
    void Update()
    {
        if (Goal.goal)
        {
            //resultUI 변수에 담아둔 오브젝트를 활성화하며 화면에 표시
            //SetActive는 오브젝트의 활성화/비활성화 전환
            resultUI.SetActive(true);
            
            //타이머 스크립트 Timer의 time 변수의 값을 Int(정수)형으로 변환하여 담기
            //Mathf.FloorToInt 함수를 이용해 소수점 아래의 값은 버리기
            int result = Mathf.FloorToInt(Timer.time);
            
            //목표 도달 시간과 최고 기록을 나타내는 UI 오브젝트 Text 컴포넌트의 text 값에 각각 값 설정하기
            resultTime.text = "ResultTime:" + result;
            bestTime.text = "BestTime:" + highScore;

            //최고 기록 갱신하기
            // SetInt - int(정수)형으로 값을 저장.
            if (highScore > result)
            {
                PlayerPrefs.SetInt("HighScore", result);
            }
        }
    }

    //재시작 버튼 처리
    //재시작 버튼이 눌렸을 때 씬을 다시 불러와서 게임이 재실행되도록 함.
    //함수를 public으로 선언하면 다른 스크립트에서도 사용할 수 있음.
   
    public void OnRetry()
    {
        SceneManager.LoadScene(
            SceneManager.GetActiveScene().name);
    }
}

 

에셋스토어에서 BGM이나 효과음 등의 사운드 데이터를 다운로드할 수 있습니다. 이번 예제에서는 Action RPG Music free를 사용합니다. 스테이지의 목표지점(Goal)에서 소리가 나도록, Audio Sourdce 컴포넌트는 사운드를 발생시킬 오브젝트에 적용(Attatch)합니다.

 

플레이어를 추적하는 카메라에는 Audio Listener가 설정되어 있습니다. Audio Listener는 소리를 담는 마이크와 같은 역할을 합니다. 씬 전체의 소리를 얻기 위해 카메라에 설정하는 경우가 많기 때문에 기본적으로 설정되는 Main Camera에도 Audio Listener가 설정되어 있습니다.

 

카메라에 설정된 AudioListener를 무효화하고 플레이어에 Audio Listener를 추가하면 플레이어의 위치에 맞춰 소리가 변하게 할 수도 있습니다.

 

 

유니티에서는 플랫폼을 아이폰이나 안드로이드폰, 콘솔 게임기기 등으로 선택할 수 있습니다. 저는 아이폰 13프로에 맞춰 화면 비율을 변경하였습니다. 6장에서 키보드 입력으로 플레이어를 조작했다면, 7장에서는 스마트폰에서는 조이스틱(가상의 컨트롤러)를 배치하여 플레이어를 조작할 수 있도록 변경하는 법을 배웁니다.

아이폰 빌드 성공!