시트 문법
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@Effects | Effects 탭의 레코드에 대한, 그 문자열 키를 통한 참조 — 무결성 검증됨(대상 탭 존재 여부, 키 컬럼 보유 여부, id 해석 가능 여부; 오타에는 제안 제공). |
IntId@Effects | Effects 탭의 레코드에 대한, 그 정수 키를 통한 참조 — RecordId@Effects와 완전한 패리티를 가진다: 동일한 방식으로 무결성 검증되며(대상 탭 존재 여부, IntId 컬럼 보유 여부, id 해석 가능 여부), 실패 시 최근접 정수 제안이 제공된다. 값은 int.ToString으로 정규화되므로, 손으로 입력한 007은 7로 해석된다. |
AssetRef@Icons | Addressables 그룹 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?=1과RecordId@Effects?=fire—?와=둘 다 "선택"을 의미하므로, 하나만 고른다.List<T>?— 리스트는 이미 빈 값을 허용한다.List<List<T>>— 중첩 리스트 불가.Pair<int?>— 선택성은 필드 수준이며 내부 타입의 일부가 아니다.
값 규칙
- bool:
true/false만 허용되며, 입력 시 대소문자를 구분하지 않는다. 정규 형태는 소문자다. - 숫자: 소수 구분자는 항상
.을 사용한다(로케일 독립적). 콤마 소수,NaN,Infinity는 입구에서 거부된다. - float 라운드트립: export는 가장 짧은 라운드트립 형식을 렌더링하므로,
1.0이1로 돌아올 수 있다 — 값은 정확히 보존된다(의미적 라운드트립). - 마커와 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이다; Once는 ClampForever의 별칭으로 받아들여지지만(Unity가 이를 정규화한다) 다시 기록되는 일은 결코 없다. 키가 없는 커브는 텍스트 형태가 없다: 선택적 컬럼의 빈 셀로만 존재하며, Export는 이를 빈 셀로 렌더링한다.
그라디언트: 색상 키는 알파를 담지 않는다(색상 섹션의 #RRGGBBAA는 에러다 — 알파는 자신만의 섹션을 갖는다); 한 섹션 안에서 @t 시간은 전부 있거나 전부 없어야 하며, 없을 때는 키가 균등하게 퍼진다(n = 1 → 0, n ≥ 2 → i/(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.0과1은 동일한 값으로 취급된다. 모든 요소와 그 순서가 일치하면 두 리스트는 중복으로 간주된다.- 두 개의 빈 참조는 서로 결코 중복이 아니다(빈 스칼라 기본값은 여전히 평범한 값이다).
- 키 컬럼은 항상 고유하다. 키 컬럼에
@overlaptrue를 쓰는 것은 모순 에러다.
시트 표시 메타데이터 (@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 / Epic은0 / 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>, 참조 조립). 커스텀 구조 마커는 새로운 데이터 형태가 아니라 (컬럼별로 검증되는) 컬럼 수준 메타데이터를 추가할 뿐이다 — 먼저 데이터를 정규화하고, 커스텀 마커는 정말로 장황한 컬럼별 주석에만 사용한다. 플러그인 작성 참고.