# Tutorial 2 - Ticket MatchMaker

このチュートリアルでは、Diarkis のチケット方式マッチメイキングを実装します。各クライアントがチケットを発行するだけで、サーバーが同じチケットタイプを持つクライアントを自動的にグループ化します。ホストを選ぶ必要も、ルームを検索する必要もありません。

終了時には以下のことが身についています:

* `DiarkisInterface` から `DiarkisMatchMaker` モジュールを取得する方法
* `DiarkisEventHandler` にマッチメイキング用コールバックを登録する方法
* チケットの発行・キャンセルと各レスポンスの処理
* SDK からメンバーリストとオーナー情報を取得して表示に反映する方法
* チケット内でブロードキャストメッセージを送受信する方法

Tutorial 1 の接続フローが前提です。このシーンは `Start()` で自動接続するため、手動の接続ステップはありません。

### チケット方式とは

Diarkis のマッチメイキングには主に 2 つの方式があります。

| 方式                 | 概要                                        |
| ------------------ | ----------------------------------------- |
| **Ticket（チケット方式）** | 各クライアントがチケットを発行し、サーバーが条件の合うクライアントをグループ化する |
| **Host/Search**    | 誰かがホストとなってルームを作り、他のクライアントがそのルームを検索して参加する  |

チケット方式ではホストを意識する必要がありません。クライアントは `SendIssueTicket()` を呼ぶだけで、サーバーがグループ化を管理します。

`ticketType`（byte 0〜255）は**マッチングのカテゴリ**を表します。同じ `ticketType` を持つチケット同士だけがグループ化されるので、ランクマッチ・カジュアルマッチなどモードごとに異なる値を使います。

### マッチングの流れ

```mermaid
flowchart TD
    A([開始]) --> B[SendIssueTicket]
    B --> C{OnMMTicketIssueResponse}
    C -->|失敗| Z([エラー終了])
    C -->|成功 - 発行者| D[チケット参加済み]
    C -->|成功 - 参加者には| DJ[OnMMTicketJoin]
    DJ --> D
    D --> E[OnMMTicketMemberJoin]
    E --> D
    D -->|必要人数が揃った| G[OnMMTicketComplete]
    D -->|キャンセル| H[SendTicketCancel]
    H --> I[OnMMTicketCancel]
    I --> Z2([終了])
```

重要な非対称性: チケットを**発行した**クライアントは `OnMMTicketIssueResponse` でチケット参加を確認しますが、`OnMMTicketJoin` は**受け取りません**。`OnMMTicketJoin` は既存のチケットに後から参加した側（2 人目以降）にのみ届きます。コードはこの両方のケースに対応しています。

### シーンのセットアップ

`Tutorials/Scenes/Tutorial2-TicketMatchMaker.unity` を開き、接続先を環境に合わせて変更してください。

```csharp
private const string HOST       = "127.0.0.1:7000";
private const string CLIENT_KEY = "";
```

Play モードに入るとシーンが自動接続します。**Network State** ラベルが緑色になり「接続済み」と表示されると、**Issue Ticket** ボタンが有効になります。

> **Tip — 同一マシンで 2 クライアントをテストする場合:** **Edit > Project Settings > Player** の **Run In Background** を有効にしてください。これを有効にしないと、フォーカスのない Unity インスタンスはネットワークイベントを処理せず、UI がウィンドウをアクティブにしたときしか更新されません。

<figure><img src="https://669307705-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FlFJ89PMX2ike3NyauXNM%2Fuploads%2FNJk3FqViYBRo04ER0yww%2Fimage.png?alt=media&#x26;token=47e9e24e-1819-4333-90bb-08e551d60143" alt=""><figcaption></figcaption></figure>

### コードの解説

#### MatchMaker モジュールの取得

```csharp
DiarkisInterface diarkis = DiarkisNetworkManager.GetDiarkisInterface(INTERFACE_NAME);
_matchMaker = diarkis.MatchMaker;
```

機能モジュールはすべて `DiarkisInterface` を通じて取得します。Tutorial 4 の `diarkis.Room` も同じパターンです。

#### イベントの登録

```csharp
DiarkisEventHandler handler = diarkis.EventHandler;

handler.OnUDPConnect(OnConnect, this);
handler.OnUDPDisconnect(OnDisconnect, this);
handler.OnUDPFail(_ => SetNetworkState("接続失敗", ColorRed), this);
handler.OnHttpError(_ => SetNetworkState("HTTP 認証エラー", ColorRed), this);

handler.OnMMTicketIssueResponse(OnIssueTicketResponse, this);
handler.OnMMTicketJoin(OnTicketJoin, this);
handler.OnMMTicketMemberJoin(args => OnTicketMemberJoin(args), this);
handler.OnMMTicketMemberLeave(args => OnTicketMemberLeave(args), this);
handler.OnMMTicketComplete(OnTicketComplete, this);
handler.OnMMTicketCancel(_ => OnTicketCancelled(), this);
handler.OnMMTicketBroadcast(OnTicketBroadcast, this);
```

このチュートリアルで登録するイベントの一覧です。

| イベント                      | 発火タイミング             | コールバックシグネチャ                            |
| ------------------------- | ------------------- | -------------------------------------- |
| `OnMMTicketIssueResponse` | サーバーがチケット発行を応答      | `Action<DiarkisMMResponseEventArgs>`   |
| `OnMMTicketJoin`          | 既存チケットに参加した（参加者側のみ） | `Action<DiarkisMMTicketJoinEventArgs>` |
| `OnMMTicketMemberJoin`    | 他のプレイヤーがチケットに参加     | `Action<DiarkisMMTicketJoinEventArgs>` |
| `OnMMTicketMemberLeave`   | プレイヤーがチケットから離脱      | `Action<DiarkisMMResponseEventArgs>`   |
| `OnMMTicketComplete`      | 必要人数が揃いマッチング成立      | `Action<DiarkisMMResponseEventArgs>`   |
| `OnMMTicketCancel`        | チケットがキャンセルされた       | `Action<DiarkisMMResponseEventArgs>`   |
| `OnMMTicketBroadcast`     | ブロードキャストメッセージを受信    | `Action<DiarkisMMSyncEventArgs>`       |

`OnDestroy` での `UnregisterCallbacks(this)` はお忘れなく。オーナーパターンの詳細は Tutorial 1 を参照してください。

#### SendIssueTicket() — チケットの発行

```csharp
private void OnTicketClicked()
{
    byte ticketType = GetTicketType();
    SetStatus("チケット待機中...");
    _matchMaker.SendIssueTicket(ticketType);
}
```

`SendIssueTicket` は非同期です。結果は `OnMMTicketIssueResponse` で届きます。

#### OnIssueTicketResponse — チケットに参加した（発行者側）

```csharp
private void OnIssueTicketResponse(DiarkisMMResponseEventArgs args)
{
    if (!args.IsSuccess())
    {
        SetStatus($"チケット発行失敗 (code: {args.GetErrorCode()})");
        return;
    }
    // 発行者は OnMMTicketJoin を受け取らないため、ここで UI を更新する
    SetStatus("チケット参加済み");
    RefreshTicketUI();
    RefreshButtons();
}
```

`OnMMTicketIssueResponse` の成功が、発行者がチケット待機状態に入ったことの確認です。`OnMMTicketJoin` は後から参加した側（2 人目以降）にのみ届きます。どちらのパスも `RefreshTicketUI()` を呼んでメンバーリストとオーナー表示を更新します。

#### メンバーリストとオーナーの更新

イベント引数からメンバー情報を読み取るのではなく、各イベント後に SDK から直接取得します。

```csharp
private void RefreshTicketUI()
{
    byte ticketType = GetTicketType();

    if (_ownerIDValueText != null)
        _ownerIDValueText.text = _matchMaker.GetTicketOwnerUID(ticketType) ?? "";

    if (_memberListText != null)
    {
        var members = _matchMaker.GetMembers(DiarkisMatchMakerType.Ticket, ticketType);
        _memberListText.text = members != null && members.Count > 0
            ? string.Join("\n", members)
            : "";
    }
}
```

`GetTicketOwnerUID()` と `GetMembers()` は常にサーバー側の最新状態を反映しているため、参加・離脱・完了のすべてのコールバックからこのヘルパーを呼んでいます。

#### OnTicketComplete — マッチング成立

```csharp
private void OnTicketComplete(DiarkisMMResponseEventArgs args)
{
    if (!args.IsSuccess())
    {
        SetStatus($"チケット完了失敗 (code: {args.GetErrorCode()})");
        return;
    }
    SetStatus("マッチング成立！");
}
```

`OnMMTicketComplete` はチケット内の**全クライアントに同時に届きます**。これをゲームセッション開始のシグナルとして使います。

<figure><img src="https://669307705-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FlFJ89PMX2ike3NyauXNM%2Fuploads%2FmaTwm1f3C6HvibsiQZ4S%2Fosef.png?alt=media&#x26;token=5e777c28-cbd3-4158-8010-fd6259e06b53" alt=""><figcaption></figcaption></figure>

#### SendTicketBroadcast — チケット内メッセージ

マッチング待機中にチケット内のメンバー全員へメッセージを送れます。`SendTicketBroadcast` は送信者自身にも届くため、ペイロードに自分の UID を含めることで送受信側を区別します。

```csharp
private void OnMessageClicked(string message)
{
    // "uid:message" 形式でエンコードして送信者を識別できるようにする
    string myUID = GetMyUID();
    byte[] payload = Encoding.UTF8.GetBytes($"{myUID}:{message}");
    _matchMaker.SendTicketBroadcast(GetTicketType(), new ArraySegment<byte>(payload));
}

private void OnTicketBroadcast(DiarkisMMSyncEventArgs args)
{
    // "uid:message" を分解して自分か他者かを判別する
    DiarkisByteVector payload = args.GetPayload();
    string raw = Encoding.UTF8.GetString(/* payload のバイト列 */);
    int sep = raw.IndexOf(':');
    string senderUID = raw[..sep];
    string message   = raw[(sep + 1)..];

    string prefix = senderUID == GetMyUID() ? "[自分]" : $"[{senderUID}]";
    AddChatLine($"{prefix} {message}");
}
```

ペイロードはバイト配列です。形式はゲームに合わせて自由に決めてください。このサンプルでは UID プレフィックス付き UTF-8 テキストを使っています。

#### TicketState によるボタン制御

`MatchMakerTicketState` でクライアントの現在位置を把握し、ボタンと入力フィールドの有効/無効を切り替えます。

```mermaid
stateDiagram-v2
    [*] --> None
    None --> TicketJoined : SendIssueTicket()
    TicketJoined --> TicketComplete : OnMMTicketComplete
    TicketJoined --> None : SendTicketCancel()
```

```csharp
MatchMakerTicketState state = _matchMaker.GetTicketState(ticketType);

bool notStarted = state == MatchMakerTicketState.None;
bool inTicket   = state == MatchMakerTicketState.TicketJoined;
bool complete   = state == MatchMakerTicketState.TicketComplete;

_ticketButton.interactable    = notStarted && _connected;
_cancelButton.interactable    = inTicket;
_ticketTypeInput.interactable = !inTicket;  // チケット中は変更不可
```

### Cancel と Leave の違い

この 2 つのメソッドは似ていますが、使うタイミングが異なります。

| メソッド               | 呼ぶタイミング            | 呼べるのは誰か                         |
| ------------------ | ------------------ | ------------------------------- |
| `SendTicketCancel` | マッチング待機中（チケット未完了）  | チケットオーナーのみ — 全メンバーのチケットをキャンセルする |
| `SendTicketLeave`  | チケット完了後、ルームが生成された後 | 任意のメンバー — 他のメンバーに影響せずルームから退出する  |

このチュートリアルの **Cancel** ボタンは `SendTicketCancel` を呼び、オーナーが待機中のチケットをキャンセルします。非オーナーのメンバーがマッチング後のルームから抜けたい場合は、代わりに `SendTicketLeave` を使います。

### バックフィル（上級）

このチュートリアルでは使用しませんが、バックフィルの概要を紹介します。

チケットが完了してゲームセッションが始まった後、プレイヤーが途中で離脱することがあります。**バックフィル**を使うと、チケットオーナーがサーバーに空きスロットへの新規プレイヤー補充をリクエストできます。マッチメイキングプロセス全体をやり直す必要はありません。

```csharp
// プレイヤーが抜けた後、オーナーが代替プレイヤーをリクエスト
_matchMaker.SendTicketBackfill(ticketType);
// → 補充が見つかると OnTicketBackfillComplete が届く

// 補充待ちをやめる場合
_matchMaker.SendTicketCancelBackfill(ticketType);
```

マッチメイキングのロジック自体はサーバー側で処理されます。クライアントはリクエストを送り、`OnTicketBackfillComplete` コールバックを待つだけです。

次は **Tutorial 3 - Host/Search MatchMaker** で、明示的なホスト/サーチャーロールを使うマッチメイキングを学びます。
