拓冰建站拓冰建站
首页 / 资讯中心 / 正文

UE4接入Steam好友系统实战:ISteamFriends集成与回调机制详解

简介面向UE4开发者尤其是需要接入Steam好友系统的研发人员这份演示资源提供了一套小型C项目源码展示如何在UE4中集成Steam Friends API。资源围绕好友列表获取、邀请发送以及接受邀请后的会话加入三个核心环节包含GetFriendsListCallBackProxy异步蓝图节点、NetBlueprintFunctionLibrary蓝图函数库与NetGameInstance自定义GameInstance等模块可用于研究Steam子系统调用及多人联机会话衔接的实现思路。包体共7个文件其中3个头文件与3个C源文件构成了主要代码逻辑另有1个Markdown说明文件辅助阅读整个压缩包仅7KB体量轻量便于快速导入分析。目前已有309人学习适合具备一定C与UE4蓝图基础、希望理解Steam Friends接口回调与在线会话流程的开发者参考。1. 项目概述与方案选型1.1 这个 Demo 到底要交付什么SteamFriendsUE4 这个项目说穿了就是给 UE4 工程接一套 Steam 好友体系的最小可运行 Demo登录后拉取好友列表、展示昵称头像和在线状态、判断好友当前在玩什么游戏、给好友发组队邀请以及把 Steam 好友面板的 Overlay 正确弹出来。我是在一个联机合作项目起步阶段做的这套东西当时团队里没人摸过 Steam Friends API 和 UE4 的对接细节与其后面在正式项目里试错不如先用一个独立 Demo 把整条链路跑通后面接大厅、接匹配、接邀请都有底。这套内容适合两类人看第一类是 UE4 联机项目的开发尤其是第一次碰 Steam 服务的第二类是想搞明白 OnlineSubsystem 和原生 Steamworks SDK 到底什么关系的人。我会以 C 实现为主蓝图侧只给事件转发思路最后把调试阶段踩过的所有坑按速查表形式整理出来可以直接照着排查。1.2 两条技术路径怎么选UE4 里做 Steam 好友功能面前有两条路一条是通过引擎自带的 OnlineSubsystemSteam 插件走 IOnlineFriends、IOnlinePresence 这些抽象接口另一条是直接包含 Steamworks SDK 头文件调用 ISteamFriends 原生接口。说实话我一开始也是用抽象层做的做到一半发现很别扭。拿一封邀请这件事举例抽象层 IOnlineFriends 只给了 SendInvite 这类通用方法参数是抽象的会话 ID但 Steam 好友体系里很多核心能力根本不在抽象层里比如拉取好友的头像、读取好友正在玩的游戏名字、配置富文本状态的本地化 token、把 Overlay 精确弹到某个好友对话框这些全都得下钻到 ISteamFriends 才能做。与其绕了一圈最终还是落回原生接口这种演示项目不如一开始就直连 Steamworks SDK代码反而干净Steam 官方文档也能逐行对上。不过正式项目我的建议是混合策略用 OnlineSubsystem 管理网络传输和会话只在好友展示、邀请这种需要深度控制的地方直接调 SDD。下面先给一个对比后面代码都是基于第二种方案。方案学习曲线功能覆盖维护成本适用场景OnlineSubsystem 抽象层中基础好友/状态/邀请低引擎升级自动适配功能简单、快速上线直接调 ISteamFriends中低全部好友能力头像Overlay富文本中需自己管理回调演示项目、功能深度定制2. 环境搭建与 Steamworks 配置2.1 拿 AppID 与 SDK 的正确姿势要跑通 Steam Friends API第一步不是写代码而是把环境身份搞定。你需要去 Steamworks 官网后台注册一个应用拿到属于自己的 AppID。如果你只是想本地验证功能可以用官方测试 ID 480Spacewar在开发环境里跑完全没问题但这里有个隐藏坑用 480 拉取好友列表时系统返回的是 Spacewar 的游戏关系链不是你自己应用的好友关系所以真正联调前切记换成自己的 AppID。Steamworks SDK 可以在 Steamworks 后台的“SDK 下载”页面拿到下载后解压里面关键的目录就两个public/steam 下面全是 C 头文件redistributable_bin 里是各个平台的动态库。UE4 引擎其实已经在 ThirdParty 里内置了一份 Steamworks源码在 Engine/Source/ThirdParty/Steamworks/Steamvxxx 目录下OnlineSubsystemSteam 插件用的就是这一份。我在项目里直接引用了引擎内置版本没有额外拷贝 SDK省掉了版本冲突的麻烦。Windows 平台上要注意的是打包出来的游戏要带着 steam_api64.dll 或 steam_api.dll编辑器模式下引擎会自己加载但独立打包时如果你引用了原生接口必须在 Build.cs 里把对应模块带上否则运行时直接崩溃报“无法定位程序输入点”之类的问题。2.2 工程级配置Build.cs 与 DefaultEngine.ini先改项目根目录下的 .uproject 文件确保 OnlineSubsystemSteam 插件启用。用记事本打开自己的工程文件在 Plugins 数组里加上这一项Plugins: [ { Name: OnlineSubsystemSteam, Enabled: true } ]然后是 Source 目录里的 Build.cs。注意我们要直接包含 Steamworks 头文件所以要引用 Steamworks 这个第三方模块同时把 OnlineSubsystem 相关模块也带上因为引擎的 Steam 网络驱动还依赖它PublicDependencyModuleNames.AddRange(new string[] { Core, CoreUObject, Engine, InputCore, OnlineSubsystem, OnlineSubsystemUtils }); PrivateDependencyModuleNames.AddRange(new string[] { Steamworks, OnlineSubsystemSteam });接下来是 Config/DefaultEngine.ini。这里有两段关键配置第一段把默认联机服务切换到 Steam第二段配置 Steam 插件的开关和测试 AppID。如果你不准备用 Steam 做网络传输只想拉好友数据第一段可以省略但保险起见建议都配上[/Script/Engine.GameEngine] NetDriverDefinitions(NameSteam,DriverClassNameOnlineSubsystemSteam.SteamNetDriver,DriverClassNameFallbackOnlineSubsystemUtils.IpNetDriver) [OnlineSubsystem] DefaultPlatformServiceSteam [OnlineSubsystemSteam] bEnabledtrue SteamDevAppId480 bVACEnabledfalseSteamDevAppId 就是刚才说的测试 ID。还有一个小细节在 Windows 平台上如果你从编辑器直接 PIE 运行有一些 Steam 接口尤其是 Overlay可能不生效解决思路是在工程目录下建一个名为 steam_appid.txt 的纯文本文件内容只写一行 AppID 数字这样非 Steam 启动的进程也能初始化 API。注意这个文件不要提交到版本库因为不同环境 AppID 可能不同。2.3 初始化流程与运行时自检UE4 的 OnlineSubsystemSteam 插件在引擎启动时会自动调用 SteamAPI_Init所以一般情况下你不需要手动初始化但 Demo 里我建议在 GameInstance 的 Init 阶段做一次健壮性检查确认 Steam 客户端真的在运行、用户真的登录了否则后面所有调用都会静默失败或者返回空数据。检查代码大概长这样bool UMySteamManager::CheckSteamReady() { if (!SteamAPI_Init()) { UE_LOG(LogTemp, Error, TEXT([SteamFriends] SteamAPI_Init failed)); return false; } if (!SteamUser() || !SteamFriends() || !SteamUtils()) { UE_LOG(LogTemp, Error, TEXT([SteamFriends] Steam interface unavailable)); return false; } if (!SteamUser()-BLoggedOn()) { UE_LOG(LogTemp, Warning, TEXT([SteamFriends] User not logged in)); return false; } int32 AppID SteamUtils()-GetAppID(); UE_LOG(LogTemp, Log, TEXT([SteamFriends] Steam ready, AppID%d), AppID); return true; }这里有个容易忽略的点SteamAPI_Init 在插件已经初始化过的情况下再次调用也会返回 true所以不用怕重复调用但前提是你没有在初始化前就去拿 ISteamFriends 指针否则拿到的是空指针。必须严格遵循“先初始化后取接口”的顺序。3. 核心功能落地Friend API 调用实践3.1 好友列表与在线状态拿到 ISteamFriends 之后第一个要做的就是从 Steam 后端拉好友列表。Steam 的好友关系分为几个分类标志最常用的是 k_EFriendFlagImmediate它表示直接好友你主动加且对方同意的不包含关注者。另一个 k_EFriendFlagAll 会把关注者也拉进来一般展示给玩家的列表用 Immediate 就够了。遍历好友的代码如下。这里重点说 GetFriendGamePlayed 这个接口它返回的是一个 FriendGameInfo_t 结构体里面包含好友正在玩的游戏 ID、IP 端口和所在大厅。如果返回值是 false说明好友当前没在游戏中我们可以借此区分“在线但空闲”和“正在玩某个游戏”两种状态int32 FriendCount SteamFriends()-GetFriendCount(k_EFriendFlagImmediate); for (int32 i 0; i FriendCount; i) { CSteamID FriendID SteamFriends()-GetFriendByIndex(i, k_EFriendFlagImmediate); FString Name UTF8_TO_TCHAR(SteamFriends()-GetFriendPersonaName(FriendID)); EPersonaState State SteamFriends()-GetFriendPersonaState(FriendID); // State 取值: Offline / Online / Busy / Away / Snooze / LookingToTrade / LookingToPlay FriendGameInfo_t GameInfo; bool bPlaying SteamFriends()-GetFriendGamePlayed(FriendID, GameInfo); bool bInMyApp bPlaying (GameInfo.m_gameID.AppID() SteamUtils()-GetAppID()); FString PlayingName bInMyApp ? TEXT(正在玩本游戏) : UTF8_TO_TCHAR(GameInfo.m_rgchGameName); }拿回来的数据别直接塞给 UI建议包一层自己的数据结构比如 FSteamFriendInfo里面放 SteamID、昵称、状态 enum、游戏名、头像纹理引用再用 TArray 暴露给蓝图或者绑定到 UMG ListView。上面代码里我用了 UTF8_TO_TCHAR 宏这里必须提醒Steam 的字符串接口返回的都是 UTF-8 编码 const char*UE4 内部默认使用 TCHARWindows 上是 UTF-16不做转换会出现中文昵称乱码。转换一定要用 UTF8_TO_TCHAR而不是 ANSI_TO_TCHAR这是很多人第一次接 Steam 都会踩的字符编码坑。3.2 头像拉取与纹理转换好友头像这件事比想象中麻烦。Steam 的流程分为两步先用 GetSmallFriendAvatar / GetMediumFriendAvatar / GetLargeFriendAvatar 拿到一个整型头像句柄再用 SteamUtils()-GetImageRGBA 把这个句柄对应的图像数据拉到内存。三种尺寸分别是 32×32、64×64 和 184×184列表缩略图用 64 就够了。拿到原始字节后就要建立 UTexture2D。这中间有个大坑GetImageRGBA 名字里写 RGBA实际上返回的是 BGRA 顺序如果喂给纹理后颜色发蓝发红多半就是这里没处理。我有两个解决办法一是直接创建 PF_B8G8R8A8 格式的纹理省一次像素翻转二是手动交换 R 和 B 通道再走 PF_R8G8B8A8。推荐前者性能更好。核心代码UTexture2D* UMySteamManager::LoadAvatarAsTexture(CSteamID FriendID) { int32 AvatarHandle SteamFriends()-GetMediumFriendAvatar(FriendID); if (AvatarHandle 0) return nullptr; uint32 Width 0, Height 0; if (!SteamUtils()-GetImageSize(AvatarHandle, Width, Height)) return nullptr; TArrayuint8 RawData; RawData.SetNum(Width * Height * 4); if (!SteamUtils()-GetImageRGBA(AvatarHandle, RawData.GetData(), RawData.Num())) return nullptr; UTexture2D* AvatarTex UTexture2D::CreateTransient(Width, Height, PF_B8G8R8A8); if (!AvatarTex) return nullptr; FTexture2DMipMap Mip AvatarTex-PlatformData-Mips[0]; uint8* DestData (uint8*)Mip.BulkData.Lock(LOCK_READ_WRITE); FMemory::Memcpy(DestData, RawData.GetData(), RawData.Num()); Mip.BulkData.Unlock(); AvatarTex-UpdateResource(); return AvatarTex; }写在最后如果头像句柄返回 0不要慌大概率是对方 Steam 没有设置头像或者当前进程因为 AppID 不对导致头像服务不可用。返回 nullptr 后 UI 层要兜底显示占位图。3.3 邀请加入与富文本状态邀请好友进游戏有两种姿势一种是 ActivateGameOverlayToUser把 Steam 原生的“邀请”对话框弹到指定好友面前用户点完由 Steam 自己处理另一种是 InviteUserToGame直接给好友发一条带连接字符串的邀请消息好友那边点了之后会在他的客户端触发回调。个人测试下来联机项目更适合第二种因为连接字符串里可以带服务器 IP 或会话 ID方便自己控制进场逻辑。我用的是后者FString ConnectString FString::Printf(TEXT(connect 192.168.1.100:7777)); SteamFriends()-InviteUserToGame(FriendID, TCHAR_TO_UTF8(*ConnectString));同时要让好友那边看得懂你在干嘛就得设置富文本状态。SetRichPresence 可以设置任意自定义键值对但 Steam 的展示规则有些特殊steam_display 这个键对应的是 Steamworks 后台配置过的本地化 token 模板模板里可以引用其他键名的占位符。比如后台配置了一个 token 叫 #Status_CurrentMap文本内容是“正在地图 {map_name} 中战斗”代码里就要这么设SteamFriends()-SetRichPresence(steam_display, #Status_CurrentMap); SteamFriends()-SetRichPresence(map_name, TCHAR_TO_UTF8(*CurrentMapName)); SteamFriends()-SetRichPresence(status, playing);这里强调两点第一steam_display 的 token 一定要在 Steamworks 后台的“本地化字符串”里配好否则好友看到的是原始 token 名第二富文本状态有推送延迟实测从设置到好友端可见通常有 2 到 5 秒延迟这是 Steam 服务器做了聚合缓存不是你的 bug。3.4 回调机制与每帧泵送Steam 的 C API 使用回调机制好友接受邀请、好友状态变化、用户点击了“加入游戏”这些事件都会以回调对象的形式排队。问题在于这些回调不会自动分发你得在每帧调用 SteamAPI_RunCallbacks 把它们泵出来。OnlineSubsystemSteam 在引擎 Tick 里会处理自己注册的那部分回调但直接调用 SDD 注册的自定义回调必须自己泵。我在 PlayerController 或自建的 Manager 的 Tick 里加了一句void UMySteamManager::Tick(float DeltaTime) { SteamAPI_RunCallbacks(); }接下来是重中之重处理好友点击“加入游戏”的回调 GameRichPresenceJoinRequested_t。这个回调在你的进程里触发数据里带着你在 InviteUserToGame 时传入的连接字符串。收到之后用 ClientTravel 发起连接class FSteamFriendsCallbackHandler { STEAM_CALLBACK(FSteamFriendsCallbackHandler, OnJoinRequested, GameRichPresenceJoinRequested_t); }; void FSteamFriendsCallbackHandler::OnJoinRequested(GameRichPresenceJoinRequested_t* CallbackData) { if (CallbackData CallbackData-m_rgchConnect) { FString ConnectStr UTF8_TO_TCHAR(CallbackData-m_rgchConnect); // 转发给 GameInstance / PlayerController UMySteamManager::Get()-HandleJoinRequest(ConnectStr); } }顺便也要监听 PersonaStateChange_t好友头像、昵称、状态发生变化时它会触发用来实时刷新好友列表比定时轮询优雅得多。4. 实操记录双账号联调全过程4.1 本地联调环境怎么搭好友功能必须有两个 Steam 账号才能真正测起来。我的做法是账号 A 在开发机跑编辑器账号 B 在另一台机器跑打包版本两台机器处于同一局域网这样既能测邀请链路又能验证后续联机连接。如果你的网络环境不允许两台机器也可以考虑在一台机器上通过 Steam 客户端切换账号来分步验证但 Overlay 和加入游戏回调会受进程限制效果打折扣。还有一个容易被忽略的点用编辑器 PIE 模式跑 Demo 时Steam Overlay 是弹不出来的因为编辑器进程不是由 Steam 客户端直接拉起的。所以正式验证 Overlay 和加入邀请流程一定要用打包版本并设置成从 Steam 库中启动或者用命令行 -applaunch AppID 拉起。4.2 邀请闭环完整流程记录我的实际测试链路是这样的账号 A 在 Demo 里创建监听服务器设置富文本状态为“正在地图 Dust2 中战斗”账号 B 打开 Steam 好友列表看到 A 的状态点击“加入游戏”Steam 会拉起 B 的 Demo 进程如果没在运行的话进入进程后触发 GameRichPresenceJoinRequested_t回调里取出连接字符串调用 ClientTravelB 进入 A 的服务器同时 A 端收到 PersonaStateChange_t知道 B 上线了立刻刷新好友列表。整条链路从 B 点击到进服延迟约 3 到 6 秒主要耗时在 Steam 拉起进程和连接握手。这里要特别说一下 ClientTravel 的目标地址格式。如果服务器在局域网连接字符串是“connect 192.168.x.x:7777”如果是带会话的 Steam 联机还可以直接用大厅 ID 去 JoinLobby。Demo 为了简单我用了 IP 直连实际在线项目建议走 Steam 的会话接口因为 Steam 网络驱动会处理好 NAT 穿透的问题。4.3 实测中的参数细节这次测试我整理了三个有参考价值的细节。第一个是好友列表标志位的选择。k_EFriendFlagAll 会把关注者Follower也包括进来实测一个 Steam 老账号的关注者能有几千人直接卡 UI。演示用 k_EFriendFlagImmediate先保证功能闭环后面要做粉丝联动再单独走。第二个是头像尺寸与内存关系。185×185 的大头像一张图原始数据约 135KB如果是几百人的列表一次性全拉会导致明显的卡顿。我的做法是列表行先显示 64px 中头像点开详情页时再异步拉大头像。第三个是 RemoveRichPresence 的使用。玩家退出关卡或者断线时如果不显式清除富文本好友端会维持旧状态很久。在 GameInstance 的 Shutdown 里调用 SteamFriends()-ClearRichPresence()比重新塞一个空字符串规范实测清状态几乎立即生效。5. 常见问题排查与两个连带坑位5.1 问题速查表把演示过程中真正踩过、以及帮别人排查过的问题整理成一张表直接对照查现象根因解决思路SteamAPI_Init 返回 falseAppID 配置错误或 Steam 未启动检查 SteamDevAppId 与 steam_appid.txt确保 Steam 客户端已登录好友列表始终为空标志位用错或对方隐私设置改用 k_EFriendFlagImmediate让对方 Steam 资料里好友列表设为公开好友中文昵称乱码UTF-8 转 TCHAR 用了错误的宏统一用 UTF8_TO_TCHAR禁止 ANSI_TO_TCHAR头像贴图颜色怪异Steam 返回 BGRA 字节序纹理格式用 PF_B8G8R8A8或手动交换 R/BOverlay 弹不出来非 Steam 启动的编辑器进程打包版本从 Steam 库启动并确认后台启用了 OverlaySetRichPresence 好友看不到steam_display token 未后台配置Steamworks 后台配置本地化字符串键名要和代码一致好友加入请求不触发未调用 SteamAPI_RunCallbacks在 Tick 里每帧泵回调打包后找不到 DLL 崩溃缺少 steam_api64.dll从 SDK 的 redistributable_bin/win64 拷贝到打包目录5.2 外接设备映射的连带坑Demo 做到后半段我想让第二个玩家在本机用手柄或自制的 USB 外设控制角色方便单人模拟双人一起接受邀请结果在“外接设备映射”上又耗了半天。UE4 的默认输入系统对手柄支持很成熟但如果你用的是非标准的 USB 外设方向盘、街机摇杆、自研按键板事情就复杂了。最核心的问题是UE4 传统 Axis Mapping 无法区分同类型的多个外接设备插两个手柄时它们会共用一套映射你没法知道输入来自哪个设备。解决思路是启用 RawInput 插件直接在 Plugins 列表里勾选然后在项目设置里配置 VendorId 和 ProductId按设备实例读取原始输入报告再按 DeviceHandle 分发到不同角色。另一个坑和 Steam 本身有关Steam 客户端默认会对手柄做输入接管一旦 Steam Input 生效你项目里拿到的输入可能被 Steam 层重映射过。我在测试时发现手柄按键错乱最后是在 Steam 设置里把对应类型手柄的“控制器配置支持”选项关掉让 UE4 直接读取原始 XInput 数据问题才消失。如果你的 Demo 目标平台是独立的室内设备而不是 PC Steam 平台这一点尤其要注意。5.3 导入模型枢纽偏移处理邀请流程通了之后我想让好友进入关卡时用他的头像生成一个立牌展示就导入了几个外部 3D 模型结果又撞上“枢纽偏移”问题也就是模型导入 UE4 后 Pivot 点不在预期位置。现象非常典型在 Blender / 3ds Max 里模型底面中心是重心导入 UE4 后整个模型悬空或者嵌入地面枢纽点跑到模型角落。原因有几个一是 DCC 软件里模型带有额外的根节点或者父级节点有负 Scale、旋转导入时 UE4 把整个变换链的偏移一起带进来了二是模型单位不一致FBX 导出的单位是厘米但导入设置里勾选了错误的比例导致 Pivot 偏移被放大。最省事的处理是在 DCC 里把模型层级清理干净所有子节点原地清零变换把 Pivot 对齐到世界原点再导出。如果模型已经导入工程还可以在运行时做个兜底修正用包围盒中心反推偏移量FBoxSphereBounds Bounds MeshComp-CalcBounds(MeshComp-GetComponentTransform()); FVector PivotOffset Bounds.Origin - MeshComp-GetComponentLocation(); MeshComp-AddLocalOffset(-PivotOffset);这段代码的意思是把组件位置从“原点”挪到模型实际包围盒中心让立牌落在地面上。注意它只适合静态展示用的模型角色骨骼网格和物理资产的枢纽偏移得回到 DCC 里根治。如果模型是从网上下载的通用资源导入设置里建议把“Convert Scene Unit”按源软件的单位正确勾选并保留“Use T0 As Ref Pose”可以避免大部分比例和位移的问题。最后说点关于这套 Demo 的体会。整个项目实际开发不到一周但大头时间全花在三个地方回调泵送的机制理解、双账号联调的环境准备、还有那两件跟 Steam 本身无关的外围问题。如果重新来一次我会在第一天就盯着 SteamAPI_RunCallbacks 这个点不放因为绝大多数“接口明明调了却没反应”的诡异问题都是它引起的。调试回调时有个小技巧很值得一试在 SteamAPI_RunCallbacks 上下一个条件断点观察每一帧到底 dispatch 了什么类型的回调这比在业务代码里打日志高效得多因为你立刻能看到 Steam 层有没有把事件送进来从而快速定位问题在“进门”还是“进门之后”。这套链路跑通之后后面再接大厅匹配就顺理成章了。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门