用户:
Fox_Awa查看:4 回复:6 评论:4 创建时间:2022-08-16T19:09:15
本文转自“凌”的个人手记。
TL;DR(“太长了,不读”)见下
handler的返回值应当改为void | Promise<void>。
entity.player.dialog等异步函数应强调其异步性,同时也应强调返回值可能为null。
(略)null和undefined不一样,或许应当为undefined | Box3DialogResponse。
今天闲着没事干读岛3的API,偶然看到有这么一个示例:
// entity.player.dialog的官方示例
/* 玩家进入游戏时,弹出一个欢迎对话框 */
world.onPlayerJoin(({entity}) => {
entity.player.dialog({
type: Box3DialogType.TEXT,
title: "吉吉喵",
content: `你好,${entity.player.name},很高兴认识你。`,
});
})
然后我就感觉特别不对劲。你这相当于阻塞游戏,怎么写的。于是我又调查了entity.player.dialog的类型。
// 在游戏中显示一个对话框。
// 目前支持3种对话框样式:文本框 Text / 选项框 Select / 输入框 Input
type Box3DialogCall =
((params:Box3TextDialogParams) => Promise<Box3DialogResponse | null> & Box3DialogCancelOption) |
((params:Box3SelectDialogParams) => Promise<DialogSelectResponse | null> & Box3DialogCancelOption) |
((params:Box3InputDialogParams) => Promise<Box3DialogResponse | null> & Box3DialogCancelOption)
好家伙,异步的啊。也就是你这返回Promise直接给扬了,实在逆天。我感觉越来越不对劲,光速去调查world.onPlayerJoin的类型。
结果我们得到了如下定义:
// world.onPlayerJoin 的类型
type Box3EventChannel‹EventType› = (handler:(event: EventType) => void) => Box3EventHandlerToken;
其中handler参数的类型为:
// 我从函数的参数列表抽出了类型。
type Handler<EventType> = (event: EventType) => void;
而若希望在语义上传入异步的handler,则应有额外的类型定义:
// 支持传入async function的Handler
type AsyncHandler<EventType> = (event: EventType) => Promise<void>;
或者我们可以使用联合类型来同时允许传入同步和异步函数:
// 使用联合类型来表示返回值,这样同时兼容同步和异步函数。
type Handler2<EventType> = (event: EventType) => Promise<void> | void;
但是handler既不是Handler | AsyncHandler,也不是Handler2,故Promise<void>的返回值在语义上讲是不被允许的(实际上,因为返回值是void,Promise<void>会被丢弃)。
而我们知道,async function的返回值必然是Promise<unknown>(unknown代指返回类型),故在此处传入async function会令人很感到很奇怪(Promise<void>被丢掉了,但又没被丢掉)。
举个生动的例子如下:
world.onPlayerJoin(({entity}) => {
// 这样是OK的。函数返回void。
})
world.onPlayerJoin(async ({entity}) => {
// 这样也OK但并不推荐,因为async函数返回Promise<void>,但Promise<void>被丢弃了。
})
// 示例
type fn = () => void
const a: fn = async () => 1
;(async () => {
const d = await a() // 认为这里的await是无效的。
console.log(d) // 实际上输出1,因为Promise<number>没有被实际丢弃。
})()
接下来我将展示几段错误代码。在2020-2021时,因此类错误导致的项目漏洞不在少数。
之所以有这类错误,实际上是因为文档并未对异步函数进行强调,导致部分初学者可能出现错误。
// 错误代码1
/* 玩家进入游戏时,弹出一个欢迎对话框 */
world.onPlayerJoin(({entity}) => {
const result = entity.player.dialog({
type: Box3DialogType.TEXT,
title: "凌",
content: `首先,${entity.player.name},这是一段错误代码。`,
});
// 下一步处理...
entity.player.dialog({
type: Box3DialogType.TEXT,
title: "凌",
content: `比如,这样就错了。`,
});
})
一旦在下一步处理后面写逻辑就大错特错了。此处需要await才可以正确使用。但也有抄示例2抄错的情况存在。
// 错误代码1
/* 玩家进入游戏时,弹出一个欢迎对话框 */
world.onPlayerJoin(({entity}) => {
const result = await entity.player.dialog({
type: Box3DialogType.TEXT,
title: "凌",
content: `首先,${entity.player.name},这是一段错误代码。`,
});
// 下一步处理...
await entity.player.dialog({
type: Box3DialogType.TEXT,
title: "凌",
content: `比如,这样就错了。`,
});
})
错误的地方在于,忘记加async而直接使用await,导致问题发生。
一部分喜欢探知的青少年创作者会去了解Promise,然后使用Promise.then完成任务。虽然也可以这样做,但是代码并不优雅。
// 正确但不推荐的代码
/* 玩家进入游戏时,弹出一个欢迎对话框 */
world.onPlayerJoin(({entity}) => {
entity.player.dialog({
type: Box3DialogType.TEXT,
title: "凌",
content: `首先,${entity.player.name},这是一段错误代码。`,
}).then(result => {
// 下一步处理...
entity.player.dialog({
type: Box3DialogType.TEXT,
title: "凌",
content: `比如,这样就错了。`,
});
});
})
还有一个大家不怎么注意到的错误是忘记判断Promise返回的result是否为null(因为result为null | Box3DialogResponse)。这个问题在Javascript中是很难被发现的,相反在Typescript中就比较简单。若不判断result为null,则代码就有报错的可能性。相反,如果一个异步函数绝对不返回null,则在Promise中使用联合类型就是不恰当的。
为什么这代码点了没有反应
world.onPlayerJoin(({ entity: { player } }) => {
// 当玩家按下按键时,触发交互
player.onPress(({ button }) => {
if (button === Box3ButtonType.ACTION1) {
if (!player.crouchButton) return;
const result = player.dialog({
type: Box3DialogType.SELECT,
title: "滤镜",
titleTextColor: new Box3RGBAColor(0, 0, 0, 1),
titleBackgroundColor: new Box3RGBAColor(0.968, 0.702, 0.392, 1),
content: "请选择滤镜",
options: ['无', '反色', '热成像'],
contentTextColor: new Box3RGBAColor(0, 0, 0, 1),
contentBackgroundColor: new Box3RGBAColor(1, 1, 1, 1),
})
// 如果玩家点击了屏幕其他区域,取消了对话框。
if (!result || result === null) {
player.directMessage('你取消了对话框。');
return;
}
// 判断玩家选了什么选项。
switch (result.index) {
case 0:
//如果选择了第三项,即:'无'
player.directMessage('你取消了滤镜');
player.colorLUT = '';
break;
case 1:
// 如果选择了第二项,即:'反色'
player.directMessage('你切换了反色');
player.colorLUT = 'luts/Inverted.lut';
break;
case 2:
//如果选择了第三项,即:'无'
// 如果选择了第一项,即:'热成像'
player.directMessage('你切换了热成像');
player.colorLUT = 'lut/Psychedelic.lut';
break;
default:
// 注意,使用 switch 分支的时候,不要漏了后面的 break;
}
}
})
});点赞0
评论