# Mirror Transport Quickstart

这份文档只讲一件事：

- 怎么把 `IGPMirrorTransport` 挂到 Mirror 的 `Transport` 位置里

适用对象很明确：

- 你的 Unity 项目已经在用 Mirror
- 你现在要把 Mirror 的传输层接到 IGP

前提是你已经准备好：

- Unity `2022.3 LTS`
- Mirror
- `cn.indiegp.sdk.unity`
- `cn.indiegp.sdk.unity.multiplayer`

Mirror 版本口径：

- 最低兼容 `v89.0.0`
- 推荐使用 `v90.0.0` 或更高版本
- 默认不需要额外宏定义；`IGP_MIRROR_HAS_SERVER_CONNECTED_WITH_ADDRESS` 只是备用开关，只有在你确认项目里的 Mirror 已经包含新版连接回调，并且明确要让本包直接调用它时才需要添加

如果你还没先跑通 IGP 主链路，先看：

- `../../IGP.UnitySDK/Documentation~/QUICKSTART.md`

## 1. 安装包

先保证工程里的 `Packages/manifest.json` 至少有这两行：

```json
"cn.indiegp.sdk.unity": "file:../../../../adapters/unity/Runtime/IGP.UnitySDK",
"cn.indiegp.sdk.unity.multiplayer": "file:../../../../adapters/unity/Runtime/IGP.UnitySDK.Multiplayer"
```

Mirror 仍然按你们项目自己的接法导入。

## 2. 场景里要放什么

最少要有两块：

1. 一块 IGP runtime
2. 一块 Mirror network

推荐最小结构：

- `IGPBootstrap`
  - `IGPRuntimeManager`
  - `IGPMultiplayerRuntime`
- `NetworkRoot`
  - `IGPMirrorTransport`
  - 你自己的 `NetworkManager`

重点只有两个：

- `IGPRuntimeManager` 和 `IGPMultiplayerRuntime` 必须在同一个 GameObject
- `IGPMirrorTransport.runtimeManager` 要显式指向这个 `IGPMultiplayerRuntime`
- `NetworkManager.transport` 要指向 `IGPMirrorTransport`

## 3. 最小接法

下面这份脚本就是最小可用接法：

```csharp
using IGP.UnitySDK;
using IGP.Multiplayer;
using IGP.Multiplayer.Mirror;
using Mirror;
using UnityEngine;

public sealed class MirrorIgpBootstrap : NetworkManager
{
    [SerializeField] private IGPRuntimeManager coreRuntime;
    [SerializeField] private IGPMultiplayerRuntime multiplayerRuntime;
    [SerializeField] private IGPMirrorTransport transport;

    public override void Awake()
    {
        coreRuntime ??= FindObjectOfType<IGPRuntimeManager>();
        multiplayerRuntime ??= FindObjectOfType<IGPMultiplayerRuntime>();
        transport ??= FindObjectOfType<IGPMirrorTransport>();

        if (coreRuntime == null || multiplayerRuntime == null || transport == null)
        {
            Debug.LogError("[Mirror IGP] Missing IGPRuntimeManager, IGPMultiplayerRuntime, or IGPMirrorTransport.");
            enabled = false;
            return;
        }

        this.transport = transport;
        networkAddress = "host";
        dontDestroyOnLoad = true;
        runInBackground = true;
        autoCreatePlayer = true;

        base.Awake();
    }
}
```

说明：

- `networkAddress = "host"` 时，client 会去连当前房主
- 如果你明确知道要连哪个玩家，也可以把 `networkAddress` 设成那个玩家的 id

## 4. 启动顺序

这块 transport 不是独立联网层。`IGPMultiplayerRuntime` 会在 Hosted + Room/Player context 有效后自动拉起数据面。

所以顺序要是：

1. 调用一次 `IGPRuntimeManager.InitializeAsync()`
2. Multiplayer 自动等待房间并建立 KCP/UDP
3. 游戏仍按自己的时机启动 Mirror host / client

简单说：

- `StartHost()` / `StartClient()` 不会触发 descriptor 或 KCP/UDP 连接
- 数据面尚未 Ready 时，transport 会保留 pending intent；连接超时从首次 Ready 后才开始

如果你们项目已经有自己的房间进入流程，最稳的做法就是在“房间已附着”之后再启动 Mirror。

## 5. host / client 怎么起

host 侧：

- 先让 IGP 完成进房
- 再调用 `StartHost()`

client 侧：

- 先让 IGP 完成进房
- 把 `networkAddress` 设成 `"host"`
- 再调用 `StartClient()`

## 6. 什么时候算接通了

至少确认这几件事：

1. IGP 已经进到同一个房间
2. IGP 数据面已经连上
3. Mirror host 已经启动
4. Mirror client 已经连上
5. 双边能互相收发 Mirror 消息

如果你想直接看一套完整可跑的例子，直接看：

- `samples/unity/MultiplayerMirrorDemo/README.md`

这个 demo 也是按同样口径提供的：

- 它是给 Mirror 项目的专项接入示例
- 不是通用 Unity starter demo

## 7. Reliable application frame

`IGPMirrorTransport` 会先按 peer 保存 reliable Mirror payload，再用单调时钟按 `Reliable Send Rate` 构建 application frame。该值默认是 `30 Hz`，允许范围为 `1..60 Hz`。每个 peer 每 Tick 最多提交一个 frame；Unity 卡帧后不会补发多个 Tick，因此可靠发送频率不再随渲染 FPS 增长。

可以在 `IGPMirrorTransport` Inspector 修改 `Reliable Send Rate`，也可以在代码里设置；两种入口最终都限制到 `1..60`：

```csharp
transport.ReliableSendRate = 60;
```

`GetBatchThreshold(Channels.Reliable)` 固定返回 `1024`，只控制 Mirror 在单帧内的小消息预合批；SDK 的跨帧聚合频率由 `ReliableSendRate` 独立控制。旧 `ReliableBatchThresholdBytes` 属性仅保留一个版本用于源码兼容，setter 不再改变行为，Inspector 也不再显示。

application frame 使用版本化 `IGMF v1` 长度协议，保留每条原始 Mirror payload 的边界和顺序。普通 frame 会控制在协商的 reliable chunk 范围内；单条大消息仍按 transport 的 reliable 最大包上限进入 SDK 现有可靠分片链路。

每个 peer 的出站队列上限为 `1024` 条、`4 MiB`、最老 `15 秒`。超过任一限制只断开造成溢出的 Mirror 逻辑 peer，不终止其他 peer 或整个 Room data plane。

## 8. Unreliable raw UDP 通道

Mirror `channelId` 表示逻辑通道，不直接等同于底层物理通道：

| Mirror 逻辑通道 | 物理通道 | 不可用行为 | 对端逻辑通道 |
| --- | --- | --- | --- |
| `Channels.Reliable` | KCP | 无 | `Channels.Reliable` |
| `Channels.Unreliable` | raw UDP | 返回 unavailable 并丢弃，不回退 | `Channels.Unreliable` |

`IGPMirrorTransport` 默认开启 `Use Raw UDP Unreliable Lane`。`IGPMultiplayerRuntime` 在 Hosted 已附加且 Room/Player context 有效后自动准备 RoomNode data plane；`ClientConnect` 与 `ServerStart` 只记录 Mirror 逻辑连接意图。UDP descriptor 缺失不影响 KCP Ready；实际发送 `Channels.Unreliable` 时，如果 raw UDP 未 ready，transport 会输出 Warning、返回 unavailable 并丢弃，不会回退到 KCP。`GetMaxPacketSize(Channels.Unreliable)` 默认是 `1200`，连接后使用协商的 UDP payload 上限。

内部 `41014` 表示 reliable `IGMF v1` application frame，`41011` 表示 raw UDP unreliable payload。`41010`、`41012`、`41013` 只保留接收兼容，新 reliable 发送不再使用。Arena 透明转发这些值，不解析 frame、压缩 flag 或业务内容。

如果你的项目暂时需要旧行为，在 Inspector 里关闭 `Use Raw UDP Unreliable Lane`，或在代码里设置：

```csharp
transport.UseRawUdpUnreliableLane = false;
```

关闭后 unreliable 发送会返回 unavailable 并丢弃，不会回退到 reliable KCP。

## 9. 自动 frame 压缩

每个完整 application frame 正文都会使用 `Deflate/Fastest` 尝试压缩一次；只有最终 wire size 更小时才采用压缩结果。压缩结果与待发送 frame 一起缓存，底层返回 `WouldBlock` 时不会重复压缩。

压缩不是开关，也没有用户可配置阈值。旧 `EnablePayloadCompression`、`CompressionThresholdBytes` 和 `MinCompressionRatio` 属性带 `Obsolete` 保留一个版本，setter 不再改变发送策略。逻辑连接握手要求双方支持 `ApplicationFrameV1`；旧 peer 只会被拒绝对应 Mirror 逻辑连接，不影响底层 Room data plane。

## 10. 统一日志

`IGPMirrorTransport` 默认不单独打印日志。它统一跟随绑定的 `IGPMultiplayerRuntime.LogLevel`：只有共享的 `IGPConfig -> Log Level` 选择非 `Off` 等级时，transport 才会通过 SDK Core 的统一日志入口写入 Unity Console；没有绑定 Multiplayer Runtime 时也不打印日志。连接请求、连接确认、连接建立、断开和服务端启停等低频流程属于 `Info`；普通 payload 收发、队列 drain 和压缩成功不逐条打印，`Warning` / `Error` 仍按对应等级输出。

MirrorTransport 日志统一显示为 `[IGP SDK]`，并带有 `scope=mirror-transport`。MirrorTransport 绑定后通过 Multiplayer 的 internal backend 消费处理后的数据、连接状态和 Peer Activity，不依赖游戏公开事件。`WouldBlock` 首次出现及持续每 5 秒最多输出一次 Warning，frame 保留在队首；恢复时输出一次 Info。队列超限、frame 解码失败和 capability 不匹配会输出带 peer 与 action 的 Error。

发送失败摘要中的 `logicalChannel` 表示 Mirror 逻辑通道，`effectiveTransport` 表示本次实际使用的 `kcp` 或 `raw-udp`。

Multiplayer backend 离开 `Ready` 时，`IGPMirrorTransport` 会立即清空待派发的入站消息，并对已建立的客户端和服务端逻辑连接各派发一次 Mirror 断开回调；底层恢复后不会自动重启 `Mirror.NetworkManager`。

Debug 级别下，每个 KCP 连接每 5 秒最多输出一条 `scope=kcp event=sample`，同时包含 UDP datagram/frame/byte 增量、`snd_una/snd_nxt/rcv_nxt` 推进、`WaitSnd` 和四个 KCP 队列、SRTT/RTO、Unity Tick gap、Socket `WouldBlock`/KCP input error，以及 application-frame 生成、压缩和出站队列峰值。该采样不预览或复制 payload，也不能直接测量 kcp2k 内部 mutex 等待。

合法 reliable payload 的原始大小超过 `16 KiB`，或线上 payload 超过约 `12 KiB` 需要可靠分片时，会记录节流后的 `large-payload-send` / `large-payload-receive` Warning。payload 接收队列硬限制为全局 `4096` 条/`16 MiB`、单玩家 `1024` 条/`4 MiB`，生命周期控制队列独立限制为 `256` 条/`1 MiB`；payload 按每次 EarlyUpdate `256` 条、`1 MiB` 或 `2 ms` 分帧派发。unreliable 溢出会丢弃并计数，reliable 溢出只断开对应逻辑连接。

## 11. 常见问题

### 一直连不上

优先检查：

- `IGPMultiplayerRuntime` 是不是还没有收到房间上下文
- 数据面是不是还没连上
- client 侧是不是太早 `StartClient()`

### 切场景后断了

优先检查：

- 同一个 runtime root 上的 `IGPRuntimeManager` 或 `IGPMultiplayerRuntime` 有没有被场景切换销毁
- `NetworkManager` 和 `IGPMirrorTransport` 有没有被重复创建

### 为什么不是普通 IP 和端口

因为这块不是直接走传统直连。

Mirror 看到的是一个 transport，但底下实际用的是 IGP 已经建立好的房间和数据面。
