🎉

アイスブレイクで使える価値観共有アプリ「ワイワイ」を Nchan で作った

2022/12/07に公開

この記事は、Sansan Advent Calendar 2022 7日目の記事です

昨日は、研究開発部 Architect グループ 八藤丸さんの 自社OCRエンジン「NineOCR」の学習効率化のためFeature Storeを導入した話 でした。学習に利用できるデータを用意するために払っていたコストを、モデルの改善というより本質的な部分に当てれるようになる素晴らしい改善だなと思いました。
今日は打って変わってゲームのお話です。

TL;DR

  • 内定者期間中に「ワイワイ」というアプリを作りました
  • インゲームのリアルタイム通信を Nchan と WebSocket で実現しています

自己紹介

Sansan Engineering Unit に所属している住江と申します。22年の新卒で、「営業DXサービス」 Sansan の開発に携わっています。

Sansan では、アプリケーションで扱うデータ量が膨大で常にパフォーマンスを意識したコードを書かないといけません。パフォーマンス・チューニングが好きな方、やりがいしかないです!普段一緒に開発しているチームメンバーは明るいながらも強く、日々触発されながらエンジニアリングを行うことができています。

社内では他にスマートセミナーというプロダクトに携わっていたこともあります。セミナーを楽に開催できるサービスで楽に開催することで開催回数を増やし接点を増やす、「出会いからイノベーションを生み出す」プロダクトで、愛しています。

ワイワイを作った経緯

22新卒の内定者期間中はイベントや懇親会はオンラインで開催されることが多く、十分深くまで話すこともできず終わることもあり、もったいないなぁと思っていました。大学に通ってる後輩も新歓がオンラインで難しいという声も聞いていて、みんなが話せて盛り上がるアイスブレイクのツールを作りたい、そう思っていたときに、『ito』ってカードゲームがおもろいっていう話を友だちから聞きました。

詳しいルールは割愛しますが、各プレイヤーはお題に沿った言葉で自分の手札にある数字を表現する、というゲームです。
表現が可笑しかったり他の人と価値観が違ったりするときに、ちょっとその人の思考などがわかって仲良くなれるアイスブレイクにぴったりのゲームです。

ただ、このゲームはカードで行う前提になっていてオンラインでできません。だから自分らでちょっとルールを足してアプリを作ってオンラインで遊べるようにしようと思ってできたのが価値観共有アプリ「ワイワイ」です。

ワイワイのゲームシステム

ワイワイは ito のルールに、数字自体を予想するというシステムを足しています

  1. 各プレイヤーに1~100までの数字が割り当てられる
  2. お題が出され、自分の数字をそのお題にそって表現する(ex. 自分に割り当てられた数字が83でお題が「無人島に持っていきたいもの」だったら「ナイフ」などと表現する)
  3. 全員が表現していき、他の人の数字を予想する
  4. 全員で話し合って数字が小さそうな人から順に予想していき、全員正解ならクリア!

といった感じです

ゲーム内でのリアルタイム通信

ワイワイはゲームシステム上、各個人が予想した数字や予想が完了したというイベントをもとにゲームが進行していくため、そのイベントを各クライアントに伝える必要があります。また、予想結果やクリア判定などは全員同じタイミングでわかったほうが盛り上がるため同時性が求められます。そのため各イベントや結果の取得はポーリングで行わず、WebSocket を利用した Pub/Sub パターンで実装することにしました。

例えば、あるプレイヤーが数字の予想を完了したしたときだと、予想の入力完了メッセージを publish して他の subscribe しているプレイヤーがそのメッセージを受け取り、UI に反映させます。

また、全員が予想し終わったときは、ルームを作成したホストが結果を集計したメッセージを publish して次の画面に進みます。

さて、この Pub/Sub を実現するためには配信するサーバーが必要なのですが、今回サーバー郡はクラウドサービスを利用せず作成していたので自分で構築する必要がありました。
そこで、今回は Nchan というものを使用しました。

Nchan は pub/sub メッセージングモデルのブローカーとして動作する nginx のモジュールで、インストールして使用することができます。設定ファイルをいくつか記述すればサーバーの実装なしにスケーラブルな pub/sub サーバーを構成できるので便利です。メッセージの保存先には Redis を設定できます。

公式の使用例はこちらです

#...
http {  
  server {
    #...

    location = /sub {
      nchan_subscriber;
      nchan_channel_id $arg_id;
    }

    location = /pub {
      nchan_publisher;
      nchan_channel_id $arg_id;
    }
  }
}

このように nginx の設定ファイルに記述するだけで、
ws://example.com/sub?id=channel_id に接続している subscriber に、
POST https://example.com/pub?id=channel_id で publish することができます。
ここでいう channel_id は Pub/Sub メッセージングモデルでいうトピックです。
とても便利ですね

ワイワイの設定は次のようになっています

upstream my_redis_server {
  nchan_redis_server redis;
}
server {
  #...

  nchan_channel_group_accounting on;

  # /sub?channel_id=hogehoge&player_num=4 の形式で使う
  location = /sub {
    nchan_subscriber;
    nchan_channel_group $arg_channel_id;
    nchan_channel_id $arg_channel_id;
    nchan_group_max_subscribers $arg_player_num;
    nchan_redis_pass my_redis_server;
  }

  # /pub?channel_id=hogehoge の形式で使う
  location = /pub {
    nchan_publisher;
    nchan_channel_group $arg_channel_id;
    nchan_channel_id $arg_channel_id;
    nchan_redis_pass my_redis_server;
  }

  # /group?channel_id=hogehoge の形式で使う
  location = /group {
    nchan_channel_group $arg_channel_id;
    nchan_group_location;
    nchan_group_max_channels 1;
    nchan_redis_pass my_redis_server;
  }
}

Nchan ではチャンネルをまとめる Channel Groups を作成できます。nchan_channel_group ディレクティブに入る値でまとめられます。チャンネルが増えてきて channel_id がコンフリクトしそうになったときなどは便利です。

グループにまとめて nchan_channel_group_accounting ディレクティブを on にすると、まとめた group のチャンネル数やメッセージ数、subscriber 数を集計・制限できます。上の設定だとGET https://example.com/group?channel_id=hoge でその情報が取得できます。

今回は group と channel が1対1対応となっているのでグループ化とは言いづらいですが、現在の部屋参加人数(subscriber数)を取得するために利用しています。

nchan_redis_pass ディレクティブではメッセージの保存先として利用する Redis への接続を指定しています。

ワイワイでは部屋IDを channel_id にしています。先程のイラストの例で言うと、部屋に入ったプレイヤーはみな ws://example.com/sub/channel_id=ROOMID を subscribe していて、アクションを起こしたプレイヤーが

POST https://example.com/pub?channel_id=ROOMID
{
	"type": "guess",
	"guesserIndex": 0,
	"guessNumbers": [23, 35, 80]
}

すれば部屋の全員にメッセージが行き渡るという形です。

ワイワイではゲーム内のアクションを保存する必要がないので、メッセージで完結させて DB に値を書き込まない構成になっているのは気に入っています。

ちなみに、WebSocket 接続が途中で途切れても再度 subscribe を開始すれば channel に配信されていたメッセージを全て取得することができます。クライアントに websocket-sharp を使用していて、CloseEventCloseEvent.code をハンドリングすることができるので、意図しない接続終了に関しては、再接続処理を行いゲームに再度合流できるようになっています。再接続すると再度同じメッセージを取得してしまうかもしれないのでメッセージで発火する UI 側のイベントは冪等にする必要がありました。

https://github.com/sta/websocket-sharp

業務と趣味開発の両立

ワイワイは、新歓や懇親会に間に合うよう2月3月に作成し4月までにリリースすることができました。実際に内定者の間や人事の方にも遊んでもらうことができ、盛り上がったようで嬉しかったです。リリース後もフィードバックや感想を受け取る口を作ったり、ゲーム中リアクションを送信できる機能を追加したりアップデートして運用・保守しています。現在 AppleStore だけで7000ダウンロード以上してもらっています。たまに YouTube で実況してもらったりするのも見かけて喜んでいます。

これは内定者期間中に作成したのですが、業務が実際に始まってからも趣味開発は続けています(SAPIENS 等)。学生自体から趣味では toC 向けのゲームやサービスを作ることが多いです。

しかし、toB 向けサービスを作っている Sansan で働くことにして本当に良かったと思います。ユーザーの業務に影響するので、toC と比較してより安全にダウンタイムを作らないようにデプロイ・マイグレーションを行う大事さを学べました。長年の監視・運用保守のためにできるだけシンプルに作る大事さも学びました。個人的にも長年運用するサービスを作れたらと思っているのでその考えは忘れないようにしたいと思います。

おまけ

リアルタイム通信面白いですよね、グループチャットなどみんな一斉に画面が変わるみたいな体験が好きです。

現在は WebSocket や WebRTC が主流だと思うのですが、新しく WebTransport という技術が出てきています。
WebSocket は TCP が利用されており、信頼性の高い通信が確保されている一方、高速性は期待できません。そのため、映像や動画配信などには不向きと言えます。
WebRTC では UDP が利用されています。UDP は TCP と違って信頼性は低くなりますが、代わりに高速転送が可能です。WebRTC ではサーバーを介さず、クライアント同士が直接通信を行うP2Pを前提としています。P2P では、接続人数が多くなるほどデータ送信の負担が増えることに繋がり接続数に限界があります。
WebSocket で利用されている TCP や、WebRTC で利用されている UDP の利点を引き出したプロトコルがQUIC です。この QUIC をコア技術として持っているのが WebTransport で、高い信頼性を保ちつつリアルタイム配信が可能となっています。つまり、WebSocket や WebRTC では対応できなかった部分をカバーしているため、両方のユースケースに対応できます。

最近スポーツのライブ配信やクラウドゲームが増えてきて映像・エンタメ分野でリアルタイム通信が求められることが多くなっています。大量データや低遅延を実現する WebTransport の動向をこれからも追っていきたいですね。

Discussion