图层(Actor)系统
This content is not available in your language yet.
图层(Actor) 是舞台上的视觉表现单元。每个图层(Actor)是一个 React 组件,它监听游戏状态的变化,并将其渲染为 UI。
图层(Actor)遵循一个固定的模式:
命令处理函数修改 gameState → 图层(Actor)监听状态变化 → 渲染对应的视觉效果这是一个纯粹的数据驱动模式——命令处理函数只负责更新数据,图层(Actor)只负责根据数据渲染。两者之间通过 Valtio 状态自动连接。
内置图层(Actor)
Section titled “内置图层(Actor)”标准框架提供了以下内置图层(Actor):
BackgroundActor — 背景
Section titled “BackgroundActor — 背景”监听 gameState.background,使用 react-spring 的 useTransition 实现背景切换的淡入淡出动画。
export function BackgroundActor() { const backgroundState = useSnapshot(gameState.background);
const transitions = useTransition( backgroundState.src ? [backgroundState.src] : [], { keys: (src) => src, from: { opacity: 0 }, enter: { opacity: 1 }, leave: { opacity: 1 }, config: { duration: backgroundState.fadeTime }, }, );
return ( <container label="背景容器"> {transitions((style, src) => ( <animated.sprite src={src} opacity={style.opacity} tint={tintSpring.tint} /> ))} </container> );}关键特性:
- 使用
useTransition管理新旧背景的切换 - 支持背景着色(tint)的平滑过渡
- 注册了
useSkipCallback以支持快进时立即完成渐变
CharacterActor — 角色
Section titled “CharacterActor — 角色”监听 gameState.character,渲染所有可见的角色立绘。
export function CharacterActor() { const characterState = useSnapshot(gameState.character);
const transitions = useTransition( characterState.characters.filter((char) => char.visible), { keys: (char) => char.src, from: { opacity: 0 }, enter: { opacity: 1 }, leave: { opacity: 0 }, config: (char) => ({ duration: char.fadeTime }), }, );
return ( <container label="立绘容器"> {transitions((style, character) => ( <CharacterSprite character={character} isCurrentSpeaker={character.name === textboxState.name} opacity={style.opacity} /> ))} </container> );}关键特性:
- 角色的位移、缩放通过
useSpring平滑过渡 - 当前说话者的立绘保持正常颜色,其他角色自动变暗(通过
charAutoTint命令可配置色调颜色) - 支持位置预设(left/center/right),坐标系以画面底部中心为原点
TextBoxActor — 文本框
Section titled “TextBoxActor — 文本框”监听 gameState.textbox,渲染对话框、姓名框和文本内容。
关键特性:
- 使用
<text>元素的printMode实现打字机效果 - 注册
useInterruptCallback支持点击完成打字 - 注册
useBeforeHandleCommandCallback在新命令前清除文本 - 悬停时显示工具栏按钮(快存、快读、设置等)
- 打印完成后显示闪烁光标
BGMActor — 背景音乐
Section titled “BGMActor — 背景音乐”一个无视觉渲染的”无头”图层(Actor),监听 gameState.bgm 的变化并调用音频 API。
export function BGMActor() { const bgmState = useSnapshot(gameState.bgm);
useEffect(() => { if (bgmState.src) { executePluginCommand('audio', { subCommand: 'load', name: 'bgm', src: bgmState.src, settings: { loop: bgmState.loop, volume: bgmState.volume ?? 1, }, }); executePluginCommand('audio', { subCommand: 'play', name: 'bgm', fadeTime: bgmState.fadeTime ?? 600, }); } }, [bgmState.src]);
return null; // 无视觉输出}VoiceActor — 角色语音
Section titled “VoiceActor — 角色语音”无头图层(Actor),监听 gameState.voice 管理语音播放。使用 play({ waitForEnd: true }) 等待自然播放结束,并在 auto 模式下通过 ticket 参与推进时序。
关键特性:
- 语音命令(
@voice)只写状态,VoiceActor 负责实际播放 - 在 auto 模式下发放 ticket,确保语音播完再推进
- 支持 Pending Ticket:语音票据可以在 barrier 打开前注册(详见命令与剧本引擎)
- 语音切换或停止时自动取消旧 ticket
SfxActor — 音效
Section titled “SfxActor — 音效”无头图层(Actor),监听 gameState.sfx 管理音效播放。每次 @sfx 命令会递增 seq 触发器,Actor 响应播放。
export function SfxActor() { const sfxState = useSnapshot(gameState.sfx);
useEffect(() => { if (sfxState.seq === 0) return; if (skipState.active) return; // 快进时不播放
const { src, loop, volume, fadeTime } = gameState.sfx; if (!src) return;
executePluginCommand('audio', { subCommand: 'load', name: `sfx_${sfxState.seq}`, src, settings: { autoPlay: true, loopRegion: loop ? [0, -1] : undefined, volume: volume ?? settingsState.volume_se, fadeTime, }, }); }, [sfxState.seq]);
// Handle stop... return null;}行为规则:
- Skip:快进模式下不播放,避免大量音效重叠
- Auto:正常播放,但不注册 ticket,不参与自动推进等待
SoundActor — 命名通道音频
Section titled “SoundActor — 命名通道音频”无头图层(Actor),监听 gameState.sound 管理命名通道的音频播放(如环境音、雨声等)。与 SfxActor 类似使用 seq 触发器驱动。
行为规则:
- Skip:快进模式下不播放,避免音频重叠
- Auto:正常播放,但不注册 ticket,不参与自动推进等待
在 Stage 中组装
Section titled “在 Stage 中组装”图层(Actor)组件在 src/pages/stage.tsx 中组装:
export function Stage() { return ( <StageContextProvider stage={stage}> <BackgroundActor /> <CharacterActor /> <TextBoxActor onButtonClick={handleButtonClick} /> <BGMActor /> <VoiceActor /> <SfxActor /> <SoundActor /> </StageContextProvider> );}创建自定义图层(Actor)
Section titled “创建自定义图层(Actor)”假设你想添加一个”屏幕震动”效果:
1. 定义状态
Section titled “1. 定义状态”export interface ShakeState { active: boolean; intensity: number;}
// 添加到 gameStateexport const gameState = proxy<GameState>({ // ...existing... shake: { active: false, intensity: 0 },});2. 创建图层(Actor)组件
Section titled “2. 创建图层(Actor)组件”import { useEffect, useState } from 'react';import { useSnapshot } from 'valtio';import { gameState } from '../state/game';import { useSkipCallback } from '@momoyu-ink/kit';
export function ShakeActor({ children }: { children: React.ReactNode }) { const shakeState = useSnapshot(gameState.shake); const [offset, setOffset] = useState({ x: 0, y: 0 });
useEffect(() => { if (!shakeState.active) { setOffset({ x: 0, y: 0 }); return; }
const interval = setInterval(() => { const intensity = shakeState.intensity; setOffset({ x: (Math.random() - 0.5) * intensity * 2, y: (Math.random() - 0.5) * intensity * 2, }); }, 16);
return () => clearInterval(interval); }, [shakeState.active, shakeState.intensity]);
// Support skip: immediately stop shaking useSkipCallback(() => { gameState.shake.active = false; });
return ( <container x={offset.x} y={offset.y}> {children} </container> );}3. 组装到 Stage
Section titled “3. 组装到 Stage”export function Stage() { return ( <StageContextProvider stage={stage}> <ShakeActor> <BackgroundActor /> <CharacterActor /> </ShakeActor> <TextBoxActor onButtonClick={handleButtonClick} /> <BGMActor /> <VoiceActor /> <SfxActor /> <SoundActor /> </StageContextProvider> );}现在 ShakeActor 包裹了背景和角色——震动时,这两个层会一起移动,而文本框保持不动。