简介这份演示工程围绕在 UE4 中接入 Steam Friends API 展开面向已具备 C 与蓝图基础、希望实现好友列表读取与邀请联机流程的开发者可用于理解 Steam 子系统调用、异步回调与会话加入之间的衔接方式。压缩包共 7 个文件以 3 个 .h 头文件、3 个 .cpp 源文件和 1 个 README.md 为主整体约 7KB代码规模精简便于快速定位关键实现与目录结构。工程中 GetFriendsListCallBackProxy 以完整蓝图节点形式封装好友列表获取并支持网络等待任务NetBlueprintFunctionLibrary 提供邀请好友的蓝图函数库NetGameInstance 则在邀请被接受后负责加入对应会话形成从读取好友到发起、接受邀请的闭环示例。目前已有 311 人学习适合用作 UE4 Steam 联机功能的最小原型参考也可据此扩展自己的好友与邀请逻辑。1. 从“好友列表永远是空的”说起在 UE4 里接 Steam Friends API最常见的失败原因往往不是编译报错代码一次编译通过运行起来好友列表却永远是空数组。这份演示工程把 C 侧整条链路铺开OnlineSubsystem 初始化、IOnlineFriends 异步拉列表、好友状态增量刷新、头像纹理生成再往上接富状态和邀请进房。适合已经能把 UE4 工程打成可执行文件、想在联机功能里补上“好友”这一块的 C 开发者如果你还在纠结蓝图能不能做答案是能但回调时序、CSteamID 转换、GC 这些点绕不开代码。下面按“接口怎么选、列表怎么拉、状态怎么刷、包怎么打”的顺序拆开讲顺序别跳跳过第二步后面全是玄学。2. 接口选型OnlineSubsystem 的封装边界与 Steamworks 原生直连动手前要先想明白一件事UE4 的 OnlineSubsystem下称 OSS是一层跨平台抽象它把好友、会话、成就都收敛成统一接口代价是抽象层只暴露各平台都有的能力。Steam 特有的富状态自定义字段、好友头像像素、分组折叠规则OSS 一概不管。所以真正的工程做法是混着用而不是二选一。2.1 两条链路抽象层与原生 SDK 各管哪一段判断标准很朴素这段数据要不要跨平台。好友列表、会话创建、邀请 UI 这些必须走 OSS因为它们背后还牵涉 NetDriver 和会话管理富状态、头像、好友分组这些只有 Steam 有直连 Steamworks 反而更短。中间最容易被忽略的一点是回调由谁驱动OSS 的 Steam 子系统在每帧 Tick 里已经调过SteamAPI_RunCallbacks()你自己再调一次就会导致回调重复触发甚至在编辑器里崩在CCallbackBase的断言上。能力OSS 接口原生 SDK 接口建议走哪条好友列表读取IOnlineFriends::ReadFriendsListGetFriendCount/GetFriendByIndexOSS异步回调已经封装好好友在线状态FOnlineUserPresenceGetFriendPersonaStateOSS 够用枚举更细时补原生富状态自定义字段IOnlinePresence能力有限SetRichPresence原生好友头像无对应接口GetMediumFriendAvatarGetImageRGBA原生邀请弹窗IOnlineExternalUI::ShowInviteUIActivateGameOverlayInviteDialogOSS跨平台且和会话绑定创建/加入会话IOnlineSessionSteam Lobby 原生 APIOSS别自己造轮子表里“建议”那一列是踩过坑之后的结论。早期我用原生GetFriendCount拿列表好处是能一次拿到头像 handle坏处是会话邀请进来之后本地用户 ID 和 OSS 的对不上最后还是得回来重做一遍映射。2.2 工程骨架Build.cs 与 Target.cs 里必须打开的两个开关模块依赖漏一个症状是编译期报“无法打开源文件 steam/steam_api.h”或者链接期一堆SteamAPI_*未解析符号。下面这份Build.cs把三个模块分开列注意OnlineSubsystemSteam放在 Public 里// Source/FriendsDemo/FriendsDemo.Build.cs using UnrealBuildTool; public class FriendsDemo : ModuleRules { public FriendsDemo(ReadOnlyTargetRules Target) : base(Target) { PCHUsage PCHUsageMode.UseExplicitOrSharedPCHs; PublicDependencyModuleNames.AddRange(new string[] { Core, CoreUObject, Engine, InputCore, OnlineSubsystem, // IOnlineSubsystem / IOnlineFriends 等抽象头 OnlineSubsystemUtils, // 蓝图函数库、IpNetDriver 回退 OnlineSubsystemSteam // 只要公共头里出现 Steam 类型就必须放 Public }); } }OnlineSubsystemSteam放 Public 而不是 Private是因为只要你的某个 Public 头文件里 include 了steam/steam_api.h或者出现了FUniqueNetIdSteam下游模块就必须能看见这个依赖路径放 Private 会在一堆不相干的地方报头文件找不到。这是 UE4 模块系统里最典型的血泪经验之一。Target.cs 里还要打开bUsesSteam它影响的是链接阶段的库引用// Source/FriendsDemo.Target.cs public class FriendsDemoTarget : TargetRules { public FriendsDemoTarget(TargetInfo Target) : base(Target) { Type TargetType.Game; ExtraModuleNames.Add(FriendsDemo); bUsesSteam true; // 漏掉它的典型症状链接期 SteamAPI_* 全部未解析 } }另外 Steamworks SDK 本身要放到引擎的第三方目录下Engine/Source/ThirdParty/Steamworks/下按引擎版本对应的那个sdk目录目录名带版本前缀放错层级的症状是 UBT 找不到steam_api64.lib报错信息里会直接打印它搜索过的路径照着路径倒推就行。2.3 启动链路DefaultEngine.ini 与 steam_appid.txt 的落位配置项决定了IOnlineSubsystem::Get()到底返回哪个实现。返回空实现时所有接口都是 nullptr好友列表当然是空的——这就是很多人“代码没问题但没数据”的根因。; Config/DefaultEngine.ini [/Script/Engine.GameEngine] NetDriverDefinitions(DefNameGameNetDriver,DriverClassNameOnlineSubsystemSteam.SteamNetDriver,DriverClassNameFallbackOnlineSubsystemUtils.IpNetDriver) [OnlineSubsystem] DefaultPlatformServiceSteam bHasVoiceEnabledtrue [OnlineSubsystemSteam] bEnabledtrue SteamDevAppId480 bInitServerOnClienttrue bUseSteamNetworkingtrue [/Script/OnlineSubsystemSteam.SteamNetDriver] NetConnectionClassNameOnlineSubsystemSteam.SteamNetConnection逐项说清楚改错任何一个都会表现成“能跑但不对”DefaultPlatformServiceSteam是总开关。写成空或NULL拿到的是空实现日志里不会有任何报错只是所有接口返回无效。SteamDevAppId480是公开的测试 AppID本地单机自测用它最省事正式发布换成自己的 AppID并且要和steam_appid.txt里的数字完全一致不一致时SteamAPI_Init直接失败。bInitServerOnClienttrue让客户端也初始化 GameServer 那一套本地起 listen server 调试方便纯客户端联机可以关关了之后本机当房主会失败。bUseSteamNetworkingtrue走 Steam 的 P2P 传输NAT 穿透省事代价是流量走中继、延迟比直连高竞技类项目要实测。最上面那两行NetDriverDefinitions是把 UE 默认的 IpNetDriver 换成 SteamNetDriver。缺了它会话能创建成功、好友能看到房间但点“加入”之后永久卡在 connecting。然后是steam_appid.txt内容只有一行数字位置极其讲究必须和可执行文件同目录。编辑器下是引擎的Binaries/Win64/打包后是包目录/Binaries/Win64/。放在工程根目录一份是没用的这是新手最常见的误解。注意编辑器启动时 OnlineSubsystemSteam 就完成了一次SteamAPI_Init。如果启动编辑器时 Steam 客户端没开后面再开客户端也不会自动重连必须重启编辑器。3. 好友数据落地C 拉列表、状态刷新与头像纹理这一部分是这个演示工程的主体。三段代码分别对应“拉一次全量”“变更时增量刷”“把头像变成能贴到 UMG 上的东西”三者串起来才是一个能用的好友面板。我习惯把它们塞进一个UGameInstanceSubsystem生命周期跟着 GameInstance 走不用管关卡切换时的清理。3.1 ReadFriendsList异步读取的完整写法与返回值处理先看头文件。数据结构用 USTRUCT 而不是裸结构体是为了后面能直接往蓝图抛委托用动态多播蓝图侧才能 Bind。// FriendsSubsystem.h #pragma once #include CoreMinimal.h #include Subsystems/GameInstanceSubsystem.h #include Interfaces/OnlineFriendsInterface.h #include FriendsSubsystem.generated.h USTRUCT(BlueprintType) struct FFriendEntry { GENERATED_BODY() UPROPERTY(BlueprintReadOnly, Category Friends) FString DisplayName; UPROPERTY(BlueprintReadOnly, Category Friends) FString PresenceText; UPROPERTY(BlueprintReadOnly, Category Friends) bool bOnline false; UPROPERTY(BlueprintReadOnly, Category Friends) bool bPlayingThisGame false; UPROPERTY(BlueprintReadOnly, Category Friends) FString SteamId; }; DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnFriendsReady, const TArrayFFriendEntry, Friends); UCLASS() class FRIENDSDEMO_API UFriendsSubsystem : public UGameInstanceSubsystem { GENERATED_BODY() public: UFUNCTION(BlueprintCallable, Category Friends) void RefreshFriendsList(); UPROPERTY(BlueprintAssignable, Category Friends) FOnFriendsReady OnFriendsReady; private: void HandleReadFriendsListComplete(int32 LocalUserNum, bool bWasSuccessful, const FString ListName, const FString ErrorStr); UPROPERTY() TMapFString, UTexture2D* AvatarCache; // 防止头像被 GC };实现里有两个判断不能省OSS 实例是否存在、本地用户 ID 是否有效。后者是“列表一直为空”的头号原因。// FriendsSubsystem.cpp #include FriendsSubsystem.h #include OnlineSubsystem.h #include Interfaces/OnlineIdentityInterface.h void UFriendsSubsystem::RefreshFriendsList() { IOnlineSubsystem* OSS IOnlineSubsystem::Get(); if (!OSS) { UE_LOG(LogTemp, Error, TEXT(没有 OnlineSubsystem 实例检查 DefaultPlatformService)); return; } IOnlineFriendsPtr Friends OSS-GetFriendsInterface(); IOnlineIdentityPtr Identity OSS-GetIdentityInterface(); if (!Friends.IsValid() || !Identity.IsValid()) { UE_LOG(LogTemp, Error, TEXT(Friends/Identity 接口无效多半是 SteamAPI_Init 失败)); return; } // 0 是本地玩家序号本地多账号时这里是循环变量 if (!Identity-GetUniquePlayerId(0).IsValid()) { UE_LOG(LogTemp, Warning, TEXT(本地用户尚未登录下一帧重试)); GetWorld()-GetTimerManager().SetTimerForNextTick(this, UFriendsSubsystem::RefreshFriendsList); return; } const FString ListName EFriendsLists::ToString(EFriendsLists::Default); Friends-ReadFriendsList( 0, ListName, FOnReadFriendsListComplete::CreateUObject(this, UFriendsSubsystem::HandleReadFriendsListComplete)); }ReadFriendsList是纯异步的Vytautas 一点它不会阻塞回调可能在几帧之后、也可能在几百毫秒之后触发中间界面必须能显示“加载中”。ListName用EFriendsLists::ToString(Default)而不是硬编码字符串是因为后续如果切到OnlinePlayers只要在线的好友或RecentPlayers最近一起玩过的只需要换一个枚举值。回调里做三件事判成功、判列表可读、转成自己的结构体。注意GetFriendsList返回的是TArrayTSharedRefFOnlineFriend是共享引用别把它存到成员变量里长期持有。void UFriendsSubsystem::HandleReadFriendsListComplete(int32 LocalUserNum, bool bWasSuccessful, const FString ListName, const FString ErrorStr) { if (!bWasSuccessful) { UE_LOG(LogTemp, Warning, TEXT(ReadFriends p a hrefhttps://download.csdn.net/download/weixin_42127937/19733659 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p