본문으로 건너뛰기
SheetForge

시트 문법

SheetForge 시트는 자기 서술적이다: A열은 마커 전용으로 예약되어 있고, 실제 데이터는 B열부터 시작한다. 행은 위치가 아니라 마커로 식별되므로, 주석 행을 어디에나 삽입해도 아무것도 깨지지 않는다.

기존 스프레드시트를 적용하려면, 데이터 앞에 마커 컬럼 하나를 삽입하고 세 개의 마커 행을 추가한다. 기존 데이터 컬럼은 그대로 유지된다.

마커 (A열)

A열의미
#주석 행 — 완전히 무시되며, 라운드트립 시 그대로 보존된다.
@name필드명 행(컬럼당 이름 하나).
@type필드 타입 행.
@desc설명 행 — 코드젠이 이를 XML 문서 주석과 인스펙터 툴팁으로 베이크한다.
@overlap(선택) 컬럼별 중복 정책 — true(허용, 기본값) / false(값 고유성 강제).
@style(선택) 시트 표시 메타데이터 — 이 시트의 그룹 라벨과 색상. 아래 참고.
@enum(선택) 시트 전체를 데이터 테이블이 아닌 enum 정의로 표시한다. 아래 참고.
@loc(선택) 시트를 현지화 시트로 표시한다 — 각 셀이 그 컬럼의 로케일 코드를 이름 붙인다. 아래 참고.
@yourMarker(선택, 플러그인 등록) 커스텀 구조 마커 — 아래 참고.
(비어 있음)데이터 행.
  • @name, @type, @desc필수다. @overlap, @style, 그리고 커스텀 마커는 선택이다.
  • 마커 행은 데이터 행보다 위에 있기만 하면 어떤 순서로든 나타날 수 있다.
  • 알 수 없는 @marker에러이며, 최근접 일치 제안이 함께 제공된다("@desc를 의도한 것인가요?"). 등록된 커스텀 마커도 제안 후보군에 합류한다.
  • @name/@type 헤더가 없는 컬럼의 데이터는 에러다(고아 데이터 가드 — 조용한 데이터 손실은 결코 허용되지 않는다).

예시(컬럼 표시 A | B | C | D):

#        | Item definitions — hand-edited by design team
@name    | codeName      | displayName | price
@type    | RecordId      | string      | int=10
@desc    | unique key    | shown in UI | shop price (gold)
         | item.sword    | Sword       | 120
         | item.potion   | Potion      |

(item.potion의 빈 price 셀은 명시적 기본값 10을 구체화한다.)

타입 시스템

모든 타입은 자기 서술적이다 — 읽는 사람은 @type 셀만으로 그 컬럼이 무엇을 담고 있는지 알 수 있다.

표기의미
int float bool string내장 스칼라.
Enum<DamageType>C# enum — enum 시트에 정의되거나(아래 참고, 코드 불필요) 플러그인이 등록한다(EnumRegistry). 멤버 이름이 검증되며, 오타에는 최근접 일치 제안이 제공된다.
List<T>리스트 — 구분자는 ;, 각 요소는 트림되며, 빈 요소는 에러이고, 빈 셀은 빈 리스트다.
RecordId이 탭의 키 컬럼 — 문자열 자기 식별자(예: item.sword). 항상 필수 스칼라다. 권장 컬럼명: codeName.
IntId이 탭의 보조 정수 키 — 탭당 최대 하나, 필수 스칼라, 런타임/세이브/백엔드 id용. 권장 컬럼명: id. 탭은 RecordId, IntId, 또는 둘 다로 키를 잡을 수 있다.
RecordId@EffectsEffects 탭의 레코드에 대한, 그 문자열 키를 통한 참조 — 무결성 검증됨(대상 탭 존재 여부, 키 컬럼 보유 여부, id 해석 가능 여부; 오타에는 제안 제공).
IntId@EffectsEffects 탭의 레코드에 대한, 그 정수 키를 통한 참조 — RecordId@Effects와 완전한 패리티를 가진다: 동일한 방식으로 무결성 검증되며(대상 탭 존재 여부, IntId 컬럼 보유 여부, id 해석 가능 여부), 실패 시 최근접 정수 제안이 제공된다. 값은 int.ToString으로 정규화되므로, 손으로 입력한 0077로 해석된다.
AssetRef@IconsAddressables 그룹 Icons의 에셋에 대한 참조 — 카탈로그를 대상으로 존재 여부가 검증된다. 서브 에셋(텍스처 안의 스프라이트, 폰트 안의 머티리얼)은 parent[sub]로 주소가 지정된다 — Addressables가 서브 오브젝트 항목에 부여하는 주소이며(예: atlas[sword]), 그 키로 검증되고 베이크되며(SubObjectName) export된다.
AssetRef@Icons<Sprite>동일한 참조를 하나의 에셋 타입으로 제한한 것: 그 에셋 — 또는 그 서브 에셋 중 하나 — 가 Sprite로 로드될 수 있을 때만 주소가 통과한다. 이름은 프로젝트가 아는 UnityEngine.Object 파생 에셋 타입이면 무엇이든 쓸 수 있다(엔진 타입이든 직접 만든 타입이든): 정확히 하나의 타입만 일치하면 짧은 이름을, 그렇지 않으면 전체 이름(MyGame.ItemData)을 쓴다. 코드젠은 AssetReferenceT<Sprite>를 내보낸다; <…> 없는 AssetRef@Icons는 제한 없이 유지된다; AssetRef<Sprite>@Icons는 올바른 표기 제안과 함께 거부된다. 아래 타입 지정 에셋 참조 참고.
LocRef@Strings현지화 시트 Strings현지화 키에 대한 참조 — RecordId@Tab과 동일하게 무결성 검증되며(존재 여부, 최근접 일치 제안, 이름 변경 전파, 피커, 드롭다운), 그 항목의 소스 로케일 텍스트를 인라인으로 미리 보여준다. 대상 탭은 @loc을 가져야 하며(그렇지 않으면 LocRefTargetNotLocalizationSheet), @Target 없는 맨 LocRef는 거부된다. List<LocRef@Strings>LocRef@Strings?는 평소대로 조합된다. 코드젠은 평범한 LocRef 구조체를 내보낸다 — 현지화 시트 참고.
Color · AnimationCurve · Gradient내장 시각적 값 타입. 각각 아래에 나오는 압축된 텍스트 형태를 가지며, 데이터 스튜디오와 웹 앱은 원문 텍스트 대신 네이티브 색상·커브·그라디언트 에디터로 이를 편집한다; 코드젠은 UnityEngine.Color / AnimationCurve / Gradient 필드를 내보낸다.
Modifier (예시)플러그인이 등록한 커스텀 셀 타입(플러그인 작성 참고) — 예: 샘플의 stat:op:value 미니 문법. CustomType@Target도 등록만으로 동작한다. 플러그인이 IReferencingCellType을 옵트인하면, 그 컬럼은 RecordId@Target과 정확히 동일하게 동작한다 — 검증·제안·이름 변경·드로잉·선택이 모두 같은 방식이다.
Pair<T> (예시)플러그인이 등록한 래퍼 타입 — 여러 내부 T 값을 하나의 셀에 담는 제네릭 값 형태 MyWrapper<T>(예: Pair<int> = 1~2). 내부 타입은 재귀적으로 해석되므로, Pair<RecordId@Effects>, Pair<Enum<DamageType>>, 중첩된 Box<Pair<int>> 모두 동작한다. 플러그인 작성 참고.

<>@는 서로 다른 것을 의미하며 공존한다: <> = 종류/래퍼(내장 List, 또는 플러그인 MyWrapper<T>), @ = 대상. 따라서 List<RecordId@Effects>는 참조의 리스트이고, Pair<RecordId@Effects>는 두 개의 참조를 담는다 — 둘 다 Effects 탭을 향한다. 정수 키도 동일한 방식으로 조합된다: List<IntId@Effects>는 정수 키 참조의 리스트다.

래퍼 타입 (MyWrapper<T>)

플러그인은 래퍼를 등록할 수 있다 — 외부 문법(구분자, 인자 수)을 소유하고 내부 타입은 Core에 위임하는 제네릭 값 형태다. 래퍼는 어떤 내부 타입과도 조합된다. 그 안의 참조는 여전히 검증되고, 키 이름 변경 시 전파되며, 탭 이름 변경 시 재작성된다(완전한 통과 처리).

거부 규칙(List와 일관됨):

표기허용 여부이유
Pair<RecordId@Effects> · Pair<Enum<E>> · Box<Pair<int>>가능스칼라, 참조, enum, 또는 다른 래퍼 위의 래퍼.
List<Pair<int>>가능복합체의 리스트. 래퍼 자체의 구분자는 ;(리스트 구분자)와 달라야 한다 — 플러그인 작성자의 책임이다.
Pair<List<int>>불가리스트는 래퍼 안에 들어갈 수 없다(List는 항상 평평하고 최외곽에 있어야 한다는 List<List<T>>와 동일한 규칙).
Pair<int>@Effects불가래퍼는 값 형태다. @는 대신 내부 리프에 붙인다(Pair<RecordId@Effects>).
Pair<int?> · Pair<int=1>불가선택성/기본값은 필드 수준 표기이며, 내부 타입의 일부가 아니다.

필수 / 선택 / 기본값

표기의미
float(마커 없음)필수 — 빈 셀은 에러다(입구에서 조용한 오염을 차단).
float?선택 — 빈 셀은 타입 기본값(0)을 구체화하며 IsDefaulted로 표시된다. 네 가지 스칼라(int / float / bool / string)에 적용되며, 세 가지 시각적 타입에도 적용된다: Color? → 투명한 검정 #00000000, AnimationCurve? → 키가 없는 커브, Gradient? → 흰색 그라디언트 `#FFFFFF@0,#FFFFFF@1
RecordId@Effects? · IntId@Effects? · AssetRef@Icons?선택적 참조 — 빈 셀은 빈 참조를 구체화한다: "아무것도 가리키지 않음"이며, 대상 탭/그룹은 보존되고 셀은 IsDefaulted로 표시된다. 이는 깨진 참조가 아니다 — 참조 무결성 검증과 에셋 키 검증은 이를 건너뛰고, 캔버스는 이에 대한 와이어를 그리지 않으며, @overlap은 두 개의 빈 참조를 중복으로 세지 않는다. 값을 실제로 담은 셀은 이전과 정확히 동일하게 검증되므로, 선택적 컬럼에서도 오타는 여전히 잡힌다.
RecordId@Effects=동일한 것을 명시적으로 쓴 형태: 비어 있는 명시적 기본값은 위의 ?만 쓴 것과 동등하다. 비어 있지 않은 기본값(RecordId@Effects=fire)은 여전히 해석되고 여전히 무결성 검증을 받는다.
int=1명시적 기본값을 갖는 선택 — 빈 셀은 1을 구체화한다.
List<T>빈 셀은 항상 허용된다(빈 리스트).

?허용되지 않는 경우, 이유는 항상 동일하다: Core는 아무것도 없는 데서 값을 지어낼 수 없으므로, 그런 타입들은 명시적 =default가 필요하다. 여기에는 다음이 해당된다:

  • Enum<T>?
  • 플러그인 커스텀 타입 — Modifier?, Modifier@Tab? 포함
  • 래퍼 — Pair<int>?

키 컬럼은 다른 이유로 제외된다: 빈 키는 중복을 낳기 때문이다. 그래서 RecordId?(키 없는 자기 식별자 형태)와 IntId?도 거부된다.

그 밖에 의도적으로 거부되는 표기:

  • int?=1RecordId@Effects?=fire?= 둘 다 "선택"을 의미하므로, 하나만 고른다.
  • List<T>? — 리스트는 이미 빈 값을 허용한다.
  • List<List<T>> — 중첩 리스트 불가.
  • Pair<int?> — 선택성은 필드 수준이며 내부 타입의 일부가 아니다.

값 규칙

  • bool: true / false만 허용되며, 입력 시 대소문자를 구분하지 않는다. 정규 형태는 소문자다.
  • 숫자: 소수 구분자는 항상 .을 사용한다(로케일 독립적). 콤마 소수, NaN, Infinity는 입구에서 거부된다.
  • float 라운드트립: export는 가장 짧은 라운드트립 형식을 렌더링하므로, 1.01로 돌아올 수 있다 — 은 정확히 보존된다(의미적 라운드트립).
  • 마커와 enum 비교는 Ordinal 방식이다(로케일에 따른 예상치 못한 동작 없음).

타입 지정 에셋 참조 (AssetRef@Group<Type>)

AssetRef@Icons는 그룹 안의 어떤 주소든 받아들인다. AssetRef@Icons<Sprite>는 이를 하나의 에셋 타입으로 좁히며, 이 제한은 세 지점에서 검사된다: 검증, 코드 생성, 작성 표면.

  • 해석되는 이름. 타입은 프로젝트가 로드할 수 있는, UnityEngine.Object에서 파생된 에셋 타입이면 무엇이든 가능하다 — 엔진 타입(Sprite, Texture2D, AudioClip, Texture 같은 추상 베이스)과 직접 만든 ScriptableObject가 똑같이 해당된다; 허용 목록(allow-list)은 없다. 컴포넌트와 에디터 전용 타입은 후보가 아니다. 정확히 하나의 타입만 그 이름을 갖고 있으면 짧은 이름을, 그렇지 않으면 네임스페이스를 포함한 전체 이름을 적는다. 모호한 이름(AmbiguousAssetType, 모든 후보가 함께 나열됨)과 알 수 없는 이름(UnknownAssetType, 최근접 일치 제안 포함)은 컬럼당 한 번, @type 행에서 보고된다.
  • 통과하는 것. 그 주소의 에셋 — 또는 그 서브 에셋 중 무엇이든 — 이 해당 타입으로 로드 가능할 때 주소는 제한을 만족한다 — 그래서 Sprite 모드로 임포트된 텍스처는 <Sprite>를 통과하고, 일반 텍스처는 셀마다 AssetTypeMismatch로 보고된다. 서브 에셋 자신은 parent[sub]로 주소를 지정할 수 있으며, 그 키는 자신의 타입에 대해서만 검사된다.
  • 생성된 코드가 참조할 수 없는 타입은 거부된다. 미리 정의된 어셈블리(Assembly-CSharp와 그 형제들 — 어셈블리 정의가 없는 모든 스크립트 폴더)에 있는 타입은 발견은 되지만 AssetTypeNotReferenceable로 보고된다. 생성된 컴패니언 어셈블리는 그런 어셈블리를 참조할 수 없고 AssetReferenceT<T>가 컴파일되지 않기 때문이다. 그 타입을 어셈블리 정의 안으로 옮기거나, <…>를 뺀다.
  • 코드젠이 내보내는 것. 해석된 타입에는 AssetReferenceT<global::UnityEngine.Sprite>를, 제한 없는 컬럼에는 AssetReference를 내보낸다. 컴패니언 어셈블리 정의는 그 타입이 속한 어셈블리를 자동으로 참조하며, 해석된 전체 이름은 스키마 핑거프린트의 일부이므로, 이름을 다시 매핑하면 코드가 재생성된다.
  • 다른 타입과 똑같이 조합된다: AssetRef@Icons<Sprite>?, List<AssetRef@Icons<Sprite>>, 그리고 Pair<AssetRef@Icons<Sprite>> 같은 래퍼 모두 동작한다; AssetRef@Icons<>(비어 있음), AssetRef@Ic<ons(그룹 이름 안의 꺾쇠 괄호), RecordId@Skills<X>(이 제한은 AssetRef 전용)는 문법 에러다.
  • 데이터 스튜디오의 컬럼 폼에는 후보 타입을 나열하고 @type 셀을 대신 다시 써 주는 타입… 버튼이 있다 — 데이터 스튜디오 참고.

시각적 값 타입 (Color, AnimationCurve, Gradient)

세 가지 내장 타입은 원문 텍스트로는 읽을 수 없는 값을 담는다. 이들의 텍스트 형태는 사람이 짧은 버전을 손으로 입력할 수 있도록 설계되어 있는 한편, 모든 도구 — 에디터, Export, Push, 웹 앱 — 는 항상 정규화된 완전한 형태를 기록하므로, 값은 시트 → Unity → 시트를 무손실로 오간다.

구분자는 세 타입 모두 공유하며 리스트 구분자보다 한 단계 아래에 있다: 하나의 값 안에서 항목,로, 항목 안의 필드:로, 섹션|로 구분되며, 키의 시간@로 붙는다. List<>의 요소는 여전히 ;로 구분되며, 세 표기법 중 어느 것도 ;를 담지 않는다 — 그래서 List<AnimationCurve> = 0:0,1:1;0:1,1:0은 깔끔하게 나뉜다. 숫자는 어디서나 소수점으로 .을 쓰며(로케일 콤마는 조용히 잘못된 값이 되는 대신 잘못된 필드 개수로 드러난다), 구분자 주변의 공백은 트림되고, 라운드트립 parse(render(parse(x))) == parse(x)는 허용되는 모든 입력에 대해 성립한다.

타입허용 입력정규 형태
Color#RGB, #RGBA, #RRGGBB, #RRGGBBAA(대소문자 구분 없음, # 필수)색상이 불투명하면 대문자 #RRGGBB, 그렇지 않으면 #RRGGBBAA#FF8800, #FF880080
AnimationCurve`key,key,…[pre:post], 여기서 키는 t:v, t:v:in:out, t:v:in:out:inW:outW:wm, 또는 t:v:in:out:inW:outW:wm:tm`이다(2, 4, 7 또는 8개 필드 — 3, 5 및 6은 에러)
Gradient`colorKeys[alphaKeys[

색상: 값은 네 바이트로 저장된다. HDR(1을 넘는 채널)은 지원되지 않는다 — 베이크된 색상은 Export 시 0…1로 클램프된다. 타입 기본값은 투명한 검정 #00000000이다.

커브: wm은 가중 탄젠트 플래그이고(0 없음 · 1 인 · 2 아웃 · 3 양쪽), tm은 탄젠트 모드Left/Right이며, 그 뒤에 선택적으로 /broken이 붙는다 — 각 쪽은 Free, Auto, Linear, Constant, ClampedAuto 중 하나이고, Unity의 커브 에디터가 쓰는 것과 같은 이름이다. 더 짧은 형태는 나머지를 채운다: 2필드 키는 이웃으로의 기울기를 탄젠트로 취하고(Linear/Linear), 가중치는 0.33333334이며 가중 없음이다; 4필드 키는 입력한 탄젠트를 그대로 유지한다(Free/Free); 7필드 키는 가중치를 더한다. 탄젠트 필드는 Infinity 또는 -Infinity(Constant 단계)일 수 있다; 시간·값·가중치는 유한해야 하고, 키의 시간은 서로 달라야 하며(키는 임포트 시점에 시간순으로 정렬되므로 입력한 순서는 중요하지 않다), 키 개수에는 제한이 없다. 모드가 숫자를 이긴다: Free가 아닌 쪽은 임포트 시점에 그 모드로부터 탄젠트 값이 다시 계산된다 — Unity가 수행하는 것과 동일한 계산이다 — 그래서 자신의 모드와 모순되는 손 입력 숫자는 교체되며, 시트·에디터·게임은 모두 하나의 커브를 보여준다. 래핑 모드는 ClampForever, Loop, PingPong, Default이다; OnceClampForever의 별칭으로 받아들여지지만(Unity가 이를 정규화한다) 다시 기록되는 일은 결코 없다. 키가 없는 커브는 텍스트 형태가 없다: 선택적 컬럼의 빈 셀로만 존재하며, Export는 이를 빈 셀로 렌더링한다.

그라디언트: 색상 키는 알파를 담지 않는다(색상 섹션의 #RRGGBBAA는 에러다 — 알파는 자신만의 섹션을 갖는다); 한 섹션 안에서 @t 시간은 전부 있거나 전부 없어야 하며, 없을 때는 키가 균등하게 퍼진다(n = 10, n ≥ 2i/(n−1)); 알파 섹션이 없으면 1@0,1@1을 뜻하고, 모드가 없으면 Blend를 뜻한다. 모드는 Blend, Fixed(단계형), PerceptualBlend이다; 선택적 색 공간(Gamma 또는 Linear)은 PerceptualBlend가 보간하는 방식만 바꾼다. 시간과 알파는 0…1이다; 시간은 임포트 시 16비트로 양자화되며, 이는 Unity가 저장하는 방식과 정확히 같으므로 화면에 보이는 값이 곧 엔진이 갖는 값이다. 키 하나만 있는 그라디언트는 Unity를 거치며 동일한 키 두 개로 라운드트립된다 — 그림은 바뀌지 않고, 키 개수만 늘어난다.

리스트: List<Color> = #F00;#0F0, List<Gradient> = #F00,#00F;#0F0,#000 — 리스트 구분자는 그대로다.

데이터 스튜디오는 이 셀들을 네이티브 색상·커브·그라디언트 필드로 보여주고, 웹 앱은 전체 에디터가 딸린 미리보기로 보여준다 — 데이터 스튜디오SheetForge Web 참고. 둘 다 정규 형태로 기록한다; 최소 형태는 사람을 위한 것이다.

키와 고유성

  • RecordId(@ 없음)가 키 컬럼이다: 탭당 최대 하나.
    • 키 컬럼이 없는 것도 유효하다 — 다른 탭이 이 탭을 참조하기 전까지는(TargetTabHasNoKey).
    • 두 개 이상이면 에러다(MultipleKeyColumns).
    • 중복된 키 값(DuplicateRecordId)과 빈 키 셀은 에러다.
  • IntId는 보조 정수 키다: 고유성은 독립적으로 강제되며, 다른 탭이 IntId@Tab을 통해 이를 참조할 수 있다.
    • 정수 키 참조는 RecordId@Tab과 동일한 무결성 검증, 최근접 일치 제안, 이름 변경 전파, 그래프/캔버스 지원을 받는다.
  • 탭은 RecordId만, IntId만, 또는 둘 다로 키를 잡을 수 있으며, 이 세 경우는 어디서든 대칭적으로 동작한다.
    • 탭이 둘 다 가지면, RecordId가 표시/식별 값이고 정수는 그 옆에 함께 표시된다.
    • 다른 탭은 같은 레코드를 두 방식 중 하나로 가리킬 수 있다: 문자열 키로는 RecordId@ThisTab, 정수 키로는 IntId@ThisTab.
  • @overlap: 일반 컬럼은 기본적으로 중복 값을 허용한다. 컬럼의 @overlap 셀에 false를 넣으면 값 기반 고유성이 강제된다.
    • 1.01은 동일한 값으로 취급된다. 모든 요소와 그 순서가 일치하면 두 리스트는 중복으로 간주된다.
    • 두 개의 빈 참조는 서로 결코 중복이 아니다(빈 스칼라 기본값은 여전히 평범한 값이다).
    • 키 컬럼은 항상 고유하다. 키 컬럼에 @overlap true를 쓰는 것은 모순 에러다.

시트 표시 메타데이터 (@style)

@style은 그룹화와 색상 지정이 에디터 안에만 머무르지 않고 시트 안에 살도록, 시트가 자신이 속한 그룹과 자신의 색상을 스스로 말하게 한다. 이는 컬럼이 아니라 시트 자체를 서술하는 유일한 마커이므로, 그 셀들은 컬럼에 정렬되지 않는다 — B열부터 시작하는 자유로운 key=value 쌍 목록이다.

@style   | title=Combat  | color=#4D8FF0
@name    | codeName      | displayName | power
@type    | RecordId      | string      | int
@desc    | unique key    | shown in UI | attack power
         | skill.fire    | Fireball    | 12
효과
title임의의 텍스트동일한 title을 공유하는 시트는 데이터 스튜디오 사이드바에서 그 제목 아래로 묶인다. 섹션은 처음 등장한 순서대로 나타나며 시트는 섹션 안에서 자신의 순서를 유지한다. title이 없는 시트는 기본 섹션에 남는다.
color#RRGGBB(십육진수 여섯 자리)이 시트가 나타나는 모든 곳을 물들인다: 사이드바 점, 캔버스의 노드 테두리, 그리고 이 시트를 향하는 모든 포트와 와이어.
  • 두 키 모두 선택이며 순서는 상관없다. 하나만 쓰거나, 둘 다 쓰거나, 둘 다 생략해도 된다. 빈 셀은 무시된다(패딩 셀도 괜찮다).
  • 검증 에러는 모두 셀 좌표와 구체적인 수정 방법을 담아 MarkerCellInvalid로 보고된다:
    • 알 수 없는 키(최근접 일치 제안 포함)
    • 반복된 키
    • 누락된 값
    • #RRGGBB 형식이 아닌 색상
  • 세 자리 축약형(#4AF)과 색상 이름은 의도적으로 거부되므로, 값은 항상 하나의 표기법으로 라운드트립된다.
  • 표시 전용: 코드젠, 베이크, 스키마 핑거프린트는 결코 @style을 읽지 않는다. 시트 색상을 바꿔도 코드가 재생성되거나 ScriptableObject가 다시 베이크되지 않는다.
  • 라운드트립 안전: @style 행은 주석 행처럼 보존된다. 컬럼을 추가·삭제·이동·이름 변경해도 이 행은 그대로 남는다 — 그 셀들이 컬럼에 속하지 않기 때문이다. 편집은 그룹·색 ✎ 폼(데이터 스튜디오 사이드바에서 시트를 우클릭)을 거치며, 이 폼이 행을 정본 형태로 다시 기록한다.
  • @style만 있는 시트(그리고 주석)는 "아직 테이블이 아님"으로 취급된다: 필수 마커 세 개가 없다고 실패하는 대신, 임포트는 경고와 함께 이를 건너뛴다. @name/@type/@desc를 추가하는 순간 정상적으로 파싱된다. 기능 및 제한 참고.
  • style은 예약된 마커 이름이다 — 이를 등록하려는 플러그인은 거부되며, @styl 같은 오타에는 @style이 제안으로 제공된다.

Enum 정의 시트 (@enum)

Enum<T> 컬럼에는 T가 필요하다. 플러그인 C#(EnumRegistry)에서 등록할 수도 있지만, 그냥 시트에 쓸 수도 있다 — 코드도, 플러그인도 필요 없다. 다음 중 하나가 참이면 시트는 enum 정의로 읽힌다:

  • @enum 마커 행을 갖고 있거나(탭 이름은 무엇이든 상관없다), 또는
  • 탭 이름이 정확히 Enum(대소문자 구분)이며 동시에 @type 행이 없다.

두 번째 규칙은 @type이 의도적으로 없어야 한다고 요구한다: 데이터 테이블은 항상 이를 갖고 있으므로, 우연히 Enum이라 불리는 기존 테이블은 계속 테이블로 남는다. @enum@type둘 다 가진 시트는 모순이며, 추측하는 대신 EnumSheetMarkerConflict로 보고된다.

@desc는 이 판별에 관여하지 않는다 — 두 종류의 시트 모두에서 허용되며, enum 시트에서는 그 컬럼의 enum을 설명한다(아래 참고).

enum 시트는 테이블을 갖지 않는다 — 스키마도, 키 컬럼도, 레코드도 없다. 컬럼 하나가 enum 하나다: @name 셀이 enum의 이름을 담고, 그 아래의 모든 행(A열은 비워둠)이 멤버 하나다.

@enum    | byte       |
@desc    | Damage kind| Elemental affinity
@name    | DamageType | Element
         | Physical   | Fire
         | Magical=10 | Ice
         | True       | Lightning

이 시트는 두 개의 enum을 정의하며, Enum<DamageType> / Enum<Element>는 이제 모든 @type 셀에서 해석된다 — 컬럼 문법은 변하지 않는다. 위에 표시된 세 가지 부가 요소는 모두 선택이다; @name 행과 멤버만 있어도 여전히 완전한 enum 시트다.

이 골격을 직접 타이핑할 필요는 없다: 시트 생성은 시트를 대신 배치해 주는 Enum 정의 템플릿을 제공하며, 이는 두 개의 내장 템플릿 중 하나다(데이터 스튜디오 ▸ 시트 생성 / 삭제 참고).

  • 순서가 곧 값이며, Name=value가 이를 고정한다. 멤버 셀은 단순 이름이거나, 명시적 정수를 가진 Name=value다 — 정확히 C#의 enum 규칙과 같다: 번호가 없는 멤버는 이전 값에 하나를 더한 값이고, 첫 번째 멤버는 0이다.
    • Normal / Rare=10 / Epic0 / 10 / 11로 컴파일된다. 코드젠은 사용자가 값을 쓴 곳에서만 = value를 내보낸다.
    • 데이터 셀과 드롭다운은 항상 이름을 사용한다(Rare이지, Rare=10이 아니다).
    • 단순 정수가 아니거나, underlying type의 범위를 벗어나는 값(자동 증가로 인한 경우 포함)은 InvalidEnumMemberValue다.
    • 이 때문에 데이터 스튜디오는 결코 멤버 순서를 재배치하지 않으며 빈 구멍을 메우지도 않는다: 멤버를 옮기면 이미 에셋에 베이크되고 세이브 파일에 저장된 값이 조용히 바뀌어 버리기 때문이다.
  • @desc는 enum을 설명한다. 컬럼의 @desc 셀은 생성된 코드에서 그 enum의 XML <summary>(IDE 툴팁)가 된다 — 데이터 테이블 필드의 @desc와 같은 취지다. 빈 셀 = 설명 없음; 마커 행 자체도 선택이다.
  • @enum 셀은 underlying type을 고른다. 컬럼의 @enum 행 셀은 그 enum의 C# underlying type을 지정할 수 있다 — byte, sbyte, short, ushort, int, uint, long, ulong 중 하나.
    • 빈 셀(또는 Enum이라는 이름의 탭에 @enum 행 자체가 없는 경우)은 int를 의미한다. 그 외의 값은 InvalidEnumUnderlyingType이다.
    • 코드젠은 public enum Grade : byte { … }를 내보낸다.
    • ulong의 경우, long.MaxValue를 초과하는 명시적 값은 시트에서 지원되지 않는다 — 대신 플러그인 C#에서 그런 enum을 등록하라.
  • 빈 셀은 건너뛰며 멤버로 읽히지 않으므로, 컬럼마다 길이가 다를 수 있고 중간의 구멍은 그냥 지나쳐진다.
  • 주석 행(#)은 시트 어디에 있든 무시된다. 한 시트에 여러 enum, 여러 개의 enum 시트 모두 괜찮다. 이름은 이들 전체에서 고유해야 하며, 플러그인이 C#에서 이미 등록한 이름이 우선한다(시트 쪽 정의는 DuplicateEnumName으로 거부된다).
  • 이름과 멤버는 C# 식별자로 사용 가능해야 한다: ASCII 문자, 숫자, _, 숫자로 시작하지 않고, 예약어가 아니어야 한다(InvalidEnumIdentifier).
    • 비-ASCII는 의도적으로 거부된다. 겉보기에 비슷한 유니코드 식별자는 서로 구별할 수 없는 타입을 만들어낼 것이기 때문이다.
    • 아래에 멤버가 없는 선언된 이름은 EnumSheetEmptyColumn이다.
    • 한 컬럼의 멤버 중 하나라도 실패하면, 그 enum 전체가 절반만 등록되는 대신 통째로 버려진다.
  • 임포트가 생성하는 것. 프로젝트 전체를 위한 SheetForgeEnums.cs 하나 — enum은 탭별 산출물이 아니라 프로젝트 수준 산출물이다 — 설정의 생성 코드 폴더에, 생성된 탭 타입들과 동일한 네임스페이스로 기록된다. 첫 임포트가 타입을 생성하고 컴파일하며, 도메인 리로드 이후 추가 클릭 없이 베이크를 완료한다.
  • 시트를 열지 않고 멤버 추가하기: 데이터 스튜디오에서 Enum<T> 셀의 드롭다운에는 **"새 멤버 추가…"**가 있으며, 이는 하나의 undo 단계로 enum 시트에 그 멤버를 스테이징한다. 플러그인 C#에서 등록된 enum은 이 행을 제공하지 않는다 — 코드가 소유하기 때문이다.
  • enum 시트는 레코드가 없으므로 결코 ScriptableObject로 베이크되지 않으며 Export/Push는 그 텍스트를 건드리지 않는다. 임포트는 이를 건너뛴 탭과는 별도로 보고한다.
  • 두 가지 경계는 기능 및 제한을 참고: 플러그인이 등록한 enum은 시트에서 확장할 수 없으며, 생성된 enum 파일은 항상 설정 폴더에 위치한다.

현지화 시트 (@loc)

@loc 마커 행은 시트를 현지화 시트로 바꾼다: 행은 키, 컬럼은 로케일이며, 각 로케일 컬럼의 @loc 셀이 그 로케일 코드를 이름 붙인다.

  • RecordId 키 컬럼이 필수다 — 키 값이 곧 현지화 키다.
  • 첫 번째 로케일 컬럼이 소스 로케일이다.
  • 로케일 컬럼은 문자열 컬럼이다. string?이 권장 형태다: 빈 셀은 그때 에러가 아니라 커버리지 공백이 된다.
  • 두 개의 선택적 컬럼이 이름으로 예약되어 있다: smart(bool)와 comment(string).
@loc     |            | en          | ko    |
@name    | codeName   | en          | ko    | comment
@type    | RecordId   | string?     | string? | string?
@desc    | key        | source text |       |
         | ui.ok      | OK          | 확인  | Confirm button

시트는 편집, Export, Push, xlsx, 웹 앱에 대해 여전히 평범한 테이블로 남는다. 달라지는 것은 출력이다: 레코드 클래스도 Database SO도 없지만, 탭별 키 상수와 — Unity Localization 패키지가 설치되어 있으면 — StringTable 동기화가 생긴다. 같은 시트에 @enum@loc이 함께 있으면 충돌 에러다.

전체 이야기 — LocRef 참조, 민팅, 브리지, 번역 워크플로우 — 는 현지화 시트에 있다.

커스텀 구조 마커 (플러그인 등록)

@overlap컬럼별 마커의 내장 예시다: 각 셀이 컬럼당 하나의 값을 담고, 컬럼별로 검증되는 마커 행이다. 플러그인은 동일한 방식으로 자신만의 마커를 등록할 수 있다 — 예를 들어 각 숫자 컬럼이 어떻게 보간되는지 주석을 다는 @curve 마커.

이 값은 검증기, 엣지 기여자, 작성 창의 컬럼 헤더 툴팁이 읽을 수 있는 도메인 무관 메타데이터(FieldSchema.MarkerValues)로 저장된다. Core는 결코 그 값 자체를 해석하지 않는다 — 검증은 마커 정의에 위임된다.

  • 등록된 커스텀 마커는 @overlap과 정확히 동일하게 취급된다: 데이터 위라면 순서는 상관없고, 중복은 거부되며, 데이터 아래의 마커는 에러다.
  • 각 마커는 오직 컬럼별 값 검증(빈 셀의 의미 포함)만을 소유한다 — 행 전체의 파싱을 넘겨받지 않는다. 데이터 "형태"는 정규화(참조, List<T>, type 컬럼)의 영역으로 남는다.
  • 커스텀 마커는 새로운 데이터 형태가 아니라 컬럼 수준 메타데이터를 위한 것이다. 등록 예시는 플러그인 작성 §4.5를 참고.
  • @style은 컬럼별이 아닌 유일한 내장 마커다(시트 자체를 서술하므로) — 그러니 이를 본뜨지 말고, @overlap을 본뜬다.

복잡한 데이터 구성하기: 정규화 우선

복잡한 구조를 표현하는 권장 방법은 참조 조립("스크립트 대신 조립하라")이다.

  • 원자(atom)는 자신의 탭에 행으로 존재한다.
  • 조합은 참조 리스트다: List<RecordId@Effects>.
  • type 컬럼(enum)은 데이터 행을 코드 원자와 연결한다 — 런타임은 이를 switch하여 동작을 디스패치한다. 내장 스크립팅 언어는 필요 없다.

미니 문법(attack:add:10과 같은 커스텀 셀 타입)은 작은 튜플을 위한 것이다 — Core는 ;: 관례를 제공하지만, 과도하게 사용하지는 말 것.

진짜로 절차적인 일회성 로직에는, 이미지를 참조하는 것과 같은 방식으로 스크립트 에셋을 참조한다: List<AssetRef@Scripts>. SheetForge는 참조를 검증하고 addressable을 베이크한다. 스크립트를 실행하는 것은 게임의 몫이다.

특수한 데이터 "형태": 까다로워 보이는 데이터(레벨 곡선 등)조차 깔끔하게 정규화된다(List<float>, 참조 조립). 커스텀 구조 마커는 새로운 데이터 형태가 아니라 (컬럼별로 검증되는) 컬럼 수준 메타데이터를 추가할 뿐이다 — 먼저 데이터를 정규화하고, 커스텀 마커는 정말로 장황한 컬럼별 주석에만 사용한다. 플러그인 작성 참고.

관련 페이지