진행 규칙과 게임 액션
이 규칙은 자신의 스크립트나 커뮤니티 연동에서 내보내기 데이터를 처리하는 방법을 설명합니다. JSON에는 실행할 소스 코드가 아닌 데이터가 들어 있습니다. 편집기 배치, 선택 상태, 내부 화면 구성은 게임을 제어하지 않습니다.
첫 번째 단계 전에
format = vnle.story, containerVersion = 1인지 확인하고 execution.contractVersion과 requiredCapabilities를 지원하는지 검사하세요. 예제는 1.1과 core.v1, substory.call-return.v1을 사용합니다. JSON Schema는 구조를 검사하지만, 실행하려면 참조, 자료형, 지원 명령도 검사해야 합니다. 알 수 없는 노드 종류를 알림 없이 건너뛰지 마세요.
각 노드의 의미
| kind | 동작 |
|---|---|
dialogue | 텍스트를 표시하고 Continue를 기다린 뒤 continuation을 따릅니다. |
choice | optionOrder 순서를 따르고 option.condition으로 선택지를 걸러 냅니다. 선택한 답변의 effects를 한 번 적용하고 continuation을 따릅니다. whenEmpty도 처리해야 합니다. |
branch | condition을 평가하고 whenTrue 또는 whenFalse를 따릅니다. |
action | effects를 순서대로 평가한 뒤 continuation을 따릅니다. |
jump | 새 반환 지점을 저장하지 않고 대상 시작점으로 이동합니다. |
call | continuation을 기억한 뒤 대상 시작점으로 들어갑니다. |
return | 가장 최근에 저장한 다음 진행 지점에서 재개합니다. Call 없이 Return을 실행하면 오류입니다. |
command | 인수를 평가하고 게임 액션을 요청한 뒤 결과를 기다립니다. 결과에 맞는 경로를 따릅니다. |
end | 하위 이야기 안에서도 현재 대화 전체를 끝냅니다. Return은 호출한 쪽으로 돌아가 진행을 재개합니다. |
조건과 자료형이 지정된 값
피연산자는 자료형이 지정된 값이 있는 literal이거나 variableRef가 있는 variable입니다. compare는 eq, ne, lt, lte, gt, gte를 지원합니다. all은 모든 하위 조건, any는 하나 이상의 하위 조건이 참이어야 하며, not은 하위 조건의 참과 거짓을 뒤집습니다. 불리언과 문자열 "true"/"false"를 혼동하지 마세요.
변수 효과는 set, add, subtract입니다. add/subtract는 −2147483648부터 2147483647까지의 정수를 사용합니다. 효과를 순서대로 평가하되, 하나라도 유효하지 않으면 일부 변경만 확정하지 마세요. 대화가 시작될 때마다 저장된 값을 초기 defaultValue로 덮어쓰면 안 됩니다.
점프 · 구매 후 창고로 이동
이동
항구의 열쇠를 이미 샀다면 Warehouse의 시작점으로 바로 이동합니다. target에는 flowRef와 entryRef가 있고, execution.entries[entryRef].nodeRef가 다음 노드를 가리킵니다. Jump는 엔진의 장면이나 맵을 자동으로 열지 않습니다. 그러려면 적절한 게임 액션이 필요합니다.
{
"flowRef": "1f69a7b6-1bdc-4dff-b8ee-9b2819b35168",
"id": "2e1710dc-c32f-418f-abca-e055d6fd28f3",
"kind": "jump",
"target": {
"entryRef": "a4dd48dc-b0e2-47fc-b102-def52583a8fe",
"flowRef": "2a0a259c-b76a-4de0-8cee-567713c2d320"
}
}Call Substory / Return · 미라가 창고를 설명하는 과정
서브스토리 호출 / 복귀
이 진행도 JSON에 정의되어 있습니다. 인사말 뒤에서 잠긴 창고에 대한 설명을 호출합니다. 게임은 continuation을 스택에 저장하고 target으로 들어갑니다. 미라의 설명 후 return에 도달하면 가장 최근 continuation을 꺼내 “Already have the key?”를 실행합니다. 중첩 호출도 같은 규칙을 따릅니다.
{
"continuation": {
"kind": "node",
"nodeRef": "95e9a55f-f6b1-4696-9c52-6ea3ea2ac7e5"
},
"flowRef": "1f69a7b6-1bdc-4dff-b8ee-9b2819b35168",
"id": "85b3aadf-2b95-4150-bc44-c0c796337874",
"kind": "call",
"target": {
"entryRef": "2bac8555-3cae-4a21-9b8c-b410ab207ebb",
"flowRef": "ea13b52e-092a-44ff-b087-0ce65d040c9a"
}
}{
"flowRef": "ea13b52e-092a-44ff-b087-0ce65d040c9a",
"id": "f8b8673a-cb86-40ae-a4f6-d37a5c1563c3",
"kind": "return"
}관련 노드는 모두 내보낸 story.json에 있습니다. Call은 다른 JSON 파일이나 스크립트를 불러온다는 뜻이 아닙니다. maxCallDepth는 호출 중첩 깊이를 제한합니다. 스택이 비어 있을 때 Return을 실행하면 오류입니다. End는 대화 전체를 끝내고 스택을 비웁니다.
Command · 상점을 열거나 열쇠 주기
명령
“Open shop”을 예로 들 수 있습니다. 게임이 자신의 상점을 열고 대화를 일시 정지합니다. 상점이 닫힌 뒤 purchased나 cancelled처럼 정해 둔 결과를 보고합니다. 이름과 인수는 해당 VNLE 프로젝트의 게임 액션으로 정의하세요. open_shop은 VNLE 내장 명령이 아닙니다.
실제 항구 내보내기는 give_item을 사용합니다. 명령은 정의를 참조하고 아이템 ID와 두 결과를 제공합니다. 게임은 열쇠를 인벤토리에 추가하거나 전달을 거절합니다. 이야기 작성자가 두 결과의 다음 경로를 이미 정해 놓았습니다.
{
"arguments": {
"3cac6185-4404-4a2a-be5f-14afa0f97137": {
"kind": "literal",
"value": {
"entityType": "item",
"type": "reference",
"value": "2d71e57e-9b58-4fdb-a33f-42b89bd8bf04"
}
}
},
"commandRef": "0723beb0-daba-4602-a665-4efc27511bc3",
"flowRef": "1f69a7b6-1bdc-4dff-b8ee-9b2819b35168",
"id": "500c28a1-ed85-4c36-9a38-bedacce1c6b7",
"kind": "command",
"outcomes": {
"12bc86a9-b2eb-4eb4-836c-a9efd936bf1b": {
"continuation": {
"kind": "node",
"nodeRef": "382640aa-5b50-4715-9148-b470bc2f7cd6"
},
"resultBindings": {}
},
"1c527580-69a6-4e99-8503-1b0de3fc8e78": {
"continuation": {
"kind": "node",
"nodeRef": "221d6b0c-99e1-45a8-bc73-768c4ee679eb"
},
"resultBindings": {}
}
}
}{
"id": "0723beb0-daba-4602-a665-4efc27511bc3",
"key": "give_item",
"name": "Give item",
"category": "custom",
"inputs": [
{
"id": "3cac6185-4404-4a2a-be5f-14afa0f97137",
"name": "Item",
"valueType": {
"entityType": "item",
"kind": "reference"
}
}
],
"outcomes": [
{
"id": "12bc86a9-b2eb-4eb4-836c-a9efd936bf1b",
"name": "Received",
"results": []
},
{
"id": "1c527580-69a6-4e99-8503-1b0de3fc8e78",
"name": "Declined",
"results": []
}
]
}- execution.commands에서 commandRef의 시그니처를, content.commandDefinitions에서 읽기 쉬운 키를 찾으세요.
- 입력 ID를 사용해 인수를 평가하세요. 빠진 선택 인수에는 선언된 defaultValue를 사용합니다.
- 명시적으로 지원하는 게임 액션을 한 번만 요청하세요. 기다리는 동안 Continue나 중복 실행을 허용하지 마세요.
- 결과 ID와 자료형이 지정된 결과 값을 검증하세요. outcome.resultBindings로 결과를 이야기 변수에 할당한 뒤 outcome.continuation을 따릅니다.
- 기술적 실패가 작성자가 정한 취소 결과와 자동으로 같아지는 것은 아닙니다. 오류를 표시하고 게임에서 재시도나 취소를 명확히 처리하세요.
이 프로젝트에서는 열쇠 전달을 거절하면 금화 다섯 개를 돌려줍니다. 내보낸 다음 경로가 처리하는 것이며 모든 Command에서 자동으로 발생하지는 않습니다. 인벤토리, 퀘스트 기록, 엔진 맵은 게임의 시스템입니다. JSON 카탈로그 항목만으로 만들어지지 않습니다.
자리표시자 · “금화가 5개 남았습니다”
텍스트와 자리표시자
{
"continuation": {
"kind": "node",
"nodeRef": "2e1710dc-c32f-418f-abca-e055d6fd28f3"
},
"flowRef": "1f69a7b6-1bdc-4dff-b8ee-9b2819b35168",
"id": "0d7f959e-8fba-4882-ae98-b66d8416d4d2",
"kind": "dialogue",
"speakerRef": "807b2957-7bab-4a28-83a4-1f3d1589bbc2",
"text": {
"bindings": {
"1084cf8e-515e-4b33-92f8-b5c260e88a9e": {
"kind": "variable",
"variableRef": "a547610e-661f-4b8d-a25b-21d042f1a2d5"
}
},
"textRef": "fcc775ff-558e-4a41-823d-183aaff03e4b"
}
}kind: text 세그먼트는 그대로 이어 붙이세요. kind: placeholder는 placeholderRef로 텍스트 기록의 정의와 node.text.bindings의 바인딩을 찾습니다. 바인딩은 리터럴 값이나 변수 값을 제공합니다. 대사를 표시할 때 값을 확정하고 자료형에 맞게 형식을 지정하세요. 자료형과 서식 옵션은 필드 참조에 있습니다.
진행, 제한 및 오류
위치, 이야기 변수, 반환 스택, 대기 중인 게임 액션을 게임 상태와 함께 저장하세요. 언어는 별도 게임 설정입니다. storyBuildId는 저장 데이터를 해당 실행 버전과 연결하는 데 도움이 됩니다. 호환되지 않는 이야기에서 다음 진행을 추측하지 마세요.
maxAutomaticTransitions, maxConditionDepth, maxConditionTerms, maxCallDepth는 자동 진행이나 깊은 중첩을 제한합니다. 없는 참조, whenEmpty: fault 상태에서 답변 없음, 알 수 없는 명령, 잘못된 자료형에는 명확한 오류가 필요합니다. UI를 다시 그린다고 구매나 변수 변경을 반복해서는 안 됩니다.
개발자나 AI에 연동 작업 요청하기
실제 내보내기 파일, 엔진 버전, 대상 플랫폼, 이 진행 규칙과 엔진 안내 페이지를 제공하세요. UI 요소와 기존 인벤토리 또는 상점 기능을 설명하세요. 실제 JSON 필드와 문서화된 엔진 API를 사용하고 아래 항목을 시험하도록 요청하세요.
- 금화 10개: 열쇠를 받고 5개가 남습니다. 금화 2개: 열쇠를 받지 못하고 금화도 줄지 않습니다.
- 인벤토리가 거절하면 이야기에서 정한 대로 환불합니다. 성공한 것으로 처리하면 안 됩니다.
- Call/Return은 의도한 노드에서 재개합니다. Call 안의 End는 대화 전체를 끝냅니다.
- 언어를 바꿔도 진행 상황이 유지되고, 누락된 번역과 의도적으로 빈 텍스트가 올바르게 처리되어야 합니다.
- 배포한 JSON, 이미지, 글꼴이 개발 폴더 밖에서도 작동해야 합니다. 이미지가 없을 때 이전 초상화가 남아 있으면 안 됩니다.