大規模ゲーム制作時の課題 なぞってピグキッチンのような大規模ゲームでは、ページ遷移やサーバとクライアント間のデータ通信が頻繁に発生します。
しかし、その度にページをリロードしてしまうとユーザがストレスを感じるゲームになってしまいます。
また、小・中規模のサービスの場合、複数のJavaScriptファイルを一つのJavaScriptファイルにoptimizeするのが一般的ですが、大規模サービスを構築する上では、ユーザエクスペリエンス向上のためにも、 画面別、機能別にモジュールを分割し遅延ロードさせる仕組み(サブモジュール開発) が非常に重要となります。
課題に対するアプローチ なぞってピグキッチンでは、Ajaxを用いることによって、ページをリロードさせずにゲームを遊ぶことができるようにしています。
また、画面別、機能別にモジュールを分割し遅延ロードさせるサブモジュール開発を行うために、社内フレームワークであるBeezを用いて開発を行っています。
Beez:
https://github.com/CyberAgent/beez Beezはまだ一般公開されていないフレームワークなのですが、近日オープンソースとして一般公開する予定なため、今回、Beezに関する記事を書かせていただきます。 (※ 2013年12月19日追記:本日、無事にOSSとして公開されました!)
Beezとは Beezはスマートフォン開発をスピーディーに進めるための、中・大規模クライアントブラウザ向けのフレームワークです。
Beezでは、サブモジュール開発を前提にしています。
後述するBeez.ControllerやBeez.Routerをうまく使用することでシンプルで柔軟なサブモジュール開発が行えます。
Beezのライブラリ構成 Beez全体のライブラリ構成は下図のようになっています。
BeezでのWebアプリケーション開発には、
Backbone 、
RequireJS 、
Underscore /
Lo-Dash 、
Zepto /
jQuery 、
Handlebars 、
Stylus を利用します。
なぞってピグキッチンではUnderscoreとZeptoを用いて実装を行っておりますが、以降はより一般的なUnderscoreとjQueryで説明をさせていただきます。
・Backbone Backboneは中・大規模クライアントブラウザ向けのフレームワークです。
BeezはBackboneを拡張することによって作られています。
そのため、Beezは基本的にはBackboneに沿った書き方で記述をすることが出来ます。
また、BackboneがModel、View、RouterのMVRモデルであるのに対し、
BeezはBackboneのMVRモデルに更にControllerとManagerと加えたMVCR+Managerモデルの設計モデルで実現されています。
※ 今回、BeezではBackboneに新たにControllerを追加しているため、敢えてBackboneをMVCモデルではなくMVRモデルと表現しています。
・RequireJS RequireJSはJavaScriptファイルの非同期読み込みとモジュール化を行うライブラリです。
RequireJSを用いることで、画面別、機能別にモジュールを分割し遅延ロードさせることができます。
・Underscore(Lo-Dash) Underscoreは配列操作などの便利なユーティリティ関数をまとめたライブラリです。
これにより、Underscoreで提供されている便利メソッドを利用出来るようになります。
・jQuery(Zepto) jQueryはDOM操作などをより簡単に記述できるようにするライブラリです。
これにより、jQueryを用いたDOMへのアクセスなどが行えるようになります。
・Handlebars HandlebarsはJavaScript用のテンプレートエンジンです。
Handlebarsを用いることで、HTMLの記述を簡潔にすることができます。
・Stylus StylusはCSSの拡張言語です。
Stylusを用いることで、CSSの記述を簡潔にすることができます。
Beezの設計モデル 次にBeezの設計モデルである、MVCR+Managerモデルに関して説明したいと思います。
・M(Model/Collection/Modic) :
Document Modelはデータ管理や状態管理、ビジネスロジック、サーバとの通信を担当します。
Beez.ModelはBackbone.Modelをextendしており、Backbone.Modelと同じ機能を有します。
Beez.CollectionはBackbone.Collectionをextendしており、Backbone.Collectionと同じ機能を有します。
また、Modicは通信通信機能がないModelです。クライアントデータのみでデータを管理する際に利用します。
・V(View) :
Document ViewはDOMイベント、Userイベントを管理し、DOMの更新を担当します。また、Modelイベントのハンドリングを行い、Modelへのイベントの伝達やModelからのデータの取得を担当します。
Beez.ViewはBackbone.Viewをextendしており、Backbone.Viewと同じ機能を有します。
・C(Controller) :
Document モジュールの玄関口を担当します。主にBeez.Routerからのイベントを起点にViewとModelの生成および初期化や、Viewの表示を行います。
・R(Router) :
Document Routerはhashchange/pushStateを管理し、Controller間の連携を実現します。
Beez.RouterはBackbone.Routerをextendしており、Backbone.Routerと同じ機能を有します。
・Manager(Manager) :
Document ManagerはBeez.Manager.Model、Beez.Manager.View、Beez.Manager.Controller、Beez.Nanager.Routerがシングルトンで存在し、各々がModel、View、Controller、Routerの管理を行い、生成、破棄、取得等々をすべて一元的に管理します。
Managerによる一元管理 上記の設計モデルの説明の中で、ManagerはModel、View、Controller、Routerの管理を行い、生成、破棄、取得等々をすべて一元的に管理すると説明しました。
ここでは、どのようにして一元管理を実現しているのかについて説明したいと思います。
Managerは内部で生成された各インスタンスをツリー構造で管理します。
また、ツリー構造上で保持するインスタンスにアクセスするためのメソッド群を提供します。
ManagerはModel、View、Controller、Router毎に存在し、Managerの自動remove等の恩恵を受けるために、各インスタンスの生成は対応するManagerを通して行うことが必須です。
(ツリー構造管理は、View、Modelのみです。他のManagerは、フラットで管理されています。)
それではサンプルコードで見ていきたいと思います。
var viewManager = beez.manager.view; // RootとなるViewのインスタンス作成 var RootView = beez.View.extend('RootView', { vidx : '@' }); // Viewのインスタンス作成(以下他のViewも同じ) var ParentBillView = beez.View.extend('ParentBillView', { vidx : 'bill', tagName : 'div', events : { }, initialize : function initialize() { }, beforeOnce : function beforeOnce() { }, before : function before() { }, render : function render() { }, after : function after() { }, afterOnce : function afterOnce() { } }); var ChildAndyView = beez.View.extend('ChildAndyView', { vidx : 'andy', tagName : 'div', events : { }, initialize : function initialize() { }, beforeOnce : function beforeOnce() { }, before : function before() { }, render : function render() { }, after : function after() { }, afterOnce : function afterOnce() { } }); // ParentTriciaView, ChildJordanView, ChildMarieView, ChildRobertViewは省略 // 'manager'のルートを生成 viewManager.root(RootView); // ルートの子としてViewを生成 viewManager.create('/@', ParentBillView); viewManager.create('/@', ParentTriciaView); // billの子としてViewを生成 viewManager.create('/@/bill', ChildAndyView); viewManager.create('/@/bill', ChildJordanView); // triciaの子としてViewを生成 viewManager.create('/@/tricia', ChildMarieView); viewManager.create('/@/tricia', ChildRobertView);とした場合、Managerは、
Root - Bill --- Andy | └ Jordan | └ Tricia - Marie └ Robertというツリー構造で各インスタンスを管理します。この状態で、
// billを破棄 beez.manager.view.remove('/@/bill');を行うと、「Bill」とその子要素である「Andy」と「Jordan」のremove()メソッドが順に実行されます。また、それらインスタンスのイベントもすべて開放されます。
結果として、Managerが管理するツリー構造は
Root - Tricia - Marie └ Robertの様に変化します。
BeezのModule構成 BeezはMVCのかたまりで1モジュールを形成し、それぞれがRouterを経由して連携する構成になっています。
また、モジュール別にビルドし、それぞれのモジュールが最初に必要になった際にロードされる、遅延ロードの仕組みもサポートしています。
遅延ロードはRequireJSを用いて実現しています。
なぞってピグキッチンでの実例 最後に、なぞってピグキッチンでの実例を紹介します。
・アクセス なぞってピグキッチンにアクセスするとまず、常に必要になる機能のViewやModelが生成されます。
各々Managerを通して生成を行うことで、ViewとModelをツリー構造で管理することができます。
生成後にManagerが管理するツリー構造は下記のようになります。
Root - CoreRoot - Header └ Footer :(主にここではViewのツリー構造を追っていきますが、Modelのツリー構造も同様に生成されます)
生成が終わったらマイページにリダイレクトします。
・アクセス → マイページ なぞってピグキッチンでは、hashを用いて現在のモジュールを管理しているため、マイページに遷移する際にはURLの後ろに#mypageが付きます。
http://piggpuzzle.amebagames.com/#mypageRouterがhashchangeを監視しており、hashchangeの内容を見て適切なコントローラーを呼び出します。
ここではmypageControllerを呼び出します。
mypageControllerは必要なViewとModelの生成および初期化を行い、Viewの表示を行います。
ここでは、アバターやデコレーションやショーケースなどがManagerを通して生成されます。
その結果、Managerが管理するツリー構造は下記のようになります。
Root - CoreRoot --- Header | └ Footer | : | └ MypageRoot - Avatar └ Deco └ Showcase :・マイページ → エリア選択画面 次にパズルボタンを押してエリア選択画面ページに飛びます。
Viewがボタンのタップイベントを検知し、Routerがhashを#areaに切り替えます。
http://piggpuzzle.amebagames.com/#areaそれによりhashchangeが走り、areaControllerが呼び出されます。
エリアモジュールには、「エリア選択ページ」と「ステージ選択ページ」と「ステージ詳細ページ」が存在します。
areaControllerはViewとModel生成のタイミングで3つのページのViewとModelを全て生成します。
Managerが管理するツリー構造は下記のようになります。
Root - CoreRoot --- Header | └ Footer | : | └ MypageRoot - Avatar | └ Deco | └ Showcase | : | └ AreaRoot --- AreaSelect └ StageSelect └ StageDetailエリアページはマイページの上のレイヤーに重なって表示されるため、マイページのモジュールを破棄せずに残しています。
・エリア選択画面 → マイページ エリア選択画面を閉じてマイページに遷移した場合を考えます。
Viewがボタンのタップイベントを検知し、Routerがhashを#mypageに切り替えます。
http://piggpuzzle.amebagames.com/#mypageこのとき、Managerのremove()メソッドによって必要の無いエリアモジュールは破棄されてツリー構造からも取り除かれます。
beez.manager.view.remove('/@/areaRoot');その結果、ツリー構造はエリアページに遷移する前の下記の状態になり、無駄なモジュールを持っていない状態になります。
Root - CoreRoot --- Header | └ Footer | : | └ MypageRoot - Avatar └ Deco └ Showcase :・まとめ 以上のように、Beezを用いて適切なタイミングで適切なモジュールを読み込んだり破棄したりすることで、無駄なモジュールを持っていたり、無駄なイベントを監視していたり、無駄なプロパティを保持したままだったりする問題が起こらなくなります。
さいごに 拙い文章でしたが最後までお付き合いいただきありがとうございました。
BeezはDocumentがしっかりしており、体系的にBeezを学ぶことが出来るチュートリアルなども用意されているので、オープンソースとして一般公開された際には是非ともそちらを見ていただければと思います。
公開後にはこちらの記事内の各々の項目にもDocumentへのリンクを追加したいと思います。
(※ 2013年12月19日追記:もろもろリンクを追加させていただきました)
・
Beez ・
Document ・
チュートリアル また、
なぞってピグキッチン はブラウザゲームでどこまでできるのかに全力で挑戦したサービスです。是非とも1度触ってみてください。
ありがとうございました!