第十二章: 部落系统
1. 本章核心目标
本章的核心目标分为显性与隐性两个层面,它们共同构成了部落系统的基础框架,并为未来的多人协作玩法奠定了基础。
-
显性目标 (玩家功能实现)
-
为玩家提供创建、加入和管理部落的基础社交功能。
-
实现部落内的权限管理,如提升/降级成员、踢出成员。
-
建立部落内部信息共享机制,包括部落日志(Tribe Log)和每日消息(Message of the Day)。
-
具象化部落的共享权益,例如共享建筑的拆除权、开门权限以及重生点等。
-
-
隐性/战略目标 (技术与项目管理)
-
搭建核心多人游戏状态管理框架:通过引入自定义的
GameMode
、GameState
和PlayerState
,确立了服务器权威(Server-Authoritative)的多人游戏架构。这是项目从单机功能开发向真正的网络多人游戏过渡的关键一步。 -
建立可扩展的数据结构:设计并实现了一套完整的部落相关数据结构(Enums 和 Structs),为系统后续扩展(如部落战争、部落科技)提供了标准化的数据模型。
-
解决唯一身份标识问题:引入“Advanced Sessions”插件,从根本上解决了之前依赖测试ID所带来的权限和所有权识别BUG,为实现可靠的多人在线功能扫清了障碍。
-
-
目标关系图
下表清晰地展示了显性与隐性目标之间的支撑与实现关系:
显性目标 (玩家功能) | 支撑该功能的隐性/战略目标 |
---|---|
创建/加入/管理部落 | 搭建核心多人游戏状态管理框架 (使用GameState 存储所有部落信息) |
权限管理 (提升/降级/踢出) | 建立可扩展的数据结构 (E_TribeRank Enum);服务器权威逻辑验证 |
部落日志/每日消息 | GameState 作为中心化数据源,分发给所有部落成员 |
共享建筑/权限 | 解决唯一身份标识问题 (确保能准确识别玩家与部落归属);与建筑系统深度集成 |
2. 系统与功能实现
本章实现了完整的部落系统,其核心功能模块与交互流程如下:
-
具体实现系统/功能列表
-
UI系统:
-
W_TribeWindow
: 部落主界面,用于展示成员、日志和每日消息。 -
W_CreateTribe
: 当玩家不属于任何部落时显示的创建界面。 -
W_TribeSwitcher
: 根据玩家状态(是否在部落中)切换上述两个界面的逻辑切换器。 -
W_TribeInvite
: 玩家接收到部落邀请时弹出的交互窗口。 -
W_TribeMemberSlot
&W_TribeLogSlot
: 用于在滚动列表中动态生成部落成员和日志条目的可复用控件。
-
-
后端逻辑系统:
-
状态管理:在
GameState
中使用TMap<FString, FSTribeInfo>
作为核心数据容器,以部落ID为键,高效存储和检索所有部落的详细信息。 -
数据结构:定义了
E_TribeRank
、S_TribeInfo
、S_TribeMemberInfo
等一系列结构体和枚举,标准化了系统数据。 -
权限验证:所有敏感操作(如邀请、踢人、修改MOTD)均在服务器端(PlayerController或GameState)进行权限检查(如验证玩家是否为Owner或Admin)。
-
-
系统交互:
-
邀请与加入:通过玩家角色的服务器端射线检测(Line Trace)来识别目标玩家并发起邀请,整个流程通过接口和RPC在客户端与服务器之间传递。
-
状态同步:当部落数据发生变更时(如新成员加入、成员被踢),
GameState
会遍历部落所有在线成员,并通过RPC调用其PlayerController
上的接口,将最新的部落信息推送给每个客户端,确保所有成员UI同步更新。
-
-
-
系统交互流程图 (以“创建部落”为例)
Code snippet
1
2
3
4
5
6
7
8
9
10
11
12graph TD
A[玩家在 W_CreateTribe 输入部落名并点击创建] --> B{调用 PlayerController 接口};
B --> C[PlayerController (客户端) 调用 RPC: CreateTribeOnServer];
C --> D{PlayerController (服务器端) 执行};
D --> E[1. 验证玩家资格(是否已在部落中)];
E --> F[2. 构建 S_TribeInfo 结构体];
F --> G[3. 获取玩家的 UniqueNetID 作为 TribeID];
G --> H{4. 调用 GameState 接口: CreateNewTribe};
H --> I[GameState (服务器端) 将新的 TribeInfo 添加到 TribeMap];
I --> J{5. 调用 PlayerController RPC: UpdateTribeWindow (客户端)};
J --> K[PlayerController (客户端) 获取部落UI引用];
K --> L[6. 调用 W_TribeWindow 的更新函数,刷新UI];
3. 关键设计思想
本章的开发体现了清晰的设计模式与原则,确保了系统的可维护性和扩展性。
- 设计模式与原则应用
设计思想 | 具体应用实例 | 带来的优势 |
---|---|---|
接口隔离原则 (Interface Segregation) | 创建了 BPI_SurvivalCharacter 、BPI_SurvivalController 、BPI_SurvivalGameState 等多个接口,用于不同蓝图类之间的通信。 |
解耦:蓝图之间不直接引用,而是通过接口调用,降低了耦合度。例如,任何Actor都可以通过接口与GameState 通信,而无需知道其具体实现。 |
单一职责原则 (Single Responsibility) | - W_TribeMemberSlot 只负责显示一个成员的信息。- GameState 只负责存储和管理所有部落的状态数据。- PlayerState 只负责存储单个玩家的部落相关状态。 |
高内聚,低耦合:每个类的功能都非常专注,易于理解、维护和复用。 |
状态模式 (State Pattern) - 简化应用 | W_TribeSwitcher 根据玩家是否在部落中(一个简单的状态)来决定显示 W_CreateTribe 还是 W_TribeWindow 。 |
逻辑清晰:将UI状态的切换逻辑集中管理,避免在主UI蓝图中散布大量的if-else判断。 |
中心化状态管理 | GameState 被用作所有部落数据的“单一事实来源”(Single Source of Truth)。所有修改都必须通过服务器在GameState 中进行,然后分发给客户端。 |
数据一致性:确保了在多人环境下所有客户端看到的部落信息都是同步和一致的,避免了网络同步问题。 |
4. 核心技术点与难点
本章的实现涉及多个关键技术点,并解决了一些典型的多人游戏开发难题。
-
核心技术点
-
客户端-服务器-客户端通信模型:熟练运用了 Unreal Engine 的 RPC(Remote Procedure Call)机制。例如,客户端发起操作(如邀请),通过 “Run on Server” RPC 将请求发送到服务器,服务器处理后,再通过 “Run on Owning Client” RPC 将结果或UI更新指令发回给相关客户端。
-
权威服务器状态管理:将部落数据 (
TribeMap
) 存储在仅存于服务器和客户端的GameState
中,所有修改逻辑均在服务器上执行,客户端只能通过RPC请求修改,确保了游戏逻辑的安全和一致性。 -
动态UI生成:在
W_TribeWindow
中,通过循环遍历GameState
推送过来的部落成员和日志数组,动态创建并填充W_TribeMemberSlot
和W_TribeLogSlot
控件,实现了数据驱动的UI更新。
-
-
技术难点与解决方案
难点 | 解决方案 | 优势/劣势 |
---|---|---|
多人状态同步:当一个玩家加入/退出部落时,如何通知所有其他在线的部落成员并更新他们的UI? | 在 GameState 的修改函数(如AddTribeMember )执行完毕后,遍历更新后的部落成员列表。对每个在线的成员,获取其 PlayerController 引用,并调用一个客户端RPC (UpdateTribeWindow ),将最新的完整部落数据推送给他们。 |
优势: 逻辑集中在GameState ,保证了状态更新的原子性和一致性。劣势: 数据量大时,全量推送部落数据可能消耗更多带宽,未来可优化为增量更新。 |
唯一身份识别:在没有Steam等在线服务时,无法获得唯一的玩家ID,导致权限、所有权判断混乱,引发BUG(如离队功能失效)。 | 引入 “Advanced Sessions” 插件。在 PlayerState 的 BeginPlay 事件中,使用 GetUniqueNetID 节点获取并存储每个玩家的唯一网络ID,并替换项目中所有硬编码的“测试ID”。 |
优势: 从根本上解决了身份识别问题,为后续的Steam会话集成做好了准备,是项目走向成熟的关键一步。 |
已有建筑归属合并:玩家创建或加入部落后,其之前放置的个人建筑如何自动变为部落资产? | 创建 MergeTribeStructures 函数,该函数使用 GetAllActorsOfClass 节点遍历关卡中所有的建筑实例。通过比对建筑的 OwnerNetID 和当前玩家的ID,筛选出属于该玩家的建筑,然后批量修改它们的部落归属信息(TribeID 、OwnerName 等)。 |
优势: 实现了核心功能需求。劣势: GetAllActorsOfClass 性能开销较大,在建筑数量庞大的服务器中可能引发卡顿,被标记为未来需要优化的点。 |
5. 自我批判与重构
在开发过程中,遇到了一些设计缺陷和实现问题,并通过反思进行了修正与重构。
-
遇到的关键问题 (“坑”)
-
UI更新不完全:最初在“加入部落”逻辑中,只更新了新加入成员的UI,而部落中其他老成员的UI没有刷新,导致信息不同步。
-
硬编码ID依赖:过度依赖“testID”和“secondPlayerID”等硬编码字符串作为玩家标识,导致在模拟双客户端测试时,权限和所有权判断逻辑出现严重BUG,例如无法正确执行“离开部落”操作。
-
父类接口调用遗漏:在重写(Override)父类的接口事件(如
StructureDestroyed
)时,忘记调用父类的同名函数 (Call to Parent Function
),导致父类中定义的基础逻辑未能执行。
-
-
反思与重构方案
原始设计/问题 | 反思 | 重构/优化方案 |
---|---|---|
问题1:UI更新不完全 | 违背了“单一事实来源”原则。UI更新的发起者应该是状态变更的源头(GameState ),而不是单个客户端。 |
重构:将UI更新逻辑提升至GameState 。在部落数据(如成员列表)被修改后,由GameState 负责向所有相关的在线客户端广播(Push)最新的数据。客户端UI只负责接收数据并渲染。 |
问题2:硬编码ID依赖 | 早期为了快速开发而使用的临时方案,缺乏对多人游戏唯一性需求的深刻理解,是项目的重大技术债务。 | 引入插件并重构:集成"Advanced Sessions"插件,并系统性地替换了所有使用硬编码ID的地方,改为从 PlayerState 获取动态且唯一的 NetID 。 |
问题3:父类接口调用遗漏 | 对UE的蓝图继承和接口覆盖机制理解不深,忽略了调用链的完整性。 | 代码审查与修正:对所有覆盖了父类事件的子蓝图进行检查,确保在执行完子类特有逻辑后,都调用了父函数,保证了功能的完整性。 |
-
如果重来一次的优化
-
提前引入唯一ID:会在项目初期就集成
Advanced Sessions
插件或实现一套临时的唯一ID生成机制,避免后期大规模的重构和因ID问题导致的调试困难。 -
优化建筑合并逻辑:会考虑在玩家放置建筑时,就在一个全局管理器(如
GameState
中的一个TMap
)中注册该建筑及其所有者ID。这样在合并部落时,可以直接查询这个管理器,而无需使用高消耗的GetAllActorsOfClass
,从而大幅提升性能。
-