NarraLeaf

自定义对话框

用 Nametag、Texts 与 useDialog 替换默认 Dialog 组件,自定义布局与状态读取

作用

对话框(Dialog)用于渲染 Say 模式下的角色对话,包括角色名牌和文本内容。通过 game.configure 替换默认的 Dialog 组件,即可实现完全自定义的对话框样式

下面使用 NametagTexts 分别渲染角色名和台词

1. 创建自定义对话框组件

使用 DialogNametagTexts 三个子组件构建布局。Dialog 为容器,Nametag 渲染角色名,Texts 渲染对话文本

import { Dialog, Nametag, Texts } from "narraleaf-react";

function GameDialog() {
  return (
    <Dialog
      className="bg-black/70 rounded-lg px-6 py-4"
      // Dialog:对话框容器,支持 className/style
    >
      <Nametag className="text-lg font-bold" color="#fbbf24" />
      {/* Nametag: 角色名、内容与颜色由组件 props 控制 */}
      <Texts className="text-base leading-relaxed" defaultColor="white" />
      {/* Texts: 默认文本外观来自组件 props;Sentence/Word 局部样式仍优先 */}
    </Dialog>
  );
}

2. 使用 useDialog 获取状态(可选)

若需在子组件中读取当前对话状态(如 donetextisNarrator),可使用 useDialog Hook:

import { Dialog, Nametag, Texts, useDialog } from "narraleaf-react";

function GameDialog() {
  const { done, text, isNarrator } = useDialog();
  // done:打字是否已完成
  // text:当前显示的文本
  // isNarrator:说话人为 null(旁白)时为 true

  return (
    <Dialog className="bg-black/70 rounded-lg px-6 py-4">
      {!isNarrator && <Nametag className="text-lg font-bold" />}
      {/* 旁白说话时隐藏名牌 */}
      <Texts defaultColor="white" />
    </Dialog>
  );
}

3. 在 App 中注册

创建 Game 时指定 Dialog,再把同一个实例传给 GameProviders。把 Game 放在 React 组件外,也可以避免每次渲染都重新创建实例

import { Game, GameProviders, Player } from "narraleaf-react";
import GameDialog from "./GameDialog";

const game = new Game({ dialog: GameDialog });

function App() {
  return (
    <GameProviders game={game}>
      <Player
        story={story}
        onReady={({ liveGame }) => liveGame.newGame()}
      />
    </GameProviders>
  );
}

确实需要在运行时替换组件时,再调用 game.configure({ dialog: GameDialog })

4. 带打字完成提示的对话框

这个版本用 useDialogdone 控制打字完成标记,用 isNarrator 控制名牌显隐:

import { Dialog, Nametag, Texts, useDialog } from "narraleaf-react";
import clsx from "clsx";

// 子组件:打字完成后显示三角形/下划线指示
function SentenceContext() {
  const { done } = useDialog();
  return (
    <>
      <Texts
        className="max-w-max flex items-center"
        defaultColor="white"
        fontSize={22}
      />
      <div className="flex flex-col items-center">
        <div className={clsx(
          "w-0 h-0 border-l-[6px] border-l-transparent border-r-[6px] border-r-transparent border-t-[10px] border-t-white",
          done ? "opacity-100" : "opacity-0"  // 打字完成时显示指示
        )} />
        <div className="w-[12px] h-[2px] bg-white mt-[2px]" />
      </div>
    </>
  );
}

export function GameDialog() {
  const { isNarrator } = useDialog();

  return (
    <Dialog
      className="absolute bottom-4 left-1/2 -translate-x-1/2 p-12 px-16 w-[90%] h-[216px]"
      style={{
        backgroundImage: "url('/ui/game-dialog.png')",
        backgroundSize: "contain",
        backgroundPosition: "bottom",
        backgroundRepeat: "no-repeat",
      }}
    >
      <div className={clsx("absolute left-[30px] -top-[15px]", { "hidden": isNarrator })}>
        <Nametag
          className="px-4 py-2 min-w-[220px] min-h-[56px] flex items-center justify-center"
          color="#2987a1"
          style={{
            backgroundImage: "url('/ui/game-dialog-nametag.png')",
            backgroundSize: "contain",
            backgroundPosition: "center",
            backgroundRepeat: "no-repeat",
          }}
        />
      </div>
      <div className="flex items-center gap-[5px] h-full">
        <SentenceContext />
      </div>
    </Dialog>
  );
}

文本与名牌默认外观应写在组件自身 props 上。GameConfig 不再负责对话文字字体、文字颜色或名牌默认颜色

5. 简单样式示例

function GameDialog() {
  return (
    <Dialog
      style={{
        backgroundColor: "rgba(0, 0, 0, 0.6)",
        borderRadius: "12px",
        padding: "24px",
        border: "1px solid rgba(255, 255, 255, 0.2)",
      }}
    >
      <Nametag style={{ marginBottom: "8px", paddingBottom: "4px", borderBottom: "2px solid rgba(255, 193, 7, 0.8)" }} />
      <Texts defaultColor="white" fontSize={18} />
    </Dialog>
  );
}

6. 添加进入与退出 Motion

Dialog 可以直接接收 Motion props。你不需要再额外包一层自己的 motion.div 来做对话框动画:

import { Dialog, Nametag, Texts } from "narraleaf-react";

function GameDialog() {
  return (
    <Dialog
      className="absolute bottom-4 left-1/2 -translate-x-1/2 w-[90%] rounded-xl bg-black/75 px-6 py-4"
      initial={{ opacity: 0, y: 24 }}
      animate={{ opacity: 1, y: 0 }}
      exit={{ opacity: 0, y: -24 }}
      transition={{ duration: 0.24, ease: "easeOut" }}
      layout
      onAnimationComplete={() => {
        // 可选的 Motion 事件处理
      }}
    >
      <Nametag className="font-bold" color="#fbbf24" />
      <Texts defaultColor="white" />
    </Dialog>
  );
}

这些 Motion props 会应用到对话框内部的 motion.div。外层包裹仍负责播放器缩放以及点击/按键行为,因此 xyscalerotate 等 transform 不会和 useAspectScale 冲突

对话框的 presence 时序由播放器管理:

  • 连续两句对话会复用同一个 dialog presence slot,所以上一句不会在下一句出现前播放 exit
  • 如果对话后面接非对话内容或空档,旧对话框可以播放 exit,同时故事流程继续推进
  • 如果旧对话框的退出动画尚未完成,后面又出现新对话,新对话会获得独立 presence key,因此旧退出和新进入可以重叠播放
  • 通过 dialog slot 渲染的菜单 prompt 也使用相同 motion 行为,然后再显示选项
  • 正在退出的对话框会忽略点击、skip 和 auto-forward,因此不会再次推进剧情

注意事项

  • 名牌内容与颜色:由 NametagnamechildrencolorclassNamestyle 等 props 控制
  • 文本默认外观:由 TextsdefaultColorfontSizefontFamilyfontWeightfontWeightBold 等 props 控制。Word / Sentence 的局部样式仍会覆盖具体词或具体句

本页目录