SDK 初始化与 C++ 接入示例

Steamworks SDK 集成创意工坊:初始化、回调与 C++ 示例

将 Steam 创意工坊接入游戏,需要先初始化 Steamworks SDK,再持续处理异步结果,并在内容确实安装完成后交给游戏加载。本页面向 C++ 游戏和上传工具开发者,给出创建项目、查询及订阅内容的调用结构。

创作者提交内容,经 Steam 工坊分发并由游戏客户端加载的流程

初始化与关闭

将 SDK 的 public/steam 头文件加入项目,按目标平台链接并分发相应 Steam API 可再发行文件。Windows 使用对应架构的 .lib 与 .dll;macOS 使用 libsteam_api.dylib;Linux 使用 libsteam_api.so。具体文件和链接方式参见 Steamworks API 概览。

如果使用 SteamAPI_RestartAppIfNecessary(appId),应在其他 Steamworks 调用之前执行;返回 true 时尽快退出当前进程,让 Steam 启动已安装版本。再调用 SteamAPI_Init(),只有返回成功后才访问 SteamUGC() 等接口或创建监听器。开发时从调试器直接启动,可在 Steam 查找的当前工作目录放仅含 App ID 的 steam_appid.txt;它可能覆盖 Steam 提供的值,不应随正式构建发布。退出时在结束监听对象后调用 SteamAPI_Shutdown()。

初始化失败时,检查 Steam 客户端是否运行、App ID 是否可识别、当前账号是否拥有应用许可,以及游戏与 Steam 是否运行在相同用户和权限上下文中。游戏服务器包含 steam_gameserver.h,使用独立的 SteamGameServer_Init、SteamGameServer_RunCallbacks 和 SteamGameServerUGC(),专用服务器不必初始化普通客户端 API。在专用服务器应用的安装配置中勾选适用的 Steamworks SDK 可再发行文件。官方服务器说明介绍了其初始化和分发配置。

回调与调用结果

回调向注册的监听对象广播事件,例如 ItemInstalled_t;STEAM_CALLBACK 在对象创建时注册,在对象销毁时取消注册。调用结果用于某次返回 SteamAPICall_t 的异步请求,通过 CCallResult 将句柄绑定到对象和成员函数。两种机制不能互换,应依据对应 API 的定义注册。

CCallResult 应作为持续存活的对象成员,不能让对象在异步结果返回前被销毁;同一个实例只跟踪最近一次设置的调用,多个并发请求应各自持有结果对象。回调函数先检查 bIOFailure,再检查结果结构中的 m_eResult;前者为真时不要把结构视作成功结果。游戏循环频繁调用 SteamAPI_RunCallbacks(),常见做法是每帧调用,官方建议至少每秒一次。监听函数在调用 SteamAPI_RunCallbacks() 的线程中执行,因此耗时工作应交给适当的任务队列。

创建与提交项目的 C++ 结构

下例说明一次创建和更新的调用顺序,是不完整的工程片段;它没有使用实际 SDK 编译,也没有运行测试。实际工具还需验证文件路径、标题、用户界面和错误提示。为了突出生命周期,每个 WorkshopUpload 对象只处理一个待完成的创建和更新任务。

#include <steam/steam_api.h>
#include <string>
#include <utility>

class WorkshopUpload {
public:
    WorkshopUpload(AppId_t game, std::string folder, std::string preview)
        : game_(game), folder_(std::move(folder)), preview_(std::move(preview)) {}

    bool Begin() {
        if (started_) return false; // 一次性对象,禁止覆盖待完成的调用
        started_ = true;
        SteamAPICall_t call = SteamUGC()->CreateItem(
            game_, k_EWorkshopFileTypeCommunity);
        if (call == k_uAPICallInvalid) {
            finished_ = true;
            return false;
        }
        createResult_.Set(call, this, &WorkshopUpload::OnCreated);
        return true;
    }

    bool Finished() const { return finished_; }

private:
    void OnCreated(CreateItemResult_t* result, bool ioFailure) {
        if (ioFailure || result->m_eResult != k_EResultOK) {
            finished_ = true; // 向用户报告创建失败
            return;
        }
        const PublishedFileId_t id = result->m_nPublishedFileId;
        // 应持久保存 id,供后续更新同一个项目。
        if (result->m_bUserNeedsToAcceptWorkshopLegalAgreement) {
            // 提示用户查看协议并完成操作;不要替用户接受。
        }

        UGCUpdateHandle_t update = SteamUGC()->StartItemUpdate(game_, id);
        if (update == k_UGCUpdateHandleInvalid ||
            !SteamUGC()->SetItemTitle(update, "示例项目") ||
            !SteamUGC()->SetItemContent(update, folder_.c_str()) ||
            !SteamUGC()->SetItemPreview(update, preview_.c_str())) {
            finished_ = true; // 报告设置失败,不提交
            return;
        }
        SteamAPICall_t call = SteamUGC()->SubmitItemUpdate(update, "首次提交");
        if (call == k_uAPICallInvalid) {
            finished_ = true;
            return;
        }
        submitResult_.Set(call, this, &WorkshopUpload::OnSubmitted);
    }

    void OnSubmitted(SubmitItemUpdateResult_t* result, bool ioFailure) {
        if (ioFailure || result->m_eResult != k_EResultOK) {
            // 报告失败;实际代码记录结果并提供排错信息。
        } else if (result->m_bUserNeedsToAcceptWorkshopLegalAgreement) {
            // 引导用户处理协议并检查项目页面。
        }
        finished_ = true;
    }

    AppId_t game_;
    std::string folder_, preview_;
    bool started_ = false;
    bool finished_ = false;
    CCallResult<WorkshopUpload, CreateItemResult_t> createResult_;
    CCallResult<WorkshopUpload, SubmitItemUpdateResult_t> submitResult_;
};

上层应在初始化成功后创建并持有 WorkshopUpload,仅调用一次 Begin();若返回 false,应立即处理未启动的错误。启动成功后,在循环中调用 SteamAPI_RunCallbacks(),直到 Finished() 后才销毁对象。SetItemContent 的目录和 SetItemPreview 的图片必须使用有效本地绝对路径;游戏应用还需启用 ISteamUGC 文件传输并配置 Steam 云配额。上述状态仅用于说明结构,正式产品还应区分成功、失败和用户待操作,而不能把 Finished() 当作成功标记。

查询句柄的生命周期

以下片段展示一次按 ID 查询。QueryOnce 也必须存活到调用结果到达;每个对象只持有一次未完成查询。调用结果成功后遍历 m_unNumResultsReturned,然后释放句柄;失败及未能发送的路径同样释放。

#include <steam/steam_api.h>

class QueryOnce {
public:
    bool Begin(PublishedFileId_t id) {
        if (started_) return false;
        started_ = true;
        handle_ = SteamUGC()->CreateQueryUGCDetailsRequest(&id, 1);
        if (handle_ == k_UGCQueryHandleInvalid) {
            finished_ = true;
            return false;
        }
        SteamAPICall_t call = SteamUGC()->SendQueryUGCRequest(handle_);
        if (call == k_uAPICallInvalid) {
            Release();
            return false;
        }
        result_.Set(call, this, &QueryOnce::OnCompleted);
        return true;
    }

    bool Finished() const { return finished_; }

private:
    void OnCompleted(SteamUGCQueryCompleted_t* result, bool ioFailure) {
        if (!ioFailure && result->m_eResult == k_EResultOK) {
            for (uint32 i = 0; i < result->m_unNumResultsReturned; ++i) {
                SteamUGCDetails_t details{};
                if (SteamUGC()->GetQueryUGCResult(handle_, i, &details)) {
                    // 将所需数据复制到应用拥有的存储中。
                }
            }
        }
        Release();
    }
    void Release() {
        if (handle_ != k_UGCQueryHandleInvalid) {
            SteamUGC()->ReleaseQueryUGCRequest(handle_);
            handle_ = k_UGCQueryHandleInvalid;
        }
        finished_ = true;
    }
    bool started_ = false;
    bool finished_ = false;
    UGCQueryHandle_t handle_ = k_UGCQueryHandleInvalid;
    CCallResult<QueryOnce, SteamUGCQueryCompleted_t> result_;
};

上层同样只调用一次 Begin(),启动成功时持有对象并处理回调,直到 Finished() 为 true。若需提前关闭或取消对象,应先设计安全的调用结果取消和句柄清理机制,确保 Steam 尚未关闭、没有回调再访问销毁的对象。需要元数据或额外预览时,在 SendQueryUGCRequest 前设置对应 SetReturn... 选项。接口和字段详见 ISteamUGC 查询参考。

订阅到安装的检查流程

使用 GetNumSubscribedItems 分配 ID 数组,再用 GetSubscribedItems 获取实际返回的数量。当前接口默认排除本地禁用项目;专用服务器调用这两个函数返回 0,应从服务器配置等其他来源取得所需 ID。对每个项目用 GetItemState 的位标志判断:Installed 和 NeedsUpdate 可以同时存在。

如果需要下载或更新,调用 DownloadItem(id, false)。当它返回 true 时等待 DownloadItemResult_t,核对应用 ID、项目 ID 和 m_eResult,不要立即访问文件。项目安装或更新还会产生 ItemInstalled_t,同样需要核对 App ID。确认可用后调用 GetItemInstallInfo 获取目录、大小及时间,再由游戏校验格式、兼容性和内容;旧版项目的路径可能指向文件。未订阅项目可能只在临时缓存中,不能假设它永久存在。

如果需要向用户显示下载进度,用 GetItemDownloadInfo 读取字节数;总字节数仅在下载开始后有效。有关测试分支、日志和 SpaceWar 示例,参见 创意工坊实现指南与 Steamworks API 示例。