先跑个题。做独立游戏也好,接商业外包也好,只要游戏想上Steam,“对接Steam”就是绕不过去的一关。我之前在一个横版动作项目里完整跑通了接入Steamworks.NET到上传构建版本的全流程,踩了不少坑,这篇文章就把整个链路拆开讲一遍:从为什么选Steamworks.NET、初始化要注意什么,到成就/云存档怎么接,最后是怎么把Build传到Steam后台并发布出来的。适合第一次做Steam上架、对SDK还比较陌生的Unity开发者参考。
1. 对接Steam的整体思路与Steamworks.NET选型
1.1 为什么是Steamworks.NET而不是直接用SDK
Steam官方提供的Steamworks SDK底层是C++接口,而Unity项目跑的是C#脚本。直接用SDK意味着你要自己搞一层C#调用C++的P/Invoke封装,尤其是在接口回调、结构体封送这块,工作量不小,Emit、回调、线程模型随便一个坑都够忙活一阵。
Steamworks.NET就是干这个事的。它用P/Invoke把Steam官方SDK包装了一遍,把C++的函数名原样暴露给C#调用,几乎每个API都能找到对应方法。好处有几点:
- 你不需要自己管C++那边内存释放、指针、结构体对齐之类的事。
- Unity Asset Store里直接能装,也可以用Package Manager走Git URL安装,集成成本低。
- 项目在GitHub上开源,社区用的人多,碰到问题能搜到大量解决方案。
- 版本跟随官方SDK节奏更新,我用的版本对应的是Steamworks SDK 1.60左右,接口覆盖比较全。
从架构上看,Steamworks.NET并没有帮你重新封装一套“Unity风格”的API,它就是把底层能力原样递到你手上。你照样要调用SteamAPI.Init()、注册回调、轮询SteamAPI.RunCallbacks(),但这正好是好事——因为你一旦理解了原生SDK的运行逻辑,换到别的平台(比如GOG、Epic)时思路也是通的。
1.2 对接前先想清楚:你要接哪些能力
很多人一上来就问“怎么接Steam”,但实际上Steam是一个很大的平台,能力分成好几块:用户信息、成就/统计、云存档、排行榜、好友/群组、覆盖层(Overlay)、联机(Steam Multiplayer/Networking)、创意工坊、游戏服(Game Server)、微交易等。
我们项目当时只用了其中几块,但正因为提前想清楚了边界,后面才没被API数量吓到。我建议你在写第一行代码前,先确认这几个问题:
| 功能需求 | 对应的官方模块 | 你需要在后台做的事 |
|---|---|---|
| 游戏内显示玩家昵称/头像 | Steam Friends & User | 不用额外配置 |
| 解锁成就 | Steam Stats & Achievements | 在Steamworks后台创建成就并标记API Name |
| 上传同步游戏进度 | Steam Cloud | 配置云存档路径规则 |
| 排行榜 | Steam Stats | 后台创建排行榜 |
| 好友邀请/覆盖层 | Steam Overlay | 初始化够了就能用 |
| 联机对战 | Steam Networking / Lobby | 需要额外处理匹配逻辑 |
一句话:想清楚接哪些,不要每个模块都看一遍,省下的时间够你调一整天bug。
1.3 开发期和上架期是两条不同的路
对接Steam还有一个容易忽略的点:开发期的“能跑”和上架期的“能发布”是两套东西。开发期你只需要:
- 一个Steam开发者账号,交过100美元(这个后面会成为你发布游戏时的App押金)。
- 在Steamworks后台创建一个应用,拿到App ID。
- 本地放一个
steam_appid.txt,让Unity编辑器里能用Steam的相关API调试。 - Steam客户端保持运行,用当前账号调试。
但到了发布期,你还要处理Depot配置、上传构建、Beta分支、商店页面内容、审核要求的材料封面图、宣传图、游戏介绍等一堆事情。所以我把本文的主体分成两大块:第一块是开发期的SDK接入写法,第二块是Steamworks后台的配置和上传发布流程。两边都跑通了,才算真正接完了Steam。
2. 打好地基:初始化与基础配置
2.1 安装Steamworks.NET的正确姿势
最省事的方式是Unity Package Manager(UPM)的Git URL安装。
打开Window -> Package Manager -> Add package from git URL...,输入:
https://github.com/rlabrecque/Steamworks.NET.git?path=/com.rlabrecque.steamworks.net装完在Packages目录下能看到com.rlabrecque.steamworks.net。它的内部结构大致是:
Plugins/ x86_64/ steam_api64.dll x86/ steam_api.dll Runtime/ Steamworks.NET相关的C#脚本这里有个重点:Steamworks.NET依赖原生DLL,而这些原生DLL要能被打进构建包。你需要在Player Settings的Active Input Handling那边无所谓,但显卡API无关、脚本后端建议用IL2CPP,同时要保证steam_api64.dll在最终PC构建的根目录。UPM包自带插件配置,大多情况不用手动拖,但如果你发现构建后初始化报错,第一件事就是检查这两个DLL在不在输出目录。
注意Steamworks.NET版本和Steam客户端并非严格一一对应,但尽量用较新版本,因为它里面预设了最新的接口签名。我用的版本对应的是Steamworks SDK 1.60(版本号从Unity Package Manager能直接看到)。如果你在Asset Store下的是一个老版本,建议直接上GitHub看最新release。
2.2 初始化时机:必须在第一个场景加载之前完成
Steam的初始化有一个硬性约束:SteamAPI.Init()必须在主线程、并且要在游戏逻辑正式跑起来之前调用。什么叫做“正式跑起来”?在Unity里就是指第一个场景中的脚本Awake之前,或者最迟在Start。
实战中我建议用静态构造函数 +[RuntimeInitializeOnLoadMethod]做到最早初始化:
using Steamworks; public static class SteamInitializer { public static bool IsInitialized { get; private set; } [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void Init() { if (!SteamAPI.Init()) { UnityEngine.Debug.LogError("[Steam] SteamAPI.Init failed."); return; } IsInitialized = true; // Unity生命周期里需要手动驱动回调分发 SteamAPI.RunCallbacks(); } }RuntimeInitializeLoadType.SubsystemRegistration在任何场景加载前触发,适合做平台SDK接入。但注意:普通场景脚本执行时,这个静态类可能还没被JIT触发,所以更好的做法是搞一个单例Manager,在一个不销毁的GameObject上挂Awake并执行初始化,同时在Update里调用SteamAPI.RunCallbacks():
using Steamworks; using UnityEngine; public class SteamManager : MonoBehaviour { private static SteamManager _instance; private bool _initialized; private void Awake() { if (_instance != null) { Destroy(gameObject); return; } _instance = this; DontDestroyOnLoad(gameObject); _initialized = SteamAPI.Init(); if (!_initialized) { Debug.LogError("[Steam] SteamAPI.Init() failed."); } } private void Update() { if (!_initialized) return; SteamAPI.RunCallbacks(); } private void OnApplicationQuit() { if (_initialized) { SteamAPI.Shutdown(); } } }几点说明:
SteamAPI.RunCallbacks()必须每帧调用,否则回调(比如成就解锁回调、云存档上传回调)永远不会触发。OnApplicationQuit里调用Shutdown是常见的做法,但要注意OnApplicationQuit在部分情况下会被Unity跳过,更多时候是无害的。- 初始化失败时不要继续调任何Steam相关方法,否则大概率直接抛异常。
2.3 调试时不能忽略的steam_appid.txt
这个文件坑过很多第一次接Steam的人。规则很简单:在Unity工程的根目录(不是Assets目录)放一个文本文件,名字叫steam_appid.txt,里面只写你的App ID,不要换行、不要空格、不要BOM。
比如你的App ID是1234560,那么文件内容就是一行:
1234560为什么要放这个文件?因为你在Unity Editor里调试时,游戏并没有通过Steam客户端启动,Steam客户端不知道你是哪个游戏。有了这个文件,SDK才知道“哦,我属于1234560这个App”,才能以这个App的身份连接Steam客户端。
如果你的App ID填错了,或者文件放在了Assets下,你会在Console里看到经典报错:
[Steamworks.NET] SteamAPI_Init() failed这玩意儿的排查优先级极高,我后面在常见问题里还会讲。
2.4 打包后的目标平台和条件编译
如果你以后要发布到Steam以外的平台,或者项目里还接了别的平台SDK,一定要把Steam相关代码用平台宏包起来:
#if UNITY_STANDALONE _initialized = SteamAPI.Init(); #endifUNITY_STANDALONE在Windows、Mac、Linux都定义了。如果将来要只发布Windows,可以细分为UNITY_STANDALONE_WIN。
另一个坑是:Unity编辑器里SteamAPI.Init()能不能成功,取决于当前电脑有没有运行Steam客户端。如果你没开Steam,初始化一定失败。这个不是代码问题,是环境问题。
3. 接功能:成就、统计、云存档和好友邀请
3.1 解锁成就:命名一致能坑你一整天
Steam成就的工作机制是这样的:在Steamworks后台创建成就时可以设置一个API Name(内部名),代码里通过这个API Name来操作成就。这个API Name是纯字符串,大小写敏感,空格也算。
后台创建成就时给你的API Name类似:
ACH_WIN_LEVEL_1代码里解锁成就:
using Steamworks; public static class SteamAchievements { public static bool Unlock(string apiName) { if (!SteamManager.IsInitialized) return false; if (SteamUserStats.GetAchievement(apiName, out bool achieved)) { if (!achieved) { SteamUserStats.SetAchievement(apiName); return SteamUserStats.StoreStats(); } } return false; } }注意有两处容易漏:
SetAchievement只是改了本地的缓存状态,不调用StoreStats()就不会被提交到Steam服务器。GetAchievement返回的是本地缓存,不是实时的。要获取最新状态,需要先RequestCurrentStats()再从回调里读取。
如果你发现成就解锁了但Steam客户端里没亮,优先查API Name是否填错,其次查StoreStats()是否被调用。
还有一个非常反直觉的事:Steam后台识别统计数据和成就是用同一套API Name体系,你在后台给成就起的名字,和代码里的名字必须完全一致。如果你中途改了后台的成就API Name,旧版本游戏里的成就数据就接不上了,这在发布后是大忌。
3.2 统计数据与存储进度
统计数据(Stats)和成就是两回事,但代码接口在一块。统计数据又分整型和浮点型,后台也要先定义统一的名字。
写数据:
SteamUserStats.SetStat("games_played", 42); SteamUserStats.StoreStats();读数据,要在请求到Server端数据之后:
// 建议在游戏启动时调用 SteamUserStats.RequestCurrentStats(); // 然后在回调里读取(或者稍后) SteamUserStats.GetStat("games_played", out int gamesPlayed);有一点要强调:RequestCurrentStats()的结果是通过回调UserStatsReceived_t返回的。Steamworks.NET里用了Callback<...>机制,你得提前注册:
private Callback<UserStatsReceived_t> _statsReceivedCallback; private void OnEnable() { _statsReceivedCallback = Callback<UserStatsReceived_t>.Create(OnUserStatsReceived); } private void OnUserStatsReceived(UserStatsReceived_t callback) { if (callback.m_eResult == EResult.k_EResultOK) { // 此时本地统计数据缓存才可用 } }RequestCurrentStats()要放在SteamAPI.Init()成功之后才能调,而且最好是玩家一开始就请求一次,后面再反复请求反而容易把本地未提交的数据覆盖掉。
3.3 云存档:把自己存档路径交给Steam云
云存档这块最容易踩的坑是把存档逻辑搞得太复杂。其实Steam云只干一件事:同步一个被标记的文件夹。
Steamworks后台有一个“Steam Cloud”页面,里面可以配置“自动云同步的路径”。这个路径是相对于游戏本地的Steam/userdata/<你的账号ID>/<AppID>/目录的。换句话说,如果你的游戏把存档写到Application.persistentDataPath下,而这个路径在Windows是C:/Users/<用户名>/AppData/LocalLow/<公司名>/<游戏名>,那你要在后台配置云同步规则,让这个路径被Steam云纳入同步。
但这有个前提:Steam云只在Steam客户端环境下有效,而且有的版本对路径匹配比较死板。我们项目的做法是:存档文件写到自己的目录,然后用SteamCloud的自动同步规则覆盖整个persistentDataPath。好处是不用写任何同步代码,坏处是一旦存档目录里混入了乱七八糟的缓存文件(比如日志、临时文件),云同步会把它们一起传上去,白白浪费空间。
如果你想更可控一点,就用SteamRemoteStorage读写:
// 写入云存档 byte[] data = System.IO.File.ReadAllBytes(localFilePath); bool success = SteamRemoteStorage.FileWrite("savegame.sav", data, data.Length);FileWrite是同步调用,动作快,但它要求数据长度不超过100MB(单个文件限制是100MB,但上传体积有配额)。读的时候用FileRead。
这里有个从真实项目里带出来的教训:不要在整个游戏存档目录上启用全局云同步,除非你很清楚自己目录里都长什么样。最好是专门搞一个SaveData目录,只同步这里。
3.4 好友邀请与Overlay:不要自己造轮子
Steam Overlay是个很好用的东西,它让你不用管UI就能实现好友邀请、截图、成就提示,甚至内置浏览器。打开好友列表邀请玩家:
SteamFriends.ActivateGameOverlay("friends");ActivateGameOverlay的可选参数里有friends、achievements、lobby、lookup等。
如果项目有联机功能,通常会先创建一个Lobby(大厅),然后邀请好友:
SteamMatchmaking.CreateLobby(ELobbyType.k_ELobbyTypePublic, 4);创建成功的回调里能拿到LobbyCreated_t,里面有m_ulSteamIDLobby,把这个ID发给朋友,他那边用JoinLobby加入。Steamworks.NET都封装好了,但要注意:Lobby类型一定要选对。k_ELobbyTypeFriendsOnly适合朋友邀请,k_ELobbyTypePublic适合公开匹配。上线前改类型会直接影响玩家能不能搜到大房间。
做联机时还有一个隐藏细节:Lobby Owner掉线时,Steam会自动转移Owner,你的代码要监听LobbyDataUpdate_t和LobbyChatUpdate_t回调来处理玩家退出房间的逻辑。
4. Steamworks后台配置与上传发布
4.1 Steamworks后台到底要配什么
SDK代码写完了,就要去Steamworks后台做准备。登录partner.steamgames.com(后台网址不用特别记,从Steam客户端开发者菜单能进)。
在这里要做的事大约是:
- 在“你的应用”里创建应用,填写游戏名,这会生成一个唯一的App ID,同时给你分配一个Default Depot(默认内容仓库)。
- 在“SteamPipe”相关页面里,确认Depot编号,通常每个App会有一个默认Depot,也可以自己加新的Depot用于区分DLC、公共资源等。
- 配置安装目录。安装目录就是Steam库中这个游戏最终落地的文件夹名。
- 配置启动项,通常就是那个exe的名字。
这里有个坑:App ID和Depot ID是两个不同的号码。App ID是游戏本身,Depot ID是某个内容仓库。上传构建的脚本里两个都要对应填对,填反了会在上传时报AppID mismatch或者干脆不认。
有关Depot的官方定义,我在这里口头翻译一下:Depot就是游戏文件的一个“集合”,一个游戏可以有好几个Depot(比如原版、高清包),最终玩家下载时按优先级合成一个完整安装目录。
4.2 用SteamPipe上传构建版本的完整流程
Steam官方推荐用SteamPipe工具上传构建。这个工具可以从Steam客户端里下载,也可以直接下载steamcmd的包,里面带steamcmd.exe和示例脚本。
上传构建的核心是准备两个VDF(Valve Data Format)文件:第一个描述整个App的构建,第二个描述某个Depot的文件映射关系。
先看App构建脚本(app_build.vdf):
"AppBuild" { "AppID" "1234560" "Desc" "build from script v1.0" "BuildOutput" "C:\steambuild\output" "ContentRoot" "C:\steambuild\content" "SetLive" "beta" "Depots" { "1234561" "depot_build.vdf" } }再看Depot脚本(depot_build.vdf):
"DepotBuildConfig" { "DepotID" "1234561" "ContentRoot" "C:\steambuild\content" "FileMapping" { "LocalPath" "*" "DepotPath" "." } "FileExclusion" "*.pdb" "FileExclusion" "*.tmp" "FileExclusion" "*.log" }注意几点:
ContentRoot是你本地放构建产物的根目录,比如Unity打包输出的文件夹。FileMapping里的LocalPath="*"表示把ContentRoot下所有文件都映射进去;DepotPath="."表示保持相对目录结构。BuildOutput是上传过程中生成中间文件的目录,可以理解成临时工作目录。FileExclusion里排除掉不需要上传的文件,比如PDB调试文件、日志文件。这个一定要写,否则一个日志文件就能把每次构建的大小拖废物。
上传命令(Windows下用steamcmd.exe):
steamcmd.exe +login <用户名> <密码> +run_app_build C:\steambuild\scripts\app_build.vdf +quit如果你是双因素认证(Steam令牌),steamcmd会提示你输入Steam Guard Code。上传过程会输出进度,显示每个文件的哈希值、上传大小,最后生成一个Build ID。
一个非常关键的配置是"SetLive" "beta"。它的意思上传完成后,直接把构建设置为名为beta的分支的Live版本。如果你不写这个字段,上传完成后不会自动发布到任何分支,你还要去Steamworks后台手动把构建“设置为测试版”。
4.3 测试发布:从Beta分支到公开上架
上传完构建后,去Steamworks后台的“测试版”标签页:
- 创建一个新的测试版分支(Beta Branch),任意起名,比如
beta。 - 为这个分支设置一个访问密码(方便只有测试人员能进)。如果留空,则所有玩家都能看到这个分支,这不适合未公开的游戏。
- 在“构建”标签页选择一个已上传的Build ID,点击“设为测试版”。
此时,你的Steam账号如果在这个App的测试权限名单里,重启Steam客户端后就能从游戏属性里切到这个Beta分支下载到最新构建来测试。这一步极其重要:任何一次大版本更新前,先跑一遍Beta分支自测再设公开,能省下不知道多少“玩家下到了坏版本”的差评。
到了要上架的时候,需要在后台的“商店页面”补全游戏名、简介、宣传图、截图、分类标签等,然后提交商店页面审核。商店页面审核过了之后,再设置发布日期、价格,游戏才算正式公开。
这中间有大量的文案和素材准备,但和对接SDK本身关系不大,就不展开讲了。
4.4 版本更新的闭环逻辑
游戏上线后依然离不开上传这套流程,只是从“第一次上传”变成了“每次更新”。现在的节奏是:
- Unity打包生成新版本,复制到
ContentRoot目录。 - 修改
app_build.vdf里的Desc(描述),用来标记这次构建的是什么内容。 - 运行
steamcmd +run_app_build。 - 在后台确认构建上传成功。
- 先切到
beta分支自测,然后把公开分支指向新Build ID。
整套流程熟练后,一次上传大概几分钟到十几分钟。上传的唯一瓶颈就是你本地上传带宽,S3式上传速度通常都能跑满。
5. 常见问题与排查技巧实录
5.1 编辑器里脚本跑了,初始化却一直报失败
这个遇到的人最多。优先排查顺序:
| 检查项 | 具体操作 |
|---|---|
| steam_appid.txt | 确认文件在Unity工程的根目录,内容只有App ID,没有多余字符 |
| Steam客户端 | 必须登录并处于运行状态 |
| DLL是否被打进工程 | 看Plugins/x86_64/steam_api64.dll存在,并且平台设置是x86_64 |
| 重复初始化 | 确认你没有在多个脚本里重复调用SteamAPI.Init() |
| Steamworks.NET版本 | 版本过老可能无法与当前Steam客户端正常握手,升级库版本 |
另一个隐蔽问题是:你在Unity编辑器里改了steam_appid.txt后,一定要重启一次Unity编辑器。因为Steamworks.NET在第一次初始化时把文件内容读进缓存了,后面再改,只要不重启,它读到的还是旧值。这个坑我印象极深,当时为它折腾了一整个下午。
5.2 成就解锁了,但Steam客户端里不显示
这种情况通常和四件事有关:
- 后台成就的API Name和代码里的字符串不完全一致(包括大小写和空格)。
SetAchievement之后没有调用StoreStats()。- 当前Steam账号之前已经解锁过这个成就(成就状态在云端是已解锁的,你在本地重复Set没用)。
- 后台成就还没通过审核/发布状态下。注意:未发布的游戏,成就虽然在后台配置了,但由于App本身处于未公开状态,成就显示可能会有延迟或者仅在测试账号可见,这是正常的。
排查时可以先打印一下GetAchievement的返回值。如果返回的achieved为true,说明成就在云端已经是解锁状态,只是客户端UI延迟显示,等一会儿就好。
5.3 上传构建后,玩家下载下来缺文件、白屏、崩溃
这个通常不是SDK问题,而是打包和上传的路径配置出了岔子。
最典型的:Unity Build出来的目录里,除了exe之外还有_Data文件夹、MonoBleedingEdge、UnityPlayer.dll等。如果你的ContentRoot只指向了exe所在的目录,但没有正确的FileMapping,或者LocalPath配成了某个子目录,就会缺一半文件。
正确的做法是:ContentRoot直接指向Unity Build输出的完整目录,LocalPath="*",然后在构建目录里做一次完整性验证(玩家视角验证:解压后目录结构应该和本地一致)。
还有一个高频坑:不要往ContentRoot里塞版本控制文件、临时文件、图片源文件,这些东西会无谓地增大包体,并且在玩家端占地方。
5.4 云存档不生效,或者不同机器之间互相覆盖
云存档生效需要同时满足:
- Steam客户端在线,当前账号购买了/拥有这个App。
- 后台“Steam Cloud”配置了允许云同步的目录。
- 本地存档确实写在那个同步路径下,或者用
SteamRemoteStorage显式写入。
如果你用了SteamRemoteStorage.FileWrite,还要注意:Steam云同步的冲突处理逻辑由客户端决定,谁后上传谁赢。你不要在游戏启动时盲目读取云存档并覆盖本地存档,否则玩家在A机器玩了5小时,到B机器一启动就被旧的本地档覆盖了,回头A机器再玩游戏时又被覆盖回去,两头丢进度。
稳妥做法:启动时先读Steam云存档的数据,和本地存档比较修改时间,然后弹窗让玩家选择“使用云端”“使用本地”“取消”。当然这是体验极好的做法;如果偷懒,至少也要在游戏关闭前老老实实把最新存档写入云端。
5.5 steamcmd上传时报错反复,但不知从何查起
上传报错分这么几种:
Invalid Depot Config:大概率是depot_build.vdf的路径不对,或者VDF文件用了带BOM的UTF-8编码。用记事本另存为“UTF-8无BOM”就行。Failed to create depot build:本地Depot缓存目录有问题,删掉BuildOutput下的临时文件,重新上传。This account does not own App ID XXX:账号没有对应App的权限,确认用的上传号在Steamworks后台有这个App的管理权限。- 上传到一半失败:本地网络问题或后台操作超时,重跑一遍即可,SteamPipe支持增量上传,不用担心重复传整个包。
注意:steamcmd登录方式如果还开着Steam客户端,有可能冲突。上传构建时建议先退出Steam客户端,或者把steamcmd的登录态独立出来。我第一次上传时就遇到过“Steam客户端和steamcmd抢同一份登录状态”导致验证卡死的怪问题。
6. 我在实际项目中沉淀的几个习惯
这些说不上是什么高级技巧,但确实帮我在后面接别的平台时少走了很多弯路。
第一个习惯:把Steam相关代码全部收敛到一个命名空间里,不要在业务代码里到处写SteamUserStats.GetStat。我封装了一个PlatformService接口,Steam只是其中一个实现。这样以后上别的平台,只需要换一套实现,业务层不用动。这个抽象看着费事,但真到要接第二个平台时你就知道值在哪了。
第二个习惯:总是在编辑器里用steam_appid.txt调试,而不是每次打包后才知道问题。初始化失败这类问题在编辑器里一眼就能看到,但打包后玩家那边的报错你是收不到的,排错成本非常高。开发规范里应该明确:所有Steam相关功能,先在编辑器里跑通,再打Beta包自测,最后才公开。
第三个习惯:上传前先清理构建目录。我自己写了一个小脚本,每次Build后自动把*.pdb、*.log、.DS_Store、Thumbs.db这类文件删掉,然后再复制到ContentRoot。保证上传到Steam的东西就是干净的。
第四个习惯:每次上传的Desc写清楚版本号和改动点。比如v1.2.3 - fix boss AI + add stats sync。别小看这行描述,到第三个版本后,你会需要在一个长列表里找出某个Build对应的改动,那时候你只能靠Desc来定位。
最后分享一个没人会告诉你的小细节
你在app_build.vdf里写的ContentRoot,最好永远指向同一个固定路径,比如C:\SteamPublish\content,而不是直接指向Unity的Build目录。原因很简单:SteamPipe会缓存文件哈希,第二次上传相同的文件能秒传。如果你每次Build目录都不固定,它就得重新读一遍所有文件,上传速度从几分钟变成十几分钟,纯粹浪费时间。
对接Steam这件事,第一次做的时候会觉得后台那一堆配置像迷宫,但只要完整跑通一遍,后面就全是重复劳动。这篇文章写到的内容基本覆盖了从SDK接入到构建上传的全流程。如果你正好卡在某个环节,按着顺序检查一遍,八成能找到答案。