# 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="/files/idAAQl6SzzTCqpW9JYmB" 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="/files/OVuWzJ50vArMb5KOTDE4" 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** で、明示的なホスト/サーチャーロールを使うマッチメイキングを学びます。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://help.diarkis.io/diarkis-client/chtoriaru/unity-chtoriaru/tutorial-2-ticket-matchmaker.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
