Pixi’VN

UI 变量

如何使用 TanStack Query/Store 将 UI 组件连接到设置、只读游戏数据以及可读写的游戏存储变量,以及如何通过 storage.setStorageHandler 使 UI 保持同步。

在 Pixi’VN UI 中,你显示或编辑的变量通常分为三类,每一类都有各自推荐的模式。 Pixi’VN 的模板已经在 src/lib/stores/ 和 src/lib/query/ 中大量使用了这些模式——值得打开看看,作为真实可用的示例。

设置变量

文本速度、字体大小或自动前进延迟等设置并非游戏存储的一部分:它们必须在每次游玩过程中持久保存(甚至在存档存在之前也是如此),因此它们保存在 localStorage 中,并镜像到一个 TanStack Store 中,以便组件能够对变化做出响应。

src/lib/stores/auto-settings-store.ts
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 等)。因为它会在整个游戏中的每一次存储写入时重新运行,所以更推荐使用这种范围广泛、集中式的处理程序,而不是零散地设置多个针对性的处理程序——这样更容易理解,也不存在一个覆盖另一个的风险。

本页内容