HTML5 ゲームを支える技術勉強会に参加しました

6/17のHTML5 ゲームを支える技術勉強会に参加してきました。 connpass.com 各発表の内容と感想等をまとめます。

HTML5 ゲームフレームワーク開発について

発表の目的

  • HTML5ゲーム開発に必要な要素を知ってもらう
  • それをHTML5でどう実現するのか共有する

アジェンダ

  • ゲーム開発用に必要な要素
  • ゲームとして成立させるための要素
  • ゲーム開発で楽するための要素
  • HTML5でどう実現する?
  • スマホとPCの両対応が前提
  • 約2年前のブラウザでも動作する機能
  • まとめ

ゲーム開発用に必要な要素

  • グラフィック描画
  • サウンド再生
  • ムービー再生
  • プレイヤーとの対話
  • リソース管理
  • ライフサイクル管理
  • デバッグ機能

HTML5でどう実現する?

グラフィック描画

  • WebGLを使う
    • GPUを使って高速に描画可能
    • HTML要素はcanvasだけあればok
    • 描画ライブラリを使うとお手軽
      • 2DならPixiJS
      • 3Dならthree.js
  • requestAnimationFrameで定期的に更新
    • ブラウザの描画タイミングを拾えるAPI
    • 最高60FPSで呼ばれる
    • 負荷が高いとフレーム間隔が伸びる
      • 60FPS前提でフレームごとに固定値のアニメーションは❌
      • じゃあどうするか
        • performance.now()でフレーム間隔を算出する
        • 起動からの経過時間をマイクロ秒単位で取得できる
        • 経過時間を算出し時間ベースでアニメーションさせる

サウンド再生

  • Web Audio APIを使用する
    • エフェクトかけたり色々できる
    • 自動再生ポリシーへの対応
      • ユーザの操作を契機に再生開始する必要がある
    • メモリ使用量に注意
      • 解放バグや解放のためにテクニックが必要です
    • バックグラウンドでも音が鳴り続ける
      • ライフサイクル制御により連携が必要
    • その他の問題や対策などqiitaにあります

t.co

ムービー再生

  • videoをWebGLに食わせる
    • WebGLはHTMLVideoElementをテクスチャとして受け入れる
    • iOSはplayinline属性が必要
    • videoも自動再生ポリシーに抵触する

プレイヤーとの対話

  • MouseEventsとTouchEventsを併用
    • イベントリスナーでキャッチできる
    • 離すイベントが発火しないパターンに注意
  • PointerEventsがやってくる
    • MouseとTouchの統合イベント

リソースの管理

  • ブラウザキャッシュで通信量節約
  • IndexeddbでJS主導の通信量節約
    • idb-cacheを使っている

t.co

ライフサイクル管理

  • resizeイベントを検知してからcanvasリサイズ
    • リサイズは少し待ってからする

デバッグ機能

  • ブラウザ備え付けの開発者ツール使う
    • drecom/stats.js

t.co

PWAゲーム開発の課題と対策

クライアント・サーバのアーキテクチャ紹介

クライアント(図で階層分けてあった・・)

  • Application(SPA)
    • animation video audio
    • afterEffects Spline.js GameFramework

サーバ(こっちも図がありましたが、構成要素だけ抜き出し)

  • AWS
  • APIリクエス
  • ALB
  • ECS
  • Target Group Blue
  • Target Group Green
    • nginx
    • API(Go)
    • fluentd
    • EC2

通信量をできるだけ減らすためのアセット配信

  • ユーザの期待
    • 出先でも遊べるようにとにかく通信量を減らして欲しい
    • どんなに面白いゲームでも通信量をがかさむゲームは遊びたくない
    • 期待に応えたい、でもゲームはwebアプリに比べアセットが肥大化しやすい
      • 最近はそうアセットサイズが1GMを超える事も珍しくない
    • ディスクキャッシュ容量はモバイルSafariだと50MB程度しかない
  • CacheStorageは救世主になるか
    • Android(chrome)限定だが、CacheStorageとPersistent Storegeの組み合わせで1GB超えのアセットの永続化はできる
    • しかしPWAを作るとなるとiOSも対応したい

通信量を減らすためにできる事

  • Cache-Controlで1年キャッシュ
    • index.html等の読み込みの起点となるファイル以外は全てmax-ageを1年にする
      • CDNへの更新確認リクエストすら飛ばさない
      • 何か不具合があっても時間が経てばキャッシュが消えて直すといった淡い期待は持たない
      • 絶対に消えないキャッシュを作るという強い意志を持ってキャッシュする
      • 同一のアセットを更新したいときにキャッシュが効くと困るため、必ずクエリにバージョン番号をつけて取得する
  • キャッシュの削除戦略
    • アセット名にコミットハッシュ(revision)を含める
    • アセットリクエストのクエリにバージョン番号を含める
    • どちらを選んでも参照するためのハッシュ値orバージョン番号を何らかの手段で取得しなければならない
  • キャッシュの取得方法
    • アプリケーションビルド時にjsやhtmlにバージョン付きで埋め込む
    • アセット取得パスを動的に組み立てる場合
    • 都度サーバのapiレスポンスに必要なアセットのバージョンを含める
    • アセットパスとバージョンの対応関係が書かれたマニフェストファイルをあらかじめ取得しておく
    • 弊社ではサーバのレスポンスに一部のアセットのバージョンだけを含める対応コストが高かったため、全てのアセットのパスとバージョン番号の書かれたファイルを参照した
  • デフォルトでgzip圧縮対象でないファイル形式のgzip対応
    • 全ての主要ブラウザがAccept-Encoding: gzipヘッダ付きでアセットを取りにきてくれるので少し工夫すると上記以外のアセットも圧縮・配信することができる
    • S3にアップロードする際、Content-Encoding: gzipをつけておけばどんなアセットでもgzip圧縮済みとして配信できる
    • ファイル形式によって圧縮効率が変わるため、ほぼ意味がない場合もある(pngとかjpeg)ので適材適所で利用する
    • 動画とかは効果が高い
  • 圧縮フォーマットの利用
    • pngはwebpにしてchormeはwebpを使う
      • chomeブラウザで遊ぶユーザのためだけに用意している
      • クライアント側で利用ブラウザで判定してリクエストを全てwebpに置き換える
  • アトラス化
    • 画像のアトラス化はやって当たり前
    • 音声データのアトラス化もある
      • asm.jsを利用したJSのwebassembly対応
      • 両方用意しておいてwebassembly非対応のブラウザの場合はasm.js側のファイルをリクエストする

ソースコードや通信内容が容易に見れる中でのチート対策

API通信内容の秘匿化

  • 大前提としてリクエスト・レスポンスの内容は簡単に見ることができる
  • リクエスト内容がわかると同じリクエストを複製したり一部を書き換えて偽装したリクエストを送ることが容易になる
    • botを作られるリスクが上がる
  • レスポンス内容がわかるとそこからゲームの攻略情報が漏れてしまう危険性がある

対策

  • リクエスト内容を共通鍵暗号方式で暗号化、復号する
    • 暗号化処理はC++で書かれたものをEmscriptenを使ってasm.js/WebAssembly化して行う
    • 通常リクエスト本文をヘッダを含めて暗号化し、POSTリクエストのbodyに入れて送る
    • 復号は専用のnginx moduleを通して行い。bodyの中身を復号したのち、本来のリクエスト内容を入手してアプリケーションサーバにリバースプロキシする
  • レスポンスの暗号化・復号

アセットパスの類推対策

  • アセットへのアクセス内容は全て簡単に見ることができる
  • アセットパスが類推可能な場合公開前のリソースを抜かれるリスクがある
  • アセット内容がjsonなどのテキストデータの場合は直接見ることができるのでゲームの攻略情報に繋がる情報を取得されるリスクがある

アセットパスの類推対策案

  • パスの途中にサーバが払い出した文字列を含める
    • サーバからもらったハッシュ値を含めてアセットを取得する
    • サーバが払い出したハッシュ値がナイトアクセスできないのでオリジンに先にアセットをアップロードしたとしても抜かれる心配がない
    • 問題: ローカルに保存したあと、ファイル名を推測されやすい
  • パス全体をハッシュ化して難読化する
    • ファイル名を含めたパスを完全に難読化する
    • 拡張子も、存在しないと動かない場合を覗きできる限りとる
    • 問題: 難読された後のパスを使ってデバッグするのは困難

実際に行った対策

  • サーバが払い出したハッシュ値を使って組み立てたアセットパス全体を難読化する
    • 難読化ロジックはc++で記述し、Emscriptenで変換する(JSのライブラリを利用していない理由は、変換後のソースが読みにくい事をあえて利用しているから)
    • アセットパス全体を難読化するのはステージング環境に限定し、開発環境ではハッシュ値付きの状態では扱う
      • デバッガビリティの確保

アセット自体の秘匿化

  • テキスト形式のファイルの秘匿化
    • apiリクエスト・レスポンス同様共通鍵方式で暗号化したものをアップロードする

画像の秘匿化

  • 画像はそのままだとdevtoolを使って見る事やダウンロードすることができてしまう
    • リリース前に実験したが、実装の問題でメモリに対する負荷を許容できなかったため見送りしたがやり方を紹介
  • HTMLImageElementを作るとどうしてもdevtoolに表示されてしまう
  • DataURLSchemaを使ってもダメ
  • WebGLに対応していることが前提になるが、WebGLRenderingContext.tex(Sub)Image2DにはImageDataを渡すことができる

アプリケーションコードの秘匿化

  • 共通鍵暗号・復号を行なっている箇所を特定されたり鍵を取られると今までの対策が破られてしまう
    • uglifyをかけた程度のソースコードでは該当の処理を特定される
    • 元のソースコードをA、暗号化した後のソース事をxhr経由で取得して復号した後にevalするプラグラムをC++で作成し、これをemscriptenで変換して利用する
      • evalしたコードはdevtoolに表示されない
      • そのままevalするとsourcemapで追えなくなるので、//#sourceUR=https://exmaple.com/app.jsのようにあのテージョンを付与してevalするとデバッグできるようになる
      • sourceURLをつけるとchromeが良かれと思ってdevtookにその名前でソースコードを表示してしまう
      • ソースコードなしの状態で一度evalしておくと、ソースコードが表示されない状態でdevtool上に表示される

インゲームのチート対策

  • ゲームの構成をインゲームとアウトゲームに分けた場合、インゲームはそのゲームの遊びのキモでありやりがいがある部分
    • それだけに攻撃を受けやすい

ロジックをどこで動かすか

  • クライアントのみで動かす場合
    • チーターから攻撃を受けやすく対策もしづらい
  • サーバのみで動かす場合
    • チートの攻撃は受けにくく対策もしやすい
    • サーバアプリケーションを書く必要がある
    • 通信していない事を意識されるゲーム体験を提供したい場合は応答速度に気を配る必要がある
  • クライアントとサーバの両方で動かす場合
    • 比較的ターン制のゲームに向いている
    • 同じロジックがサーバでも動くでチートはほぼ不可能でクライアントで計算した結果をすぐに表示できるのでユーザ体験もよくなる
    • アーキテクチャ的にも複雑になる

PWAのネイティブアプリ化

  • 期待されている事
    • 通信量の削減
    • パフィーマンスの向上
    • 各種ブラウザ起因のバグの解消
  • 実際の実装はwebviewを使うことがほとんどなのでブラウザ特有の問題は残る
  • 通信量の削減だけは対応できた(xhrとimage編)
  • WKWebViewのWKUserScriptを.atDocumentStartとして作れば任意のJSをアプリケーションロード前に読み込まれることができる
    • プリソードするJS内でXMKHttpRequestやHTMLImageElementの読み込みりょりをフックしてURLをネイティブ側にプロキシする
  • 動画編
    • あらかじめネイティブ側でダウンロードしたバイナリデータをblobに変換し、与えたblobデータを使って動画を再生できるようになる

まとめ

  • 触れられなかった具体的な話はbuilderscon tokyo 2019でする予定

HTML5でリアルタイム対戦の3Dシューティングゲームを開発してしまった。反省会

  • 技術的な条件
    • 3Dでサクサク
    • 縦画面
    • 片手で遊べる
    • 20人/サーバ
    • iPhone6(+)対応
  • 技術選定
  • Unity❌
    • モバイルで3D動かない
  • three.js❌
    • エディターがない
  • PlayCanvas⭕️(?)
    • 一見なかなかいい
    • 短期間でそれっぽいプロトタイプ
    • エディタがいい
    • 3Dサクサク

ドローコール管理

  • ドローコールの予算を立てる(100~150)

    • キャラクター、環境、発射体、シャドウ
    • スキンしたメッシュのバッチングはできない
    • マテリアルを減らす
    • カスタムシェーダを必要最低限
    • ライティング戦略
      • ライティングは贅沢
      • Scene全体に対し、ライト1つだけ
      • ポイントライトは必要最低限に
      • ベイクしたライティングを使用したいが、
        • バグがでとる
        • ダウンロードサイズが大きくなる
        • 静的オブジェクトは一度生成したらそのあとも使うのでドローコール1回で済むようにしている
  • FBXのインポートに要注意

    • PlayCanvasのFBXインポート機能は素晴らしい
    • マテリアルが組み込まれているFBXがあるとドローコールが倍に
  • パフォーマンスまとめ
    • 3Dアセット=重い
    • エンジン信用するな

ネットワーク

  • 要件
    • latency: 20~40ms(LTE/4G)
  • websocket: Head-of-Line blocking(1つのパケットを失うとその後のパケットも再度通信が必要に)
    • MTUを見極める:〜1400bytes超えちゃうと2個になる=>latencyが上がる
    • 通信不良によるパケット損失
  • 解決策
    • メッセージのサイズを1265bytesに制限
    • 移動:メッセージをバッチし、50msごとに通信
    • バッチにタイムスタンプをつけて、100ms以上古いメッセージを捨てる
    • 補完と外挿
    • ライブイベントはリアルタイムで通信

ビルドサイズ

  • ビルドサイズ7〜10MB
    • モデルはtextureなしで250k(45k圧縮)
    • CDNコスト3~5万円/月 for 5000~7000 DAU
    • キャッシュが残らない
    • PlayCanvasのアセットバンドリングを対応していない

ネットワークまとめ

  • メッセージングサーバはややこしい
  • ネットワークの深いところまで知っておかないと痛い目にある
  • ビルドサイズは気をつけないとバカにならない

まとめ

  • 3Dの課題は技術だけでは解決できない
  • 完璧なフレームワークはまだまだ
    • Unityがなんとか頑張ってくれる事を期待

感想

  • ブラウザゲーム特有(またはブラウザだけ顕著に現れてしまう)問題に向き合って一つ一つ対応している知見が聞ける素晴らしい勉強会でした
  • HTML5のゲームちゃんと作るにはネットワーク周りの知識がかなり必要
  • DRECOMさんのAROWおもしろそう

arow.world