개요
지난 편에서 웨이브 표를 ScriptableObject로 만들었다. 카드도 그렇게 만들었다. 그런데 사실 이걸 오늘 처음 써봤다.
이름만 보면 “스크립트로 만들 수 있는 객체”인데, 그게 무슨 뜻인지 감이 안 왔다. 클래스면 그냥 클래스지 왜 따로 이름이 붙어 있나 싶었다.
만들어보니 손은 금방 익었다. 끝까지 안 잡힌 건 다른 쪽이었다. 그래서 이걸 언제 쓰는 게 맞나. 일반 클래스나 구조체로 두면 안 되는 이유가 뭔지가 계속 걸렸다.
이번 글이 답할 질문. 값을 어디에 둘지 정할 때 무엇을 보고 정하는가?
1. 스탯을 어디에 둘까 — 후보 셋
카드를 먹으면 공격력, 공격속도, 공격범위, 최대 체력, 이동속도가 오른다. 그런데 이 값들은 지금 세 컴포넌트에 흩어져 있다.
| 값 | 지금 있는 곳 |
|---|---|
| 이동속도 | 이동 담당 |
| 공격력 / 공격속도 / 사거리 | 무기 담당 |
| 최대 체력 | 체력 담당 |
후보를 셋 놓고 봤다.
| 후보 | 구조 |
|---|---|
| A | 각 컴포넌트에 “값 올려주는 함수”를 뚫고, 적용부가 골라 부른다 |
| B | 스탯 전용 클래스에 값을 다 모으고, 각 컴포넌트가 거기서 읽는다 |
| C | 기본값은 파일(SO)에 두고, 올라간 배수는 따로 관리한다 |
처음엔 C가 제일 간단해 보였다. 값이 한 파일에 모여 있고 인스펙터에서 바로 고칠 수 있으니 깔끔하겠다 싶었다.
2. C가 무너진 이유 — SO는 공유된다
SO가 어떤 물건인지 알고 나니 C가 먼저 무너졌다.
프리팹은 꺼낼 때마다 복사본이 생긴다. 좀비를 100마리 만들면 각자 자기 체력을 갖는다. 한 마리가 맞아서 체력이 깎여도 원본 파일은 그대로다.
SO는 다르다. 몇 군데서 참조하든 파일 하나 그대로다.
여기서 문제가 나온다. 게임이 도는 중에 SO의 값을 바꾸면, 에디터에서는 그 값이 파일에 그대로 저장된다. 플레이를 멈춰도 안 돌아온다.
C는 “SO에 적힌 기본값을 읽어서 런타임에 굴린다”인데, 값을 SO에서 직접 굴리면 밸런스 데이터가 오염된다. 그러니 어딘가 런타임 보관소에 복사해두고 거기서 굴려야 한다. 그건 결국 B에 한 겹을 더 얹은 것이다.
B보다 간단할 수 없는 구조를 B보다 간단하다고 생각하고 있었다.
설명을 듣고 나니 바로 납득이 됐다. 헷갈렸던 건 어려워서가 아니라 SO가 복사되는 물건인지 공유되는 물건인지를 몰라서였다. 그 한 가지가 정해지니 나머지는 자동으로 따라왔다.
여기서 규칙 하나가 확정됐다.
SO는 읽기 전용으로 다룬다. 변하는 값은 SO가 아니라 게임이 도는 동안만 사는 객체가 갖는다.
프로젝트 폴더 규칙이 이미 이 경계를 반영하고 있었다는 것도 뒤늦게 보였다. SO를 정의하는 코드는 Scripts/Data/에, 실제 값이 든 파일은 Data/에 두기로 되어 있었다.
3. 2층 구조 — 양식과 채워진 종이
SO를 만들면서 손에 잡힌 건 이 구조였다.
1층은 코드다. 어떤 칸이 있는지만 정한다. 카드라면 제목, 어떤 스탯인지, 얼마나 올릴지 세 칸이다.
[CreateAssetMenu(fileName = "UpgradeCard", menuName = "Game/UpgradeCard")]
public class UpgradeCard : ScriptableObject
{
[SerializeField] private string _title;
[SerializeField] private StatType _stat;
[SerializeField] private float _amount;
public string Title => _title;
public StatType Stat => _stat;
public float Amount => _amount;
}
맨 위 한 줄이 에디터 우클릭 메뉴에 항목을 만들어준다.

2층은 그 메뉴로 만든 파일들이다. 칸을 채운 종이에 해당한다.

카드 하나를 열면 아까 정한 세 칸이 그대로 보인다.

카드가 다섯 종인데 코드는 여전히 클래스 하나다. 카드를 늘려도 늘어나는 건 파일뿐이고, 값을 고쳐도 다시 빌드할 필요가 없다.
더 쉬운 비유로
서류 양식을 인쇄하는 것과
그 양식에 이름을 적어 넣는 것
양식은 한 번 만들면 된다. 사람이 100명이면 종이가 100장 필요할 뿐이지 양식을 100번 만들지는 않는다.
그리고 종이를 한 장 더 쓰는 데는 인쇄소가 필요 없다. 밸런스를 고치는 데 개발자가 필요 없다는 게 바로 이 뜻이다.
4. 그래서 언제 쓰나
여기가 제일 오래 안 잡힌 부분이었다. 값을 담는 방법은 이미 여러 개 알고 있는데, 그중에 SO를 골라야 하는 상황이 어떤 건지가 안 그려졌다.
써보고 나서 정리한 기준은 셋이다.
| 질문 | 그렇다 | 아니다 |
|---|---|---|
| 여러 곳이 같은 값을 봐야 하나 | SO가 맞다 | 그냥 필드로 충분 |
| 그 값이 게임 중에 안 변하나 | SO가 맞다 | 런타임 객체가 가져야 한다 |
| 자주 고칠 값인가 (밸런스) | SO가 맞다 | 코드에 둬도 된다 |
셋 다 맞으면 SO다. 카드와 웨이브 표가 정확히 그랬다. 값을 여러 곳이 읽고, 게임 중에는 안 변하고, 밸런스라 자주 고친다.
반대로 플레이어의 현재 체력은 셋 다 아니다. 한 곳만 쓰고, 매 순간 변하고, 밸런스 값이 아니다. 그건 그냥 컴포넌트 필드다.
“공유되는 값”이라는 성질이 장점이 되는 자리와 단점이 되는 자리가 정확히 반대다. 안 변하는 값에는 장점이고, 변하는 값에는 사고다.
5. A를 고른 진짜 기준
C가 빠지고 A와 B가 남았다. 둘은 교환 관계다.
| A (각자 갖기) | B (한 곳에 모으기) | |
|---|---|---|
| 관리 | 흩어져 있다 | 한눈에 보인다 |
| 컴포넌트 독립성 | 자기 값을 자기가 갖는다 | 스탯 클래스가 없으면 못 움직인다 |
| 지금 드는 작업 | 함수만 추가 | 이미 도는 코드를 전부 뜯음 |
B가 관리 면에서는 낫다. 그런데 A로 갔다. 기준 두 개가 컸다.
첫째, 지금 뜯어야 하는 양이다. 세 컴포넌트는 이미 잘 돌고 있다. B는 그걸 다 뜯어야 하고, 뜯는 동안 멀쩡하던 게 깨질 수 있다. A는 값을 올려주는 함수를 하나씩 다는 것뿐이라 기존 동작을 안 건드린다.
둘째가 더 컸다.
A에서 B로 가는 건 나중에도 할 수 있는데, B에서 A로 돌아오는 길은 없다.
되돌릴 수 있는 쪽을 먼저 고르면, 틀렸을 때 치를 값이 작다.
판단할 때 스스로에게 던진 질문은 이거였다. “카드가 10종으로 늘면 새 카드 하나 추가하는 데 파일을 몇 개 건드리나.”
A에서는 같은 종류의 스탯이면 파일 하나 만들면 끝이고 코드는 0줄이다. 새로운 종류면 목록에 항목 하나, 갈래 하나, 대상 함수 하나가 는다.
switch(card.Stat)
{
case StatType.AttackRange:
_weapon.AddRange(card.Amount);
break;
case StatType.MaxHealth:
_health.AddMaxHealth(card.Amount);
break;
// ...
}
대가도 적어뒀다
A를 고른 대가가 둘 있다.
하나, 스탯 전체를 한 번에 볼 방법이 없다. 화면에 능력치 5종을 다 띄우려면 세 컴포넌트를 각각 읽어야 한다. 지난 편에서 그 표시를 못 붙이고 넘어간 게 이 대가가 청구된 자리다.
둘, 갈래가 스탯 종류만큼 길어진다. 지금은 5개라 괜찮은데 10개를 넘으면 B로 옮기는 걸 다시 볼 생각이다.
이렇게 대가를 같이 적어두는 게 나중에 되돌아올 때의 이정표가 된다는 걸 이번 프로젝트에서 계속 확인하고 있다.
정리
- 프리팹은 복사되고 SO는 공유된다. 둘 다 파일이지만 꺼내 쓸 때 일어나는 일이 반대다.
- SO는 읽기 전용으로 다룬다. 게임 중에 값을 바꾸면 에디터에서는 그게 파일에 저장돼 남는다.
-
코드는 양식,
.asset은 채워진 종이. 카드가 다섯 종이어도 클래스는 하나다. - 밸런스를 고치는 데 다시 빌드하지 않아도 되는 것이 데이터를 파일로 빼는 실익이다.
- SO를 쓸 자리는 셋으로 갈린다. 여러 곳이 같은 값을 보는가, 게임 중에 안 변하는가, 자주 고치는 값인가.
- 공유된다는 성질은 안 변하는 값에는 장점이고 변하는 값에는 사고다.
- 되돌릴 수 있는 쪽을 먼저 고른다. A에서 B로는 갈 수 있지만 그 반대는 없다.
- “새 것 하나 추가할 때 파일을 몇 개 건드리나”가 구조를 고르는 실용적인 질문이다.
- 대가를 같이 적어두면 나중에 되돌아올 자리가 표시된다. 갈래가 10개를 넘으면 다시 본다.
참고 자료
- Unity 6000.0 Manual - ScriptableObject
- Unity 6000.0 Manual - CreateAssetMenu
- Unity 6000.0 Manual - Prefabs
- Microsoft Learn - switch 문
- 지난 편 - 유니티 학습 프로젝트 < 10 >
한줄 평
- 손에 익히는 데는 30분이 걸렸는데 “언제 쓰는 물건인가”를 정리하는 데 그 몇 배가 걸렸고, 결국 답은 값이 변하냐 안 변하냐 하나였다!