AFM(Apple Foundation Models)을 활용한 텍스트 어드벤처 게임 개발 [제1회] 전체 설계와 데모 '피의 연못의 밤'
요약
Apple Foundation Models(AFM)의 온디바이스 기능을 활용하여, 플레이어가 자유롭게 대화할 수 있는 텍스트 어드벤처 게임을 개발하는 과정을 소개합니다. 이 데모는 선택지 기반이 아닌 자연어 커맨드를 통해 등장인물과 상호작용하며 시나리오와 잡담을 진행하도록 설계되었습니다.
핵심 포인트
- AFM 온디바이스 모델로 텍스트 어드벤처 게임 구현
- 자유 대화(Freeform Dialogue)를 통한 몰입감 증대
- LLM 활용 시, 등장인물별 상호작용 로직 설계 중요
- 개발 과정에서 발생한 실패 사례와 구조적 접근법 공유
서론
※본 기사는 필자의 Qiita 계정에서 전재된 내용입니다 (멀티 포스팅).
현재 오이타현을 배경으로 한 어드벤처 게임을 개발하고 있습니다. '포트피아 연쇄살인사건'이나 '오호츠크에 사라지는' 같은, 그 시대의 게임 분위기를 목표로 하고 있습니다. 다만 이번에는 Apple Foundation Models(이하 AFM)의 온디바이스 모델을 활용하여, 커맨드를 지정하는 대신 자신의 말로 등장인물에게 말을 걸 수 있는 텍스트 어드벤처 게임으로 개발하고 있습니다.
게임 자체는 대체로 완성되어 시나리오 미세 조정과 디버깅에 착수하고 있습니다. 개발을 진행하면서 소규모 LLM을 게임에 응용할 때의 지식이 조금 쌓였습니다. 그래서 본편 공개에 앞서 엔진의 구조를 시험해 볼 수 있는 작은 데모 '피의 연못의 밤'을 만들었습니다. GitHub에도 소스 코드를 공개했습니다.
이 연재에서는 텍스트 어드벤처 게임에서 LLM을 어디에 사용할지, 등장인물다운 대화를 어떻게 만들지, 그리고 매번 다른 답변을 하는 게임을 어떻게 테스트할지를 구현과 개발 과정에서 발생한 실패 사례를 곁들여 소개합니다.
제1회는 데모의 내용과 엔진 전체 설계를 설명합니다. Swift의 세부적인 구현은 다음 회차에 다루겠습니다.
참고로, 이 기사는 2026년 10월 10일 시점의 데모 구현을 대상으로 합니다. 측정값에는 각각의 측정일과 환경을 병기했습니다.
1. 데모 '피의 연못의 밤'에서 할 수 있는 것
무대는 벳푸에 있는 스낵바 '피의 연못'. 주인공은 직원인 '아오이'와 식사를 마치고, 개점 전 가게로 돌아옵니다.

카운터에서는 '마마'가 잔을 닦으며 준비하고 있습니다. 여기서부터 '젊은 손님들에게도 오게 하려면 무엇을 바꿔야 할까'라는 대화가 시작됩니다.
플레이어는 예를 들어 다음과 같이 입력할 수 있습니다.
이 가게 입구는 들어가기 어렵지 않아?
흘리는 음악을 바꿔보면 어떨까?
간판을 새로 하는 건 어때?
화면에 나열된 선택지를 고르는 대신, 자신의 말로 화제를 던집니다. 가게 안에서는 마마의 의견을 듣고, 대기실에서는 아오이와 단둘이 상담할 수 있습니다. (본편에서는 수사를 위해 대기실에 들어가게 되므로 그 사양이 이어지고 있습니다. 현실의 가게에서는 손님이 백야드에 들어가는 것은 금기입니다.)
아오이, 잠깐 대기실에서 이야기하자.
장소를 바꾸면 거기에 있는 인물이나 대화 내용도 바뀝니다. 대화 상대는 문장의 맨 앞에서 이름을 부르거나, 화면 메뉴로 지정할 수 있습니다. 이동 장소 역시 화면에서 선택할 수 있습니다.
그 자리에 등장인물이 여러 명 있을 때, 누구에게 말을 걸었는지 자유 입력만으로 판단하는 것은 어려운 과제입니다. 현실이라면 시선 등이 단서가 되지만, 텍스트에서는 그러한 정보가 없기 때문에 이름 부르기나 화면 선택을 사용하고 있습니다.
가게 상담 외에도, 마마나 아오이가 좋아하는 것 또는 평소 생활에 대해 듣거나, 플레이어 자신의 취향을 이야기할 수 있습니다.
아오이, 좋아하는 영화는?
나는 재즈를 좋아해.
이 데모에는 클리어 조건이나 엔딩이 없습니다. 가게 안과 대기실을 오가며 시나리오 대화, 자유로운 잡담, 기억의 메커니즘을 시험하기 위한 것입니다.
2. 왜 AFM을 사용하는가
Apple의 Foundation Models framework에는 Apple Intelligence의 온디바이스 모델 외에도 클라우드상의 Private Cloud Compute (PCC)에서 동작하는 모델도 준비되어 있습니다. PCC 모델은 PrivateCloudComputeLanguageModel로, 온디바이스 모델은 SystemLanguageModel로 제공됩니다. 자세한 내용은 Apple의 PCC 모델 설명과 SystemLanguageModel 설명을 참조해 주십시오.
다만, PCC 이용에는 조건이 있습니다. 2026년 10월 13일 시점의 Apple 안내에 따르면, App Store Small Business Program 가입, 각 앱의 App Store 최초 다운로드 수가 200만 미만이어야 하며, 계정에 PCC 사용 권한(entitlement) 부여가 필요합니다. 이번 개발 환경에서는 현시점에서 PCC를 이용할 수 없으며, 또한 온디바이스에서 처리하는 것이 목적이기도 하므로, PCC에서의 동작이나 성능은 미검증입니다.
이번 데모에서는 SystemLanguageModel.default를 사용하여 입력 해석·자유 대화·기억 인출을 모두 온디바이스에서 처리합니다. 모델이 이용 가능한 상태라면, 인터넷 연결 없이도 작동합니다. PCC로의 전환이나 폴백(fallback)은 진행하지 않았습니다.
이번 게임에서 기기 내 모델(on-device model)을 선택한 이유는, 반복적인 대화를 외부 LLM API에 연결하지 않고도 구동하고 싶었기 때문입니다. 이 구조라면 플레이어가 입력한 말을 외부에 보내지 않고 처리할 수 있으며, 외부 API 호출 비용도 발생하지 않습니다.
한편, 개발 과정에서 화제 착오나 같은 이야기의 반복이 발생했습니다. 자유롭게 대화하는 것과 저희가 의도한 게임 진행이 성립되는 것은 별개로 생각해야 한다는 것을 느꼈습니다.
그래서 이번에는 AFM에게 맡길 작업을 좁히고, 게임의 진행을 코드와 스크립트로 제어하는 설계로 했습니다.
3. 설계 방침: 이야기의 진행은 스크립트와 규칙으로 결정한다
이번 구성에서는 장면의 흐름, 배경 설명(地の文), 중요한 대사, 장소 이동 등을 스크립트와 규칙 엔진이 결정합니다. AFM은 플레이어의 말을 스크립트에 연결하고, 정형화된 대사가 없는 대화에 대한 답변을 생성합니다.
예를 들어, 가게 음악 이야기가 나올 때는 스크립트에 다음 대화가 쓰여 있습니다.
마마: 음악은 쇼와 가요(昭和歌謡)야. 정해져 있잖아. ……아오이의 영화 음악? 그거, 안 졸려?
아오이: 실례네. 밤에 어울리는 건 저런 거야.
이것은 실제 스크립트에 있는 정형화된 대사이며, LLM 생성 예시가 아닙니다.
마마는 쇼와 가요를 틀고 싶어 합니다. 아오이는 영화 음악이 밤에 어울린다고 생각하며, 마마의 말에 반박합니다. 이 인물들 간의 주고받음은 작가가 쓴 대로 출력됩니다. 대사에 수반되는 표정 등의 연출도 스크립트에서 지정합니다.
다만, 플레이어에게 '음악에 대해 이야기하기'라는 버튼을 누르게 할 필요는 없습니다. '다른 곡을 틀어볼까?'라는 말을 해당 화제에 대한 입력으로 해석합니다.
담당 업무를 정리하면 다음과 같습니다.
| 처리 | 담당 |
|---|---|
| 플레이어의 자유 입력이 어떤 화제/이동에 해당하는지 해석 | AFM과 보정 규칙(補正のルール) |
| ... | |
| 말의 해석에는 유동성이 있지만, 규칙 엔진에 의한 진행은 같은 스크립트・상태・해석된 입력이라면 동일한 결과가 나옵니다. 자유 대화로 생성되는 답변 문구는 계속해서 유동성이 있습니다. |
이 분담에는 테스트상의 이점도 있습니다. 생성된 대사의 전문을 고정하지 않아도 '음악 화제로 처리되었는지', '스크립트의 정해진 줄이 나왔는지'를 확인할 수 있기 때문입니다.
엔진 전체 구조
먼저, 입력부터 화면 표시까지의 전체 구조를 봐주세요. 플레이어에 대한 기억 추출과 그 이후 대화 반영도 포함합니다. 기억의 자세한 구현은 3회에서 다루겠지만, 이번에는 그 역할을 소개합니다.

파란색 상자가 AFM, 노란색 상자는 코드로 처리하는 규칙, 초록색 상자는 스크립트 데이터입니다. 먼저, 파란색 세 개의 상자와 중앙의 규칙 엔진에 주목해 주세요.
① 입력 해석: 플레이어의 말을 화제나 이동 후보에 대입합니다. -
② 자유 대화: 정형화된 대사로 받지 않는 대화에 인물로서 답변을 합니다. -
③ 기억 추출: 플레이어가 말한 자신에 대한 것을 기억하고, 그 발언을 들은 인물의 다음 대화에 사용합니다.
중앙의 규칙 엔진이 해석된 입력을 받아 스크립트 줄을 선택하고, 장면이나 플래그를 업데이트합니다. 그림 각 상자 아래에는 대응하는 타입・함수・파일 이름도 적혀 있습니다. 후보 생성이나 해석 보정, 출력 검사 등의 세부 구현은 다음 회차부터 설명하겠습니다.
여기서는 이 전체 구조에 따라 AFM의 3가지 역할을 살펴보겠습니다.
4. AFM이 담당하는 3가지 역할
4.1 플레이어의 자유 입력을, 현재 선택 가능한 후보로 분류하기
첫 번째 역할은 플레이어의 문장을 스크립트에 있는 화제나 이동 후보에 대입하는 것입니다.
틀어볼 곡을 바꿔보면 어떨까? → 의도: 음악
대기실에서 이야기하자. → 이동: 대기실
위 예시에서는 '틀어볼 곡을 바꿔보면 어떨까?'를 '음악에 대해 이야기하고 싶다', '대기실에서 이야기하자'를 '대기실로 이동하고 싶다'라는 의미의 입력으로 취급합니다. 의도: 음악과 이동: 대기실은 각각의 해석을 게임 내에서 표현하기 위한 기호입니다. 다만, AFM이 항상 이렇게 해석할 수 있는 것은 아니며, 다른 화제나 이동으로 착오하기도 합니다.
해석 후보는 고정적이지 않고, 현재 위치나 대화 상대에 따라 바뀝니다. 구현에서는 그 시점의 후보를 사용해, AFM이 출력할 수 있는 값을 정한 '구조화된 출력 스키마'를 매번 만듭니다. AFM에게는 그 후보 중 하나, 또는 '아무것도 아님'을 선택하게 했습니다.
여기서 AFM이 출력하는 것은 선택한 후보를 나타내는 기호입니다. 코드가 그 기호를 화제나 이동의 입력으로 변환하여 규칙 엔진에 전달합니다. 어떤 대사를 표시할지, 어느 장면으로 넘어갈지는 이후 처리에서 결정됩니다.
모델의 해석 결과에는 규칙에 의한 보정(補正)도 추가되어 있습니다. 예를 들어, 개발 과정에서 '다음에 같이 영화 보자'라는 발언을 했을 때, 대기실로 이동하는 것이 선택되었습니다. 이 발언은 시나리오에 준비된 주제에도, 현재 위치에서의 이동에도 해당하지 않습니다. 본래는 '어느 것에도 해당 없음(どれでもない)'으로 분류하고, 이동하지 않은 채 자유 대화로 답변하는 것을 상정합니다. '어느 것에도 해당 없음'은 발언을 무시한다는 의미가 아니라, 정형 대사나 이동에 연결시키지 않고 대화 그 자체로 받아들이기 위한 선택입니다.
이러한 오해를 줄이기 위해, 모델이 이동을 선택했을 경우에는 발언에 목적지나 이동 방식의 표현이 포함되어 있는지 규칙으로 확인합니다.
이에 대응하는 구현은 InputInterpreter.swift에 있습니다. 동적 스키마와 보정에 대한 자세한 내용은 제2회에서 설명하겠습니다.
4.2 등장인물로서 짧은 답변 생성하기
정형 대사가 없는 주제나 잡담에는 AFM이 인물로서 답변합니다. 이번 개발에서는 답변이 길어질 경우, 자기 모순이나 설정에 없는 이야기가 나오기 쉽다고 느꼈습니다. 따라서 현재는 2~3문장 정도를 기준으로 생성하고 있습니다.
모델에게 전달하는 것은 단순히 인물의 이름만은 아닙니다.
- 인물의 설정, 성격, 말투, 대사 예시
- 현재 장면에서의 행동
- 현재 주제에 대해 그 인물이 가지고 있는 생각
- 지금까지 정형 대사로 이야기한 내용과 직전의 주고받음
- 그 인물이 알고 있는 주인공의 취향 등
예를 들어, 빨간 문에 대해 엄마는 '열어보는 재미가 있다'고 생각합니다. 아오이는 엄마 앞에서 문을 바꿀 안건을 내놓지만, 실제로는 안에 보이지 않는 느낌을 마음에 들어 합니다. 대기실에서는 그 본심을 이야기하도록 설정했습니다. 같은 질문이라도 누구에게, 어디서 들었는지에 따라 답변에 사용하는 정보나 말투가 달라집니다.
자유 대화에서는 인물의 입장이나 생각을 시나리오에 작성하고, AFM이 현장감 있는 표현을 만듭니다. 다만, 이야기의 전개와 관련된 주제에서는 AFM이 답변을 자유 생성하게 하면, 작가가 예정하지 않은 약속이나 관계 변화를 이야기할 위험이 있습니다.
예를 들어, 주인공과 아오이의 미래에 대해서는 이 데모 대화에서 임의로 이야기를 진행시키고 싶지 않습니다. 그래서 입력이 '두 사람의 미래'로 해석된 경우에는 자유 생성하지 않고, 아오이의 '……오늘 밤은 그 이야기는 하지 말자. 가게 이야기잖아.'처럼 시나리오에 준비된 대사를 표시합니다. 미래 이야기를 피해서 오늘 밤 가게 상담으로 되돌리는 것까지는 작가가 결정하는 것입니다. (주인공과 아오이의 미래에 대해서는 본편을 플레이하시면 알 수 있습니다.)
자유 대화에서는 한 왕복(往復)마다 새로운 모델 세션을 만들고, 필요한 정보를 조합하여 전달합니다. 인물 설정과 규칙은 instructions 쪽에, 플레이어의 발언이나 기억은 입력 쪽에 넣습니다.
생성 후에는 상대방을 부르는 방식, 같은 대사의 반복, 현재 발언과 관계없는 주제 끌기, 작품 세계 밖으로 나가는 말 등을 검사합니다. 필요하면 한 번만 재작업하고, 그래도 사용할 수 없으면 준비된 정형 대사로 돌아갑니다. 이는 부자연스러운 답변을 줄이기 위한 장치이며, 대화의 품질을 완전히 보장하는 것은 아닙니다.
구현은 Talk.swift와 SafeTalker.swift에 있습니다.
4.3 플레이어가 말한 자신의 것을 기억하기
세 번째는 플레이어가 말한 취향이나 출신 등을 추출하는 기능입니다.
예를 들어, '나는 재즈가 좋다'라는 발언에서 좋아하는 음악으로 '재즈'를 추출하고, 검사를 통과한 값만 기억합니다. 이번에 기억할 수 있는 항목은 좋아하는 음식, 음료, 음악, 영화, 가게 분위기, 출신, 취미, 싫어하는 것의 8가지 종류입니다.
기억은 대화 기록 전체를 계속 가지고 가는 시스템과는 별도로 관리합니다. 답변 후에 백그라운드에서 추출하여 나중에 대화 주제가 맞을 때 사용합니다.
여기서 중요한 것은 누가 그 발언을 들었는지입니다.
대기실에서 아오이에게만 이야기한 취향은 엄마의 대화용 프롬프트에는 넣지 않습니다. 반면, 가게 안에는 엄마와 아오이가 있으므로, 거기서 추출 및 검사를 통과한 정보는 두 사람이 들은 정보로 기억합니다.
기억을 대화에 포함할지는 코드로 결정합니다. 다만, 전달된 정보를 답변에 잘 반영할 수 있는지에는 생성의 변동성이 있고, 추출 자체에도 오류가 남습니다. 추출 오류로 게임 진행이 바뀌는 것을 막기 위해, 이번에는 기억을 잡담에만 사용하고 이야기 전개 조건에는 사용하지 않습니다.
구현은 ProtagonistMemory.swift에 있습니다.
5. 한 번의 입력이 화면에 나타날 때까지
앞서 설명한 전체 구조 중, ①입력 해석부터 규칙 엔진을 거쳐 정형 대사를 표시하는 경로를 추적해 보겠습니다.
구체적인 예시로, 가게 안에서 아오이가 '너는 어떻게 생각해?'라고 물은 후, 엄마에게 '흐르는 곡을 바꿔보면 어떨까?'라고 입력한 경우를 따라가겠습니다.
- 입력을 검사하고, 현재 상태에서 화제 및 이동의 후보를 만듭니다.
- 입력을 분류하고, 보정 과정을 거쳐 '의도: 음악'으로 처리합니다.
- 규칙 엔진이 대본의 조건에 맞는 줄을 선택합니다.
- 엄마와 아오이가 순서대로 이전에 소개된 정형 대사를 표시합니다.
이 경우, AFM은 입력 해석에 사용하지만, 표시되는 2줄의 대사는 생성하지 않습니다.
이 음악의 정형 대사는 이미 표시되었는지 기록하며, 같은 플레이 중에는 다시 표시되지 않도록 합니다. 따라서, 음악 화제를 다시 꺼내도 이전에 나온 2줄은 반복되지 않습니다. 대신, 대본에 적힌 '답변하는 인물의 음악에 대한 생각'을 AFM에 전달하여 현재 발언에 대한 답변을 생성합니다. 처음에는 대본대로의 상호작용으로 인물의 입장을 보여주고, 그 후에는 같은 화제에 대해 자유롭게 대화를 이어갈 수 있는 구조입니다.
입력 해석에 실패한 경우에는 '아무것도 아님'으로 처리하고 계속 진행합니다. 자유 대화 생성이 중단된 경우에도 정형 대사로 돌아갈 수 있도록, 실패 시 동작을 코드 측에 준비해 두었습니다.
6. 소스코드 구성
구현은 SwiftUI 앱과 Swift 패키지인 Yukemuri로 나뉘어 있습니다.
UI/
UI/Resources/scenario.json 대본 및 인물 설정
UI/Resources/Graphics/ 배경 및 인물 이미지
...
대본의 scenario.json에는 대사뿐만 아니라, 화제 설명과 발언 예시, 인물 설정, 지식, 대사의 조건, 장면이나 연출 정보도 작성되어 있습니다.
규칙 엔진인 YukemuriEngine은 Foundation Models에 의존하지 않습니다. 해석된 입력과 게임 상태를 받아 새로운 상태와 표시해야 할 줄 또는 연출을 반환합니다.
YukemuriLLM의 GameSession.swift가 입력 해석, 규칙 엔진, 대화 생성, 기억을 연결합니다. 화면(Screen)과 CLI 모두 이 구조를 사용합니다.
대본 작성법에 관심이 있다면 데이터 사양도 참고해 주세요.
7. 데모 시연 방법 및 현재 한계
동작 환경 및 실행
이 데모의 동작 환경은 다음과 같습니다. Foundation Models 프레임워크 전체의 최소 요구 사항이 아니라, 이 리포지토리 구현을 대상으로 한 요구 사항입니다.
- macOS 27 이상 Apple Silicon 탑재 Mac
- Apple Intelligence가 활성화되어 있고, 온디바이스 모델(on-device model) 사용이 가능한 상태
- 소스에서 빌드하는 경우 Xcode 27 이상 (Swift 6.2)
GitHub의 Releases (v1.0)에서 DMG 버전을 다운로드할 수 있습니다. 배포 앱은 Apple 공증을 받지 않았으므로, 첫 실행 시 절차는 README를 참조해 주세요.
소스에서 실행하는 경우, 리포지토리를 가져와 UI/UI.xcodeproj를 Xcode로 열고, UI 스킴을 My Mac으로 실행합니다.
Apple Intelligence가 비활성화되거나 모델 준비 중이라 AFM 사용이 불가능할 경우, 오류를 표시하고 멈춥니다.
먼저 시도해 보고 싶은 것들
처음에는 다음 순서로 시도하는 것이 구조를 이해하기 쉬울 것 같습니다.
- 도입부 대화에 한 번 답변한 후, 아오이의 '당신은 어떻게 생각해?'까지 진행하고, 엄마에게 가게에서 나오는 음악 이야기를 꺼냅니다.
- 같은 화제를 다른 방식으로 다시 꺼내어 정형 대사 이후의 자유 대화를 확인합니다.
- 대기실로 이동하여 아오이에게만 자신이 좋아하는 음악 등을 이야기합니다.
- 다른 대화를 거쳐 아오이에게 '내가 좋아하는 음악, 기억해?'라고 묻고, 가게 안으로 돌아와 엄마에게도 같은 질문을 합니다.
기억 추출은 백그라운드에서 이루어지므로, 취향을 말한 직후에는 아직 반영되지 않은 경우가 있을 수 있습니다. 또한, 생성되는 답변이 매번 같지는 않습니다.
소스에서 디버그 버전을 실행하면, 메뉴의 '개발(Development)'에서 해석 후보와 결과, 답변 생성 출처, 모델에 전달된 프롬프트, 추출된 기억을 확인할 수 있습니다. 보이는 대화뿐만 아니라 내부적으로 무엇이 일어났는지 추적할 수 있습니다.
배포하는 릴리스 버전은 플레이 로그를 남기지 않습니다. 디버그 버전은 입력이나 프롬프트를 기기 내 로그에 기록하지만, '개발' 메뉴에서 기록을 중지할 수 있습니다. 입력 검사로 막힌 발언 등 본문을 기록하지 않는 입력도 있습니다.
생성에는 대기 시간과 변동성이 있다
2026년 10월 9일 M4 MacBook Air 및 macOS 27.0.1에서 측정한 결과, 입력 해석은 중앙값 0.5초, 2~3문장 대사 생성은 중앙값 3.2초였습니다. 데모 평가와 100턴 시연 플레이를 통해 얻은 값으로, 기기나 입력에 따라 달라집니다.
정형화된 대사로 답변하는 경우에도 해석을 기다리는 시간이 있고, 자유로운 대화의 경우에는 생성에 더 많은 시간이 걸립니다. 생성 결과가 오랫동안 돌아오지 않은 사례도 있어, 대기 시간 처리는 해결 과제로 남아 있습니다.
또한, 규칙 기반 보정이나 출력 검사를 추가하더라도 주제를 혼동하거나 반복하거나 설정과 맞지 않는 답변을 완전히 없애지는 못했습니다. 테스트에서 발견된 사례들을 시나리오, 지식, 규칙 개선에 연결하고, 그 입력을 회귀 테스트에 추가하고 있습니다.
8. 다음 편부터 다룰 내용
연재에서는 다음 순서로 구현과 검증을 소개할 예정입니다.
| 회차 | 내용 |
|---|---|
| 제1회 | 전체 설계 및 데모 '피의 연못의 밤' (본문) |
| ... | |
다음 편에서는 자유 입력을 어떻게 주제나 이동 후보로 연결하는지를 다룰 예정입니다. 현재 상태에서 후보를 만들고, DynamicGenerationSchema로 출력 형태를 제약하며, 모델의 혼동을 규칙으로 보정하는 과정까지를 소스 코드에 따라 설명하겠습니다. |
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기