# Unity Quick Start

如果你是第一次接这套联调流程，先看：

- [START HERE](START-HERE.md)
- [EDITOR DEBUG](EDITOR-DEBUG.md)

这份 `Quick Start` 只覆盖一条最短可验证路径：

- 在一个干净 Unity 工程里接入 `IGP.UnitySDK`
- 在场景里挂 `IGPRuntimeManager`
- 只配置 `appId`
- 在游戏启动流程里主动调用 `IGPRuntimeManager.InitializeAsync()`
- 通过 Curio desktop 注入启动信息或由游戏自己附着 desktop
- 验证授权、room join、ready、realtime、achievement 主链路

当前 `Network.SendData(...)` 和 `Network.SendReliableData(...)` 都走同一条可靠 KCP 链路。
区别只剩下大消息处理：`SendReliableData(...)` 会在需要时自动分片。

当前 quickstart 覆盖房间、成就和授权验证主链路。

如果你不想自己先写最小脚本，包内已经提供一份现成代码样例：

- `Samples~/StarterDemo/IGPUnityStarterDemo.cs`
- `Samples~/StarterDemo/README.md`

如果你只想先看授权这一段，直接跳到：

- [AUTHORIZATION](AUTHORIZATION.md)

如果你想先看 Unity 事件总表，直接看：

- [Unity Event Reference](EVENT-REFERENCE.md)

## 前置条件

- Windows
- Unity `2022.3 LTS`
- Curio desktop hosted 启动链路可用
- 本地可访问 `IGP.UnitySDK` 包目录：

```text
adapters/unity/Runtime/IGP.UnitySDK
```

## 1. 创建一个干净 Unity 工程

建议直接使用空 3D Core 模板。

## 2. 通过 `manifest.json` 加入 IGP 包

打开 Unity 工程里的：

```text
Packages/manifest.json
```

在 `dependencies` 中加入：

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

说明：

- 这是当前内部开发最稳定的接法，适合本地源码包验证和问题排查。
- 当前正式对外发布包为 `.unitypackage`，本地 `file:` 包只用于开发和问题排查。
- 如果使用正式包，请先导入 `cn.indiegp.sdk.unity-<version>.unitypackage`，不需要在这里加入 `file:` 依赖。

如果你的项目已经在用 Mirror，并且想直接把 IGP 挂到 Mirror 的 `Transport` 槽位，再额外加一行：

```json
"cn.indiegp.sdk.unity.mirror-transport": "file:../../../../adapters/unity/Runtime/IGP.UnitySDK.MirrorTransport"
```

然后在场景里使用 `IGPMirrorTransport`。

Mirror 的完整接法单独放在：

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

## 3. 添加一个最小 bootstrap 脚本

在工程里新建：

```text
Assets/Scripts/IGPQuickstartDriver.cs
```

写入：

```csharp
using System;
using UnityEngine;
using IGP.UnitySDK;
using IGP.UnitySDK.Models;

public sealed class IGPQuickstartDriver : MonoBehaviour
{
    [SerializeField] private IGPRuntimeManager runtimeManager;
    [SerializeField] private string debugMessageType = "quickstart_ping";
    [SerializeField] private string unlockAchievementKey = "first_session";
    [SerializeField] private string progressAchievementKey = "matches_played";

    private void Awake()
    {
        runtimeManager ??= FindObjectOfType<IGPRuntimeManager>();
    }

    private void OnEnable()
    {
        if (runtimeManager == null)
        {
            Debug.LogError("[IGP Quickstart] IGPRuntimeManager is missing.");
            enabled = false;
            return;
        }

        runtimeManager.onRoomJoined.AddListener(HandleRoomJoined);
        runtimeManager.onMapChanged.AddListener(HandleMapChanged);
        runtimeManager.onMessageReceived.AddListener(HandleMessageReceived);
        runtimeManager.onError.AddListener(HandleError);
    }

    private async void Start()
    {
        var ok = await runtimeManager.InitializeAsync();
        if (!ok)
        {
            Debug.LogError("[IGP Quickstart] SDK initialization failed.");
        }
    }

    private void OnDisable()
    {
        if (runtimeManager == null)
        {
            return;
        }

        runtimeManager.onRoomJoined.RemoveListener(HandleRoomJoined);
        runtimeManager.onMapChanged.RemoveListener(HandleMapChanged);
        runtimeManager.onMessageReceived.RemoveListener(HandleMessageReceived);
        runtimeManager.onError.RemoveListener(HandleError);
    }

    [ContextMenu("IGP/Send Quickstart Message")]
    public async void SendQuickstartMessage()
    {
        if (runtimeManager == null)
        {
            return;
        }

        await runtimeManager.SendMessageAsync(new Message
        {
            type = debugMessageType,
            roomId = runtimeManager.CurrentRoomId,
            playerId = runtimeManager.PlayerId,
            reliable = true,
            content = new
            {
                text = "hello from quickstart",
                sentAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
            }
        });

        Debug.Log("[IGP Quickstart] Sent realtime message.");
    }

    [ContextMenu("IGP/Unlock Achievement")]
    public async void UnlockAchievement()
    {
        if (runtimeManager == null)
        {
            return;
        }

        var result = await IGPSDK.UnlockAchievementAsync(runtimeManager, unlockAchievementKey);
        Debug.Log($"[IGP Quickstart] Unlock success={result.success}, duplicated={result.duplicated}");
    }

    [ContextMenu("IGP/Report Achievement Progress")]
    public async void ReportAchievementProgress()
    {
        if (runtimeManager == null)
        {
            return;
        }

        var result = await IGPSDK.ReportAchievementProgressAsync(
            runtimeManager,
            progressAchievementKey,
            1,
            "quickstart_manual");

        Debug.Log($"[IGP Quickstart] Progress success={result.success}, duplicated={result.duplicated}");
    }

    private async void HandleRoomJoined(Room room)
    {
        Debug.Log($"[IGP Quickstart] Joined room id={room.id}, code={room.code}");
        await runtimeManager.SetReadyAsync(true);
        Debug.Log("[IGP Quickstart] Local player marked ready.");
    }

    private void HandleMapChanged(IGPMapChangeData mapChange)
    {
        var previousMap = $"{mapChange.previousMapPublicId}:{mapChange.previousMapVersionId}";
        var currentMap = $"{mapChange.currentMapPublicId}:{mapChange.currentMapVersionId}";
        Debug.Log($"[IGP Quickstart] Map changed {previousMap} -> {currentMap}");
    }

    private void HandleMessageReceived(string messageType, object content)
    {
        Debug.Log($"[IGP Quickstart] Message type={messageType}, payload={content}");
    }

    private void HandleError(string error)
    {
        Debug.LogError($"[IGP Quickstart] Runtime error: {error}");
    }
}
```

## 4. 在场景里挂载运行时组件

1. 创建一个空对象，例如 `IGPBootstrap`
2. 给它挂上：
   - `IGPRuntimeManager`
   - `IGPQuickstartDriver`
3. 把 `IGPQuickstartDriver.runtimeManager` 指向同一个对象上的 `IGPRuntimeManager`
4. SDK 会自动挂载项目里的 `IGPConfig`。如果项目里还没有配置资源，会自动创建 `Assets/IGP/IGPConfig.asset`
5. 在 `IGPConfig` 上填写 IGP 运营提供的 `appId`
6. 如果你的游戏后面会切场景，但不希望联机中断，就把这个对象做成跨场景常驻，并确保游戏里始终只有一个 `IGPRuntimeManager`

补充说明：

- `IGPRuntimeManager` 跟着场景一起被销毁时，当前房间连接也会一起断开
- 最常见的误用是：第一个场景把它设成常驻了，但后续场景里又放了一个新的同名对象

## 5. Unity Editor 里怎么调试

当前支持两种调试方式：

1. 只调 desktop 能力
2. 调完整 hosted 房间链路

### 5.1 只调 desktop 能力

如果你只是想在 Unity Editor 里调这些能力：

- desktop session attach
- unlock achievement
- report achievement progress

可以直接在 Editor 里 Play。一般不需要填写 `Desktop Executable Path Debug Override`；只有要模拟真实安装路径或排查 exe 绑定问题时，才填写这款游戏真正的 Windows 可执行文件路径。

### 5.2 调完整 hosted 房间链路

如果你要调这些能力：

- launch ticket
- hosted bootstrap
- 自动进房
- ready / start / finish
- 运行中换地图
- 房间消息
- state
- RPC
- KCP（统一可靠）

那就不能只靠 Unity Editor 直接 Play。

这条链路仍然需要通过 Curio desktop 启动游戏，原因是 `IGPRuntimeManager` 要在启动时读取 desktop 注入的 launch ticket 参数，然后自动完成 hosted bootstrap。

建议直接用：

- `samples/unity/MirrorTransportDemo/README.md`
- 以及其中的：
  - `Run-MirrorTransportDemo-DesktopHost.ps1`
  - `Run-MirrorTransportDemo-DesktopGuest.ps1`

里面已经给了：

- 本地假服务自动联调
- 双机真实 desktop 联调
- 手动观察窗口状态的方式
- 真实 host / guest 启动脚本和结果文件说明

补一句定位：

- `MirrorTransportDemo` 是给已经用了 Mirror、并且要把 Mirror transport 接到 IGP 的项目看的专项示例
- 如果你只是第一次接 Unity SDK 主链路，不要先从它开始

如果你是要看真实双端联调、房间号、启动信息、日志和结果文件，优先看这两个脚本的说明，不要只看 Editor 面板。

再补一句：这套 host / guest 脚本是给桌面端复现双机场景用的，不是 `SDK 联调` 正式入口本身。你走 desktop 的 `SDK 联调` 页时，不需要先打开开发者模式；只有按脚本复现 host / guest 时，才看这两个脚本的说明。

如果你要在 Unity Editor 里走完整流程，现在已经有一个正式面板入口：

1. 在 desktop 的 `SDK 联调` 页里生成并复制 `Unity 启动包`
2. 选中场景里的 `IGPRuntimeManager`
3. 进入 Inspector 里的 `Unity Editor 联调`
4. 把整包 JSON 粘贴到 `Launch Package JSON`
5. 点击 `Apply Launch Package`
6. 进入 Play
7. 点击 `Connect Current Test Room`

这套入口对 host 和 guest 是一样的，不需要额外分两套配置。

- 想调 host，就用 host 自己那份启动包
- 想调 guest，就用 guest 自己那份启动包

如果你还要一起调 desktop 能力，比如成就，一般不需要额外配置路径。只有要模拟正式安装路径，或排查本地 exe 绑定问题时，才在 `IGPConfig -> 调试参数 -> Desktop Executable Path Debug Override` 里填写这款游戏真正的 Windows 可执行文件路径；不要填 desktop 路径，也不要填 `Unity.exe`。

## 6. 最小验收标准

启动后至少确认以下日志链路成立：

1. 出现 `Joined room` 日志
2. 出现 `Local player marked ready` 日志
3. 手动触发 `Send Quickstart Message` 后出现发送日志
4. 手动触发 `Unlock Achievement` 或 `Report Achievement Progress` 后出现成功日志

如果要验证一局结束后不重启游戏直接换地图，先保持在同一个房间里，由 desktop / lobby 选择新地图并下发新的房间快照。Unity Console 应出现 `Map changed oldMap:oldVersion -> newMap:newVersion` 日志。游戏业务里可以在 `HandleMapChanged(IGPMapChangeData mapChange)` 中切 Unity scene、addressable key 或自己的地图资源。

如果你用的是 `MirrorTransportDemo` 的手动调试面板，`Unlock Achievement` 旁边现在可以先填 `Achievement Id`。这里要填后台已经配置好的成就 ID，不要直接沿用默认值。

如果你要反复点同一个按钮测试，`Event Id` 可以留空让面板每次自动生成新的；如果你想验证幂等去重，再手动填同一个 `Event Id`。

`Report Achievement Progress` 下面也可以先填进度成就 ID、进度值和来源键；`Progress Event Id` 留空时同样会自动生成新的。

## 7. 下一步怎么扩

当 `Quick Start` 跑通后，再按目标继续：

- 最小代码样例：`Samples~/StarterDemo`
- 房间生命周期：`Samples~/RoomLifecycle`
- realtime / state / RPC：`Samples~/RealtimeMessaging`
- 综合手动联调：`Samples~/HostedPlayground`

如果你在做跨引擎 SDK 对齐，不要把这里的 `MonoBehaviour` 形态当成平台标准协议。
平台标准仍然以仓库里的 `core/` 和 `bridge/` contract 为准。
