あんさんぶるスターズ！！Musicのマルチプレイ機能「みんなでライブ」のマッチング実装

Happy Elements Advent Calendar 2020 23日目の記事です。

# みんなでライブ
![8_みんなでライブ_マッチング画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123340/9b7232ec-7aed-4ba9-1e09-9dccfebe27db.png)

　あんさんぶるスターズ！！Musicでは今年の11月に「みんなでライブ」のβ版をリリースしました。「みんなでライブ」はランダムなプロデューサーとマッチングして一緒にライブができる機能で、いつもとは違うアイドルの組み合わせや衣装を楽しむことができます。

　本記事では、マルチプレイゲームの実装の一例として、この「みんなでライブ」のプロデューサー同士のマッチングの仕組みについてご紹介します。

## 基本的なマッチングの仕組み
　「みんなでライブ」のマッチングロジックはアプリケーションサーバー内の処理で完結させています。あんさんぶるスターズ！！Musicのサーバーは複数のインスタンスで稼働しているため、全ユーザー共通でアクセスできるようにマッチングに関するデータはRedis上に置いています。
参考：[Happy Elements Advent Calendar 2020 17日目「AWSでのサービスローンチ3番勝負2020 in Happy Elements」](https://qiita.com/hiroshi_sumi/items/917ed1fff9777b1ce6fb#%E5%8B%9D%E8%B2%A0%EF%BC%91%E3%81%82%E3%82%93%E3%81%95%E3%82%93%E3%81%B6%E3%82%8B%E3%82%B9%E3%82%BF%E3%83%BC%E3%82%BAbasic%E3%81%82%E3%82%93%E3%81%95%E3%82%93%E3%81%B6%E3%82%8B%E3%82%B9%E3%82%BF%E3%83%BC%E3%82%BAmusic)

　あんスタ！！Musicではマルチプレイの部屋の単位をホールと呼んでいます。このホールのデータをすでにマッチングしている人数によってRedis Streamsに入れています。
参考：[Redis Streamsについて](http://mogile.web.fc2.com/redis/topics/streams-intro.html)

　Redis Streamsはほぼキューとして扱っており、`xadd`でデータを末尾に追加し、`xread`や`xreadgroup`で先頭の要素を取り出しています。コンシューマグループなんかを使ってうまく実装できるのではないかと思ってただのlistではなくRedis Streamsでの実装を行いましたが、現状単なるキューとしてしか使われてないのでlistでも別によかったかもと思っています。

　マッチングしたいプレイヤーからのリクエストがあった場合には、ホール内の人数（１〜４人）で分けられた4つのストリームのうちより埋まっている4人のホールのストリームから順に`xread`してホールを取得します。これによって最も埋まっているホールから順にマッチングされていき、どのストリームにもホールがない場合は新しくホールを作成します。

　マッチングの条件によってそれぞれストリームを作っており、現状の仕様では4（人数）×2（2Dプレイor3Dプレイ）×2（編成の総合値によるマッチングタイプ2種）で16本のストリームで成り立っています。

　5人揃ってマッチングが完了した際にはホールのデータをホールのIDをキーとしてRedisに保存して、その後のプレイで参照や操作ができるようにしています。

### サンプルコード（Ruby on Rails）
説明用コードなので適宜読み替えてください。
例としてJsonでRedisに保存する形式で書いています。

```
MAX_ROOM_USERS_COUNT = 5

consumer_group_name = "hoge"
user_id = 1
play_2d_mode = false
redis = Redis.new

def get_stream_key(count, play_2d_mode, matching_type)
    "stream_key_#{count}_#{play_2d_mode}_#{matching_type}"
end

def xadd(hall, play_2d_mode, matching_type)
    redis.xadd(get_stream_key(hall.user_ids.size, play_2d_mode, matching_type), {hall: hall.to_json}, id: "*")
end


(MAX_ROOM_USERS_COUNT - 1).downto(1) do |n|
    picked_hall_ids = []
    while do
        data = redis.xreadgroup(consumer_group_name, user_id, get_stream_key(n, play_2d_mode, matching_type), '>', count: 1, noack: true)
        break if data.empty?

        hall_data = data[get_stream_key(n, play_2d_mode, matching_type)][0][1]["hall"]
        hall = Hall.new(JSON.parse(hall_data))

        # 一度取得したhallをxaddしているので一巡したら同じhallが取得される
        break if picked_hall_ids.include?(hall.id)

        picked_hall_ids << hall.id
        if hall.add_user(user_id)
            if hall.user_ids.size >= MAX_ROOM_USERS_COUNT
                redis.set(hall.id, hall.to_json)
            else
                xadd(hall, play_2d_mode, matching_type)
            end
            return hall
        end
    end
end

hall = Hall.new
hall.add_user(user_id)
xadd(hall, play_2d_mode, matching_type)
return hall
```

## マッチングにおける追加処理
　ただプレイヤーを振り分けるならこれでOKですが、マッチングにおいて2つ処理を入れています。

### 退出プレイヤーの削除処理
　一つは、退出してしまっているプレイヤーの削除処理です。ホールのデータをキューに入れてしまっているため、退出があった場合にそのタイミングでホールのデータを触ることができません。

　そのため、ユーザーデータに現在入室しているホールのIDを保存しておき、退出のリクエストがあった場合にはこれをnilにするようにしています。そして、そのホールに別の誰かがマッチングしたタイミングで、ホールにいるはずのプレイヤーのホールIDを参照してまだホールに入ったままかチェックします。ホールIDがnilか違うホールIDになっているプレイヤーは削除し、削除後の人数を元にストリームに追加するようにしています。

　このようにマッチング処理的には退出後ラグが生じてしまっていますが、ゲーム画面ではゲームクライアント同士の双方向通信によって退出を検知しているので他のプレイヤーが退出したタイミングで画面は更新されるようにしています。

　ほとんどの場合は退出するより先に高速でマッチングが完了するため、退出ボタンを押してもすでにマッチングが完了していて退出できないパターンが多く発生しました。なので現在は、退出は人が少ないタイミングや何らかの要因によってマッチングが完了しなかった場合などに操作不能となってしまうための救済措置としての位置づけになっており、入室から数秒後に退出可能な状態になるようにしています。

### アイドルの重複防止
　もう一つは、ホール内のアイドルの重複の防止です。
　「みんなでライブ」ではそれぞれのプレイヤーがアイドルを選出してマッチングするので、ただマッチングさせるだけでは同じアイドルが複数いるホールができてしまうことになります。そのため、`xread`したホール内に同じアイドルがいなければマッチング成立、重複があればそのホールはそのままストリームに`xadd`して次のホールを`xread`しています。これを何度か繰り返すことで可能な限り重複を防いではいますが、マッチング時間がかかりすぎないように、稀にあまりにホールが見つからない場合はユニットに編成している次のアイドルが選出されるようにしてマッチングを成立させています。

## まとめ
　現状ではあまりややこしいマッチングロジックはなくほとんどのパターンでキューから取り出すだけなので、平均しても1秒以下で5人のマッチングが完了しています。

　ここまで文字ばかりで説明してしまったので図でまとめておきます。Redis Streamsの部分はキューとして扱えるなら何でもいいと思います。
![マッチング.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123340/e6bbb802-0bc7-6b90-17ad-18805edb530c.png)


# プライベートホール
![12_みんなでライブ_プライベートホール.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123340/ec7a820e-be0d-8622-f638-c27d7b1962fa.png)


　「みんなでライブ」ではプライベートホールを近日公開予定です。プライベートホールではIDを共有することで知り合いとマッチングして一緒に遊ぶことができる機能です。一足先にこちらの実装についても紹介したいと思います。こちらは開発中の情報なので書いてある仕様などは変更になることがあります。

## プライベートホールのマッチング
　プライベートホールではホールIDとは別で共有用のIDを発行していて、この共有用IDを元にしたキーでRedisに保存しています。ホールに入室するユーザーはこの共有用IDを元にリクエストを送り、Redisから取り出して入室処理を行って再度同じキーでRedisにセットします。

　プライベートホールではセンターのプレイヤーをホールマスターとして扱い、ホールマスターがホールメンバーを確定してライブを開始することができます。この確定のリクエストが来た時に、プライベートホール用のRedisのキーから取り出して通常のマッチング完了と同様の形式でRedisに保存します。その後は通常のマッチングからライブを行うのと同じフローです。

## プライベートホールでの繰り返しプレイ
　プライベートホールのライブについては、同じプレイヤーどうしで繰り返しライブを行えるようにライブ後再度同じホールのマッチング画面に遷移するようにしています。繰り返しプレイの際、共有用のIDはそのまま使用できるようにしています。

　これは、ライブ後にプライベートホール用にRedisに保存してあるデータを再度取得できるようにすることで実現しています。ただし設計上、内部値であるホールのIDを使いまわしてしまうと都合が悪いので、ホールマスターによるプレイヤー確定時にホールIDだけ新規に発行するようにしています。このようにすることで、同じ共有用のIDで何度も遊べるけれど内部的にはライブごとに別のホールとして扱えるようにしています。

　このような実装になっているので、ホールメンバー確定後に入室したプレイヤーがいたとしてもそのプレイヤーは次のライブ用に割り当てられたホールIDのデータを取得しています。そのため、現状のライブに影響はなく待っていれば他のプレイヤーが帰ってきて次から参加できるようにもなっています。（※あまりに時間が経ちすぎた場合には切断されてしまうことがあります）

## まとめ
　特にややこしくはないですが、プライベートホールのマッチングの流れについても画像にまとめておきます。
![アドカレ2020用素材.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123340/635b6612-5064-be18-7ccc-0bd99af2121d.png)

# 全体まとめ
　本記事ではあんさんぶるスターズ！！Musicの「みんなでライブ」のマッチングの実装について紹介しました。マッチングロジックについてはこの実装が完璧というわけではないかもしれませんが、実装工数やマッチングの速さ、リリースしてみての所感的にはそれほど悪くはなかったかもと思っています。

# メンバー募集
Happy Elements株式会社 カカリアスタジオでは、
いっしょに【熱狂的に愛されるコンテンツ】をつくっていただけるメンバーを大募集中です！
もし弊社にご興味持っていただけましたら、是非一度
下記採用サイトをご覧ください。
[Happy Elements株式会社 採用特設サイト](https://recruit.happyelements.co.jp/)