Sprite Flight (1) 씬, 프리팹, 첫 스크립트

이번 글에서는 아래 내용을 정리한다.

  • Unity Learn의 Sprite Flight 과정을 고른 이유
  • 1편: 프로젝트 만들기, 에디터 창 배치, 씬, 카메라, 테두리
  • 2편: Rigidbody 2D와 Collider 2D, 물리 재질, 프리팹
  • 3편: 첫 MonoBehaviour 스크립트, StartUpdate, 무작위 크기와 속도
  • 따라 하다가 걸린 질문들. 프리팹과 복사의 차이, 2D 물리와 3D 물리, 필드 초기화, public[SerializeField]

왜 이 과정인가

지난 글에서 C#을 한 바퀴 돌았으니 이제 에디터 차례다. 유니티 에디터는 글로 읽어서 되살아나는 것이 아니라 손으로 클릭해야 돌아온다. 그래서 유니티 공식 학습 사이트의 2D 입문 과정을 하나 골랐다.

고른 기준은 세 가지였다.

  • 글로 된 단계별 문서일 것. 영상 강의는 피한다.
  • Unity 6 기준일 것. 학원에서 배운 2022년과 API 이름이 달라진 것이 있다.
  • 짧을 것. 총 4시간 30분, 튜토리얼 8편이고 편당 20분 안팎이다.

Sprite Flight는 삼각형 우주선이 튀어 다니는 육각형 장애물을 피하는 아케이드 게임이다. 빈 프로젝트에서 시작해 UI, 파티클, 빌드까지 한 바퀴 돈다.

브라우저 자동 번역은 끄고 영문으로 보는 것이 낫다. 번역기를 거치면 Prefab이 “조립식 구조물”, Inspector가 “검사관”, Rigidbody가 “강체"가 되는데, 에디터 메뉴는 영문이라 어디를 눌러야 하는지 대응이 안 된다.

1편: 2D 게임 월드 만들기

프로젝트와 레이아웃

Unity Hub에서 Universal 2D 템플릿으로 프로젝트를 만든다. Hub의 새 프로젝트 화면에는 낯선 항목이 몇 개 있다.

항목 선택
Unity 조직(Organization) 계정 아래의 소속 단위. 라이선스와 클라우드 서비스를 묶는다 기본으로 생긴 개인 조직
프로젝트 이름 폴더 이름이 된다. 빌드 스크립트와 외부 도구 경로에 그대로 들어간다 영문
AI Assistant 사용 에디터 안의 유료 AI 도우미 패키지. 14일 체험 후 크레딧 결제 끈다
Unity CLI 사용 터미널의 unity 명령과 MCP로 에디터를 제어하는 패키지 아무쪽이나
소스 제어 공급자 git init.gitignore 생성까지만 해 준다. 원격 연결은 어차피 직접 한다 없음

에디터를 열면 Layout 드롭다운에서 2 by 3을 고르고 Project 창을 Hierarchy 아래로 끌어 놓는다. 창은 네 개만 기억하면 된다.

서버로 치면
Hierarchy 월드에 있는 객체 트리
Scene / Game 월드를 보는 눈. Scene은 편집용, Game은 실제 화면
Inspector 선택한 객체의 속성 뷰어이자 편집기
Project 리소스 폴더
Console 로그 창

씬, 스프라이트, 카메라

Project 창의 Scenes 폴더에서 새 씬 “Game"을 만들고 연다. 씬은 존(맵) 하나다.

Hierarchy에서 우클릭하고 2D Object > Sprites > Hexagon Flat Top을 만들어 이름을 “Obstacle"로 바꾼다. 이것이 장애물이다. Inspector에서 Position을 (0, 0, 0)으로 둔다.

Main Camera를 선택하고 Camera 컴포넌트에서 세 가지를 만진다.

  • Projection이 Orthographic인지 확인한다. 2D는 원근이 없다.
  • Size를 7.5로 올린다. 세로로 보이는 범위의 절반이다.
  • Environment의 Background Type을 Skybox에서 Solid Color로 바꾼다.

테두리 네 개

2D Object > Sprites > Square로 사각형을 만들어 “Border_Bottom"으로 이름 짓고, Rect 툴(T)로 가로로 길게 늘여 화면 아래쪽에 놓는다. Ctrl+D로 복제해 Top, Left, Right를 만든다. Game 뷰의 비율을 16:9로 고정하고 테두리가 화면 가장자리에 맞도록 조정한다.

마지막으로 Hierarchy에서 Create Empty로 빈 오브젝트 “Borders"를 만들고 네 테두리를 그 안으로 끌어 넣는다. 빈 GameObject는 폴더 역할을 한다.

여기서 하나 떠올랐다. 예전에 테두리를 안 보이게 만들 때 체크박스를 껐던 기억이 있는데, 이 튜토리얼은 테두리를 보이는 벽으로 쓰는 설계라 그 단계가 없다. 체크박스는 셋이고 역할이 다르다.

위치 역할 벽을 안 보이게 할 때
Inspector 맨 위, 이름 왼쪽 GameObject 전체를 끈다(SetActive). 콜라이더도 같이 꺼진다 쓰면 안 된다. 벽 자체가 없어진다
Sprite Renderer 컴포넌트 왼쪽 그리기만 끈다. 콜라이더는 살아 있다 이것
Hierarchy의 눈 아이콘 에디터 Scene 뷰에서만 숨긴다. 게임에는 영향 없음 작업 편의용

서버로 치면 첫 번째는 엔티티를 존에서 제거하는 것이고, 두 번째는 엔티티는 두고 클라이언트에 그리지 말라는 플래그만 끄는 것이다.

2편: 날아다니는 장애물 프리팹

Rigidbody 2D와 Collider 2D

Obstacle에 Add Component > Rigidbody 2D를 붙이고 Play를 누르면 아래로 떨어진다. Rigidbody가 붙는 순간 물리 엔진이 그 오브젝트를 매 틱 계산 대상에 넣는다.

그런데 테두리를 뚫고 떨어진다. 부딪힐 모양이 없기 때문이다. Obstacle에 Polygon Collider 2D, 테두리 네 개에 Box Collider 2D를 붙이면 바닥에서 멈춘다. Polygon은 스프라이트 외곽선을 따라 자동으로 모양을 잡는다.

둘의 관계는 이렇다.

역할 서버로 치면
Collider2D 충돌을 판정하는 모양. 그 자체로는 움직이지도 계산하지도 않는다 바운딩 박스
Rigidbody2D 붙이면 물리 엔진이 매 틱 계산한다. 중력, 속도, 힘 물리 계산 대상 등록

Rigidbody2D의 Body Type은 셋이다. Dynamic은 물리가 전부 계산하고, Kinematic은 코드로 움직이되 충돌 이벤트는 내고, Static은 안 움직인다. 벽은 Collider만 있으면 되고, 장애물은 Dynamic이다.

그리고 규칙 하나. 충돌 콜백이 오려면 부딪히는 둘 중 하나에는 Rigidbody2D가 있어야 한다. Collider끼리만 있으면 아무 일도 안 일어난다.

Collider에는 Is Trigger 체크박스가 있다. 학원 때 중요했던 기억은 있는데 내용이 안 떠올랐던 것이다.

Is Trigger 꺼짐 Is Trigger 켜짐
물리 서로 밀고 막는다. 벽 통과한다. 물리적으로 없는 것 취급
콜백 OnCollisionEnter2D OnTriggerEnter2D
용도 벽, 바닥, 장애물 코인, 골인 지점, 감지 영역

Trigger는 서버의 영역 진입 이벤트다. 마을 입장 판정, 스킬 범위처럼 “들어왔다"만 알면 되고 물리적으로 막을 필요는 없는 것이다. 2022년 팀 프로젝트에서 내가 짠 TrapBlockTrigger.cs가 정확히 이것이었다. Trigger 영역에 플레이어가 점프 중에 들어오면 숨어 있던 블록을 나타나게 했다.

꼭 기억할 것 ① 2D 물리와 3D 물리는 서로를 모른다

유니티에는 물리 엔진이 두 개다. 2D(Box2D)와 3D(PhysX)이고, 컴포넌트도 Rigidbody2D/BoxCollider2DRigidbody/BoxCollider로 따로 있다. 두 엔진은 서로를 전혀 모른다. 벽은 2D 콜라이더인데 공에 3D Rigidbody를 붙이면 공이 벽을 그냥 통과한다. 에러도 경고도 없다. 콜백도 OnCollisionEnter2DOnCollisionEnter가 따로 있어서, 2D 콜라이더에 OnCollisionEnter를 쓰면 영원히 불리지 않는다. 컴파일은 되고 Console은 깨끗하다. 충돌이 안 되는데 코드가 맞아 보이면 컴포넌트 이름과 콜백 이름 끝의 2D부터 대조한다.

물리 재질

Assets에 Materials 폴더를 만들고 Create > 2D > Physics Material 2D로 “PhysMat_Bouncy"를 만든다. Bounciness를 1.0으로 놓고 Obstacle의 Polygon Collider 2D의 Material 칸에 넣는다. Play를 누르면 장애물이 바닥에서 튀어 오른다. 1.0은 에너지 손실 없이 튕긴다는 뜻이다.

Play 중에 Rigidbody 2D의 Gravity Scale을 0으로 바꾸면 떨어지지 않고 떠 있는다. 튜토리얼이 여기서 강조하는 것이 하나 있다. Play 중에 바꾼 값은 Play를 멈추면 되돌아간다. 마음에 드는 값을 찾았으면 멈춘 뒤 다시 입력해야 한다.

프리팹

Assets에 Prefabs 폴더를 만들고 Hierarchy의 Obstacle을 그 폴더로 드래그한다. Hierarchy의 글씨가 파란색으로 바뀐다. 그 프리팹을 Scene 뷰로 여러 번 끌어다 놓으면 장애물이 여러 개가 되고, 각자 따로 튄다. 전부 선택해 Create Empty Parent로 “Obstacles” 아래에 묶는다.

여기서 질문이 생겼다. Ctrl+C, Ctrl+V로 늘리는 것과 프리팹은 무엇이 다른가.

Ctrl+V는 복사본 N개를 만들고, 프리팹은 원본 하나와 그것을 가리키는 인스턴스 N개를 만든다. 서버로 치면 복사본은 몬스터 데이터를 코드에 하드코딩해서 붙여 넣은 것이고, 프리팹은 몬스터 템플릿 테이블에 한 줄 두고 스폰할 때 참조하는 것이다.

Ctrl+C / Ctrl+V 프리팹
저장 위치 씬 파일 안에 N개 전부 원본은 .prefab 파일, 씬에는 참조와 바뀐 값만
원본을 고치면 아무 일 없음 전부 바뀐다
개별 예외 각자 고친다 인스턴스 오버라이드. 원본을 고쳐도 유지된다
런타임 생성 불가능 Instantiate(prefab)
Git diff 10개면 10군데 .prefab 파일 한 곳
협업 씬 파일을 같이 잡아야 한다 한 사람은 프리팹, 한 사람은 씬

그런데 인스턴스는 “복사본"이 아니다. 참조 + 바꾼 값 목록이다. 씬 파일에는 이렇게 기록된다.

Obstacle (1):  Obstacle.prefab 참조, 오버라이드 { position = (-3, 2), rotation = 45 }
Obstacle (2):  Obstacle.prefab 참조, 오버라이드 { position = (4, -1) }

씬을 열 때 유니티가 프리팹을 읽고 그 위에 오버라이드를 덮는다. 프리팹을 고치면 “전부 찾아가서 고치는” 것이 아니라 다음에 조립할 때 바뀐 원본이 쓰이는 것이다. 그리고 프리팹은 자기에게서 나온 인스턴스가 어디 있는지 모른다. 인스턴스 쪽이 원본을 가리킬 뿐이다. 몬스터 템플릿 테이블이 스폰된 몬스터 목록을 들고 있지 않은 것과 같다.

이 구조는 장점과 단점이 같은 곳에서 나온다. 원본 하나를 여러 곳이 공유하므로 한 번 고치면 전부 바뀌고, 바뀌면 안 되는 곳까지 바뀐다. MMORPG에서 “무녀” 속성 테이블 한 줄을 패치했더니 무녀 용병, 무녀 몬스터, 무녀 NPC가 전부 바뀌어 사이드 이펙트가 난 것과 정확히 같은 현상이다. 유니티에서 이것을 막는 방법은 서버와 같다.

방법 서버로 치면
Prefab Variant 원본을 상속한 파생 테이블. ShamanShaman_Mercenary, Shaman_Monster
인스턴스 오버라이드 스폰 데이터의 개별 보정값
프리팹 분리 아예 다른 테이블
Unpack Prefab 참조를 끊고 복사본으로. 되돌리기 어려워 드물다

Hierarchy의 오브젝트를 Project 창으로 드래그하면 예외 없이 프리팹이 된다. 이미 프리팹 인스턴스인 것을 끌어다 놓으면 Original Prefab과 Prefab Variant 중 고르라고 묻는다.

2022년 WinAPI 모작의 CScene_Start.cpp에서 pSwitchC3부터 pSwitchC39까지 변수 37개를 나열하고 Clone()으로 찍어 냈는데, 그것이 프리팹 없이 손으로 프리팹을 흉내 낸 것이었다. 원본 하나에 Clone()Instantiate다.

3편: 무작위 크기, 방향, 속도

첫 스크립트

Assets에 Scripts 폴더를 만들고 Create > MonoBehaviour Script로 “Obstacle"을 만든다. Unity 6의 Create 메뉴에는 스크립트 종류가 셋이다.

메뉴 생성되는 내용 용도
MonoBehaviour Script class X : MonoBehaviour에 빈 Start()Update() GameObject에 붙일 스크립트
Empty C# Script class X { }. 상속 없음 붙지 않는 순수 로직. 계산, 데이터 구조
ScriptableObject Script class X : ScriptableObject 데이터 에셋. 기획 테이블의 한 행

셋의 차이는 템플릿뿐이고 어느 것으로 만들어도 같은 .cs 파일이다. 기준은 “콘텐츠냐 시스템이냐"가 아니라 “씬 안의 오브젝트에 붙어야 하느냐” 다. 손님이 걸어가는 것은 MonoBehaviour이고, 손님이 사면 돈이 얼마 오르는지는 빈 클래스이고, 상품 가격표는 ScriptableObject다. ScriptableObject는 2022년 타워 디펜스에서 Data_Turret로 이미 썼던 것이다.

그리고 GameObject에 붙일 수 있는 것은 MonoBehaviour를 상속한 클래스뿐이고, 파일 이름과 클래스 이름이 같아야 한다. 다르면 컴파일은 되는데 컴포넌트로 붙일 수 없다. C++에서는 파일 이름이 아무래도 상관없었으니 여기서 한 번은 걸린다.

만들어진 기본 스크립트다.

using UnityEngine;

public class Obstacle : MonoBehaviour
{
    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
    }

    // Update is called once per frame
    void Update()
    {
    }
}
  • using UnityEngine;#include다. MonoBehaviour, Vector3, Rigidbody2D가 이 네임스페이스에 있다. 상속과는 무관하다. 상속이 주는 것은 transform, gameObject, GetComponent 같은 멤버다.
  • Start는 첫 Update 직전에 한 번, Update는 매 프레임 불린다. 서버의 Init과 Tick이다.
  • C++ 개발자 눈에 이상한 점이 있다. StartUpdateoverride가 없다. 유니티는 가상 함수가 아니라 이름으로 찾는다. 그래서 Upadte로 잘못 쓰면 에러 없이 조용히 안 불린다. 안 쓰는 Update는 지우는 것이 관례다.

: MonoBehaviour를 지우면 GameObject에 붙일 수 없게 되고 Start, Update는 아무도 안 부르는 일반 함수가 된다. 이미 붙어 있던 것은 Inspector에 “The associated script can not be loaded"로 깨진다.

자주 쓰는 콜백은 열 개 안팎이다.

시점 함수 서버로 치면 용도
생성 Awake 생성자 자기 자신 초기화
생성 OnEnable 등록 이벤트 구독 +=
생성 Start Init 다른 오브젝트 참조
매 틱 FixedUpdate 고정 틱 (기본 0.02초) 물리
매 틱 Update Tick 입력, 이동, 타이머
매 틱 LateUpdate 후처리 카메라 추적
물리 OnCollisionEnter2D / OnTriggerEnter2D 충돌 / 영역 이벤트
소멸 OnDisable 해제 이벤트 해제 -=
소멸 OnDestroy 소멸자

AwakeStart는 순서가 다르다. 모든 오브젝트의 Awake가 끝난 뒤에 Start들이 불리므로 자기 것은 Awake, 남의 것은 Start다. OnEnableOnDisable이 구독과 해제의 짝이다.

고정 크기, 그리고 왜 이 코드가 이상한가

Start에 이 한 줄을 넣으면 장애물이 두 배가 된다.

transform.localScale = new Vector3(2, 2, 1);

localScale은 Inspector의 Scale 칸이고, local은 부모 기준이라는 뜻이다. Z에 1을 넣는 이유는 0이면 두께가 0인 물체가 되어 일부 계산이 이상해지기 때문이다. 2D에서 Z 스케일은 항상 1이다. Vector3는 struct라 new를 써도 힙 할당이 없다.

그런데 이 코드는 리뷰 기준으로 걸린다. 고정값을 Start에서 박는다면 에디터에서 하는 것이 맞다. Inspector에서 Scale을 3으로 바꿔 놔도 Play를 누르는 순간 2로 덮어써지고, 왜 안 바뀌는지 코드를 뒤져야 한다. 기획자가 못 건드리고, 프리팹 오버라이드도 무력화된다. 튜토리얼이 이렇게 쓴 이유는 다음 단계에서 그 2가 무작위로 바뀌기 때문이다. 코드가 스케일을 제어한다는 것을 먼저 보여 주는 중간 단계다.

그러면 크기를 바꾸는 코드는 어디에 두어야 하나.

값의 성격 어디서
고정 에디터. 코드에 안 씀
생성 시 무작위 Start. 범위는 [SerializeField]
사건에 따라 한 번 변경 그 사건을 처리하는 함수. Update에서 매 프레임 조건 검사하지 않는다
매 프레임 연속 변화 Update 또는 트윈

“버프가 걸렸는지 매 틱 검사해서 스탯을 갱신"하지 않고 “버프 적용 시점에 스탯을 갱신"하는 것과 같다.

무작위 크기와 범위 변수

float minSize = 0.5f;
float maxSize = 2.0f;

void Start()
{
    float randomSize = Random.Range(minSize, maxSize);
    transform.localScale = new Vector3(randomSize, randomSize, 1);
}

RandomUnityEngine.Random이다. .NET에도 System.Random이 있어서 using System;을 같이 쓰면 이름이 충돌한다.

여기서 질문. 범위 변수를 Start 밖 필드 초기화 자리에서 바로 Random.Range로 채우면 안 되나.

일부만 된다. 필드 초기화는 생성자 시점에 실행되는데, 유니티 컴포넌트는 그때 아직 GameObject에 붙어 있지 않다.

필드 초기화 자리 판정
float minSize = 0.5f; 좋음. 그냥 값
float size = Random.Range(0.5f, 2f); 동작은 하나 비권장. 유니티 함수를 생성자 시점에 부른다
Rigidbody2D rb = GetComponent<Rigidbody2D>(); 안 됨. 오브젝트가 아직 없는 시점

서버로 치면 생성자에서는 자기 메모리만 초기화하고, 존에 등록된 뒤의 Init에서 주변을 참조하는 규칙과 같다. 상수 대입은 문제없고, Start에서 minSize = 0.5f;라고 다시 넣는 것이 오히려 이상하다.

Rigidbody에 힘 주기

Rigidbody2D rb;

void Start()
{
    rb = GetComponent<Rigidbody2D>();
    rb.AddForce(Vector2.right * 100);
}

Rigidbody2D rb;에는 초기값이 없다. C++03이라면 Rigidbody2D* m_rb;에 쓰레기 값이 들어가서 생성자에서 NULL을 넣어야 했지만, C#의 필드는 초기화하지 않으면 자동으로 기본값이 들어간다. 참조 타입은 null, 숫자는 0이다. = null을 굳이 쓰지 않는다. 단, 지역 변수는 다르다. 함수 안에서 선언만 하고 읽으면 컴파일 에러다.

GetComponent<Rigidbody2D>()는 “나와 같은 GameObject에 붙은 Rigidbody2D를 달라"다. 없으면 null을 돌려주고 다음 줄에서 NullReferenceException이 난다. Start에서 한 번 찾아 필드에 캐싱하는 이유는 GetComponent가 검색이라 Update에서 매 프레임 부르면 낭비이기 때문이다.

Obstacle 프리팹의 Rigidbody 2D에서 Gravity Scale을 0으로 두면 떨어지지 않고 직진한다.

무작위 속도와 방향

float minSpeed = 50f;
float maxSpeed = 150f;

void Start()
{
    float randomSpeed = Random.Range(minSpeed, maxSpeed);
    Vector2 randomDirection = Random.insideUnitCircle;
    rb.AddForce(randomDirection * randomSpeed);
}

Random.insideUnitCircle은 반지름 1인 원 안의 무작위 점을 Vector2로 돌려준다. x, y 모두 -1에서 1 사이이고 길이는 1 이하다. 프로퍼티라서 괄호가 없고, 읽을 때마다 새 값이 나온다. 길이를 정확히 1로 하려면 .normalized를 붙인다.

public이냐 [SerializeField]

튜토리얼은 마지막에 범위 변수 네 개를 public으로 바꿔 Inspector에 띄운다.

public float minSize = 0.5f;
public float maxSize = 2.0f;
public float minSpeed = 50f;
public float maxSpeed = 150f;

public 필드는 [SerializeField]가 없어도 Inspector에 뜨고 저장된다. 동작은 같다. 차이는 다른 코드에서 쓸 수 있느냐뿐이다.

Inspector 다른 코드에서 읽기 다른 코드에서 쓰기
public float minSize O O O
[SerializeField] private float minSize O X X
위 + public float MinSize => minSize O O X

“Inspector에 띄우고 싶다"와 “다른 코드가 쓰게 하고 싶다"는 별개의 요구인데 public은 둘을 한 번에 켜 버린다. 튜닝 값은 그 클래스만 쓰면 되므로 [SerializeField] private이 맞다. public이면 다른 스크립트가 obstacle.minSize = 0을 할 수 있고, 값이 이상해졌을 때 프로젝트 전체를 검색해야 한다. 학원 시절 코드는 전부 public이었는데 작은 프로젝트라 문제가 안 났을 뿐이다.

예외는 데이터만 담는 클래스다. [System.Serializable] 데이터 묶음과 ScriptableObject는 로직이 없어서 public 필드로 둔다.

최종 코드

using UnityEngine;

public class Obstacle : MonoBehaviour
{
    public float minSize = 0.5f;
    public float maxSize = 2.0f;
    public float minSpeed = 50f;
    public float maxSpeed = 150f;

    Rigidbody2D rb;

    void Start()
    {
        float randomSize = Random.Range(minSize, maxSize);
        transform.localScale = new Vector3(randomSize, randomSize, 1);

        float randomSpeed = Random.Range(minSpeed, maxSpeed);
        Vector2 randomDirection = Random.insideUnitCircle;

        rb = GetComponent<Rigidbody2D>();
        rb.AddForce(randomDirection * randomSpeed);
    }
}

실전 기준으로 고친다면 두 가지다. public[SerializeField] private으로 바꾸고, 클래스 위에 [RequireComponent(typeof(Rigidbody2D))]를 붙여 Rigidbody 누락을 막는다.

뭘 배웠지?

  • 에디터 창은 Hierarchy, Scene/Game, Inspector, Project, Console 다섯 개다.
  • Collider2D는 모양, Rigidbody2D는 물리 계산 참가자다. 충돌 콜백은 둘 중 하나에 Rigidbody가 있어야 온다.
  • Is Trigger를 켜면 막지 않고 통과시키며 OnTriggerEnter2D가 불린다. 영역 진입 이벤트다.
  • 2D 물리와 3D 물리는 별개 엔진이고 섞이면 에러 없이 조용히 충돌이 안 된다.
  • 프리팹 인스턴스는 복사본이 아니라 참조 + 오버라이드다. 원본을 고치면 전부 바뀌고, 바뀌면 안 되는 곳도 바뀐다. Variant로 분리한다.
  • MonoBehaviour를 상속해야 GameObject에 붙고, 파일 이름과 클래스 이름이 같아야 한다.
  • StartUpdate는 이름으로 찾는다. 오타가 나면 조용히 안 불린다.
  • 고정값은 코드가 아니라 에디터에 둔다. 값이 바뀌는 사건이 있으면 그 사건의 함수에서 바꾼다.
  • 필드는 자동으로 기본값이 들어간다. 필드 초기화 자리에서 GetComponent는 안 된다.
  • Play 중에 바꾼 값은 저장되지 않는다.
  • 튜닝 값은 [SerializeField] private, 데이터 클래스는 public.

생각 해보기

문제 1

테두리 벽이 화면에 보이지 않게 하되 장애물은 계속 튕기게 하려면 어느 체크박스를 꺼야 할까? Inspector 맨 위의 체크박스를 끄면 어떻게 될까? (난이도 : 下)

문제 2

장애물에 Rigidbody2DPolygonCollider2D가 있고 벽에 BoxCollider(3D)가 붙어 있다. Play를 누르면 무슨 일이 벌어질까? Console에는 무엇이 찍힐까? (난이도 : 中)

문제 3

프리팹 인스턴스 하나의 Scale을 3으로 오버라이드한 뒤 원본 프리팹의 Scale을 2로 바꿨다. 그 인스턴스의 Scale은 얼마일까? 원본의 Color를 바꾸면 그 인스턴스의 Color는 어떻게 될까? (난이도 : 中)

문제 4

아래 코드는 왜 예외가 날까? 어떻게 고쳐야 할까? (난이도 : 中)

public class Obstacle : MonoBehaviour
{
    Rigidbody2D rb = GetComponent<Rigidbody2D>();

    void Start()
    {
        rb.AddForce(Vector2.right * 100);
    }
}

문제 5

void update()라고 소문자로 썼다. 컴파일은 될까? 실행하면 어떻게 될까? (난이도 : 下)

문제 6

아래 두 코드는 결과가 같다. 어느 쪽을 써야 하고 왜 그럴까? (난이도 : 中)

// A
void Start() { transform.localScale = new Vector3(2, 2, 1); }

// B
// 코드 없이 Inspector의 Scale에 (2, 2, 1)을 입력한다

문제 7

MMORPG에서 공유 속성 테이블 한 줄을 고쳤을 때 의도하지 않은 몬스터까지 바뀌는 사이드 이펙트를, 유니티 프리팹에서는 어떤 기능으로 막을 수 있을까? 세 가지 이상 말해 보자. (난이도 : 中)

문제 8

AwakeStart의 실행 순서는 어떻게 다르고, 다른 오브젝트의 컴포넌트를 참조하는 코드는 왜 Start에 두어야 할까? (난이도 : 中)