自定义对话框
用 Nametag、Texts 与 useDialog 替换默认 Dialog 组件,自定义布局与状态读取
作用
对话框(Dialog)用于渲染 Say 模式下的角色对话,包括角色名牌和文本内容。通过 game.configure 替换默认的 Dialog 组件,即可实现完全自定义的对话框样式
下面使用 Nametag 和 Texts 分别渲染角色名和台词
1. 创建自定义对话框组件
使用 Dialog、Nametag、Texts 三个子组件构建布局。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 获取状态(可选)
若需在子组件中读取当前对话状态(如 done、text、isNarrator),可使用 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. 带打字完成提示的对话框
这个版本用 useDialog 的 done 控制打字完成标记,用 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。外层包裹仍负责播放器缩放以及点击/按键行为,因此 x、y、scale、rotate 等 transform 不会和 useAspectScale 冲突
对话框的 presence 时序由播放器管理:
- 连续两句对话会复用同一个 dialog presence slot,所以上一句不会在下一句出现前播放
exit - 如果对话后面接非对话内容或空档,旧对话框可以播放
exit,同时故事流程继续推进 - 如果旧对话框的退出动画尚未完成,后面又出现新对话,新对话会获得独立 presence key,因此旧退出和新进入可以重叠播放
- 通过 dialog slot 渲染的菜单 prompt 也使用相同 motion 行为,然后再显示选项
- 正在退出的对话框会忽略点击、skip 和 auto-forward,因此不会再次推进剧情