【Unity】通信の話

はじめに

はじめまして。 カヤックのソーシャルゲーム事業部の Unity エンジニアのです。
今回は Unity の通信周りについて紹介します。
この記事はカヤック Unity アドベントカレンダー 2016 の 19 日目の記事です。

HTTP Methods

HTTP(The Hypertext Transfer Protocol)はクライアントとサーバー間の通信のためのプロトコルなんです。このプロトコルでは幾つかの通信メソッドを定義してます。

  • GET
  • POST
  • HEAD
  • PUT
  • DELETE
  • OPTIONS
  • CONNECT

見た目上は多いかもしれないが、実際にゲームを作る時はほとんと GET と POST しか使わないです。

では GET と POST の違いを見てみましょう。

GET POST
キャッシュ できる(こともある) しない(原則)
Encoding type application/x-www-form-urlencoded application/x-www-form-urlencoded 又は multipart/form-data(バイナリー用)
データサイズ制限 ある。URL にデータを載せるので、URL の長さに(2048 キャラクター)制限される ない
安全性 URL にデータを載せるので、パスワードなど秘密情報を送る時に使っちゃいけない GET より安全ですが、より安全性を求めるなら特別な技術が必要(後で紹介します)

複雑に見えますが、これだけ覚えておけば基本大丈夫です:

  • 情報を取得するには GET
  • 情報を送るには POST(特にセンシティブな情報の場合)

URLのエスケープ

リクエストをする時に、パラメータを指定することがよくあります。英数字のパラメータは大丈夫ですが、時には記号や日本語が入ってるパラメータを指定することもあるでしょう。
このようなリクエストも考えると、リクエストをする前に、パラメータをエスケープする必要があります。例えば、foo bar&abcあいうえおfoo%20bar%26abc%E3%81%82%E3%81%84%E3%81%86%E3%81%88%E3%81%8A にしてから、サーバーに送るのです。

エスケープする方法はこのドキュメントを見てみましょう:
Unity Script - WWW.EscapeURL

自前でやってもいいですが、既存ライブラリーもあるので、こだわりがないときは既存のものを使うのがオススメです。

ヘッダ

リクエストとレスポンス本体に関係ない追加情報をつけるため、HTTP のヘッダー(Headers)が使われています。ヘッダーには認証情報や、キャッシュ情報、エンコード、クッキーなどの情報がよく入っています。具体的にはこのページに参考するのがオススメです(英語版のほうがいいです):
MDN - HTTP Headers

改変対策

ゲームではルールでプレイヤー間の公平性を確保のが大事です。改変対策しないと、ユーザーがフェイクなリクエストで他のユーザーデータを操作したり、自動プレイツールでレベルアップしたりすることが可能になりやすいです。ここではよく使う二つの技術を紹介します。

Signature

最も一般的で、有効な方法はリクエストのヘッダーに Signature をつける方法です。流れは大体こんな感じです:

  1. クライアントがリクエストのメソッド、URL、パラメータ、時刻(ここが大事)と他認証に必要な情報を文字列でつないで、BaseString を作る。
  2. BaseString を Encode する。
  3. エンコードされてた BaseString と、秘密な SigningKey を使って、決まったハッシュメソッドで Signature を算出。

細かいことは Twitter の開発サイトを参考にするのをオススメします:
Twitter Dev - Creating a signature

さらに、OAuth という成熟した認証フローがあるので、大体の場合に、OAuth を使うのがオススメです。ドキュメントはこちら:
OAuth

リバースエンジニアリング対策

いずれの Signature 方法でも、最終的に SecretKey というパスワードみたいな文字列が必要となっています。この文字列を手に入れると偽リクエストが作れるので、SecretKey を隠すのが大事です。
文字列はバイナルにコンパイルされても、string コマンドでリストアップできるから、SecretKey を直接にコードに書くではなく、独自なメソッドで組み込んだほうが安全なんです。

チャレンジレスポンス認証

Signature とは別にチャレンジレスポンス認証(Challenge and Response Authentication)という方法もあります。
やり方は Wiki を参考するといいでしょう。

ただ、チャレンジレスポンス認証ではパラメータが認証フローに参加しないので、Proxy などを設けて、パラメータを改竄することができるんです(通称 Man in the middle Attack)。厳密な対策はやはり OAuth などを取り込んだほうがいいです。

ユニーク ID を使ったリトライ

通信のタイムアウトや通信切断時などの後、同じリクエストを送り直すことになりますが、失敗した方のリクエストがサーバに到達していた場合、リトライの通信と合わせて2回サーバで処理が走ることになります。これによって意図せずアイテムが二重で使用されたりする懸念があります。

この対策として、更新系のリクエスト(POST)のとき、GUID などリクエストごとにユニークな ID を発行してリクエストパラメータに追加しておき、通信エラー時は同じ GUID でリトライします。
サーバ側はユーザごとに最後のレスポンスおよび GUID をキャッシュしておき、キャッシュの GUID と同じ GUID のリクエストが送られてきたら、処理を行わずキャッシュされたレスポンスをそのまま返します。
こうしておくことで、通信エラー時にリトライしてもサーバで二重処理されることがなくなります。

CDN

Content delivery network のことを意味しています。Wiki での紹介はこちら

画像、音声、動画など静的なファイルはサイズが大きいし、リクエストごとに変わらないので、アプリサーバーではなく、CDN にホストすることが一般的です。
CDN はファイルを複数のサーバーに置くことで、広範囲、高速度、スケールしやすいなどの特徴があるんです。

世の中では CDN サービスを提供する会社がいっぱいあります。Amazon の CloudFront、MaxCDN、CloudFlare などそれぞれプランがあるので、必要に応じて使うのがいいでしょう。

Webフロントエンジニアが一般参加型コンテンツをつくった話

この記事はカヤックアドベントカレンダー2016の18日目の記事です。

こんばんわ!
今年の春より社会人になりました、HTMLファイ部のゆみこふ( @yumikokh )です。
普段の業務ではクライアントワークチームでWebを作っております。

が!

あるリアルイベントで、一般参加型コンテンツの制作を担当させていただきました。

今回は、 @ki_230 先輩の記事

ウェブページをできる限りネイティブアプリっぽく魅せるテクニックまとめ 〜アップルにリジェクトされつづけるなら、ウェブアプリとストアをつくって自前で配信してしまおうという企み〜 - KAYAC engineers’ blog

に便乗しまして、そのときの知見について書こうと思います。

もくじ

  1. 一般参加型コンテンツ?
  2. システム構成
  3. ブラウザ感を隠す小技集
  4. 音のだしわけ
  5. おまけ
  6. さいごに

一般参加型コンテンツ?

『一般参加型コンテンツ』と言われてもピンと来ないと思います。

ここでは一般の来業者の方に実際に参加して楽しんでいただく、アーケードゲームのようなものを指しています。 (なにかイケてる呼び方があればこっそり教えてください)

システム構成

制作したのは、画面上のマーカーをタッチしてポイントをゲット、
みんなでスコアを競うというシンプルなゲームです。
(諸事業によりコンテンツの詳しい内容は割合します)

60インチのタッチディスプレイを使用したため、センシングに関して実装の必要がありませんでした。
コンテンツはopenFrameworksでもにょもにょ…ではなく、
いつもどおりHTMLとCSSとJavascriptで書いて
Chromeブラウザをフルスクリーンにして見せているところがポイントです。

音の出力はモニタ側(HDMI接続)と別スピーカー(オーディオ端子接続)の2箇所です。
得点したときのピコーンという効果音はモニタ側からステレオ出力、
拍手のようなオーディエンスの音を別スピーカーからモノラル出力します。

ブラウザ感を隠す小技集

ひとつひとつはとても単純ですが
「なーんだただのChromeじゃんっ」と思われないためには大切なことです。
(当然ながら@ki_230先輩の記事と重複するところがあります)

スワイプを防ぎましょう

document.body.addEventListener('touchmove', (ev) => {
    ev.preventDefault();
});

タッチでテキストや画像の見た目を操作できないようにしましょう

* {
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0); // タップ時のハイライトを透明に
  -webkit-touch-callout: none; // 長押し時のメニューを消す
  -webkit-user-select: none; // テキストなどの選択を防ぐ
}

img {
    pointer-events: none; // 画像ドラッグを防ぐため ※重要
}

右クリックメニューを防ぎましょう

document.oncontextmenu = function() {
    return false;
}

カーソルを消しましょう

trans_cursor.cur という透明画像を用意してカーソルとして割り当てます。 f:id:ymym0308:20161218234752p:plain← 👀
デバッグ時にはカーソル欲しくなるのでキーボードでクラスを切り替えられるようにしておきます。

.trans-cursor {
    cursor: url(../images/trans_cursor.cur), auto;
}

音のだしわけ

Audio Midi 設定

まずは既存ソフトを使ってちょっと設定を行います。
一般的に音チャンネルを出し分けするのに使われるソフトにSoundflowerが挙げられますが
今回はAudio Midi 設定というMac標準のアプリだけで設定が完結しました。
(オーディオインターフェースも必要なし!)
左下の「+」よりオリジナルセットを作成し、内蔵出力->HDMIの順にチェックをすると
4つの出力チェンネルが作成され、1,2はHDMI、3,4は内蔵出力になっています。

f:id:ymym0308:20161219005111p:plain

(なぜか内蔵出力の色が濃いグレーのままなので適応されてるか不安でしたが大丈夫でした)

実装

Web Audio APIという音声処理用の高機能なインターフェースを使います。
WebAudioAPIについての詳しい説明は こちら

モニタ側はステレオ出力、別スピーカーではモノラル出力の想定です。
音源読み込み時にsubSpearkerFlgをtrueにしたものを別スピーカーで出力するようにします。

↓イメージ図

f:id:ymym0308:20161219003812p:plain

// 初期設定
const ac = new AudioContext; // Audio Contextの作成
const splitter = ac.createChannelSplitter(2); // 異なるチャンネルのソースををモノラルとして扱えるようにする
const merger = ac.createChannelMerger(4); // 異なるモノラル入力をまとめる
if( ac.destination.maxChannelCount != 4 ) throw new Error('Audio Midi設定が適応されていません');
ac.destination.channelCount = 4;

// ここから新しいソースノードを作成する時の処理
// subSpeakerFlgがtrueのとき別スピーカーで出力するように指定
createNewSource(buffer) {
    const source = ac.createBufferSource();
    source.buffer = buffer; // XHRで音源をロードしておく
    source.connect(splitter); // ステレオチャンネルをモノラルで扱えるようにする

    if (!subSpeakerFlg) {
        // モニタ出力の場合
        splitter.connect(merger, 0, 0); // 左チャンネルからの入力をモニタ側の左チャンネルで出力
        splitter.connect(merger, 1, 1); // 右チャンネルからの入力をモニタ側の右チャンネルで出力

    } else {
        // スピーカー出力の場合
        // 左・右チャンネルからの入力を別スピーカー(モノラル)で出力するように指定
        // AudioMidi設定で設定した3チャンネル目で出力するようにする
        splitter.connect(merger, 0, 2);
        splitter.connect(merger, 1, 2);
    }

    merger.connect(ac.destination);
}

おまけ

今回のイベントでは他にもスコアを出すコンテンツがあり、その合計点を競うというものでした。
スコアは1つのサーバーで管理するので、
Node.jsで書かれた送受信用の別アプリを同時に立ち上げておき、OSC通信でやりとりをしています。
別アプリについてはここでは触れないですが、コンテンツ側では以下の単純なコードで送信を行うことができました。(雑な説明

const socket = io.connect(); // セッティング
socket.emit(“point”, finalScore); // 送信

また、当初はWeb技術だけで本物のネイティブアプリを作ることができるElectronを採用する予定でしたが
今回はChromeだけで間に合いました 🍣

さいごに

Webブラウザで実現できることは多岐にわたり、
Webエンジニアであっても新しい言語を覚えることなく、
インターネットの外に飛び出すことができるようになりました!

私の普段の業務では、インターネットを通してユーザーの方に制作したサイトに訪問していただくので
はてぶやツイッターなどからしかユーザーの方の声は聞こえません。
今回のように現場展示のコンテンツの場合、
自分が作ったコンテンツでユーザーの方が楽しんでいただいている様子を直接目にすることができて
通常業務とはまた違った喜びがありますね :)

先輩方のサポートもとても助かりました!ありがとうございました!

このように、カヤックでは新卒1年目であっても
所属の垣根を超えたお仕事に関わることができます。
チャレンジ精神旺盛な仲間を絶賛募集中です!

明日は @konboi さんです。おたのしみに!