用户:
Nomen查看:3 回复:12 评论:3 创建时间:2023-12-24T03:54:04
这里是没有开场白的Nomen,最近阿吉又发新API了,看着很有意思,但是实际文档 和效果相当迷人,所以我决定将官方API包装起来并且自定义事件喵,同时还支持分组监听管理与双向信息交换
如果还没有搞懂新API,可以前往这个帖子进行简单的入门
shequ.codemao.cn/community/674361
这里是一份例子,使用了七莓大佬的UiWrapper.client.js
/**
在RemoteWrapper中,通信分为两种协议:data和communicate
data用于直接传输消息,而communicate则用于交换消息
communicate会话中,接收方必须回应,否则可能导致无限等待,在服务端RemoteServerWrapper提供了timer函数用于控制超时
如果想要直接向客户端发送消息,可以直接操作rmsInstance.client.send发送原始数据
*/
// 玩家进行抽卡的例子
// 当玩家点击抽卡按钮,服务端返回结果
// 使用七莓大佬的UiWrapper作为按钮进行演示
// CLIENT clientIndex.js
let rmc = RemoteClientWrapper();
pullButton.eventOnce("pointerdown", ()=>{
rmc.communicate({
action: "抽卡", // 发送的自定义的内容,可以是你想要的任何东西
},(result)=>{
CREATE_DIALOG(result.message); // 显示弹窗,展示服务端发送的结果,结果应该是 {message: "无锋剑x1"}
});
});
// SERVER index.js
world.onPlayerJoin(({ entity }) => {
let rms = new RemoteServerWrapper(entity); // 初始化管理器
rms.onMessage("communicate", async (request, entity) => {
if(request.action === "抽卡"){
entity.yuanshi -= 160;
let result = RANDOM_TRASH(); // 随机生成抽卡结果
await entity.UPDATE_DATA();
return {message: result}; // 直接作为函数返回即可,函数可以是异步函数
}
})
});
// 另一个例子,展示简易聊天框
// CLIENT clientIndex.js
let rmc = RemoteClientWrapper();
sendButton.eventOnce("pointerdown", ()=>{
rmc.send(chatBox.findChildByName("textbox").text); // 假想的文字输入框
rmc.onMessage("data", (message)=>{ // 收到来自服务器的新消息
chatBox.createChildNode(UiText)
.config("textContent", message); // 创建文本并且显示给玩家
})
});
// SERVER index.js
let rms = new RemoteServerWrapper(); // 全局玩家聊天管理器
world.onPlayerJoin(({ entity }) => {
rms.add(entity); // 将玩家加入管理器
RemoteServerWrapper.listen(entity, "data", (arg)=>{ // 单独监听玩家,实际作用和onMessage差不多
rms.send(arg, "data"); // 向所有收管理的玩家广播该玩家发送的消息
});
});
world.onPlayerLeave(({entity})=>{
rms.cancel(entity); // 当玩家退出的时候取消管理这名玩家以释放资源
})
以下为Remote.server.js,需要有效地放置在服务端
class RemoteGroups {
constructor(target) {
this.target = Array.isArray(target) ? target : [target];
this.listeners = [];
}
send(message) {
remoteChannel.sendClientEvent(this.target, message);
}
sendOne(entity, message) {
if (this.target.includes(entity)) {
remoteChannel.sendClientEvent([entity], message)
}
}
each(c) {
this.target.forEach(v => c(v, this));
}
add(entity) {
this.target.push(entity);
}
cancel(entity) {
this.target = this.target.filter(v => v !== entity);
}
stop() {
this.listeners.forEach(v => v.cancel());
this.target = [];
this.listeners = [];
}
_register(f, hdls) {
this.listeners.push(remoteChannel.onServerEvent((a) => {
if (this.target.includes(a.entity) && typeof a.args?.id !== "number") hdls[a.args?.id]?.(a);
if (this.target.includes(a.entity)) f(a);
}));
}
}
/**
* @callback WhenGetMessage
* @param {JSONValue|} arg 客户端发送的数据
*/
/**
* @typedef {Object} RemoteServerEventToken
* @property {Function} cancel 取消事件
*/
/**
* @typedef {Object} RemoteServerEvent
* @property {GameEntity} type 玩家实例
* @property {WhenGetMessage} handler 回调函数,当事件触发时执行
* @property {number} id 会话id
*/
class RemoteServerWrapper {
static quitCode = {
"1": "传入的实体/实体组不正确",
"2": "非服务端"
}
/**@type {Map<GameEntity, RemoteServerEvent>} */
static events = new Map();
/**
* 监听特定玩家触发的事件
* @param {GameEntity} entity 要监听的玩家
* @param {"data"|"communicate"} type 会话类型
* @param {WhenGetMessage} f 回调函数,当事件触发时执行
* @param {number?} 会话id,如果不指定则为通用触发器,指定后仅该id的会话会被捕获
* @returns {RemoteServerEventToken} 调用返回值的cancel属性取消事件
*/
static listen(entity, type = "data", f, id) {
if (!this.events.get(entity)) this.events.set(entity, []);
this.events.get(entity).push({ type, handler: f, id });
return {
cancel: () => {
this.off(entity, f);
}
}
}
/**
* 仅监听一次特定玩家触发的事件
* @param {GameEntity} entity 要监听的玩家
* @param {"data"|"communicate"} type 会话类型
* @param {WhenGetMessage} f 回调函数,当事件触发时执行
* @param {number?} id 会话id,如果不指定则为通用触发器,指定后仅该id的会话会被捕获
* @returns {RemoteServerEventToken} 调用返回值的cancel属性取消事件
*/
static once(entity, type = "data", f, id) {
if (!this.events.get(entity)) this.events.set(entity, []);
let handler = function (...args) {
this.off(entity, f);
return f(...args);
};
this.events.get(entity).push({
type, handler, id
});
return {
cancel: () => {
this.off(entity, handler);
}
}
}
/**
* 向特定客户端广播消息
* @param {GameEntity} entity 要监听的玩家
* @param {JSONValue} message 向客户端发送的可序列化数据
* @param {"data"|"communicate"} type 会话类型
* @param {number} id 会话id,可通过这个id追踪会话并且监听
* @returns {number} 会话id
*/
static send(entity, message, type = "data", id = Date.now()) {
remoteChannel.sendClientEvent([entity], this.wrap(type, message, id));
return id;
}
static emit(type, ctype, self, id, ...args) {
if (!this.events.get(type)) this.events.set(type, []);
this.events.get(type).forEach(v => {
if (typeof v.handler === "function" && (!id || !v.id || (id && id === v.id)) && v.type === ctype) {
if (ctype === "data") v.handler.apply(this, args)
else if (ctype === "communicate") {
let res = v.handler.apply(this, args);
if (res.then) res.then(r => self.sendOne(type, r, "data", id))
else self.sendOne(type, res, "data", id)
}
}
});
}
static off(entity, f) {
if (this.events.get(entity)) this.events.set(entity, this.events.get(entity).filter(v => v.handler !== f));
}
static async timer(f, ms) {
let done = false, rejected = false, timeout;
try {
let result = await Promise.race([(async () => { let res = await f(); clearTimeout(timeout); done = true; return res })(),
new Promise((_r, reject) => {
timeout = setTimeout(() => {
rejected = done = true;
reject("Timeout");
}, ms);
})]);
if (result && done && !rejected) return result;
} catch (err) {
if (!done) throw err;
if (rejected) return new Error("Timeout");
}
}
constructor(target) {
if ((Array.isArray(target) && target.every(v => v instanceof GameEntity)) || target instanceof GameEntity) this.client = new RemoteGroups(target)
else throw this._quit("1");
try { remoteChannel } catch { throw this._quit("2") };
this._start();
}
/**
* 监听受管理的玩家发送的消息
* @param {"data"|"communicate"} type 会话类型
* @param {WhenGetMessage} f 回调函数,当事件触发时执行
*/
onMessage(type, f) {
this.client.target.forEach(entity => {
if (!this.constructor.events.get(entity)) this.constructor.events.set(entity, []);
this.constructor.events.get(entity).push({ type, handler: f });
});
}
/**
* 向所有当前实例管理的客户端广播消息
* @param {JSONValue} message 向客户端发送的可序列化数据
* @param {"data"|"communicate"} type 会话类型
* @param {number} id 会话id,可通过这个id追踪会话并且监听
* @returns {number} 会话id
*/
send(message, type = "data", id = Date.now()) {
this.client.send(this.wrap(type, message, id));
return id;
}
/**
* 向特定客户端发送消息
* @param {GameEntity} entity 指定发送的玩家
* @param {JSONValue} message 向客户端发送的可序列化数据
* @param {"data"|"communicate"} type 会话类型
* @param {number} id 会话id,可通过这个id追踪会话并且监听
* @returns {number} 会话id
*/
sendOne(entity, message, type = "data", id = Date.now()) {
this.client.sendOne(entity, this.wrap(type, message, id));
return id;
}
/**
* 添加管理特定实体
* @param {GameEntity} entity 添加管理的实体
*/
add(entity) {
this.client.add(entity);
}
/**
* 取消管理特定实体,并且清除共享事件管理器中的受管理单位事件
* @param {GameEntity} entity 取消管理的实体
*/
cancel(entity) {
this.client.cancel(entity);
this.constructor.events.delete(entity);
}
/**
* 终止当前实例,并且清除共享事件管理器中的受管理单位事件
* 这仅应该在你认为你需要放弃该实例并且优化资源时才能调用,停止后尝试调用实例方法可能导致错误
*/
stop() {
this.client.target.forEach(v => this.constructor.events.delete(entity));
this.client.stop();
}
/**
* 向所有当前实例管理的客户端交换数据
* 返回的数组包含着所有结果
* @param {JSONValue} data 向客户端发送的可序列化数据
* @param {Function} rec 当收到某个客户端返回的数据时触发回调
* @returns {Promise<JSONValue|Error>[]} 当所有客户端均响应,返回结果数组(即使出错)
*/
async communicateEntities(data, rec) {
let r = [];
this.client.each((t) => {
r.push(new Promise(resolve => {
if (t && !t.destroyed) {
this.constructor.once(t, "data", (args) => { rec(args.data); resolve(args.data); }, this.send(data, "communicate"));
}
}))
});
return Promise.all(r);
}
/**
* 向特定的实例交换数据
* @param {GameEntity} entity 玩家实例
* @param {JSONValue} data 向客户端发送的可序列化数据
* @param {Function} rec 当收到某个客户端返回的数据时触发回调
* @returns {Promise<JSONValue|Error>} 当客户端均响应,返回结果(即使出错)
*/
async communicate(entity, data, rec) {
if (!this.client.target.includes(entity)) return null;
return new Promise(resolve => {
if (entity && !entity.destroyed) {
this.constructor.once(entity, "data", (args) => { rec(args.data); resolve(args.data); }, this.sendOne(entity, data, "communicate"))
}
});
}
wrap(type, data, id) {
return {
type,
data,
id
}
}
_toData(data) {
if (typeof data === "object" && data !== null) return data
else return { data };
}
_start() {
this.client._register(({ entity, args }) => {
this.constructor.emit(entity, this._toData(args).type, this, this._toData(args).id, args, entity);
},{});
}
_quit(c) {
return new Error(`[Remote Server] ${this.constructor.quitCode[c]}`);
}
}
以下为Remote.client.js,需要有效地放置在客户端
function RemoteClientWrapper() {
let events = [], seed = 2024, random = () => (seed = (seed * 9301 + 49297) % 233280, parseInt(seed / 233280.0 * 1000000));
function wrap(type, data, id) {
return { type, data, id }
}
function send(data, id, type = "data") {
console.log("sent")
remoteChannel.sendServerEvent(wrap(type, data, id));
return id;
}
function onMessage(type = "data", f) {
events.push({ type, handler: f });
return { cancel() { events = events.filter(v => v.handler !== f); } }
}
function communicate(data, f) {
events.push({
type: "communicate", handler: (arg) => {
f(arg);
events = events.filter(v => v.handler !== f);
}, id: send(data, random(), "communicate")
});
}
remoteChannel.events.on("client", arg => {
if (arg.type === "communicate") {
events.filter(v => v.type === arg.type).forEach(f => {
let res = f.handler(arg.data);
if (res.then) res.then(r => send(r, arg.id))
else send(res, arg.id);
});
} else if (arg.type === "data") {
events.filter(v => v.type === arg.type).forEach(f => f.handler(arg.data));
}
});
return {
send,
onMessage,
communicate
}
}
这是Remote系列第一个版本,可能会存在大量漏洞与错误,但还是感谢你看完,我们下次再见
Nomen改一下实例代码
// 另一个例子,展示简易聊天框
// CLIENT clientIndex.js
let rmc = RemoteClientWrapper();
sendButton.eventOnce("pointerdown", ()=>{
rmc.send(chatBox.findChildByName("textbox").text); // 假想的文字输入框
});
rmc.onMessage("data", (message)=>{ // 收到来自服务器的新消息
chatBox.createChildNode(UiText)
.config("textContent", message); // 创建文本并且显示给玩家
})点赞0
评论
七式草莓666,用了 UiWrapper 居然不给下载链接(推荐使用你的 github 链接,方便更新)!
另外请注意,UiText 不具备输入功能,你可以把简易聊天框改成选择选项然后提交。
点赞0
评论
七式草莓这里标一下UiWrapper1.1.0的下载链接:UiWrapper-1.1.0.7z:https://static.codemao.cn/coco/player/unstable/rJqP3QVvT.7z
点赞0
评论
NomenUiWrapper是一个由七式喵莓开发的GUI管理工具,包装并且简化了GUI操作,并且拥有着完整的JSDoc与文档教程 现已发布至Box3-Tools仓库,并且长期更新维护 github.com/helloyork/Box3-Tools/tree/main/src/GUI/UiWrapper/1.1.0
点赞0
评论
神岛吉吉喵imgsrc="https://static.codemao.cn/emoji/codemao/%E7%BC%96%E7%A8%8B%E7%8C%AB_%E5%8E%89%E5%AE%B3%E4%BA%86.gif"alt="emotion_编程猫_厉害了"
点赞0
评论