JavaScript | GREE Engineering https://labs.gree.jp/blog Tue, 27 Sep 2022 10:41:59 +0000 ja hourly 1 https://wordpress.org/?v=6.7.2 https://labs.gree.jp/blog/wp-content/uploads/2015/01/favicon.png JavaScript | GREE Engineering https://labs.gree.jp/blog 32 32 81337045 Mozilla Hubsの音響とアバターTips - 夏のインターン完了報告書より https://labs.gree.jp/blog/2020/09/20816/ Tue, 29 Sep 2020 11:18:08 +0000 https://labs.gree.jp/blog/?p=20816 GREE VR Studio Lab, Directorの白井暁彦です。

ラボではMozillaが開発するブラウザで利用できるオープンソースWebVR「Hubs」の研究を行っています。
この夏、関連テーマでインターンに取り組んでいただいた静岡大学の坂口塔也さんからインターン完了報告書が届きましたので、一部編集して紹介していきたいと思います。

はじめに

このドキュメントは、GREE VR Studio Lab(以下ラボ)でインターンシップを経験した坂口さんが、その内容を振り返り得た知見を一部共有するものです。公開にあたってはラボのディレクターである白井が加筆やリンクや画像など、必要な情報を補った上で公開版を作成しています。

まずインターンシップはコロナウイルス感染症による状況を鑑みて、2020年6月から9月末までフルリモート形式で実施しました。主な内容は「Mozilla Hubsを活用したVRライブイベントの開発および発信事業のサポート」です。コロナ禍において急速に需要が高まるソーシャルWebVRプラットフォーム「Hubs」に関する基盤技術の収集・検証、特に空間設計やHubsのアバターに関する問題の調査・解消が中心になります。テクニカルアーティストやリサーチャーのような側面もあるお仕事といえます。

坂口さん(左上)とVRStudioLabの面々

このブログエントリーでは主に、音響に関する問題解決と、アバター関連の不具合調査と解決、最後に坂口さんのインターンふりかえりについて紹介します。

関連情報

すでに公開されたイベントやリソースへのリンクはこちら

Hubsドキュメント日本語化(Github)

VRSionUp!7「Hubs Study」

「Spokeを使ったイベント設営TIPS」

VTech Challenge 2020「Withコロナ時代の社会問題の解決」

・「交流型WebVRにおける空間音響のオンライン評価手法」(第25回 日本バーチャルリアリティ学会大会論文集)


PannerNodeによるHubsの音響設計

まずは音響関係の研究です。学術的な研究発表としては第25回 日本バーチャルリアリティ学会大会論文集「交流型WebVRにおける空間音響のオンライン評価手法」にて発表を行っていますので詳細はこちらの予稿Slideshareもご参照ください。

新型コロナウイルスの爆発的な感染拡大以前からの課題ではあったのですが、不特定多数の参加者が会話ベースのコミュニケーションを行う国際会議や展覧会をオンラインで開催するにあたり(単なるブラウザで動くWeb3Dではなく)ソーシャルVRプラットフォームが注目されており、その一つであるHubsは実際にVR関係の国際会議等に活用され始めています。国際会議IEEE VR 2020では専用のHubs会場が作られ(hubs.ieeevr.online)、口頭発表の視聴に並列し、デモやポスター発表、200件近い交流型発表がHubs上で行われ、ラボでもHubsを活用した発表を行いました。環境問題や新型感染症といった問題が大きくなる昨今、こうした学会や交流型イベントのオンライン化の取り組みが今後より積極的に行われることが予想されます。一方で、こうしたソーシャルVRプラットフォームに共通する課題の一つに、音声混合があります。

VRChatやHubsなど、いくつかのソーシャルVRプラットフォーム上において、簡易的な空間音響モデルが適用されているものの、音場再現のような特殊なデバイスや、コストの高い演算処理が必要な手法はリアルタイム処理には向いておらず、まだ普及していません。従来どおり、3次元空間内の音源はすべてヘッドホンなどの音響機器によってステレオ2チャンネルに集約された音として知覚されています。

学会のポスター発表や懇親会など、不特定多数の話者が同一空間において無秩序に会話を行う場合、電話のような2者間通話モデルとは異なり、音声処理におけるPush-to-Talkのような話者以外のミュート処理を施すことも難しいでしょう。またアバターシステムによっては、相手の表情や頷きなど高解像度を必要とする情報を考慮することも難しくなります。また学会のポスター発表や懇親会のような環境は、ビデオ会議システムによる事前に話者が知られている会議や講演者が定められたWebinarと違い、「会場の賑わい」のような未知の参加者同士の会話が聞こえてくる環境の再現も重要となります。

PannerNode APIと減衰モデル

Hubsでは、空間音響にWeb Audio API の PannerNode インターフェイスが JavaScript ベースで実装されています。この API は W3C Audio Working Group が標準化して、Mozilla が PannerNode API として整備しています。

【図1: PannerNodeの指向性に関するパラメータ。ConeInnerAngle = 30, ConeOuterAngle=120 程度に設定した例。】

PannerNodeで設定できるパラメータはいくつかありますが、距離に対する減衰に関わる主要なパラメータは以下の通りです。

  • Distance Model:減衰の関数モデル (Linear/Inverse/Exponentialから選択可能)
  • Rolloff Factor:減衰の傾き
  • Ref Distance:減衰を開始する距離
  • Max Distance:減衰を終了する距離

Hubsでは音声の指向性も設定できます。ただしこれは動画や音声ファイル等の静的メディアにのみ設定可能で、ライブストリーミングで共有されるユーザーの入力音声には適用できません。上図で示すように、指向性に関して設定できるパラメータは「ConeInnerAngle」「ConeOuterAngle」「ConeOuterGain」の3種類となります。ConeInnerAngleの内側では減衰が起きませんが、ConeOuterAngleの外側では、音量はConeOuterGainの値で一定となります。そして、その間のグラデーションになっている部分で、前述のDistance Model(Linear/Inverse/Exponential)で設定した減衰が有効となります。

ちなみに各ブラウザ環境でサポートされている実装はそれぞれ異なります。例えば、立体音響におけるユーザーの方向を表現するOrientationはInternet ExplorerやSafariで実装がないため、指向性を制御するcone値と、距離に対する減衰モデルである distanceModel(以下、減衰モデル)と、それに付随するパラメータが Hubs 内において制御可能で、デフォルトでInverseが採用されています。さらに HubsのシーンエディタSpokeのデフォルトでは ConeInnerAngle=360,  ConeOuterAngle=0 となっています。これは減衰を行わない「どこからでも聞こえる設定」と思われます。指向性を細めたいときは両者の値を逆にするのが使いやすいと思います。

細かな数式や理論については論文を読んで頂くとして、実験方法についてはコロナ禍を配慮して「完全にオンラインで行えること」という挑戦がありました(実験自体はいつでもこちらのURLから体験することができます)。複数の男女が会話する様子が立体音響空間で混ざっており「空間を自由に動けたとしても、この音の聞こえ方だとどちらのグループに所属しているかわからない!」という感覚が味わえます。なお実験に利用した音源はオープンデータ「千葉大学 3人会話コーパス (Chiba3Party)」を使用しています。

この実験から得られた結論としては「(Hubsのデフォルトと反して)Inverseモデルでは、遠距離における定位の難易度が上がる」というシンプルな知見です。

【図2: 結論・オンライン実験で2つのモデルが確認できた】

Inverse モデルを利用した場合、音の減衰の強さが距離によって異なるため、音源との距離感を音声から掴むのが難しくなります。よって音源が多い環境、つまり同時参加者が多いイベント等では会話の定位精度が低くなります。サイン波やホワイトノイズといったシンプルな音源であれば音位置の定位精度は高くなりますが、会話では低い傾向になります。特にホワイトノイズについては、位置関係が変わる際に聞こえる音の周波数が変わり、音量以外の情報を得ることができることもわかりました。実験協力者からのコメントとして「実験で利用した女性の会話は抑揚や声量の変化が大きく定位しづらい」という意見もみられました。会話においては抑揚や話者毎の声量の差などが要因となり、完全なノーマライズが難しいことが背景にあります。これはライブエンタメのための音響技術やボイスチェンジャー「転生こえうらない」などでも共通した見解で、話者毎の音声が単一のチャンネルに含まれている状態でノーマライズ処理を行う方法などがありますが、やはりリアルタイム処理は難しい研究課題になると考えます。

さて実験結果から得られたLinearモデルの活用方法として、各パラメータの調整テクニックは以下の通りになります。

・Ref Distanceパラメータは人口密度の高いイベントほど小さくするのが良い。
・ただし「減衰が早すぎて聞こえにくい」ということがないように注意が必要。
・Max Distanceはその名の通り聞こえてほしい範囲を設定すれば良い。

といった具体的な調整方針になります。また「賑わいを表現したい」といった目的でLinearモデルを利用した場合にも「どれだけ離れても微かに聞こえる」状態を作りたいこともあるでしょう。この場合は、Rolloff Factorの値を0.9~0.95に設定してみましょう。これで、Max Distanceの距離を超えても微かに聞こえ続けるようになります。

このような知見に加えて、静的音源であればConeOuterGainを活用することで、指向性が設定できます。これは動画のサイネージのような利用法に向いています。例えば図1においてConeOuterGainを0.01とすれば「音源の背中側では音量が小さい」というような状態を生み出すことができます。このテクニックはラボの成果発表会のようなシーンにおいて、動画が横に高密度で並んでいる場所でConeOuterAngleを小さめに設定するといった方法で活用しています。


Hubsアバター関連の不具合調査と解決

続いて、Hubsの独自アバター関連における不具合調査レポートになります。日本国内ではVRMのようなアバター専用フォーマットが普及していますが、Hubsでは独自の構造を持ったGLBファイルが採用されています。ラボの開発チームがオープンソースにフィードバックを返しながら改善していく様子が読み取れますので当時の時系列と本家Mozillaのソースツリーへのリンク込みで紹介します。

アバタープレビューに不要なメタリックが適用される問題の解決

Hubsにおいて、ルーム内でのアバターの描画とアバター選択時のプレビュー画面でのアバターの描画は、異なるソース(“avatar-preview.js“など)によって行われており、それぞれ異なった見た目で表示される場合があります。例えば、ルーム内では図3左のように表示されるのに、プレビュー画面では図3右のように表示される場合があります。

【図3:Hubs ルーム上での表示(左) と プレビュー上での表示(右)】

トゥーン表現を活用するようなアバターだとユーザーが想定する描画とかけ離れてしまう場合があります。もちろんアバターを制作しているBlenderでのマテリアル設定においても、左のように表示されるようにUnlit(アンリット=ライティングの影響を受けない)設定を行っていますが「アバター選択時のプレビューでは輝いてしまう」という現象(おそらくバグ)です。

【図4:同ファイルのシーン内での比較。トゥーン表現をしたいのに、プレビュー画面では輝いている】

先に解決方法を述べると、Hubsのデフォルトアバターのマテリアル”Bot_PBS”をそのまま転用することで解決できます(日本語ドキュメント参照)。ただし、”Bot_PBS”を新規に作成してそれを再現した場合は上手くいきませんでした。

ラボの開発チームとしてはGitHubにてMozilla公式リポジトリに報告し(Avatar badly metallic in preview window · Issue #2541 · mozilla/hubs)、8月に「Medium Quality Mode」が実装された際にこの Issue は 無事Closeとなりました。

なお、Hubsにおいて利用が推奨されているシェーダーは、「Principled BSDF」と「Background」の2種類です。「Principled BSDF」ではORMマップのそれぞれのチャンネルにメタリック、ラフネス、オクリュージョン等のマップを設定できますが、「Background」では光の影響を受けないためにメタリックに表示されることはありません。しかしプレビュー画面においてはメタリックやラフネスのマップが存在しない場合に、それらの値が勝手に適用され、ユーザーが想定しない描画が行われてしまう…というロジックが現象の背景であるようです。

結論:Hubsでトゥーン表現のアバターを使いたい場合はORMマップを生成しましょう

さて(欧米ではそうでもないかもしれませんが)日本ではVTuberをはじめ、トゥーン表現のアバターは非常に重要な表現手法です。トゥーン表現のアバターを作成したい場合は、ORM(オクリュージョン・ラフネス・メタリック)のマップを別途用意しないと当バグを回避できないということになります。とはいえプレビュー画面に限った現象なので、Mozilla公式サーバーなどで個人でカスタムアバターを使う場合には問題ないと思いますが、例えばHubs Cloudで独自サービスとしてアニメっぽいデフォルトアバターを公開する場合など、一般のエンドユーザーが利用するアバターの場合には適切とはいえないのでORMマップを生成しましょう。

アバターの目だけ表示される問題の解決

2020年6月にラボ版Hubs Cloudで発生していた現象です。オリジナルで開発したアバター同士が接近したとき、本来なら図5左のようにアバター全体が半透明になって表示されるはずなのに、Hubs Cloudでは図5右のように目の部分のみ表示される状態が発生していました。

【図5:接近の際の挙動(左)公式ロボットアバター)/(右)接近しても透過されず目だけ残っている】

検証を進めた結果、この現象は目に限って発生しているのではないことが分かりました。(都合により画像は出せませんが)胴体と首だけ、つまりHeadボーンの頂点のみ非表示になるケースもありました。この現象はアバター同士の距離によって変わるので、アートチームも一緒になって原因究明に時間がかかってしまいました。最終的にはHubs公式に存在したバグで(Fix non-detached heads for avatars · Issue #2520 · mozilla/hubs)、検証環境のHubs Cloudが最新のソースツリーと異なっているため発生したということがわかりました。

独自ドメインでのHubs Cloud AWS上では、AWS Cloud Formation上で確認できますが、独自のバージョン番号が付番されております。これは必ずしも最新のソースツリーでは有りません。バグが発生した場合は、まずは公式Mozilla Hubs (hubs.mozilla.com) で同じ現象が起きるかどうかを確認し、その後ソースツリーを確認するのがよさそうです。

パーソナルスペースを表現するバブル

なおこのバグに関連しているのは、Hubsにおける「personal space bubbles」というアバター同士が接近した際の処理システムです。この実装は personal-space-bubbles.js 等に記述されています。Hubsの各ユーザーが利用しているアバターには不可視の球体が設定されており、この接触を基準としてアバター同士の表示を切り替えています。この球体を泡に例えて 「personal-space-bubbles」 と名付けているようです。自分のアバターと他人のアバターが重なった際に半透明に表示することで視界の干渉を防いでいます。おそらく以前のHubsはアバター全体を半透明にするのではなく、このバブルに接触したボーンを非表示にする実装になっていて、その名残りだったのではないかと推測しています。

既存アバターをHubsに対応させる

【図6:[ファイル]→[アペンド] /[Object] あるいは [Scene]を選ぶ】

既存アバターをHubsに持ち込む際は(MozillaReality/hubs-avatar-pipelines)の「AvatarBot_base_for_export.blend」をベースに設定するのが良いでしょう。Blenderでこのblendファイルを開いたあと、ファイルメニューの「アペンド」から、既存アバターの「Object」あるいは「Collection」を選択(図6)、デフォルトアバターの不要なメッシュを削除し、ボーンにウェイトを再設定します。なお、Hubsには肘や膝のIKが無いため、腰から下や腕などは予め削除しておきましょう。

これ以降の作業において、注意すべきポイントは以下の通り:

【必須要件】

(1) Blenderのバージョン2.82以降を利用すること。それ以降の最新のリリースが対応しているかどうかは、Hubs公式DiscordにてBlenderのバージョン名で検索することで確認できる場合があります。

(2) 胴体と頭のボーン構成がHubsのデフォルトアバターと同じであること。Hips > Spine > Neck > Head > RightEye/LeftEye は必須で、満たしていない場合は全ボーンをまとめてHead扱いされてしまいます。デフォルトアバターのボーン構成は図7のとおりです。

【図7:デフォルトアバターのボーン構成】

(3) glTFを出力する際に、Hubsのドキュメント(Using the Blender glTF Exporter · Hubs by Mozilla日本語版)の通りにエクスポート設定を行うこと。

(4) Neckのウェイトが顔に乗らないように設定すること。というのも、Hubsの一人称視点において、自分のアバターが自分の視界を遮らないための処理として、Headボーン以下に割当てられたメッシュを非表示にするように実装されているので。そのため、顔のメッシュにNeckやSpineなどのボーンのウェイトが残っていると、視界を遮ってしまうことがあります。

【推奨要件】

(5) Hubsのデフォルトアバターのマテリアル Bot_PBS をそのままアペンドして、テクスチャ設定などを変更しましょう。マテリアルを新規作成したり、Backgroundマテリアルを利用しても動作することはしますが、前述した通りアバタープレビューがメタリックになってしまう問題があります。また、ORMやノーマルのマップは該当アバター用に新規に割り当てること。

(6) デフォルトアバターの手のボーンを再利用すること。HMD利用時に表示される手のアニメーションはデフォルトアバターの手のボーンに登録されています。これは新規に設定するのが結構面倒なので、デフォルトアバターの手のボーンを既存アバターの手のメッシュに新規にウェイト付けしましょう。

独自アバター 実装フローの例

まず、公式デフォルトアバターのBlendファイルを開きます。不要なメッシュを削除して、独自アバターを図6のようにアペンド、独自アバターのボーンをリンク解除して削除し、ウェイトの再設定を行います、必要に応じてアバターの腕や足などを削除します。

【図8: マテリアルの選択】

図のようにデフォルトアバターのマテリアル Bot_PBS を独自アバターに割り当てて、独自アバターのUVマップを Bot_PBS に割り当てます。Bot_PBS のテクスチャで、デフォルトアバターのデータを独自アバターのマップに差し替えていきます。

【図9: UVマップの選択】

【図10: エクスポート時の設定】

最後に、エクスポートになります。チェックボックスは図10のとおりに選択しましょう。これで完成です。

Hubs内で実施しているHubs用アバター開発ワークショップの様子

Hubs内で実施しているHubs用アバター開発ワークショップの様子

ラボのインターン生活をふりかえって

最後に、坂口さんによるラボのインターン生活ふりかえりをまとめていただきました。

坂口:Hubsについてこのインターン経験を通して、Mozilla Hubsの実用に関する知見がとにかく高まりました。シーン設計、アバター制作、イベント実施にまたがって業務を行ってきたので、「Hubs Cloudでのカスタマイズを行わない範囲でのHubs活用」については国内でもトップレベルにノウハウを手にしているかもしれないと思います。一方で、ラボは研究開発、特に「R2D」を方針にしているので、ツールの使い方だけでなく『結局のところHubsは何に有用なのか?ということに答えを出さないと意味がない』という探求を行っていますし、僕自身もそう考えています。

白井:そうですね!(いろいろな業務案件があってなかなかこの記事には書けませんが)会社としてはこの期間に「REALITY Spaces」の発表なども行っているので、単なる研究から開発までのターンアラウンドがとても大事なサイクルとしてありましたし、坂口さんのスピードと貢献は内部のチームにもしっかり伝わっていました。

白井:今回のインターンは、4ヶ月の間、全てをオンライン上で行いました。私自身とも、他のインターンとも、一度も対面をすることはありませんでしたが、そのあたりはいかがでしたか?

坂口:コロナ禍とはいえ「これは学生にしては割と珍しい体験をしているのでは…」と感じています。フルリモートは「出勤が発生しない」などの恩恵はありましたけど、やはりコミュニケーション不足をどう補うか!は課題でしたね。コロナ禍以前において、休憩時間や業務外のカジュアルなやりとりや、対面していることによって生じる小さな所作が、業務に様々な影響を与えていたことを認識しました。

白井:ラボではけっこう雑談会や社会問題を扱った思考実験などもやりましたけど、まあそれでも難しかったですよねえ…!森ビルさんと「KIDS' WORKSHOP」でバーチャル六本木ヒルズも作ったりしたのですが…!

坂口:あれは大変でしたが楽しかったですね!いくつかのイベントの実施に関わらせていただいて、様々な人とのオンラインでの出会いがありましたが、特に印象深いのはKIDS' WORKSHOPの中学生やBlenderワークショップの参加者のみなさんですね。まさにコロナ禍のテレプレゼンスの課題をHubsなり他の手法なりで解決しないといけないということに向き合った4ヶ月間でした。そして同じバーチャルインターン環境に加えて、言語的・時間的な壁もあるであろうスイスのMilaさんがめちゃくちゃ凄いということなども刺激を受けました。

スイスからのMilaさんによる国際イベント「Virtual Beings World」でのHubs活用

白井:これから一旦、卒業論文に取り掛かるということですが、今後はどうされますか?

坂口:理想はバーチャル世界でのイベント開発で喰っていければ…漠然と考えていましたが、このインターンを通してより真剣に考えるようになりましたし、同時に、コロナ禍における自分にあった働き方についてもよく考えないといけない、と考えるようになりました。やはり学びの機会になるイベントというのは続けていきたいと思います。またリアルとバーチャルの両方で拠点・コミュニティをつくっていきたいとも考えるようになりましたね。

白井:それは今後も研究していきたいテーマですね!卒論が楽しみです。

仲間を募集しています!

坂口さんのようなインターンの例は非常に稀有ではありますが、ご自身がVTuberやアバター社会におけるクリエイターであること、また「まだ名前のないお仕事」を探求するため、ラボは技術や発信を両輪で開発しています。

完成した技術をもったエンジニアリングも大切ですが、この分野の開拓を楽しんでいけることも重要な才能ですね。

VTech ChallengeVRSionUp! といった公開イベントも定期的に行っていますので、YouTube LiveTwitter@VRStudioLabなどで交流いただければ幸いです。

なお組織上の所属はバーチャルライブ配信アプリ「REALITY」を作っているWright Flyer Live Entertainment改め2020年10月1日より「REALITY株式会社」になります。採用も積極的に行っていますので、まずはどんな仕事があるのか覗いてみてください!

]]>
20816
JSConf JP 2019に参加してきました https://labs.gree.jp/blog/2019/12/20129/ Wed, 04 Dec 2019 10:09:39 +0000 https://labs.gree.jp/blog/?p=20129 こんにちは、開発本部 フロントエンドデザイングループ UI開発チームの大野です。
2019年11月30日(土) 〜 2019年12月01日(日) の2日間開催されました「JSConf JP 2019」に参加してきましたのでレポートさせていただきます。

JSConf JPとは

https://jsconf.jp/2019/

 

会場について

上野と秋葉原の中間辺りにあるアーツ千代田 3331は、

旧練成中学校を利用して誕生したアートセンター

なのだそうで、各セッション会場も授業を受けているような不思議な感覚でした。
私は初めて伺った会場ですが、アートセンターの名の通り展示会などのイベントも頻繁に行われているそうなので、ご存知の方も多かったようです。

Room A (体育館)

今回のイベントで一番広いルームです。
体育館なので流石に広い!

Room B (B105)

地下にある教室っぽいルームです。
他のルームに比べるとコンパクトでしたが、スライドが近くてとても見やすかったです。

Room C (屋上)

屋上のテントの中にあるルームです。
この季節なので少し寒かったのですが、スタッフの方々がホッカイロを支給してくださったので非常に助かりました。
奥行きがありましたが、後ろの席近くにもサブモニターがあり、広いのに見やすいルームでした。

セッション

国内外から多くのスピーカー、参加者がいらっしゃっていたので、事務的な説明も含め、セッション内容は日本語、英語両方が使われていました。
Room B以外の会場のステージ脇には英語字幕も用意してあり、英語が苦手な私でも理解しやすい配慮がされていて素晴らしかったです。
2日間本当に盛り沢山な内容で、全てはご紹介できませんが個人的に特に気になったセッションを抜粋してご紹介いたします。

The State of JavaScript
by Raphaël Benitte and Sacha Greif


彼らは毎年
State of JavaScriptというJavaScriptに関するアンケートを行っており、そのデータを交えて近年の傾向を紹介していました。
フレームワークの流行りや年収など様々なデータ集計されており、今現在は2016〜2018年までの結果が閲覧出来てとても参考になります。
2019年分のアンケートは今も受付中のようですので、ご興味ある方は
こちらからぜひ回答してみてください。

WebAuthnで実現する安全・快適なログイン
by Eiji Kitamura / えーじ


まずはじめに

WebAuthn(Web Authentication API)とは

Web Authentication API (別名 WebAuthn) は、ウェブサイトで登録、認証、二要素認証を行うためにパスワードや SMS のテキストを使用するのではなく、公開鍵暗号を使用します。これはフィッシングや情報漏洩、 SMS や他の二要素認証に対する攻撃といった厄介なセキュリティ問題を解決し、同時にユーザーの利便性を向上させます (ユーザーが多くのパスワードを管理する必要がなくなるため)。

今ではOTPを使った2段階認証を行うサービスは多くありますが、それでもフィッシングやキーロガーを使ったハッキングを防ぐのは困難です。
特にフィッシングによる被害が最も大きく、その場合OTPすら意味を成さない。
そこでWebAuthnを使って安全性を大きく高める方法を紹介されていました。

フィッシングに対するOTPの防御率は76%。
物理的なセキュリティーキーでは100%防げているとのことでした。

またChrome 79からは生体認証も可能になるので、セキュリティーキーのように使用でき、安全性に加えて利便性も劇的に向上することになります。
(生体情報はデバイスに保存されるため、紛失で失われるリスクはあります)
2019/12/3現在はiOS SafariがWebAuthn未対応となっていますが、次のバージョンアップで対応予定となっているため、実装コストも低くなり活用しやすいAPIになりました。
認証が必要なプロダクトには必ず入れておきたい機能です。

4年分のプロシージャルなJS
by Andy Hall


元Adobe、現フリーのエンジニアさんで、プロシージャルについて何から始めたら良いか、どんな物ができるかをご自身の制作物を通して解説されていました。
Andy Hall氏のGithub上のDemoはどれもとても興味深く参考になるものばかりでしたが、個人的には特にWebAudio + Proc-genで作られた楽曲の自動生成が面白いプロダクトでした。
音楽理論と組み合わせて「I → V → VII → I」というコード進行や「Verse、Bridge、Chorus」なども設定できるようになっていますので、遊んでみるととても楽しいです。

Building and Deploying for the Modern Web with JAMstack
by Guillermo Rauch


Guillermo Rauch氏は、古くはMooTools の Core Developerとして活動されていたそうで、現在までにSocket.ioやNext.jsなど様々なプロジェクトに関わってきた方です。
そんな彼の専門であるリアルタイムWebについて、JAMstackの概念を交えて多角的な解説を聞くことが出来ました。
積極的に利用したいと思っているSSRのデメリットも聞けたので、改めて利用するかを検討するきっかけになりました。

そして後半は、彼が創始者であり高速なデプロイを実現するPaaSであるZeit Nowを実際に触りつつ、その速さとシンプルさを体感しました。
こちらは知らなかったので、これから色々触ってみたいです!

Web の自重
by Jxck


「ブラウザの多様性がなくなった結果Webの今後ってどうなると思うのか、Chromeの一強の状況って本当に健全なのかとかの問題提起について聞きたい」という依頼を受けたことから今回のセッション内容が決まったそうで、なかなかの難題だったそうです(笑)
Webの自重というセッションタイトルは、本当にその通りだなーと思う内容でした。

新しい仕様を入れないとモダンブラウザとして認められない、でも互換性は保たなければならない、それを繰り返しているうちに肥大し、もはやゼロベースでブラウザを作るのは不可能なのでは?というレベルまで来ている。
ただ全てをゼロから作る必要はない。
そして現実的に現在のゼロとはChromiumとなっている。
そんな状況を打破するための考察が聞けたことで、改めてWebという広い視点を持つきっかけになりました。

まとめ

2日に渡って参加させていただきましたが、すぐにでも試してみたいAPIやサービス、考え方など様々な刺激を得ることができましたし、自分に足りないものも改めて実感することができました。

このような豪華なスピーカーの皆さんのお話を聞く機会をくださった運営の皆様、どうもありがとうございました!次回も必ず参加させていただきます!

]]>
20129
フロントエンドカンファレンス福岡 2019に参加してきました https://labs.gree.jp/blog/2019/11/19968/ Thu, 21 Nov 2019 07:32:38 +0000 https://labs.gree.jp/blog/?p=19968 こんにちは、開発本部 フロントエンドデザイングループ UI開発チームの大野です。
2019年11月16日(土)に福岡の九州産業大学にて開催されました「フロントエンドカンファレンス福岡 2019」に初参加してきましたのでレポートさせていただきます。

フロントエンドカンファレンス福岡 2019とは

フロントエンドカンファレンス福岡」は2018年に初開催され、
今年で2回目の開催となります。
今回は「新しい視点を見つけよう」をテーマに、様々な視点の発表がありました。

https://frontend-conf.fukuoka.jp

  • 2019年11月16日(土)12:00 〜 18:00
  • 九州産業大学 12号館
  • タイムテーブル
    12:00 開場・受付
    12:40 オープニング ルームA
    13:00 セッション ルームA・B・C
    18:00 クロージング ルームA
    18:30 終了
  • ハッシュタグ
    カンファレンス = #fec_fukuoka
    ルームA = #fec_fukuoka_a
    ルームB = #fec_fukuoka_b
    ルームC = #fec_fukuoka_c

会場到着

九州産業大学は1号館〜23号館まであるそうで、会場となる12号館が分からず迷っていました。
が、道中でmerpayパーカーを着たかたがいらっしゃったので、ついて行かせていただいて無事に入り口まで辿り着くことができました。

受付を終えてからまだ少し時間があったので、フリードリンクとお菓子をいただきつつスポンサーブースをぶらぶら。
いただいたノベルティが、トートバッグ、カンファレンスのステッカー、メガネクリーナー、珪藻土コースターだったのですが、コードの書かれたメガネクリーナーとコースターが面白かったです。

コードの実行結果はこちら。

See the Pen
FECF2019
by Kai (@kai-ono)
on CodePen.

CSSの変数の有無とサイズが違いますが同じロゴに!
遊び心があって終わった後も楽しいノベルティでした。
そしてブースを周っているとこんなホワイトボードも。

写真が少し見えにくいので補足しますと、こんな感じです。

ES201X 使っている 使っていない express 使っている 使っていない
TypeScript Nuxt.js
Flow Next.js
jQuery Nest.js
React その他
hapi
Closure library
prototype.js
AngularJS
GatsbyJS
Vue
Angular

1セッション終わった頃に撮った写真なのですが、絶大なるTypeScript人気!
個人的には今Nuxt.jsをよく触っているので、「もう使いたくない」の感想のみでちょっと寂しい気持ちに...
とりあえずカンファレンス終了後の結果を楽しみにしつつセッションへ。

参加セッション

今回私が参加したセッションです。
時間が重なっていて惜しくも参加できなかったセッションも多々ありますが、資料が公開されていますので拝見したいと思います。

HTML Optimization for Web Performance

13:00〜14:00 [ ルームA ]
株式会社メルペイ / 泉水 翔吾

近年Vue.js、React、ServiceWorkerの普及でWebサイトの表示速度は劇的に上がりましたが、こちらのセッションではより根本的なブラウザのレンダリングに焦点を当てたパフォーマンス改善の紹介をされていました。
フレームワークの最適化も大切ですが、原点に戻ってUX含めた改善を行うことも大切だと再認識できる内容でした。
本セッションで紹介されていたNative lazy-loadingは、サポートするブラウザが多くなれば積極的に使っていきたいので、今後も注目していきたい属性です。

フロントエンドにおけるアーキテクチャとの向きあい方

14:00〜14:30 [ ルームB ]
日本事務器株式会社 / 甲斐田 亮一

長期プロジェクトで陥りがちなアーキテクチャの破綻とどのように向き合っていくか、というお話でした。
オーバーエンジニアリングとのバランスは難しいところだと思いますが、後手後手の対応にならないようにある程度冗長性、汎用性を持たせることは重要ですよね。
自分も陥りがちな問題なので、バランスの取れた設計を心がけたいと思いました。

明日からはじめるテストのあるフロントエンド開発

14:30〜15:00 [ ルームB ]
サイボウズ株式会社 / 向井 咲人

最初にご紹介した「フロントエンドにおけるアーキテクチャとの向きあい方」のセッションと通ずるものがある内容で、複雑化していくアーキテクチャに対しての解決策の一つとしてテストコードを書くというアプローチをされていました。
Testing Trophyという手法は初めて伺ったので興味深く大変勉強になりました。
フロントエンドにおいては、テストコードを書いてカバレッジを上げていく、という作業はあまり馴染みがありませんでしたが、選択肢の一つとして積極的に検討していきたい手法でした。

チームラボのフロントエンドアーキテクチャ

15:00〜15:30 [ ルームA ]
チームラボ株式会社 / 田村 亮弥

フロントエンド開発の苦労話を実案件を交えて紹介されていました。
Nuxt.js + TypeScriptのお話もされていたので、苦労話はとても共感出来ると共に非常に参考になる内容でした。
駅でよく見かけるイノベーション自販機のUI部分をelectronで開発されていたことも驚きでした。

JavaScriptの読み込みを考える〜場所、async、defer、その仕組みと使い所〜

16:00〜16:30 [ ルームB ]
株式会社オミカレ / 前川 昌幸

「HTML Optimization for Web Performance」のセッションのようにパフォーマンス改善のお話でしたが、こちらはJavaScriptに焦点を当てて深堀りされていました。
実務でのレンダリングブロックは画像によるものが多かったため、JavaScriptのパフォーマンス改善まで意識することは少なかったのですが、async、deferを利用できる場所がないかは再考の必要があると感じさせられたセッションでした。

ウェブフォント今昔物語

16:30〜17:00 [ ルームB ]
株式会社FOLIO / 大木 尊紀

ウェブフォントといえば、多くの場合重い、読み込みが遅いという印象が強めですが、それ以上にユーザーに対して提供できるメリットがたくさんあるので、積極的に使ったほうが良いというお話でした。
最適化するのに少々手間はかかりますが、アクセシビリティの向上を目指すためにはなくてはならないものだと感じました。
ただライセンス周りは非常にややこしいので、配信サービスを使うのが安全とのことです。

これからのフロントエンドセキュリティ

17:00〜18:00 [ ルームB ]
株式会社セキュアスカイ・テクノロジー / 長谷川 陽介

普段バックエンド程はセキュリティに関して意識する機会は多くないと思いますが、
たとえ静的なサイトであってもDom-Based XSSなどの脆弱性は存在するので、それらの攻撃手法と対策が紹介されていました。
ブラウザのバグを利用した攻撃手法などを見ていると、やはり完全に防ぐことは難しいと感じました。だからこそ1つ1つの対策を漏れなく行っていくことが大切ですね。

まとめ

本カンファレンスのテーマである「新しい視点を見つけよう」の通り、パフォーマンスやセキュリティ、アーキテクチャなど様々な切り口から見たフロントエンドのお話が聞けてたくさんの刺激をいただきました。
来年の開催があれば是非また参加したいと思います。

またセッションの最後に行う質疑応答にslidoを使用されていましたが、
セッションが終わると同時に全て消えてしまうことが残念だったので、ログが残るようなものだと見直せて嬉しかったなと感じました。

そして気になるJavaScript総選挙の結果はこのようになっていました!

TypeScriptは揺るぎないですが、Nuxt.jsにも赤丸がたくさん付いていてホッとしてます。
その他の欄にも以下3つが追加されていました。

Gridsome
StrongLoop
Native

やはりNuxt.js、Next.js、GatsbyJsといった静的サイトジェネレータが勢いを増してきている印象を受けます。
ReactベースのNext.js、GatsbyJsはまだ使ったことがないですが、それらも選択肢の一つとして使えるように知見を広げていきたいと思います。

]]>
19968
AWS のお得な機能だけでネイティブゲームサーバをつくる https://labs.gree.jp/blog/2015/10/14404/ Mon, 19 Oct 2015 01:33:07 +0000 http://labs.gree.jp/blog/?p=14404 昨今何かと話題に挙がってきた AWS Lambda と AWS DynamoDB を活用して格安で堅牢、高性能なゲームサーバを作ります。

既存システムの苦労をもとに、サーバの開発や運用を頑張らずにすむための仕組みとネイティブアプリからの AWS Lambda の利用方法を簡単に紹介します。

サーバが良くわからんという、ネイティブゲーム、ネイティブアプリエンジニアにオススメです。

当内容は多分に個人主観を含んでおり、時事的な要素も含まれています。
検索して十分な資料があると考えられるツールやライブラリの利用方法等は省略しています。

おさらい

まずは既存のゲームサーバの構成を初歩からおさらいしてみましょう。

1

簡単にサービスする方法としてアプリからのリクエストを受ける HTTP のサーバと利用者の情報を格納しておくデータベースが考えられます。しかし、すぐに思いつくだけでもいくつかの問題があります。

  • 通信が暗号化されておらず、利用者を危険にさらしてしまう
  • 四六時中ダウンしていないか気になってしまう
  • サーバ更新のときや、データ変更などで頻繁にメンテ入りしてしまう
  • 色々詰め込まれ、暗黙知まみれになってしまう

この程度でもサーバ二台は必要です。復旧もたいへんです。とても実用に耐えられません。クラウドの一番安いインスタンスを選んでも毎月千円前後でしょうか。

2015年10月しらべ、最小インスタンスの月額。


そして効率よくサービスするために最低でも次のような構成が考えられます。

2

  • 正しいドメインを取得し、 SSL 通信を利用して利用者を守る
  • Load Balancer を構築し、サーバの冗長性を保ち、安全にデプロイする
  • 各システムの異常を知らせるための監視システムを設置し、障害時間を短縮する
  • データベースのバックアップやレプリカを運用し、安全にスケールしやすいようにする

スケールアウトしやすくサービスの人気に応じて品質を高めやすく、こういった構成は多いのではないかと思われます。

費用としては、ロードバランサーとデータベースは単一障害点とし、DNS は他社サービスを利用しても6台分と SSL 証明書、ドメインネーム代が発生します。もろもろで毎月一万円ぐらいかかりそうです。

わずかなアプリケーションでもそれなりに費用がかかり、その効率がよくなるには数十台~100台ほどの規模が良さそうです。

理想ではインスタンスもところどころ強化し、インフラ代だけで数百万円規模に膨らみ、相対的に運用システムのコストを抑えることです。
そして、このころにはデプロイの高速化やサービス全体の最適化、賢い構成管理、運用自動化、分割の戦略も課題になっているころです。

3

しかし、ネイティブアプリはもちろん、ウェブアプリですらリッチクライアント化してきており、アプリケーションサーバの役割は減ってきています。従来のようにページ毎に状態をすべて集め、 HTML テンプレートにあてはめて出力することはなく、必要なときに必要な状態だけをクライアントは取得し、保持し、クライアントにて画面を構成させ、ゲームの進行をします。

6

デプロイやモニタリング等のシステムを一式整備して揃え、そのためのアプリケーションサーバを構築したとしても、ネイティブアプリではたかだか数台で賄えてしまう可能性があり、それにしてはシステムを運用するためのオーバーヘッドが大きいのは明らかです。


Lambda と DynamoDB とネイティブアプリについてのもくろみ

おさらいができたところで現実的なところを見てみます。

先ほどまでのサーバ構成についてはいわゆる IaaS レベルを想定しており、運用者はサーバ OS の管理者となり、自由に設定やミドルウェアの導入等ができます。情報も多く、対応できるインフラエンジニアも比較的多くいます。

5

対して、 PaaS や SaaS、BaaS ではより高レベルな開発、運用ができますが、えてして *aaS はサーバ開発関連の話で、サービスも多く、情報が散逸してしまい、ネイティブアプリ開発者からしてみたら興味外ではないかと思われます。ネイティブアプリ向けでは API や SDK 等が揃っていてサーバ側の開発を不要とするものもありますが、一気に自由度が失われてしまい、ちょうど良さそうなものを探し、習得するのに苦労します。

4

しかし、そういった中でも Lambda はネイティブアプリと相性が良いと考え、その理由を以下のように考えています。

  • 関数単位でプログラムを書くことができ、最小のコードはとても小さくで済む
  • Web フレームワークっぽさが全くなく、いわゆる CGI よりも動作を把握しやすい
  • デプロイの工夫などが必要なく、モニタリング等も揃っている。モニタリング対象も少ない
  • 関数の入出力が原則 JSON であるが、 HTML を考えなくて良いのでネイティブアプリであればむしろ好都合
  • Lambda には状態を保持することができず、キャッシュ等の仕組みも無いが、ネイティブアプリ側で対応できる
  • コマンドラインからプログラムを実行するのと大差無く、ベンダーロックインしない。 Lambda を利用しなくても容易に別の環境で動かすことができる
  • 最小設定ではひとつのクエリにて 128M までのメモリしか使えないが、昨今の HTTP を処理するスレッドと比較しても引けを取らない
  • HTTPd では REMOTE_ADDRESS とされるアクセス元のアドレスが取得できるが、スマホアプリはキャリア NAT のアドレスになることが多く、一意に識別することはできない

そして DynamoDB と組み合わせた場合は、

  • おおむね Key Value Store でスキーマがゆるく、いわゆるオブジェクト指向によるデータ構造と相性が良い
  • リクエストやレスポンスが心なしか遅いが、ネイティブアプリでは非同期かつクエリの粒度を変えやすい
  • リレーショナルモデルは扱いにくいが、ネイティブアプリではソーシャル要素が薄い傾向にある
  • DynamoDB Stream と組み合わせて他の Lambda をバッチ処理的に扱える
  • nodejs や python 環境があれば DynamoDB Local を利用してすべてオフライン環境でも開発できる

と、それぞれの利点を活かし、欠点をネイティブアプリの特徴で回避できるため相性が良さそうです。もちろん、暗号化通信に対応済みの SDK もあり、アプリから安全に直接 Lambda 関数を呼び出せます。

7

全体的にシンプルになりつつも、高い安定性と拡張性を獲得できそうです。AWS でも EC2 関連は比較的高額ですが、驚くことに他のサービスは非常に安く設定されています。


利用計画

それぞれの動作確認を行いつつセットアップしていきます。

  • SDK のビルドと組み込み
  • ネイティブアプリから Lambda の呼び出し確認
  • Lambda から DynamoDB の読み書き確認
  • 既存サーバからの移行

SDK のビルドと組み込み

SDK は C++11 で書かれており、 CMake によってビルドします。すでにアプリを CMake でビルドしていた場合はとても簡単に組み込めますが、そうでない場合は単独でライブラリをビルドし、バイナリをリンクする手間がかかります。

SDK には AWS の他のサービスの API も存在するため、考えなしにビルドするとえらい時間がかかります。
https://github.com/awslabs/aws-sdk-cpp の各ディレクトリ

ビルドは CMake なので容易ですしカスタマイズもできるので Android も Linux としてビルドするのがオススメです(理由は後述)。
toolchain ファイルは aws-sdk-cpp 内にもありますが、ネイティブアプリ本体と合わせるため別途、
https://github.com/taka-no-me/android-cmake.git こちらを利用します。

Windows 以外の OS では cURL と OpenSSL, zlib に外部依存してます。
cURL と OpenSSL のビルドは大変面倒ですが、 https://github.com/gcesarmza/curl-android-ios を利用するのが比較的簡単です。
if(PLATFORM_ANDROID) の場合、外部依存リポジトリを git clone してきてビルドしてしまうので注意します。
aws-sdk-cpp とアプリ本体で別々バージョンの cURL と OpenSSL をリンクするとえらい容量になるのでアプリ本体の cURL と OpenSSL は共有させます。

集めるもの

  • git clone https://github.com/awslabs/aws-sdk-cpp.git
  • git clone https://github.com/gcesarmza/curl-android-ios.git
  • wget http://zlib.net/zlib-1.2.8.tar.gz
  • git clone https://github.com/taka-no-me/android-cmake.git # Android ビルド用

CMakeLists.txt を編集し、必要な部分だけ取り込む

ネイティブアプリの場合わずかなパッケージ容量を抑えるために aws-sdk-cpp に付属しているライブラリを使うのもおすすめです。
https://github.com/awslabs/aws-sdk-cpp/tree/master/aws-cpp-sdk-core/include/aws/core/utils

すでに CMake を使っている場合、CMakeLists.txt に add_subdirectory(aws-sdk-cpp) や include_directories() を加え、アプリと一緒にビルドします。
ライブラリを単独でビルドする場合、 README.md の通りオプションを設定してビルドします。

  • -G オプションによるビルドツールの選択( XCode,VisualStudio,Makefile)
  • Android の場合、 -DCMAKE_TOOLCHAIN_FILE と -DANDROID_ABI(armv7,8,x86) -DANDROID_TOOLCHAIN_NAME(gcc4.8,4.9,clang) などの指定
  • -DCMAKE_BUILD_TYPE=Release
  • -DTARGET_ARCH=Linux
  • 必要に応じて、 STATIC_LINKING=1 など

アプリのほうもビルドの確認を行います。だいたい次のヘッダにパスが通ってれば十分使えます。

#include <aws/lambda/LambdaClient.h>
#include <aws/core/client/ClientConfiguration.h>
#include <aws/core/auth/AWSCredentialsProvider.h>
#include <aws/core/Region.h>
#include <aws/lambda/model/InvokeRequest.h>
#include <aws/lambda/model/InvocationType.h>
#include <aws/lambda/model/InvokeResult.h>
#include <aws/lambda/LambdaErrors.h>
#include <aws/lambda/LambdaErrorMarshaller.h>
#include <aws/core/utils/Outcome.h>
#include <aws/core/utils/logging/AWSLogging.h>
#include <aws/core/utils/logging/ConsoleLogSystem.h>
#include <aws/core/utils/base64/Base64.h>
#include <aws/core/utils/Array.h>

ビルドが上手く行かない場合、 make VERBOSE=1 を設定するとビルドコマンドがそのまま表示されます。 CMakeLists.txt 内に message 関数を利用して、いわゆる print デバッグで不適切なパラメータを調べるのもお手軽です。

CMakeLists.txt を編集を維持しつつ SDK を更新するときは次のように rebase していきます。

git fetch
git commit -am "my edit"
git rebase origin/master

Lambda の準備とネイティブアプリから呼び出し確認

Lambda function を起動するための、アプリケーション向けのユーザを作り、
呼び出されるための Lambda functoin を準備します。

8

  • AWS マネジメントコンソールにログインします
  • ヘッダのタブを Edit し、 IAM と Lambda と DynamoDB を加えておきます
  • IAM を開き、 Users → Create New Users を選択します
  • Credential 情報をメモしておきます
  • Policy → Create Policy を選択します
  • Policy Generator を選び、
  • AWS Service に AWS Lambda
  • Actions より Invoke Function
  • AWS は * の一文字をいれ Add Statement を押します
  • Next Step → Policy Name を分かりやすく invoke-Lambda などとして、 Create Policy します。
  • 先ほど作った User を選択し、 Attach Policy ボタンを押します
  • そして作成したポリシー名、 invoke-Lambda などを選択して設定します。

ここまでの手順で、このユーザの Security Credentials を知っている場合、インターネット中のどこからでも Lambda を呼び出すことが出来ます。そしてアプリケーションにこの鍵を埋め込んでしまうため、流出しても良いように余計なポリシーを付与しないようにしましょう。

  • Create a Lambda function を押し、 simple-mobile-backend を選びます
  • Ranking などと function 名を設定し、 Role から * Basic with DynamoDB を選びます
  • 一時的に IAM Role を作る画面になるのでその場で作ってしまいます
  • 他は特に設定することなく、 function 作成まで行います。
  • function が出来たら念のため、 switch 文内の dynamo.* 操作をコメントアウトしておきます。
  • Save and Test を行い、 Template は Mobile Backend で試します。
  • 実行結果は Cloudwatch に保存されるため、ページ下部のリンクから Cloudwatch を確認しにいきます。

ここまでで AWS 側の設定は完了です。 AWS 関連の資料は幸いにも大量にあるため、検索すれば画像つきでの解説もでてきます。

つぎにアプリから呼び出してみます。

// Trace レベルでログはたくさん出力
const std::shared_ptr<Aws::Utils::Logging::LogSystemInterface> logr(new Aws::Utils::Logging::ConsoleLogSystem(Aws::Utils::Logging::LogLevel::Trace));
Aws::Utils::Logging::InitializeAWSLogging(logr);

// 作成した IAM User の鍵を記述
Aws::Auth::AWSCredentials cred("****************CYDQ", "****************************43WX");
Aws::Client::ClientConfiguration config;
config.region = Aws::Region::AP_NORTHEAST_1;
//config.verifySSL = false; // OS にそなわる証明書が読み込まれない場合はひとまず無視

Aws::Lambda::LambdaClient lambda(cred,config);
Aws::Lambda::Model::InvokeRequest invoke;
invoke.SetFunctionName("Ranking");
invoke.SetInvocationType(Aws::Lambda::Model::InvocationType::Event);
invoke.SetBody("{\"operation\": \"echo\", \"message\": \"Hello Lambda!\"}");
lambda.Invoke(invoke);

僅かこれだけです、他の AWS SDK と比較しても記述量や機能に大差ありません。

まずは PC でビルドして PC からアクセスできるか確認した方が良いでしょう。
呼び出されたかどうかの確認は Cloudwatch の時刻で分かります。また呼び出し回数は Metrics に含まれているのでそれを参照するのも良いと思います。

失敗しやすいポイントはライブラリの組み込み周りで、

  • リポジトリは aws-sdk-cpp だがライブラリ名は aws-cpp-sdk や aws-cpp-sdk-lambda となる
  • cURL のクライアント証明書パスが合わなく、 aws-sdk には設定箇所が無い。 curl ビルド時にも設定できる
  • スレッドセーフではない(と思う)

Lambda から DynamoDB の読み書き確認

10月現在、 DynamoDB は新しい GUI と従来の GUI の両方が扱えますが従来の GUI を想定しています。

  • マネジメントコンソールより DynamoDB を選ぶ
  • Table Name は Ranking
  • Hash Attribute Name には player
  • Range Attribute Name には score
  • あとは初期設定のまま Continue していき、 Use Basic Alarms のチェックははずし、 Create

JavaScript のコードから

var params = {};
params.TableName = "Ranking";
params.Item = {
  player: {S:event.player},
  score: {N:''+event.score}
};
dynamo.putItem(params, function(err,data){});

として、 event にプレイヤー名とスコア数値を渡せば Dynamo にレコードが増えるのを確認します。

より詳しい使い方は SDK に書いてありますが、 SQL よりは仕様が薄く、シンプルに扱えます。

既存サーバからの移行や互換性

たとえばフルスタック系 HTTPd のサービスでは URL のパースや application/x-www-form-urlencoded 等でパラメータを渡しているかと思われます。すでに application/json だった場合はしめたものです。 Body の JSON がそのまま Lambda の handler の第一引数に渡ります。

exports.handler = function (event, context) {
  console.log('Received event:', JSON.stringify(event, null, 2));

あとは通常の Web サーバと同じくパラメータの型や値の検証を経て、処理していきます。先に作った DynamoDB のテーブルをリアルタイムランキング風に扱うとすると、整合性は諦めて、次のようにデータを更新することができます

dynamo.query({ // 自身のハイスコアが無いかチェック
    TableName: "Ranking",
    KeyConditions: {
      player: {
        ComparisonOperator: 'EQ',
        AttributeValueList: [{ S: event.player }]
      },
      score: {
        ComparisonOperator: 'GE',
        AttributeValueList: [{ N: '' + event.score }]
      }
    },
    Limit: 1,
  }, function (err, data) {
    if (!err && data.Count == 0) {

      dynamo.putItem({ // ハイスコアを追加
        TableName: "Ranking", 
        Item: {
          player: { S: event.player },
          score: { N: '' + event.score }
        }
      }, function (err, data) { });

      dynamo.query({ // 古いスコアを検索
        TableName: "Ranking",
        KeyConditions: {
          player: {
            ComparisonOperator: 'EQ',
            AttributeValueList: [{ S: event.player }]
          },
          score: {
            ComparisonOperator: 'LT',
            AttributeValueList: [{ N: '' + event.score }]
          }
        },
      }, function (err, data) {
        if (!err) {
          for (var n = 0; n < data.Items.length; n++) {
            dynamo.deleteItem({ // 古いスコアを削除
              TableName: "Ranking",
              Key: {
                player: data.Items[n].player,
                score: data.Items[n].score
              },
            }, function (err, data) { });
          }
        }
      });
    }
  });

d1

アプリの動作と並列してデータの保存を行うだけであればとても簡単です。ランキングのスコア登録や、オンラインバックアップにはこの手法でだいたい対応できます。

当サンプルの score に range key を設定しなければ dynamo.updateItem による部分アップデートの対象にもできます。また、 DynamoDB を便利に扱う npm も結構あるので、慣れてきたら利用していくのが良さそうです。まずは、面倒ですが DynamoDB の API をそのまま使うほうが理解度を高めやすい印象でした。

アイテムの一部だけ更新

d2

アイテムの一部だけ更新するときは getItem → 修正 → updateItem の流れになりますが、並列に更新が行われると両方の書き込みのうち、片方の更新が失われてしまいます。 getItem のパラメータに ConsistentRead: true を設定し、 updateItem のパラメータの AttributeUpdates に更新後のアイテムを、 Expected に取得したアイテムそのままの状態を渡します。こうすることで更新しようとしたフィールドに変更があれば更新に失敗し、それを検出することができます。

いわゆる楽観ロックはお手軽にできます。 Lambda はその名の通り、他の言語のラムダ関数と同じく与えられた DynamoDB の状態以外に副作用を及ぼす時はこの手は使えません。

二重処理防止

d3

updateItem するときに条件を付ける方法は先と同じで、 Boolean のフィールドを利用します。このフィールドは初期値が false とし、もとが false であることを期待しつつ true の値を書き込みます。こうすることで処理が並列に動いてしまっても、先に書き込めた方が成功になり、片方はエラーになるので、二重処理を防げます。

このように悲観ロックもできます。もちろん、ロック解放漏れに気を付ける必要があります。

トレードなど

d4

悲観ロックを複数のレコードに対して行う事で、片方の値を減らし、減らした分だけもう片方の値を増やすなど、他のプレイヤーとのトレードもできます。ただし DynamoDB はシャーディングされているため、一意に処理順を決定させることがおそらくできなく、二層ロックのようなもので回避するのは難しいと考えられます。また、途中で処理が止まってしまう事もあり、処理時間と比例して利用料が課金されることもあり、タイムアウト処理は厳しめに設定したほうがよいでしょう。

より活用していくには

  • SDK はスレッドセーフでなさそうなので SDK 専用スレッドとメインスレッドをキューで繋ぎ、非同期化
  • cURL のコンテキストに注意する
  • CloudFormation で IAM の細かい管理
  • AWS CLI でのデプロイ
  • ボトルネックや特殊な処理を VPC 内の EC2 へ移行、 Web 系インスタンスとの共存
  • 静的な状態を S3 に設置
  • 実行ログやメトリクス選定と CloudWatch の利用
  • nodejs , python と DynamoDB Local でオフライン開発環境の整備

モヤモヤ感

  • aws-sdk-cpp のバイナリがおもいのほか大きい。 .so にて 10M
  • 複数の Lambda Function を atomic に更新するには工夫が要る
  • 更新直後は動作が遅い。通常数 ms で実行が終わるところが、数百 ms にもなることもある
  • SQL が人間に優しい言語とすると、 DynamoDB の書き方は機械に近く、そのパラメータは抽象構文木にも見えてくる
  • DynamoDB でもリアルタイムランキングといった用途にはあまり向いていない

まとめ

とても安く始めることができ、実装量も少なく、安定性が非常に高く、拡張の余裕も十分にあり、運用の手間がかかりません。ネイティブアプリにオススメ。

9

]]>
14404
rejs - Vanilla JS Module Builderの紹介 https://labs.gree.jp/blog/2014/12/12311/ Fri, 05 Dec 2014 15:00:31 +0000 http://labs.gree.jp/blog/?p=12311 このエントリは GREE Advent Calendar 2014 6日目の記事です。

皆さんこんにちは!Reflowしてますか?
13卒でArt部の和智(@watilde)です。業務では、GREE Platformで使われている内製 JS/CSS FrameWorkのコミッターとかしてます。

まえがき

弊社は、スマートフォンの黎明期よりブラウザ向けのアプリを開発してきました。環境の変化への追従や、度重なる機能追加でJavaScriptのコードの規模は肥大化していきました。

役割ごとにモジュール化をしてファイル分割を行わないと可読性が落ち、じわじわと保守コストが上がっていきます。しかし、古くからある秘伝のソースはAMDやCommonJSで書かれていないVanillaなJSです。r.js、Browserify、Webpack などのツールでモジュール化するにも、書き換えが大量に発生して導入するのはとっても②大変です。

そんな背景から、Vanilla JSのモジュール化を行う第一歩として、rejsと呼ばれるVanilla JSのModule Builderがグリーでは利用されています。今回は、そんなrejsの紹介を行います!

rejs - Vanilla JS Module Builderの紹介

rejsは、グリーに所属するJason Parrottによって開発されているOSSなVanilla JS Module Builderです。グリーでは、一部のゲームや内製のライブラリ開発で使用されています。

URL: https://github.com/Moncader/rejs

rejsの説明をVanilla JS Module Builderとしてみましたが、分かりやすく言うと「ファイルのリストをその内容に基づいてソートしてくれるツール」となります。

ファイルのリストをその内容に基づいてソート

ファイルのリストをその内容に基づいてソート

明確にexport/importの機能があるわけではありませんが、exportは「グローバルにプロパティ生やす」で、importは「ファイルに存在しないものを参照する」なのでModule Builderとして成立していると考えています。

仕組み

簡単に処理の流れについて

  1. ファイルの名前とソースを受け取る
  2. acornを使い、ファイルごとにソースのASTを取得
  3. ASTのNodeごとにファイルのGlobal Scopeにある変数の詳細とその状態を保持
  4. ファイルのGlobal Scopeごとに、内部で定義している変数と使用している変数を解析
  5. 以上を元に、依存関係を解決するように各Nodeを順序付けしてソート

実際に使ってみる

rejsを使うには、3つの方法があります。

  • rejsのcli
  • gruntプラグイン
  • gulpプラグイン

ぞれぞれ、実際に使ってみましょう。

構成例

いきなり使う前に、まずはファイル構成についての説明です。

下記のようなディレクトリ構成を例に話を進めていきます。

  • src
    • sampleOne.js
    • sampleTwo.js
    • sampleThree.js
  • dist
    • XmasPresent.js
  • test
    • fixture.js
    • test.js

srcディレクトリ内にソースファイルがあり、

distディレクトリに依存関係を解決し、結合済みのXmasPresent.jsを書き出すという想定です。

次に、それぞれのソースファイルについての説明します。

sampleOne.js

グローバル空間に宣言済みであろうxmasオブジェクトの

present.box.colorというプロパティにredという文字列を入れています。

(function(global) {

  global.xmas.present.box.color = 'Red';

}(this));

sampleTwo.js

グローバル空間に宣言済みであろうxmasオブジェクトに

present.boxというプロパティを追加しています。

(function() {

  xmas.present = {
    box: {}
  };

}());

sampleThree.js

グローバル空間にxmasというオブジェクトを宣言しています。

var xmas = {};

さて、今回のケースでは

sampleThree.js, sampleTwo.js, sampleOne.jsの順に結合すれば正しく動作します。

期待する振る舞いとして、結合したものと比較して何も表示されなければOKとします。

$ cat src/sampleThree.js src/sampleTwo.js src/sampleOne.js > dist/fixture.js
$ diff dist/*.js

使ってみる

環境を構築したら、次は実際にコンパイルをしてみます。

cli

npm経由でrejsコマンドをインストールできます。

$ npm install -g rejs

使い方は、-hオプションで見ることができます。

$ rejs -h
Usage:
    rejs [options] [file ...]

Examples:
    rejs foo.js bar.js baz.js
    rejs --out out.js foo.js bar.js baz.js

Options:
  -h, --help Print this message
  -o, --out Output to single file
  -v, --version Print rejs version

基本的な使い方は、rejs [options] [files ...] となっており、-o オプションで書き出すファイル名を指定することができます。

今回は、distディレクトリにXmasPresent.jsとして書き出せばいいので、下記のコマンドを実行すれば完了です。

$ rejs --out dist/XmasPresent.js src/*.js

Gruntプラグイン

次に、rejsの作者が開発しているGruntプラグインを用いたコード例です。

URL: https://github.com/Moncader/grunt-rejs

Gruntfile.js:

module.exports = function(grunt) {
  grunt.initConfig({
    rejs: {
      options: {},
      target: {
        files: {
          'dist/XmasPresent.js': 'src/*.js',
        }
      }
    }
  });
  grunt.loadNpmTasks('grunt-rejs');
};

registerTaskを省略していますが、

grunt.configのプロパティを使いタスクを走らせてビルドしてみましょう。

$ grunt rejs

Running "rejs:target" (rejs) task
File "dist/XmasPresent.js" created.

Done, without errors.

これで、dist/XmasPresent.jsがビルドされます。

gulpプラグイン

最後に、@kuuが開発しているgulpプラグインを用いたコード例を紹介します。

URL: https://github.com/kuu/gulp-rejs

READMEを参考に、gulpfileを書きます。

gulpfile.js:

var gulp = require('gulp');
var rejs = require('gulp-rejs');

gulp.task('rejs', function () {
  return gulp.src('src/*.js')
    .pipe(rejs('XmasPresent.js'))
    .pipe(gulp.dest('dist'));
});

コマンドを実行してビルドしてみましょう。

$ gulp rejs

[11:29:55] Using gulpfile ~/Development/rejs-example/gulpfile.js
[11:29:55] Starting 'rejs'...
[11:29:55] Finished 'rejs' after 22 ms

やっぱりgulpfile見やすくて良いですね。

Code example on Github

これにてrejsの紹介はおしまいです!

下記URLにて本エントリに出てくるソースコード一式を公開してあるので、

ご自由にご利用くださいm(_ _)m

 

URL: https://github.com/watilde/rejs-example

 

あとがき

今後もグリーでは、社内で開発したライブラリを公開したりOSSコミュニティに貢献していきます。
クリスマスを待つ間、引き続きGREE Advent Calendar 2014をお楽しみ頂ければ幸いです!
明日はマーケティング部の戸井田明俊さんと情報システム部の亀井利光さんです。


 

参考資料

記事の補足として、記事内で出てきたツールやライブラリのURLと用語の説明を載せておきます。併せてご覧頂ければと思います。

URL(s)

用語

  • Vanilla JS Module: グローバル・ネームスペースを介してシンボルをexport/importする従来のJavaScriptコード
]]>
12311
Server Sent Events(SSE)の使いどころと使い方 https://labs.gree.jp/blog/2014/08/11070/ Mon, 11 Aug 2014 02:28:43 +0000 http://labs.gree.jp/blog/?p=11070 Flameの箱を捨ててしまったためどうやって送り返すか困っています。@kyo_agoです。

今日は2014年6月にβ公開したGREEチャットで通信に使用しているSSEを紹介したいと思います。

SSEとは

SSEとはServer-Sent Eventsの略でW3Cで提案されているhtml5関連APIの一種です。

これはサーバとの通信やJavaScript APIを中心としたもので、サーバからPush通信を行うための仕様です。

サーバからPush通信に関してはこれまでもCometやWebSocketが存在しましたが、SSEは互換性や効率などの点でそれ以外の技術に対する特徴があります。

ここからは具体的な仕様や、実際に使用した場合の感想などを紹介したいと思います。

通信方式

SSEはHTTP/1.1を使用し、Content-Type: text/event-streamで通信を行います。

基本的な通信内容は以下のとおりです。

HTTP/1.1 200 OK
Content-Type: text/event-stream

data: Hello Server Sent Event!

特徴はContent-Lengthが指定されていないことと、サーバから通信が切断されないことです。

この特徴によりクライアントはサーバから断続的に送信されてくるデータを随時受け取ることができます。

JavaScript API

SSEはJavaScriptから通信内容を取得するAPIとしてEventSource objectを提供しています。

基本的な実装は以下のとおりです。

var es = new EventSource('/sse/api');
es.addEventListener('message', function (event) {
    console.log(event.data); // 上記の通信内容の場合、「Hello Server Sent Event!」が出力される
});

new EventSourceの第一引数はSSE形式でデータを返すURL文字列です。

new EventSourceで生成したインスタンスはaddEventListenerでイベントを設定でき、'open', 'message', 'error'やその他サーバが指定したイベントを捕捉できます。

addEventListenerで捕捉したイベントはcallback関数の引数のdataプロパティで受信したデータを取得できます。

Cometとの違い

SSEはCometと同じような問題を解決するために策定され、技術的にも近いものになっています。

しかし、SSEは後発なこととW3C上で議論されていることなどから、Cometに存在した問題点がいくつか改善されています。

具体的には以下の様な違いがあります。

  • Cometはサーバから最初にデータを受信できた時点で切断し、次のデータは再接続して取得するため、SSEとくらべて再接続のコストがかかる。
  • SSEは専用のJavaScript APIが存在し、切断時の再接続、取得できたデータの履歴管理、カスタムイベントの提供が標準で行われる。
  • SSEは通信方式が仕様化されているため、サーバ側の各言語やフレームワークでライブラリ等が提供されていることが多い。
  • Cometは通常のHTTP通信に近い動作を行うため、ブラウザの互換性が高い(SSEはAndroid標準ブラウザ等で動かすために特殊な対応が必要な場合がある)

WebSocketとの違い

WebSocketはSSEとくらべて広い利用方法を想定して定義された仕様ですが、利用方法として「サーバからのPush通信を行う」ことも含まれているためSSEとかぶるところがあります。

本来、SSEが既存の技術の組み合わせで作られていることに対して、WebSocketは既存の技術のしがらみ無しに作られていることから単純な比較が難しい部分もありますが、合わせて語られることが多いためここで比較したいと思います。

WebSocketとSSEは以下の様な違いがあります。

  • WebSocketはHTTPではなく専用のプロトコルを使うため、パフォーマンスが高い
  • SSEはHTTPを使うため、通信の互換性が高い。
  • SSEはHTTPのため、既存のセキュリティモデルを流用できる(既存のセキュリティモデルに引っ張られる)
  • SSEはHTTPなので、同じOriginの別URLでコンテンツを提供できる(htmlやjs, css、画像、他のSSE API等を同じドメインとポートで提供できる)

ブラウザ互換性

SSEは仕様上JavaScript APIが定義されているため、ブラウザがサポートしていない場合JavaScriptからAPIを呼び出すことができません。

ただ、これに関してはXHRを使ったPolyfillが公開されているため、IE等のSSE未サポートブラウザでも大半の動作は同じように動かすことが可能です。

(ちなみに、このPolyfillはSSEをネイティブでサポートしているブラウザのバグを潰す目的でも開発されているため仕様との互換性はネイティブのEventSourceより高い場合もあります。ただし、内部でXHRを使っているためAndroid標準ブラウザや古いIEなどのXHR自体に問題のあるブラウザでは動作がおかしい場合もあるため注意してください)

各通信方法との比較

それぞれを簡単に比較すると以下のとおりです。

SSE WebSocket Comet
通信コスト
JavaScriptAPI
サーバサイドサポート
通信互換性
仕様安定性
ブラウザ互換性

通信コストはWebSocketが効率的です。SSEは接続を維持するためCometよりは効率的に通信が可能です。

JavaScriptAPIはSSE、WebSocketは仕様化されているものが存在しますが、Cometの場合独自に定義する必要があります。

サーバサイドサポートに関してはSSE、WebSocketは仕様化されているため各種言語でのライブラリが提供されていますが、Cometは仕様化されていないこともあり存在しない場合もあります。

通信互換性に関して、WebSocketは独自プロトコルのため接続できない場合もあります。SSE、CometはどちらもHTTPを使用しますが、Cometの方が通常のHTTP接続の形式に近く接続性は高いと思っています。

仕様安定性に関して、SSEはプロトコル自体が簡単な事もあり仕様バージョンで混乱することはないと思います。WebSocketは過去に若干通信バージョンが混乱しましたが、現在では比較的安定しています。Cometはそもそも決められた仕様が存在しないため細かい部分で調整が必要になる可能性があります。

ブラウザ互換性に関して、Cometだと古いIEやAndroidでも比較的容易に接続できますが、SSEだとPolyfillを使っても素直に接続できない場合があります。WebSocketはFlashなどを使ってPolyfillを作成することも可能ですが、Androidの標準ブラウザはFlashが動作せず、WebSocket自体もサポートされていないため対応が困難です。

SSEの問題点

ここからはSSEを実際に使ってわかった問題点を紹介したいと思います。

  1. ブラウザの実装にバグが多い(Polyfillがブラウザ実装無視してオブジェクト乗っ取るレベル)
  2. デバッグ辛い(Polyfillのコードがかなり複雑なのと、そもそもStream通信のデバッグは辛い)
  3. Polyfillのパフォーマンスは高くない(初期化コストはネイティブオブジェクトとくらべてiPhoneだと体感できるレベル)
  4. Android 2系の対応は辛い(特にCORSと絡めるとほんとうに辛い)

(1)に関しては基本的にPolyfillを使うことで回避できますが、最新のブラウザだと改善されている項目も多いため、UA判断でネイティブオブジェクトを使用しても問題ないと思います。

ちなみに、ブラウザ上のバグはPolyfillでテストケース化されています

(2)はおそらく仕様上解析が大変な部分があることと、パフォーマンス的な意味でPolyfillのコードが複雑なため、通信中のデバッグはかなり困難でした。

ただ、そもそもネイティブオブジェクトでデバッグを行う場合内部状態が外から参照できないため、コードが複雑であってもPolyfillを使用して開発したほうが楽な部分はあります(SSEのイベント発火前にbreak pointを入れたりできるので)

(3)は、実際SSEを使用する場合Polyfillは必須に近い存在ですが、やはりネイティブオブジェクトと比較するとパフォーマンス的に若干ペナルティがあります。

そのため、できるだけ速度を稼ぎたい場合はUA等で切り出してネイティブオブジェクトを使ったり、そもそもSSEではなく素のXHRで対応するほうが良いと思います。

(4)のAndroid 2系に関しては、今回htmlとSSE APIのドメインが違ったことで特に対応が困難でした。

Android 2系は元々一回のデータ受信が4KBを超えないとXHRからデータを取得できないというバグがあり、PolyfillもXHRを使用しているためこの影響を受けます。

更にAndroid 2系はXHR Level2をサポートしておらずCORSが使えないため、対応するためにiframe経由のpostMessageでデータのやり取りを行いました。

これ自体SSEの問題というわけではありませんが、実際対応をする場合には注意してください。

本当のSSE

ここからは一般的に言われるSSEの評価と使ってみての実感を紹介します。

SSEは一般的にEventSource, frankly, sucksと言われますが、Polyfillを使用すればEventSource自体はそこまで大きな問題と感じませんでした。

ただ、Polyfillは実装が複雑でデバッグが難しく、Android等の問題があるブラウザ上での開発は難しいと感じました。

速度的にはPolyfillを使用しても十分な速度ではありますが、高速なデータのやり取りをする場合ネイティブオブジェクトを直接使用するほうが高速に動作します。

また、Polyfillはブラウザがネイティブ実装を持っている場合でもPolyfill objectでの動作を優先しますが、最近のブラウザではネイティブオブジェクトの動作も改善しているため場合によってはPolyfillなしでも実装は可能だと感じました。
(それでもPolyfillの方が仕様への準拠度が高いことと、各ブラウザの動作を統一したかったためPolyfillを使用しました)

SSE自体に関してはプロトコル、APIともに比較的単純なため、簡単なサンプル実装や、本格的に実装する上で困難になる部分はありませんでした。

SSEの今後

ここまでSSEの過去、現在を紹介してきましたが、最後にSSEの今後に関して紹介したいと思います。

SSEは仕様的に現在「勧告候補(Candidate Recommendation)」ですが、各言語のバインディングやブラウザ上の実装も進んでいることから、今後仕様レベルで大きな変更が行われる可能性は低いのではないかと思っています。

では、今後SSEが大きく使われていくかというと個人的には疑問があります。理由として、SSEはあくまでもCometの発展形であり、既存のHTTP上でこれまであったXMLHTTPRequestでPolyfillが書ける範囲の機能しか提供しないためです。

もちろんサーバから簡単にPush通信を送る場合には便利な方法ではありますが、もしWebSocketが使えるのであればWebSocketを使う方が良いと思います。

このため、ネットワーク上の互換性が進み、WebSocketがHTTP並に接続できるようになればあえてSSEを使う理由は小さくなっていくと思います。

]]>
11070
ナウい「LWF」 https://labs.gree.jp/blog/2013/12/9755/ Sat, 14 Dec 2013 15:01:54 +0000 http://labs.gree.jp/blog/?p=9755 LWF Logo

こんにちは、WG基盤開発部ゲーム基盤チームの何です。内製プロダクトチームを陰から支える仕事をしています、チーム名を噛みやすいのが玉にキズ。最近はLWFをメインにフロントエンド周りのお・も・て・な・しをしています。滝川クリステルさん大好きです。

『GREE Advent Calendar 2013』15日目では ナウい「LWF」 と題しまして、LWFが最近どのように使われているのかを社内の活用例と共に紹介したいと思います。今更聞けないLWFの豆知識を始め、現場の開発で役立てているアシストツール、LWFの最新の事情についても触れていこうと思います。死語だらけの記事ですが、これを読んであなたもナウいLWFの世界へLet's Dive!

LWFの簡単なおさらい

「LWF」- Lightweight SWFとはFlashコンテンツから変換したデータをマルチプラットフォームで再生可能にするアニメーションエンジンです。昨年オープンソースプロジェクトとしてリリースされ、現在グリー内の様々なプロダクトで活用されております。

技術の概要などは過去にもEngineers' Blogの記事で紹介され、昨年末にはLWFにフォーカスしたTechTalkも開催されました。最近ではクリエイターの間でも徐々に浸透し始め、Creators' Blogの記事を始め様々な開発者の方のブログなどでも取り扱ってくれています。今も継続的に更新されており、パフォーマンス、対応環境共に日々進化しています。

また、ナウい話題としましては「LWF for C++」が先週から公開され、同時にCocos2D-xやiOS UIKitで使うためのRendererも一緒にありますので、さらに幅広い環境で利用できるようになりました。

Why LWF?

グリー社内では、「探検ドリランド」を始め様々なプロダクトでLWFを導入していますが、LWFを使用するメリットとしましては

  • 多くのクリエイターにとって親しみやすいAdobe Flash CS6 及び CCを使い制作することができる
  • 同一ソースでHTML5, Unityを始め複数環境の多様な描画方式に対応している
  • スマートフォンゲームコンテンツの開発にフォーカスしている

が挙げられます。

実際クリエイターがオーサリングしたアニメーションをコンバートし、エンジニア側でその出力データを表示させるだけなので、餅は餅屋で制作工数を大幅削減することができます。幅広く使われているFlashで制作できるのでクリエイターの学習コストも抑えられ、HTML5で使う場合なら実行環境に合わせてレンダリング方式をも選択できるので、パフォーマンスチューニングも行いやすいです。

気になるパフォーマンス関連につきましてはこちらの記事で競合技術とのパフォーマンス比較などについて取り上げています。その後も更なる高速化が実現されて、現状ここまでパフォーマンスが上がっています。

WebGL版パフォーマンスサンプルCanvas版パフォーマンスサンプル

弾幕すらヌルヌル動きます。
弾幕サンプル

また、端末依存のバグの対処ノウハウが多く、多くの端末や実行環境をサポートしているのも魅力的です。ブラウザ向け以外にもUnity版などでネイティブ環境への導入もできるため、実質的に一回作ったFlashコンテンツをブラウザ/非ブラウザの両方に出力できるのも便利です。

上記を踏まえつつ、新しい機能やイベントを実装する際に短い時間で導入できることから、昨年から複数プロダクトにて活用されています。

lwf-dri1-4lwf-dri2-3

LWFの開発効率を上げるナウい方法

しかしいきなりLWFを使うといっても、まだまだ新しい技術なのでやや心もとない感じを受けるかと思われます。そこで以下ではグリー社内で使われている、LWFを導入する際に使っているアシストツールやそれらの活用法についてご紹介します。未経験の方が躓きやすいポイントや落とし穴の多くをスマートに解決できます。

LWFS

まずそもそもどうやってLWFフォーマットへ変換したらいいのか、変換したファイルのプレビューはどうすればいいのかが大きな問題になってくるのですが、これらの問題を華麗に解決してくれるのがLWFSです。LWFSはLWFを変換、プレビューできるビューワーで、必要な素材をディレクトリに配置するだけで、LWFフォーマットへの変換に加え、各種レンダリングモードでのプレビューをブラウザ上で行えます。社内ではLWFの変換作業はほぼすべてLWFSで行っており、テストツールとしても活用しています。

LWFSについては過去の記事でも取り上げられており、Windows/Mac/Linux、各環境で動作しておりますので以下からぜひダウンロードしてお試しください。

LWFSダウンロード


LWF Loader

LWF Loaderは共通的なアニメーション設定など重複しがちなパラメータなどをすべて引き取り、必要の部分だけユーザに指定させ、LWFを最小限の努力で楽に使うために作られたWrapperです。既存コードを大幅に短縮できる上、必要な機能をひと通り内包していますので、社内でも広く使われております。

window.addEventListener("DOMContentLoaded", function() {
  window.requestAnimationFrame = (function() {
    return  window.requestAnimationFrame       ||
      window.webkitRequestAnimationFrame ||
      window.mozRequestAnimationFrame    ||
      window.oRequestAnimationFrame      ||
      window.msRequestAnimationFrame     ||
      function(callback, element) {
        window.setTimeout(callback, 1000 / 60);
      };
  })();

  (function(){
    LWF.useCanvasRenderer();

    var stage = document.getElementById("lwf-test");
    var cache = LWF.ResourceCache.get();

    var current_time, from_time;
    function calc_tick() {
      current_time =  Date.now() / 1000.0;
      tick = current_time - from_time;
      from_time = current_time;
      return tick;
    }

    cache.loadLWF({
      "lwf": "test.lwf",
      "prefix": "",
      "stage": stage,
      "use3D": false,
      "useBackgroundColor": true,
      "onload": function(lwf){
        lwf.setFrameRate(24);
        var main = function(){
          lwf.exec(calc_tick());
          lwf.render();
          window.requestAnimationFrame(main);
        }

        window.requestAnimationFrame(main);
      }
    });
  })();
});

通常の場合、LWFを再生するためには上記のようにパラメータを細かく設定する必要があるのですが、LWF Loaderを使えば以下のようにシンプルに実装可能です。

var setting = {
  lwf: "test.lwf",
  prefix: "",
  privateData: {}
};

var lwfLoader = new window.LwfLoader();
window.addEventListener('DOMContentLoaded', function() {
  var element = document.getElementById('lwf-test');
  lwfLoader.playLWF(element, setting);
});

先日オープンソースプロジェクトとしてリリースされ、サンプルやAPIドキュメントも公開しました。上記のLWFSと組み合わせて使うのがオススメです。

LWF Loader 紹介
LWF Loader ドキュメント


Gruntを活用する

LWF Loaderの使用に必要なunderscore.jsを始め、各チーム内で様々なフロントエンド開発用のjsファイルやその他のライブラリを読み込んでいる場合が多く、ファイル管理やデプロイする際のjsファイルのminificationなどの手順を自動化するためにGruntを導入しています。

一例ですが、このような感じで使っています

Gruntプラグイン 内容
grunt-contrib-jshint jsコードの品質チェック
grunt-contrib-concat 複数jsファイルのconcatenation
grunt-closure-compiler jsファイルのminification
grunt-lodash underscore.jsと互換性があるライブラリ

LWF関連の更新があった際に、簡単にテスト/リリースできるように関連タスクを自動化させています。

その他のグリーにおいてのGrunt活用事例につきましては先日のAdvent Calendar記事をご覧ください。


Jenkinsを活用する

クリエイターがリソースをエンジニアに引き渡したあとに

  • コミットされた素材はいつのタイミングで変換すればいいのか
  • 変換したあとの管理はどうするのか
  • そもそも正しい素材が送られてきているか

など様々な小粒タスクに直面することが多いのですが、ケアレスミスが起こりやすい部分なのでJenkinsで自動化してミスを防ぎます。一例ですが、JenkinsジョブにリモートのLWFSを連動させ、素材がコミットされる度にLWFSで変換し、専用ディレクトリへ出力するというプロセスを取っています。コミットされた素材の中身を素早く確認するため、更新がある度にスクリーンショットを自動で撮って、即時に表示して比較する手法を採用しているチームもあります。

ナウい「LWF for C++」

せっかくなので先日公開されたナウい「LWF for C++」を簡単に紹介します。
HTML向けのJavaScript版、Unity向けのC#版に続き、その他native環境向けのC++での実装になります。標準描画環境としてCocos2D-xのRendererが提供されている以外に、iOS UIKitで動せるRendererも公開されています。使い方はどちらも大変シンプル:

Cocos2D-x Renderer

LWFデータをnodeとして扱うだけであとはすべてCocos2D-xの文法に従います。

LWFNode *lwfNode = LWFNode::create("sample.lwf");
    SpriteBatchNode *batch = SpriteBatchNode::createWithTexture(lwfNode->getTexture());
    batch->addChild(lwfNode);
    this->addChild(batch);

iOS UIKit Renderer

StoryboardにLWF Viewを置いてパラメータを設定するだけで再生ができます。UIKit Rendererは現在CocoaPodsで提供されていますので、既存のXcode projectへ追加する際も下記のPodfileを準備してpod installするだけで大変お手軽です。

pod 'LWF/UIKit'

lwf-uikit-sample

どちらのRendererでも再生に使う素材は従来のLWFファイルを使い回せますので、すでに「LWF for HTML5」を触ったことがある方は手軽に試せると思います。また、ゲームエンジンとして使いやすいように、「LWF for C++」ではLuaをサポートしています。HTML5版と同じく外部スクリプトから制御できるようになっておりますので、ぜひぜひご活用ください。

参照ページ
LWF for C++
LWF for C++: Cocos2D-x Renderer Sample
LWF for C++: UIKit Renderer Sample
CocoaDocs: iOS UIKit Objective-C LWF API Reference

まとめ

当初はドキュメントやサンプルも乏しく、とっつきにくいイメージを抱かれていたLWFですが、ドキュメントの整備も進み、LWF LoaderやLWFSといったツールも充実してきています。LWFを採用したプロダクトも次々とリリースされていて、ノウハウも徐々に溜まりつつありますので、少しでも導入コストが高いイメージを払拭できていればと思います。

LWF for Flash Wiki
LWF Demo

数多ある競合技術とは細かい差はあるものも、LWFでは「オーサリングのしやすさ」と「パフォーマンス」を両方考慮し、もっともバランスのいい実装を提供していると自負しております。2Dゲームの制作に関わってる方、スマートフォンブラウザで少しでも滑らかにアニメーションを提供したい方はぜひ一度お試しください。

明日は鈴木晃一さんの番です! Check it out!

]]>
9755
クライアントサイドJavaScriptのライセンス管理 https://labs.gree.jp/blog/2013/12/9652/ Wed, 11 Dec 2013 02:35:01 +0000 http://labs.gree.jp/blog/?p=9652 最近シリコンウエハーもらって嬉しかったago(@kyo_ago)です。

このエントリはGREE Advent Calendar 2013 11日目の記事です。

今回はクライアントサイドJavaScriptにおけるライセンス管理の問題を取り上げたいと思います。

ライセンス管理の問題点

「使用しているライブラリのライセンス管理をどうするか」はクライアントサイドJavaScriptにかぎらず発生する問題ですが、クライアントサイドJavaScriptには以下の様な特徴があるため問題が複雑になります。

  • コードが結合、圧縮される場合がある
    クライアントサイドJavaScriptでは読み込みの速度を上げるため、使用しているライブラリの結合、圧縮を行うことがあります。しかし、この時誤ってライセンス文が捨てられてしまうことがあります。
  • ソースが外部に公開される
    クライアントサイドJavaScriptではソースコードは基本的に公開された状態で配布されるため、比較的自由度の高いライセンスであっても権利上問題になることがあります。

ライセンス管理の問題点に対する対応

これらの問題に対しては一般的に以下の様な対応が取られます。

  1. 圧縮前にライセンス文を退避し、圧縮後に退避しておいたライセンス文を追加する
  2. 使用しているライブラリのライセンス文だけを別ファイルに書き出し、htmlやJavaScriptのソース内にコメントでライセンスが書かれたファイルへのURLを記載する
  3. 未圧縮版のソースへのURLをコメントで埋め込む
  4. SourceMapで未圧縮版を配布する

ただ、1, 2の方法は手動での対応が必要になるため自動化が難しいという問題があり、3, 4の方法は本来不要な未圧縮コードを公開する必要があるという問題があります。

圧縮ライブラリ側の対応

この点に関しては圧縮ライブラリ側での対応も行われています。

ここからは各圧縮ライブラリがライセンス文を残すために行っている対応を紹介します。

  • Closure Compiler
    ブロックコメントの先頭を/**で開始して、コメント内に@licenseを記述する

/**
  @license some license information here
   */

/*! some copyright information here */

  • mishoo/UglifyJS2
    • commentsオプションを渡して、独立したブロックコメント内に@licenseを記述する(--commentsで残したいコメントの正規表現を指定することも可能)

/* @license some license information here */

この中でClosure Compilerは「一度圧縮するとライセンス表記が変わる」という問題があり、そのため2回圧縮するとライセンス表記が消えてしまう点に注意してください。

上記のライセンス表記をClosure Compilerで圧縮すると以下の形式で出力されます(このまま再度圧縮するとライセンス表記が消えてしまいます)

/*
 some license information here
<h3 id="hs_b7a5e508822d051164ca69465860e8f6_header_0">/</h3>

各ライブラリの記述方式

次は比較的メジャーなライブラリを元に、ライセンス文の記述方式を紹介します。

jQuery

まずは未圧縮版jQueryのライセンス表記です。

jQueryのライセンス表記は割と一般的なライセンス表記です。

ただ、この形式でもYUI Compressorでは正しくライセンス表記が残りますが、Closure Compiler、UglifyJS2で圧縮した場合にはライセンス表記が消えてしまいます(UglifyJS2は正規表現で対応可能)

/*!
 * jQuery JavaScript Library v2.0.3
 * http://jquery.com/
 *
 * Includes Sizzle.js
 * http://sizzlejs.com/
 *
 * Copyright 2005, 2013 jQuery Foundation, Inc. and other contributors
 * Released under the MIT license
 * http://jquery.org/license
 *
 * Date: 2013-07-03T13:30Z
 */

ちなみに、圧縮版は以下の形式になっています。

/*! jQuery v2.0.3 | (c) 2005, 2013 jQuery Foundation, Inc. | jquery.org/license
//@ sourceMappingURL=jquery-2.0.3.min.map
<h3 id="hs_b7a5e508822d051164ca69465860e8f6_header_1">/

AngularJS

次は未圧縮版AngularJSのライセンス表記です。

こちらはClosure Compiler、UglifyJS2では正しくライセンス表記が残りますが、YUI Compressorで圧縮した場合にはライセンス表記が消えてしまいます。

/**
 * @license AngularJS v1.2.4
 * (c) 2010-2014 Google, Inc. http://angularjs.org
 * License: MIT
 */

ちなみに、圧縮版のライセンス表記は以下のようになっているため、どの圧縮ライブラリで圧縮してもライセンス表記は消えてしまいます。

/*
 AngularJS v1.2.4
 (c) 2010-2014 Google, Inc. http://angularjs.org
 License: MIT
<h3 id="hs_b7a5e508822d051164ca69465860e8f6_header_2">/

Underscore.js

Underscore.jsのライセンス表記はラインコメントの組み合わせで出来ています。

この形式はどの圧縮ライブラリでもサポートされておらず、基本的に手動で対応を行う必要があります。

//     Underscore.js 1.5.2
//     http://underscorejs.org
//     (c) 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
//     Underscore may be freely distributed under the MIT license.

これに関しては、本家Issueにも「/*!で始まる形式に変更してほしい」という要望が挙げられていますが、作者から却下されています。

Use bang comment to preserve license · Issue #1280 · jashkenas/underscore

Underscore.jsは圧縮版、未圧縮版でライセンス表記に違いはありませんでした。

Backbone.js

Backbone.jsのライセンス表記もUnderscore.jsと同じようにラインコメントの組み合わせで出来ています。

//     Backbone.js 1.1.0

//     (c) 2010-2011 Jeremy Ashkenas, DocumentCloud Inc.
//     (c) 2011-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
//     Backbone may be freely distributed under the MIT license.
//     For all details and documentation:
//     http://backbonejs.org

しかも最初のコメントの後に改行が入っているため、さらに圧縮ライブラリでの処理が難しくなっています。

Backbone.jsの圧縮版にはライセンス表記はなくなっていますが、SourceMapへの参照が入っています(ファイル末尾)

//# sourceMappingURL=backbone-min.map

Zepto.js

Zepto.jsの場合、ライセンス表記は以下のように「ライセンス表記へのURLを記述する」形式になっています。

/* Zepto v1.1 - zepto event ajax form ie - zeptojs.com/license */

Underscore.jsは圧縮版、未圧縮版でライセンス表記に違いはありませんでした。

Esprima

Esprimaの場合、ライセンス文は記述されていますが、「License」と言った文字は記載されていないため、圧縮ライブラリでの処理は困難になります。

/*
  Copyright (C) 2013 Ariya Hidayat <ariya.hidayat@gmail.com>
  Copyright (C) 2013 Thaddee Tyl <thaddee.tyl@gmail.com>
  Copyright (C) 2013 Mathias Bynens <mathias@qiwi.be>
  Copyright (C) 2012 Ariya Hidayat <ariya.hidayat@gmail.com>
  Copyright (C) 2012 Mathias Bynens <mathias@qiwi.be>
  Copyright (C) 2012 Joost-Wim Boekesteijn <joost-wim@boekesteijn.nl>
  Copyright (C) 2012 Kris Kowal <kris.kowal@cixar.com>
  Copyright (C) 2012 Yusuke Suzuki <utatane.tea@gmail.com>
  Copyright (C) 2012 Arpad Borsos <arpad.borsos@googlemail.com>
  Copyright (C) 2011 Ariya Hidayat <ariya.hidayat@gmail.com>

  Redistribution and use in source and binary forms, with or without
  modification, are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.

  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
  DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
  THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
<h3 id="hs_b7a5e508822d051164ca69465860e8f6_header_3">/</h3>

解決案

ここまで紹介してきたようにクライアントサイドJavaScriptではライセンス管理が非常に煩雑になる可能性があります。

この問題に関して、本来であればライブラリの作者、圧縮ライブラリ双方の対応で解決することがベストだとは思いますが、とりあえず何とかしたいのでいい感じに取得できるgrunt taskを作成しました。

grunt-license-saverの紹介

このgrunt taskはここまで紹介したすべての形式のライセンス文を解析でき、text, Markdown, JavaScript, JSONのいずれかの形式で書き出すことができます。

kyo-ago/grunt-license-saver

使い方

以下のコマンドでinstallします。

$ npm install grunt-license-saver --save-dev

Gruntfile.jsに以下の内容を記述します。

grunt.initConfig({
  'save_license' : {
    'libs' : {
      'src' : ['js/lib.js'], // ディレクトリ以下すべてのJSファイルが対象の場合、['js/**/*.js']とする
      // 'format' : '', // formatは'text', 'JavaScript', 'Markdown', 'JSON'のいずれかの指定が可能。指定がない場合、destに指定されたファイル名の拡張子から推測
      'dest' : 'license.text' // ['license.text', 'license.js']形式で複数指定も可能
    }
  }
});
grunt.loadNpmTasks('grunt-license-saver');

以下のコマンドを実行します。

grunt save_license

これで以下の様なJSを元にして

/*!
 * jQuery JavaScript Library v2.0.3
 * http://jquery.com/
 *
 * Includes Sizzle.js
 * http://sizzlejs.com/
 *
 * Copyright 2005, 2013 jQuery Foundation, Inc. and other contributors
 * Released under the MIT license
 * http://jquery.org/license
 *
 * Date: 2013-07-03T13:30Z
 */
alert(1);
//     Backbone.js 1.1.0

//     (c) 2010-2011 Jeremy Ashkenas, DocumentCloud Inc.
//     (c) 2011-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
//     Backbone may be freely distributed under the MIT license.
//     For all details and documentation:
//     http://backbonejs.org
alert(2);

以下の様な内容のlicense.textが生成されます。

!
 * jQuery JavaScript Library v2.0.3
 * http://jquery.com/
 *
 * Includes Sizzle.js
 * http://sizzlejs.com/
 *
 * Copyright 2005, 2013 jQuery Foundation, Inc. and other contributors
 * Released under the MIT license
 * http://jquery.org/license
 *
 * Date: 2013-07-03T13:30Z
<br />

     Backbone.js 1.1.0
     (c) 2010-2011 Jeremy Ashkenas, DocumentCloud Inc.
     (c) 2011-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
     Backbone may be freely distributed under the MIT license.
     For all details and documentation:
     http://backbonejs.org

基本的にはMarkdownやtext形式で書き出して、圧縮後のJS内にコメントとして埋め込むことを推奨します。

ただ、その後複数回圧縮される可能性がある場合、JavaScript形式で書き出してコード内に埋めることを推奨します。(改行等がエスケープ文字列に置き換えられるためライセンス表記の見た目は変わってしまいますが、JavaScriptの変数へ保存する形式で書き出すため再圧縮されてもライセンス文が残ります)

明日は先日社内で開催されたJavaScript hackathonでHaskellの講師をしていただいた@beketaさんです。

]]>
9652
Grunt Xmas!!! https://labs.gree.jp/blog/2013/12/8892/ Mon, 09 Dec 2013 03:00:30 +0000 http://labs.gree.jp/blog/?p=8892 いまやさんからバトンタッチ! みなさんこんにちは! すっかりフロントエンド開発では定着したGruntの話はもう飽きてると思いますが、今日はフロントエンドではなく、バックエンドエンジニア向けにどう活用できるかをご紹介したいと思います。

※このエントリは GREE Advent Calendar 2013 9日目の記事です。

自己紹介

石川将行と申します。IDは ishikawam か M_Ishikawa です。ソーシャルゲームの開発エンジニアです。

ぼくのチームではGruntはフロントエンド(HTML, JS, CSS等マークアップ)制作のエンジニアだけでなくバックエンド(PHP等サーバサイド)開発のエンジニアも使ってます。
双方ともに同じリポジトリで同じGruntの設定(=Gruntfile.js)を共有しています。

backend_frontend

Gruntはプロジェクトをビルドするためのスクリプト(タスク)を自由に組み合わせることができるので、担当者の役割(この場合はフロントエンドとバックエンド)によってうまく使い分けることができます。

Gruntのおさらい

grunt-title

今年はPHPCONとかYAPC AsiaとかでGruntの話をさせていただきましたのでそこから抜粋しておさらいします。

Gruntとは、

  • Node.js製のフロントエンド開発支援ツール
  • フロントエンド関連ファイルのビルドを行う
    • オリジナルと公開用とにディレクトリを分けて管理し、公開用には画像を圧縮したりJSを難読化したりCSSプリプロセッサ変換(Sass等)をしたりテストしたり etc... ができます。
  • プラグインによりツールを拡張できる
    • プラグインの開発が簡単(JavaScript)

といった特徴があります。

もっとわかりやすく、例えば何ができるかというと

  • CoffeeScriptを変換する
  • 画像を圧縮したり管理したりする
  • JSを難読化する
  • ファイル名をハッシュで隠蔽する
  • Sassを変換する(Compass)
  • シンタックスチェック(LINTチェック)する
  • CSSを圧縮する
  • ユニットテストを走らせる
  • ファイルやコードの置換or変換スクリプトを実行する
  • etc...

などなど、開発していてコードを書く以外のことをまとめてやってくれます。

Gruntはnpmで管理されており(つまりNode.jsモジュール)、実質各機能(タスク)を担当するGruntプラグインもまたnpmで管理されています。
npmはパッケージ管理だけでなく依存管理、つまりNode.js版のComposer(PHP), Bundler(Ruby), Carton(Perl) のような管理機能をも持ち合わせていると考えていただければよろしいかと思います。

Gruntのライバル?

GruntはNode.jsの流れに乗ってすごい速さで成長しており、v0.4が今年の2月にリリースされた当時はnpmに登録されたプラグインの数は1,000に満たなかったのですが、12/5時点で1,900にも登っています。
そういう意味では枯れてきており、似たようなもので比較的新しいBrunchというツールと比較されることもありますが、これはフロントエンドエンジニアがモックを最速で作るまではいいですが、継続的にプロジェクトで使っていくとなるとプラグインの拡張性の問題(数が比じゃない)、独自プラグインを気軽に扱えること、等を考えればGruntに取って代わるものとはいえません。人気も相まってGruntプラグインはフロントエンド制作の域を大きく超えてきています。

YeomanはGruntとJSライブラリ管理ツールのBowerとを組み合わせたものです。モックを爆速で作りたい場合には非常に有効かつ継続的にも利用できます。(我がチームはYeomanではないですがBowerも導入しています。)

どうやって使ってるか

ぼくのチームではこんな感じで使ってます。

Gruntプラグイン 内容
(grunt) Grunt本体です。npmで管理されているのでGruntもひとつのNode.jsのモジュールとして扱われます。
grunt-contrib-compass SassをCSSのへコンバートするCompassを実行
grunt-contrib-imagemin pngmin, jpegtran等画像圧縮ツールを実行
grunt-contrib-concat ファイルを結合
grunt-contrib-uglify JSを圧縮・難読化
grunt-contrib-cssmin CSSを圧縮
grunt-contrib-copy ファイルのコピー
grunt-contrib-clean ファイルの削除
grunt-contrib-jshint JSをシンタックスチェックするjshintを実行
grunt-contrib-watch ファイルの変更監視とタスクの実行、およびLiveReloadの実行
grunt-md5filename ファイル名のハッシュ難読化
grunt-phpunit PHPのユニットテストを行うphpunitを実行

 

それぞれの導入経緯はたとえばこんな感じでした。(一部フィクションです。伝えやすいように大げさにしてます。)

CSSの更新がマークアップエンジニアとゲーム開発エンジニアで衝突!

  • 【導入前】UI制作者がCSSを納品、それを開発エンジニアがリポジトリに追加。しかし緊急時には開発エンジニアがUIを制作、手っ取り早くデザインするためにインラインスタイルを書いてしまった
  • 【導入後】リポジトリ内でSass管理しGruntでCSSや圧縮CSSを生成。クラスを編集しても影響範囲等が明確で開発エンジニアが容易に(正しく)CSSを書けるようになった
  • grunt-contrib-compass, grunt-contrib-cssmin, grunt-contrib-csslint で実現

ゲームにJS多用でコードが複雑になり保守が大変!

  • 【導入前】それまでフィーチャーフォン版ゲームを作っていたがスマートフォンでJSを多用することに。JSerが作ったものの、開発エンジニアが保守しずらい
  • 【導入後】Coffeeの変換、JSファイルの結合、シンタックスチェック、ユニットテスト、難読化まで一度にできるになり、保守が楽になった
  • grunt-contrib-coffee, grunt-contrib-concat, grunt-contrib-uglify, grunt-contrib-jshint で実現

制作画像の圧縮を行う担当が画像種別(カード、アイテム、レイアウト、等)によりバラバラで管理も分散

  • 【導入前】いまある画像が圧縮済みかオリジナルか、等を把握するのが困難
  • 【導入後】画像の圧縮最適化タスクをGruntで自動化、管理しているので人為ミスがなくなった
  • grunt-imagemin で実現

ファイル名を隠蔽(ハッシュ化)したい

  • 【導入前】手動でスクリプトを回していた。うっかり忘れて事故につながることも
  • 【導入後】自動でやってくれるのでなにも考えなくて良くなった
  • grunt-md5filename で実現

PHPのユニットテストをしたい

  • 【導入前】手動でphpunitを叩いていた
  • 【導入後】ファイルの変更を監視して自動でphpunitが実行、エラーがあればGrowlで通知
  • grunt-contrib-watch, grunt-phpunit, grunt-notify で実現

毎回ビルドの度にコマンドを叩いてからじゃないとブラウザチェックができない

  • 【導入前】Sass-CSS変換やJSの難読化のコマンドを叩いてからブラウザで確認するまでに処理が終わるのを待たないといけない
  • 【導入後】ファイルの変更を監視してくれてGrowlでの通知もありLiveReloadもしてくれるので手間が省けた
  • grunt-contrib-watch, grunt-notify で実現

この他にもいろんな開発における痒いところにGruntプラグインは届いてくれるのです。

 

PHPでGruntを使う

npmで公開されているプラグインからPHP関連のものは2013/12/4時点で下記のとおりです。
過去30日間のダウンロード数の多い順に並べました。

Gruntプラグイン 内容 DL数
grunt-phpunit phpunit を実行 1077
grunt-phplint php -l を実行 735
grunt-php PHP の Built-in web server を実行 597
grunt-phpcs PHP_CodeSniffer を実行 445
grunt-php-set-constant 動的にPHPファイルdefineの内容を書き換えます 297
grunt-php-cs-fixer PHP Coding Standard Fixer を実行 274
grunt-php2html htmlをphpより生成 273
grunt-php-to-json PHPのArrayをJSONに変換 231
grunt-phpspec phpspec を実行 157
grunt-phpdocumentor phpdoc を実行 119
grunt-phpcpd PHP Copy/Paste Detector を実行 67
grunt-laravel-validator Generate PHP validations that use Laravel from JS descriptions of the input data format 54
grunt-phpmd PHP Mess Detector を実行 53
grunt-typo3-phpunit TYPO3 UnitTests を実行 47
grunt-php-analyzer Grunt interface to php-analyzer 39
chop-grunt-php-builder Grunt plugin to statically build PHP files 32
grunt-haml-php Process HAML templates using MtHaml, a PHP port of Haml. 32
grunt-jade-php grunt-jade-php 12
grunt-phptpl Processes PHP files (e.g. PHP templates) to static files. 8

(2013/12/4時点)

以上がPHPのキーワードでGruntpluginを検索したものです。
もちろんこれ以外にもWebゲーム開発において使えるプラグインは沢山眠っています。探すのもまた楽しいですよ! -> Grunt Plugins

他にも使えるGruntの便利どころ

Gruntを使い込んでいくと、あれもできるかも?これもできるかも?と、使いどころが見えてきます。
ぼくが使ったちょっと変わったTipsを一つご紹介します。

DocumentRootを分けて利用した話

 

Gruntでは気軽にファイル群を生成できるので、WebサーバでDocumentRootをスイッチして同じアセットファイルを参照してるいますが、

  • Webアプリケーションフレームワークを介すもの
  • 静的コンテンツを表示するもの

を分けて実際のゲームもモックHTMLも同時に確認できたり、

  • 本番デプロイする圧縮、難読、最適化されたアセットファイルを参照するもの
  • 開発デバッグしやすいようにオリジナルの見やすいアセットファイルを参照するもの

を分けて開発確認とリリースQA確認とを同時に吐き出せるようにして効率化を図っています。

カスタムタスクの定義のコツ

watch(grunt-contrib-watch) はコーディング中に変更のあったファイルを監視して自動でタスクを実行してくれるのでとても便利です。が、万能というわけではなく使いどころを気をつけないと無駄にタスクが走ってしまうことになります。
例えばcompass、cssmin、uglifyはwatchするけどjshintやphpunitは手動にしておく、など使い分けます。これによってタスク完了の待ち時間の調整ができます。
実はGruntのWatchはまだまだなところがあって、プラグインが対応していないと個別ファイルごとのWatchとタスク実行ができない場合が多くあります。

あとは、カスタムタスクの定義は役割(バックエンドエンジニア、HTMLマークアップエンジニア、アニメーションクリエイター、等)によって使いやすく分けると捗ります。

最後に

僕がGruntを入れるときはまだGREEではそんなに使われていませんでした。使っていたとしてもクリエイティブエンジニアが個人で使っていたりして限られていました。
この1年の間に急速に進化し、普及したと思います。今では日本でもブログで書かれたり講演のテーマになったり雑誌で紹介されたりしてとてずいぶんみかけるようになりました。

Gruntをチームに導入するにあたり、実はそれまではフロントエンドのエンジニアとバックエンドのエンジニアはリポジトリも別でスケジュールも別、フロントエンドで制作されたUIモックがバックエンドのエンジニアに納品され、それをリポジトリに組み込んでいく、という一方通行の作業でした。なのでもしUIの修正が必要になったらUI製作者へのフィードバックが大変でコンフリクトのコストもかかっていました。

しかし、Grunt導入とともにリポジトリに参加していただき、また、上記でDocumentRootを分けたことでUIモック開発とゲームアプリ開発を同期して開発を進めることができるようになり無駄を省き大きくコスト削減につながりました。それまでSassを扱えなかったプログラマがすぐに扱えるようにもなりました。

 

さて、今回はGruntをPHPでということでしたが、他の言語でも使えそうなプラグインも多数公開されています。もしくはこの機会にプラグインを作ってみてもいいかもしれません。
ソースを読んでいただくとわかりますが公式プラグインでさえ数行でできていて非常に簡単、シンプルに作ることができます。

そしてオープンソースとして公開(つまりnpmへ登録)も簡単にできますので、自慢のプラグインができたら是非公開しましょう。

 

Gruntについてもっと知りたい方は

等に実際の導入手引もありますのでご覧ください。

 

おまけ

せっかくクリスマスなので npm xmas を実行するGruntプラグインを作りました。ソースを見ていただければいかに簡単に作れちゃうかわかると思います。
エンジニアの皆様は是非これでクリスマスを乗り切って下さい!

grunt-xmas

animation

$ npm install grunt grunt-xmas --save-dev
$ grunt xmas

 

あしたは矢口裕也さんです!よろしくお願いしまーす!

 

]]>
8892
Canvas から生成した PNG 画像に独自の情報を埋め込む https://labs.gree.jp/blog/2013/12/8594/ Sat, 07 Dec 2013 15:00:11 +0000 http://labs.gree.jp/blog/?p=8594 こんにちは、Multimedia Engineering Team のいまやです。
Advent Calendar 8日目の記事ですが、特にそういうのとは関係なく好きなことを書きます。

はじめに

Canvas で色々やっていると、 Canvas#toDataURL() を使って生成した PNG 画像に独自の情報を埋め込みたくなることがよくあると思います。
今回は、この生成した画像に独自の情報を埋め込む方法を説明したいと思います。

PNG フォーマットおさらい

PNG フォーマットについては(以前個人ブログですが)紹介したので、詳しく知りたい方はそちらをご覧ください。

ここでは簡単なおさらいのみにします。
PNG は以下のような構造になっています。

  • PNG シグネチャ(8バイト)
  • チャンク群(以下のデータが連続して配置)
    • データの長さ(4バイト)
    • チャンクタイプ(4バイト)
    • データ(可変)
    • CRC32(4バイト)

簡単ですね。
あとで出てくるので少し記憶にとどめておいて欲しいのですが、チャンクのサイズはデータ+12バイトとなります。

今回は、IDATチャンクの前にプライベートチャンク(仕様に存在しないけど勝手につくっていいチャンク)をつくって挿入したいと思います。

準備

PNG のチャンクをつくるには、いくつかの決まり事を知る必要があったり、必要な計算などがあります。
具体的には以下のものです。

  • チャンク名の命名規則
  • プライベートチャンクの挿入位置
  • CRC32 の計算

これらについて順に説明していきます。

チャンク名の命名規則

プライベートチャンクは勝手に作っていいチャンクですが、チャンク名にはルールがあります。
まず、前述のおさらいでも書いてありますが、4バイトなので(基本的には人間の読める)4文字となります。
そして、それぞれの文字の先頭のbitが立っているかどうかに意味があるようになっています。
(人間が簡単に判別するには、それぞれの文字が大文字(0)か小文字(1)かで判断することができます。)

それぞれの文字の先頭ビットがどういう意味を持つのかは以下の通りです。

  • 1文字目: 必須チャンクかどうか。今回は必須チャンクではないので小文字(1)
  • 2文字目: パブリックなチャンクかどうか。今回は勝手につくるプライベートなチャンクなので小文字(1)
  • 3文字目: 予約。現在の仕様ではかならず大文字(0)
  • 4文字目: 複写可能かどうか。画像の内容が変化してもコピーしてよいならば小文字(1)、コピーしてはいけないなら大文字(0)

上記の条件を踏まえて、今回は "hoGe" チャンクを作ることにしましょう。
「必須ではなく」「プライベートな」「画像の内容が変わってもコピー可能な」チャンクです。

プライベートチャンクの挿入位置

hoGe チャンクをつくるのを決めたのは良いですが、どこに挿入するかも決める必要があります。
基本的には必須チャンクである IHDR, PLTE, IDAT, IEND のどこにいれるかという話しになります。(このうち、IHDR, IEND は先頭と最後になくてはいけないという決まりがあります。)
仕様に記述されているのはどのチャンクの前後になくてはいけないなど決められているチャンクもありますが、今回はプライベートチャンクですので適当に最初の IDAT の前にいれることにします。

CRC32 の計算

CRC32 というのはチェックサムアルゴリズムの一種です。簡単なアルゴリズムなので調べればすぐに実装できるとおもいますが、今回は拙作の zlib.js で使用しているものを利用します。
CRC32 の対象となるのはチャンクタイプとデータ部分です。

実装

では、さっそく実装に入りたいと思います。
ここからはコードが中心になりますが、さほど難しくないと思います。

CRC32 を計算するライブラリのロード

事前に以下のような形で読み込んでおきます。

<script src="https://rawgithub.com/imaya/zlib.js/master/bin/crc32.min.js"></script>

DataURL の作成

まずは埋め込む対象となる PNG 画像を Canvas#toDataURL() で作成しましょう。

var canvas = document.createElement('canvas');
var dataurl = canvas.toDataURL();

ここでは適当に Canvas を作っていますが、もちろんアプリケーションなどで描画した Canvas でもかまいません。

Base64 デコード

base64 文字列のデコードは、window.atob を用います。使えない環境の場合は拙作の base64.js などのライブラリでやります。
また、この段階からついでに Uint8Array に変更しておきます。

// Data URL からデータ部分を抜き出し
var b64data = dataurl.split(',', 2);

// Base64 デコード
var decoded = window.atob(b64data.pop());

// Uint8Array に変換
var png = new Uint8Array(
    decoded.split('').map(function(char) {
        return char.charCodeAt(0);
    })
);

PNG への埋め込みの準備

ここから PNG バイナリを探索してチャンクの挿入を行う訳ですが、PNG バイナリのどこを読んでいるか覚える変数が必要となります。
また、埋め込んだ後のデータを保存するバッファと、どこまで書き込んだかも覚えておきましょう。

埋め込んだ後の全体のサイズは

  • 現在のPNGバイナリのサイズ + 埋め込むデータのサイズ + 12

となります。なぜそうなるか分からない場合は、最初におさらいした PNG のチャンクの説明をみてください。

// 埋め込むデータ
var data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];

// 書き込み用バッファ
var implanted = new Uint8Array(png.length + data.length + 12);

シグネチャのチェック

まず、ファイル先頭の PNG シグネチャが一致しているか確認しましょう。
これによって PNG ファイルかどうかを簡単に判別することができます。

var Signature = String.fromCharCode(137, 80, 78, 71, 13, 10, 26, 10);

if (String.fromCharCode.apply(null, png.subarray(rpos, rpos += 8)) !== Signature) {
    throw new Error('invalid signature');
}

PNG のチャンクを探索する

シグネチャの確認が済んだら、ここからはチャンクが連続して配置されています。
必要のないチャンクは読み飛ばす事で高速に IDAT チャンクを探す事が出来ます。
前述のシグネチャのチェックとまとめて function にしておくと便利です。

function process(png, type, handler) {
    var dataLength;
    var chunkType;
    var nextChunkPos;
    var Signature = String.fromCharCode(137, 80, 78, 71, 13, 10, 26, 10);
    var rpos = 0;
    
    // シグネチャの確認
    if (String.fromCharCode.apply(null, png.subarray(rpos, rpos += 8)) !== Signature) {
        throw new Error('invalid signature');
    }
    
    // チャンクの探索
    while (rpos < png.length) {
        dataLength = (
            (png[rpos++] << 24) |
            (png[rpos++] << 16) |
            (png[rpos++] <<  8) |
            (png[rpos++]      )
        ) >>> 0;
    
        nextChunkPos = rpos + dataLength + 8;
    
        chunkType = String.fromCharCode.apply(null, png.subarray(rpos, rpos += 4));
        
        if (chunkType === type) {
            return handler(png, rpos, dataLength);
        }
        
        rpos = nextChunkPos;
    }
}

process(png, 'IDAT', function(png, rpos, length) {
    // rpos - 8 = チャンクの開始位置
    insertHogeChunk(implanted, data, png, rpos - 8);
});

insertHogeChunk メソッドの中身は後で作ります。

チャンクの作成

IDAT チャンクを見つけたら、その直前に hoGe チャンクを埋め込むので、埋め込むチャンクを作成する function を作っておきます。

function createHogeChunk(data) {
    var dataLength = data.length;
    var chunk = new Uint8Array(4 + 4 + dataLength + 4);
    var type = [104, 111,  71, 101];
    var crc;
    var pos = 0;
    var i;
    
    // length
    chunk[pos++] = (dataLength >> 24) & 0xff;
    chunk[pos++] = (dataLength >> 16) & 0xff;
    chunk[pos++] = (dataLength >>  8) & 0xff;
    chunk[pos++] = (dataLength      ) & 0xff;
    
    // type
    chunk[pos++] = type[0];
    chunk[pos++] = type[1];
    chunk[pos++] = type[2];
    chunk[pos++] = type[3];
    
    // data
    for (i = 0; i < dataLength; ++i) {
        chunk[pos++] = data[i];
    }
    
    //crc
    crc = Zlib.CRC32.calc(type);
    crc = Zlib.CRC32.update(data, crc);
    chunk[pos++] = (crc >> 24) & 0xff;
    chunk[pos++] = (crc >> 16) & 0xff;
    chunk[pos++] = (crc >>  8) & 0xff;
    chunk[pos++] = (crc      ) & 0xff;
    
    return chunk;
}

作成したチャンクの挿入

IDAT チャンクの前に先ほどの function をつかって hoGe チャンクを挿入します。

function insertHogeChunk(implanted, data, png, rpos) {
    var chunk = createHogeChunk(data);
    var pos = 0;
    
    // IDAT チャンクの前までコピー
    implanted.set(png.subarray(0, rpos), pos);
    pos += rpos;
    
    // hoGe チャンクをコピー
    implanted.set(chunk, pos);
    pos += chunk.length;
    
    // IDAT チャンク以降をコピー
    implanted.set(png.subarray(rpos), pos);
    
    return implanted;
}

Base64 エンコード

ここまででプライベートチャンクを埋め込んだ PNG の作成はできているのですが、
Canvas#toDataURL が DataURL を返しているので、形式をあわせます。
Base64 デコード とおなじように window.btoa が使える場合はそちらを、使えない場合は base64.js などを使います。

// Uint8Array から bytestring に変換
var implantedString = "";
for (i = 0, il = implanted.length; i < il; ++i) {
    implantedString += String.fromCharCode(implanted[i]);
}

// Base64 に変換
var implantedBase64 = window.btoa(implantedString);

DataURL の作成

Base64 に変換したのであとは DataURL 形式にするだけです。

var implantedDataURL = 'data:image/png;base64,' + implantedBase64;

データの取り出し

基本的には挿入時と同じような処理で hoGe チャンクを探してデータ部分を抜き出すだけです。
埋め込み時に使った process() を使います。

var extractedData = process(implanted, "hoGe", function(png, rpos, length) {
    return png.subarray(rpos, rpos += length);
});

終わりに

みなさんどんどん画像に余計な情報を埋め込みましょう。
明日は石川さんです。

]]>
8594