增加保护
This commit is contained in:
parent
922ddaba9a
commit
12a84aec53
999
MD/Multiplayer_System.md
Normal file
999
MD/Multiplayer_System.md
Normal file
@ -0,0 +1,999 @@
|
||||
# TOHOTOPIA 联机系统技术文档
|
||||
|
||||
> 最后更新: 2026/04/13
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [系统概述](#1-系统概述)
|
||||
2. [网络拓扑结构](#2-网络拓扑结构)
|
||||
3. [核心类层级与职责](#3-核心类层级与职责)
|
||||
4. [Steam 集成层](#4-steam-集成层)
|
||||
5. [消息协议层](#5-消息协议层)
|
||||
6. [Action 同步机制](#6-action-同步机制)
|
||||
7. [心跳与状态监控](#7-心跳与状态监控)
|
||||
8. [断线重连机制](#8-断线重连机制)
|
||||
9. [数据一致性校验](#9-数据一致性校验)
|
||||
10. [房间生命周期](#10-房间生命周期)
|
||||
11. [关键数据结构](#11-关键数据结构)
|
||||
12. [时序图](#12-时序图)
|
||||
13. [文件索引](#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)
|
||||
```
|
||||
|
||||
### 平台切换
|
||||
|
||||
```csharp
|
||||
// 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()`
|
||||
|
||||
#### 房间搜索过滤
|
||||
|
||||
```csharp
|
||||
// 默认搜索条件
|
||||
SteamMatchmaking.AddRequestLobbyListDistanceFilter(Worldwide);
|
||||
SteamMatchmaking.AddRequestLobbyListStringFilter("Game", "TOHOTOPIA", Equal);
|
||||
SteamMatchmaking.AddRequestLobbyListStringFilter("GameState", "0", Equal); // 仅菜单状态
|
||||
// 可选: 按名称/房间码过滤
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 消息协议层
|
||||
|
||||
### 5.1 消息基类与类型
|
||||
|
||||
所有网络消息继承自 `BaseMessage`, 使用 `MemoryPackUnion` 实现多态序列化。
|
||||
|
||||
```csharp
|
||||
[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
|
||||
|
||||
```csharp
|
||||
[MemoryPackable]
|
||||
public partial class ActionNetData
|
||||
{
|
||||
public uint Version; // Action 版本号(递增序列号)
|
||||
public string MapHash; // 执行前的 MapData 哈希值
|
||||
public CommonActionParams Param; // Action 参数
|
||||
public CommonActionId ActionId; // Action 类型标识
|
||||
}
|
||||
```
|
||||
|
||||
#### MapConfirmMessage
|
||||
|
||||
```csharp
|
||||
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:
|
||||
|
||||
```csharp
|
||||
public partial class CommonActionId
|
||||
{
|
||||
public CommonActionType ActionType;
|
||||
public WonderTypeEnum WonderType;
|
||||
public ResourceType ResourceType;
|
||||
public UnitType UnitType;
|
||||
// ... 更多子类型字段
|
||||
|
||||
public uint Id => ComputeId(); // 哈希唯一标识
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 Action 参数
|
||||
|
||||
```csharp
|
||||
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`), 使用递增序列:
|
||||
|
||||
```csharp
|
||||
// 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 计算
|
||||
|
||||
```csharp
|
||||
// 发送心跳时记录时间
|
||||
void OnSendHeartbeat() => PingRecordTime = Time.time;
|
||||
|
||||
// 收到心跳回复时计算 Ping
|
||||
void OnReceiveHeartbeatReply() => Ping = Time.time - PingRecordTime;
|
||||
```
|
||||
|
||||
### 7.4 玩家确认数据 (`PlayerConfirmData`)
|
||||
|
||||
```csharp
|
||||
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 托管判定
|
||||
|
||||
```csharp
|
||||
// 当玩家离线或错误超过 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 房主主动全量同步
|
||||
|
||||
房主也可以主动触发全量同步:
|
||||
|
||||
```csharp
|
||||
// 同步给单个成员
|
||||
GameNetSender.ForceUpdate(memberId);
|
||||
|
||||
// 同步给所有成员
|
||||
GameNetSender.BroadcastForceUpdate();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 数据一致性校验
|
||||
|
||||
### 9.1 MapData Hash
|
||||
|
||||
使用 MemoryPack 序列化 MapData 并计算 Unity `Hash128`:
|
||||
|
||||
```csharp
|
||||
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)
|
||||
|
||||
#### 成员 => 房主
|
||||
|
||||
```csharp
|
||||
// 成员发送校验
|
||||
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 判等
|
||||
|
||||
```csharp
|
||||
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 标记实现:
|
||||
|
||||
```csharp
|
||||
// 房主设置踢人标记
|
||||
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)
|
||||
|
||||
```csharp
|
||||
[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 (对局配置)
|
||||
|
||||
```csharp
|
||||
[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 (玩家确认状态集合)
|
||||
|
||||
```csharp
|
||||
[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 (房间列表条目)
|
||||
|
||||
```csharp
|
||||
[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: 关键单例访问
|
||||
|
||||
```csharp
|
||||
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)` 获取随机数, 保证跨端一致
|
||||
@ -79,7 +79,7 @@ namespace TH1_Core.Managers
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (task == null)
|
||||
{
|
||||
Debug.LogError("试图添加一个空的任务!");
|
||||
@ -87,7 +87,8 @@ namespace TH1_Core.Managers
|
||||
}
|
||||
|
||||
//如果当前不是我的回合,并且进来了一个UISequencerTask,那么要先缓存,直到我的回合再显示出来
|
||||
if (Main.MapData.CurPlayer != Main.MapData.PlayerMap.SelfPlayerData && (task as UISequencerTask) != null)
|
||||
if (Main.MapData?.CurPlayer != null && Main.MapData?.PlayerMap?.SelfPlayerData != null &&
|
||||
Main.MapData.CurPlayer != Main.MapData.PlayerMap.SelfPlayerData && (task as UISequencerTask) != null)
|
||||
{
|
||||
_taskNotCurPlayList.Enqueue(task);
|
||||
}
|
||||
@ -147,7 +148,8 @@ namespace TH1_Core.Managers
|
||||
|
||||
public static void CheckNotCurQueue()
|
||||
{
|
||||
|
||||
|
||||
if (Main.MapData?.CurPlayer == null || Main.MapData?.PlayerMap?.SelfPlayerData == null) return;
|
||||
if (Main.MapData.CurPlayer != Main.MapData.PlayerMap.SelfPlayerData)return;
|
||||
while(_taskNotCurPlayList.Count > 0)
|
||||
_taskQueue.Enqueue(_taskNotCurPlayList.Dequeue());
|
||||
|
||||
@ -1840,6 +1840,9 @@ namespace Logic.Action
|
||||
|
||||
protected override bool Execute(CommonActionParams actionParams)
|
||||
{
|
||||
//Step #0 鲁棒性保护
|
||||
if (actionParams?.UnitData == null || actionParams.TargetUnitData == null || actionParams.MapData == null) return false;
|
||||
|
||||
//Step #0 为anim做数据准备
|
||||
GridData originGrid = null;
|
||||
GridData targetGrid = null;
|
||||
@ -1859,10 +1862,13 @@ namespace Logic.Action
|
||||
originCity = actionParams.UnitData.City(Main.MapData);
|
||||
targetCity = actionParams.TargetUnitData.City(Main.MapData);
|
||||
originPlayer = actionParams.UnitData.Player(Main.MapData);
|
||||
MapRenderer.Instance.ROUnitMap.TryGetValue(actionParams.UnitData.Id, out originUnitRenderer);
|
||||
MapRenderer.Instance.ROUnitMap.TryGetValue(actionParams.TargetUnitData.Id, out targetUnitRenderer);
|
||||
originUnitProjectileType = actionParams.UnitData.GetProjectileType(actionParams.MapData,targetGrid);
|
||||
targetUnitProjectileType = actionParams.TargetUnitData.GetProjectileType(actionParams.MapData,targetGrid);
|
||||
MapRenderer.Instance?.ROUnitMap?.TryGetValue(actionParams.UnitData.Id, out originUnitRenderer);
|
||||
MapRenderer.Instance?.ROUnitMap?.TryGetValue(actionParams.TargetUnitData.Id, out targetUnitRenderer);
|
||||
if (targetGrid != null)
|
||||
{
|
||||
originUnitProjectileType = actionParams.UnitData.GetProjectileType(actionParams.MapData,targetGrid);
|
||||
targetUnitProjectileType = actionParams.TargetUnitData.GetProjectileType(actionParams.MapData,targetGrid);
|
||||
}
|
||||
}
|
||||
|
||||
//Step #2 处理逻辑计算,提前存储anim使用到的数据
|
||||
@ -1882,8 +1888,9 @@ namespace Logic.Action
|
||||
bool pushed = originGridAfter != null && originGridAfter.Id != originGrid.Id && originGridAfter.Id == targetGrid.Id;
|
||||
|
||||
//如果在视野,播放动画
|
||||
if (Main.MapData.PlayerMap.SelfPlayerData.Sight.CheckIsInSight(originGrid.Id) ||
|
||||
Main.MapData.PlayerMap.SelfPlayerData.Sight.CheckIsInSight(targetGrid.Id))
|
||||
if (Main.MapData?.PlayerMap?.SelfPlayerData?.Sight != null &&
|
||||
(Main.MapData.PlayerMap.SelfPlayerData.Sight.CheckIsInSight(originGrid.Id) ||
|
||||
Main.MapData.PlayerMap.SelfPlayerData.Sight.CheckIsInSight(targetGrid.Id)))
|
||||
{
|
||||
//如果发生了推动,先播放双方的移动动画,再播放攻击动画
|
||||
//推击保证不会击杀target,所以targetGridAfter!=null可以正确区分推击和MoveKill
|
||||
|
||||
@ -233,7 +233,9 @@ namespace Logic
|
||||
public void OnDisconnectToHost()
|
||||
{
|
||||
if (_curState == GameState.Menu) return;
|
||||
if (Main.MapData?.Net == null) return;
|
||||
if (Main.MapData.Net.Mode != NetMode.Multi) return;
|
||||
if (!LobbyManager.Instance.Lobby.IsInitialized()) return;
|
||||
if (LobbyManager.Instance.Lobby.IsLobbyOwner()) return;
|
||||
EventManager.Publish(new ExecuteUIBottomBottomBarQuit());
|
||||
}
|
||||
|
||||
@ -39,15 +39,16 @@ namespace Logic.Skill
|
||||
|
||||
public override void OnDamaged(MapData mapData, SettlementInfo info)
|
||||
{
|
||||
if (info == null || !info.IsKill || info.DamageTargetGrid == null || info.DamageTargetCity == null) return;
|
||||
if (mapData == null) return;
|
||||
if (info == null || !info.IsKill || info.DamageTarget == null || info.DamageTargetGrid == null || info.DamageTargetCity == null) return;
|
||||
var tmpUnitFullType = info.DamageTarget.UnitFullType;
|
||||
UpdateFullType(info.DamageTarget);
|
||||
//如果要复活的位置上有地方单位,先将对方passiveMove开
|
||||
if (info.DamageTargetGrid.RealUnit(mapData, out var unit))
|
||||
Main.UnitLogic.PassiveMoveAway(mapData,unit);
|
||||
mapData.AddUnitData(info.DamageTargetGrid.Id, info.DamageTargetCity.Id, FullType,out var newUnit);
|
||||
newUnit.OriginUnitFullType = tmpUnitFullType;
|
||||
|
||||
if (newUnit != null)
|
||||
newUnit.OriginUnitFullType = tmpUnitFullType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,134 +0,0 @@
|
||||
/*
|
||||
* @Author: 白哉
|
||||
* @Description:
|
||||
* @Date: 2026年03月10日 星期二 14:03:29
|
||||
* @Modify:
|
||||
*/
|
||||
|
||||
|
||||
using System;
|
||||
using Logic.CrashSight;
|
||||
using Steamworks;
|
||||
using UnityEngine;
|
||||
|
||||
|
||||
namespace TH1_Logic.Tools
|
||||
{
|
||||
public static class SteamCloudStorage
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否可用(Steamworks 已初始化且云存储已启用)
|
||||
/// </summary>
|
||||
public static bool IsAvailable()
|
||||
{
|
||||
try
|
||||
{
|
||||
return SteamManager.Initialized && SteamRemoteStorage.IsCloudEnabledForAccount()
|
||||
&& SteamRemoteStorage.IsCloudEnabledForApp();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入文件到 Steam Cloud
|
||||
/// </summary>
|
||||
/// <param name="fileName">虚拟文件名,如 "achievement.json"</param>
|
||||
/// <param name="data">UTF-8 字节数据</param>
|
||||
/// <returns>是否成功</returns>
|
||||
public static bool WriteFile(string fileName, byte[] data)
|
||||
{
|
||||
if (!IsAvailable()) return false;
|
||||
|
||||
try
|
||||
{
|
||||
bool success = SteamRemoteStorage.FileWrite(fileName, data, data.Length);
|
||||
if (!success)
|
||||
{
|
||||
LogSystem.LogError($"[SteamCloud] 写入失败: {fileName}");
|
||||
}
|
||||
return success;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogSystem.LogError($"[SteamCloud] 写入异常: {fileName} | {e.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 Steam Cloud 读取文件
|
||||
/// </summary>
|
||||
/// <param name="fileName">虚拟文件名</param>
|
||||
/// <returns>文件内容字节数组,失败返回 null</returns>
|
||||
public static byte[] ReadFile(string fileName)
|
||||
{
|
||||
if (!IsAvailable()) return null;
|
||||
|
||||
try
|
||||
{
|
||||
if (!SteamRemoteStorage.FileExists(fileName)) return null;
|
||||
|
||||
int size = SteamRemoteStorage.GetFileSize(fileName);
|
||||
if (size <= 0) return null;
|
||||
|
||||
byte[] buffer = new byte[size];
|
||||
int readBytes = SteamRemoteStorage.FileRead(fileName, buffer, size);
|
||||
|
||||
if (readBytes <= 0) return null;
|
||||
|
||||
// 实际读取长度可能小于 buffer 大小
|
||||
if (readBytes < size)
|
||||
{
|
||||
byte[] trimmed = new byte[readBytes];
|
||||
Array.Copy(buffer, trimmed, readBytes);
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogSystem.LogError($"[SteamCloud] 读取异常: {fileName} | {e.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除 Steam Cloud 上的文件
|
||||
/// </summary>
|
||||
public static bool DeleteFile(string fileName)
|
||||
{
|
||||
if (!IsAvailable()) return false;
|
||||
|
||||
try
|
||||
{
|
||||
return SteamRemoteStorage.FileDelete(fileName);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogSystem.LogError($"[SteamCloud] 删除异常: {fileName} | {e.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查文件是否存在于 Steam Cloud
|
||||
/// </summary>
|
||||
public static bool FileExists(string fileName)
|
||||
{
|
||||
if (!IsAvailable()) return false;
|
||||
|
||||
try
|
||||
{
|
||||
return SteamRemoteStorage.FileExists(fileName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9a96892e098a418ba18da3d76258893b
|
||||
timeCreated: 1773123973
|
||||
@ -1,399 +0,0 @@
|
||||
// using System.Collections.Generic;
|
||||
// using System.IO;
|
||||
// using UnityEngine;
|
||||
// using Steamworks;
|
||||
// using Logic.CrashSight;
|
||||
// using TH1_Logic.Net;
|
||||
//
|
||||
//
|
||||
// namespace TH1_Logic.Steam
|
||||
// {
|
||||
// public class TH1SteamManager
|
||||
// {
|
||||
// public static TH1SteamManager Instance { get; } = new TH1SteamManager();
|
||||
//
|
||||
// [Header("Steam 连接状态")]
|
||||
// public bool IsSteamInitialized = false;
|
||||
// public bool IsLoggedIn = false;
|
||||
// public string CurrentUserName = "";
|
||||
// public CSteamID CurrentUserID;
|
||||
//
|
||||
// [Header("房间测试")]
|
||||
// private int _maxLobbyMembers = 4;
|
||||
// private SteamLobbyManager _lobby;
|
||||
// private GameObject _guiObj;
|
||||
//
|
||||
//
|
||||
// // 初始化Steam
|
||||
// public void Init()
|
||||
// {
|
||||
// LogSystem.LogInfo("开始初始化Steam...");
|
||||
// LogSystem.LogInfo($"启动方式检测: {(SteamAPI.RestartAppIfNecessary(new Steamworks.AppId_t(480)) ? "需要通过Steam启动" : "直接启动或已通过Steam启动")}");
|
||||
//
|
||||
// // 检查Steam是否运行
|
||||
// if (!SteamAPI.IsSteamRunning())
|
||||
// {
|
||||
// LogSystem.LogError("Steam客户端未运行!请先启动Steam。");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // 检查steam_appid.txt文件状态
|
||||
// CheckSteamAppIdFile();
|
||||
//
|
||||
// // 初始化Steam API
|
||||
// IsSteamInitialized = SteamAPI.Init();
|
||||
//
|
||||
// if (!IsSteamInitialized)
|
||||
// {
|
||||
// LogSystem.LogError("Steam API初始化失败!");
|
||||
// LogSystem.LogError("可能的原因:");
|
||||
// LogSystem.LogError("1. Steam客户端未运行");
|
||||
// LogSystem.LogError("2. steam_appid.txt文件配置错误(开发环境)");
|
||||
// LogSystem.LogError("3. 没有以Steam方式启动应用(开发环境)");
|
||||
// LogSystem.LogError("4. Steam App ID不匹配");
|
||||
// return;
|
||||
// }
|
||||
// LogSystem.LogInfo("Steam API初始化成功!");
|
||||
//
|
||||
// // 检查用户登录状态
|
||||
// try
|
||||
// {
|
||||
// CheckUserLoginStatus();
|
||||
// }
|
||||
// catch (System.Exception e)
|
||||
// {
|
||||
// LogSystem.LogError($"检查用户登录状态异常: {e.Message}");
|
||||
// }
|
||||
//
|
||||
// // 显示启动信息
|
||||
// DisplayLaunchInfo();
|
||||
//
|
||||
// // 初始化Steam房间管理器
|
||||
// try
|
||||
// {
|
||||
// InitializeSteamLobbyManager();
|
||||
// }
|
||||
// catch (System.Exception e)
|
||||
// {
|
||||
// LogSystem.LogError($"Steam房间管理器初始化异常: {e.Message}");
|
||||
// }
|
||||
//
|
||||
// LogSystem.LogInfo("Steam测试环境初始化完成!");
|
||||
//
|
||||
// // 构建 GUI 输入 Mono
|
||||
// _guiObj = new GameObject();
|
||||
// _guiObj.name = "SteamGUI";
|
||||
// _guiObj.AddComponent<SteamGUIMono>();
|
||||
// _guiObj.SetActive(true);
|
||||
// }
|
||||
//
|
||||
// // 定时更新
|
||||
// public void Update()
|
||||
// {
|
||||
// if (!IsSteamInitialized || !IsLoggedIn) return;
|
||||
//
|
||||
// // 更新Steam回调
|
||||
// SteamAPI.RunCallbacks();
|
||||
//
|
||||
// // 更新P2P消息
|
||||
// _lobby.Update();
|
||||
// }
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 检查steam_appid.txt文件状态
|
||||
// /// </summary>
|
||||
// private void CheckSteamAppIdFile()
|
||||
// {
|
||||
// string steamAppIdPath = Path.Combine(Directory.GetCurrentDirectory(), "steam_appid.txt");
|
||||
//
|
||||
// if (File.Exists(steamAppIdPath))
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// string content = File.ReadAllText(steamAppIdPath).Trim();
|
||||
// LogSystem.LogInfo($"发现steam_appid.txt文件,App ID: {content}");
|
||||
// }
|
||||
// catch (System.Exception e)
|
||||
// {
|
||||
// LogSystem.LogWarning($"读取steam_appid.txt失败: {e.Message}");
|
||||
// }
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// LogSystem.LogInfo("未发现steam_appid.txt文件 - 这在Steam平台发布时是正常的");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 显示启动信息
|
||||
// /// </summary>
|
||||
// private void DisplayLaunchInfo()
|
||||
// {
|
||||
// if (!IsSteamInitialized) return;
|
||||
//
|
||||
// // 获取当前App ID
|
||||
// var currentAppId = SteamUtils.GetAppID();
|
||||
// LogSystem.LogInfo($"当前Steam App ID: {currentAppId}");
|
||||
//
|
||||
// // 检查启动方式
|
||||
// bool launchedViaSteam = SteamApps.BIsSubscribedApp(currentAppId);
|
||||
// LogSystem.LogInfo($"通过Steam启动: {(launchedViaSteam ? "是" : "否")}");
|
||||
//
|
||||
// // 显示Steam环境信息
|
||||
// LogSystem.LogInfo($"Steam语言: {SteamApps.GetCurrentGameLanguage()}");
|
||||
// LogSystem.LogInfo($"Steam服务器连接: {(SteamUser.BLoggedOn() ? "已连接" : "未连接")}");
|
||||
//
|
||||
// // 检查DLC和订阅状态
|
||||
// if (currentAppId.m_AppId == 480) // Spacewar测试应用
|
||||
// {
|
||||
// LogSystem.LogInfo("当前使用Spacewar测试应用 - 适用于开发测试");
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// LogSystem.LogInfo($"当前使用正式应用ID: {currentAppId.m_AppId}");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 检查用户登录状态
|
||||
// /// </summary>
|
||||
// private void CheckUserLoginStatus()
|
||||
// {
|
||||
// if (!IsSteamInitialized) return;
|
||||
//
|
||||
// IsLoggedIn = SteamUser.BLoggedOn();
|
||||
//
|
||||
// if (IsLoggedIn)
|
||||
// {
|
||||
// CurrentUserID = SteamUser.GetSteamID();
|
||||
// CurrentUserName = SteamFriends.GetPersonaName();
|
||||
// LogSystem.LogInfo($"Steam用户已登录: {CurrentUserName} ({CurrentUserID})");
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// LogSystem.LogWarning("Steam用户未登录");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 初始化Steam房间管理器
|
||||
// /// </summary>
|
||||
// private void InitializeSteamLobbyManager()
|
||||
// {
|
||||
// if (!IsSteamInitialized || !IsLoggedIn) return;
|
||||
// try
|
||||
// {
|
||||
// _lobby ??= new SteamLobbyManager();
|
||||
// // 初始化房间管理器
|
||||
// _lobby.InitCallbacks();
|
||||
//
|
||||
// // 订阅事件
|
||||
// _lobby.OnLobbyCreatedEvent += OnLobbyCreated;
|
||||
// _lobby.OnLobbyEnteredEvent += OnLobbyEntered;
|
||||
// _lobby.OnLobbyLeftEvent += OnLobbyLeft;
|
||||
// _lobby.OnMemberJoinedEvent += OnMemberJoined;
|
||||
// _lobby.OnMemberLeftEvent += OnMemberLeft;
|
||||
// _lobby.OnHostChangedEvent += OnHostChanged;
|
||||
// _lobby.OnLobbyErrorEvent += OnLobbyError;
|
||||
//
|
||||
// // 订阅P2P事件
|
||||
// SimpleP2P.Instance.OnPeerConnectedEvent += OnP2PPeerConnected;
|
||||
// SimpleP2P.Instance.OnPeerDisconnectedEvent += OnP2PPeerDisconnected;
|
||||
// SimpleP2P.Instance.OnMessageReceivedEvent += OnP2PMessageReceived;
|
||||
// SimpleP2P.Instance.OnConnectionErrorEvent += OnP2PConnectionError;
|
||||
//
|
||||
// LogSystem.LogInfo("Steam房间管理器初始化成功!");
|
||||
// LobbyManager.Instance.Lobby = _lobby;
|
||||
// }
|
||||
// catch (System.Exception e)
|
||||
// {
|
||||
// LogSystem.LogError($"Steam房间管理器初始化失败: {e.Message}");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private void OnDestroy()
|
||||
// {
|
||||
// if (IsSteamInitialized)
|
||||
// {
|
||||
// _lobby.Cleanup();
|
||||
// SteamAPI.Shutdown();
|
||||
// LogSystem.LogInfo("Steam API已关闭");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// #region 房间事件处理
|
||||
//
|
||||
// private void OnLobbyCreated(CSteamID lobbyId)
|
||||
// {
|
||||
// LogSystem.LogInfo($"[测试] 房间创建成功: {lobbyId}");
|
||||
// }
|
||||
//
|
||||
// private void OnLobbyEntered(CSteamID lobbyId)
|
||||
// {
|
||||
// LogSystem.LogInfo($"[测试] 进入房间: {lobbyId}");
|
||||
// LogSystem.LogInfo($"[测试] 房间成员数: {_lobby.GetMemberCount()}/{_lobby.GetMemberLimit()}");
|
||||
// }
|
||||
//
|
||||
// private void OnLobbyLeft()
|
||||
// {
|
||||
// LogSystem.LogInfo("[测试] 离开房间");
|
||||
// }
|
||||
//
|
||||
// private void OnMemberJoined(CSteamID memberId)
|
||||
// {
|
||||
// string memberName = SteamFriends.GetFriendPersonaName(memberId);
|
||||
// LogSystem.LogInfo($"[测试] 成员加入: {memberName} ({memberId})");
|
||||
// }
|
||||
//
|
||||
// private void OnMemberLeft(CSteamID memberId)
|
||||
// {
|
||||
// string memberName = SteamFriends.GetFriendPersonaName(memberId);
|
||||
// LogSystem.LogInfo($"[测试] 成员离开: {memberName} ({memberId})");
|
||||
// }
|
||||
//
|
||||
// private void OnHostChanged(CSteamID oldHost, CSteamID newHost)
|
||||
// {
|
||||
// string oldHostName = SteamFriends.GetFriendPersonaName(oldHost);
|
||||
// string newHostName = SteamFriends.GetFriendPersonaName(newHost);
|
||||
// LogSystem.LogInfo($"[测试] 房主变更: {oldHostName} -> {newHostName}");
|
||||
// }
|
||||
//
|
||||
// private void OnLobbyError(string error)
|
||||
// {
|
||||
// LogSystem.LogError($"[测试] 房间错误: {error}");
|
||||
// }
|
||||
//
|
||||
// #endregion
|
||||
//
|
||||
// #region P2P事件处理
|
||||
//
|
||||
// private void OnP2PPeerConnected(CSteamID peerId)
|
||||
// {
|
||||
// string peerName = SteamFriends.GetFriendPersonaName(peerId);
|
||||
// LogSystem.LogInfo($"[测试] P2P连接建立: {peerName} ({peerId})");
|
||||
// }
|
||||
//
|
||||
// private void OnP2PPeerDisconnected(CSteamID peerId)
|
||||
// {
|
||||
// string peerName = SteamFriends.GetFriendPersonaName(peerId);
|
||||
// LogSystem.LogInfo($"[测试] P2P连接断开: {peerName} ({peerId})");
|
||||
// }
|
||||
//
|
||||
// private void OnP2PMessageReceived(CSteamID senderId, byte[] data)
|
||||
// {
|
||||
// string senderName = SteamFriends.GetFriendPersonaName(senderId);
|
||||
// string message = System.Text.Encoding.UTF8.GetString(data);
|
||||
// LogSystem.LogInfo($"[测试] 收到P2P消息 from {senderName}: {message}");
|
||||
// }
|
||||
//
|
||||
// private void OnP2PConnectionError(string error)
|
||||
// {
|
||||
// LogSystem.LogError($"[测试] P2P连接错误: {error}");
|
||||
// }
|
||||
//
|
||||
// #endregion
|
||||
//
|
||||
// #region 测试方法
|
||||
//
|
||||
// // 创建测试房间
|
||||
// public void CreateLobby()
|
||||
// {
|
||||
// if (!CanPerformLobbyAction()) return;
|
||||
//
|
||||
// LogSystem.LogInfo("[测试] 创建房间...");
|
||||
// _lobby.CreateFriendsLobby(_maxLobbyMembers);
|
||||
// }
|
||||
//
|
||||
// // 离开房间
|
||||
// public void LeaveLobby()
|
||||
// {
|
||||
// if (!_lobby.IsInLobby())
|
||||
// {
|
||||
// LogSystem.LogWarning("[测试] 当前不在房间中");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// LogSystem.LogInfo("[测试] 离开房间...");
|
||||
// _lobby.LeaveLobby();
|
||||
// }
|
||||
//
|
||||
// // 解散房间
|
||||
// public void DisbandLobby()
|
||||
// {
|
||||
// if (!_lobby.IsInLobby())
|
||||
// {
|
||||
// LogSystem.LogWarning("[测试] 当前不在房间中");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// if (!_lobby.IsLobbyOwner())
|
||||
// {
|
||||
// LogSystem.LogWarning("[测试] 只有房主可以解散房间");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// LogSystem.LogInfo("[测试] 解散房间...");
|
||||
// _lobby.DisbandLobby();
|
||||
// }
|
||||
//
|
||||
// // 发送测试消息
|
||||
// public void SendMessage()
|
||||
// {
|
||||
// if (!_lobby.IsInLobby())
|
||||
// {
|
||||
// LogSystem.LogWarning("[测试] 当前不在房间中,无法发送消息");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// string testMessage = $"测试消息 from {CurrentUserName} at {System.DateTime.Now:HH:mm:ss}";
|
||||
// GameNetSender.Instance.BroadcastString(testMessage);
|
||||
// LogSystem.LogInfo($"[测试] 广播消息: {testMessage}");
|
||||
// }
|
||||
//
|
||||
// // 显示在线好友
|
||||
// public void ShowOnlineFriends()
|
||||
// {
|
||||
// if (!CanPerformLobbyAction()) return;
|
||||
//
|
||||
// var friends = _lobby.GetOnlineFriends();
|
||||
// LogSystem.LogInfo($"[测试] 在线好友数量: {friends.Count}");
|
||||
//
|
||||
// foreach (var friend in friends)
|
||||
// {
|
||||
// LogSystem.LogInfo($"[测试] 好友: {friend.name} ({friend.id})");
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // 打开邀请界面
|
||||
// public void OpenInviteOverlay()
|
||||
// {
|
||||
// if (!_lobby.IsInLobby())
|
||||
// {
|
||||
// LogSystem.LogWarning("[测试] 当前不在房间中,无法邀请好友");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// LogSystem.LogInfo("[测试] 打开Steam邀请界面...");
|
||||
// _lobby.OpenInviteOverlay();
|
||||
// }
|
||||
//
|
||||
// private bool CanPerformLobbyAction()
|
||||
// {
|
||||
// if (!IsSteamInitialized)
|
||||
// {
|
||||
// LogSystem.LogError("[测试] Steam未初始化");
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// if (!IsLoggedIn)
|
||||
// {
|
||||
// LogSystem.LogError("[测试] Steam用户未登录");
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// return true;
|
||||
// }
|
||||
//
|
||||
// #endregion
|
||||
// }
|
||||
// }
|
||||
@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6d8d18aba4f448e095c425fe5a24a1c8
|
||||
timeCreated: 1756803052
|
||||
@ -456,6 +456,8 @@ namespace TH1Renderer
|
||||
|
||||
private void UpdateRoad()
|
||||
{
|
||||
if (_gridData == null || _road == null || _mapData == null) return;
|
||||
|
||||
//如果当前的feature没有road
|
||||
if (_gridData.Feature != TerrainFeature.Road)
|
||||
{
|
||||
@ -470,31 +472,31 @@ namespace TH1Renderer
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//TODO 最好把dir做成enum
|
||||
for (int i = 1; i <= 9; i++)
|
||||
{
|
||||
int dirForHuman = i;
|
||||
int dirForComputer = dirForHuman - 1;
|
||||
var t = _road.transform.Find($"Road{dirForHuman}");
|
||||
if (!_mapData.CheckIfNearByGridRoadCanConnenct(_gridData, dirForComputer))
|
||||
if (!_mapData.CheckIfNearByGridRoadCanConnenct(_gridData, dirForComputer))
|
||||
{
|
||||
if (t != null)
|
||||
GameObject.Destroy(t.gameObject);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if (t == null)
|
||||
{
|
||||
GameObject road = new GameObject($"Road{dirForHuman}");
|
||||
SpriteRenderer sr = road.AddComponent<SpriteRenderer>();
|
||||
ResourceCache.Instance.SpriteCache.Roads.TryGetValue((_gridData.Terrain == TerrainType.Land ? "Road" : "WaterRoad") + dirForComputer,out var tmpSprite);
|
||||
ResourceCache.Instance?.SpriteCache?.Roads?.TryGetValue((_gridData.Terrain == TerrainType.Land ? "Road" : "WaterRoad") + dirForComputer,out var tmpSprite);
|
||||
sr.sprite = tmpSprite;
|
||||
road.transform.parent = _road.transform;
|
||||
road.transform.localPosition = new Vector3(0, 0, 0);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -231,8 +231,8 @@ namespace TH1Renderer
|
||||
Sprite = param.Sprite;
|
||||
VFXObject = param.VFXObject;
|
||||
IsPlaying = false;
|
||||
_renderer = VFXObject.GetComponent<SpriteRenderer>();
|
||||
_transfrom = VFXObject.GetComponent<Transform>();
|
||||
_renderer = VFXObject != null ? VFXObject.GetComponent<SpriteRenderer>() : null;
|
||||
_transfrom = VFXObject != null ? VFXObject.GetComponent<Transform>() : null;
|
||||
}
|
||||
|
||||
public virtual void SetPlay(GridVFXPlayType playType = GridVFXPlayType.PlayOnce)
|
||||
@ -246,10 +246,11 @@ namespace TH1Renderer
|
||||
|
||||
public virtual void Play(GridVFXPlayType playType = GridVFXPlayType.PlayOnce)
|
||||
{
|
||||
if (VFXObject == null) return;
|
||||
VFXObject.SetActive(true);
|
||||
IsPlaying = true;
|
||||
var animancer = VFXObject.GetComponent<AnimancerComponent>();
|
||||
if (VFXAnim != null)
|
||||
if (VFXAnim != null && animancer != null)
|
||||
{
|
||||
animancer.Play(VFXAnim);
|
||||
//这里还要处理音频 TODO 将来配表化
|
||||
@ -264,11 +265,10 @@ namespace TH1Renderer
|
||||
if(playType is GridVFXPlayType.PlayOnce)
|
||||
Timer.Instance.TimerRegister(VFXObject, () =>
|
||||
{
|
||||
//TODO 这里可能为null但不知道为什么 将来要排雷
|
||||
VFXObject?.SetActive(false);
|
||||
IsPlaying = false;
|
||||
}, VFXAnim.length,"GridVFXRenderer_" + Type);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -285,7 +285,7 @@ namespace TH1Renderer
|
||||
public virtual void Stop()
|
||||
{
|
||||
IsPlaying = false;
|
||||
VFXObject.SetActive(false);
|
||||
VFXObject?.SetActive(false);
|
||||
}
|
||||
|
||||
public virtual void Update()
|
||||
@ -480,7 +480,7 @@ namespace TH1Renderer
|
||||
public override void Play(GridVFXPlayType playType = GridVFXPlayType.PlayOnce)
|
||||
{
|
||||
IsPlaying = true;
|
||||
VFXObject.SetActive(true);
|
||||
VFXObject?.SetActive(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -117,7 +117,9 @@ namespace TH1_UI.View.Info
|
||||
{
|
||||
|
||||
if (Pool.Count < 5 ) return;
|
||||
if (Main.MapData?.PlayerMap?.SelfPlayerData == null) return;
|
||||
var player = Main.MapData.PlayerMap.SelfPlayerData;
|
||||
if (Table.Instance?.UnitTypeDataAssets == null) return;
|
||||
|
||||
//Step #1 先设置Pool的对象图样
|
||||
bool firstSelect = true;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user