跳转到内容

事件系统

末语的事件系统连接了 Rust 引擎层和 JavaScript 层。事件从引擎底层产生,经过 Kit SDK 的封装后,分发到你的组件中。

使用 addEventListener 监听全局事件:

import { addEventListener } from '@momoyu-ink/kit';
// 注册监听器,返回清理函数
const cleanup = addEventListener('keydown', (e) => {
console.log(e.key, e.code);
});
// 通常在 React 的 useEffect 中使用
useEffect(() => {
return addEventListener('keydown', handler);
}, []);

在元素上直接绑定事件处理函数:

<sprite
src="ui/button.png"
onClick={(e) => console.log('clicked at', e.x, e.y)}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
/>

在元素上通过 props 绑定,事件会沿节点树向上冒泡。

事件触发时机
onClick鼠标点击
onMouseDown鼠标按下
onMouseUp鼠标抬起
onMouseMove鼠标移动
onMouseEnter鼠标进入(不冒泡)
onMouseLeave鼠标离开(不冒泡)
属性类型说明
xnumber舞台坐标 X
ynumber舞台坐标 Y
buttonnumber鼠标按键(0=左, 1=中, 2=右)
ctrlKeybooleanCtrl 键是否按下
shiftKeybooleanShift 键是否按下
altKeybooleanAlt 键是否按下
metaKeybooleanMeta 键是否按下
addEventListener('click', (e: MouseEvent) => { ... });
addEventListener('mousedown', (e: MouseEvent) => { ... });
addEventListener('mouseup', (e: MouseEvent) => { ... });
addEventListener('mousemove', (e: MouseEvent) => { ... });
addEventListener('contextmenu', (e: MouseEvent) => { ... });
addEventListener('doubleclick', (e: MouseEvent) => { ... });
<container onKeyDown={(e) => { ... }} onKeyUp={(e) => { ... }} />
属性类型说明
keystring按键名称(如 "Enter", "a", "Escape"
codestring物理按键代码(如 "KeyA", "Space"
ctrlKeybooleanCtrl 键是否按下
shiftKeybooleanShift 键是否按下
altKeybooleanAlt 键是否按下
metaKeybooleanMeta 键是否按下
repeatboolean是否为重复按键
addEventListener('keydown', (e: KeyboardEvent) => { ... });
addEventListener('keyup', (e: KeyboardEvent) => { ... });
<sprite
src="ui/touch_area.png"
onTouchStart={(e) => { ... }}
onTouchMove={(e) => { ... }}
onTouchEnd={(e) => { ... }}
onTouchCancel={(e) => { ... }}
/>
属性类型说明
idnumber触摸点 ID
xnumber舞台坐标 X
ynumber舞台坐标 Y
forcenumber压力(如果支持)

冒泡事件(鼠标和触摸事件的大部分类型)会从目标节点向上传播到根节点。你可以在事件对象上调用方法来控制传播:

<container onClick={(e) => {
// 阻止事件继续冒泡
e.stopPropagation();
}}>
<sprite onClick={(e) => {
// 阻止默认行为
e.preventDefault();
}} />
</container>
属性/方法类型说明
targetIdnumber事件目标节点 ID
currentTargetIdnumber当前处理节点 ID
bubblesboolean是否冒泡
defaultPreventedboolean默认行为是否被阻止
stopPropagation()() => void停止冒泡
preventDefault()() => void阻止默认行为

以下事件只能通过 addEventListener 监听:

addEventListener('ready', () => {
// 引擎初始化完成,可以开始渲染
});
addEventListener('beforeunload', () => {
// 用户尝试关闭窗口
// 可以在此弹出确认对话框
uiActions.confirm('确定要退出吗?', () => {
executePluginCommand('system', { subCommand: 'quit' });
});
});
addEventListener('resize', (e) => {
console.log('New size:', e.width, e.height);
});
addEventListener('fullscreen', (e) => {
console.log('Fullscreen:', e.fullscreen);
});
addEventListener('focus', (e) => {
console.log('Focused:', e.focused);
});
addEventListener('wheel', (e) => {
console.log('Scroll delta:', e.deltaX, e.deltaY);
});

这些事件在剧本执行过程中被触发,通常由框架内部(Stage)处理:

事件说明使用场景
scenariocommandline剧本命令行Stage 命令分发
scenariotext剧本文本行Stage 文本处理
scenariowaiting进入等待状态调试信息
scenariowaitingcancelled等待取消(超时或跳过)Skip 回调触发

末语在 QuickJS 环境中提供了 requestAnimationFrame polyfill:

function animate() {
// 每帧执行
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

这在需要逐帧更新的场景(如粒子效果、自定义动画)中很有用。但大多数情况下,使用 react-spring 的声明式动画更为合适。