ビジュアルノベルモード
全画面のビジュアルノベルです。シーンの背景、キャラクターの立ち絵、ダイアログボックス、選択肢ボタンがあり、物語の進行に合わせて AI が変える変数で動きます。画面のコード(
index.tsx)は<Chat />を使わずに画面全体を描き、プレイヤーが入力する場所に<MessageInput />を置きます。素の Tailwind とインラインスタイルだけで、コンポーネントライブラリは使いません。
コードなしで作る
ビジュアルノベルの部品のいくつかは、もう画面のコードを書かなくても作れます。
- キャラクターの立ち絵:キャンバスでキャラクターの設定を右クリックし、立ち絵… を選びます。チャットでは、そのキャラクターが話すたびに、セリフの上にこの画像と名前が表示されます。ビジュアルとオーディオを参照してください
- 本のような文字の見た目:プレイヤー画面で 会話 のブロックを選び、メッセージの見た目 を 小説 にします
- 変数に合わせて変わる背景や立ち絵:プレイヤー画面で 画像 の部品を追加し、画像 を 「<変数>」に合わせて変える にします。変数に入っている
@asset:参照の画像が表示されるので、AI が変数を変えると画像も変わります - 物語の場面に合わせた画像:シーン画像は、表示するタイミング に書いた一文が成り立つと自動で出てきます
- 選択肢ボタン:プレイヤー画面のボタンに プレイヤーの代わりに発言 のステップを付けます
このページの残りでは、画面全体をコードで作ります。返信を読んで地の文とセリフを分けるダイアログボックス、返信の文章から取り出した選択肢ボタン、レイアウトの完全なコントロールがほしいときの方法です。
これから作るもの
全画面のビジュアルノベル画面です。
- シーンの背景:AI が変数を変えて背景画像を切り替え(教室、通り、夜空……)、画面のコードがそれを全画面の
background-imageとして表示します - キャラクターの立ち絵:AI がいま話している人物と表情を設定し、画面のコードが対応する立ち絵を画面中央に表示します
- ダイアログボックス:画面下の半透明の枠に、キャラクター名とセリフを表示します。斜体の文 は地の文や心の声、普通の文はキャラクターのセリフとして扱います
- 選択肢ボタン:AI が選択肢を出すと、画面のコードが押せるボタンを画面に重ねて表示します
- 全画面モード:画面のコードは
<Chat />を入れ子にせず、自前の全画面レイアウトを返します。普通のチャットの吹き出しはまったく出ません
しくみ
AI は、毎回の返信に書く指示で画面を動かします。
AI's response:
[current_bg: set "@asset:your-classroom-reference"]
[current_speaker: set "Yuki"]
[speaker_emotion: set "happy"]
*The classroom is bathed in morning light. Cherry blossom petals occasionally drift in through the window.*
Yuki turns to face you with a smile:
Good morning! You're here early today.エンジンがこれらの指示を読み取ると:
current_bgが教室の画像になる → 画面のコードがbackground-imageを教室に切り替えるcurrent_speakerが"Yuki"になる → ダイアログボックスに「Yuki」という名前が出るspeaker_emotionが"happy"になる → 画面のコードが Yuki の笑顔の立ち絵を探して表示する- 画面のコードが最新のメッセージの文章を解析する。斜体 の部分は地の文、普通の文はキャラクターのセリフとして描かれる
エンジンの処理の流れ:
AI の返信 → エンジンが指示を取り出す → 変数を更新 → 画面のコードが変数を読んで描き直す
→ 背景レイヤー:<div style={{ backgroundImage: ... }} />
→ 立ち絵レイヤー:<img src={SPRITES[speaker + "_" + emotion]} />
→ ダイアログボックスレイヤー:地の文(グレーの斜体)とセリフ(白の通常の書体)を区別
→ 選択肢レイヤー:show_choices = true のときボタンが出る手順
ステップ 1:変数を作る
ビジュアルノベルの表示を動かすには、変数が 4 つ必要です。
キャンバスで、カードの下にある 追加できるもの の + 変数 を 1 つずつ押します。それぞれ下の名前を付けてください。画面のコードも AI も、名前で変数を探します。
変数 1:いまの背景
| 項目 | 値 | 理由 |
|---|---|---|
| 変数名 | current_bg | AI は [current_bg: set "..."] と書いて背景を切り替え、コードは api.variables.current_bg を読みます |
| 型 | 文字 | 値は @asset: 参照か画像の URL |
| 初期値 | 最初の場面の背景の @asset: 参照 | AI が何も変えないうちに、新しいプレイで表示されるもの。ステップ 3 のあとで入れます |
| ビヘイビアルール | 場面の背景画像。場面が別の場所に移るたびに、アセット表の対応する参照を使って変える。 | いつ変えるか、どの値を使うかを AI に伝えます |
変数 2:いま話している人物
| 項目 | 値 | 理由 |
|---|---|---|
| 変数名 | current_speaker | AI は [current_speaker: set "name"] と書いて話し手を切り替えます |
| 型 | 文字 | 値はキャラクター名 |
| 初期値 | Narrator | 地の文のモードで始まる。特定のキャラクターは話していない状態 |
| ビヘイビアルール | いま話している人物。キャラクターが話すときはその名前に、地の文や心の声のときは "Narrator" にする。 | 使い方のルールを AI に伝えます |
変数 3:話し手の表情
| 項目 | 値 | 理由 |
|---|---|---|
| 変数名 | speaker_emotion | AI は [speaker_emotion: set "happy"] と書いて表情を切り替えます |
| 型 | 文字 | 値は表情のキーワード |
| 初期値 | neutral | ふつうの表情で始まる |
| ビヘイビアルール | 話しているキャラクターの表情。neutral, happy, sad, angry, surprised, shy のどれか1つだけ。表情が変わるたびに更新する。 | 表情を列挙しておくと、立ち絵のない表情を AI が作り出さなくなります |
変数 4:選択肢を出す
| 項目 | 値 | 理由 |
|---|---|---|
| 変数名 | show_choices | AI は [show_choices: set true] と書いて選択肢ボタンを出します |
| 型 | スイッチ | 表示 / 非表示の 2 つだけ |
| 初期値 | オフ | 選択肢ボタンは最初は隠しておく |
| ビヘイビアルール | プレイヤーに選択肢を出すときにオンにする。それ以外はオフのまま。 | プレイヤーに選んでもらうときだけオンにするよう AI に伝えます |
[name: set value] という書き方を AI に教える必要はありません。それはエンジンがやります。ビヘイビアルールには、それぞれの値がいつ変わるかだけを書きます。
AI がコードを実行することはありません
AI は指示を書き、エンジンがそれを変数に反映し、画面のコードが変数を読んで描き直します。AI が指示を書く → エンジンが読み取る → 変数が更新される → 画面のコードが描き直す、という流れです。
ステップ 2:ビジュアルノベル用の指示を設定に書く
AI には、ビジュアルノベルの画面向けに書いていること、文章をどう書けばいいかを知らせる必要があります。
キャンバスの キャラクターと世界 のブロック(毎ターン送られます)で、ブロック右上の + を押して設定を追加します。
| 項目 | 値 | 理由 |
|---|---|---|
| 名前 | ビジュアルノベルの指示 | 自分用の目印 |
| AI に渡すタイミング | キャラクターと世界(毎ターン) | 書式のルールはすべての返信に効かせたい |
本文:
[Visual Novel Mode]
You are generating content for a visual novel engine. Every response must include directives to control the screen.
Format rules:
1. Set the scene with directives at the start of your response:
[current_bg: set "backgroundReference"]
[current_speaker: set "characterName"]
[speaker_emotion: set "emotion"]
2. Text formatting:
- *Italic text* = narration or inner monologue. Use for describing environments, character actions, inner thoughts.
- Plain text (no formatting) = character dialogue/speech.
- Do not wrap dialogue in quotation marks — just write plain text.
3. When you want to give the player a choice:
- Use [show_choices: set true]
- List choices at the end of the text in this format:
A) Choice text
B) Choice text
C) Choice text
4. Each response should contain only one scene fragment (3-5 sentences). Keep the pacing tight, like a real visual novel.
5. Available emotions: neutral, happy, sad, angry, surprised, shy
6. Always update current_bg when switching scenes. Always update current_speaker and speaker_emotion when a character speaks.AI は、画面のコードが自分の文章をどう読むかを知りません。だから書式ははっきり書いておきます。地の文は斜体、セリフは普通の文。そうしないと AI が自由に書いてしまい、コードが地の文とセリフを見分けられなくなります。
ステップ 3:アセットを用意してアップロードする
ビジュアルノベルには背景画像とキャラクターの立ち絵が必要です。用意のしかたは 2 通りあります。
- 方法 A(おすすめ):Yumina のアセットライブラリにアップロードし、
@asset:参照を使う。安定していて、期限切れになりません - 方法 B:外部の
https://の画像 URL を使う(自分のサーバー、画像ホスティングなど)。手軽ですが、リンクが切れることがあります
Yumina にアセットをアップロードする
- パネル → アセット を開く
- 画像ファイルを ドラッグ&ドロップ するか、アップロード を押す
- アップロードしたファイルには、それぞれ
@asset:参照が付く(@asset:a1b2c3d4-e5f6-7890のような形) - アセットの Ref をコピー で、その参照をコピーする
@asset:参照とは? Yumina の内部のアセット識別子です。画面のコードでは、<img src="@asset:xxx" />やインラインスタイルの中の@asset:が描画のときに本物の CDN の URL に変わります。api.resolveAssetUrl("@asset:xxx")を使えば URL を文字列で受け取れます。変数にも@asset:の値を入れられます。
用意しておきたいアセット
背景(16:9 推奨、1920×1080 以上):
| 場面 | ファイル名の例 | 用途 |
|---|---|---|
| 教室(昼) | classroom_morning.jpg | 授業、会話の場面 |
| 学校の廊下 | hallway.jpg | 場面のつなぎ |
| 通り(夕方) | street_evening.jpg | 放課後の場面 |
| 寝室(夜) | room_night.jpg | 夜の場面 |
アップロードしたら、背景ごとの @asset: 参照を控えておきます。どの参照がどの場面かを AI がわかるように、あとで設定に書き込みます。
キャラクターの立ち絵(背景透過の PNG 推奨、高さ 1000px 以上):
キャラクターと表情ごとに 1 枚ずつ用意します。ステップ 5 で対応させやすいよう、characterName_emotion.png の形で名前をそろえておきましょう。
| キャラクター | ファイル名の例 | 参照の例 |
|---|---|---|
| Yuki(笑顔) | yuki_happy.png | @asset:abc123... |
| Yuki(悲しみ) | yuki_sad.png | @asset:def456... |
| Teacher(ふつう) | teacher_neutral.png | @asset:ghi789... |
どの背景を使うかを AI に伝える
ステップ 2 のビジュアルノベルの指示の設定に参照表を足し、どの @asset: 参照がどの場面かを AI に知らせます。立ち絵はここには書きません。AI が設定するのは話し手と表情だけで、立ち絵はコードが選ぶからです。
[Background Reference Table]
- Classroom daytime: @asset:your-classroom-reference
- School hallway: @asset:your-hallway-reference
- Street evening: @asset:your-street-reference
- Bedroom night: @asset:your-bedroom-reference
When switching backgrounds, use the references above as values. For example:
[current_bg: set "@asset:your-classroom-reference"]ここで current_bg に戻り、初期値 を教室の参照にします。
アセットがまだなくても試せます
画像がないとき、画面のコードは単色の背景を表示します。先にしくみを動かして、アセットはあとから足しましょう。手早く試作するなら、素材サイトの https:// の URL でもかまいません。
ステップ 4:オープニングを書く
オープニングは、ビジュアルノベルの最初の場面です。キャンバスでオープニングの行をクリックして書きます。
*The first day of April. The tail end of cherry blossom season.*
*You push open the classroom door. The familiar smell of chalk dust and wood hits you. Most seats are still empty — ten minutes until class starts.*
*In the seat by the window, a girl you've never seen before is quietly gazing outside.*
*A transfer student? You don't remember anyone like her in your class.*オープニングでは、指示を使って変数を設定しません。新しいプレイは各変数の 初期値 から始まるので、current_bg はもう教室を指しています。別の場所から始まるオープニングを追加するなら、キャンバスでオープニングを変数の上にドラッグして、そのオープニング用の初期値を持たせます(オープニングと設定を参照)。
ステップ 5:ビジュアルノベルの画面のコードを書く
画面のコードが画面全体を受け持ちます。<Chat /> を入れ子にする代わりに、背景、立ち絵、ダイアログボックス、選択肢ボタンを自分で描きます。プレイヤーの入力は、画面の下に置いた <MessageInput /> が受け持ちます。
パネル → フロントエンドコード → index.tsx を開き、中身を次のものに置き換えます。SPRITES には、ステップ 3 で用意した自分の参照を入れてください。
// Sprite for each "name_emotion" (lowercase name). Copy the refs from Panels → Assets.
const SPRITES = {
yuki_neutral: "@asset:your-yuki-neutral-reference",
yuki_happy: "@asset:your-yuki-happy-reference",
yuki_sad: "@asset:your-yuki-sad-reference",
teacher_neutral: "@asset:your-teacher-reference",
};
export default function MyWorld() {
const api = useYumina();
const renderMarkdown = api.renderMarkdown;
const msgs = api.messages || [];
const lastMsg = msgs[msgs.length - 1];
const content = lastMsg ? String(lastMsg.content || "") : "";
// ---- Read variables ----
const bgRef = String(api.variables.current_bg || "");
const bgUrl = bgRef ? api.resolveAssetUrl(bgRef) : "";
const speaker = String(api.variables.current_speaker || "Narrator");
const emotion = String(api.variables.speaker_emotion || "neutral");
const showChoices = Boolean(api.variables.show_choices);
// ---- Clean content: strip directive lines, keep only narrative text ----
const cleanContent = content
.split("\n")
.filter((line) => !line.trim().match(/^\[.+:\s*(set|add|subtract|multiply|toggle|append|merge|push|delete)\s+.+\]$/) && !line.trim().match(/^\[.+:\s*[+-]?\d+\]$/))
.join("\n")
.trim();
// ---- Parse text: distinguish narration (italic) from dialogue (plain text) ----
// Split text into paragraphs and classify each one
const paragraphs = cleanContent
.split("\n\n")
.map((p) => p.trim())
.filter((p) => p.length > 0);
const parsed = paragraphs.map((p) => {
// If the entire paragraph is wrapped in *, or every line starts with *, it's narration
const isNarration = /^\*[^*].*[^*]\*$/.test(p.trim())
|| p.trim().startsWith("*");
// Check if it's a choice line (A) B) C) format)
const isChoice = /^[A-Z]\)\s/.test(p.trim());
return { text: p, isNarration, isChoice };
});
// ---- Sprite (looked up from the character name and emotion) ----
const spriteRef = speaker !== "Narrator"
? SPRITES[`${speaker.toLowerCase()}_${emotion}`]
: null;
const spriteUrl = spriteRef ? api.resolveAssetUrl(spriteRef) : null;
// ---- Extract choices ----
const choices = parsed
.filter((p) => p.isChoice)
.map((p) => p.text.replace(/^[A-Z]\)\s*/, ""));
// ---- Render ----
return (
<div style={{
position: "relative",
width: "100%",
minHeight: "500px",
borderRadius: "12px",
overflow: "hidden",
background: "#000",
}}>
{/* ===== Background layer ===== */}
<div style={{
position: "absolute",
inset: 0,
backgroundImage: bgUrl ? `url(${bgUrl})` : "linear-gradient(135deg, #1e293b, #0f172a)",
backgroundSize: "cover",
backgroundPosition: "center",
filter: "brightness(0.7)",
transition: "background-image 0.8s ease",
}} />
{/* ===== Character sprite layer ===== */}
{spriteUrl && (
<div style={{
position: "absolute",
bottom: "120px",
left: "50%",
transform: "translateX(-50%)",
zIndex: 2,
transition: "opacity 0.5s ease",
}}>
<img
src={spriteUrl}
alt={`${speaker} - ${emotion}`}
style={{
maxHeight: "350px",
objectFit: "contain",
filter: "drop-shadow(0 4px 12px rgba(0,0,0,0.5))",
}}
onError={(e) => { e.target.style.display = "none"; }}
/>
</div>
)}
{/* ===== Dialogue box layer ===== */}
<div style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
zIndex: 3,
background: "linear-gradient(transparent, rgba(0,0,0,0.85) 30%)",
padding: "60px 24px 24px",
}}>
{/* Character name label */}
{speaker !== "Narrator" && (
<div style={{
display: "inline-block",
padding: "4px 16px",
marginBottom: "8px",
background: "rgba(99,102,241,0.8)",
borderRadius: "6px 6px 0 0",
color: "#e0e7ff",
fontSize: "14px",
fontWeight: "bold",
letterSpacing: "0.05em",
}}>
{speaker}
</div>
)}
{/* Text content */}
<div style={{
background: "rgba(15,23,42,0.9)",
borderRadius: speaker !== "Narrator" ? "0 12px 12px 12px" : "12px",
padding: "16px 20px",
border: "1px solid rgba(148,163,184,0.2)",
minHeight: "80px",
}}>
{parsed
.filter((p) => !p.isChoice)
.map((p, i) => (
<p key={i} style={{
margin: i > 0 ? "10px 0 0" : "0",
color: p.isNarration ? "#94a3b8" : "#e2e8f0",
fontStyle: p.isNarration ? "italic" : "normal",
fontSize: "15px",
lineHeight: 1.8,
}}
dangerouslySetInnerHTML={{
__html: renderMarkdown(
p.isNarration
? p.text.replace(/^\*|\*$/g, "")
: p.text
),
}}
/>
))
}
</div>
</div>
{/* ===== Choice button layer ===== */}
{showChoices && choices.length > 0 && (
<div style={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
zIndex: 4,
display: "flex",
flexDirection: "column",
gap: "10px",
width: "80%",
maxWidth: "400px",
}}>
{choices.map((choice, i) => (
<button
key={i}
onClick={() => {
api.setVariable("show_choices", false);
api.sendMessage(choice);
}}
style={{
padding: "14px 20px",
background: "rgba(30,27,75,0.9)",
border: "1px solid rgba(99,102,241,0.6)",
borderRadius: "10px",
color: "#c7d2fe",
fontSize: "15px",
fontWeight: "600",
cursor: "pointer",
textAlign: "left",
transition: "all 0.2s ease",
backdropFilter: "blur(8px)",
}}
onMouseEnter={(e) => {
e.target.style.background = "rgba(67,56,202,0.8)";
e.target.style.borderColor = "#818cf8";
}}
onMouseLeave={(e) => {
e.target.style.background = "rgba(30,27,75,0.9)";
e.target.style.borderColor = "rgba(99,102,241,0.6)";
}}
>
{choice}
</button>
))}
</div>
)}
{/* ===== Player input layer ===== */}
{/* No <Chat /> wrapper — drop <MessageInput /> in directly so the player can still type */}
<div style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
zIndex: 5,
}}>
<MessageInput />
</div>
</div>
);
}ブロックごとの説明:
- 内容の整理:
cleanContentは[current_bg: set "xxx"]のような指示の行を取り除きます(set/add/subtract/multiply/toggle/append/merge/push/delete のすべての操作と、[hp: -10]のような短縮形に一致します)。指示はもうエンジンが読み取っているので、コンポーネントで表示する必要はありません - 段落の解析:文章を空行で段落に分け、それぞれを地の文(
*で始まる)、セリフ(普通の文)、選択肢(A)の形で始まる)に分類します - 背景レイヤー:
backgroundImageでいまの場面の背景を表示します。filter: brightness(0.7)で少し暗くして、手前の文字を読みやすくします。transitionで、背景を切り替えるときにクロスフェードが付きます - 立ち絵レイヤー:
speakerとemotionからSPRITESの立ち絵を探します。onErrorは読み込めなかった画像を隠します。Narrator のときや、その名前と表情の項目がないときは、立ち絵を出しません - ダイアログボックスレイヤー:画面下の半透明の枠です。
speakerが「Narrator」でなければ、枠の上にキャラクター名の札が出ます。地の文はグレーの斜体、セリフは白の通常の書体です - 選択肢ボタンレイヤー:
show_choicesがtrueで、文章にA)B)C)の形の選択肢があると、画面の中央にボタンが出ます。ボタンを押すと選択肢が隠れ(show_choicesがfalseになる)、プレイヤーの選んだものが送られます
立ち絵の探し方
SPRITES のキーは、小文字のキャラクター名、アンダースコア、表情をつなげたものです(yuki_happy)。アップロードした立ち絵ごとに 1 行ずつ足してください。キャラクター名が半角英数字だけでない場合は、好きなキーを使い、current_speaker にはそのとおりの名前を書くよう AI に伝えます。値には https:// の URL も使えます。
ステップ 6:全画面のレイアウトか、吹き出しを変えたチャットか
上のコードは自前のレイアウトを返すので、画面いっぱいに表示されます。一番外側の要素は width: "100%" と minHeight: "500px" です。いつもウィンドウ全体を埋めたいなら 100vh を使ってください。全画面にする専用のスイッチはありません。
2 つのパターンを比べると:
- 普通のチャット + 吹き出しのカスタマイズ:
return <Chat renderBubble={...} />。プラットフォームのチャットの枠組みはそのままで、吹き出しの見た目だけを変えます - 純粋なビジュアルノベル(このレシピ):
return <div>...all VN elements...<MessageInput /></div>。<Chat />はまったく使わず、すべてを自分のコードで描きます
チャットとビジュアルノベルを切り替える:「ふだんは普通のチャットで、特別な場面だけビジュアルノベル」にしたいなら、画面のコードの中で変数(たとえば
vn_mode)によって分岐させます。オフなら<Chat />を、オンならビジュアルノベルのレイアウトを返します。AI かビヘイビアがその変数を切り替えれば、プレイの途中で画面が入れ替わります。
ステップ 7:AI が画面を動かすしかた(指示の例)
プレイ中、AI の返信で画面がどう変わるかを見てみましょう。
場面 1:始まり(Narrator モード)
AI の返信:
[current_bg: set "@asset:your-classroom-reference"]
[current_speaker: set "Narrator"]
[speaker_emotion: set "neutral"]
*An April morning. The air carries the sweet scent of cherry blossoms.*
*You walk into the classroom and find a girl you don't recognize sitting by the window. She's resting her chin on her hand, staring outside, lost in thought.*表示される結果:教室の背景 + 立ち絵なし + グレーの斜体の地の文。
場面 2:キャラクターのセリフ
AI の返信:
[current_speaker: set "Yuki"]
[speaker_emotion: set "surprised"]
*She seems to notice you looking and turns her head.*
Oh, hello. Are you in this class too?
[speaker_emotion: set "shy"]
Sorry, I just transferred here today... I don't really know anyone yet.表示される結果:背景はそのまま(current_bg の指示がないので前の値が残る)+ 返信の最後の表情(shy)の Yuki の立ち絵 + ダイアログボックスに「Yuki」の名前 + 斜体の地の文と通常の書体のセリフが交互に並ぶ。
場面 3:プレイヤーに選択肢を出す
AI の返信:
[current_speaker: set "Narrator"]
[show_choices: set true]
*Yuki looks at you, a hint of expectation in her eyes.*
*What do you do?*
A) Introduce yourself and start a conversation
B) Nod briefly and head back to your seat
C) Offer to show her around the classroom and school表示される結果:地の文 + 画面の中央に押せるボタンが 3 つ。プレイヤーが 1 つ押すとボタンが消え、選んだ文がプレイヤーの返事として AI に送られます。
場面 4:場面の切り替え
AI の返信:
[current_bg: set "@asset:your-hallway-reference"]
[current_speaker: set "Narrator"]
*The bell rings. The hallway instantly comes alive as students stream out in pairs and small groups.*
[current_speaker: set "Yuki"]
[speaker_emotion: set "happy"]
Want to have lunch on the rooftop together? I found a really nice spot.表示される結果:背景が廊下に変わる(クロスフェード付き)+ 地の文 + Yuki の笑顔の立ち絵 + セリフ。
ステップ 8:斜体の地の文と普通のセリフの見分け方
画面のコードは、簡単なルールで 2 種類の文章を見分けます。
| 書き方 | 認識されるもの | 表示 | 用途 |
|---|---|---|---|
*This is italic text* | 地の文 | グレー(#94a3b8)、斜体 | 周りの描写、キャラクターの動作、心の声 |
This is plain text | セリフ | 白(#e2e8f0)、通常の書体 | キャラクターが話す内容 |
A) Choice text | 選択肢 | ボタン | プレイヤーが押して選ぶ |
このルールは、設定でもう AI に伝えてあります。AI がたまに書式を間違えても(セリフを斜体にするなど)、* で始まらないものはすべてセリフとして扱われるので、表示が崩れることはありません。
なぜ
>の引用や**太字**ではなく斜体なの? ほとんどのモデルは、ロールプレイで地の文や動作にもともと斜体を使います。いちばん安定して守ってくれる書式だからです。
ステップ 9:保存してテストする
- 上部バーの 保存 を押す
- プレイ を押してプレイテストを始める(プレイテストではマッシーを使います)
- 全画面のビジュアルノベルの表示が見えるはず。背景 + ダイアログボックス + オープニングの地の文
- 入力欄にメッセージを入力する(たとえば「彼女に挨拶する」)
- AI の返信には指示が入っているはず。背景が変わったり、キャラクターが出てきたり、枠の中にセリフが表示されたりします
- AI が選択肢を出したら、画面の中央にボタンが出ます。1 つ押して試してみましょう
- 会話を続けながら、場面が変わるときに AI が
current_bgを、キャラクターが話すときにcurrent_speakerとspeaker_emotionを更新しているかを見ます。値が変わった理由、変わらなかった理由は、プレイテストの このターン に表示されます
うまくいかないときは:
| 症状 | 考えられる原因 | 直し方 |
|---|---|---|
| 背景が黒い、または単色のグラデーション | current_bg が空か、正しい画像の参照ではない | プレイテストで変数の値を確認する。パネル → アセット の @asset: 参照か、https:// の URL になっているはず |
| 立ち絵が見えない | その名前と表情の SPRITES の項目がない | キーが小文字の名前 + _ + 表情(yuki_happy)になっているか、参照が正しいかを確認する |
| 指示の行が画面に出てしまう | 指示の書式が標準と違い、正規表現に一致しなかった | 書式が [variableName: set "value"] になっているか確認する。コロンのあとのスペースに注意 |
| 全部が地の文になる / 全部がセリフになる | AI が書式のルールに従っていない | 設定の書式の指示がはっきりしているか確認する。ビヘイビアルールで念押しすることもできます |
| 選択肢ボタンが出ない | show_choices が true になっていない、または A) の形の選択肢がない | AI の返信に [show_choices: set true] と A) の形の選択肢が入っているか確認する |
| 画面が全画面にならない | 一番外側の要素が表示領域を埋めていない | 一番外側の div に minHeight: "100vh" か height: "100%" を足す |
| 何も表示されない | コンパイルエラー | パネル → フロントエンドコード に エラー とメッセージが出るので、示された行を直す |
応用のヒント
複数のキャラクターの会話
1 つの返信の中で、複数のキャラクターを切り替えられます。
[current_speaker: set "Yuki"]
[speaker_emotion: set "happy"]
The weather is so nice today!
[current_speaker: set "Teacher"]
[speaker_emotion: set "neutral"]
Alright everyone, class is starting. Please take your seats.
[current_speaker: set "Narrator"]
*The classroom falls silent in an instant.*エンジンはこれを順番に反映するので、画面には最後の current_speaker の立ち絵が残ります。段落ごとにそれぞれのキャラクターの立ち絵を出したいなら、段落ごとに直前の [current_speaker: set ...] の指示を(メッセージの元の文章から)読むようにコードを変えます。
切り替えの演出
背景レイヤーの CSS には transition: background-image 0.8s ease が入っているので、背景の切り替えにクロスフェードが付きます。場面の種類によって演出を変えることもできます。
- 普通の切り替え:クロスフェード(実装済み)
- 回想:白いフラッシュを重ねる
- 緊迫した場面:画面を揺らすアニメーションを付ける
効果音や BGM と組み合わせる
場面ごとに専用の音楽を付けましょう。オーディオトラックの 条件付きBGM を使う(current_bg が廊下の参照のあいだは廊下のテーマを流す。オーディオ設計を参照)か、ビヘイビアを追加します。発動するとき → current_bg で 変数が変化、次の場合のみ current_bg がその場面の参照と等しい、効果は 音楽を再生。昼夜サイクルのレシピでも、時間帯について同じ考え方を使っています。
早見表
| やりたいこと | やり方 |
|---|---|
| 背景を切り替える | AI が [current_bg: set "@asset:..."] を送る |
| 話し手を切り替える | AI が [current_speaker: set "characterName"] を送る |
| 表情を切り替える | AI が [speaker_emotion: set "emotion"] を送る |
| 選択肢ボタンを出す | AI が [show_choices: set true] + A) B) C) の形の選択肢を送る |
| 地の文とセリフを見分ける | *斜体* = 地の文、普通の文 = セリフ |
| 全画面のビジュアルノベル | index.tsx がビジュアルノベルのレイアウトを直接返し(<Chat /> なし)、下に入力用の <MessageInput /> を置く |
| キャラクターの立ち絵 | パネル → アセット にアップロードし、その参照を SPRITES に name_emotion の形で並べる |
| 選択肢を押したらメッセージを送る | ボタンの onClick で api.sendMessage(choiceText) を呼ぶ |
| 立ち絵、変数に合わせて変わる背景、選択肢ボタンをコードなしで | 設定 → 立ち絵…、プレイヤー画面の 画像 の部品を変数に 合わせて変える ようにする、プレイヤー画面のボタン |
