UI 变量
如何使用 TanStack Query/Store 将 UI 组件连接到设置、只读游戏数据以及可读写的游戏存储变量,以及如何通过 storage.setStorageHandler 使 UI 保持同步。
在 Pixi’VN UI 中,你显示或编辑的变量通常分为三类,每一类都有各自推荐的模式。 Pixi’VN 的模板已经在 src/lib/stores/ 和 src/lib/query/ 中大量使用了这些模式——值得打开看看,作为真实可用的示例。
设置变量
文本速度、字体大小或自动前进延迟等设置并非游戏存储的一部分:它们必须在每次游玩过程中持久保存(甚至在存档存在之前也是如此),因此它们保存在 localStorage 中,并镜像到一个 TanStack Store 中,以便组件能够对变化做出响应。
import { Store } from "@tanstack/store";
type AutoSettingsStore = {
enabled: boolean;
time: number;
};
export namespace AutoSettings {
export const store = new Store<AutoSettingsStore>({
enabled: Boolean(localStorage.getItem("auto_forward_enabled") ?? false),
time: Number(localStorage.getItem("auto_forward_second") ?? 1),
});
export function setEnabled(value: boolean) {
localStorage.setItem("auto_forward_enabled", value.toString());
store.setState((state) => ({ ...state, enabled: value }));
}
export function setTime(value: number) {
localStorage.setItem("auto_forward_second", value.toString());
store.setState((state) => ({ ...state, time: value }));
}
}import { useSelector } from "@tanstack/react-store";
import { AutoSettings } from "@/lib/stores/auto-settings-store";
function AutoForwardToggle() {
const enabled = useSelector(AutoSettings.store, (state) => state.enabled);
return (
<Switch checked={enabled} onCheckedChange={AutoSettings.setEnabled} />
);
}只读游戏变量
对于只需要显示的变量(例如数值、标志或一段对话),直接在 TanStack Query 的 queryFn 中从游戏存储读取它们。
import { useQuery } from "@tanstack/react-query";
import { storage } from "@drincs/pixi-vn";
export function useQueryAffection() {
return useQuery({
queryKey: ["affection_use_query_key"],
queryFn: async () => storage.get<number>("affection") ?? 0,
});
}游戏存储变量只在叙事步骤(step)/ go back期间、运行叙事节点(label)时,或加载存档时发生变化——Pixi’VN 无法知道你有依赖该数据的查询,因此你的 UI 必须在这些事件发生后要求 TanStack Query 重新获取数据。与其逐个使查询键失效,不如在少数几个调用点一次性使所有查询失效更简单(在 narration.continue()/goNext、stepHistory.back()、narration.call()/jump 之后,以及恢复存档之后):
const queryClient = useQueryClient();
narration.continue({}).then(() => {
queryClient.invalidateQueries();
});可读写的游戏变量
对于 UI 本身可以更改的变量(例如选中的选项、与任务标志绑定的开关),使用与设置相同的 TanStack Store 封装方式,但让它基于游戏存储而不是 localStorage,这样该值就能在存档和 go back 之后依然保留:
import { storage } from "@drincs/pixi-vn";
import { Store } from "@tanstack/store";
const SELECTED_QUEST_KEY = "selectedQuestId";
export namespace Memo {
export const store = new Store<{ selectedQuestId: string | undefined }>({
selectedQuestId: storage.get<string>(SELECTED_QUEST_KEY),
});
export function setSelectedQuestId(id: string | undefined) {
storage.set(SELECTED_QUEST_KEY, id);
store.setState((state) => ({ ...state, selectedQuestId: id }));
}
}因为该 setter 直接更新 Store,所以对于通过这个同一个 setter 所做的更改,UI 会自动保持同步——无需手动刷新。
使 UI 与其他地方发生的存储更改保持同步
上面的 Store 模式仅在 UI 自身调用 storage.set 时才能保持 UI 同步。如果叙事节点(label)、叙事步骤(step)或游戏的其他任何部分更改了同一个游戏存储变量,则不会有任何机制通知该 Store(或读取该键的 useQuery)进行刷新。
要在一个地方捕获每一次游戏存储写入,请使用 storage.setStorageHandler。它允许你注册回调函数,当变量被设置、被移除,或临时变量过期时触发——无论在游戏中的任何位置,而不仅仅是来自 UI:
import { storage } from "@drincs/pixi-vn";
storage.setStorageHandler({
onSetVariable: (key, value) => {
queryClient.invalidateQueries();
},
onRemoveVariable: (key) => {
queryClient.invalidateQueries();
},
onClearOldTempVariable: (key) => {
queryClient.invalidateQueries();
},
});同一时间只能有一个处理程序
setStorageHandler 不会叠加多个处理程序——它内部只保存一个,每次调用都会替换前一个。如果你在多个文件中调用它,只有最后一个注册的处理程序会真正运行;之前的那些会悄悄停止触发。请在靠近应用启动的单一位置一次性设置它(例如你的根 provider),使用一个处理程序完成 UI 所需的所有操作(使查询失效、更新 stores 等)。因为它会在整个游戏中的每一次存储写入时重新运行,所以更推荐使用这种范围广泛、集中式的处理程序,而不是零散地设置多个针对性的处理程序——这样更容易理解,也不存在一个覆盖另一个的风险。
请在靠近应用启动的单一位置一次性设置它(例如你的根 provider),使用一个处理程序完成 UI 所需的所有操作(使查询失效、更新 stores 等)。因为它会在整个游戏中的每一次存储写入时重新运行,所以更推荐使用这种范围广泛、集中式的处理程序,而不是零散地设置多个针对性的处理程序——这样更容易理解,也不存在一个覆盖另一个的风险。