Skip to content

カスタムUIガイド

カスタムUIは、クリエイターがワールドに独自の見た目を与える手段です。Battle Royaleにはヘルスバー、キルフィード、動的マップを備えた完全な戦術インターフェースがあります。恋愛ワールドのSakura Seasonはキャラクター立ち絵と背景を使ったビジュアルノベルレイアウトを採用しています。隠れた人気作 Still は、UI全体にパズルゲームを丸ごと走らせています。

これらのクリエイターは誰もコードを自分で書いていません。望むものを説明し、Studio AIが構築しました。

このガイドではワールドを素晴らしく見せる技術を解説します。基礎とセットアップについては はじめに:ビジュアルとオーディオ を参照してください。


ワールドの3つの見た目

Yuminaのすべてのワールドは3つのビジュアルスタイルのいずれかに収まります。ここでの判断がそれ以降すべてを形作ります。

デフォルトチャット(カスタムUI不要)

メッセージはテキストバブルとして表示されます。下部に入力ボックス。すべてが自然にスクロールします。新規ワールドはどれもここから始まり、多くのワールドにはこれで十分です。

適したジャンル:日常系ロールプレイ、シンプルなアドベンチャー、キャラクターチャット、文章こそが体験となるあらゆるワールド。

AIの文章があなたのワールドの特別さなら、デフォルトチャットはプレイヤーをそれに集中させてくれます。できるからといってカスタムUIを追加しないでください。

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

プラットフォームで最も人気のカスタマイズ。チャット体験の全機能(スクロール、ストリーミング、スワイプ、入力ボックス)を保ちつつ、各メッセージの見た目を変えます。

これで開けるもの

  • ワールドのムードに合わせたテーマ背景とフォント
  • 台詞の横のキャラクター立ち絵
  • すべてのメッセージに見えるステータスバー(HP、ゴールド、親密度)
  • ダークトーンと不気味なフォントのホラーゲームスタイリング
  • マルチキャラクターシーンで話者を色分け

チャットを置き換えているのではありません。装飾しているのです。

フルアプリモード

最も強力なオプション。すべてのピクセルを制御します。チャットは大きなデザインの中の一コンポーネントになるか、まったく省略して完全に別のものを作るかです。

これで開けるもの

  • シーン背景とキャラクタースプライトを使ったビジュアルノベルエンジン
  • 場所をクリックするとメッセージが送信されるマップナビゲーション
  • ターン制バトル画面
  • 「アプリ」ごとに異なるAI挙動を起動する電話シミュレータ
  • インタラクティブダッシュボード、インベントリ画面、クエストジャーナル

Battle Royaleは戦術概観にフルアプリモードを使用。Stillはパズルインターフェースに使用。これらのワールドはチャットアプリのようには見えません。

判断:デフォルトチャットから始めましょう。ビジュアルな雰囲気が必要ならカスタムバブルを追加。インタラクティブパネル、ゲームライクなレイアウト、非チャット体験が必要ならフルアプリモードへ。


Studio AIがあなたのビルダー

コードを書く必要はありません。何が欲しいかを知り、それを明確に説明する必要があるだけです。

仕組み

エディタを開き Enter Studio をクリック、AIアシスタントと話します。望む見た目をふつうの言葉で説明。Studio AIがコードを生成し、Canvasパネルでライブプレビューが見えます。

良いプロンプト

説明が具体的なほど良い結果が出ます。パターンは:レイアウトムード、表示する 変数 を説明する。

漠然(AIが全てを推測する必要あり):

「クールな感じにして」

良い(明確なレイアウトとムード):

「ダークでホラーテーマのインターフェースが欲しい。上部に赤いヘルスバー、メッセージは古いタイプライターテキスト風で黄ばんだ紙のように、縁周りにダークなフォグエフェクト」

素晴らしい(レイアウト+ムード+特定変数+振る舞い):

「ビジュアルノベルレイアウトを構築。scene_bg 変数からフルスクリーン背景画像。左に character_sprite からキャラクター立ち絵。底部に半透明の対話ボックス、キャラクター名は speaker_name からピンクで。親密度が75を超えたら、控えめなハートパーティクルエフェクトを追加」

イテレーティブなプロセス

誰も初回で完璧にしません。最高のワールドは反復を通して構築されます。

  1. 大枠を描写する -- 「シーン背景とキャラクター立ち絵のあるビジュアルノベルレイアウトが欲しい」
  2. Canvasプレビューを確認 -- レイアウトは適切? スペーシングは良い?
  3. 詳細を改良 -- 「対話ボックスをもっと透明に。キャラクター立ち絵を右側に移動。対話にセリフフォントを使用」
  4. 磨きをかける -- 「シーンが変わるときフェードインアニメーションを追加。親密度が増加したら親密度メーターを光らせる」

各ラウンドは数秒です。5ラウンドのイテレーションは、すべてを最初に説明しようと1時間かけるよりも勝ります。

Studio AIに変数について何を伝えるか

Studio AIはあなたのワールドの変数定義を読めますが、何が重要かを明示するのが助けになります。

「私の変数:health(0-100、赤いバーで表示)、gold(数値、コインアイコン付きテキストで表示)、location('forest' や 'cave' のような文字列、右上に表示)、is_night(ブール、true なら背景を暗くする)」


何ができるか:ブリッジAPI

カスタムUIは変数を表示するだけでなく、はるかに多くのことができます。ブリッジが提供するもの — 何の関数を呼ぶかではなく、何を構築できるかで説明します。

ゲーム状態を読む

UIは任意の変数、完全なメッセージ履歴、現在のプレイヤー、選択中のモデル、AIが生成中かどうかなどを読めます。これがステータスバー、インベントリ、クエストトラッカーが動作する仕組みです -- 変数を読み視覚的に表示します。

チャットを制御

UIはプレイヤーとしてメッセージを送信、既存メッセージを編集/削除、AIに再生成を依頼、ストリーム中の生成を停止、会話を再起動できます。これがインタラクティブボタンの仕組みです -- 「ポーションを飲む」ボタンがそのテキストをプレイヤーのメッセージとして送り、AIに応答をトリガーします。

オーディオを再生

UIはBGMの再生、効果音、トラック間のフェード、音量制御ができます。変数と組み合わせると、ロケーションやムードに応じて自動で変わる音楽を持てます。

サイドコンプリーション — 1つのワールドに複数のAI「声」

これはプラットフォーム全体で最も強力な機能の1つです。UIは ai.complete() を呼んで、メインチャットに一切触れない別のAI会話を実行できます。AIはあなたのUIにのみ応答 — プレイヤーはそれをチャットメッセージとして見ず、メイン会話の履歴や状態にも影響しません。

これが何を開くか考えてみましょう:

  • NPC電話会話:あるキャラクターが自分のチャットウィンドウをUI内に持つ。プレイヤーがテキストし、AIがそのキャラクターの声で返信し、メインストーリーは別途進む。各サイドキャラクターは独自のシステムプロンプトと性格を持てる。
  • AI生成のアイテム説明:プレイヤーがインベントリのアイテムにホバーすると、現在のストーリーコンテキストに基づいてAIがその場でユニークな説明を書く。
  • ヒントシステム:プレイヤーの状況を分析しヒントを提供する「考える」ボタン — メインAIがキャラクターを崩さず。
  • 内なる独白パネル:NPCが考えていることを表示するサイドパネル、対話を駆動するプロンプトとは別のAIプロンプトで生成。
  • 翻訳や要約パネル:メインチャットと並行するリアルタイムAI駆動の会話要約や翻訳。

includeLorebook: "matched" を渡せばサイドAIがメインチャットと同じワールド伝承とキャラクタープロフィールを参照できる — サイド会話を脱線させずに正典内に保ちます。あるいはワールドコンテキストが要らないタスク(翻訳、分類、純粋なユーティリティ)には省略します。

サイドコールはメインチャットと同じレート制限とクレジット課金を共有します。完全なメソッドシグネチャ、制限、includeLorebook オプションについては APIリファレンス を参照してください。

不可視のコンテキスト注入

UIはメインAIが次ターンに見るがプレイヤーにはチャットに見えないメッセージを送れます。injectContext() を呼ぶとエンジンが次のプロンプトに単発のシステム(またはユーザー)メッセージを差し込み、自動的に破棄します。

これがメイン会話の外で起きたことにAIを反応させる方法です:

  • オフステージイベント:「NPCはあなたが去った後ひとり呟いた:『手紙を見つけられてはならない』」 AIはこれを次の応答に自然に織り込みます。
  • 環境変化:「雨が降り始めた。洞窟入口は今や部分的に水没」 プレイヤーはこの指示を見ませんが、AIは雨を描写します。
  • UI駆動の結果:プレイヤーがカスタムUIのボタン(店からの盗みなど)をクリックすると、何が起きたかをAIに伝えるコンテキストを注入し、反応させる。
  • 電話メッセージや通知:「謎のテキストを受け取った:『今夜9時、いつもの場所』」 プレイヤーにシステムメッセージを見せずに、AIがこれを物語に組み込む。

別AIコールを実行する ai.complete() と違い、injectContext()メイン AIの次の応答にフィードします。両者は補完的:別のAI声が欲しいなら ai.complete()、プレイヤーが言わなかったことをメインAIに知らせたいなら injectContext()

メソッドシグネチャについては APIリファレンス を参照してください。

保存とロード

セッション間で生き残る永続ストレージ。ハイスコア、アンロックされた実績、プレイヤー設定、カスタム設定 -- プレイセッション間で記憶したいすべて。

ナビゲートと通知

没入モードのトグル、トースト通知の表示、クリップボードへのコピー、greetingバリアントの切り替え。UIはプラットフォーム標準インターフェースが持つのと同じコントロールを持ちます。

完全なAPIの場所

タイプシグネチャと例を備えたメソッドごとの完全リファレンスは2か所にあります:

複雑なUIを構築するときはStudio AI、Claude、CursorにWorld Specを与えてください。技術的詳細を扱ってくれます。


3つのカスタマイズパス

パス1:Studio AIに頼む(ほとんどのクリエイターに推奨)

これがほとんどの成功ワールドが採るパスです。望むものを説明し、Studio AIがコードを書き、対話を通して改良します。

強み:コード知識不要。速いイテレーション。Studio AIは全APIを知り、エッジケース(ストリーミング、空状態、モバイルレイアウト)を自動で処理。

いつ使うか:常にここから始める。Studio AIに無理なことに当たった時だけ別パスに切り替える。

パス2:外部AI(Claude、Cursor、ChatGPT)を使う

別のAIツールを好む場合、または長い会話が活きる複雑なものを構築する場合、コードを書くあらゆるAIを使えます。鍵はYuminaの技術コンテキストを与えることです。

外部AIに伝える:

  • コードはTSX(React)、サンドボックスiframe内で動作
  • すべてはグローバル:React、useYumina、Icons、Chat、MessageList、MessageInput、Tailwind CSS
  • エントリファイルは index.tsxexport default function MyWorld() { ... }
  • ゲーム状態は useYumina() から -- 変数、メッセージ、ストリーミング状態、すべて
  • const/let/アロー関数の代わりに varfunction() を使う
  • TypeScript構文なし(ジェネリクスなし、as アサーションなし、インターフェースなし)

いつ使うか:複雑なマルチファイルUI、会話の制御を強めたい、AI駆動のコードエディタで既に作業している。

パス3:手書きコード(経験者向け)

エディタを開き、カスタムUIに行ってTSXを直接書く。ライブプレビューがタイプするたびに更新。

いつ使うか:Reactで考える開発者、すべての細部を精密に制御したい。


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

これらはどのパスを採っても適用されます。Studio AIを使っているなら自動的に扱われます -- このセクションはフードの下で何が起きているかの理解、または何かおかしくなった時のデバッグ用です。

6つのルール

1. エントリファイルのフォーマット

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

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

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

これらはどこでも既に利用可能、インポート不要:ReactuseYuminaIconsChatMessageListMessageInputuseAssetFont、すべてのTailwind CSSクラス。

import React from "react" と書いても何も壊れません(静かに剥がされます)が、不要です。

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. const/let/アロー関数ではなく varfunction() を使う

サンドボックスは時に const/let とアロー関数でスコープ問題があります。varfunction() の方が堅牢です:

tsx
// こちらを推奨
var api = useYumina()
var items = api.variables.inventory || []

// これではなく
const api = useYumina()
const items = api.variables.inventory ?? []

6. TypeScript構文なし

ジェネリクス(<T>)なし、インターフェースなし、as 型アサーションなし、satisfies なし。サンドボックスはTSXをコンパイルしますが完全なTypeScriptはしません。


共通パターン

これらはトップワールドが組み合わせるビルディングブロックです。各説明はパターンが何をするか、いつ使うかを伝えます。コードサンプルは折りたたみ可能 -- 参考用ですが、Studio 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

health、gold、親密度、location、その他の変数を表示する固定パネル。通常チャットの上(<Chat>children プロップを使用)または横(flexレイアウト)に配置。

いつ使うか:プレイヤーが常に見る必要があるステータスを追跡するワールド -- 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>
  )
}

サイドバーゲームパネル

左にチャット、右にキャラクター情報、ステータス、インベントリ、マップを表示する固定パネル。両世界の最良:プレイヤーはフルチャット体験 AND 永続的ゲーム情報を得る。

いつ使うか: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>
  )
}

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

クリックでメッセージを送信したり変数を設定したりするボタン。タイピング以外の最もシンプルなインタラクティビティ。特にオープニング挨拶で強力 -- それをキャラクター作成画面、難易度セレクタ、分岐ストーリーオープナーに変える。

いつ使うか:プレイヤーがタイピングする代わりに(あるいは加えて)選択肢から選ぶワールド。

コード例:キャラクター作成としての挨拶
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を追加する。 デフォルトチャットはきれいで速く、プラットフォームによってメンテされています。ワールドの強みが文章の質なら、デフォルトチャットはプレイヤーをそれに集中させます。UIの複雑さをそれ自体のために追加しないでください。

ストリーミングを忘れる。 AIが生成中のとき msg.isStreaming は true でコンテンツは不完全です。バブルは部分テキストを優雅に扱うべき -- コンテンツを完全だと仮定してパースしないでください。

モバイルでテストしない。 多くのプレイヤーが電話を使います。サイドバーが320px幅ならモバイル画面に収まりません。レスポンシブなTailwindクラス(小画面でパネルを隠す hidden md:block)を使うか、レイアウトを狭幅でテストしてください。

入力をブロック。 フルアプリモードに行って <MessageInput />(または api.sendMessage() を呼ぶ自分の入力)を含めるのを忘れると、プレイヤーがAIと話せません。常にメッセージを送る方法があることを確認してください。

const とアロー関数を使う。 サンドボックスはこれらでスコープ問題を起こすことがあります。代わりに varfunction() を使ってください。Studio AIは自動的にこれをやりますが、手書きや外部AIからの貼り付けでは注意。

グローバルをインポートする。 import React from "react"import { useState } from "react" を書くとエラーが起きます。React、useYumina、Icons、Chat、MessageList、MessageInput -- これらはすべてグローバルです。インポートしないでください。


関連項目

機械可読仕様 → World Spec: Custom UI