キャラクター作成フォーム
プレイヤーがセッションを開くとキャラクター作成画面が表示されます — 名前を入力、クラスを選択、バックストーリーを書き、「Start Adventure」をクリックすると、チャットが本物のストーリーオープニングにジャンプします。最初のAI返信から、AIはプレイヤーキャラクターのすべてを知っています。
これから作るもの
最初のメッセージはストーリーではなく、キャラクター作成フォームです。フォームはRoot Componentによってレンダリングされ、以下を含みます:
- テキスト入力 — プレイヤーがキャラクター名を入力
- 3つのクラス選択ボタン — Warrior / Mage / Rogue
- テキストエリア — プレイヤーがバックストーリーを書く
- 「Start Adventure」ボタン — クリックすると全情報が変数に保存され、本物のストーリーオープニングにジャンプ
ジャンプ後、ロアエントリ内の{{player_name}}、{{player_class}}、{{player_backstory}}マクロは、プレイヤーが入力した内容でエンジンによって自動的に置き換えられます。AIが最初の返信を書く時点で、すでに完全なキャラクタープロフィールを持っています。
前提条件
このレシピはRecipe #1の2つのコアテクニックの上に直接構築されます:
| テクニック | ソース | このレシピでの使い方 |
|---|---|---|
switchGreeting(index)でオープニング間をジャンプ | Recipe #1 Part 1 | プレイヤーがフォームに入力した後、「作成画面」から「ストーリーオープニング」にジャンプ |
エントリ内容での{{variableId}}マクロ置換 | Recipe #1 Part 2 | エントリ内の{{player_name}}などのマクロは、プロンプトビルド時にプレイヤー入力で置換 |
Recipe #1をまだ読んでいない場合は、まずそこから始めてください:UIによるシーンジャンプとエントリ切り替え。
仕組み
完全なシーケンス:
1. プレイヤーが新規セッションを開始 → グリーティング#1(キャラクター作成フォーム)を見る
2. Root Componentの`<Chat renderBubble>`が`msg.messageIndex === 0`を検出、フォームUIをレンダリング
3. プレイヤーが名前を入力、クラスを選択、バックストーリーを書く
4. プレイヤーが「Start Adventure」をクリック
→ コードが api.setVariable("player_name", "Elara") を呼ぶ
→ コードが api.setVariable("player_class", "Mage") を呼ぶ
→ コードが api.setVariable("player_backstory", "Grew up in a wizard's tower...") を呼ぶ
→ コードが api.switchGreeting(1) を呼ぶ
→ 最初のメッセージが瞬時にグリーティング#2(本物のストーリーオープニング)に切り替わる
5. プレイヤーが最初のメッセージを送信
→ エンジンがプロンプトをビルド → エントリ内の {{...}} マクロをスキャン
→ {{player_name}} は "Elara" に置換
→ {{player_class}} は "Mage" に置換
→ {{player_backstory}} は "Grew up in a wizard's tower..." に置換
→ AIが完全なキャラクタープロフィールを受け取る → 最初の返信を書く重要なポイント: setVariableは即座に有効になりますが、AIが変更を見るのは次にプロンプトがビルドされたときだけです。そのため順序は:まずsetVariableで値を保存 → switchGreetingでジャンプ → プレイヤーがメッセージを送信 → AIが返信でキャラクター情報を使える、です。
ステップバイステップ
ステップ1:変数の作成
プレイヤーのキャラクター情報を保存する3つの文字列変数が必要です。
エディタ → サイドバー → Variables タブ → 「Add Variable」をクリックし、これら3つを作成:
変数1:Character Name
| フィールド | 値 | 理由 |
|---|---|---|
| Display Name | Character Name | エディタ内で自分用の識別名 |
| ID | player_name | エントリ内の{{player_name}}マクロがこのIDを参照 |
| Type | String | 名前はテキスト |
| Default Value | Traveler | プレイヤーが名前を入力せずに開始した場合、AIは「Traveler」と呼ぶ |
| Category | Custom | 整理ラベル、純粋に管理用 |
| Behavior Rules | Do not modify this variable. It is set by the player via the character creation form. | AIにキャラクターの名前を自分で変更しないよう伝える |
変数2:Character Class
| フィールド | 値 | 理由 |
|---|---|---|
| Display Name | Character Class | 自分用の識別名 |
| ID | player_class | エントリ内の{{player_class}}マクロがこのIDを参照 |
| Type | String | クラスはテキスト("Warrior"、"Mage"、"Rogue") |
| Default Value | 空のまま | 空は未選択を意味。Root Componentはこの値をチェックしてどのボタンをハイライトするか決める |
| Category | Custom | 整理ラベル |
| Behavior Rules | Do not modify this variable. It is set by the player via the character creation form. | AIにクラスを自分で変更しないよう伝える |
変数3:Character Backstory
| フィールド | 値 | 理由 |
|---|---|---|
| Display Name | Character Backstory | 自分用の識別名 |
| ID | player_backstory | エントリ内の{{player_backstory}}マクロがこのIDを参照 |
| Type | String | バックストーリーはテキスト |
| Default Value | 空のまま | 空 = プレイヤーがバックストーリーを書かなかった。エントリ内の対応箇所は空文字列になる |
| Category | Custom | 整理ラベル |
| Behavior Rules | Do not modify this variable. It is set by the player via the character creation form. | AIにバックストーリーを自分で変更しないよう伝える |
なぜ
player_nameにはデフォルト値があり、他の2つにはないのか? 名前はほぼすべてのシナリオで必要だからです — AIはキャラクターを何かと呼ぶ必要があります。「Traveler」のフォールバック値は、AIが返信で気まずい空白や「名前のないキャラクター」を書くのを防ぎます。クラスとバックストーリーは空でも問題ありません — AIは合理的に無視するか即興で対応できます。
ステップ2:「First Message」で2つのグリーティングを作成
エディタを開き、サイドバーのFirst Messageタブをクリック。
最初のグリーティング(キャラクター作成画面)を作成:
「Create First Message」ボタンをクリック。テキストボックスに次のように書きます:
*A warm glow envelops you. You feel yourself taking shape — but your identity is not yet defined.*
*An ancient voice echoes through the void:*
"Welcome, traveler. Before you step into this world, tell me — who are you?"このテキストは雰囲気作りの装飾です — 実際のフォームUIは、このテキストの下のRoot Componentによってレンダリングされます。プレイヤーが見るのは:上に雰囲気のあるパッセージ、下にインタラクティブなキャラクター作成フォーム。
2番目のグリーティング(本物のストーリーオープニング)を作成:
下の「Add Greeting」ボタンをクリック。タブ2に切り替え、実際のストーリーオープニングを書きます:
*{{player_name}} pushes open the gate of destiny.*
*You are a {{player_class}}, and this is your first time setting foot in the Elderlands. The silhouette of a distant city shimmers in the dawn light, and a cobblestone road stretches toward the unknown.*
*A breeze brushes your face, carrying the scent of grass and distant hearth-smoke. You take a deep breath — the adventure begins now.*
Three paths lie before you: a wide road leading to town, a narrow trail through the woods, and a slope descending to the river. Which way do you go?グリーティング内でもマクロは動作する
2番目のグリーティングの{{player_name}}と{{player_class}}に注目。これらのマクロは表示時に変数の現在の値に置換されます。そのため、プレイヤーがフォームに入力し変数がsetVariableで更新された後、switchGreeting(1)がこのグリーティングに切り替わると、プレイヤーはストーリーオープニング内に自分のキャラクター名とクラスを見ます。
グリーティングの順序 = インデックス
タブ1 = インデックス0(キャラクター作成画面、デフォルト表示)、タブ2 = インデックス1(ストーリーオープニング)。Root Component内のswitchGreeting(1)呼び出しは2番目にジャンプします。
ステップ3:マクロを使用するロアエントリを作成
これで、AIに送られるすべてのプロンプトにキャラクター情報を注入するエントリを作成します。
エディタ → Entries タブ → 新規エントリを作成
| フィールド | 値 | 理由 |
|---|---|---|
| Name | Player Character Profile | 自分用の識別名 |
| Section | System Presets | プリセットセクションのエントリは常にAIに送信される |
| Enabled | Yes(トグルオン) | 常時アクティブ — キャラクター情報はAIが常に必要とするもの |
内容:
[Player Character Profile]
Name: {{player_name}}
Class: {{player_class}}
Backstory: {{player_backstory}}
Always address the player by their character's name. Adjust interactions, available skills, and encounters based on their class and backstory.何が起きるのか?
エンジンがプロンプトをビルドするとき、このテキストをスキャンします:
{{player_name}}→ 変数player_nameの現在の値に置換(例:「Elara」){{player_class}}→ 変数player_classの現在の値に置換(例:「Mage」){{player_backstory}}→ 変数player_backstoryの現在の値に置換(例:「Grew up in a wizard's tower」)
変数が空文字列なら対応箇所は空白です。たとえば、プレイヤーがバックストーリーを書かなかった場合、AIは「Backstory:」の後に何もないものを見る — AIは通常、空のフィールドを無視するか即興で対応します。
ステップ4:Root Componentでキャラクター作成フォームを構築
これがコアステップ — チャット内にインタラクティブなキャラクター作成フォームをレンダリングします。
エディタ → Custom UI セクション → index.tsxを開く → このコードを貼り付け(デフォルトのreturn <Chat />を置き換え):
export default function MyWorld() {
const api = useYumina();
// ---- Form state ----
const [name, setName] = React.useState(
String(api.variables.player_name || "")
);
const [selectedClass, setSelectedClass] = React.useState(
String(api.variables.player_class || "")
);
const [backstory, setBackstory] = React.useState(
String(api.variables.player_backstory || "")
);
// Check whether character creation is already done (class is set = form was submitted)
const hasCreated = String(api.variables.player_class || "") !== "";
// Class list
const classes = [
{ id: "Warrior", label: "Warrior", icon: "⚔️", desc: "Melee specialist, high HP" },
{ id: "Mage", label: "Mage", icon: "🔮", desc: "Ranged magic, high MP" },
{ id: "Rogue", label: "Rogue", icon: "🗡️", desc: "Agile and stealthy, high crit" },
];
// Handle "Start Adventure"
const handleStart = () => {
if (!selectedClass) return; // Must pick a class first
api.setVariable("player_name", name.trim() || "Traveler");
api.setVariable("player_class", selectedClass);
api.setVariable("player_backstory", backstory.trim());
api.switchGreeting?.(1); // Jump to greeting #2 (story opening)
};
return (
<Chat renderBubble={(msg) => (
<div>
{/* Render message text (platform already rendered HTML, use msg.contentHtml directly) */}
<div
style={{ color: "#e2e8f0", lineHeight: 1.7 }}
dangerouslySetInnerHTML={{ __html: msg.contentHtml }}
/>
{/* Character creation form — only on first message & not yet created */}
{msg.messageIndex === 0 && !hasCreated && (
<div
style={{
marginTop: "20px",
padding: "24px",
background: "linear-gradient(135deg, #1e1b4b 0%, #1a1a2e 100%)",
borderRadius: "16px",
border: "1px solid #312e81",
}}
>
{/* Title */}
<div
style={{
fontSize: "18px",
fontWeight: "bold",
color: "#c4b5fd",
marginBottom: "20px",
textAlign: "center",
}}
>
Create Your Character
</div>
{/* Name input */}
<div style={{ marginBottom: "16px" }}>
<div
style={{
fontSize: "13px",
color: "#a5b4fc",
marginBottom: "6px",
fontWeight: "600",
}}
>
Character Name
</div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your name (leave blank for 'Traveler')"
style={{
width: "100%",
padding: "10px 14px",
background: "#0f172a",
border: "1px solid #334155",
borderRadius: "8px",
color: "#e2e8f0",
fontSize: "14px",
outline: "none",
boxSizing: "border-box",
}}
/>
</div>
{/* Class selection */}
<div style={{ marginBottom: "16px" }}>
<div
style={{
fontSize: "13px",
color: "#a5b4fc",
marginBottom: "8px",
fontWeight: "600",
}}
>
Choose a Class
</div>
<div style={{ display: "flex", gap: "10px" }}>
{classes.map((cls) => (
<button
key={cls.id}
onClick={() => setSelectedClass(cls.id)}
style={{
flex: 1,
padding: "14px 10px",
background:
selectedClass === cls.id
? "linear-gradient(135deg, #4338ca, #6366f1)"
: "#1e293b",
border:
selectedClass === cls.id
? "2px solid #818cf8"
: "1px solid #334155",
borderRadius: "10px",
color:
selectedClass === cls.id ? "#e0e7ff" : "#94a3b8",
cursor: "pointer",
textAlign: "center",
transition: "all 0.2s",
}}
>
<div style={{ fontSize: "24px", marginBottom: "4px" }}>
{cls.icon}
</div>
<div
style={{
fontSize: "14px",
fontWeight: "bold",
marginBottom: "2px",
}}
>
{cls.label}
</div>
<div style={{ fontSize: "11px", opacity: 0.7 }}>
{cls.desc}
</div>
</button>
))}
</div>
</div>
{/* Backstory */}
<div style={{ marginBottom: "20px" }}>
<div
style={{
fontSize: "13px",
color: "#a5b4fc",
marginBottom: "6px",
fontWeight: "600",
}}
>
Backstory (optional)
</div>
<textarea
value={backstory}
onChange={(e) => setBackstory(e.target.value)}
placeholder="A few sentences about your character's history..."
rows={3}
style={{
width: "100%",
padding: "10px 14px",
background: "#0f172a",
border: "1px solid #334155",
borderRadius: "8px",
color: "#e2e8f0",
fontSize: "14px",
outline: "none",
resize: "vertical",
boxSizing: "border-box",
fontFamily: "inherit",
}}
/>
</div>
{/* Start Adventure button */}
<button
onClick={handleStart}
disabled={!selectedClass}
style={{
width: "100%",
padding: "14px",
background: selectedClass
? "linear-gradient(135deg, #7c3aed, #a855f7)"
: "#374151",
border: "none",
borderRadius: "10px",
color: selectedClass ? "#f5f3ff" : "#6b7280",
fontSize: "16px",
fontWeight: "bold",
cursor: selectedClass ? "pointer" : "not-allowed",
transition: "all 0.2s",
}}
>
{selectedClass ? "Start Adventure" : "Pick a class first"}
</button>
</div>
)}
</div>
)} />
);
}コードウォークスルー
状態管理:
const api = useYumina()— 変数読み書きとグリーティング切り替えのためのYumina APIを取得name/selectedClass/backstory— 入力フィールド、クラスボタン、テキストエリアを追跡する3つのReact状態React.useState(String(api.variables.player_name || ""))— 初期値は変数から読み取り。新規セッションではデフォルト、既存セッションでは保存された変数から復元hasCreated—player_classが空文字列かチェック。空 = キャラクター未作成;空でない = 作成済み、フォームを隠す
フォームUI:
msg.messageIndex === 0 && !hasCreated— 最初のメッセージかつキャラクター作成前のみフォームを表示(msgは<Chat renderBubble>から渡される)classes.map(...)— クラスリストを反復し、各々ボタンをレンダリング。選択されたクラスはハイライトされたボーダーとグラデーション背景selectedClass === cls.id— これが現在選択されているクラスか確認、ハイライトに使用disabled={!selectedClass}— クラスが選択されるまでボタンはグレーアウトされクリック不可
サブミットロジック(handleStart):
api.setVariable("player_name", name.trim() || "Traveler")— 名前を保存。プレイヤーが空白のままにした場合「Traveler」にフォールバックapi.setVariable("player_class", selectedClass)— クラスを保存api.setVariable("player_backstory", backstory.trim())— バックストーリーを保存api.switchGreeting?.(1)— グリーティング#2にジャンプ。?.オプショナルチェーンはAPIが利用できない場合のエラーを防ぐ
この呼び出し順序の理由は?
setVariable x 3 → switchGreeting(1)
↑ ↑
先にデータ保存 次にジャンプswitchGreetingの前にsetVariableを呼ぶ必要があります。グリーティングの{{player_name}}と{{player_class}}マクロは表示時に即座に置換されます — 先にジャンプして後で保存すると、マクロはまだ古い値(空文字列またはデフォルト)を保持します。
ステップ5:保存してテスト
- エディタ上部のSaveをクリック
- Start Gameをクリックするか、ホームページに戻って新規セッションを開始
- 最初のグリーティングの雰囲気あるテキストの下にキャラクター作成フォームが見える
- 名前フィールドに「Elara」と入力
- Mageボタンをクリック — ハイライトされ、下のボタンが「Start Adventure」に変わる
- バックストーリーボックスに「Grew up in a wizard's tower and stumbled upon a portal to another world」と入力
- Start Adventureをクリック
- 最初のメッセージが瞬時に「Elara pushes open the gate of destiny. You are a Mage...」に切り替わる — フォームが消える
- メッセージを送信(例:「I head toward the town」)— AIの返信が「Elara」と呼びかけ、Mageクラスに基づくインタラクションを書く
AIが実際にキャラクター情報を得たことを確認:
メッセージ送信後、AIの返信を確認:
- キャラクター名を使用(「Elara」、「you」や「Traveler」ではなく)
- クラス関連の詳細に言及(Mage = 魔法、杖、呪文など)
- バックストーリーを書いた場合、AIが参照することがある(「You recall your days in the wizard's tower...」)
AIがこの情報を使っていない場合は、下のトラブルシューティング表を確認してください。
トラブルシューティング
| 症状 | 想定される原因 | 対処 |
|---|---|---|
| キャラクター作成フォームが見えない | Root Componentのコードが保存されていない、または構文エラー | Custom UIセクション下部のコンパイル状態を確認 — 緑の「OK」が表示されるべき |
| 「Start Adventure」をクリックしても何も起きない | クラスが選択されていない | クラスが選ばれていないとボタンはグレーアウト(disabled) — まずクラスをクリック |
| ボタンをクリックしたがグリーティングが切り替わらない | グリーティングが1つしかない | First Message タブに2つのグリーティング(タブ1とタブ2)があることを確認 |
グリーティングが切り替わったが{{player_name}}が生のテキストとして見える | マクロが置換されていない | 変数IDが正しく綴られていることを確認(player_name、playerNameではない) |
| AI返信がキャラクター名を使わない | エントリがアクティブでない | ロアエントリが有効で、内容に{{player_name}}が含まれていることを確認 |
| AI返信がデフォルトの「Traveler」を使う | setVariableがswitchGreetingの後に呼ばれた | コードがswitchGreetingの前にsetVariableを呼ぶことを確認 |
| キャラクター作成後もフォームが表示される | hasCreatedチェックが間違っている | player_classのデフォルト値が空文字列であることを確認(空でない値ではない) |
さらに進める:キャラクター作成の拡張
クラスを追加
classes配列に新しい要素を追加するだけ:
const classes = [
{ id: "Warrior", label: "Warrior", icon: "⚔️", desc: "Melee specialist, high HP" },
{ id: "Mage", label: "Mage", icon: "🔮", desc: "Ranged magic, high MP" },
{ id: "Rogue", label: "Rogue", icon: "🗡️", desc: "Agile and stealthy, high crit" },
{ id: "Cleric", label: "Cleric", icon: "✨", desc: "Healing and blessings, great support" },
{ id: "Ranger", label: "Ranger", icon: "🏹", desc: "Ranged attacks, expert tracker" },
];他のコード変更は不要 — ボタンは自動的に表示され、選択時にselectedClassが新しいクラスのidになります。
ビヘイビアルールとの組み合わせ
Recipe #1と同様に、クラスに基づいて異なるロアエントリを自動的に有効/無効にできます。例:
- ノレッジベースに「Warrior Lore」、「Mage Lore」、「Rogue Lore」エントリを作成、デフォルトで無効
- Behaviorsタブで、
player_classが一致したとき対応するエントリを有効にする3つのビヘイビアを作成 handleStart内にapi.executeAction("choose-class-warrior")のような呼び出しを追加
これで各クラスは異なるラベルだけでなく、まったく異なる世界観とAI動作を持ちます。
後続メッセージにキャラクター情報を表示
Root Componentの<Chat renderBubble>に「キャラクター情報バー」を追加して、各メッセージの上部にキャラクター名とクラスを表示できます:
{/* In the return, above the message content */}
{hasCreated && (
<div style={{
display: "flex",
gap: "8px",
marginBottom: "8px",
fontSize: "12px",
color: "#a5b4fc",
}}>
<span>{String(api.variables.player_name)}</span>
<span style={{ opacity: 0.5 }}>|</span>
<span>{String(api.variables.player_class)}</span>
</div>
)}クイックリファレンス
| やりたいこと | 方法 |
|---|---|
| プレイヤー入力テキストを保存 | 文字列変数を作成 + api.setVariable("id", value) |
| 選択ボタンを構築 | React状態で選択を追跡 + クリックでsetSelectedClass(id) |
| フォーム送信後に異なるオープニングにジャンプ | まずすべての値をsetVariable、次にswitchGreeting(index) |
| AIにキャラクター情報を知らせる | エントリ内容で{{variableId}}マクロを使用 — エンジンがプロンプトビルド時に置換 |
| フォームを一度だけ表示 | hasCreatedの変数をチェック — 作成後にフォームが消える |
| 条件が満たされるまでボタンを無効化 | disabled={!condition} + 対応するグレーアウトスタイル |
| グリーティング内でもキャラクター情報を表示 | グリーティングテキストに直接{{player_name}}などのマクロを書く |
自分で試す — インポート可能なデモワールド
このJSONをダウンロードし、新規ワールドとしてインポートしてすべての動作を確認してください:
インポート方法:
- Yumina → My Worlds → Create New World に移動
- エディタで More Actions → Import Package をクリック
- ダウンロードした
.jsonファイルを選択 - 全グリーティング、変数、Root Componentが事前設定された新規ワールドが作成されます
- 新規セッションを開始して試す
含まれるもの:
- 2つのグリーティング(キャラクター作成フォーム + ストーリーオープニング)
- 3つの変数(名前用
player_name、クラス用player_class、バックストーリー用player_backstory) - 1つのロアエントリ(
{{player_name}}、{{player_class}}、{{player_backstory}}マクロを使用するキャラクタープロフィール) - 完全なRoot Component(キャラクター作成フォームUI)
これはRecipe #4です
Recipe #1ではボタンベースのグリーティング切り替えとマクロ置換を学びました。このレシピはそれらを完全なキャラクター作成フローに組み合わせます。今後のレシピはこの基盤の上にさらに構築していきます — 能力値割り当て、装備選択、マルチステップオンボーディングなど。
