feat: 集成Rust WASM渲染引擎与TypeScript ECS框架 (#228)
* feat: 集成Rust WASM渲染引擎与TypeScript ECS框架 * feat: 增强编辑器UI功能与跨平台支持 * fix: 修复CI测试和类型检查问题 * fix: 修复CI问题并提高测试覆盖率 * fix: 修复CI问题并提高测试覆盖率
This commit is contained in:
235
packages/platform-wechat/src/EngineBridge.ts
Normal file
235
packages/platform-wechat/src/EngineBridge.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* Rust 引擎桥接层
|
||||
* 负责在微信小游戏环境中初始化和管理 Rust WASM 引擎
|
||||
*/
|
||||
|
||||
import type { IPlatformCanvas } from '@esengine/platform-common';
|
||||
import { WeChatAdapter } from './WeChatAdapter';
|
||||
|
||||
/**
|
||||
* 引擎配置
|
||||
*/
|
||||
export interface EngineBridgeConfig {
|
||||
wasmPath: string;
|
||||
canvasWidth?: number;
|
||||
canvasHeight?: number;
|
||||
enableWebGL2?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 引擎桥接层
|
||||
* 将微信平台能力桥接到 Rust WASM 引擎
|
||||
*/
|
||||
export class EngineBridge {
|
||||
private _adapter: WeChatAdapter;
|
||||
private _canvas: IPlatformCanvas;
|
||||
private _gl: WebGLRenderingContext | WebGL2RenderingContext | null = null;
|
||||
private _wasmInstance: any = null;
|
||||
private _config: EngineBridgeConfig;
|
||||
|
||||
constructor(adapter: WeChatAdapter, config: EngineBridgeConfig) {
|
||||
this._adapter = adapter;
|
||||
this._config = config;
|
||||
|
||||
// 创建主 Canvas
|
||||
const windowInfo = adapter.getSystemInfo();
|
||||
const width = config.canvasWidth ?? windowInfo.windowWidth;
|
||||
const height = config.canvasHeight ?? windowInfo.windowHeight;
|
||||
|
||||
this._canvas = adapter.canvas.createCanvas(width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化引擎
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
// 获取 WebGL 上下文
|
||||
this._gl = this._getWebGLContext();
|
||||
if (!this._gl) {
|
||||
throw new Error('无法获取 WebGL 上下文');
|
||||
}
|
||||
|
||||
// 加载 WASM 模块
|
||||
const imports = this._createWASMImports();
|
||||
this._wasmInstance = await this._adapter.wasm.instantiate(
|
||||
this._config.wasmPath,
|
||||
imports
|
||||
);
|
||||
|
||||
// 初始化引擎
|
||||
if (this._wasmInstance.exports.init) {
|
||||
this._wasmInstance.exports.init();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 WebGL 上下文
|
||||
*/
|
||||
private _getWebGLContext(): WebGLRenderingContext | WebGL2RenderingContext | null {
|
||||
const contextType = this._config.enableWebGL2 ? 'webgl2' : 'webgl';
|
||||
const gl = this._canvas.getContext(contextType, {
|
||||
alpha: false,
|
||||
antialias: false,
|
||||
depth: false,
|
||||
stencil: false,
|
||||
premultipliedAlpha: true,
|
||||
preserveDrawingBuffer: false
|
||||
});
|
||||
|
||||
return gl as WebGLRenderingContext | WebGL2RenderingContext | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 WASM 导入对象
|
||||
*/
|
||||
private _createWASMImports(): Record<string, Record<string, any>> {
|
||||
return {
|
||||
env: {
|
||||
// 内存
|
||||
memory: this._adapter.wasm.createMemory(256, 16384),
|
||||
|
||||
// 平台桥接函数
|
||||
platform_log: (ptr: number, len: number) => {
|
||||
const message = this._readString(ptr, len);
|
||||
console.log('[Engine]', message);
|
||||
},
|
||||
|
||||
platform_error: (ptr: number, len: number) => {
|
||||
const message = this._readString(ptr, len);
|
||||
console.error('[Engine]', message);
|
||||
},
|
||||
|
||||
platform_now: () => {
|
||||
return performance.now();
|
||||
},
|
||||
|
||||
// WebGL 桥接
|
||||
gl_bindBuffer: (target: number, buffer: number) => {
|
||||
this._gl?.bindBuffer(target, this._getGLObject(buffer));
|
||||
},
|
||||
|
||||
gl_bufferData: (target: number, dataPtr: number, dataLen: number, usage: number) => {
|
||||
const data = this._readBuffer(dataPtr, dataLen);
|
||||
this._gl?.bufferData(target, data, usage);
|
||||
},
|
||||
|
||||
gl_clear: (mask: number) => {
|
||||
this._gl?.clear(mask);
|
||||
},
|
||||
|
||||
gl_clearColor: (r: number, g: number, b: number, a: number) => {
|
||||
this._gl?.clearColor(r, g, b, a);
|
||||
},
|
||||
|
||||
gl_drawArrays: (mode: number, first: number, count: number) => {
|
||||
this._gl?.drawArrays(mode, first, count);
|
||||
},
|
||||
|
||||
gl_drawElements: (mode: number, count: number, type: number, offset: number) => {
|
||||
this._gl?.drawElements(mode, count, type, offset);
|
||||
},
|
||||
|
||||
gl_enable: (cap: number) => {
|
||||
this._gl?.enable(cap);
|
||||
},
|
||||
|
||||
gl_disable: (cap: number) => {
|
||||
this._gl?.disable(cap);
|
||||
},
|
||||
|
||||
gl_viewport: (x: number, y: number, width: number, height: number) => {
|
||||
this._gl?.viewport(x, y, width, height);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 WASM 内存读取字符串
|
||||
*/
|
||||
private _readString(ptr: number, len: number): string {
|
||||
const memory = this._wasmInstance?.exports.memory as WebAssembly.Memory;
|
||||
if (!memory) return '';
|
||||
|
||||
const bytes = new Uint8Array(memory.buffer, ptr, len);
|
||||
return new TextDecoder().decode(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 WASM 内存读取缓冲区
|
||||
*/
|
||||
private _readBuffer(ptr: number, len: number): ArrayBuffer {
|
||||
const memory = this._wasmInstance?.exports.memory as WebAssembly.Memory;
|
||||
if (!memory) return new ArrayBuffer(0);
|
||||
|
||||
return memory.buffer.slice(ptr, ptr + len);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 WebGL 对象(暂时简化实现)
|
||||
*/
|
||||
private _getGLObject(_id: number): WebGLBuffer | null {
|
||||
// TODO: 实现 WebGL 对象管理
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Canvas
|
||||
*/
|
||||
get canvas(): IPlatformCanvas {
|
||||
return this._canvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 WebGL 上下文
|
||||
*/
|
||||
get gl(): WebGLRenderingContext | WebGL2RenderingContext | null {
|
||||
return this._gl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 WASM 实例
|
||||
*/
|
||||
get wasmInstance(): any {
|
||||
return this._wasmInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清屏
|
||||
*/
|
||||
clear(r: number, g: number, b: number, a: number): void {
|
||||
if (this._gl) {
|
||||
this._gl.clearColor(r, g, b, a);
|
||||
this._gl.clear(this._gl.COLOR_BUFFER_BIT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染一帧
|
||||
*/
|
||||
render(): void {
|
||||
if (this._wasmInstance?.exports.render) {
|
||||
this._wasmInstance.exports.render();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新逻辑
|
||||
*/
|
||||
update(deltaTime: number): void {
|
||||
if (this._wasmInstance?.exports.update) {
|
||||
this._wasmInstance.exports.update(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁引擎
|
||||
*/
|
||||
dispose(): void {
|
||||
if (this._wasmInstance?.exports.dispose) {
|
||||
this._wasmInstance.exports.dispose();
|
||||
}
|
||||
this._wasmInstance = null;
|
||||
this._gl = null;
|
||||
}
|
||||
}
|
||||
289
packages/platform-wechat/src/WeChatAdapter.ts
Normal file
289
packages/platform-wechat/src/WeChatAdapter.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
/**
|
||||
* 微信小游戏平台适配器
|
||||
*/
|
||||
|
||||
import type {
|
||||
IPlatformAdapter,
|
||||
PlatformWorker,
|
||||
WorkerCreationOptions,
|
||||
PlatformConfig
|
||||
} from '@esengine/ecs-framework';
|
||||
|
||||
import type { SystemInfo } from '@esengine/platform-common';
|
||||
|
||||
import { WeChatCanvasSubsystem } from './subsystems/WeChatCanvasSubsystem';
|
||||
import { WeChatAudioSubsystem } from './subsystems/WeChatAudioSubsystem';
|
||||
import { WeChatStorageSubsystem } from './subsystems/WeChatStorageSubsystem';
|
||||
import { WeChatNetworkSubsystem } from './subsystems/WeChatNetworkSubsystem';
|
||||
import { WeChatInputSubsystem } from './subsystems/WeChatInputSubsystem';
|
||||
import { WeChatFileSubsystem } from './subsystems/WeChatFileSubsystem';
|
||||
import { WeChatWASMSubsystem } from './subsystems/WeChatWASMSubsystem';
|
||||
import { getWx, isWeChatMiniGame } from './utils';
|
||||
|
||||
/**
|
||||
* 微信小游戏 Worker 包装
|
||||
*/
|
||||
class WeChatWorker implements PlatformWorker {
|
||||
private _worker: WechatMinigame.Worker;
|
||||
private _state: 'running' | 'terminated' = 'running';
|
||||
|
||||
constructor(worker: WechatMinigame.Worker) {
|
||||
this._worker = worker;
|
||||
}
|
||||
|
||||
get state(): 'running' | 'terminated' {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
postMessage(message: any, _transfer?: Transferable[]): void {
|
||||
this._worker.postMessage(message);
|
||||
}
|
||||
|
||||
onMessage(handler: (event: { data: any }) => void): void {
|
||||
this._worker.onMessage((res) => {
|
||||
handler({ data: res });
|
||||
});
|
||||
}
|
||||
|
||||
onError(handler: (error: ErrorEvent) => void): void {
|
||||
this._worker.onError((error) => {
|
||||
handler(error as unknown as ErrorEvent);
|
||||
});
|
||||
}
|
||||
|
||||
terminate(): void {
|
||||
this._worker.terminate();
|
||||
this._state = 'terminated';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小游戏平台适配器
|
||||
*/
|
||||
export class WeChatAdapter implements IPlatformAdapter {
|
||||
readonly name = 'wechat-minigame';
|
||||
readonly version: string;
|
||||
|
||||
// 子系统实例
|
||||
private _canvas: WeChatCanvasSubsystem | null = null;
|
||||
private _audio: WeChatAudioSubsystem | null = null;
|
||||
private _storage: WeChatStorageSubsystem | null = null;
|
||||
private _network: WeChatNetworkSubsystem | null = null;
|
||||
private _input: WeChatInputSubsystem | null = null;
|
||||
private _file: WeChatFileSubsystem | null = null;
|
||||
private _wasm: WeChatWASMSubsystem | null = null;
|
||||
|
||||
private _deviceInfo: WechatMinigame.DeviceInfo | null = null;
|
||||
private _windowInfo: WechatMinigame.WindowInfo | null = null;
|
||||
private _appBaseInfo: WechatMinigame.AppBaseInfo | null = null;
|
||||
|
||||
constructor() {
|
||||
if (!isWeChatMiniGame()) {
|
||||
throw new Error('当前环境不是微信小游戏环境');
|
||||
}
|
||||
|
||||
// 使用新的分离 API 获取系统信息
|
||||
const wxApi = getWx();
|
||||
this._deviceInfo = wxApi.getDeviceInfo();
|
||||
this._windowInfo = wxApi.getWindowInfo();
|
||||
this._appBaseInfo = wxApi.getAppBaseInfo();
|
||||
this.version = this._appBaseInfo.SDKVersion;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// IPlatformAdapter 基础实现
|
||||
// ========================================================================
|
||||
|
||||
isWorkerSupported(): boolean {
|
||||
// 微信小游戏支持 Worker,但有限制
|
||||
return typeof getWx().createWorker === 'function';
|
||||
}
|
||||
|
||||
isSharedArrayBufferSupported(): boolean {
|
||||
// 微信小游戏不支持 SharedArrayBuffer
|
||||
return false;
|
||||
}
|
||||
|
||||
getHardwareConcurrency(): number {
|
||||
// 微信小游戏无法获取真实核心数,返回保守值
|
||||
return 2;
|
||||
}
|
||||
|
||||
createWorker(script: string, options?: WorkerCreationOptions): PlatformWorker {
|
||||
// 微信小游戏 Worker 需要指定文件路径,不支持内联脚本
|
||||
// script 参数应该是 worker 文件的路径
|
||||
const worker = getWx().createWorker(script, {
|
||||
useExperimentalWorker: true
|
||||
});
|
||||
|
||||
return new WeChatWorker(worker);
|
||||
}
|
||||
|
||||
createSharedArrayBuffer(_length: number): SharedArrayBuffer | null {
|
||||
// 微信小游戏不支持 SharedArrayBuffer
|
||||
return null;
|
||||
}
|
||||
|
||||
getHighResTimestamp(): number {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
getPlatformConfig(): PlatformConfig {
|
||||
return {
|
||||
maxWorkerCount: 1,
|
||||
supportsModuleWorker: false,
|
||||
supportsTransferableObjects: true,
|
||||
maxSharedArrayBufferSize: 0,
|
||||
workerScriptPrefix: '',
|
||||
limitations: {
|
||||
noEval: true,
|
||||
requiresWorkerInit: true,
|
||||
memoryLimit: this._deviceInfo?.memorySize
|
||||
? parseInt(String(this._deviceInfo.memorySize)) * 1024 * 1024
|
||||
: 256 * 1024 * 1024,
|
||||
workerNotSupported: false,
|
||||
workerLimitations: [
|
||||
'Worker 必须使用独立文件,不支持内联脚本',
|
||||
'仅支持 1 个 Worker 实例',
|
||||
'不支持 SharedArrayBuffer',
|
||||
'Worker 文件需要在 game.json 中配置'
|
||||
]
|
||||
},
|
||||
extensions: {
|
||||
platform: 'wechat-minigame',
|
||||
sdkVersion: this._appBaseInfo?.SDKVersion
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async getPlatformConfigAsync(): Promise<PlatformConfig> {
|
||||
return this.getPlatformConfig();
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 子系统访问器
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* 获取 Canvas 子系统
|
||||
*/
|
||||
get canvas(): WeChatCanvasSubsystem {
|
||||
if (!this._canvas) {
|
||||
this._canvas = new WeChatCanvasSubsystem();
|
||||
}
|
||||
return this._canvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取音频子系统
|
||||
*/
|
||||
get audio(): WeChatAudioSubsystem {
|
||||
if (!this._audio) {
|
||||
this._audio = new WeChatAudioSubsystem();
|
||||
}
|
||||
return this._audio;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取存储子系统
|
||||
*/
|
||||
get storage(): WeChatStorageSubsystem {
|
||||
if (!this._storage) {
|
||||
this._storage = new WeChatStorageSubsystem();
|
||||
}
|
||||
return this._storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网络子系统
|
||||
*/
|
||||
get network(): WeChatNetworkSubsystem {
|
||||
if (!this._network) {
|
||||
this._network = new WeChatNetworkSubsystem();
|
||||
}
|
||||
return this._network;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取输入子系统
|
||||
*/
|
||||
get input(): WeChatInputSubsystem {
|
||||
if (!this._input) {
|
||||
this._input = new WeChatInputSubsystem();
|
||||
}
|
||||
return this._input;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件系统子系统
|
||||
*/
|
||||
get file(): WeChatFileSubsystem {
|
||||
if (!this._file) {
|
||||
this._file = new WeChatFileSubsystem();
|
||||
}
|
||||
return this._file;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 WASM 子系统
|
||||
*/
|
||||
get wasm(): WeChatWASMSubsystem {
|
||||
if (!this._wasm) {
|
||||
this._wasm = new WeChatWASMSubsystem();
|
||||
}
|
||||
return this._wasm;
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 系统信息
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* 获取系统信息
|
||||
*/
|
||||
getSystemInfo(): SystemInfo {
|
||||
const device = this._deviceInfo!;
|
||||
const window = this._windowInfo!;
|
||||
const app = this._appBaseInfo!;
|
||||
|
||||
return {
|
||||
brand: device.brand,
|
||||
model: device.model,
|
||||
pixelRatio: window.pixelRatio,
|
||||
screenWidth: window.screenWidth,
|
||||
screenHeight: window.screenHeight,
|
||||
windowWidth: window.windowWidth,
|
||||
windowHeight: window.windowHeight,
|
||||
statusBarHeight: window.statusBarHeight || 0,
|
||||
system: device.system,
|
||||
platform: device.platform as SystemInfo['platform'],
|
||||
SDKVersion: app.SDKVersion,
|
||||
benchmarkLevel: device.benchmarkLevel || 0,
|
||||
memorySize: device.memorySize ? parseInt(String(device.memorySize)) : undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较基础库版本
|
||||
*/
|
||||
compareVersion(v1: string, v2: string): number {
|
||||
const a1 = v1.split('.').map(Number);
|
||||
const a2 = v2.split('.').map(Number);
|
||||
const len = Math.max(a1.length, a2.length);
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
const n1 = a1[i] || 0;
|
||||
const n2 = a2[i] || 0;
|
||||
if (n1 > n2) return 1;
|
||||
if (n1 < n2) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持某个 API
|
||||
*/
|
||||
canIUse(schema: string): boolean {
|
||||
return getWx().canIUse(schema);
|
||||
}
|
||||
}
|
||||
23
packages/platform-wechat/src/index.ts
Normal file
23
packages/platform-wechat/src/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 微信小游戏平台适配器包
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
// 主适配器
|
||||
export { WeChatAdapter } from './WeChatAdapter';
|
||||
|
||||
// 引擎桥接
|
||||
export { EngineBridge } from './EngineBridge';
|
||||
export type { EngineBridgeConfig } from './EngineBridge';
|
||||
|
||||
// 子系统
|
||||
export { WeChatCanvasSubsystem } from './subsystems/WeChatCanvasSubsystem';
|
||||
export { WeChatAudioSubsystem } from './subsystems/WeChatAudioSubsystem';
|
||||
export { WeChatStorageSubsystem } from './subsystems/WeChatStorageSubsystem';
|
||||
export { WeChatNetworkSubsystem } from './subsystems/WeChatNetworkSubsystem';
|
||||
export { WeChatInputSubsystem } from './subsystems/WeChatInputSubsystem';
|
||||
export { WeChatFileSubsystem } from './subsystems/WeChatFileSubsystem';
|
||||
export { WeChatWASMSubsystem } from './subsystems/WeChatWASMSubsystem';
|
||||
|
||||
// 工具
|
||||
export { getWx, isWeChatMiniGame } from './utils';
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 微信小游戏音频子系统
|
||||
*/
|
||||
|
||||
import type {
|
||||
IPlatformAudioSubsystem,
|
||||
IPlatformAudioContext
|
||||
} from '@esengine/platform-common';
|
||||
import { getWx, promisify } from '../utils';
|
||||
|
||||
/**
|
||||
* 微信音频上下文包装
|
||||
*/
|
||||
class WeChatAudioContext implements IPlatformAudioContext {
|
||||
private _ctx: WechatMinigame.InnerAudioContext;
|
||||
|
||||
constructor(ctx: WechatMinigame.InnerAudioContext) {
|
||||
this._ctx = ctx;
|
||||
}
|
||||
|
||||
get src(): string { return this._ctx.src; }
|
||||
set src(value: string) { this._ctx.src = value; }
|
||||
|
||||
get autoplay(): boolean { return this._ctx.autoplay; }
|
||||
set autoplay(value: boolean) { this._ctx.autoplay = value; }
|
||||
|
||||
get loop(): boolean { return this._ctx.loop; }
|
||||
set loop(value: boolean) { this._ctx.loop = value; }
|
||||
|
||||
get volume(): number { return this._ctx.volume; }
|
||||
set volume(value: number) { this._ctx.volume = value; }
|
||||
|
||||
get duration(): number { return this._ctx.duration; }
|
||||
get currentTime(): number { return this._ctx.currentTime; }
|
||||
get paused(): boolean { return this._ctx.paused; }
|
||||
get buffered(): number { return this._ctx.buffered; }
|
||||
|
||||
play(): void { this._ctx.play(); }
|
||||
pause(): void { this._ctx.pause(); }
|
||||
stop(): void { this._ctx.stop(); }
|
||||
seek(position: number): void { this._ctx.seek(position); }
|
||||
destroy(): void { this._ctx.destroy(); }
|
||||
|
||||
onPlay(callback: () => void): void { this._ctx.onPlay(callback); }
|
||||
onPause(callback: () => void): void { this._ctx.onPause(callback); }
|
||||
onStop(callback: () => void): void { this._ctx.onStop(callback); }
|
||||
onEnded(callback: () => void): void { this._ctx.onEnded(callback); }
|
||||
onError(callback: (error: { errCode: number; errMsg: string }) => void): void {
|
||||
this._ctx.onError(callback as any);
|
||||
}
|
||||
onTimeUpdate(callback: () => void): void { this._ctx.onTimeUpdate(callback); }
|
||||
onCanplay(callback: () => void): void { this._ctx.onCanplay(callback); }
|
||||
onSeeking(callback: () => void): void { this._ctx.onSeeking(callback); }
|
||||
onSeeked(callback: () => void): void { this._ctx.onSeeked(callback); }
|
||||
|
||||
offPlay(callback: () => void): void { this._ctx.offPlay(callback); }
|
||||
offPause(callback: () => void): void { this._ctx.offPause(callback); }
|
||||
offStop(callback: () => void): void { this._ctx.offStop(callback); }
|
||||
offEnded(callback: () => void): void { this._ctx.offEnded(callback); }
|
||||
offError(callback: (error: { errCode: number; errMsg: string }) => void): void {
|
||||
this._ctx.offError(callback as any);
|
||||
}
|
||||
offTimeUpdate(callback: () => void): void { this._ctx.offTimeUpdate(callback); }
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小游戏音频子系统实现
|
||||
*/
|
||||
export class WeChatAudioSubsystem implements IPlatformAudioSubsystem {
|
||||
createAudioContext(_options?: { useWebAudioImplement?: boolean }): IPlatformAudioContext {
|
||||
const ctx = getWx().createInnerAudioContext({
|
||||
useWebAudioImplement: _options?.useWebAudioImplement
|
||||
});
|
||||
return new WeChatAudioContext(ctx);
|
||||
}
|
||||
|
||||
getSupportedFormats(): string[] {
|
||||
return ['mp3', 'wav', 'aac', 'm4a'];
|
||||
}
|
||||
|
||||
async setInnerAudioOption(options: {
|
||||
mixWithOther?: boolean;
|
||||
obeyMuteSwitch?: boolean;
|
||||
speakerOn?: boolean;
|
||||
}): Promise<void> {
|
||||
return promisify(getWx().setInnerAudioOption.bind(getWx()), options);
|
||||
}
|
||||
}
|
||||
209
packages/platform-wechat/src/subsystems/WeChatCanvasSubsystem.ts
Normal file
209
packages/platform-wechat/src/subsystems/WeChatCanvasSubsystem.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* 微信小游戏 Canvas 子系统
|
||||
*/
|
||||
|
||||
import type {
|
||||
IPlatformCanvasSubsystem,
|
||||
IPlatformCanvas,
|
||||
IPlatformImage,
|
||||
TempFilePathOptions,
|
||||
CanvasContextAttributes
|
||||
} from '@esengine/platform-common';
|
||||
import { getWx } from '../utils';
|
||||
|
||||
/**
|
||||
* 微信小游戏 Canvas 包装
|
||||
*/
|
||||
class WeChatCanvas implements IPlatformCanvas {
|
||||
private _canvas: WechatMinigame.Canvas;
|
||||
|
||||
constructor(canvas: WechatMinigame.Canvas) {
|
||||
this._canvas = canvas;
|
||||
}
|
||||
|
||||
get width(): number {
|
||||
return this._canvas.width;
|
||||
}
|
||||
|
||||
set width(value: number) {
|
||||
this._canvas.width = value;
|
||||
}
|
||||
|
||||
get height(): number {
|
||||
return this._canvas.height;
|
||||
}
|
||||
|
||||
set height(value: number) {
|
||||
this._canvas.height = value;
|
||||
}
|
||||
|
||||
getContext(
|
||||
contextType: '2d' | 'webgl' | 'webgl2',
|
||||
contextAttributes?: CanvasContextAttributes
|
||||
): RenderingContext | null {
|
||||
const wxAttributes: WechatMinigame.ContextAttributes | undefined = contextAttributes ? {
|
||||
alpha: typeof contextAttributes.alpha === 'boolean'
|
||||
? (contextAttributes.alpha ? 1 : 0)
|
||||
: contextAttributes.alpha,
|
||||
antialias: contextAttributes.antialias,
|
||||
preserveDrawingBuffer: contextAttributes.preserveDrawingBuffer,
|
||||
antialiasSamples: contextAttributes.antialiasSamples
|
||||
} : undefined;
|
||||
return this._canvas.getContext(contextType, wxAttributes);
|
||||
}
|
||||
|
||||
toDataURL(): string {
|
||||
return this._canvas.toDataURL();
|
||||
}
|
||||
|
||||
toTempFilePath(options: TempFilePathOptions): void {
|
||||
this._canvas.toTempFilePath({
|
||||
x: options.x,
|
||||
y: options.y,
|
||||
width: options.width,
|
||||
height: options.height,
|
||||
destWidth: options.destWidth,
|
||||
destHeight: options.destHeight,
|
||||
fileType: options.fileType,
|
||||
quality: options.quality,
|
||||
success: options.success,
|
||||
fail: options.fail,
|
||||
complete: options.complete
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取原始微信 Canvas 对象
|
||||
*/
|
||||
getNativeCanvas(): WechatMinigame.Canvas {
|
||||
return this._canvas;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小游戏 Image 包装
|
||||
*/
|
||||
class WeChatImage implements IPlatformImage {
|
||||
private _image: WechatMinigame.Image;
|
||||
|
||||
constructor(image: WechatMinigame.Image) {
|
||||
this._image = image;
|
||||
}
|
||||
|
||||
get src(): string {
|
||||
return this._image.src;
|
||||
}
|
||||
|
||||
set src(value: string) {
|
||||
this._image.src = value;
|
||||
}
|
||||
|
||||
get width(): number {
|
||||
return this._image.width;
|
||||
}
|
||||
|
||||
get height(): number {
|
||||
return this._image.height;
|
||||
}
|
||||
|
||||
get onload(): (() => void) | null {
|
||||
return this._image.onload as (() => void) | null;
|
||||
}
|
||||
|
||||
set onload(value: (() => void) | null) {
|
||||
this._image.onload = value as any;
|
||||
}
|
||||
|
||||
get onerror(): ((error: any) => void) | null {
|
||||
return this._image.onerror as ((error: any) => void) | null;
|
||||
}
|
||||
|
||||
set onerror(value: ((error: any) => void) | null) {
|
||||
this._image.onerror = value as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取原始微信 Image 对象
|
||||
*/
|
||||
getNativeImage(): WechatMinigame.Image {
|
||||
return this._image;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小游戏 Canvas 子系统实现
|
||||
*/
|
||||
export class WeChatCanvasSubsystem implements IPlatformCanvasSubsystem {
|
||||
private _mainCanvas: WeChatCanvas | null = null;
|
||||
private _windowInfo: WechatMinigame.WindowInfo;
|
||||
|
||||
constructor() {
|
||||
this._windowInfo = getWx().getWindowInfo();
|
||||
}
|
||||
|
||||
createCanvas(width?: number, height?: number): IPlatformCanvas {
|
||||
const canvas = getWx().createCanvas();
|
||||
|
||||
// 设置尺寸
|
||||
if (width !== undefined) {
|
||||
canvas.width = width;
|
||||
}
|
||||
if (height !== undefined) {
|
||||
canvas.height = height;
|
||||
}
|
||||
|
||||
const wrappedCanvas = new WeChatCanvas(canvas);
|
||||
|
||||
// 首次创建的是主 Canvas
|
||||
if (!this._mainCanvas) {
|
||||
this._mainCanvas = wrappedCanvas;
|
||||
}
|
||||
|
||||
return wrappedCanvas;
|
||||
}
|
||||
|
||||
createImage(): IPlatformImage {
|
||||
const image = getWx().createImage();
|
||||
return new WeChatImage(image);
|
||||
}
|
||||
|
||||
createImageData(width: number, height: number): ImageData {
|
||||
// 微信小游戏 3.4.10+ 支持 createImageData
|
||||
if (typeof getWx().createImageData === 'function') {
|
||||
return getWx().createImageData(width, height) as unknown as ImageData;
|
||||
}
|
||||
|
||||
// 降级方案:创建标准 ImageData
|
||||
const data = new Uint8ClampedArray(width * height * 4);
|
||||
return {
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
colorSpace: 'srgb'
|
||||
} as ImageData;
|
||||
}
|
||||
|
||||
getScreenWidth(): number {
|
||||
return this._windowInfo.screenWidth;
|
||||
}
|
||||
|
||||
getScreenHeight(): number {
|
||||
return this._windowInfo.screenHeight;
|
||||
}
|
||||
|
||||
getDevicePixelRatio(): number {
|
||||
return this._windowInfo.pixelRatio;
|
||||
}
|
||||
|
||||
getMainCanvas(): IPlatformCanvas | null {
|
||||
return this._mainCanvas;
|
||||
}
|
||||
|
||||
getWindowWidth(): number {
|
||||
return this._windowInfo.windowWidth;
|
||||
}
|
||||
|
||||
getWindowHeight(): number {
|
||||
return this._windowInfo.windowHeight;
|
||||
}
|
||||
}
|
||||
204
packages/platform-wechat/src/subsystems/WeChatFileSubsystem.ts
Normal file
204
packages/platform-wechat/src/subsystems/WeChatFileSubsystem.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* 微信小游戏文件系统子系统
|
||||
*/
|
||||
|
||||
import type {
|
||||
IPlatformFileSubsystem,
|
||||
FileInfo
|
||||
} from '@esengine/platform-common';
|
||||
import { getWx } from '../utils';
|
||||
|
||||
/**
|
||||
* 微信小游戏文件系统子系统实现
|
||||
*/
|
||||
export class WeChatFileSubsystem implements IPlatformFileSubsystem {
|
||||
private _fs: WechatMinigame.FileSystemManager;
|
||||
|
||||
constructor() {
|
||||
this._fs = getWx().getFileSystemManager();
|
||||
}
|
||||
|
||||
async readFile(options: {
|
||||
filePath: string;
|
||||
encoding?: 'ascii' | 'base64' | 'binary' | 'hex' | 'ucs2' | 'utf-8' | 'utf8';
|
||||
position?: number;
|
||||
length?: number;
|
||||
}): Promise<string | ArrayBuffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._fs.readFile({
|
||||
filePath: options.filePath,
|
||||
encoding: options.encoding as any,
|
||||
position: options.position,
|
||||
length: options.length,
|
||||
success: (res) => resolve(res.data),
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
readFileSync(
|
||||
filePath: string,
|
||||
encoding?: 'ascii' | 'base64' | 'binary' | 'hex' | 'ucs2' | 'utf-8' | 'utf8',
|
||||
position?: number,
|
||||
length?: number
|
||||
): string | ArrayBuffer {
|
||||
return this._fs.readFileSync(filePath, encoding as any, position, length);
|
||||
}
|
||||
|
||||
async writeFile(options: {
|
||||
filePath: string;
|
||||
data: string | ArrayBuffer;
|
||||
encoding?: 'ascii' | 'base64' | 'binary' | 'hex' | 'ucs2' | 'utf-8' | 'utf8';
|
||||
}): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._fs.writeFile({
|
||||
filePath: options.filePath,
|
||||
data: options.data,
|
||||
encoding: options.encoding as any,
|
||||
success: () => resolve(),
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
writeFileSync(
|
||||
filePath: string,
|
||||
data: string | ArrayBuffer,
|
||||
encoding?: 'ascii' | 'base64' | 'binary' | 'hex' | 'ucs2' | 'utf-8' | 'utf8'
|
||||
): void {
|
||||
this._fs.writeFileSync(filePath, data, encoding as any);
|
||||
}
|
||||
|
||||
async appendFile(options: {
|
||||
filePath: string;
|
||||
data: string | ArrayBuffer;
|
||||
encoding?: 'ascii' | 'base64' | 'binary' | 'hex' | 'ucs2' | 'utf-8' | 'utf8';
|
||||
}): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._fs.appendFile({
|
||||
filePath: options.filePath,
|
||||
data: options.data,
|
||||
encoding: options.encoding as any,
|
||||
success: () => resolve(),
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async unlink(filePath: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._fs.unlink({
|
||||
filePath,
|
||||
success: () => resolve(),
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async mkdir(options: {
|
||||
dirPath: string;
|
||||
recursive?: boolean;
|
||||
}): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._fs.mkdir({
|
||||
dirPath: options.dirPath,
|
||||
recursive: options.recursive,
|
||||
success: () => resolve(),
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async rmdir(options: {
|
||||
dirPath: string;
|
||||
recursive?: boolean;
|
||||
}): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._fs.rmdir({
|
||||
dirPath: options.dirPath,
|
||||
recursive: options.recursive,
|
||||
success: () => resolve(),
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async readdir(dirPath: string): Promise<string[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._fs.readdir({
|
||||
dirPath,
|
||||
success: (res) => resolve(res.files),
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async stat(path: string): Promise<FileInfo> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._fs.stat({
|
||||
path,
|
||||
success: (res) => {
|
||||
const stats = res.stats as WechatMinigame.Stats;
|
||||
resolve({
|
||||
size: stats.size,
|
||||
createTime: stats.lastAccessedTime,
|
||||
modifyTime: stats.lastModifiedTime,
|
||||
isDirectory: stats.isDirectory(),
|
||||
isFile: stats.isFile()
|
||||
});
|
||||
},
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async access(path: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._fs.access({
|
||||
path,
|
||||
success: () => resolve(),
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async rename(oldPath: string, newPath: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._fs.rename({
|
||||
oldPath,
|
||||
newPath,
|
||||
success: () => resolve(),
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async copyFile(srcPath: string, destPath: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._fs.copyFile({
|
||||
srcPath,
|
||||
destPath,
|
||||
success: () => resolve(),
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getUserDataPath(): string {
|
||||
return `${getWx().env.USER_DATA_PATH}`;
|
||||
}
|
||||
|
||||
async unzip(options: {
|
||||
zipFilePath: string;
|
||||
targetPath: string;
|
||||
}): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._fs.unzip({
|
||||
zipFilePath: options.zipFilePath,
|
||||
targetPath: options.targetPath,
|
||||
success: () => resolve(),
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 微信小游戏输入子系统
|
||||
*/
|
||||
|
||||
import type {
|
||||
IPlatformInputSubsystem,
|
||||
TouchHandler,
|
||||
TouchEvent
|
||||
} from '@esengine/platform-common';
|
||||
import { getWx } from '../utils';
|
||||
|
||||
/**
|
||||
* 微信小游戏输入子系统实现
|
||||
*/
|
||||
export class WeChatInputSubsystem implements IPlatformInputSubsystem {
|
||||
onTouchStart(handler: TouchHandler): void {
|
||||
getWx().onTouchStart((res) => {
|
||||
handler(this.convertTouchEvent(res));
|
||||
});
|
||||
}
|
||||
|
||||
onTouchMove(handler: TouchHandler): void {
|
||||
getWx().onTouchMove((res) => {
|
||||
handler(this.convertTouchEvent(res));
|
||||
});
|
||||
}
|
||||
|
||||
onTouchEnd(handler: TouchHandler): void {
|
||||
getWx().onTouchEnd((res) => {
|
||||
handler(this.convertTouchEvent(res));
|
||||
});
|
||||
}
|
||||
|
||||
onTouchCancel(handler: TouchHandler): void {
|
||||
getWx().onTouchCancel((res) => {
|
||||
handler(this.convertTouchEvent(res));
|
||||
});
|
||||
}
|
||||
|
||||
offTouchStart(handler: TouchHandler): void {
|
||||
getWx().offTouchStart(handler as any);
|
||||
}
|
||||
|
||||
offTouchMove(handler: TouchHandler): void {
|
||||
getWx().offTouchMove(handler as any);
|
||||
}
|
||||
|
||||
offTouchEnd(handler: TouchHandler): void {
|
||||
getWx().offTouchEnd(handler as any);
|
||||
}
|
||||
|
||||
offTouchCancel(handler: TouchHandler): void {
|
||||
getWx().offTouchCancel(handler as any);
|
||||
}
|
||||
|
||||
supportsPressure(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
private convertTouchEvent(res: WechatMinigame.OnTouchStartListenerResult): TouchEvent {
|
||||
return {
|
||||
touches: res.touches.map((t: WechatMinigame.Touch) => ({
|
||||
identifier: t.identifier,
|
||||
x: t.clientX,
|
||||
y: t.clientY,
|
||||
force: t.force
|
||||
})),
|
||||
changedTouches: res.changedTouches.map((t: WechatMinigame.Touch) => ({
|
||||
identifier: t.identifier,
|
||||
x: t.clientX,
|
||||
y: t.clientY,
|
||||
force: t.force
|
||||
})),
|
||||
timeStamp: res.timeStamp
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* 微信小游戏网络子系统
|
||||
*/
|
||||
|
||||
import type {
|
||||
IPlatformNetworkSubsystem,
|
||||
RequestConfig,
|
||||
RequestResponse,
|
||||
IDownloadTask,
|
||||
IUploadTask,
|
||||
IPlatformWebSocket
|
||||
} from '@esengine/platform-common';
|
||||
import { getWx, promisify } from '../utils';
|
||||
|
||||
/**
|
||||
* 微信 WebSocket 包装
|
||||
*/
|
||||
class WeChatWebSocket implements IPlatformWebSocket {
|
||||
private _task: WechatMinigame.SocketTask;
|
||||
|
||||
constructor(task: WechatMinigame.SocketTask) {
|
||||
this._task = task;
|
||||
}
|
||||
|
||||
send(data: string | ArrayBuffer): void {
|
||||
this._task.send({ data });
|
||||
}
|
||||
|
||||
close(code?: number, reason?: string): void {
|
||||
this._task.close({ code, reason });
|
||||
}
|
||||
|
||||
onOpen(callback: (res: { header: Record<string, string> }) => void): void {
|
||||
this._task.onOpen(callback as any);
|
||||
}
|
||||
|
||||
onClose(callback: (res: { code: number; reason: string }) => void): void {
|
||||
this._task.onClose(callback as any);
|
||||
}
|
||||
|
||||
onError(callback: (error: any) => void): void {
|
||||
this._task.onError(callback);
|
||||
}
|
||||
|
||||
onMessage(callback: (res: { data: string | ArrayBuffer }) => void): void {
|
||||
this._task.onMessage(callback as any);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小游戏网络子系统实现
|
||||
*/
|
||||
export class WeChatNetworkSubsystem implements IPlatformNetworkSubsystem {
|
||||
async request<T = any>(config: RequestConfig): Promise<RequestResponse<T>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
getWx().request({
|
||||
url: config.url,
|
||||
method: config.method as any,
|
||||
data: config.data,
|
||||
header: config.header,
|
||||
timeout: config.timeout,
|
||||
dataType: config.dataType as any,
|
||||
responseType: config.responseType as any,
|
||||
success: (res) => {
|
||||
resolve({
|
||||
data: res.data as T,
|
||||
statusCode: res.statusCode,
|
||||
header: res.header as Record<string, string>
|
||||
});
|
||||
},
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
downloadFile(options: {
|
||||
url: string;
|
||||
filePath?: string;
|
||||
header?: Record<string, string>;
|
||||
timeout?: number;
|
||||
}): Promise<{ tempFilePath: string; filePath?: string; statusCode: number }> & IDownloadTask {
|
||||
const task = getWx().downloadFile({
|
||||
url: options.url,
|
||||
filePath: options.filePath,
|
||||
header: options.header,
|
||||
timeout: options.timeout,
|
||||
success: () => {},
|
||||
fail: () => {}
|
||||
});
|
||||
|
||||
const promise = new Promise<{ tempFilePath: string; filePath?: string; statusCode: number }>((resolve, reject) => {
|
||||
task.onProgressUpdate(() => {});
|
||||
getWx().downloadFile({
|
||||
...options,
|
||||
success: (res) => resolve({
|
||||
tempFilePath: res.tempFilePath,
|
||||
filePath: res.filePath,
|
||||
statusCode: res.statusCode
|
||||
}),
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
|
||||
return Object.assign(promise, {
|
||||
abort: () => task.abort(),
|
||||
onProgressUpdate: (callback: any) => task.onProgressUpdate(callback),
|
||||
offProgressUpdate: (callback: any) => task.offProgressUpdate(callback)
|
||||
});
|
||||
}
|
||||
|
||||
uploadFile(options: {
|
||||
url: string;
|
||||
filePath: string;
|
||||
name: string;
|
||||
header?: Record<string, string>;
|
||||
formData?: Record<string, any>;
|
||||
timeout?: number;
|
||||
}): Promise<{ data: string; statusCode: number }> & IUploadTask {
|
||||
const task = getWx().uploadFile({
|
||||
url: options.url,
|
||||
filePath: options.filePath,
|
||||
name: options.name,
|
||||
header: options.header,
|
||||
formData: options.formData,
|
||||
timeout: options.timeout,
|
||||
success: () => {},
|
||||
fail: () => {}
|
||||
});
|
||||
|
||||
const promise = new Promise<{ data: string; statusCode: number }>((resolve, reject) => {
|
||||
getWx().uploadFile({
|
||||
...options,
|
||||
success: (res) => resolve({
|
||||
data: res.data,
|
||||
statusCode: res.statusCode
|
||||
}),
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
|
||||
return Object.assign(promise, {
|
||||
abort: () => task.abort(),
|
||||
onProgressUpdate: (callback: any) => task.onProgressUpdate(callback),
|
||||
offProgressUpdate: (callback: any) => task.offProgressUpdate(callback)
|
||||
});
|
||||
}
|
||||
|
||||
connectSocket(options: {
|
||||
url: string;
|
||||
header?: Record<string, string>;
|
||||
protocols?: string[];
|
||||
timeout?: number;
|
||||
}): IPlatformWebSocket {
|
||||
const task = getWx().connectSocket({
|
||||
url: options.url,
|
||||
header: options.header,
|
||||
protocols: options.protocols,
|
||||
timeout: options.timeout
|
||||
});
|
||||
return new WeChatWebSocket(task);
|
||||
}
|
||||
|
||||
async getNetworkType(): Promise<'wifi' | '2g' | '3g' | '4g' | '5g' | 'unknown' | 'none'> {
|
||||
const res = await promisify<{ networkType: string }>(
|
||||
getWx().getNetworkType.bind(getWx()),
|
||||
{}
|
||||
);
|
||||
return res.networkType as any;
|
||||
}
|
||||
|
||||
onNetworkStatusChange(callback: (res: {
|
||||
isConnected: boolean;
|
||||
networkType: string;
|
||||
}) => void): void {
|
||||
getWx().onNetworkStatusChange(callback);
|
||||
}
|
||||
|
||||
offNetworkStatusChange(callback: Function): void {
|
||||
getWx().offNetworkStatusChange(callback as any);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* 微信小游戏存储子系统
|
||||
*/
|
||||
|
||||
import type {
|
||||
IPlatformStorageSubsystem,
|
||||
StorageInfo
|
||||
} from '@esengine/platform-common';
|
||||
import { getWx, promisify } from '../utils';
|
||||
|
||||
/**
|
||||
* 微信小游戏存储子系统实现
|
||||
*/
|
||||
export class WeChatStorageSubsystem implements IPlatformStorageSubsystem {
|
||||
getStorageSync<T = any>(key: string): T | undefined {
|
||||
try {
|
||||
return getWx().getStorageSync<T>(key);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
setStorageSync<T = any>(key: string, value: T): void {
|
||||
getWx().setStorageSync(key, value);
|
||||
}
|
||||
|
||||
removeStorageSync(key: string): void {
|
||||
getWx().removeStorageSync(key);
|
||||
}
|
||||
|
||||
clearStorageSync(): void {
|
||||
getWx().clearStorageSync();
|
||||
}
|
||||
|
||||
getStorageInfoSync(): StorageInfo {
|
||||
const info = getWx().getStorageInfoSync();
|
||||
return {
|
||||
keys: info.keys,
|
||||
currentSize: info.currentSize,
|
||||
limitSize: info.limitSize
|
||||
};
|
||||
}
|
||||
|
||||
async getStorage<T = any>(key: string): Promise<T | undefined> {
|
||||
try {
|
||||
const res = await promisify<{ data: T }>(
|
||||
getWx().getStorage.bind(getWx()),
|
||||
{ key }
|
||||
);
|
||||
return res.data;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async setStorage<T = any>(key: string, value: T): Promise<void> {
|
||||
await promisify(getWx().setStorage.bind(getWx()), { key, data: value });
|
||||
}
|
||||
|
||||
async removeStorage(key: string): Promise<void> {
|
||||
await promisify(getWx().removeStorage.bind(getWx()), { key });
|
||||
}
|
||||
|
||||
async clearStorage(): Promise<void> {
|
||||
await promisify(getWx().clearStorage.bind(getWx()), {});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 微信小游戏 WASM 子系统
|
||||
*/
|
||||
|
||||
import type {
|
||||
IPlatformWASMSubsystem,
|
||||
IWASMInstance,
|
||||
WASMImports,
|
||||
WASMExports
|
||||
} from '@esengine/platform-common';
|
||||
|
||||
/**
|
||||
* 微信小游戏 WASM 子系统实现
|
||||
*/
|
||||
export class WeChatWASMSubsystem implements IPlatformWASMSubsystem {
|
||||
async instantiate(path: string, imports?: WASMImports): Promise<IWASMInstance> {
|
||||
// 微信小游戏使用 WXWebAssembly.instantiate
|
||||
// path 应该是相对于小游戏根目录的 .wasm 文件路径
|
||||
if (typeof WXWebAssembly === 'undefined') {
|
||||
throw new Error('当前微信基础库版本不支持 WebAssembly');
|
||||
}
|
||||
|
||||
const wxImports: WXWebAssembly.Imports | undefined = imports as WXWebAssembly.Imports | undefined;
|
||||
const instance = await WXWebAssembly.instantiate(path, wxImports);
|
||||
|
||||
return {
|
||||
exports: instance.exports as WASMExports
|
||||
};
|
||||
}
|
||||
|
||||
isSupported(): boolean {
|
||||
return typeof WXWebAssembly !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 WASM 内存
|
||||
* 用于 Rust/WASM 引擎的内存交互
|
||||
*/
|
||||
createMemory(initial: number, maximum?: number): WebAssembly.Memory {
|
||||
if (typeof WXWebAssembly === 'undefined') {
|
||||
throw new Error('当前微信基础库版本不支持 WebAssembly');
|
||||
}
|
||||
|
||||
return new WXWebAssembly.Memory({
|
||||
initial,
|
||||
maximum,
|
||||
shared: false // 微信小游戏不支持 shared memory
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 WASM Table
|
||||
*/
|
||||
createTable(initial: number, maximum?: number): WebAssembly.Table {
|
||||
if (typeof WXWebAssembly === 'undefined') {
|
||||
throw new Error('当前微信基础库版本不支持 WebAssembly');
|
||||
}
|
||||
|
||||
return new WXWebAssembly.Table({
|
||||
element: 'anyfunc',
|
||||
initial,
|
||||
maximum
|
||||
});
|
||||
}
|
||||
}
|
||||
27
packages/platform-wechat/src/types/wx-extensions.d.ts
vendored
Normal file
27
packages/platform-wechat/src/types/wx-extensions.d.ts
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 微信小游戏类型定义扩展
|
||||
* 补充官方类型定义包缺失的 API
|
||||
*/
|
||||
|
||||
declare namespace WechatMinigame {
|
||||
interface Wx {
|
||||
/**
|
||||
* 判断小程序的 API,回调,参数,组件等是否在当前版本可用
|
||||
* @param schema 使用 ${API}.${method}.${param}.${option} 或者 ${component}.${attribute}.${option} 方式来调用
|
||||
* @returns 当前版本是否可用
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 对象的属性或方法
|
||||
* wx.canIUse('console.log')
|
||||
* wx.canIUse('CameraContext.onCameraFrame')
|
||||
*
|
||||
* // wx接口参数、回调或者返回值
|
||||
* wx.canIUse('openBluetoothAdapter')
|
||||
* wx.canIUse('getSystemInfoSync.return.safeArea.left')
|
||||
* wx.canIUse('showToast.object.image')
|
||||
* ```
|
||||
*/
|
||||
canIUse(schema: string): boolean;
|
||||
}
|
||||
}
|
||||
46
packages/platform-wechat/src/utils.ts
Normal file
46
packages/platform-wechat/src/utils.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 微信小游戏工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取微信全局对象
|
||||
*/
|
||||
export function getWx(): WechatMinigame.Wx {
|
||||
if (typeof wx === 'undefined') {
|
||||
throw new Error('当前环境不是微信小游戏环境');
|
||||
}
|
||||
return wx;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测当前是否为微信小游戏环境
|
||||
*/
|
||||
export function isWeChatMiniGame(): boolean {
|
||||
try {
|
||||
if (typeof wx === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
const wxObj = wx as WechatMinigame.Wx;
|
||||
return typeof wxObj.getWindowInfo === 'function' &&
|
||||
typeof wxObj.createCanvas === 'function' &&
|
||||
typeof wxObj.createImage === 'function';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将微信回调风格 API 转换为 Promise
|
||||
*/
|
||||
export function promisify<T>(
|
||||
fn: (options: any) => void,
|
||||
options: any = {}
|
||||
): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
fn({
|
||||
...options,
|
||||
success: (res: T) => resolve(res),
|
||||
fail: (err: any) => reject(err)
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user