v
Skip to content

カスタムUIガイド ​

画面のコードは、プレイヤーが見る画面を描く小さな React(TSX)アプリです。チャットの見た目を変えたり、まわりにパネルを足したり、チャットとはまったく違う画面に置き換えたりできます。置き場所は パネル → フロントエンドコード です。

いまはその多くがコードなしでできます。プレイヤー画面 のエディタでは、オープニングのページ、ステータスのパネル、持ち物、地図、ボタンをテンプレートから作れて、ボタンからは変数を変えたり、ビヘイビアを動かしたり、AI を呼んだりできます。テンプレートや部品ではやりたいことができないときに、画面のコードを使います。それでも、ほとんどのクリエイターは手で書いていません。どんな画面にしたいかを AIアシスタントに説明して、コードを書いてもらっています。

基本とセットアップは はじめに:ビジュアルとオーディオ を見てください。


ワールドの 3 つの見た目 ​

デフォルトのチャット(カスタムUIなし) ​

メッセージは吹き出しで表示され、下に入力欄があり、自然にスクロールしていきます。新しいワールドはどれもここから始まり、多くのワールドはこれで十分です。

向いているもの:日常系のロールプレイ、シンプルな冒険もの、キャラクターとのチャット、文章そのものが体験になっているワールド。

プレイヤー画面の メッセージの見た目(標準、吹き出し、小説、便箋、ターミナル)と、その細かな調整項目を使えば、コードなしでこのチャットの見た目を変えられます。

カスタムのメッセージバブル ​

チャットの機能(スクロール、ストリーミング、スワイプ、入力欄)はそのままに、メッセージ一つひとつの見た目を変えます。

これで作られているもの:

  • ワールドの雰囲気に合わせた背景とフォント
  • 台詞の横に出るキャラクターの立ち絵
  • どのメッセージにも表示されるステータスバー(HP、ゴールド、好感度)
  • 暗い色調と不気味なフォントのホラーゲーム風
  • 複数キャラクターの場面で、話し手ごとに色分け

フルアプリモード ​

画面全体をあなたのコードが描きます。チャットはレイアウトの中の部品のひとつになるか、チャットを使わずにまったく別のものを作ります。

これで作られているもの:

  • シーン背景とキャラクターのスプライトを使ったビジュアルノベル画面
  • 場所をクリックするとメッセージが送られる地図での移動
  • ターン制のバトル画面
  • 「アプリ」ごとに AI の振る舞いが変わるスマホシミュレーター
  • インタラクティブなダッシュボード、持ち物画面、クエスト日誌

Battle Royale は戦況の一覧表示にフルアプリモードを使っています。Still はパズルの画面に使っています。

まずはデフォルトのチャットから始めましょう。見た目の雰囲気がほしければ、メッセージの見た目を変えるか、カスタムバブルを足します。インタラクティブなパネルやゲームらしいレイアウト、チャットではない体験が必要ならプレイヤー画面を使い、それで足りないときにフルアプリモードにします。


AIアシスタントに作ってもらう ​

使い方 ​

キャンバス右上の AIアシスタント を押して、ほしい見た目をふだんの言葉で説明します。アシスタントが画面のコードを パネル → フロントエンドコード に書き込み、キャンバス上のプレイヤー画面のプレビューで結果を確認できます。

いい頼み方 ​

説明が具体的なほど、いい結果になります。レイアウト、雰囲気、表示したい 変数 を伝えてください。

あいまい(AI がすべてを推測するしかない):

「かっこよくして」

いい(レイアウトと雰囲気がはっきりしている):

「ダークなホラー調の画面にしたい。上に赤い体力バー、メッセージは黄ばんだ紙に打った古いタイプライターの文字みたいに、画面の縁には暗い霧のエフェクト」

とてもいい(レイアウト + 雰囲気 + 具体的な変数 + 動き):

「ビジュアルノベルのレイアウトを作って。背景は scene_bg 変数の画像を全画面に。左に character_sprite のキャラクター立ち絵。下に半透明の台詞ボックスを置いて、キャラクター名は speaker_name からピンクで表示。好感度が 75 を超えたら、さりげないハートのパーティクルを足して」

何回かに分けて頼む ​

  1. 全体像を伝える -- 「シーン背景とキャラクター立ち絵のあるビジュアルノベルのレイアウトにしたい」
  2. プレビューを確認する -- レイアウトはしっくりくる? 余白はちょうどいい?
  3. 細部を直す -- 「台詞ボックスをもっと透明に。立ち絵を右側に移動。台詞は明朝体で」
  4. 仕上げる -- 「シーンが変わるときにフェードインを入れて。好感度が上がったら好感度メーターを光らせて」

長い説明を 1 回で渡すより、短いやりとりを何回か重ねるほうが早く仕上がります。

変数についてアシスタントに伝えること ​

アシスタントは変数を読めますが、何が大事かをはっきり書いておくと助かります。

「変数はこう:health(0〜100、赤いバーで表示)、gold(数値、コインのアイコン付きの文字で表示)、location('forest' や 'cave' のような文字列、右上に表示)、is_night(真偽値、true なら背景を暗くする)」


画面のコードでできること ​

コードは useYumina() を通じてゲームとやりとりします。作れるものの観点でまとめると、次のとおりです。

ゲームの状態を読む ​

UI はどの変数でも、メッセージ履歴全体、いまのプレイヤー、選ばれているモデル、AI が生成中かどうか、なども読めます。ステータスバー、持ち物、クエストトラッカーはこうして動いています。変数を読んで表示しているだけです。

api.variables.hunger は、ID が hunger の変数を探し、見つからなければ 表示名 が hunger の変数を探します。どちらの書き方でも通るので、キャンバスで見えている名前で作った HUD は、ID がランダムな UUID でも実際の値を読めます。書き込みも同じで、api.setVariable("hunger", 50) はその変数に届きます。詳しいルールは API リファレンス の「ID と表示名」を見てください。

チャットを操作する ​

UI からプレイヤーとしてメッセージを送る、既存のメッセージを編集・削除する、AI に再生成させる、ストリーミング中の生成を止める、会話をやり直す、といったことができます。たとえば「薬を飲む」ボタンは、その文をプレイヤーのメッセージとして送り、AI が返答します。

ビヘイビアを動かす ​

api.executeActionAndWait("buy", { price: 5 }) は、プレイヤーがボタンを押した をトリガーにしていて ボタン の名前が buy のビヘイビアを実行し、値を渡します。ビヘイビアの側では {param.price} として読めます。ショップ、クラフト、戦闘のダイスロールはキャンバスのビヘイビアに置いておき、コードはどのアイテムがクリックされたかを伝えるだけ、という作り方ができます。ビヘイビア · ボタンから値を渡す を見てください。

カードの AI を呼ぶ ​

キャンバス上の UI 型 の AI は、画面から呼ばれるのを待っています。api.callAi("Fortune Mushroom", input) はその AI を実行して { text, fields } を返します。AI のせりふと、回答形式 で埋めた値です。モデル、見えるもの、再試行、クールダウン、予備のせりふはすべてキャンバスで設定し、答えはコードに届く前にチェックされます。せりふの行き先 を 画面のチャンネル にしておけば、api.onAiOutput(channel, cb) でその出力を受け取ることもできます。AI を見てください。

AI を直接呼ぶ ​

プロンプトを全部自分で書きたいときは、ai.complete() でメインのチャットには一切触れない別の AI 呼び出しを実行できます。プレイヤーにはチャットのメッセージとして見えず、メインの会話の履歴や状態にも影響しません。使いみちの例:

  • NPC とのスマホでの会話:あるキャラクターが UI の中に自分のチャット画面を持つ。プレイヤーがメッセージを送ると、AI がそのキャラクターの口調で返し、メインの物語は別に進む。脇役ごとに別のシステムプロンプトと性格を持たせられる。
  • AI が書くアイテム説明:プレイヤーが持ち物のアイテムにカーソルを合わせると、いまの物語の流れをふまえて、AI がその場で説明を書く。
  • ヒント機能:プレイヤーの状況を見てそっと手がかりを出す「考える」ボタン。メインの AI はキャラクターを崩さずにすむ。
  • 心の声のパネル:NPC が考えていることを横のパネルに出す。台詞を動かしているのとは別の AI プロンプトで生成する。
  • 翻訳や要約のパネル:会話の要約や翻訳を AI に書かせ、メインのチャットの横に表示する。

includeLorebook: "matched" を渡すと、サイドの AI もメインのチャットと同じ世界の設定やキャラクターのプロフィールを見られます。世界の情報がいらない作業(翻訳、分類、ただの道具的な処理)なら省きます。

サイドの呼び出しにはメインのチャットとは別のレート制限枠があり(毎分 100 回)、料金はメインのチャットと同じくトークン単位です。メソッドの完全なシグネチャ、制限、includeLorebook のオプションは API リファレンス にあります。

見えないコンテキストを差し込む ​

UI から、メインの AI は次のターンに読むけれどプレイヤーのチャットには表示されないメッセージを送れます。injectContext() を呼ぶと、エンジンが次のプロンプトに 1 回だけシステム(またはユーザー)メッセージを差し込み、そのあと自動で捨てます。

メインの会話の外で起きたことに AI を反応させるには、これを使います。

  • 舞台裏の出来事:「あなたが去ったあと、NPC はひとりつぶやいた。『あの手紙だけは見つけられてはいけない』」 AI はこれを次の返答に織り込みます。
  • 環境の変化:「雨が降りはじめた。洞窟の入口は半分水につかっている」 プレイヤーにはこの指示が見えませんが、AI は雨を描写します。
  • UI での行動の結果:プレイヤーがカスタムUIのボタンを押したとき(店で盗みを働くなど)、何が起きたかを伝えるコンテキストを差し込んで、AI に反応させる。
  • スマホのメッセージや通知:「謎めいたメッセージが届いた。『今夜9時、いつもの場所で』」 プレイヤーにシステムメッセージを見せずに、AI がこれを物語に組み込みます。

ai.complete() は別の AI 呼び出しを実行し、injectContext() は メイン の AI の次の返答に入ります。別の AI の声がほしいときは ai.complete()、プレイヤーが言っていないことをメインの AI に知らせたいときは injectContext() を使います。プレイヤーがいまどの部屋にいるか、画面に何が出ているかのように、ターンをまたいで変わらない情報には setScene() を使います。変えるまで、すべての AI 呼び出しにそれが渡されます。

メソッドのシグネチャは API リファレンス を見てください。

オーディオを再生する ​

UI から BGM や効果音を鳴らしたり、曲をフェードで切り替えたり、音量を調整したりできます。変数と組み合わせれば、場所や雰囲気に合わせて変わる音楽も作れます。

保存と読み込み ​

保存先は 2 種類あります。api.storage は、このブラウザにワールドごとに文字列を保存します。設定、最後に開いたタブ、この端末でのハイスコアなど。api.sessionStorage は、小さな JSON の記録をプレイセッションと一緒にサーバーに保存するので、ほかの端末に移っても、チェックポイントでもついてきます。

画面の切り替えと通知 ​

没入モードの切り替え、トースト通知の表示、クリップボードへのコピー、オープニングの切り替え、ペルソナ管理やモデル選択を開く、といったことができます。

API の全体はどこにある?

メソッドごとの完全なリファレンスは、型シグネチャと例つきで 2 か所にあります。

複雑な UI を作るときは、World Spec を自分の AI(Claude、ChatGPT、Cursor)に渡してください。


カスタマイズの 3 つの方法 ​

方法 1:AIアシスタントに頼む ​

ほしいものを説明すると、アシスタントがコードを書き、会話しながら直していきます。

強み:コードの知識はいりません。何度でもすぐ直せます。アシスタントは API を知っていて、ストリーミング、空の状態、スマホでのレイアウトといった細かいところも面倒を見てくれます。

いつ使うか:まずはここから。アシスタントにできないことにぶつかったときだけ、ほかの方法に切り替えます。

方法 2:自分の AI(Claude、ChatGPT、Cursor)を使う ​

上部バーの AI を接続 から、Claude、ChatGPT / Codex、Cursor、Claude Code、Codex CLI にサインインしてもらえば、画面のコードも含めてカードを直接編集できます。AIアシスタントと自分の AI を見てください。

コードを自分で貼りつけたい場合は、AI に次のことを伝えます。

  • コードは TSX(React)で、サンドボックスの iframe の中で動く
  • React、useYumina、Icons、Chat、MessageList、MessageInput、Tailwind CSS はすべてグローバルで使える
  • エントリファイルは index.tsx で、export default function MyWorld() { ... }
  • ゲームの状態は useYumina() から取る -- 変数、メッセージ、ストリーミング状態、ほか全部
  • フックはインポートせず、React.useState などを使う

いつ使うか:複数ファイルにまたがる複雑な UI、じっくり長く相談したいとき、もともと AI つきのコードエディタで作業しているとき。

方法 3:手で書く(経験のある開発者向け) ​

パネル → フロントエンドコード を開いて、TSX を直接書きます。入力するそばから、コンパイルの表示が OK か エラー かを示します。

いつ使うか:React で考えるのに慣れた開発者や、細部まで自分でコントロールしたい人。


カスタムUIのコードの基本ルール ​

どの方法をとっても当てはまります。AIアシスタントは最初からこれを守って書きます。このセクションは、裏で何が起きているかを知りたいときや、うまく動かないときのデバッグ用です。

5 つのルール

1. エントリファイルの形

index.tsx は関数コンポーネントを default でエクスポートする必要があります。これが UI のルートになります。

tsx
export default function MyWorld() {
  return <Chat />;
}

2. グローバル -- インポートしない

次のものはどこでもそのまま使えて、インポートはいりません:React、useYumina、Icons、Chat、MessageList、MessageInput、useAssetFont、ロア操作の部品(LoreSlot、LoreButton など)、モデル選択のコンポーネント、Tailwind CSS のすべてのクラス。全部の一覧は API リファレンス にあります。

import 文はコンパイル時に取り除かれます。なので import React from "react" は何もしませんし、import { useState } from "react" と書くと useState は未定義のままになります。

3. 自分のファイルはインポートできる

複数ファイルの画面のコードは ES モジュールの書き方を使います。

tsx
import StatBar from "./stat-bar"
import DialogueBox from "./dialogue-box"

4. useState() ではなく React.useState() を使う

React はモジュールとして使えますが、フックは個別に取り出されていません。必ず React. をつけます。

tsx
var [count, setCount] = React.useState(0)

5. TypeScript の型は取り除かれるだけで、チェックはされない

型注釈やインターフェースはコンパイル時に消えるので、書いても壊れはしませんが、何もチェックされません。AIアシスタントが書くのは JSX を使った素の JavaScript です。const、let、var、アロー関数はどれも使えます。


よく使うパターン ​

カスタムの画面の多くは、次の部品の組み合わせでできています。コード例は折りたたんであります。参考用で、この種のコードは AIアシスタントが書いてくれます。

カスタムのメッセージバブル ​

チャットの機能はそのままに、メッセージの見た目を変えます。<Chat renderBubble={...} /> で吹き出しの描画だけを引き受け、それ以外(スクロール、ストリーミング、スワイプ、入力)はプラットフォームに任せます。

いつ使うか:チャット全体を作り直さずに、テーマに合ったメッセージ(ダークなホラー、上品な恋愛もの、SF のターミナル)にしたいとき。プレイヤー画面の メッセージの見た目 では足りないとき。

コード例:ステータスつきのテーマ吹き出し
tsx
export default function MyWorld() {
  var api = useYumina()

  return (
    <Chat renderBubble={function(msg) {
      if (msg.role === "user") {
        return (
          <div className="ml-auto max-w-[80%] rounded-xl bg-blue-500/20 px-4 py-3 text-blue-100">
            {msg.rawContent}
          </div>
        )
      }

      return (
        <div className="mr-auto max-w-[85%] rounded-xl border border-zinc-700 bg-zinc-900 p-4">
          <div dangerouslySetInnerHTML={{ __html: msg.contentHtml }} />
          <div className="mt-3 flex gap-4 text-xs text-zinc-400">
            <span>HP {api.variables.health}/100</span>
            <span>Gold {api.variables.gold}</span>
          </div>
        </div>
      )
    }} />
  )
}

ステータス表示と HUD ​

体力、ゴールド、好感度、現在地など、変数を表示する固定のパネルです。ふつうはチャットの上(<Chat> の children プロップを使う)か横(flex レイアウト)に置きます。プレイヤー画面の ステータス盤 と 冒険HUD のテンプレートなら、コードなしで作れます。

いつ使うか:プレイヤーが常に見ている必要のある数値を追うワールド -- RPG、サバイバルゲーム、好感度メーターのある恋愛シミュレーション。

コード例:上部の HUD バー
tsx
export default function MyWorld() {
  var api = useYumina()

  return (
    <Chat>
      <div className="shrink-0 px-4 py-2 bg-black/60 backdrop-blur flex gap-4 text-xs text-zinc-300">
        <div className="flex items-center gap-1">
          <Icons.Heart className="w-3 h-3 text-red-400" />
          <span>{api.variables.health || 100}/100</span>
        </div>
        <div className="flex items-center gap-1">
          <Icons.Coins className="w-3 h-3 text-amber-400" />
          <span>{api.variables.gold || 0}</span>
        </div>
        <div className="ml-auto text-zinc-500">
          {api.variables.location || "Unknown"}
        </div>
      </div>
    </Chat>
  )
}

ビジュアルノベルのレイアウト ​

全画面のシーン背景、キャラクターのスプライト、下に半透明の台詞ボックス。シーンやキャラクターの情報は、物語の進行に合わせて AI が更新する変数から読むのがふつうです。ひととおりの作り方は レシピ:ビジュアルノベルモード にあります。

いつ使うか:恋愛、ドラマ、日常ものなど、ふつうのチャット画面より見た目の雰囲気が大事な物語。

コード例:シーン背景つきの VN の枠
tsx
export default function MyWorld() {
  var api = useYumina()
  var bg = api.variables.scene_bg
  var sprite = api.variables.character_sprite
  var speaker = api.variables.speaker_name
  var lastMsg = (api.messages || []).slice(-1)[0]

  return (
    <div
      className="relative w-full h-full bg-cover bg-center"
      style={{
        backgroundImage: bg
          ? "url(" + bg + ")"
          : "linear-gradient(135deg, #1e293b, #0f172a)"
      }}
    >
      {sprite && (
        <img
          src={sprite}
          className="absolute bottom-0 left-1/2 -translate-x-1/2 max-h-[80%] pointer-events-none"
        />
      )}

      <div className="absolute inset-x-4 bottom-4">
        <div className="rounded-xl border border-white/10 bg-black/70 p-4 backdrop-blur-sm">
          {speaker && (
            <div className="mb-1 text-sm font-bold text-pink-300">{speaker}</div>
          )}
          <div className="leading-relaxed text-zinc-100">
            {lastMsg ? lastMsg.content : ""}
          </div>
        </div>

        <div className="mt-2">
          <MessageInput />
        </div>
      </div>
    </div>
  )
}

サイドバーのゲームパネル ​

左にチャット、右にキャラクター情報、ステータス、持ち物、地図などを出す固定パネル。プレイヤーはチャットをそのまま使いながら、話している最中にゲームの情報を確認できます。

いつ使うか:RPG、アドベンチャーゲームなど、チャットしながらステータスや持ち物を見る必要があるワールド。

コード例:チャット + サイドバー
tsx
export default function MyWorld() {
  var api = useYumina()

  return (
    <div className="flex h-full">
      <div className="flex-1 min-w-0">
        <Chat />
      </div>
      <aside className="w-72 shrink-0 border-l border-border bg-card p-4 overflow-y-auto">
        <div className="text-sm font-bold mb-3">{api.variables.player_name || "Adventurer"}</div>

        <div className="space-y-2 text-xs">
          <div className="flex justify-between">
            <span className="text-muted-foreground">HP</span>
            <span>{api.variables.health || 100}/{api.variables.max_health || 100}</span>
          </div>
          <div className="h-1.5 rounded-full bg-zinc-800 overflow-hidden">
            <div
              className="h-full bg-red-500 transition-all duration-300"
              style={{ width: ((api.variables.health || 100) / (api.variables.max_health || 100) * 100) + "%" }}
            />
          </div>
        </div>

        <div className="mt-4 text-xs text-muted-foreground">
          <div className="font-medium mb-2">Inventory</div>
          <div className="grid grid-cols-3 gap-1">
            {(api.variables.inventory || []).map(function(item, i) {
              return (
                <div key={i} className="aspect-square rounded border border-border bg-muted flex items-center justify-center text-[10px]">
                  {item.name || "?"}
                </div>
              )
            })}
          </div>
        </div>
      </aside>
    </div>
  )
}

インタラクティブなボタンと選択肢 ​

クリックするとメッセージを送ったり、変数を変えたり、ビヘイビアを動かしたりするボタンです。よくある使い方はオープニングで、キャラクター作成画面、難易度の選択、物語の分岐の入口にします。

ほとんどはプレイヤー画面でコードなしでできます。ボタンには プレイヤーの代わりに発言、変数を変える、ビヘイビアを動かす、AI を呼ぶ の手順を入れられ、オープニング用には オープニングを選ぶ、名前を入力、難易度を選ぶ などのテンプレートがあります。ボタンを吹き出しの中に置きたいときや、手順では表せないロジックが必要なときにコードを使います。

いつ使うか:プレイヤーに入力してもらう代わりに(または入力と合わせて)、選択肢から選んでもらいたいワールド。

コード例:オープニングをキャラクター作成にする
tsx
export default function MyWorld() {
  var api = useYumina()

  return (
    <Chat renderBubble={function(msg) {
      if (msg.messageIndex === 0 && msg.role === "assistant") {
        return (
          <div className="space-y-4">
            <div dangerouslySetInnerHTML={{ __html: msg.contentHtml }} />
            <div className="flex gap-3">
              <button
                onClick={function() {
                  api.setVariable("class", "Warrior")
                  api.sendMessage("I choose Warrior")
                }}
                className="px-4 py-3 rounded-lg border border-zinc-600 hover:bg-zinc-800 transition"
              >
                Warrior
              </button>
              <button
                onClick={function() {
                  api.setVariable("class", "Mage")
                  api.sendMessage("I choose Mage")
                }}
                className="px-4 py-3 rounded-lg border border-zinc-600 hover:bg-zinc-800 transition"
              >
                Mage
              </button>
            </div>
          </div>
        )
      }

      return <div dangerouslySetInnerHTML={{ __html: msg.contentHtml }} />
    }} />
  )
}

よくあるミス ​

必要ないのにカスタムUIを足す。 デフォルトのチャットはすっきりしていて速く、プラットフォームが面倒を見ています。ワールドの強みが文章なら、デフォルトのチャットのほうがプレイヤーは文章に集中できます。

ストリーミングを忘れる。 AI が生成中のあいだは msg.isStreaming が true で、内容はまだ途中です。吹き出しは途中の文章でも崩れないようにしてください。内容が完成している前提でパースしないこと。

スマホで試していない。 スマホで遊ぶプレイヤーはたくさんいます。サイドバーの幅が 320px あると、スマホの画面には収まりません。レスポンシブな Tailwind のクラス(小さい画面でパネルを隠す hidden md:block)を使うか、狭い幅でレイアウトを確認してください。

入力をふさぐ。 フルアプリモードで <MessageInput />(または api.sendMessage() を呼ぶ自前の入力欄)を入れ忘れると、プレイヤーは AI と話せません。メッセージを送る手段が必ずあるようにしてください。

オープニングを切り替える前に変数を変える。 api.switchGreeting() は、変数をそのオープニングの開始値に戻します。先にこれを await してから、自分の変数を設定してください。

グローバルをインポートする。 import 文は取り除かれるので、import { useState } from "react" と書くと useState は未定義のままです。React、useYumina、Icons、Chat、MessageList、MessageInput はすべてグローバルです。インポートしないでください。


関連項目 ​

機械が読むための仕様 → World Spec: Custom UI