0408 - Unity 부트캠프 TIL [2일차] 유니티 기초 강의 3~4주차

Hyeon O·2025년 4월 8일

Unity_BootCamp 1주차

목록 보기
2/5
post-thumbnail

📚 오늘의 학습

  • Unity 게임 개발 3 주차 강의: [고양이 밥 주기 게임 구현]
  • Unity 게임 개발 4 주차 강의: [카드 뒤집기 게임 구현]

어제와 동일하게, 새롭거나 다시 떠올린 개념을 정리해보고자 한다.

  1. Destorty() 함수에 대해서 → 파라미터의 사용을 중심으로
  2. 게임 오브젝트를 삭제하는 방법론
  3. ToString() 함수에 대해서 → 파라미터의 사용을 중심으로
  4. Application.targetFrameRate = 60;
  5. 레벨 시스템 구현에 관하여
  6. 카드를 배치하는데 있어 구현 방법
  7. arr.OrderBy(x => Random.Range(0f,7f)).ToArray();
  8. 싱글 톤에 대한 정리

🧠 오늘 새롭게 배운 것 & 다시 떠올린 개념들

1. Destroy()

Unity에서 주로 오브젝트를 삭제하는 함수로, 게임 오브젝트나 컴포넌트를 메모리에서 제거할 때 사용되는 함수이다.

Destroy(gameObject); // 현재 게임 오브젝트 삭제
Destroy(this); // 현재 컴포넌트 삭제
Destroy(Object, time); // 일정 시간 후 삭제

첫 줄에 파라미터가 gameObject 인 경우,

현재 스크립트가 붙어있는 게임 오브젝트 전체를 제거한다.

즉, 모든 컴포넌트, 자식 오브젝트도 함께 사라진다.

파라미터가 this인 경우,

스크립트가 붙어 있는 컴포넌트 자신만 삭제한다.

즉, 게임 오브젝트는 살아있다.

어떤 조건을 만족하면 이 스크립트 컴포넌트만 1회성 동작 후 제거하고 싶을 때 사용한다.

UI 요소에서 임시 애니메이션을 담당하는 컴포넌트가 더 이상 필요 없을 경우에 제거하는 방식이다.

여기서 this 는 해당 스크립트 인스턴스(=해당 클래스 인스턴스)를 의미하므로,

스크립트는 오브젝트에 붙어 있는 컴포넌트이다.

Unity에서 스크립트는 MonoBehaviour 를 상속 받아 컴포넌트가 되니까,

Monobehaviour 를 상속받는 컴포넌트만 제거된다.

다음으로 파라미터가 (Object, time) 인 경우,

Obejcttime 의 시간 만큼 지난 후에 삭제한다.

위 기능을 통해 코루틴이나 Invoke() 없이 타이밍 조절이 가능하다.

이제 내부 작동 방식을 좀 더 깊게 살펴보면

Destroy() 를 호출해도 즉시 메모리에서 제거되지 않고 다음 프레임에서 제거된다.

그 프레임 동안은 여전히 null 이 아니다.
해당 오브젝트가 이미 다른 코드에서 참조되고 있으면, 딜레이 후 삭제되면서 NullReferenceException 가능성이 존재한다.

Unity의 내부 엔진에서 위 함수의 호출은 C++ 객체를 제거하는 작업을 예약한다.

즉, C# 레벨에서는 플래그만 설정되고, GC와 Unity 엔진이 나중에 처리한다.

2. 게임 오브젝트를 삭제하는 방법

정확히는 카메라 화면에서 벗어나 게임 진행 상에 이제 의미가 없는 오브젝트의 삭제에 대한 생각이다.

고양이 밥 주기 게임 구현에서

고양이에게 밥을 다 주면 카메라에서 벗어나도록 구현하였고,

Destroy 함수에 딜레이 파라미터를 주어, 화면 밖으로 벗어난 후 삭제되도록 구현하였다.

위 구현을 할 당시 나는 카메라 영역을 가져와 조건문으로 벗어나면 삭제하도록 하는 방법과 트리거 콜라이더를 활용한 방법을 생각하게 되었고,

  1. 어떤 방법이 가장 효율적인 것 인지에 대한 고민과
  2. 내가 생각한 것 외에 또 다른 방법이 있을지에 대해 생각하였다.

우선 내가 처음 생각한 방식은 다음 코드로 구현 가능하다

Vector3 viewPos = Camera.main.WorldToViewportPoint(transform.position);
if (viewPos.x < 0 || viewPos.x > 1 || viewPos.y < 0 || viewPos.y > 1) {
    Destroy(gameObject);
}

위 방식은

정확하게 현재 카메라에서 보이는 영역을 기준으로 판단한다.

씬 상 어디에 있든 통일된 기준으로 적용 가능한 장점을 가진다.

다만,

카메라 호출 연산 비용이 다소 크고

WorldToViewportPoint 함수는 내부적으로 행렬 연산이 수행되므로 매 프레임 체크하기에는 부담된다.

Destroy() 함수의 딜레이를 쓰는 방식은 비교적 가볍기 때문에 결국 위 구현에서 이 방식이 바람직한 구현이었다.

또 다른 방식으로는 트리거 콜라이더를 사용하는 방법이다.

void OnTriggerExit2D(Collider2D col) {
    if (col.CompareTag("Cat")) {
        Destroy(Cat.gameObject);
    }
}

화면 외곽에 보이지 않는 트리거 콜라이더를 배치하여

범위를 벗어나면 제거하는 방식으로

  1. 많은 오브젝트 관리에 효율적이고
  2. 카메라를 직접 계산하지 않고도 영역 제어가 가능하다
  3. 또한 한 번만 연산되므로 성능 부담도 덜하다.

물론 물리 연산이 필요한 오브젝트(콜라이더 필요)에만 적용 가능하겠지만,

대규모의 오브젝트가 쏟아져 나오는 경우 더 적합한 구현이 될 것 같다.

결국

  • 오브젝트 수가 적을 땐Destroy(obj, delay)같은 간단한 구조가 빠르게 구현되고 문제 없음
  • 오브젝트 수가 많아질수록 → 실시간 충돌 검사, 트리거 영역, 오브젝트 풀링 도입 등으로 성능 최적화 고려 필요
  • 게임 장르나 연출 의도에 따라 → 어떤 삭제 방식이 더 자연스럽고 몰입감 있는지도 중요 요소
  • 에디터/기획자의 피드백 반영 → 기능은 같아도 구현 방식이 바뀔 수 있으니, 유연하게 리팩토링 가능한 구조가 중요

상황에 따라 가장 적합한 방식을 선택하고, 필요하다면 벤치마킹을 통해 비교 후 결정하는 것으로 결론지을 수 있을 것 같다.

3. ToString() 함수에 대해서 → 파라미터의 사용을 중심으로

ToString() 함수는 객체를 문자열로 표현하기 위한 표준메소드이다.

기본적으로 매개 변수 없이 호출되지만,

파라미터를 사용해서 더 많은 활용이 가능하다.

 Level = Score /5;
 LevelTxt.text = Level.ToString();

위 코드에서 사용한 것 과 같이 Unity에서 Text 오브젝트에 문자열을 전달하기 위한 전처리 용으로 주로 사용하며 .ToString() 으로 도트 앞에 타입이 다른 변수나 객체를 문자열로 표현할 수 있게 한다.

형식 지정자를 통해 포메팅이 가능한데,

함수의 파라미터로 넣으면 된다.

지정자의미예시설명
"C" 또는 "C2"Currency (통화)$1,234.57통화 기호 + 천 단위 + 소수점
"D" 또는 "D5"Decimal (정수 전용)0123410진수, 자리수 지정 가능
"E" 또는 "E2"Exponential (지수)1.23E+003과학적 표기법
"F" 또는 "F2"Fixed-point1234.57고정 소수점, 쉼표 없음
"N" 또는 "N2"Number1,234.57천 단위 구분자 포함
"P" 또는 "P1"Percent123,456.70%값 × 100 후 % 기호
"X" 또는 "X4"Hexadecimal4D216진수 (정수 전용)

4. 문자열 보간

C#에서 S"....{변수}" 형식으로 변수 값을 문자열에 직접 삽입할 수 있다.

이를 활용하면

 frontimage.sprite = Resources.Load<Sprite>($"image{idx}");
 //image1~10까지 파일이 있다고 가정

위 코드는 frontimage 오브젝트의 sprite 컴포넌트에 Resources 폴더에 있는 image1~10 중 하나의 Sprite를 불러오라는 의미이다.
idx 변수 값에 의해 어떤 이미지 파일을 전달하게 되는 것이다.

여기서 ($"image{idx}") 이 형식을 통해 많은 이미지 파일에 접근할 수 있게 된다.

5. Application.targetFrameRate = 60;

이번 고양이 밥 주기 게임 구현에서

게임 매니저 스크립트에 위 코드를 구현하였다.

기기의 프레임에 따라 고양이의 속도가 달라질 수 있으니

이를 사전에 방지하기 위해 프레임을 고정시키는 코드이다.

또한 VSync와 충돌 가능성 있다. 이 경우QualitySettings.vSyncCount = 0; 으로 비활성화하고 사용한다.

6. 카드를 배치하는데 있어 구현 방법

이번 카드 게임 구현에서 강의에서는 16장의 카드를 기하학적으로 계산하여 코드로 구현하였다

    int[] arr = {0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7};
    for (int i = 0; i < 16; i++)
    {
        GameObject go = Instantiate(card, this.transform);
        float x = (i % 4) * 1.4f - 2.1f;
        float y = (i / 4) * 1.4f - 3.0f;

        go.transform.position = new Vector2(x,y);
        Debug.Log(arr[i]);
        go.GetComponent<Card>().Setting(arr[i]);
    }

    GameManager.instance.cardCount = arr.Length;
}

위 코드는 반복문과 수학적인 설계로 구현되었다.

설계에 시간이 걸리긴 하지만 구현 이후 별도 설정이 필요 없고 코드로 완전 제어가 가능하다는 점에서 큰 장점이 있다.

이 방법을 생각하지 못해서 여운이 있다. 수학적으로 접근한다는 점에서 특히 세련되고 단단한 코드라고 생각했다.

하지만 배치 위치가 고정적인 만큼 단점도 있을 것 같다.

그래서 다른 방식으로 구현하는 방법에 대해 정리해 보았다.

  1. 프리셋 위치 배치 방식
Vector2[] positions = new Vector2[] {
    new Vector2(-2f, 2f), new Vector2(0f, 2f), new Vector2(2f, 2f),
    new Vector2(-2f, 0f), new Vector2(0f, 0f), new Vector2(2f, 0f),
    ...
};

위 방식은 배열 또는 리스트에 Vector를 저장하여 배치하는 방식이다.

비대칭, 불규칙 배치가 가능하고 카드를 특정 아트워크 구조에 맞춰 배치 가능하다.

다만 수작업(노가다..)으로 좌표를 입력해야 하므로, 위치가 많아지면 관리가 어려워진다.

  1. UI 그리드 레이아웃 사용

전에 비슷한 구현을 해서 그런지 이 방식을 가장 먼저 생각해본 것 같다.

이 방식은Canvas + GridLayoutGroup을 사용하여 카드를 RectTransform으로 UI 요소로 배치한다.

해상도에 대해 대응이 자동이라서 반응형 디자인 구현에 용이하다.

또한 UI 도구로 위치 제어가 간편하다.

다만, World Space가 아닌 Canvas Space에서만 동작한다.

UI 위주가 아니면 부적합 할 수 있다.

  1. Anchor 객체 또는 빈 GameObject 포인트에 스폰

이 방식도 사실 수학적인 구현을 보기 전에 먼저 떠올렸던 방식이다.

레이아웃을 씬에서 직접 구현하기에 직관성이 좋고 코드도 짧게 구현 가능하다.

public Transform[] spawnPoints;

for (int i = 0; i < 16; i++) {
    Instantiate(card, spawnPoints[i].position, Quaternion.identity);
}

미리 빈 오브젝트를 배치한 후, 카드들을 그 위치에 생성하는 코드만 작성해주면 된다.

다만, 수학적으로 구현한 것 과 달리 씬에서 변화에 연약하다.

씬 관리에 능숙하거나 디자이너/기획자와 협업 시 유용할 것으로 생각된다.

7. arr.OrderBy(x => Random.Range(0f,7f)).ToArray();

이번에 카드 뒤집기 게임에서 카드를 무작위로 섞기 위한 코드였다.

위 코드에 쓰인 문법을 정리해보자.

  • arr: 배열 [0,0,1,1,...7,7]
  • .OrderBy(...): 정렬 메서드 (LINQ)
  • x => Random.Range(...): 람다식으로 정렬 기준 정의
  • .ToArray(): 다시 배열로 반환

OrderBy(...)는 System.Linq 네임스페이스에 포함된 확장 메서드로

리스트나 배열을 특정 기준에 따라 정렬한다.

x => Random.Range(0f, 7f) 는 람다식 표현으로

x는 배열의 각 요소,

=>는 x에 대해어떤 값을 반환할지를 지정하는 람다 연산자이고,

여기서는 Random.Range(0f, 7f) 값을 반환한다.

8. 싱글 톤에 대하여

아직 많은 게임을 개발해 본 것은 아니지만,

예제나 개인 프로젝트, 팀 프로젝트를 진행하면서 싱글 톤 구현은 거의 빠진 적이 없다.

하지만 아직 두리뭉술하게 떠다니는 싱글 톤에 대해서 정확하게 짚고 넘어가려한다.

싱글 톤이란 전체에서 단 하나의 인스턴스만 존재하도록 보장하는 디자인 패턴이다.

처음에는 이게 무슨 소리인가 하였고 지금까지 납득만 하였다.

즉, 어디서든 접근이 가능하고,

인스턴스를 중복 생성할 수 없다.

전역적으로 관리되는 객체에 사용하는 디자인 패턴이다.

그렇다면 왜 게임 개발에서 싱글 톤이 필요할까?

게임은 다른 개발과 달리,

상태 중심적인 구조를 가지고 있다.
달리 말하면 게임은 단순히 버튼 누르고 반응하는 웹/앱과 달리 다음과 같은 복합적인 상태 관리를 요구한다.

즉, "지금 게임이 어떤 상황인지", "무슨 이벤트가 진행 중인지", "무엇을 보여줘야 할지" 등, 여러 가지 시스템이 동시에 상호작용하는 구조다.

그렇기에 언제, 어디서든 접근해야 하는 중앙 집중형 매니저가 필요하다.

만일 이런 매니저들이 여러 개 만들어지면 동기화 문제, 충돌, 상태 꼬임이 생기기 때문에, 하나만 존재하게 강제하는 것이 싱글톤의 역할이다.

Unity에서 싱글 톤 구현은 다음과 같다.

public class GameManager : MonoBehaviour
{
    public static GameManager instance; //정적 인스턴스

    void Awake()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject); // 씬이 바뀌어도 유지
        }
        else
        {
            Destroy(gameObject); // 중복 인스턴스 제거
        }
    }
}

위 클래스가 붙은 오브젝트가 씬에 존재하거나 생성될 때를 가정해보자.

Unity 내부에서는 Awake()Start() 순서로 콜백을 호출할 것이다.
Awake() 에서 조건문을 통해
정적(static) 변수 instance가 null이라면, 이 인스턴스가 첫 번째 생성된 싱글톤임을 의미하게 되고 따라서 instance = this; 로 자기 자신을 싱글 톤으로 등록한다.

DontDestroyOnLoad(gameObject); 코드는 간단한 예제 게임에는 생략되지만, 씬이 여러 개인 복잡한 게임에서는 씬 전환 시 오브젝트가 파괴되지 않도록 유지해 준다.

즉, 생명주기를 고정한다.

모든 싱글 톤에 필요한 것은 아니다. 매 씬마다 게임 매니저를 새로 구현하는 상황이나 단일 씬 게임의 경우 필요하지 않은 코드이다.

조건문의 결과가 false가 되어 else 문으로 간다면,

즉 두 번쨰 인스턴스가 생성되면 중복 인스턴스를 제거함으로써

오직 하나의 GameManager만 존재하게 된다.

이러한 싱글 톤 구현을 통해

다른 스크립트에서 다음과 같이 instance 를 통해 싱글 톤 객체를 직접 사용할 수 있다.

GameManager.instance.AddScore();
GameManager.instance.GameOver();

instancestatic 변수이기 때문에 클래스명으로 접근 가능하고

이 방식은 어디서든 접근할 수 있기 때문에 매우 강력하다.

💭 오늘의 회고

강의를 들으면서 구현을 해보면서 의문이 들었던 주제를 가져와 하나 하나 살펴보고,
애매하게 기억하거나 정리되지 않은 개념을 다시 한번 정리하는 시간이었다.
여전히 두서없는 정리로 보이지만 , 배움에 있어서 의미없는 시간은 아니었다.

profile
천천히, 꾸준하게, 끝까지

0개의 댓글