简介:这份资源面向希望在虚幻引擎4中接入Steam好友系统的C++开发者,尤其是需要实现好友列表拉取、邀请与加入会话等联机功能的项目组。它通过一个可运行的演示工程,展示如何把Steam Friends API封装成蓝图可调用的节点与函数库,解决UE4原生蓝图难以直接处理Steam异步回调与网络等待任务的问题。压缩包共7个文件,包含3个.h头文件、3个.cpp实现文件以及1个md说明文档,整体约7KB,体量轻巧但结构完整。其中GetFriendsListCallBackProxy以蓝图节点形式封装好友列表获取,可执行网络等待任务;NetBlueprintFunctionLibrary提供邀请好友的标准函数库;NetGameInstance则负责在邀请被接受后加入对应会话。已有311人学习,适合具备一定C++与UE4基础、想快速理解Steam联机模块集成思路的开发者参考。
1. 从一次联机大厅翻车说起:这套 UE4 集成 Steam Friends 的演示到底能干什么
去年帮一个做独立联机小游戏的朋友看项目,他们用 UE4 做了一套大厅系统,玩家能建房、能进房,但好友列表死活拉不出来,邀请好友的按钮点下去像石沉大海。排查了两天,最后发现是 Steam 的初始化时机和回调绑定顺序全错了——这类问题在 UE4 里太常见,因为引擎自带的 Online Subsystem 封装了一层,很多人只调了接口,没搞懂底层 Steamworks 的回调是怎么被驱动起来的。这份 SteamFriendsUE4 演示就是冲着这个场景来的:它把 UE4 里集成 Steam Friends API 的完整链路拆开,从初始化、好友列表拉取、富状态展示到邀请与加入,全部用 C++ 写清楚。适合正在做联机功能、被 Steam 好友系统卡住的 UE4 开发者,也适合想搞明白 Online Subsystem 和 Steamworks 之间那层“黑匣子”的人。它不教你做游戏,只教你把这套好友系统接对、接稳。
2. 集成前的底层账:Steamworks、Online Subsystem 与 UE4 的三层关系
2.1 为什么不能直接调 Steamworks 的 C++ 接口
Steamworks SDK 本身提供的是纯 C++ 接口,比如ISteamFriends、ISteamUser这些。理论上你可以在 UE4 里直接 include 头文件然后调,但实际项目里几乎没人这么干,原因有三个。第一,Steamworks 的回调机制依赖SteamAPI_RunCallbacks()被周期性调用,UE4 有自己的 Tick 和线程模型,你得自己找地方塞进去,塞错了就是回调永远不触发。第二,UE4 的 Online Subsystem 已经帮你把好友、会话、成就这些抽象成了统一接口,直接调 Steamworks 等于绕过了引擎的会话管理和网络层,后面做跨平台或者换平台时全部要重写。第三,Steam 的 Overlay 和输入系统跟 UE4 的 Slate UI 有交互,直接调底层容易出焦点和输入冲突。
所以常见做法是走 UE4 的 Online Subsystem Steam 模块,通过IOnlineSubsystem::Get()拿到接口,再调IOnlineFriends、IOnlineSession这些。这份演示的核心价值就在于,它把这条链路上每个环节的 C++ 调用都摆出来了,你能看到从GetFriendsInterface()到ReadFriendsList()再到OnReadFriendsListComplete回调的完整流程。
2.2 环境准备与模块依赖配置
在动手之前,先把工程配置理清楚。UE4 项目要启用 Online Subsystem Steam,需要在.uproject和Build.cs里做对应声明。下面是我一般会走的步骤。
第一步,确认引擎版本和 Steamworks SDK 版本匹配。UE4 4.27 和 UE5 早期版本对 Steamworks 的封装略有差异,演示里用的是 4.27 的接口风格,如果你在 UE5 上跑,部分头文件路径要调整。
第二步,修改.uproject,在Plugins数组里启用 Online Subsystem Steam:
{ "FileVersion": 3, "EngineAssociation": "4.27", "Plugins": [ { "Name": "OnlineSubsystemSteam", "Enabled": true }, { "Name": "OnlineSubsystem", "Enabled": true } ] }第三步,在项目的Build.cs里加入模块依赖:
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "OnlineSubsystem", "OnlineSubsystemUtils" }); // Steam 模块按需动态加载,避免非 Steam 平台编译报错 DynamicallyLoadedModuleNames.Add("OnlineSubsystemSteam");这里有个细节:OnlineSubsystemSteam建议放在DynamicallyLoadedModuleNames而不是PublicDependencyModuleNames,因为它是运行时按平台加载的。如果你直接写进 Public 依赖,在非 Steam 环境下编译可能找不到对应模块。这个坑我踩过,当时在 Android 打包时直接编译失败,查了半天才发现是模块加载方式的问题。
第四步,配置DefaultEngine.ini,指定默认在线子系统:
[/Script/Engine.GameEngine] +NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemUtils.IpNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver") [OnlineSubsystem] DefaultPlatformService=Steam [OnlineSubsystemSteam] bEnabled=true SteamDevAppId=480SteamDevAppId=480是 Valve 提供的测试用 App ID,正式项目要换成你自己的。用 480 测试的好处是不需要额外申请就能跑通好友和会话功能,但注意它不支持某些正式 App 才有的特性,比如自定义富状态。
2.3 初始化时机:为什么 GameInstance 比 Actor 更合适
Steam 的初始化必须在游戏逻辑开始之前完成,而且要保证只初始化一次。放在GameInstance的Init()里是最稳妥的,因为 GameInstance 的生命周期覆盖整个游戏运行期,不会因为关卡切换被销毁重建。
// MyGameInstance.h #pragma once #include "CoreMinimal.h" #include "Engine/GameInstance.h" #include "Interfaces/OnlineSessionInterface.h" #include "MyGameInstance.generated.h" UCLASS() class STEAMFRIENDSDEMO_API UMyGameInstance : public UGameInstance { GENERATED_BODY() public: virtual void Init() override; virtual void Shutdown() override; IOnlineFriendsPtr GetFriendsInterface() const; private: IOnlineSubsystem* OnlineSubsystem; IOnlineFriendsPtr FriendsInterface; };// MyGameInstance.cpp #include "MyGameInstance.h" #include "OnlineSubsystem.h" #include "OnlineSubsystemUtils.h" void UMyGameInstance::Init() { Super::Init(); // 获取默认在线子系统,此时应已由引擎根据 ini 配置加载 Steam 模块 OnlineSubsystem = IOnlineSubsystem::Get(); if (OnlineSubsystem) { FriendsInterface = OnlineSubsystem->GetFriendsInterface(); UE_LOG(LogTemp, Log, TEXT("Online Subsystem: %s"), *OnlineSubsystem->GetSubsystemName().ToString()); } else { UE_LOG(LogTemp, Error, TEXT("Online Subsystem 未初始化,检查 DefaultEngine.ini 配置")); } } void UMyGameInstance::Shutdown() { FriendsInterface.Reset(); OnlineSubsystem = nullptr; Super::Shutdown(); }这段代码的逻辑是:在Init()里拿到子系统实例和好友接口指针,在Shutdown()里释放。注意IOnlineFriendsPtr是共享指针,不需要手动 delete。参数方面,IOnlineSubsystem::Get()不带参数时返回 ini 里配置的默认平台,你也可以传"Steam"强制指定,但那样在非 Steam 环境下会返回空。
提示:如果你在编辑器里跑,确保 Steam 客户端已经登录,否则
IOnlineSubsystem::Get()可能返回空或者子系统名不是 Steam。
3. 好友列表拉取与富状态展示:从 ReadFriendsList 到 OnReadFriendsListComplete
3.1 异步回调模型:为什么 ReadFriendsList 不能立刻拿结果
UE4 的 Online Friends 接口是异步的。你调ReadFriendsList()只是发起请求,真正的数据要等 Steam 后端返回后,通过OnReadFriendsListComplete委托回调给你。这个模型跟直接调 Steamworks 的ISteamFriends::GetFriendCount()完全不同,后者是同步的,但 UE4 为了跨平台统一,全部包成了异步。
void UMyGameInstance::RequestFriendsList() { if (!FriendsInterface.IsValid()) { UE_LOG(LogTemp, Warning, TEXT("FriendsInterface 无效,无法拉取好友列表")); return; } // 绑定回调,注意先解绑再绑定,避免重复触发 FriendsInterface->ClearOnReadFriendsListCompleteDelegate(OnReadFriendsListCompleteDelegateHandle); OnReadFriendsListCompleteDelegateHandle = FriendsInterface->AddOnReadFriendsListCompleteDelegate_Handle( FOnReadFriendsListComplete::CreateUObject(this, &UMyGameInstance::OnReadFriendsListComplete) ); // 参数:本地用户索引、好友列表类型、是否只读在线好友 bool bSuccess = FriendsInterface->ReadFriendsList(0, EFriendsLists::Default, TEXT("")); if (!bSuccess) { UE_LOG(LogTemp, Error, TEXT("ReadFriendsList 调用失败")); } } void UMyGameInstance::OnReadFriendsListComplete( int32 LocalUserNum, bool bWasSuccessful, const FString& ListName, const FString& ErrorStr) { if (!bWasSuccessful) { UE_LOG(LogTemp, Error, TEXT("好友列表拉取失败: %s"), *ErrorStr); return; } // 拉取成功后遍历好友 TArray<TSharedRef<FOnlineFriend>> Friends; if (FriendsInterface->GetFriendsList(0, ListName, Friends)) { for (const auto& Friend : Friends) { FString DisplayName = Friend->GetDisplayName(); FString RealName = Friend->GetRealName(); bool bIsOnline = Friend->GetPresence().bIsOnline; FString RichPresence = Friend->GetPresence().GetStatusString(); UE_LOG(LogTemp, Log, TEXT("好友: %s | 在线: %d | 状态: %s"), *DisplayName, bIsOnline, *RichPresence); } } }逻辑说明:ReadFriendsList的第一个参数是本地用户索引,单机游戏传 0 就行;第二个参数EFriendsLists::Default表示默认好友列表,Steam 平台下就是全部好友;第三个参数是列表名过滤,传空字符串表示不过滤。回调里通过GetFriendsList拿到TArray<TSharedRef<FOnlineFriend>>,每个元素包含显示名、真实名、在线状态和富状态字符串。
参数方面,GetPresence().GetStatusString()返回的就是 Steam 富状态,比如“正在玩 XXX 游戏”。这个字符串的格式取决于玩家在 Steam 客户端里设置的状态,你可以在游戏里通过SetRichPresence自定义。
3.2 富状态自定义:SetRichPresence 的键值对怎么用
Steam 的富状态本质是一组键值对,比如status、game、steam_display这些。UE4 的IOnlineFriends接口提供了SetRichPresence方法,但不同引擎版本支持程度不一样。4.27 里可以直接调:
bool UMyGameInstance::SetMyRichPresence(const FString& Key, const FString& Value) { if (!FriendsInterface.IsValid()) { return false; } // 参数:本地用户索引、键、值 bool bSuccess = FriendsInterface->SetRichPresence(0, Key, Value); if (bSuccess) { UE_LOG(LogTemp, Log, TEXT("富状态设置成功: %s = %s"), *Key, *Value); } return bSuccess; }常见做法是设置status键为“在大厅中”或“在游戏中”,然后 Steam 客户端会把这个状态展示给你的好友。注意富状态的键名有长度限制,一般不超过 64 个字符,值不超过 256 个字符。超长会被截断,而且不会有报错,这个坑很隐蔽。
注意:富状态更新不是实时的,Steam 后端有缓存,通常几秒到十几秒才同步到好友那边。如果你做的是快节奏游戏,别指望用它做实时状态同步。
3.3 好友邀请与加入:Session 接口的配合
好友列表拉出来之后,下一步就是邀请好友加入游戏。这部分要跟IOnlineSession配合,流程是:创建会话、设置会话可加入、通过好友接口发送邀请、好友接受后加入会话。
void UMyGameInstance::InviteFriendToSession(const FUniqueNetId& FriendId) { IOnlineSessionPtr SessionInterface = OnlineSubsystem->GetSessionInterface(); if (!SessionInterface.IsValid()) { return; } // 获取当前会话名,通常是 GameSession FName SessionName = NAME_GameSession; // 通过 Session 接口发送邀请 bool bSuccess = SessionInterface->SendSessionInviteToFriend(0, SessionName, FriendId); if (bSuccess) { UE_LOG(LogTemp, Log, TEXT("邀请已发送")); } else { UE_LOG(LogTemp, Warning, TEXT("邀请发送失败,检查会话是否存在且可加入")); } }这里的关键参数是SessionName,必须跟你创建会话时用的名字一致,否则找不到会话。另外,会话必须设置了bAllowInvites和bAllowJoinViaPresence为 true,否则邀请发不出去。这两个标志在创建会话时通过FOnlineSessionSettings设置。
4. 避坑与排查:Steam Friends 集成里最容易翻车的五个点
4.1 现象:好友列表始终为空,回调也不触发
原因:最常见的是 Steam 客户端没启动或者没登录,导致IOnlineSubsystem::Get()返回的不是 Steam 子系统。另一个可能是SteamDevAppId配置错误,Steam 后端拒绝请求。
解决:先在Init()里打印子系统名称确认是 Steam,再检查 Steam 客户端是否在线。如果用的是 480 测试 App ID,确认没有其他程序占用同一个 App ID。
4.2 现象:回调触发多次,好友列表重复刷新
原因:每次调ReadFriendsList都绑定了新的委托,但没有清理旧的。UE4 的委托句柄是累加的,不手动 Clear 就会重复触发。
解决:在绑定前先调ClearOnReadFriendsListCompleteDelegate,把之前的句柄清掉。我一般会把句柄存成成员变量,每次绑定前清理。
4.3 现象:打包后好友功能失效,编辑器里正常
原因:打包时OnlineSubsystemSteam模块没有被正确加载,或者DefaultEngine.ini没有被打进包。另一个常见原因是 Steamworks 的 DLL 没有放到打包目录。
解决:检查打包日志里有没有OnlineSubsystemSteam的加载记录,确认Config目录下的 ini 文件被正确打包。Steamworks 的steam_api64.dll要放在可执行文件同级目录。
4.4 现象:富状态设置成功但好友看不到
原因:富状态的键名不符合 Steam 的显示规则。Steam 客户端只展示特定键,比如status、steam_display,你自定义的键如果没有在 Steamworks 后台配置对应的本地化文本,就不会显示。
解决:优先用status键,值用英文或本地化字符串。如果要做复杂的富状态展示,需要在 Steamworks 后台的“富状态”配置里定义本地化规则。
4.5 现象:邀请发送成功但好友收不到
原因:会话没有设置为可加入,或者好友的 Steam 客户端设置了“勿扰”模式。另一个可能是会话的bAllowInvites为 false。
解决:检查创建会话时的FOnlineSessionSettings,确保bAllowInvites = true、bAllowJoinViaPresence = true、bUsesPresence = true。这三个标志缺一不可。
5. 进阶技巧:用控制台命令和日志验证好友系统是否真的通了
调试 Steam Friends 最头疼的是“看起来调用了但没效果”。我一般会在项目里加几个控制台命令,直接手动触发好友列表拉取和邀请,配合日志输出,能快速定位问题出在哪一层。
// 在 GameInstance 或 PlayerController 里注册控制台命令 static FAutoConsoleCommandWithWorldAndArgs DebugFriendsCmd( TEXT("debug.Friends"), TEXT("调试好友系统: debug.Friends list | invite <index> | presence <key> <value>"), FConsoleCommandWithWorldAndArgsDelegate::CreateLambda( [](const TArray<FString>& Args, UWorld* World) { if (Args.Num() == 0) return; UMyGameInstance* GI = Cast<UMyGameInstance>(World->GetGameInstance()); if (!GI) return; if (Args[0] == TEXT("list")) { GI->RequestFriendsList(); } else if (Args[0] == TEXT("presence") && Args.Num() >= 3) { GI->SetMyRichPresence(Args[1], Args[2]); } } ) );这个命令的用法是:在游戏里按~打开控制台,输入debug.Friends list触发好友列表拉取,输入debug.Friends presence status "在大厅"设置富状态。配合UE_LOG的输出,你能看到每一步的返回值。
验证方法上,我习惯分三层查:第一层看IOnlineSubsystem::Get()返回的子系统名是不是 Steam;第二层看ReadFriendsList的返回值是不是 true;第三层看回调里的bWasSuccessful和ErrorStr。三层都过了,好友列表基本就能出来。如果第三层报错,ErrorStr通常会告诉你具体原因,比如“Steam 未初始化”或者“App ID 无效”。
还有一个技巧是用 Steam 客户端的“好友与聊天”窗口对照。你在游戏里拉取到的好友列表,应该跟 Steam 客户端里看到的一致。如果数量对不上,检查ReadFriendsList的第三个参数是不是传了过滤条件。
从那以后我每次接 Steam 好友系统,都强制走一遍“子系统名 → 接口有效性 → 回调返回值 → 客户端对照”这四步,少一步都可能漏掉一个隐蔽的配置问题。希望帮到你。
本文还有配套的精品资源,点击获取