跳转到内容

状态管理

末语框架使用 Valtio 进行状态管理。游戏运行时的所有数据——背景、角色、文本、BGM、设置等——都存储在响应式状态对象中。

Valtio 的工作方式非常直观:

import { proxy, useSnapshot } from 'valtio';
// 创建可变的代理状态
const state = proxy({ count: 0 });
// 在任意位置直接修改
state.count++;
// 在 React 组件中读取(只读快照,自动触发重渲染)
function MyComponent() {
const snap = useSnapshot(state);
return <text text={`${snap.count}`} />;
}
  • proxy() — 创建一个可变的响应式对象
  • useSnapshot() — 在组件中获取只读快照,当数据变化时自动重渲染
  • 直接赋值 — 在组件外(如命令处理函数中)直接修改 proxy 对象即可

gameState 管理所有与游戏进程相关的数据。定义在 src/state/game.ts 中。

import { gameState } from '../state/game';
gameState.story // 故事信息
gameState.background // 背景状态
gameState.character // 角色状态
gameState.textbox // 文本框状态
gameState.bgm // 背景音乐状态
gameState.voice // 当前语音状态
gameState.sfx // 音效状态
gameState.sound // 命名通道音频状态
interface StoryState {
title: string; // 当前章节标题(用于存档界面显示)
}
interface BackgroundState {
src: string; // 图片路径
fadeTime: number; // 渐变时长(毫秒)
tint?: string; // 着色(CSS 颜色值)
skippable: boolean; // 渐变是否可跳过
}
interface CharacterPreset {
x?: number; // X 坐标
y?: number; // Y 坐标
scale?: number; // 缩放
tint?: string; // 着色
visible?: boolean; // 是否可见
pivot?: [number, number]; // 锚点
fadeTime?: number; // 动画时长
}
interface CharacterState {
presets: Record<string, CharacterPreset>; // 角色预设
characters: Character[]; // 当前舞台上的角色列表
currentSpeaker?: string; // 当前说话者的名字
autoTint: string; // 非说话角色的自动色调(默认 '#666')
}
interface Character {
name?: string; // 角色名(标识符)
src: string; // 立绘路径
x: number; y: number; // 位置
scale: number; // 缩放
tint: string; // 着色
visible: boolean; // 是否可见
pivot: [number, number]; // 旋转中心
fadeTime: number; // 动画时长
}
interface TextBoxState {
name: string; // 说话者名字
text: string; // 文本内容
visible: boolean; // 文本框是否可见
shouldClear?: boolean; // 下次显示文本前是否清空
shouldAddNewline?: boolean; // 是否添加换行
// 文本渲染配置(通过 @textBox 命令设置)
printMode: 'instant' | 'typewriter' | 'printer';
printSpeed: number;
fillColor: string;
lineHeight: number;
indent: number;
stroke: boolean;
strokeColor: string;
strokeWidth: number;
shadow: boolean;
shadowColor: string;
shadowOffsetX: number;
shadowOffsetY: number;
shadowBlur: number;
shadowWidth: number;
}
interface BGMState {
src: string; // 音乐路径
loop: boolean; // 是否循环
volume?: number; // 音量
fadeTime?: number; // 渐变时长
}
interface VoiceState {
src: string; // 语音文件路径;为空时表示当前无语音
channelName: string; // 音频实例名,默认是 'voice',也可拆为 'voice_{name}'
volume?: number; // 语音音量;未指定时回落到 settingsState.volume_voice
}

@voice 命令只负责更新 gameState.voice,实际播放和 auto ticket 结算由 VoiceActor 管理。

interface SfxState {
seq: number; // 递增触发器,每次 @sfx 命令 +1
src: string; // 音效文件路径
loop: boolean; // 是否循环
volume?: number; // 音量;未指定时回落到 settingsState.volume_se
fadeTime?: number; // 渐入时长
stopSeq: number; // 停止触发器,每次 @sfxStop 命令 +1
stopFadeTime?: number; // 停止时的淡出时长
}

SfxActor 通过 seq 变化触发播放,每次创建独立音频实例(sfx_1, sfx_2, …)。Skip 时不播放,Auto 时正常播放但不参与等待。

interface SoundState {
seq: number; // 递增触发器,每次 @sound 命令 +1
channel: string; // 通道名称
src: string; // 音频文件路径
loop: boolean; // 是否循环
volume?: number; // 音量;未指定时回落到 settingsState.volume_se
fadeTime?: number; // 渐入时长
stopSeq: number; // 停止触发器,每次 @soundStop 命令 +1
stopChannel: string; // 要停止的通道名称
stopFadeTime?: number; // 停止时的淡出时长
}

SoundActor 行为与 SfxActor 类似,但使用 channel 作为音频实例名。同一通道的新音频会替换旧的。Skip 时不播放,Auto 时正常播放但不参与等待。

import { resetGameState } from '../state/game';
// 重置为默认值(回到主菜单时使用)
resetGameState();

settingsState 管理用户偏好设置,自动持久化到引擎的永久变量中。定义在 src/state/settings.ts 中。

import { settingsState } from '../state/settings';
interface SettingsData {
display: string; // 显示模式:'720' | '1080' | 'fullscreen'
volume_bgm: number; // BGM 音量(0~1)
volume_se: number; // 音效音量(0~1)
volume_voice: number; // 语音音量(0~1)
text_speed: number; // 文字速度(倍率)
auto_interval: number; // 自动播放间隔(秒)
skip_voice: boolean; // 快进时是否跳过语音
}
  • 自动持久化 — 修改 settingsState 后,会自动防抖保存(300ms)到引擎的永久变量中
  • 自动应用显示设置 — 修改 display 值后,会自动调用系统 API 调整窗口大小或切换全屏
  • 自动同步 Auto 间隔 — 修改 auto_interval 值后,会自动同步到 Kit 的 setDefaultAutoTailMs(),用于自动播放模式的推进间隔
  • 启动时恢复 — 引擎启动时,自动从永久变量中加载上次保存的设置
// 直接修改即可,会自动保存
settingsState.volume_bgm = 0.5;
settingsState.display = 'fullscreen';

uiState 管理临时的 UI 交互状态。定义在 src/state/ui.ts 中。

import { uiState, uiActions } from '../state/ui';
// 显示通知
uiActions.notify('操作成功');
uiActions.notify('保存完成', { duration: 3000 });
// 清除所有通知
uiActions.clearNotifications();
// 弹出确认框
uiActions.confirm(
'确定要退出吗?',
() => { /* 确认回调 */ },
() => { /* 取消回调(可选) */ }
);
// 截取当前画面(用于存档缩略图)
await uiActions.takeSnapshot(320, 180);

如果你需要管理额外的游戏数据,可以创建新的状态模块:

src/state/custom.ts
import { proxy } from 'valtio';
export interface CustomState {
favorability: Record<string, number>; // 角色好感度
flags: Record<string, boolean>; // 剧情标记
inventory: string[]; // 物品栏
}
export const customState = proxy<CustomState>({
favorability: {},
flags: {},
inventory: [],
});

然后在组件或命令处理函数中使用:

import { customState } from '../state/custom';
import { useSnapshot } from 'valtio';
// 在命令处理函数中修改
customState.favorability['Alice'] = (customState.favorability['Alice'] ?? 0) + 10;
// 在组件中读取
function StatusPanel() {
const snap = useSnapshot(customState);
return (
<text text={`Alice 好感度: ${snap.favorability['Alice'] ?? 0}`} />
);
}