TH1/MD/GameMDInDetail/联机系统详解.md
2026-04-13 15:15:08 +08:00

32 KiB

TOHOTOPIA 联机系统技术文档

最后更新: 2026/04/13


目录

  1. 系统概述
  2. 网络拓扑结构
  3. 核心类层级与职责
  4. Steam 集成层
  5. 消息协议层
  6. Action 同步机制
  7. 心跳与状态监控
  8. 断线重连机制
  9. 数据一致性校验
  10. 房间生命周期
  11. 关键数据结构
  12. 时序图
  13. 文件索引

1. 系统概述

TOHOTOPIA 是一款 4X 回合制策略游戏,联机系统基于 Steam P2P 构建,采用 房主即服务器(Host-as-Server) 的中心化架构。

核心设计原则

  • Action 原子化: 游戏以 Action 为最小操作单位推进,每名玩家在自己的回合内执行 N 步 Action 操作
  • 房主权威: 所有 Action 必须经房主验证后才广播执行,房主拥有最终游戏状态权威
  • 确定性同步: 使用共享随机数种子 (RandomSeed),保证所有客户端执行相同 Action 后状态一致
  • 序列化: 使用 MemoryPack 进行高性能二进制序列化

网络模式 (NetMode)

None       - 未初始化
Single     - 单机模式
Multi      - 联机模式
Spectator  - 观战模式

2. 网络拓扑结构

                    ┌─────────────┐
                    │   房主(Host) │
                    │ = 服务器+客户端│
                    └──────┬──────┘
                           │
              ┌────────────┼────────────┐
              │            │            │
        ┌─────▼─────┐ ┌───▼───┐ ┌─────▼─────┐
        │  成员 A    │ │成员 B │ │  成员 C    │
        │ (Client)  │ │(Client)│ │ (Client)  │
        └───────────┘ └───────┘ └───────────┘

  - 成员 => 房主: 单播 (SendMessage)
  - 房主 => 所有成员: 广播 (BroadcastMessage)
  - 房主 => 单个成员: 定向发送 (SendMessageToPlayer)
  - 成员之间: 不直接通信, 所有消息经房主中转

通信方式

方向 方法 说明
成员 => 房主 GameNetSender.SendMessage() 自动获取房主 ID 并发送
房主 => 全体 GameNetSender.BroadcastMessage() 广播给所有已连接的成员
房主 => 单人 GameNetSender.SendMessageToPlayer() 指定 memberId 定向发送
任意 => 任意 GameNetSender.SendHeartbeatReply() 心跳回复(不受房主限制)

3. 核心类层级与职责

TH1_Logic.Net (接口层/抽象层)
├── ILobby                  - 房间接口定义
├── LobbyBase               - 空实现(非 Steam 平台兜底)
└── LobbyManager            - 单例, 持有 ILobby 实例

TH1_Logic.Steam (Steam 实现层)
├── SteamLobbyManager       - ILobby 实现, 房间管理
├── SimpleP2P               - P2P 连接管理(底层传输)
├── SteamObjectSerializer   - 消息定义 & 序列化
├── GameNetSender           - 发送端(业务层)
├── GameNetReceiver          - 接收端(业务层)
└── SteamGUIMono            - 调试 GUI (F3 开关)

RuntimeData (数据层)
├── NetData                 - 网络数据(玩家映射、Action 序列、随机种子)
├── PlayerConfirmData       - 玩家确认状态(Ping、心跳、状态)
├── PlayerConfirmMap        - 全体玩家确认状态集合
└── MapData.MapConfig       - 对局配置(含联机用 MultiCivs)

平台切换

// LobbyManager.Init()
#if UNITY_EDITOR || STEAM_CHANNEL
    Lobby = new SteamLobbyManager();  // Steam 平台
#else
    Lobby = new LobbyBase();          // 非 Steam 平台(空实现)
#endif

4. Steam 集成层

4.1 SimpleP2P - P2P 连接管理

单例: SimpleP2P.Instance

负责底层 P2P 连接的建立、维护、消息收发。

功能 方法 说明
初始化 Initialize() 注册连接状态变化回调、会话请求回调
监听 CreateListenSocket() 在虚拟端口 1214 创建监听套接字
连接 ConnectToPeer(CSteamID) 主动连接到指定玩家(仅非房主调用)
发送 SendTo(CSteamID, byte[]) 通过已建立的连接发送数据
发送(无连接) SendToWithOutConnect() 通过 SteamNetworkingMessages 发送(用于邀请)
广播 Broadcast(byte[]) 向所有已连接玩家发送
轮询 PollMessages() 轮询所有连接和 Channel 0 上的消息
断开 DisconnectFromPeer(CSteamID) 断开指定连接
清理 Cleanup() 关闭所有连接和监听套接字

连接状态机

           ConnectP2P()
               │
               ▼
    ┌─── Connecting ──────┐
    │          │           │
    │          ▼           │
    │    FindingRoute      │
    │          │           │
    │          ▼           │
    │     Connected ◄─────┘
    │          │
    │    ┌─────┴──────┐
    │    ▼             ▼
    │ ClosedByPeer  ProblemDetected
    │    │             │
    │    ▼             ▼
    └─► None (断开)

关键参数

  • 虚拟端口: 1214
  • 连接超时: 30 秒
  • 最大重试: 3 次
  • 最大连接数: 10
  • 消息轮询批次: 32 条/连接, 10 条/Channel

消息发送标志

flags = 8  (可靠传输 Reliable)
flags = 0  (不可靠传输 Unreliable)
flags |= 1 (有序传输 NoDelay)

4.2 SteamLobbyManager - 房间管理

实现接口: ILobby

Steam 回调注册

回调 接收者 触发时机
LobbyCreated_t 创建者 CreateLobby 完成
GameLobbyJoinRequested_t 被邀请者 接受 Steam 邀请时
LobbyEnter_t 所有成功进入者 进入房间成功
LobbyChatUpdate_t 房间内所有人 成员进/退/踢/封禁
LobbyMatchList_t 搜索者 公开房间搜索结果返回

房间属性 (LobbyData)

Key 说明 示例值
Game 游戏标识(防止跨游戏搜索) "TOHOTOPIA"
Owner 房主昵称 "白哉"
Version 游戏版本号 "1.2.0"
RoomName 房间名称 "白哉的房间"
RoomCode 房间码(Base36 编码) "A1B2C3"
GameState 游戏状态 "0" (菜单)
disbanded 解散标记 "true"
kick_{memberId} 踢人标记 "true"

加入方式

  1. Steam 好友邀请 - InviteFriend() / OpenInviteOverlay()
  2. 房间码加入 - JoinByRoomCode(code) (Base36 编码的 LobbyID)
  3. 房间 ID 直接加入 - JoinLobbyById(lobbyId)
  4. 游戏内邀请 - SendGameInvite() (通过 P2P 发送 InviteMessage, 不走 Steam 邀请框)
  5. 公开房间列表搜索 - SearchPublicLobbies()

房间搜索过滤

// 默认搜索条件
SteamMatchmaking.AddRequestLobbyListDistanceFilter(Worldwide);
SteamMatchmaking.AddRequestLobbyListStringFilter("Game", "TOHOTOPIA", Equal);
SteamMatchmaking.AddRequestLobbyListStringFilter("GameState", "0", Equal); // 仅菜单状态
// 可选: 按名称/房间码过滤

5. 消息协议层

5.1 消息基类与类型

所有网络消息继承自 BaseMessage, 使用 MemoryPackUnion 实现多态序列化。

[MemoryPackable]
[MemoryPackUnion(1, typeof(StringMessage))]
[MemoryPackUnion(2, typeof(GameStartMessage))]
// ... 共 16 种消息类型
public abstract partial class BaseMessage
{
    public abstract P2PMsgType MessageType { get; }
}

5.2 消息类型总览

枚举值 消息类型 方向 说明
String (1) StringMessage 任意 调试用字符串消息
GameStart (2) GameStartMessage 房主 => 成员 开始游戏, 携带完整 MapData
ActionConfirm (3) ActionConfirmMessage 成员 => 房主 请求执行 Action
ActionExcute (4) ActionExcuteMessage 房主 => 成员 广播 Action 执行
TurnEnd (5) TurnEndMessage 成员 => 房主 请求结束回合
MapConfirm (6) MapConfirmMessage 双向 游戏数据心跳校验
ForceUpdate (7) ForceUpdateMessage 房主 => 成员 强制全量同步 MapData
ChangeCiv (8) ChangeCivMessage 成员 => 房主 修改文明/阵营
UpdateLobbyData (9) UpdateLobbyDataMessage 房主 => 成员 同步房间配置
RequestLobbyData (10) RequestLobbyDataMessage 成员 => 房主 请求房间配置
Heartbeat (11) HeartbeatMessage 成员 => 房主 成员心跳
MemberStateSync (12) MemberStateSyncMessage 房主 => 成员 房主心跳 + 状态下发
RequestForceUpdate (13) RequestForceUpdateMessage 成员 => 房主 请求断线重连
HeartbeatReply (14) HeartbeatReplyMessage 任意 => 任意 心跳回复(用于 Ping 计算)
ChatMessage (15) ChatMessage 双向 聊天消息
InviteMessage (16) InviteMessage 任意 => 任意 游戏内邀请(不经 P2P 连接)

5.3 关键消息数据结构

ActionNetData

[MemoryPackable]
public partial class ActionNetData
{
    public uint Version;           // Action 版本号(递增序列号)
    public string MapHash;         // 执行前的 MapData 哈希值
    public CommonActionParams Param; // Action 参数
    public CommonActionId ActionId;  // Action 类型标识
}

MapConfirmMessage

public partial class MapConfirmMessage : BaseMessage
{
    public ulong MemberId;         // 发送者 SteamID
    public uint PlayerId;          // 游戏内 PlayerId
    public int Index;              // 当前 Action 序列长度
    public ActionNetData ActionData; // 最后一个 ActionNetData (用于比对)
}

6. Action 同步机制

6.1 Action 系统概述

游戏中所有操作以 Action 为原子单位, 由 CommonActionType 枚举定义:

Action 类型 说明 主体
Gain 收获一次性资源 Grid
Build 建设建筑 Grid
StartWonder / BuildWonder 开启/建造奇观 Player / Grid
TrainUnit 训练单位 City
GridMisc 地格杂项操作 Grid
UnitAction 单位自身行为 Unit
CityLevelUpAction 城市升级 City
UnitSkill 单位技能 Unit
LearnTech 学习科技 Player
UnitMove 单位移动 Unit
UnitAttack / UnitAttackAlly / UnitAttackGround 单位攻击 Unit
PlayerAction 玩家行为(外交等) Player
TurnStart / TurnEnd 回合开始/结束 Player
PlayerSurrender 玩家投降 Player
BuyCultureCard 购买文化卡 Player
AIParamControl AI 参数控制 Player

6.2 Action 标识系统

每个 Action 由 CommonActionId 唯一标识, 组合了 ActionType + 各种子类型枚举。使用 Hash 计算唯一 ID:

public partial class CommonActionId
{
    public CommonActionType ActionType;
    public WonderTypeEnum WonderType;
    public ResourceType ResourceType;
    public UnitType UnitType;
    // ... 更多子类型字段
    
    public uint Id => ComputeId(); // 哈希唯一标识
}

6.3 Action 参数

public partial class CommonActionParams
{
    // 序列化字段(网络传输)
    public uint PlayerId, UnitId, CityId, GridId;
    public uint TargetUnitId, TargetGridId, TargetPlayerId;
    
    // 运行时引用(MemoryPackIgnore, 接收后通过 RefreshParams 恢复)
    [MemoryPackIgnore] public MapData MapData;
    [MemoryPackIgnore] public PlayerData PlayerData;
    [MemoryPackIgnore] public UnitData UnitData;
    // ...
}

接收端通过 RefreshParams() 根据 ID 重新绑定运行时引用。

6.4 房主执行 Action 流程

房主操作 UI
    │
    ▼
ActionLogicBase.CompleteExecute(params)
    │
    ├── 检查 Net.Mode == Multi && IsLobbyOwner
    │       │
    │       ▼
    │   GameNetSender.ActionExecute(id, params)  ──► 广播给所有成员
    │
    ├── BeforeExecute(params)
    │       └── 记录 ActionNetData 到 Net.Actions 列表
    │
    ├── Execute(params)          ──► 实际游戏逻辑
    │
    └── AfterExecute(params)
            └── 触发技能生命周期、刷新外交、更新 UI 等

6.5 成员执行 Action 流程

成员操作 UI
    │
    ▼
ActionLogicBase.CompleteExecute(params)
    │
    ├── 检查 Net.Mode == Multi && !IsLobbyOwner
    │       │
    │       ▼
    │   GameNetSender.ActionConfirm(id, params)  ──► 发送给房主
    │   (如果是 TurnEnd, 本地不执行, return false)
    │
    └── 不执行本地 Action(等待房主广播)

        ... 等待 ...

房主收到 ActionConfirmMessage
    │
    ├── 校验 Version 一致性
    ├── 校验当前回合玩家一致性
    ├── RefreshParams() 恢复引用
    │
    └── action.CompleteExecute(params)  ──► 房主本地执行 + 广播

成员收到 ActionExcuteMessage
    │
    ├── 校验 Version 一致性
    ├── 校验 MapHash (若不一致, 记录差异)
    ├── RefreshParams() 恢复引用
    │
    └── action.NetCompleteExecute(params)  ──► 成员本地执行(不触发广播)

6.6 回合结束流程

成员点击"结束回合"
    │
    ├── CompleteExecute(TurnEnd action)
    │       └── GameNetSender.TurnEndConfirm()  ──► 发送给房主
    │       └── return false (本地不执行)
    │
    ▼
房主收到 TurnEndMessage
    │
    └── Main.PlayerLogic.EndPlayerTurn(MapData, playerId)
            │
            └── 触发 TurnEnd + 下一个 TurnStart Action
                    │
                    └── 广播给所有成员

6.7 版本号机制

每个 Action 都携带版本号 (Version), 使用递增序列:

// NetData
public uint GetActionVersion()
{
    if (Actions.Count == 0) return 0;
    return Actions[^1].Version + 1;
}
  • 发送时: Version = MapData.Net.GetActionVersion()
  • 接收时: 校验 message.Version == 本地 GetActionVersion()
  • 不一致时: 触发 SendRequestForceUpdate() 请求全量同步

7. 心跳与状态监控

7.1 成员心跳 (成员 => 房主)

HeartbeatMessage {
    MemberId: ulong     // 发送者 SteamID
    State: GameState    // 当前游戏状态
}
  • 成员定时发送 SendHeartbeat() 给房主
  • 房主收到后:
    1. 回复 HeartbeatReply
    2. 更新 PlayerConfirmData.ConfirmTime
    3. 检查成员 GameState 是否异常(如成员在 Menu 而游戏还在进行)

7.2 房主状态同步 (房主 => 成员)

MemberStateSyncMessage {
    State: GameState                                    // 房主游戏状态
    MemberId: ulong                                     // 房主 SteamID
    PlayerConfirm: Dictionary<ulong, PlayerConfirmData> // 全体玩家状态
}
  • 房主定时广播 SendMemberStateSync()
  • 成员收到后:
    1. 回复 HeartbeatReply
    2. 检查自己的状态是否为 OK, 若不是则请求重连
    3. 检查游戏是否已结束(房主在 Menu 而自己还在游戏)

7.3 Ping 计算

// 发送心跳时记录时间
void OnSendHeartbeat() => PingRecordTime = Time.time;

// 收到心跳回复时计算 Ping
void OnReceiveHeartbeatReply() => Ping = Time.time - PingRecordTime;

7.4 玩家确认数据 (PlayerConfirmData)

public partial class PlayerConfirmData
{
    public ulong MemberId;           // Steam ID
    public MemberNetState State;     // 当前状态
    public float PingRecordTime;     // Ping 记录起始时间
    public float Ping;               // 实际 Ping 值
    public float ErrorTime;          // 进入错误状态的时间
    public float ConfirmTime;        // 最后收到心跳的时间
}

成员网络状态 (MemberNetState)

状态 说明 触发条件
None 未初始化 默认状态
OK 正常 心跳校验通过
Leaved 已离开 主动退出房间
Disconnected 断线 心跳检测成员状态异常
Timeout 超时 长时间未收到心跳
Error 数据错误 Action Hash 校验失败

AI 托管判定

// 当玩家离线或错误超过 10 秒, 由 AI 接管操作
public bool IsNeedAI()
{
    if (State == MemberNetState.Leaved) return true;
    return State != MemberNetState.OK && Time.time - ErrorTime > 10f;
}

8. 断线重连机制

8.1 触发条件

  1. 版本号超前: 成员收到的 ActionExcute 版本号 > 本地版本号
  2. 心跳状态异常: 成员发现自己在房主端的状态不是 OK
  3. MapHash 校验失败: 心跳校验发现 Action 数据不一致

8.2 重连流程

成员检测到需要重连
    │
    ▼
GameNetSender.SendRequestForceUpdate()
    ├── 发送 RequestForceUpdateMessage 给房主
    └── 切换游戏状态为 ForceUpdating(防止重复请求)
    
    ▼
房主收到 RequestForceUpdateMessage
    │
    └── GameNetSender.ForceUpdate(memberId)
            └── 发送 ForceUpdateMessage { MapData = Main.MapData } 给该成员
            
    ▼
成员收到 ForceUpdateMessage
    │
    ├── Step 1: Main.GameLogic.ChangeState(GameState.Menu) - 结束当前对局
    ├── Step 2: 关闭所有 UI
    ├── Step 3: Main.NetResumeMatch(message.MapData) - 使用房主的 MapData 恢复对局
    └── (显示断线重连提示 2 秒)

8.3 房主主动全量同步

房主也可以主动触发全量同步:

// 同步给单个成员
GameNetSender.ForceUpdate(memberId);

// 同步给所有成员
GameNetSender.BroadcastForceUpdate();

9. 数据一致性校验

9.1 MapData Hash

使用 MemoryPack 序列化 MapData 并计算 Unity Hash128:

public static string GetMapDataHash(MapData mapData)
{
    // 临时移除 Actions 列表(不参与哈希)
    var actions = mapData.Net.Actions;
    mapData.Net.Actions = null;
    
    byte[] bytes = MemoryPackSerializer.Serialize(mapData);
    mapData.Net.Actions = actions;
    
    var hash128 = new Hash128();
    hash128.Append(bytes);
    return hash128.ToString();
}

9.2 心跳校验 (MapConfirm)

成员 => 房主

// 成员发送校验
MapConfirmMessage {
    MemberId,
    PlayerId,
    Index = Actions.Count,               // 本地 Action 序列长度
    ActionData = Actions[^1]             // 最后一个 Action
}

房主校验逻辑

收到 MapConfirmMessage
    │
    ├── Index > 房主 Actions.Count ?
    │       └── 是: 标记该成员为 Error 状态
    │
    ├── Actions[Index-1].IsEqual(message.ActionData) ?
    │       └── 否: 标记该成员为 Error 状态
    │       └── 是: 标记该成员为 OK 状态
    │
    └── 输出日志: 本地 hash vs 消息 hash

成员校验逻辑

成员收到房主的 MapConfirmMessage
    │
    ├── Index > 本地 Actions.Count ?
    │       └── 是: 请求重连 (SendRequestForceUpdate)
    │
    └── Actions[Index-1].IsEqual(message.ActionData) ?
            └── 否: 请求重连 (SendRequestForceUpdate)

9.3 ActionNetData 判等

public bool IsEqual(ActionNetData other)
{
    if (Version != other.Version) return false;
    if (MapHash != other.MapHash) return false;
    if (ActionId != other.ActionId) return false;
    return true;
}

9.4 编译时校验 (CHECK_ACTIONDEFFERENCE)

#if CHECK_ACTIONDEFFERENCE
    // 每次 AfterExecute 后序列化一份 MapData 副本
    // 下次 BeforeExecute 时与当前 MapData 比较
    // 如发现差异, 输出详细日志
#endif

10. 房间生命周期

10.1 创建房间

调用 CreateLobby(maxMembers, isPublic)
    │
    ├── 状态: None => Creating
    ├── SteamMatchmaking.CreateLobby()
    │
    ▼ 回调: LobbyCreated_t
    │
    ├── 设置 LobbyData (Game, Owner, Version, RoomName, RoomCode, GameState)
    ├── 设置 Rich Presence
    ├── 状态: Creating => InLobby
    └── 触发 OnLobbyCreatedEvent

10.2 加入房间

调用 JoinLobby(lobbyId)
    │
    ├── 版本号检查(不一致则拒绝)
    ├── 状态: None => Joining
    ├── SteamMatchmaking.JoinLobby()
    │
    ▼ 回调: LobbyEnter_t
    │
    ├── 状态: Joining => InLobby
    ├── 检查是否被踢
    ├── 如果不是房主: SimpleP2P.ConnectToPeer(房主)
    ├── 刷新成员列表
    └── 触发 OnLobbyEnteredEvent

10.3 开始游戏

房主点击"开始游戏"
    │
    ├── 本地初始化游戏 (包括 MapData, NetData 等)
    ├── NetData.RefreshPlayerNet() - 建立 SteamID => PlayerId 映射
    ├── GameNetSender.GameStart() - 广播 GameStartMessage(携带完整 MapData)
    ├── LobbyManager.SetGameState(1) - 更新房间状态
    │
    ▼ 成员收到 GameStartMessage
    │
    ├── Main.NetStartGame(message.MapData) - 使用房主的 MapData 初始化
    └── 关闭大厅 UI, 进入游戏

10.4 离开房间

调用 LeaveLobby()
    │
    ├── 状态: InLobby => Leaving
    ├── SimpleP2P.DisconnectAll() - 断开所有 P2P 连接
    ├── SteamMatchmaking.LeaveLobby()
    ├── 重置状态 (CurrentLobby = Nil, CachedOwner = Nil)
    ├── 清除 Rich Presence
    └── 触发 OnLobbyLeftEvent

10.5 房主变更

房间内成员变化 (LobbyChatUpdate_t)
    │
    └── CheckOwnerChange()
            │
            ├── 新房主 == 自己 ?
            │       └── 是: 接管服务器角色
            │
            └── 新房主 != 自己 ?
                    ├── SimpleP2P.DisconnectAll()
                    └── SimpleP2P.ConnectToPeer(新房主)

10.6 踢人机制

Steam 没有直接踢人 API, 通过 LobbyData 标记实现:

// 房主设置踢人标记
SteamMatchmaking.SetLobbyMemberData(CurrentLobby, $"kick_{memberId}", "true");

// 成员定时检查(每 2 秒)
void CheckIfKicked()
{
    var kickData = SteamMatchmaking.GetLobbyMemberData(CurrentLobby, SteamUser.GetSteamID(), "kicked");
    var specificKick = SteamMatchmaking.GetLobbyData(CurrentLobby, $"kick_{selfId}");
    if (kickData == "true" || specificKick == "true") LeaveLobby();
}

11. 关键数据结构

11.1 NetData (联机核心数据, 挂载在 MapData.Net)

[MemoryPackable]
public partial class NetData
{
    public NetMode Mode;                            // 网络模式
    public uint CurPlayerId;                        // 当前操作的玩家 ID
    public Dictionary<ulong, uint> Players;         // SteamID => PlayerId 映射
    public List<ActionNetData> Actions;             // 所有行为的有序序列
    public int RandomSeed;                          // 确定性随机种子(房主生成)
    
    [MemoryPackIgnore] public float PlayerStartTime; // 玩家回合开始时间
    [MemoryPackIgnore] public float GameStartTime;   // 游戏开始时间
}

11.2 MapConfig (对局配置)

[MemoryPackable]
public partial class MapConfig
{
    public uint Width, Height;               // 地图尺寸
    public uint PlayerCount;                 // 玩家数量
    public AIDifficult AIDiff;               // AI 难度
    public GameMode GameMode;                // 游戏模式(征服/完美/创意)
    
    // 联机用
    public List<MemberCiv> MultiCivs;        // 成员文明选择列表
    
    // 超时控制
    public bool IsLimitTime;                 // 是否限时
    public int TimeLimitSeconds;             // 限时秒数(默认 180)
}

11.3 PlayerConfirmMap (玩家确认状态集合)

[MemoryPackable]
public partial class PlayerConfirmMap
{
    public Dictionary<ulong, PlayerConfirmData> PlayerConfirm;
    
    // 获取或创建(惰性初始化)
    public PlayerConfirmData GetPlayerConfirm(ulong memberId);
    
    // 批量更新(收到房主状态同步时)
    public void UpdatePlayerConfirm(Dictionary<ulong, PlayerConfirmData> playerConfirm);
}

11.4 LobbyListInfo (房间列表条目)

[MemoryPackable]
public partial class LobbyListInfo
{
    public ulong LobbyId;
    public ulong OwnerId;
    public string OwnerName;
    public string RoomName;
    public string Version;
    public int CurrentPlayers;
    public int MaxPlayers;
    public int GameState;
}

12. 时序图

12.1 完整 Action 同步时序 (成员操作)

   成员                        房主                       其他成员
    │                           │                           │
    │  ActionConfirmMessage     │                           │
    │ ─────────────────────►    │                           │
    │  {Version, MapHash,       │                           │
    │   ActionId, Params}       │                           │
    │                           │                           │
    │                    校验 Version                       │
    │                    校验 Player                        │
    │                    RefreshParams                      │
    │                    CompleteExecute                    │
    │                           │                           │
    │                           │  ActionExcuteMessage      │
    │  ActionExcuteMessage      │ ─────────────────────►    │
    │ ◄─────────────────────    │  {Version, MapHash,       │
    │  {Version, MapHash,       │   ActionId, Params}       │
    │   ActionId, Params}       │                           │
    │                           │                           │
    │  校验 Version             │              校验 Version │
    │  校验 MapHash             │              校验 MapHash │
    │  RefreshParams            │              RefreshParams│
    │  NetCompleteExecute       │        NetCompleteExecute │
    │                           │                           │

12.2 心跳与校验时序

   成员                        房主
    │                           │
    │  HeartbeatMessage         │
    │ ─────────────────────►    │
    │  {MemberId, GameState}    │
    │                           │
    │                    更新 ConfirmTime
    │                    检查状态异常
    │                           │
    │  HeartbeatReplyMessage    │
    │ ◄─────────────────────    │
    │  {MemberId}               │
    │                           │
    │  计算 Ping                │
    │                           │
    │  MemberStateSyncMessage   │
    │ ◄─────────────────────    │
    │  {State, PlayerConfirm}   │
    │                           │
    │  检查自身状态              │
    │  如异常 => 请求重连        │
    │                           │
    │  MapConfirmMessage        │
    │ ─────────────────────►    │
    │  {Index, ActionData}      │
    │                           │
    │                    校验 Action 一致性
    │                    更新成员状态
    │                           │

12.3 断线重连时序

   成员                        房主
    │                           │
    │  检测到版本号/Hash不一致    │
    │  或收到异常状态            │
    │                           │
    │  RequestForceUpdateMessage│
    │ ─────────────────────►    │
    │  {MemberId}               │
    │                           │
    │  切换状态:ForceUpdating   │
    │                           │
    │  ForceUpdateMessage       │
    │ ◄─────────────────────    │
    │  {完整 MapData}           │
    │                           │
    │  结束当前游戏              │
    │  关闭所有 UI              │
    │  NetResumeMatch(MapData)  │
    │  恢复游戏                 │
    │                           │

13. 文件索引

Steam 集成层

文件 路径 说明
SimpleP2P.cs TH1_Logic/Steam/ P2P 底层连接管理
SteamLobbyManager.cs TH1_Logic/Steam/ Steam 房间管理 (ILobby 实现)
SteamObjectSerializer.cs TH1_Logic/Steam/ P2P 消息类型定义 & 序列化
GameNetSender.cs TH1_Logic/Steam/ 网络消息发送端
GameNetReceiver.cs TH1_Logic/Steam/ 网络消息接收端
SteamGUIMono.cs TH1_Logic/Steam/ 调试 GUI (F3 开关)

网络抽象层

文件 路径 说明
ILobby.cs TH1_Logic/Net/ 房间接口定义
LobbyManager.cs TH1_Logic/Net/ 房间管理器单例

数据层

文件 路径 说明
NetData.cs TH1_Data/ 网络数据 (模式、玩家映射、Action 序列、随机种子)
MapData.cs TH1_Data/ 地图数据 (含 MapConfig、Net 等)
PlayerData.cs TH1_Data/ 玩家数据
MatchData.cs TH1_Data/ 对局限制数据
GridData.cs TH1_Data/ 地格数据
CityData.cs TH1_Data/ 城市数据
UnitData.cs TH1_Data/ 单位数据
RuntimeData.cs TH1_Data/ 运行时数据

Action 逻辑层

文件 路径 说明
ActionLogic.cs TH1_Logic/Action/ Action 基类、工厂、参数、ID 系统
BuildActionLogic.cs TH1_Logic/Action/ 建设类 Action 逻辑
GridMiscActionLogic.cs TH1_Logic/Action/ 地格杂项 Action 逻辑
PlayerActionLogic.cs TH1_Logic/Action/ 玩家行为 Action 逻辑(外交、英雄等)
TrainUnitActionLogic.cs TH1_Logic/Action/ 训练单位 Action 逻辑
UnitActionLogic.cs TH1_Logic/Action/ 单位自身行为 Action 逻辑
AIActionBase.cs TH1_Logic/AI/ AI 行为基类 (含 ActionNetData 定义)

附录 A: 关键单例访问

SimpleP2P.Instance                    // P2P 连接管理
LobbyManager.Instance.Lobby           // 房间接口(ILobby)
GameNetSender.Instance                // 消息发送
GameNetReceiver.Instance              // 消息接收
Main.MapData                          // 当前地图数据
Main.MapData.Net                      // 网络数据(NetData)
Main.Instance.ConfirmMap              // 玩家确认状态集合
Main.Instance.MapConfig               // 当前对局配置

附录 B: 调试工具

  • F3: 开关 Steam 调试 GUI (SteamGUIMono)
  • 调试面板内容:
    • Steam 初始化状态、用户信息
    • 房间状态、成员列表、P2P 连接数
    • 房主可踢出成员
    • 好友列表 & 邀请
    • 文明切换按钮

附录 C: 注意事项

  1. 房主端口清理: 初始化监听套接字前会强制清理端口 1214 上的旧连接
  2. 版本控制: 加入房间时校验 Version 字段, 版本不一致无法加入
  3. Steam 隐私限制: Steam 客户端 API 无法获取玩家资料隐私设置, 需 UI 提示
  4. 嵌套 Action: ExecuteWithoutFullActionPeriod() 用于 Action 内部嵌套调用其他 Action, 不触发网络同步
  5. AI 托管: 玩家断线超过 10 秒或已离开时, 由 AI 自动接管其操作
  6. 随机数一致性: 所有 Action 逻辑必须通过 NetData.GetRandom(map) 获取随机数, 保证跨端一致