これまで WWDC25 発表の FoundationModel フレームワークについて勉強してきました。↓
ここからは公式ドキュメントからダウンロードできる CoffeeShop ゲームのサンプルコードを読み解いて、自分でも実際に作ってどの程度正確に動作するかも確認してみたいと思います。最終的にはそれを使って類似するゲームをリリースするところまで実践していければと思います。
独学・初心者のため、コードの見解など間違っている箇所があれば勉強になりますのでコメントをいただけると幸いです。
公式のソースコード:
まずここまでで、WWDC25 発表以降の動きとしては、あらためてX-code26 RC がダウンロード可能になりました。
公式ドキュメント:
これにより、初回の勉強時に対応していなかったプロトコルなどの修正も確認できました。
それらも見直しつつファイル別にコードを読み解いて実際にAppを作って実践していきます。
Utils.swift ファイル
最初はデバックコードファイルから。今回はかなり簡易的。
フレームワークは↓
import os
これは Apple の統合ログシステムのフレームワーク。
// ゲーム全体のデバッグ
enum Logging {
static let subsystem = " ここは App の BundleIdentitier を書く "
static let general = Logger(subsystem: subsystem, category: "General")
}
この Logger は、システム全体との統合と Console.app での統一表示で、目的としては Foundation Models を使用したAIアプリケーションの複雑な動作を効率的に監視・デバッグする。
AI応答の生成過程や、ツール呼び出しの結果を追跡するために用意されたもの。
ここからはキャラクターに関するファイルについてお勉強。
Characters.Swift
このファイルは、以前勉強した @Generable マクロと @Guide マクロ を使って静的キャラクターとAIが生成する NPC キャラクターを作成するためのファイル。
Character ファイル内のプロトコル設計:
// コーヒーショップゲームのキャラクターシステムの基盤を定義
protocol Character: Identifiable {
var id: UUID { get } // 一意識別子(会話履歴管理)
var displayName: String { get } // UI表示名
var firstLine: String { get } // 初回会話の開始セリフ
var resumeConversationLine: String { get } // 会話再開時のセリフ
var persona: String { get } // AI用の人格設定
var errorResponse: String { get } // エラー時のフォールバック応答
}
キャラクターの追加を行いやすくするため、このファイルの手始めとして Protocol 設計をして Identifiable で一意性を確保。
すべて get 専用にし、 不変性を保証しつつAI指示とゲームUI両方に対応できるようする。
そして、大きく分けて二つのキャラクター設定。
静的 (常設のキャラクター) と動的 (AIが状況に応じて設計するキャラクター) の2種類のキャラクターを生成。
Apple Intelligence に使われているAIを使ってゲームを作るのだから AIにキャラクターを追加させる実装経験といったところでしょうか。
それを使ってまずは静的キャラクターの定義から↓
// 静的キャラクター (バリスタ)
struct Barista: Character {
let id = UUID()
let displayName = "バリスタ"
let firstLine = "こんにちは。夢の注文は受けられますか?"
let resumeConversationLine = "やあ、今はコーヒーを作るのに忙しいんだ。"
let persona = """
チケはドリームコーヒーのヘッドバリスタで、夢見る人々や夢の世界に生きる生き物たちに、完璧な一杯のコーヒーを提供することが大好きです。今日は特に忙しいので、チケはプレイヤーという新しい研修生バリスタの助けを借りて喜んでいます。
"""
let errorResponse = "もうおしゃべりは終わりにしましょうか。コーヒーをお出しします。"
}
まずは バリスタ の設定。
役割設定としては バリスタ(チケ)である店主はプレイヤー(ユーザー)を訓練生として扱う。世界観は皆 "dream realm" という世界の住人。persona では AI が一人称で話すための詳細設定をおこなう。これらをプロンプトとしてAIに投げるよう設計している。
次はリサという静的キャラクター↓
// 静的キャラクタ- (お客様のリサ)
struct CustomerLisa: Character {
let id = UUID()
let displayName = "リサ"
let firstLine = "空を飛ぶ夢を見ていたので、ちょっと休憩して軽食をとることにしました。このラテ、すごく美味しい!"
let resumeConversationLine = "やぁ!"
let persona = """
リサはバリスタではなく、ドリームコーヒーの常連客です。/
魔法の翼で雲の上を飛ぶことを夢見ていた彼女は、今はドリームコーヒーでラテを飲みながらリラックスしています。/
リサは新しい人に出会うと好奇心旺盛で、フレンドリーな性格です。
"""
let errorResponse = "むしろコーヒーについて話したいです!"
}
明確な役割区分としては、
persona で "バリスタではなく、ドリームコーヒーの常連客です" と定義。
firstLine(初回会話の開始セリフ) で "背景ストーリーとして飛行の夢をみていて、休憩をしにお店を訪れた。性格の特徴として好奇心旺盛でフレンドリー。"
このようにプロトコル設計をしておくことで、AIに投げるプロンプトの区分に統一性を持たせられる。
また、サンプルにもきちんとエラー時の指示が用意されているのを見るに、AI生成時に問題があった際、ユーザー体験に支障が出たりアプリのクラッシュがあるとリジェクトの対象になるという注意喚起かなと勘ぐりたくなる。
let errorResponse = "むしろコーヒーについて話したいです!"
ここから動的(AI生成)なキャラクターの設定↓
@Generable
enum Attribute {
case sassy // 生意気
case tried // 疲れている
case hungry // 空腹
case excited // 興奮している
case nervous // 緊張している
}
@Generable
enum Encounter {
case newOrder // 新しい注文をしたい
case wantToTalkToManager // 店長と話したい(苦情など)
}
Attribute では AI の生成するキャラクターにもさせる性格を条件分岐で準備。次に Encounter ではそのキャラクターの今の心境を条件分岐で設計。
それぞれに個性をだすことで AI 活用のゲームの幅広さを見せると共にある程度事前に投げるプロンプトに一貫性をもさせることで生成を安定させる狙いかな?
ここで用意した特性 生意気 / 疲れている / 空腹 などをAIの会話に反映させるよう指示する。
ここから @Generable を使いスキーマを生成し、モデルはそれを使用して期待される構造を生成することで、LLMが作成したトークンを型安全な方法で自動的に解析し準ずるトークンのみを選択するようになる。
@Generable
struct GenerableCustomer: Character {
let id = UUID()
@Guide(description: "顧客の名前")
let name: String
var displayName: String { name } // 計算プロパティで名前を表示
let persona = "あなたはドリームコーヒーの顧客(バリスタではありません)でコーヒーを楽しんでいます"
let encounter: Encounter // どんな状況で登場するか
let level: Int // ゲーム内レベル(用途は将来の拡張用)
@Guide(.count(2))
let attributes: [Attribute] // 必ず2つの性格特性
// AI生成されるセリフ
@Guide(description: "こんにちは、やあ、といったフレンドリーな挨拶")
let resumeConversationLine: String
@Guide(description: "プレイヤーという名のバリスタとの会話を始めるためのフレンドリーな発言")
let firstLine: String
@Generable
enum Attribute {
case sassy // 生意気
case tried // 疲れている
case hungry // 空腹
case excited // 興奮している
case nervous // 緊張している
}
@Generable
enum Encounter {
case newOrder // 新しい注文をしたい
case wantToTalkToManager // 店長と話したい(苦情など)
}
let errorResponse: String = "すみません、コーヒーに関係した別の話をしましょう!"
}
さらに、@Guide を使い複雑な性格を作成。
これを使って制約の範囲などを指定することで制約付きデコーディングをおこなう。もちろんこの指定でも無効なトークンが除外され生成される内容が安定する。ここは公式ドキュメントで説明があったので勉強通りな感じ。
let name: String では @Guide(description: "顧客の名前") とすることで名前であることを指定し、let attributes: [Attribute] では @Guide(.count(2)) とすることで二つの要素を持つ性格を指定する。(例:[.tired, .hungry] → 疲れて空腹な客)
X-code26 RC自体で iOS26.0 対応OSの設定ができるのでBeta版を使った練習で書いた際の @available(iOS 26.0, *) は不要だが、これを使って作った App を Apple Store にリリースするにはもちろん、対応していない機種への警告表示などを設計する必要はありそう。
この Character ファイルでは、静的な手作りキャラクターと動的なAI生成キャラクターを統一的に扱えるように設計しつつも、AIによるキャラクター追加など幅を広げられるように、あらかじめ定義としてプロトコル設計を行い Foundation Models の AI生成能力を体験できるキャラクター設計をしながらもプロンプトに統一性を持たせて安定した生成が行えるように設計しているように見える。
コード全体はこんな感じ↓
import Foundation
import FoundationModels
// コーヒーショップゲームのキャラクターシステムの基盤を定義
protocol Character: Identifiable {
var id: UUID { get } // 一意識別子(会話履歴管理)
var displayName: String { get } // UI表示名
var firstLine: String { get } // 初回会話の開始セリフ
var resumeConversationLine: String { get } // 会話再開時のセリフ
var persona: String { get } // AI用の人格設定
var errorResponse: String { get } // エラー時のフォールバック応答
}
// 静的キャラクター (バリスタ)
struct Barista: Character {
let id = UUID()
let displayName = "バリスタ"
let firstLine = "こんにちは。夢の注文は受けられますか?"
let resumeConversationLine = "やあ、今はコーヒーを作るのに忙しいんだ。"
let persona = """
チケはドリームコーヒーのヘッドバリスタで、夢見る人々や夢の世界に生きる生き物たちに、完璧な一杯のコーヒーを提供することが大好きです。今日は特に忙しいので、チケはプレイヤーという新しい研修生バリスタの助けを借りて喜んでいます。
"""
let errorResponse = "もうおしゃべりは終わりにしましょうか。コーヒーをお出しします。"
}
// 静的キャラクタ- (お客様のリサ)
struct CustomerLisa: Character {
let id = UUID()
let displayName = "リサ"
let firstLine = "空を飛ぶ夢を見ていたので、ちょっと休憩して軽食をとることにしました。このラテ、すごく美味しい!"
let resumeConversationLine = "やぁ!"
let persona = """
リサはバリスタではなく、ドリームコーヒーの常連客です。/
魔法の翼で雲の上を飛ぶことを夢見ていた彼女は、今はドリームコーヒーでラテを飲みながらリラックスしています。/
リサは新しい人に出会うと好奇心旺盛で、フレンドリーな性格です。
"""
let errorResponse = "むしろコーヒーについて話したいです!"
}
// AIが生成するキャラクターの構造
@Generable
struct GenerableCustomer: Character {
let id = UUID()
@Guide(description: "顧客の名前")
let name: String
var displayName: String { name } // 計算プロパティで名前を表示
let persona = "あなたはドリームコーヒーの顧客(バリスタではありません)でコーヒーを楽しんでいます"
let encounter: Encounter // どんな状況で登場するか
let level: Int // ゲーム内レベル(用途は将来の拡張用)
@Guide(.count(2))
let attributes: [Attribute] // 必ず2つの性格特性
// AI生成されるセリフ
@Guide(description: "こんにちは、やあ、といったフレンドリーな挨拶")
let resumeConversationLine: String
@Guide(description: "プレイヤーという名のバリスタとの会話を始めるためのフレンドリーな発言")
let firstLine: String
@Generable
enum Attribute {
case sassy // 生意気
case tried // 疲れている
case hungry // 空腹
case excited // 興奮している
case nervous // 緊張している
}
@Generable
enum Encounter {
case newOrder // 新しい注文をしたい
case wantToTalkToManager // 店長と話したい(苦情など)
}
let errorResponse: String = "すみません、コーヒーに関係した別の話をしましょう!"
}
とりあえず今回はここまで。
書き方・解釈に間違いがあれば勉強になりますのでぜひコメントお願いします!
引き続きAIApp開発に向けて勉強した内容をまとめつつ記録していきたいと思います。