# Mirror Transport Quickstart

这份文档只讲一件事：

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

适用对象很明确：

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

前提是你已经准备好：

- Unity `2022.3 LTS`
- Mirror
- `cn.indiegp.sdk.unity`
- `cn.indiegp.sdk.unity.mirror-transport`

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.mirror-transport": "file:../../../../adapters/unity/Runtime/IGP.UnitySDK.MirrorTransport"
```

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

## 2. 场景里要放什么

最少要有两块：

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

推荐最小结构：

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

重点只有两个：

- `IGPRuntimeManager` 要先存在
- `NetworkManager.transport` 要指向 `IGPMirrorTransport`

## 3. 最小接法

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

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

public sealed class MirrorIgpBootstrap : NetworkManager
{
    [SerializeField] private IGPRuntimeManager runtimeManager;
    [SerializeField] private IGPMirrorTransport transport;

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

        if (runtimeManager == null || transport == null)
        {
            Debug.LogError("[Mirror IGP] Missing IGPRuntimeManager 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 不是独立联网层，它依赖 IGP 已经把房间和数据面拉起来。

所以顺序要是：

1. 先让 `IGPRuntimeManager` 完成房间附着
2. 再让 IGP 数据面连上
3. 最后再启动 Mirror 的 host / client

简单说：

- 不要一进场景就直接 `StartHost()` / `StartClient()`
- 先等 IGP 这边已经进房、并且数据面可用

如果你们项目已经有自己的房间进入流程，最稳的做法就是在“房间已附着”之后再启动 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/MirrorTransportDemo/README.md`

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

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

## 7. Reliable batching 粒度

`IGPMirrorTransport` 的 `Reliable Batch Threshold Bytes` 默认是 `1024`。这个值为 1200-byte KCP MTU 的 KCP header、IGP frame/envelope 和 player ID 保留空间，使常见 Mirror reliable batch 能放入单个 KCP segment，降低额外分片与 HoL 阻塞。

如需按项目特征调整，可以在 Inspector 上修改，或在代码里设置：

```csharp
transport.ReliableBatchThresholdBytes = 1024;
```

这个设置不限制单条 reliable 大消息大小。单条大消息仍然按 transport 的 reliable 最大包上限进入 SDK 的可靠分片链路。

## 8. Unreliable raw UDP 通道

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

| Mirror 逻辑通道 | 优先物理通道 | 回退行为 | 对端逻辑通道 |
| --- | --- | --- | --- |
| `Channels.Reliable` | KCP | 无 | `Channels.Reliable` |
| `Channels.Unreliable` | raw UDP | UDP 未 ready 时回退到 KCP | `Channels.Unreliable` |

`IGPMirrorTransport` 默认开启 `Use Raw UDP Unreliable Lane`。该开关只声明 Mirror `Channels.Unreliable` 的优先发送路由，UDP descriptor 缺失不影响 KCP 建连。实际发送 `Channels.Unreliable` 时，如果 raw UDP 未 ready，transport 会输出 Warning 并自动回退到 reliable KCP。回退只改变物理通道，不改变 Mirror 逻辑通道。`GetMaxPacketSize(Channels.Unreliable)` 默认是 `1200`，连接后会使用 Arena 协商的 UDP payload 上限；payload 超过当前上限时仍会直接发送失败。

内部 `41010` 到 `41013` 只标记 Mirror 逻辑通道和压缩状态。Arena 透明转发这些值，不使用它们决定物理路由。

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

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

关闭后，Mirror reliable / unreliable 都会走旧的 reliable KCP 路径。

## 9. 可选大包压缩

`IGPMirrorTransport` 可以对较大的可靠 Mirror payload 做 Deflate 压缩。这个能力默认开启；如需调整，可以在 `IGPMirrorTransport` Inspector 上修改 `Enable Payload Compression`，或在代码里设置：

```csharp
transport.EnablePayloadCompression = true;
transport.CompressionThresholdBytes = 2 * 1024;
transport.MinCompressionRatio = 0.90f;
```

开启后不代表每个包都会被压缩。transport 会同时检查：

- 本地开启了压缩；
- 对端握手协商支持压缩；
- payload 大小达到阈值；
- 压缩后至少比原始 payload 小 10%。

只有全部满足时，才会发送压缩消息。Mirror 和游戏业务层收到的仍然是原始 payload。第一版只主动压缩 reliable 通道的大包。

## 10. 统一日志

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

MirrorTransport 日志统一显示为 `[IGP SDK]`，并带有 `scope=mirror-transport`。MirrorTransport 绑定后会通过 `OnDataReceived` 事件消费实时数据，并临时关闭 `IGPNetwork` 的轮询读取保留队列，避免 Mirror 已经处理的数据继续堆在 `ReadData`/`ReadRawData` 队列里。排查 Mirror 读侧是否及时处理时，优先看 `ProcessQueuedMessages drained ... pendingAfterDrain=... maxDelayMs=...` 日志，而不是通用 `Queued incoming packet pending=...`。

发送摘要中的 `logicalChannel` 表示 Mirror 逻辑通道，`effectiveTransport` 表示本次实际使用的 `kcp` 或 `raw-udp`。接收摘要只记录可以确认的 `logicalChannel`。

Runtime 进入 `ConnectionFailed` 时，`IGPMirrorTransport` 会立即清空待派发的入站消息、停止 transport，并对现有客户端和服务端连接各派发一次 Mirror 断开回调。

合法 reliable payload 的原始大小超过 `16 KiB`，或线上 payload 超过约 `12 KiB` 需要可靠分片时，会记录节流后的 `large-payload-send` / `large-payload-receive` Warning；接收队列超过 `4096` 条或 `16 MiB` 时会记录 `inbound_queue_deep`。这些日志全部是 `action=observe-only`：不会丢包、限流、改变发送结果或连接状态，持续发生时最多每 30 秒汇总一次。

## 11. 常见问题

### 一直连不上

优先检查：

- `IGPRuntimeManager` 是不是还没进房
- 数据面是不是还没连上
- client 侧是不是太早 `StartClient()`

### 切场景后断了

优先检查：

- `IGPRuntimeManager` 有没有被场景切换销毁
- `NetworkManager` 和 `IGPMirrorTransport` 有没有被重复创建

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

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

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