게임 개발

플레이 중에 바꾼 값이 안 날아간다: 스크립터블 오브젝트 이야기

플레이 모드에서 만진 수치가 그대로 남는 스크립터블 오브젝트. 밸런싱, 기획 데이터 분리, 런타임 파싱 제거까지 실제 개발에서 쓰는 방식을 정리했다.

CodingJoa 9분 정도 걸립니다

플레이 중에 바꾼 값이 안 날아간다: 스크립터블 오브젝트 이야기

플레이 버튼을 누르고, 인스펙터에서 캐릭터 공격력을 12에서 30으로 올린다. 확실히 손맛이 좋아진다. 그런데 정지 버튼을 누르는 순간 숫자는 다시 12로 돌아가 있다. MonoBehaviour 필드에 담긴 값은 플레이 모드가 끝나면 통째로 초기화되기 때문이다.

이 상황을 정말 여러 번 겪었다. 며칠 만에 겨우 마음에 드는 수치를 찾아놓고, 정지를 누르는 순간 날려먹는다. 그래서 메모장에 옮겨 적거나, 화면을 캡처해두거나, 끄기 전에 코드로 돌아가 다시 하드코딩하는 짓을 반복했다. 지금 생각하면 어이없는 낭비였다.

스크립터블 오브젝트를 쓰기 시작한 뒤로 이 잡일이 통째로 사라졌다.

플레이 모드에서 만진 숫자가 그대로 남는다

스크립터블 오브젝트는 씬에 붙는 컴포넌트가 아니다. 프로젝트 폴더 안에 파일로 존재하는 에셋이다. 그래서 플레이 도중에 값을 건드리면 그 변경이 에셋 파일 자체에 기록된다. 정지를 눌러도 숫자는 살아남는다.

일반 변수와 나란히 놓고 보면 차이가 선명하다.

저장 위치플레이 중 수정종료 후 값에디터에서 편집
MonoBehaviour 필드가능리셋가능(플레이 전)
일반 C# 클래스 변수코드로만리셋불가
ScriptableObject 에셋가능유지가능

표의 저 한 칸이 작업 속도를 뒤집는다. 밸런싱할 때는 플레이를 켜둔 채로 인스펙터에서 숫자를 계속 굴린다. 손대는 즉시 게임에 반영되고, 감이 오는 지점을 찾으면 그냥 정지를 누르면 된다. 그게 곧 최종 수치가 된다. 옮겨 적을 일도, 다시 코드를 열 일도 없다.

물론 함정도 있다. 이 저장은 에디터 안에서만 통한다. 빌드된 게임에서 스크립터블 오브젝트 값을 런타임에 바꿔봐야 파일로 남지 않는다. 앱을 껐다 켜면 원래대로다. 그러니 유저 세이브 데이터를 여기에 담으면 안 된다. 어디까지나 개발 편의 장치이고, 읽기 위주의 설정 데이터를 담는 그릇으로 보는 게 맞다.

기획 데이터를 코드 밖으로 빼면 벌어지는 일

내가 스크립터블 오브젝트를 가장 자주 꺼내 쓰는 영역은 기획 데이터다. 몬스터 스탯, 스킬 계수, 아이템 정보 같은 것들.

구조는 단순하다. 가운데 데이터 에셋 하나가 수치를 쥐고 있고, 캐릭터와 무기와 소모품이 거기에 매달려 있다. 그 값을 하나 바꾸면 세 방향이 동시에 움직인다. 수치의 출처가 한 군데로 모인다는 게 핵심이다.

이걸 코드에 박아두면 숫자 하나 바꿀 때마다 컴파일을 기다려야 한다. 프로젝트가 커질수록 이 대기 시간이 살인적이다. 에셋으로 빼두면 파일 하나 클릭해서 인스펙터에서 고치면 끝이다. 컴파일이 아예 없다.

더 값진 건 기획자가 직접 만질 수 있다는 부분이다. 프로그래머한테 “이 값 좀 3으로 바꿔주세요"라고 부탁하고 기다릴 필요가 없다. 본인이 인스펙터에서 조정하고 바로 플레이해서 확인한다. 이 왕복 주기가 짧아질수록 게임의 완성도가 올라간다고 믿는다. 결국 재미는 수치를 몇 번 만져봤느냐에서 나오니까.

기존 코드에서 데이터를 빼낼 때는 대충 이 순서를 밟는다.

  1. 코드 여기저기 흩어진 상수와 매직 넘버를 한 자리에 긁어모은다.
  2. 성격이 비슷한 것끼리 묶어 SO 클래스 하나로 정의한다.
  3. [CreateAssetMenu]를 붙여 우클릭 메뉴에서 에셋을 뽑을 수 있게 한다.
  4. 기존 코드가 그 에셋을 바라보도록 참조를 갈아끼운다.
  5. 인스펙터에서 값을 흔들어보며 플레이 테스트로 검증한다.

JSON이나 엑셀 대신 SO를 쓰는 이유

기획 데이터를 파일로 관리한다고 하면 대개 JSON, XML, 엑셀부터 떠올린다. 나도 그랬다. 그런데 성능 쪽에서 보면 이 선택에는 청구서가 따라붙는다.

유니티 공식 채널의 ‘알쓸유잡 라이브’에서 모바일 최적화 이북을 다룬 회차를 봤는데, 이 부분을 꽤 또렷하게 짚어준다. 골자는 이렇다. C#에서 string은 값 타입이 아니라 참조 타입이고, 힙에 할당된다. 문자열을 다룰 때마다 가비지 컬렉터가 치워야 할 쓰레기가 생긴다는 뜻이다.

JSON도 XML도 결국 텍스트 포맷이다. 이걸 런타임에 파싱하면 그만큼 가비지가 쌓이고, GC가 도는 순간 프레임이 튄다. 영상에서는 개발 단계에서 엑셀이나 JSON을 쓰는 건 문제 삼지 않는다. 대신 빌드 시점에 스크립터블 오브젝트로 자동 변환하는 파이프라인을 갖춰두라고 권한다.

이 조언이 현실적이라고 본다. 엑셀은 대량 입력에 압도적으로 편하다. 몬스터가 300종인데 SO 에셋 300개를 손으로 만드는 건 고문이다. 그래서 나는 규모에 따라 갈라 쓴다.

  • 항목이 수십 개 이하 → 처음부터 SO 에셋으로 직접 관리
  • 항목이 수백 개 이상 → 엑셀이나 JSON으로 작성하고 SO로 굽는 임포터를 만듦

어느 쪽을 택하든 게임이 도는 중에는 텍스트를 파싱하지 않는다는 원칙만은 같다.

가비지를 줄이는 습관, SO 하나로 끝나지 않는다

같은 영상에 GC 관련 조언이 줄줄이 나온다. 스크립터블 오브젝트와 결이 맞닿아 있어 함께 적어둔다. 코드 리뷰할 때 이 목록을 체크리스트처럼 훑는다.

  • 태그 비교는 gameObject.tag == "..." 말고 CompareTag()로 — 전자는 내부에서 문자열을 새로 만들어 넘긴다
  • GetComponent()는 Start에서 한 번 받아 캐싱한다 — 컴포넌트 목록을 훑는 비싼 API다
  • 상황에 따라 GetComponent() 대신 TryGetComponent()를 검토한다
  • 코루틴의 yield return new WaitForSeconds(...)는 미리 만들어두고 재사용한다 — new가 매번 할당을 부른다
  • LINQ와 정규표현식은 로딩 구간에 몰아넣고 인게임에서는 뺀다

마지막 항목을 두고 영상이 취하는 태도가 흥미롭다. 무조건 금지가 아니라, 개발 편의성과 성능 사이의 거래이므로 성능이 빡빡하지 않은 구간에서는 마음껏 써도 된다는 쪽이다. 프로토타입 단계에서 편하게 쓰다가 출시 전에 걷어내는 것도 방법이라고 덧붙인다.

동의한다. 첫 줄부터 모든 최적화 규칙을 지키려 들면 개발이 앞으로 나가질 않는다. 어디는 조이고 어디는 풀어줄지 가르는 판단이 규칙 암기보다 훨씬 중요하다.

‘느낌’으로 하는 최적화의 최후

여기서 하나는 못 박고 싶다. SO로 바꾸면 GC가 준다는 말이, 그것만으로 게임이 빨라진다는 뜻은 아니다.

영상에서 들은 이야기 중 제일 뼈아팠던 대목이 있다. 성능이 안 나오니까 아티스트에게 텍스처 해상도를 줄여달라고 요청한다. 1024를 512로, 512를 256으로, 그래도 안 되니 128까지. 그렇게 그림을 깎아냈는데도 프레임은 그대로다. 뒤늦게 프로파일링해보니 병목은 로직과 물리 쪽이었다. 텍스처와는 아무 상관도 없었다. 아티스트만 피눈물을 흘린 셈이다.

영상은 이걸 두고 “프로파일링 없이 하는 최적화는 운이 좋기를 기대하는 것"이라고 표현한다. 그러면서 프로파일링의 조건 세 가지를 이렇게 정리한다.

  1. 일찍 — 출시 몇 달 전이 아니라 개발 초기부터
  2. 자주 — 수동이든 빌드 자동화든 주기적으로
  3. 타겟 디바이스에서 — 에디터나 PC가 아니라 실제 기기에서

특히 세 번째가 결정적이다. 에디터와 빌드는 동작 방식이 다르다. 예를 들어 GetComponent()는 에디터에서 메모리 할당이 일어나지만 빌드 결과물에서는 일어나지 않는다. 에디터 수치만 보고 판단하면 있지도 않은 병목을 잡겠다고 시간을 태우게 된다.

모바일이면 발열도 변수다. 기기가 달아오르면 시스템이 클럭을 강제로 낮추는 서멀 스로틀링에 걸린다. 똑같은 시나리오인데 처음 잰 값과 5분 뒤에 잰 값이 딴판이 된다. 영상에서는 재고 식히고, 다시 재고 식히는 식으로 측정하라고 권한다. 귀찮지만 그렇게 안 하면 숫자를 믿을 수가 없다.

실제로 내가 쓰는 방식

정리하면 이렇게 굴린다.

밸런싱 단계에서는 플레이 중 인스펙터 수정을 대놓고 활용한다. 값이 살아남는다는 특성 자체가 도구다. 플레이 → 수정 → 확인 → 정지, 이게 한 사이클이고 여기서 컴파일 대기는 0초다.

디버깅 단계에서는 디버그 설정용 SO를 하나 만들어둔다. 무적 모드, 특정 스테이지 바로 진입, 재화 무한 같은 플래그를 몰아넣고 플레이 도중에 껐다 켠다. 코드를 열어 bool 하나 뒤집고 컴파일을 기다릴 이유가 사라진다.

기획 데이터는 규모를 보고 직접 관리하거나 임포터로 변환한다. 어느 길로 가든 도착지는 SO다.

그리고 이 모든 판단 위에 프로파일러가 있다. SO로 바꿔서 좋아진 것 같으면, 정말 좋아졌는지 유니티 프로파일러로 확인한다. 특정 로직이 의심되면 그 코드 앞뒤에 프로파일러 마커를 박아 어느 구간이 얼마를 먹는지 들여다본다. 영상에서도 이 마커가 의외로 안 쓰이는 기능이라고 짚는데, 나 역시 알고 난 뒤로 활용도가 확 늘었다. 짐작으로 “여기가 느린 것 같은데"라고 말하는 것과, 구간별 밀리초를 보여주며 말하는 것은 팀 안에서 무게가 다르다.

마치며

  • 스크립터블 오브젝트의 진짜 값어치는 플레이 모드에서 만진 숫자가 에디터에 그대로 남는다는 점이다. 밸런싱과 디버깅 속도가 눈에 띄게 달라진다.
  • 단, 이 저장은 에디터 한정이다. 빌드된 게임의 유저 세이브 데이터로는 못 쓴다.
  • 기획 데이터를 SO로 빼면 컴파일 없이 수정되고, 기획자가 직접 손댈 수 있다.
  • JSON이나 XML을 인게임에서 파싱하면 문자열 탓에 가비지가 쌓인다. 작성은 편한 도구로 하되 최종 형태는 SO로 굽는 게 낫다.
  • 어떤 최적화든 프로파일링이 먼저다. 일찍, 자주, 타겟 디바이스에서.

자주 묻는 질문

Q. 플레이 중에 바꾼 스크립터블 오브젝트 값이 빌드에서도 저장되나요?

아니다. 에디터에서만 유지된다. 빌드된 게임에서는 런타임에 값을 바꿔도 파일로 기록되지 않고, 앱을 재실행하면 원래 값으로 돌아간다. 유저 세이브 데이터는 따로 저장 방식을 마련해야 한다.

Q. 기획 데이터를 엑셀로 관리 중인데 전부 SO로 갈아엎어야 하나요?

작성 도구까지 버릴 필요는 없다. 엑셀은 대량 입력에 편하니 계속 쓰고, 빌드 시점에 SO로 변환하는 임포터를 두는 편이 낫다. 유니티 공식 채널에서 권하는 방식도 같다. 핵심은 인게임에서 텍스트를 파싱하지 않는 것이다.

Q. 스크립터블 오브젝트를 쓰면 성능이 좋아지나요?

런타임 텍스트 파싱이 사라지니 그로 인한 가비지는 줄어든다. 다만 그게 게임 전체 성능 개선을 보장하지는 않는다. 병목이 어디인지는 프로파일러로 확인해야 한다. 짐작으로 최적화하면 엉뚱한 데를 깎게 된다.

Q. 씬마다 데이터를 다르게 쓰고 싶은데 가능한가요?

가능하다. 같은 SO 클래스로 에셋을 여러 개 만들어두고 씬이나 상황에 따라 참조만 갈아끼우면 된다. URP에서 퀄리티별 렌더 파이프라인 에셋을 여러 벌 준비해두고 기기 사양에 맞춰 교체하는 것과 같은 발상이다.

Q. 프로파일링은 언제부터 시작해야 하나요?

프로토타이핑 단계는 넘어가도 되지만, 본격적인 개발에 들어섰다면 그때부터가 적기다. 출시 몇 달 전에 몰아서 하면 이미 구조가 굳어 손댈 여지가 별로 없다.

참고 자료

이 글 공유하기 X Facebook 네이버

이 글의 내용은 작성 시점에 직접 실행하고 확인한 결과와 글쓴이 개인의 판단을 담은 기록입니다. 도구의 버전·가격·동작은 시간이 지나면 달라질 수 있으니, 따라 하실 때는 공식 문서를 함께 확인해주세요. 자세한 내용은 이용약관에 정리해두었습니다.

03

게임 개발 |