跳转到内容

LayaSteam 插件实例(Windows)

从 LayaAir 3.4.1 开始,LayaNative 提供跨平台扩展插件机制。插件通过统一的 Extension API 注册 JSVM 接口,平台工程负责生成并装载对应形态的原生库。

目标平台常见插件形态
Windows.dll 动态库
Android按 ABI 编译的 .so 动态库
iOS静态库或编入 Xcode 工程的源码
Linux.so 动态库
鸿蒙按 ABI 编译的 .so 动态库

本例只保留 Steam 初始化、用户信息、Overlay、Rich Presence 和排行榜:

  • steam_native.dll:实现 LayaNative Extension,并导出 laya_extension_init
  • laya_steam.layaext.json:声明扩展名称、API 版本和 Windows 库路径。
  • steam_api64.dll:Steamworks SDK 的 64 位可再发行运行库。
  • globalThis.laya_steam:TypeScript 使用的扩展对象。
  • SteamAPI_RunCallbacks():每帧驱动 Overlay、排行榜等异步回调。
  1. 阅读 LayaNative 扩展插件开发,了解 manifest、入口宏、平台库形态与线程规则。
  2. Steamworks 创建应用,并取得自己的 AppID。
  3. 从 Steamworks 后台下载 Steamworks SDK。完整 SDK 受 Steamworks 许可约束,不要提交到示例项目。
  4. 安装 Visual Studio 的 C++ 桌面开发组件,并准备支持新 Extension API 的 LayaNative 3.4.1 Windows SDK 与客户端。
  5. 测试时启动桌面 Steam,并登录具有该应用许可的账号。

本文示例只编译 Release|x64;这是示例实现范围,不是 LayaNative Extension 的平台限制。

TypeScript、C++ 源码和 Windows 发布模板都放在 LayaPro 项目内:

LayaSteamProject/
├─ build-templates/windows/release/
│ ├─ config.ini
│ ├─ laya_steam.layaext.json
│ ├─ steam_native.dll
│ └─ steam_api64.dll
├─ native/LayaSteam/
│ ├─ LayaSteam.vcxproj
│ ├─ SteamBinding.cpp
│ ├─ SteamBridge.cpp
│ └─ SteamBridge.h
├─ src/plugins/layasteam/LayaSteam.ts
├─ src/Main.ts
└─ settings/BuildSettings.json

LayaPro 发布 Windows 时会把 build-templates/windows 合并到发布目标,所以 release/ 子目录中的 manifest 和 DLL 最终与 exe 同目录。新 Extension API 不需要把 DLL 作为 Laya 资产导入,也不再使用 assets/plugins、DLL .metarelease/extensions/$DLL_PATHS

config.ini 必须开启扩展加载:

[common]
GraphicsAPI=WebGL
LoadExtension=true
[Render]
VSync=true

manifest 使用扩展名 .layaext.json

{
"extension": {
"name": "laya_steam",
"version": "1.0.0",
"api_version": 1,
"description": "LayaSteam Windows extension sample"
},
"libraries": {
"windows.x86_64": "steam_native.dll"
},
"dependencies": []
}

运行时扫描 manifest,按当前平台加载库,再把扩展对象挂到 globalThis[extension.name],因此本例的 TypeScript 对象名为 globalThis.laya_steam

#include <steam/steam_api.h>
bool SteamBridge::restart(uint32_t appId) {
return SteamAPI_RestartAppIfNecessary(appId);
}
bool SteamBridge::init() {
SteamErrMsg message{};
if (SteamAPI_InitEx(&message) != k_ESteamAPIInitResult_OK)
return false;
if (!SteamUser() || !SteamFriends() || !SteamUtils() || !SteamUserStats()) {
SteamAPI_Shutdown();
return false;
}
initialized_ = true;
return true;
}
void SteamBridge::update() {
if (initialized_) SteamAPI_RunCallbacks();
}
void SteamBridge::shutdown() {
if (initialized_) SteamAPI_Shutdown();
initialized_ = false;
}

Overlay 不要做成只能调用一次的状态。每次点击都直接调用 Steam API:

bool SteamBridge::openOverlay(const std::string& dialog) {
if (!initialized_ || !SteamUtils()->IsOverlayEnabled()) return false;
SteamFriends()->ActivateGameOverlay(dialog.c_str());
return true;
}

排行榜接口是异步的。FindOrCreateLeaderboardUploadLeaderboardScoreDownloadLeaderboardEntries 返回后,通过 CCallResult 接收结果;TypeScript 每帧调用 update() 才会触发回调。

插件包含 <extension/LayaExtension.h>,在 LAYA_EXT_EVENT_INIT 中把函数注册到 iface->get_exports()

#include <extension/LayaExtension.h>
#include <jsvm/JSVM.h>
struct Export {
const char* name;
jsvm_callback callback;
};
constexpr Export exports[] = {
{"restartAppIfNecessary", restart},
{"init", init},
{"shutdown", shutdown},
{"update", update},
{"getStatus", status},
{"isOverlayEnabled", overlayEnabled},
{"getSteamId", steamId},
{"getPersonaName", personaName},
{"activateGameOverlay", openOverlay},
{"setRichPresence", setPresence},
{"findOrCreateLeaderboard", findLeaderboard},
{"uploadLeaderboardScore", uploadScore},
{"downloadLeaderboardEntries", downloadScores},
{"getLeaderboardState", leaderboard},
};
int onEvent(LayaExtEventType event,
const LayaExtensionInterface* iface, void*) {
if (event == LAYA_EXT_EVENT_DEINIT) {
SteamBridge::instance().shutdown();
return 0;
}
if (event != LAYA_EXT_EVENT_INIT || !iface) return 0;
jsvm_env env = iface->get_env();
jsvm_value target = iface->get_exports();
if (!env || !target) return -1;
for (const auto& item : exports) {
jsvm_value function;
if (jsvm_create_function(env, item.name, JSVM_AUTO_LENGTH,
item.callback, nullptr, &function) != jsvm_ok)
return -1;
if (jsvm_set_named_property(env, target, item.name, function) != jsvm_ok)
return -1;
}
return 0;
}
int extensionInit(const LayaExtensionInterface*, LayaExtensionInitInfo* info) {
if (!info) return -1;
info->api_version = LAYA_EXTENSION_API_VERSION;
info->name = "laya_steam";
info->version = "1.0.0";
info->on_event = onEvent;
info->user_data = nullptr;
return 0;
}
LAYA_EXTENSION_ENTRY(extensionInit)
LAYA_EXTENSION_ENTRY_NAMED(laya_steam, extensionInit)

Windows DLL 至少应导出 laya_extension_init。具名宏还会导出 laya_extension_init_laya_steam,便于静态或具名加载场景使用。不要再导出旧兼容入口 LayaExtInit

最小工程需要:

  • 包含目录:LayaNative SDK 的 include、Steamworks SDK 的 public
  • 库目录:LayaNative SDK 的 lib、Steamworks SDK 的 redistributable_bin/win64
  • 链接库:conch.libsteam_api64.lib
  • C++ 运行库:/MT,平台为 x64
Terminal window
$env:LAYANATIVE_SDK_ROOT = "D:\path\to\LayaNativeSDK\Runtime\x64\release"
$env:STEAMWORKS_SDK_ROOT = "D:\path\to\steamworks_sdk\sdk"
msbuild .\native\LayaSteam\LayaSteam.vcxproj `
/p:Configuration=Release /p:Platform=x64

示例工程把编译产物直接输出到 build-templates/windows/release/,并复制 Steamworks 可再发行库。不要提交 build/.vs/.obj.lib.exp.pdb 或完整 Steamworks SDK。

业务代码不直接关心 DLL 路径,只读取 manifest 声明的全局对象:

export interface SteamNative {
restartAppIfNecessary(appId: number): boolean;
init(): boolean;
shutdown(): void;
update(): void;
getStatus(): { initialized: boolean; appId: number; lastError: string };
isOverlayEnabled(): boolean;
getSteamId(): string;
getPersonaName(): string;
activateGameOverlay(dialog: string): boolean;
setRichPresence(key: string, value: string): boolean;
findOrCreateLeaderboard(name: string): boolean;
uploadLeaderboardScore(score: number, keepBest?: boolean): boolean;
downloadLeaderboardEntries(first: number, last: number): boolean;
getLeaderboardState(): LeaderboardState;
}
export class LayaSteam {
static load(): SteamNative {
if (!Laya.LayaEnv.isConch)
throw new Error("LayaSteam only runs in a LayaNative Windows build");
const plugin = (globalThis as any).laya_steam as SteamNative | undefined;
if (!plugin) throw new Error("laya_steam extension is not loaded");
return plugin;
}
}

Web 预览没有 Native 环境,应显示提示信息,不要在浏览器中调用插件。

仓库公开示例保留 APP_ID = 0。发布前替换为自己的 AppID;只有大于 0 时才调用 restartAppIfNecessary()

private static readonly APP_ID = 0;
private steam!: SteamNative;
private phase = "idle";
onStart(): void {
if (!Laya.LayaEnv.isConch) return;
this.steam = LayaSteam.load();
if (Main.APP_ID > 0 && this.steam.restartAppIfNecessary(Main.APP_ID)) {
(window as any).conch.exit();
return;
}
if (!this.steam.init())
throw new Error(this.steam.getStatus().lastError);
this.steam.setRichPresence("status", "Playing LayaSteamProject");
this.steam.findOrCreateLeaderboard("LayaNative_Sample_Score");
this.phase = "finding";
Laya.timer.frameLoop(1, this, this.onFrame);
}
private onFrame(): void {
this.steam.update();
const board = this.steam.getLeaderboardState();
if (this.phase === "finding" && board.ready && !board.findPending) {
this.steam.uploadLeaderboardScore(1000, true);
this.phase = "uploading";
} else if (this.phase === "uploading" && !board.uploadPending) {
this.steam.downloadLeaderboardEntries(1, 10);
this.phase = "downloading";
} else if (this.phase === "downloading" && !board.downloadPending) {
this.phase = "done";
}
}
onDestroy(): void {
Laya.timer.clearAll(this);
this.steam?.shutdown();
}

每次点击都调用 activateGameOverlay("friends"),不要设置永久的一次性标记,也不要在首次点击后移除事件:

button.on(Laya.Event.MOUSE_DOWN, this, () => {
if (!this.steam.activateGameOverlay("friends"))
console.warn(this.steam.getStatus().lastError);
});

UI 和日志应对用户信息主动脱敏:

function mask(value: string, head = 1, tail = 1): string {
if (value.length <= head + tail) return "***";
return `${value.slice(0, head)}***${value.slice(-tail)}`;
}

在 LayaPro 中打开项目,发布平台选择 Windows。本例固定使用 WebGL。发布完成后,关键文件应与 exe 同目录:

release/
├─ LayaSteamProject.exe
├─ config.ini
├─ laya_steam.layaext.json
├─ steam_native.dll
├─ steam_api64.dll
└─ ...

确认 config.ini 中仍有 LoadExtension=true。同时确认所用 Windows 客户端来自 LayaNative 3.4.1 或更高版本并包含新 ExtensionManager;仅支持旧 LayaExtInit/Laya.importNative 的兼容客户端不能加载本例。

本地直接启动时,可在 exe 同目录创建只包含 AppID 的 steam_appid.txt。该文件仅用于本地开发,不要提交或上传到 Steam Depot。正式验证 Overlay 时,在 Steamworks 中把启动项指向发布的 exe,并通过桌面 Steam 客户端启动。

示例启动后,绿色方块持续旋转,状态面板显示 Steam 初始化、Overlay 和排行榜状态;图中用户信息已经脱敏:

LayaSteam 示例运行结果

点击 OPEN STEAM FRIENDS 后打开 Steam Overlay。点击“返回游戏”后再次点击同一按钮,Overlay 仍能重新打开:

Steam Overlay 已由 LayaSteam 打开

完整验证至少包括:

  1. DLL 导出 laya_extension_init,发布目录存在 manifest,且 LoadExtension=true
  2. init() 成功,UI 和日志不暴露完整账号名、SteamID 或私有测试配置。
  3. Overlay: ready,好友 Overlay 可以打开、返回、再次打开。
  4. 排行榜完成查找、上传、下载,点击上传按钮后分数增加。
  5. 退出场景时执行 shutdown(),程序无崩溃。
现象检查项
laya_steam extension is not loaded检查 LoadExtension=true、manifest 与 DLL 是否在 exe 同目录,并确认客户端支持 3.4.1 新 Extension API。
DLL 没有加载dumpbin /exports steam_native.dll 确认存在 laya_extension_init,并检查 steam_api64.dll 是否同目录。
SteamAPI_InitEx failed确认桌面 Steam 已运行、账号拥有应用许可、AppID 正确,并检查 steam_api64.dll
AppID 不一致restartAppIfNecessary() 的参数、steam_appid.txt 和 Steam 启动应用必须一致。
Overlay: waiting必须通过 Steam 客户端启动,且用户和游戏的 Steam Overlay 设置均已开启。Overlay 可能延迟数秒就绪。
第一次能打开,返回后不能再打开每次点击都调用 ActivateGameOverlay,不要使用一次性状态,同时每帧执行 SteamAPI_RunCallbacks()
排行榜一直 pending检查是否每帧调用 update(),并确认 CCallResult 生命周期覆盖异步请求。
Windows 运行时报渲染上下文错误检查发布配置和客户端版本是否匹配;本例使用 windows.renderMode: "webgl"