目录

Lyra - EOS

什么是 Lyra 的 EOS 实现

Lyra 并没有把 EOS SDK 直接写进 Gameplay 代码里。它的在线系统是一个“配置驱动 + CommonUser / CommonSession 抽象”的结构:玩法层只关心登录、开房、搜房、加入、跳转,真正底层是 Null、Steam、EOS 还是 SteamEOS,由 Target 和 ini 配置决定。

  • 玩法层LyraGame、前端流程、GameMode 只调用 CommonUserSubsystemCommonSessionSubsystem
  • 抽象层CommonUser 负责用户登录、权限检查、平台用户和本地玩家绑定;CommonSession 负责 Host / Find / Join / Travel。
  • 在线服务层:当前工程默认走 OSSv1,也就是 OnlineSubsystemIOnlineIdentityIOnlineSession 这套接口。
  • EOS Provider:当使用 LyraGameEOSLyraServerEOS Target 时,默认 OnlineSubsystem 会变成 OnlineSubsystemEOS

所以它的整体关系更像这样:

1
2
3
4
5
6
7
8
9
Lyra Gameplay / UI / GameMode
CommonUser / CommonSession
OnlineSubsystem OSSv1 接口
OnlineSubsystemEOS 插件
EOS SDK / Epic Online Services

它到底用的是 OSS 还是 Online Services

1. 当前 CommonUser 默认走 OSSv1

Plugins/CommonUser/Source/CommonUser/CommonUser.Build.cs 里有一个关键开关:

1
bool bUseOnlineSubsystemV1 = true;

这个值为 true 时,CommonUser 会:

  • 依赖OnlineSubsystem
  • 定义宏COMMONUSER_OSSV1=1
  • 使用接口IOnlineSubsystemIOnlineIdentityIOnlineSessionIOnlineExternalUIIOnlinePresence

也就是说,虽然 EOS Target 会启用 OnlineServicesEOS,但这份 Lyra 代码里的 CommonUser / CommonSession 主路径还是 OSSv1。

2. OSSv2 路径存在,但不是当前主路

代码里能看到 #if COMMONUSER_OSSV1 / #else 两套分支:

1
2
3
4
5
6
7
8
9
#if COMMONUSER_OSSV1
    Online::GetSubsystem(GetWorld());
    OnlineSub->GetIdentityInterface();
    OnlineSub->GetSessionInterface();
#else
    GetServices(GetWorld(), EOnlineServices::Default);
    OnlineServices->GetAuthInterface();
    OnlineServices->GetLobbiesInterface();
#endif
  • OSSv1:老 OnlineSubsystem 接口,Lyra 当前实际使用。
  • OSSv2OnlineServicesInterface / OnlineServicesEOS 新接口,代码有保留,但不少地方仍有 TODO。

所以学习这份工程时,先按 OSSv1 理解最准确。


EOS 是怎么被启用的

1. 默认工程不是 EOS

普通 Config/DefaultEngine.ini 里默认是:

1
2
[OnlineServices]
DefaultServices=Null

这说明普通 Lyra 默认是本地 Null Online,不是 EOS。

2. EOS Target 会指定 CustomConfig

Source/LyraGameEOS.Target.cs 的核心是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
CustomConfig = "EOS";

EnablePlugins.AddRange(
    new string[]
    {
        "OnlineServicesEOS",
        "OnlineSubsystemEOS"
    }
);

OptionalPlugins.Add("EOSReservedHooks");

这代表构建 LyraGameEOS 时,会额外读取:

1
Config/Custom/EOS/DefaultEngine.ini

Dedicated Server 也有对应版本:

1
2
LyraServerEOS.Target.cs
LyraServerSteamEOS.Target.cs

3. Custom EOS 配置切换底层 OnlineSubsystem

Config/Custom/EOS/DefaultEngine.ini 里最关键的是:

1
2
3
4
5
6
7
8
[OnlineSubsystem]
DefaultPlatformService=EOS

[OnlineSubsystemEOS]
bEnabled=true

[OnlineServices]
DefaultServices=Epic

这三段的意思是:

  • DefaultPlatformService=EOS:OSSv1 的默认 OnlineSubsystem 变成 EOS。
  • bEnabled=true:启用 OnlineSubsystemEOS
  • DefaultServices=Epic:OSSv2 的默认服务设为 Epic;虽然当前 CommonUser 主路是 OSSv1,但配置也给 OSSv2 留了入口。

EOSSettings 负责哪些能力

EOS 自己的设置在:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[/Script/OnlineSubsystemEOS.EOSSettings]
bEnableOverlay=True
bEnableSocialOverlay=True
bEnableEditorOverlay=True
bUseEAS=True
bUseEOSConnect=True
bMirrorStatsToEOS=True
bMirrorAchievementsToEOS=True
bUseEOSSessions=True
bMirrorPresenceToEAS=True

这些开关可以这样理解:

  • bUseEAS:使用 Epic Account Services,用于 Epic 账号体系登录。
  • bUseEOSConnect:使用 EOS Connect,用于跨平台产品用户身份。
  • bUseEOSSessions:Session 使用 EOS 后端能力。
  • bMirrorPresenceToEAS:把 Presence 同步到 Epic Account 侧。
  • bEnableOverlay / bEnableSocialOverlay:启用 EOS Overlay 和社交 Overlay。
  • bMirrorStatsToEOS / bMirrorAchievementsToEOS:把统计和成就同步到 EOS。

实际项目上线时,还要填自己的 Artifact 信息:

1
2
DefaultArtifactName=ARTIFACTNAME
+Artifacts=(ArtifactName="ARTIFACTNAME",ClientId="CLIENTID",ClientSecret="CLIENTSECRET",ProductId="PRODUCTID",SandboxId="SANDBOXID",DeploymentId="DEPLOYTMENTID",EncryptionKey="ENCRYPTIONKEY")

Lyra 示例里这些值默认是注释状态,不能直接拿来上线。


EOS NetDriver 是怎么接进来的

EOS 配置还替换了网络驱动:

1
2
3
4
5
[/Script/Engine.Engine]
!NetDriverDefinitions=ClearArray
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="/Script/SocketSubsystemEOS.NetDriverEOS",DriverClassNameFallback="/Script/OnlineSubsystemUtils.IpNetDriver")
+NetDriverDefinitions=(DefName="BeaconNetDriver",DriverClassName="/Script/SocketSubsystemEOS.NetDriverEOS",DriverClassNameFallback="/Script/OnlineSubsystemUtils.IpNetDriver")
+NetDriverDefinitions=(DefName="DemoNetDriver",DriverClassName="/Script/Engine.DemoNetDriver",DriverClassNameFallback="/Script/Engine.DemoNetDriver")
  • GameNetDriver:正常玩家联机走 EOS Socket NetDriver。
  • BeaconNetDriver:Party / Reservation Beacon 也走 EOS Socket。
  • Fallback:如果 EOS NetDriver 不可用,会退回 IpNetDriver

这就是为什么 Lyra 的 ClientTravel 最后拿到的连接字符串可以交给 EOS 网络层处理。


CommonUser 登录流程

1. 前端普通玩家登录

前端流程在 ULyraFrontendStateComponent 里排了几个步骤:

1
2
3
4
QueueStep("Wait For User Initialization")
QueueStep("Try Show Press Start Screen")
QueueStep("Try Join Requested Session")
QueueStep("Try Show Main Screen")

如果平台不需要 Press Start,Lyra 会直接:

1
UserSubsystem->TryToInitializeForLocalPlay(0, FInputDeviceId(), false);

这个函数内部请求的是:

1
Params.RequestedPrivilege = ECommonUserPrivilege::CanPlay;

所以普通前端进入游戏时,默认只要求“能玩本地游戏”,不一定强制在线。

2. Dedicated Server 登录

Dedicated Server 启动时,ALyraGameMode::TryDedicatedServerLogin() 会检查自己是不是在默认地图上运行。如果是,它会调用:

1
UserSubsystem->TryToLoginForOnlinePlay(0);

这个函数内部请求的是:

1
Params.RequestedPrivilege = ECommonUserPrivilege::CanPlayOnline;

成功或失败后都会继续:

1
HostDedicatedServerMatch(ECommonSessionOnlineMode::Online);

这说明 Lyra 的 Dedicated Server 登录失败不一定阻止开服。注释里也写得很清楚:一些 OnlineSubsystem 可能期望服务器登录,但 Dedicated Server 本身不强制依赖用户登录。

3. CommonUser 的实际登录步骤

LoginLocalUser() 会创建 FUserLoginRequest,然后进入 ProcessLoginRequest()

它的大致顺序是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
检查当前是否已经 LoggedIn
尝试 TransferPlatformAuth
尝试 AutoLogin
尝试 ShowLoginUI
检查 CanPlay / CanPlayOnline 权限
绑定 LocalPlayer / PlayerState 的 UniqueNetId

OSSv1 下 TransferPlatformAuth 直接返回 false,所以当前主路更像:

1
2
3
4
5
6
7
已有登录
AutoLogin
External Login UI
Privilege Check

相关接口都是 OSSv1:

1
2
3
IdentityInterface->AutoLogin(...)
ExternalUI->ShowLoginUI(...)
IdentityInterface->GetUserPrivilege(...)

Online Context 是怎么理解的

ECommonUserOnlineContext 里有几个容易混的概念:

1
2
3
4
5
6
Game
Default
Service
ServiceOrDefault
Platform
PlatformOrDefault

可以这样记:

  • Default:当前默认 OnlineSubsystem。EOS Target 下通常就是 EOS。
  • Platform:当前原生平台服务。例如 SteamEOS 下可能是 Steam。
  • Service:外部在线服务。Lyra 这里预留给更复杂的服务拆分。
  • Game:玩法代码使用的高层语义。它会先处理平台上下文,再处理最终服务上下文。

ProcessLoginRequest() 里有一段关键逻辑:

1
2
3
4
5
6
7
8
if (Request->DesiredContext == ECommonUserOnlineContext::Game)
{
    Request->CurrentContext = ResolveOnlineContext(ECommonUserOnlineContext::PlatformOrDefault);
}
else
{
    Request->CurrentContext = ResolveOnlineContext(Request->DesiredContext);
}

这就是 SteamEOS 场景下“先平台、后 EOS”的基础。


SteamEOS 和纯 EOS 的差异

纯 EOS

Config/Custom/EOS/DefaultEngine.ini

1
2
3
4
5
[OnlineSubsystem]
DefaultPlatformService=EOS

[OnlineServices]
DefaultServices=Epic

纯 EOS 下,Default OnlineSubsystem 就是 EOS。

SteamEOS

Config/Custom/SteamEOS/DefaultEngine.ini

1
2
3
4
5
6
7
8
9
[OnlineSubsystem]
DefaultPlatformService=EOS
NativePlatformService=Steam

[/Script/OnlineSubsystemEOS.EOSSettings]
SteamTokenType=WebApi:epiconlineservices

[OnlineSubsystem]
bLoadNativeOSSBeforeDefault=true

这个配置表达的是:

  • DefaultPlatformService=EOS:游戏主要在线服务还是 EOS。
  • NativePlatformService=Steam:原生平台是 Steam。
  • SteamTokenType:EOS 通过 Steam WebApi token 做平台集成。
  • bLoadNativeOSSBeforeDefault:先加载 Steam,再加载 EOS,保证 EOS 初始化时 Steam DLL 和平台环境已经准备好。

所以 SteamEOS 不是“只用 Steam”,而是:

1
2
Steam 负责原生平台身份 / 发布平台集成
EOS 负责跨平台在线服务 / Session / Presence 等

CommonSession 开房流程

1. UserFacingExperience 生成 HostRequest

Lyra 的每个可见玩法体验由 ULyraUserFacingExperienceDefinition 描述。它会创建:

1
UCommonSession_HostSessionRequest* Result = Subsystem->CreateOnlineHostSessionRequest();

然后填入:

1
2
3
4
Result->MapID = MapID;
Result->ModeNameForAdvertisement = UserFacingExperienceName;
Result->ExtraArgs.Add("Experience", ExperienceName);
Result->MaxPlayerCount = MaxPlayerCount;

也就是说,Lyra 开房不是直接写死地图和模式,而是从 UserFacingExperience 数据资产生成 HostRequest。

2. HostSession 进入在线 Session 创建

UCommonSessionSubsystem::HostSession() 会判断:

1
2
3
4
5
Offline
    -> 直接 ServerTravel

Online / LAN
    -> CreateOnlineSessionInternal

当前 OSSv1 主路最后会进入:

1
CreateOnlineSessionInternalOSSv1(...)

3. OSSv1 创建 EOS Session

创建 Session 时,Lyra 会构造 FCommonSession_OnlineSessionSettings

1
2
3
4
5
FCommonSession_OnlineSessionSettings HostSettings(
    Request->OnlineMode == ECommonSessionOnlineMode::LAN,
    Request->bUsePresence,
    MaxPlayers
);

然后写入几个可搜索字段:

1
2
3
4
5
6
7
HostSettings.bUseLobbiesIfAvailable = Request->bUseLobbies;
HostSettings.bUseLobbiesVoiceChatIfAvailable = Request->bUseLobbiesVoiceChat;
HostSettings.Set(SETTING_GAMEMODE, Request->ModeNameForAdvertisement, ViaOnlineService);
HostSettings.Set(SETTING_MAPNAME, Request->GetMapName(), ViaOnlineService);
HostSettings.Set(SETTING_MATCHING_TIMEOUT, 120.0f, ViaOnlineService);
HostSettings.Set(SETTING_SESSION_TEMPLATE_NAME, "GameSession", ViaOnlineService);
HostSettings.Set(SETTING_ONLINESUBSYSTEM_VERSION, true, ViaOnlineService);

最后调用:

1
Sessions->CreateSession(*UserId, NAME_GameSession, HostSettings);

如果当前默认 OnlineSubsystem 是 EOS,这个 CreateSession 实际就是交给 OnlineSubsystemEOS


搜房和加入流程

1. 搜房 FindSessions

FindSessions() 会创建 FCommonOnlineSearchSettingsOSSv1

1
2
3
4
5
6
QuerySettings.Set(SETTING_ONLINESUBSYSTEM_VERSION, true, Equals);

if (InSearchRequest->bUseLobbies)
{
    QuerySettings.Set(SEARCH_LOBBIES, true, Equals);
}

真正查找时又加了:

1
2
SearchSettings->QuerySettings.Set(SETTING_SESSION_TEMPLATE_NAME, "GameSession", Equals);
Sessions->FindSessions(*LocalPlayer->GetPreferredUniqueNetId().GetUniqueNetId(), SearchSettings);

所以 Lyra 默认只找同一套模板创建出来的游戏 Session,避免搜到不兼容的房间。

2. 加入 JoinSession

加入时主路是:

1
2
3
Request->Result.Session.SessionSettings.bUsesPresence = true;
Request->Result.Session.SessionSettings.bUseLobbiesIfAvailable = bUseLobbiesDefault;
Sessions->JoinSession(*LocalPlayer->GetPreferredUniqueNetId().GetUniqueNetId(), NAME_GameSession, Request->Result);

成功后:

1
FinishJoinSession(Result)

如果没有启用 Reservation Beacon,就会直接:

1
InternalTravelToSession(NAME_GameSession);

3. Travel 到服务器

InternalTravelToSession() 里会通过 OnlineSubsystem 解析连接字符串:

1
2
Sessions->GetResolvedConnectString(SessionName, URL);
PlayerController->ClientTravel(URL, TRAVEL_Absolute);

这一步非常关键:客户端不需要自己拼 IP 或 EOS P2P 地址,而是让当前 OnlineSubsystem 返回正确的连接字符串。


Lobby Schema 是干什么的

EOS 配置里还有:

1
2
3
4
5
6
[OnlineServices.Lobbies]
+SchemaDescriptors=(Id="GameLobby", ParentId="LobbyBase")
+SchemaAttributeDescriptors=(Id="GAMEMODE", Type="String", Flags=("Public"), MaxSize=64)
+SchemaAttributeDescriptors=(Id="MAPNAME", Type="String", Flags=("Public"), MaxSize=64)
+SchemaAttributeDescriptors=(Id="MATCHTIMEOUT", Type="Double", Flags=("Public"))
+SchemaAttributeDescriptors=(Id="SESSIONTEMPLATENAME", Type="String", Flags=("Public"), MaxSize=64)

这些主要服务于 OSSv2 / OnlineServices Lobbies 路径。虽然当前 CommonSession 主路是 OSSv1,但 Lyra 已经把 OSSv2 Lobby 需要的 Schema 也写在配置里。

可以理解成:

  • OSSv1 Session Settings:用 SETTING_GAMEMODESETTING_MAPNAME 等键值广告房间。
  • OSSv2 Lobby Schema:提前声明 Lobby 有哪些字段、字段类型、是否可搜索。

Presence 是怎么更新的

UCommonUserBasicPresence 会监听:

1
CommonSession->OnSessionInformationChangedEvent

当 Session 状态变化时,它会把状态同步到在线服务:

1
2
3
Status_InGame
Status_MainMenu
Status_Matchmaking

并写入:

1
2
game_mode
map_name

EOS 配置里对应:

1
2
3
4
5
6
7
[/Script/CommonUser.CommonUserBasicPresence]
bEnableSessionsBasedPresence=true
PresenceStatusInGame=Status_InGame
PresenceStatusMainMenu=Status_MainMenu
PresenceStatusMatchmaking=Status_Matchmaking
PresenceKeyGameMode=game_mode
PresenceKeyMapName=map_name

这说明 Lyra 的 Presence 不是到处手动更新,而是跟 Session 状态绑定。


QuickPlay 是怎么工作的

QuickPlaySession() 的逻辑很简单:

1
2
3
4
5
6
7
进入 Matchmaking 状态
FindSessions
如果找到结果,Join 第一个
如果没找到,HostSession 自己开房

代码里甚至有 TODO,说明它现在没有复杂排序:

1
// We should probably look at ping?

所以 Lyra 的 QuickPlay 更像教学用的基础实现,不是成熟商业匹配系统。

如果正式做项目,通常还要扩展:

  • 匹配规则:地图、模式、地区、段位、队伍人数。
  • 结果排序:Ping、人数、服务器负载。
  • 失败重试:搜不到房、Join 失败、Session 满员后的回退。
  • Dedicated Server 分配:不要只靠玩家 Host。

学习时应该先看哪些文件

配置入口

1
2
3
Config/DefaultEngine.ini
Config/Custom/EOS/DefaultEngine.ini
Config/Custom/SteamEOS/DefaultEngine.ini

重点看:

1
2
3
4
5
6
DefaultPlatformService
DefaultServices
OnlineSubsystemEOS.EOSSettings
NetDriverDefinitions
OnlineServices.Lobbies
CommonUserBasicPresence

Target 入口

1
2
3
4
Source/LyraGameEOS.Target.cs
Source/LyraServerEOS.Target.cs
Source/LyraGameSteamEOS.Target.cs
Source/LyraServerSteamEOS.Target.cs

重点看:

1
2
3
CustomConfig = "EOS";
EnablePlugins.AddRange(...)
OptionalPlugins.Add("EOSReservedHooks");

登录入口

1
2
3
Plugins/CommonUser/Source/CommonUser/Private/CommonUserSubsystem.cpp
Plugins/CommonUser/Source/CommonUser/Public/CommonUserTypes.h
Plugins/CommonUser/Source/CommonUser/Private/AsyncAction_CommonUserInitialize.cpp

重点看:

1
2
3
4
5
6
7
CreateOnlineContexts()
TryToInitializeForLocalPlay()
TryToLoginForOnlinePlay()
LoginLocalUser()
ProcessLoginRequest()
HandleUserLoginCompleted()
HandleCheckPrivilegesComplete()

Session 入口

1
2
Plugins/CommonUser/Source/CommonUser/Private/CommonSessionSubsystem.cpp
Plugins/CommonUser/Source/CommonUser/Public/CommonSessionSubsystem.h

重点看:

1
2
3
4
5
6
7
CreateOnlineHostSessionRequest()
HostSession()
CreateOnlineSessionInternalOSSv1()
FindSessionsInternalOSSv1()
JoinSessionInternalOSSv1()
InternalTravelToSession()
CleanUpSessions()

Lyra 接入点

1
2
3
4
Source/LyraGame/UI/Frontend/LyraFrontendStateComponent.cpp
Source/LyraGame/GameModes/LyraGameMode.cpp
Source/LyraGame/GameModes/LyraUserFacingExperienceDefinition.cpp
Source/LyraGame/System/LyraGameInstance.cpp

这些文件说明 Lyra 是如何把前端、登录、体验数据、开服和 travel 串起来的。


最容易踩的坑

  • 只改 DefaultEngine.ini 不够:EOS 主要在 Config/Custom/EOS 里,必须用对应 Target 或 CustomConfig 才会加载。
  • 插件启用不等于登录成功:还要填正确的 ArtifactNameClientIdClientSecretProductIdSandboxIdDeploymentId
  • 当前主路是 OSSv1:不要看到 OnlineServicesEOS 就以为 CommonUser 已经切到 OSSv2。
  • Session 和 Lobby 概念会混:OSSv1 下 Lyra 仍然用 IOnlineSession,但可以通过 bUseLobbiesIfAvailable 让底层使用可用的 Lobby 能力。
  • SteamEOS 不是 Steam 联机:它是 Steam 原生平台 + EOS 跨平台服务。
  • QuickPlay 很基础:Lyra 的 QuickPlay 更像示例,不是完整 matchmaking。
  • Dedicated Server 登录失败仍可能开服:代码注释说明服务器不强制依赖用户登录,但某些 OnlineSubsystem 可能需要。

简述工作流程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
构建 LyraGameEOS / LyraServerEOS
加载 Config/Custom/EOS/DefaultEngine.ini
Default OnlineSubsystem 变成 EOS
CommonUser 初始化 OnlineSubsystem + Identity
前端或服务器触发用户登录
CommonSession 创建 / 搜索 / 加入 Session
OnlineSubsystemEOS 把请求转给 EOS
成功后 ServerTravel / ClientTravel

一句话总结

Lyra 的 EOS 不是“玩法代码直接调用 EOS SDK”,而是通过 Target 选择配置、通过 OnlineSubsystemEOS 提供底层能力、通过 CommonUserCommonSession 把登录与联机流程包装成游戏可用的通用接口。