资讯动态

UE4 Steam好友系统集成实战:从OnlineSubsystem到好友邀请

发布时间:2026/9/30 18:32:45 来源:尧图企业网站定制
简介面向有C和UE4基础、正在接入Steam好友功能的开发者这份演示资源用精简源码展示了好友交互链路。工程内三个关键类分工明确好友列表回调代理把异步网络等待封装成蓝图节点负责从Steam子系统获取好友列表网络蓝图函数库作为标准蓝图函数库对外提供邀请好友的接入入口网络游戏实例则作为自定义游戏实例类在好友接受邀请后自动加入对应会话。三个模块从读取好友列表、发送邀请到邀请接受后进入会话基本覆盖了Steam好友交互的主要流程这种基于异步等待与蓝图调用的写法有助于理解UE4中Steam子系统与蓝图节点、函数库、游戏实例之间的协作方式。资源共7个文件包含3个头文件、3个C源文件和1个说明文档压缩包仅7KB头文件负责接口声明源文件负责逻辑实现说明文档用于快速上手。目前已有310人学习适合想验证Steam Friends API集成流程并迁移到实际项目的中级UE4开发者。1. 先看清 Steam Friends API 在 UE4 里的真实形状大部分人对 Steam Friends 集成的第一反应是翻出 SteamSDK 的 ISteamFriends 一顿操作最后发现回调不进、头像不显示、邀请点了没反应。UE4 里这事儿的正确打开方式是走 OnlineSubsystem 的 IOnlineFriends 接口它把 Steam、Xbox、Epic 的好友系统统一成一套 C 接口你不用再对着 SteamSDK 的 C 风格函数手写回调。这份演示项目解决的就是这么一个问题用 C 把好友列表、好友状态、头像一个不落搬进 UI并且能从工程里直接发起邀请。适合那些要在大厅、房间、社交系统里接入 Steam 好友的 UE4 开发者也适合刚接触 OnlineSubsystem 想找一个完整链路的人。本文会从环境初始化一路讲到双客户端实测重点放在那些文档没说透的坑上。2. 环境初始化OnlineSubsystemSteam 与开发者模式两道坎2.1 Steam 封装层你实际打交道的不是 SteamSDKUE4 里接 Steam 好友官方推荐路径是 OnlineSubsystemSteam 插件它把 Steam 的 C API 包成了 UE 的接口层。好友相关功能集中在 IOnlineFriendsPresence 相关在 IOnlinePresenceSession 相关在 IOnlineSession。三个接口各管一摊但交互联动很频繁——邀请好友进游戏就是 Session 和 Friends 协作的结果。先看工程配置文件这个是所有功能跑起来的前提[OnlineSubsystem] DefaultPlatformServiceSteam [OnlineSubsystemSteam] bEnabledtrue SteamDevAppId480 [/Script/OnlineSubsystemSteam.SteamNetDriver] NetConnectionClassNameOnlineSubsystemSteam.SteamNetConnection这里有几个参数在文档里容易看漏。DefaultPlatformService决定整个工程默认走哪个子系统写成 Steam 后你调用Online::GetSubsystem()拿到的就是 Steam 实现。SteamDevAppId是开发阶段的 AppID我用的是 480这是 Spacewar 的公开测试 AppID好处是本地能看到任意 Steam 账号的数据坏处是它不代表你的正式游戏。NetConnectionClassName这行不能删好友邀请拉起 Session 后P2P 连接要靠它建立。注意一个细节SteamDevAppId只在没有 steam_appid.txt 时生效。实际工程里我更推荐用文件方式原因下一节说。2.2 开发者模式与 steam_appid.txt为什么列表永远是空的第一次跑这个演示工程最常见的现象是日志里没有任何报错但好友列表读出来长度为 0。排查一圈发现 Steam 的 AppID 没对上当前运行实例。UE4 的 OnlineSubsystemSteam 初始化时会先找项目根目录下的 steam_appid.txt找不到才回退到配置里的SteamDevAppId。项目根目录指的是.uproject所在目录不是 Content 目录。新建一个文本文件改名为 steam_appid.txt内容一行# 项目根目录下的 steam_appid.txt内容只有一行 # 480 是 Steam 官方公开测试 AppIDSpacewar 480注意文件不要带 BOM不要有多余空行。Windows 下用记事本保存容易踩这个坑保存时编码选 UTF-8 无 BOM。文件存在后即使你本地没跑 Steam 客户端UE4 也能以开发者模式初始化 SteamAPI代价是好友列表、头像这些需要登录态的功能拿不到真实数据。我一般会同时开一个 Steam 客户端并登录测试账号这样拿到的才是完整数据。日志里出现SteamAPI_Init() succeeded不代表登录成功它只代表 SDK 初始化完成。真正的登录态要看登录回调这个在 UE4 里是异步的后面代码会讲到。2.3 C 侧获取 IOnlineFriends 的正确时机拿到 Friends 接口本身不难难在时机。Steam API 初始化是异步的如果在模块启动 Immediately 就取指针很可能拿到空对象。这个演示工程把初始化放在 GameInstance 的Init()之后这时候子系统已经就绪。// FriendsDemo.h #pragma once #include CoreMinimal.h #include OnlineSubsystem.h #include Interfaces/OnlineFriendsInterface.h #include Interfaces/OnlinePresenceInterface.h class FFriendsDemo { public: void Init(); void Shutdown(); private: TSharedPtrIOnlineFriends, ESPMode::ThreadSafe FriendsInterface nullptr; TSharedPtrIOnlinePresence, ESPMode::ThreadSafe PresenceInterface nullptr; FDelegateHandle ReadFriendsListHandle; void OnReadFriendsListComplete( int32 LocalUserNum, bool bWasSuccessful, const FString ListName, const FString ErrorStr); };把两个接口的指针和委托句柄一起收在类里原因很实际委托绑定后如果类先销毁而回调后到就是野指针崩溃。保存FDelegateHandle是为了在Shutdown()里能精确解绑。常见做法是在Shutdown()里判断FriendsInterface.IsValid()后调用ClearOnReadFriendsListCompleteDelegate传回句柄。初始化代码里不要直接ReadFriendsList先确认Online::GetSubsystem(GetWorld())非空。Steam 没装、AppID 不对、网络异常时这个指针会为 null空指针上调用接口是静默失败没有任何日志这也是排查日志里最让人迷惑的地方。3. 好友列表与状态把 UOnlineFriend 变成 UI 能吃的数据3.1 ReadFriendsList 回调链名字、列表类型与分页好友列表读取是异步的核心方法是ReadFriendsList它有三个参数本地用户编号、列表名、回调委托。UE4 把好友列表按场景区分默认是EFriendsLists::DefaultSteam 上还有一个EFriendsLists::InGame只返回正在玩同一款游戏的好友。做大厅邀请时更常用后者。// FriendsDemo.cpp #include FriendsDemo.h #include OnlineSubsystem.h #include OnlineSessionInterface.h void FFriendsDemo::Init() { const IOnlineSubsystem* Subsystem Online::GetSubsystem(); if (Subsystem nullptr) { UE_LOG(LogTemp, Error, TEXT(OnlineSubsystem is null, check steam_appid.txt)); return; } FriendsInterface Subsystem-GetFriendsInterface(); PresenceInterface Subsystem-GetPresenceInterface(); if (FriendsInterface.IsValid()) { ReadFriendsListHandle FriendsInterface-AddOnReadFriendsListCompleteDelegate_Handle( FOnReadFriendsListComplete::CreateRaw( this, FFriendsDemo::OnReadFriendsListComplete)); // 读取默认好友列表LocalUserNum 在单机登录场景下通常为 0 FriendsInterface-ReadFriendsList( 0, EFriendsLists::ToString(EFriendsLists::Default)); } } void FFriendsDemo::OnReadFriendsListComplete( int32 LocalUserNum, bool bWasSuccessful, const FString ListName, const FString ErrorStr) { if (!bWasSuccessful) { UE_LOG(LogTemp, Error, TEXT(ReadFriendsList failed: %s), *ErrorStr); return; } TArrayTSharedRefFOnlineFriend Friends; FriendsInterface-GetFriendsList(LocalUserNum, ListName, Friends); for (const TSharedRefFOnlineFriend Friend : Friends) { const FString DisplayName Friend-GetDisplayName(); const FUniqueNetIdPtr UserId Friend-GetUserId(); UE_LOG(LogTemp, Log, TEXT(Friend: %s (%s)), *DisplayName, *UserId-ToString()); } }这段代码的关键在回调时机AddOnReadFriendsListCompleteDelegate_Handle注册的是完成通知不是数据就绪通知。回调触发时你要主动调GetFriendsList把结果拉回来结果缓存在子系统内部。ListName参数要与读取时传入的字符串完全一致否则取出来是空数组。另外一个容易翻车的地方LocalUserNum在分屏多人游戏里才有意义PC 单账号场景固定传 0。但回调返回的也是这个编号你要在回调里校验它和发起请求时一致。Steam 双开测试时这个值偶尔会错乱校验一下能省大量排查时间。3.2 UOnlineFriend 字段与头像加载别指望自带 URLFOnlineFriend数据类里能拿到的字段如下其中头像和状态是 UI 显示最常踩坑的部分。字段获取方法坑点玩家昵称GetDisplayName()Steam 上返回的是账户上的昵称不是当前改的备注真实姓名GetRealName()需要好友授权拿不到是空字符串好友唯一 IDGetUserId()64 位 Steam ID转字符串时注意精度游戏状态GetGamePlayedInfo()/GetPresence()需要 Presence 查询默认不推送头像GetAvatarUrl()早期封装经常返回空要自己拼 CDN 地址头像是个老问题。UE4 的 OnlineSubsystemSteam 里GetAvatarUrl实现不稳定4.2x 某些版本会返回空字符串。我一般不用它而是走 SteamSDK 的原生接口拿头像 hash再拼出 Steam 官方头像 CDN 的完整地址然后用 UE 的纹理加载任务异步拉取。// 伪代码示意从好友 ID 拿头像 hash 并拼 URL // Steam 通过 ISteamFriends 原生接口获取UE 封装没有暴露这个能力 #include steam/steam_api.h ISteamFriends* SteamFriends SteamFriends(); if (SteamFriends nullptr) return; // GetSmallFriendAvatar 返回的是头像 ID不是 URL int AvatarID SteamFriends-GetMediumFriendAvatar(UserId-AsShared().Get()); if (AvatarID 0) { // 0 表示用户还没设置头像 return; } // 正确做法是从头像 ID 反查 hash char AvatarHash[34]; if (SteamFriends-GetAvatarHash(AvatarID, AvatarHash, sizeof(AvatarHash))) { // 拼接规则取 hash 前两位作为子目录后半段作为文件名 FString AvatarPath FString::Printf( TEXT(/public/images/avatars/%s/%s.jpg), *FString(AvatarHash).Left(2), *FString(AvatarHash)); }这里最容易被误导的是GetSmallFriendAvatar返回的 int 值很多人以为这就是头像资源句柄直接拿去创建纹理结果全是黑色贴图。它只是头像索引必须通过GetAvatarHash转成 hash 字符串。拿到 URL 后加载纹理是另一套异步流程建议用一个独立的UAsyncTaskDownloadImage派生类管理不要每一帧都创建新任务。3.3 状态同步Presence 事件要注册不是轮询好友的游戏状态、在线状态走的是 Presence 系统它和好友列表数据是两套。列表只告诉你“谁在好友名单里”Presence 才告诉你“那家伙现在在干嘛”。演示工程里这个区分很典型ReadFriendsList成功不等于状态就有数据必须单独走一遍 Presence 查询。// 在 Init 中追加 Presence 回调注册 if (PresenceInterface.IsValid()) { // 有新 Presence 数据到达时触发比如好友上线、进游戏 PresenceInterface-AddOnPresenceReceivedDelegate_Handle( FOnPresenceReceived::CreateRaw( this, FFriendsDemo::OnPresenceReceived)); // 主动查询好友状态只查一次后续靠事件驱动 PresenceInterface-QueryPresence( 0, *UserIds[0]); } void FFriendsDemo::OnPresenceReceived( const FUniqueNetId UserId, const TSharedRefFOnlineUserPresence Presence) { if (Presence-bIsOnline) { UE_LOG(LogTemp, Log, TEXT(Friend is online: %s), *Presence-Status.StatusStr); } // bIsInGame 表示正在游玩 // bIsPlayingThisGame 表示正在玩本工程对应的游戏 if (Presence-bIsPlayingThisGame) { // 可以在这里拉取对方的 Session 信息用于加入 } }关键区别在这OnPresenceReceived是事件触发好友状态一变就广播而QueryPresence是主动拉取一次调用只管一次。正确的架构是启动时 Query 一次拉全量后续靠事件增量更新。不要每帧调 QueryPresenceSteam 服务器会限流高频请求会被静默丢弃。事件回调里bIsOnline和bIsPlayingThisGame是两个独立判断别混。前者表示好友在线后者表示好友正在玩你这个游戏——只有后者为真时你才应该显示“加入游戏”按钮。有些工程只判断了在线状态非 Steam 好友加了个 URL 标记的假状态直接让加入按钮失效这在演示里是个隐藏 bug。4. 好友交互与排查邀请失效、头像黑块、状态不同步4.1 四个高频翻车现象与处理下面这四条是这个演示工程在真实设备上最常见的故障按出现频率排序。现象一好友列表读取成功但长度为 0原因steam_appid.txt里的 AppID 与当前运行游戏不一致。本地调试时 UE4 编辑器用的是项目文件指定的 AppID打包后的 exe 用的是编译时的 AppID两者不一致时列表为空且无报错。解决确认文件在项目根目录AppID 写入正确打包后检查 exe 同级目录是否也放了一份 steam_appid.txt。现象二头像全是黑色纹理原因直接用GetSmallFriendAvatar返回值创建纹理或GetAvatarUrl返回空之后没有 fallback。解决走GetAvatarHash拼 CDN 地址异步下载完成后设置图片控件。头像加载是异步任务不要在回调外直接用返回值填充 UI。现象三好友一直在玩但状态显示离线原因只ReadFriendsList没QueryPresence。好友列表数据里包含的在线状态是列表快照不会自动更新。解决初始化时对每个好友 ID 调一次QueryPresence再注册OnPresenceReceived接收后续变化。注意QueryPresence要等好友列表读完之后再逐个调列表还没回就直接查拿到的 ID 是无效的。现象四点击邀请按钮没有任何反应原因邀请发到了 Session但当前没创建任何 Session或 Overlay 在开发者模式下被禁用。解决先创建或加入一个 Session再调邀请接口。还有一个导致无反应的常见点Steam Overlay 没被启用。工程构建配置里需要勾选 “Enable Steam Overlay”本地编辑器调试时 Overlay 通常不弹出但邀请逻辑已经下发好友端能看到 P2P 连接请求。4.2 日志怎么看UE 的 Log 与 Steam 的 Log 分开读排错第一步是分清日志来源。UE4 侧的分类是LogOnline、LogOnlineSession、LogNetSteam 侧是 SDK 自带的steam_*.log。两者在工程Saved/Logs目录下都有文件名不同。日志关键字出现的意图含义SteamAPI_Init succeeded启动阶段SteamSDK 初始化成功不代表登录成功Logged in as/Login completeStartPlayFab 后登录成功此时才能读好友Login failure登录失败AppID 错误或 Steam 客户端未运行ReadFriendsList Complete读取回调列表读取完成数据在内存需主动 GetOSS: GetFriendsList数据访问打印好友数量为 0 时排查 AppIDPresence: QueryPresence 查询状态数据返回对应回调触发这里有个实操建议LogOnline的冗余日志量很大建议在工程DefaultEngine.ini里把 Online 日志的 Verbosity 调成 Log 而不是 VeryVerbose否则 Steam 的网络协商日志会淹没真正有用的登录信息。[Core.Log] LogOnlineLog LogOnlineSessionLog LogNetLog打包后的游戏想看在线日志-LogCmds是后悔药启动参数加-log -LogCmdsLogOnline Verbose就能在发布版里看到完整的好友交互日志不用重新编译。这个技巧在定位“只有正式环境才出现”的问题时特别管用。4.3 双客户端测试必要但与直觉相反的约束单开编辑器测好友功能永远测不出完整链路。正确姿势是双实例一个编辑器实例登录 A 账号另开一个非编辑器实例登录 B 账号A 邀请 BB 接受邀请。这里有两个反直觉的点。第一个是 AppID 必须完全一致。A 用 480、B 用自己游戏的正式 AppID两边的好友列表能看到彼此但邀请时 Session 握手会失败因为AppID不匹配时 Steam 拒绝建立 P2P 通道。第二个是双开编辑器会被 Steam 限制同一机器上两个编辑器实例共用同一个 Steam 客户端登录态会相互踢掉。稳妥的做法是编辑器实例加-NoSteam参数跑纯逻辑另一个用打包出来的 exe 登录真实客户端来测。-NoSteam有个代价不初始化 SteamSDK好友读取全空。所以别指望它来测好友功能它只能用来验证游戏逻辑不依赖 Steam 也能跑。想完整测就准备两台机器或者一台机器加一台云主机同一 AppID 不同账号这个坑我踩了两天才反应过来现在每次搭测试环境都先核对 AppID。5. 验证与进阶双客户端闭环与 10 分钟自检清单演示工程跑通后推荐按下面这个闭环验证流程走一遍这套流程能在 10 分钟内暴露 90% 的集成问题。先准备两个 Steam 账号互为好友一台机器跑打包后的游戏 exe 登录 A 账号另一台机器跑相同 exe 登录 B 账号。A 打开好友列表确认能看到 B 的昵称和状态B 打开好友列表确认能看到 A。A 发起游戏邀请B 端弹出邀请弹窗点击接受后两边都进入同一 Session能正常连接和传输数据。这五步每一步都有明确的观察点第一步看 AppID 日志第二步看 Presence 回调第三步看邀请是否到达好友端第四步看 Session 连接是否建立第五步看 NetDriver 心跳是否正常。哪一步断了就回到第 4 章的对应条目排查。进阶用法是把邀请信息和 Session 数据绑定。好友邀请弹窗默认只显示“邀请你玩游戏”这种通用文案如果你能拿到邀请方的 Session 信息就可以直接显示房间名和人数。// 通过 ISteamFriends 原生活动邀请时带上房间信息 // 这里用 SteamFriends 的 ActivateGameOverlayInviteDialog 会带邀请弹窗 // 但更受控的方式是自己维护一个邀请数据结构 struct FGameInviteData { FString RoomName; int32 CurrentPlayers; int32 MaxPlayers; FString HostUserId; };把这组数据塞进邀请消息的user_data字段好友端解析出来再渲染体验比默认弹窗好得多。但这个只对同一 AppID 生效跨游戏邀请走的是 Overlay 原生流程自定义数据会被 Steam 忽略。从那以后我每次接 Steam Friends 都强制走一遍这套自检AppID 一致性、初始化日志、列表读取、Presence 查询、Session 握手。五条过完功能不跑通的情况基本不存在。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑