v
Skip to content

ビジュアルノベルモード ​

全画面のビジュアルノベルです。シーンの背景、キャラクターの立ち絵、ダイアログボックス、選択肢ボタンがあり、物語の進行に合わせて 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.

エンジンがこれらの指示を読み取ると:

  1. current_bg が教室の画像になる → 画面のコードが background-image を教室に切り替える
  2. current_speaker が "Yuki" になる → ダイアログボックスに「Yuki」という名前が出る
  3. speaker_emotion が "happy" になる → 画面のコードが Yuki の笑顔の立ち絵を探して表示する
  4. 画面のコードが最新のメッセージの文章を解析する。斜体 の部分は地の文、普通の文はキャラクターのセリフとして描かれる
エンジンの処理の流れ:
  AI の返信 → エンジンが指示を取り出す → 変数を更新 → 画面のコードが変数を読んで描き直す
    → 背景レイヤー:<div style={{ backgroundImage: ... }} />
    → 立ち絵レイヤー:<img src={SPRITES[speaker + "_" + emotion]} />
    → ダイアログボックスレイヤー:地の文(グレーの斜体)とセリフ(白の通常の書体)を区別
    → 選択肢レイヤー:show_choices = true のときボタンが出る

手順 ​

ステップ 1:変数を作る ​

ビジュアルノベルの表示を動かすには、変数が 4 つ必要です。

キャンバスで、カードの下にある 追加できるもの の + 変数 を 1 つずつ押します。それぞれ下の名前を付けてください。画面のコードも AI も、名前で変数を探します。

変数 1:いまの背景 ​

項目値理由
変数名current_bgAI は [current_bg: set "..."] と書いて背景を切り替え、コードは api.variables.current_bg を読みます
型文字値は @asset: 参照か画像の URL
初期値最初の場面の背景の @asset: 参照AI が何も変えないうちに、新しいプレイで表示されるもの。ステップ 3 のあとで入れます
ビヘイビアルール場面の背景画像。場面が別の場所に移るたびに、アセット表の対応する参照を使って変える。いつ変えるか、どの値を使うかを AI に伝えます

変数 2:いま話している人物 ​

項目値理由
変数名current_speakerAI は [current_speaker: set "name"] と書いて話し手を切り替えます
型文字値はキャラクター名
初期値Narrator地の文のモードで始まる。特定のキャラクターは話していない状態
ビヘイビアルールいま話している人物。キャラクターが話すときはその名前に、地の文や心の声のときは "Narrator" にする。使い方のルールを AI に伝えます

変数 3:話し手の表情 ​

項目値理由
変数名speaker_emotionAI は [speaker_emotion: set "happy"] と書いて表情を切り替えます
型文字値は表情のキーワード
初期値neutralふつうの表情で始まる
ビヘイビアルール話しているキャラクターの表情。neutral, happy, sad, angry, surprised, shy のどれか1つだけ。表情が変わるたびに更新する。表情を列挙しておくと、立ち絵のない表情を AI が作り出さなくなります

変数 4:選択肢を出す ​

項目値理由
変数名show_choicesAI は [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 にアセットをアップロードする ​

  1. パネル → アセット を開く
  2. 画像ファイルを ドラッグ&ドロップ するか、アップロード を押す
  3. アップロードしたファイルには、それぞれ @asset: 参照が付く(@asset:a1b2c3d4-e5f6-7890 のような形)
  4. アセットの 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 で用意した自分の参照を入れてください。

tsx
// 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:保存してテストする ​

  1. 上部バーの 保存 を押す
  2. プレイ を押してプレイテストを始める(プレイテストではマッシーを使います)
  3. 全画面のビジュアルノベルの表示が見えるはず。背景 + ダイアログボックス + オープニングの地の文
  4. 入力欄にメッセージを入力する(たとえば「彼女に挨拶する」)
  5. AI の返信には指示が入っているはず。背景が変わったり、キャラクターが出てきたり、枠の中にセリフが表示されたりします
  6. AI が選択肢を出したら、画面の中央にボタンが出ます。1 つ押して試してみましょう
  7. 会話を続けながら、場面が変わるときに 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) を呼ぶ
立ち絵、変数に合わせて変わる背景、選択肢ボタンをコードなしで設定 → 立ち絵…、プレイヤー画面の 画像 の部品を変数に 合わせて変える ようにする、プレイヤー画面のボタン