Cocos2d-xマルチプレイヤーアクションゲーム事始め

こんにちは、ガレージスタジオでネイティブシフト推進を行っている久富木隆一(@ryukbk)です。

本エントリは「GREE Advent Calendar 2014」16日目の記事です。今年も忙しい師走となりましたが、皆様いかがお過ごしでしょうか。Advent Calendarなるものが世間で流行っているようですが、宗教的信条の無い者としてはどうなのと思いつつも、お祭りは何だかんだで放っておけない性分でございます。

今回は、オンラインマルチプレイヤーゲーム、中でもアクションゲームの作り方について考えてみます。その過程で

  • マルチプレイヤーアクションゲームが克服すべき課題と、発展の歴史
  • Cocos2d-x 3.3、Cocos Studio 2による、C++11を用いた開発

について触れた上で、Cocos2d-x  & node.jsによるオンラインマルチプレイヤーアクションゲームのサンプルコードSantaShooterをご紹介します。

e-sportsの盛んな海外を中心としたPCゲームの世界、特にFPSゲームエンジンでのネットワークを介したマルチプレイヤーモードの実装の変遷から、Cocos2d-xの最低限の使い方まで、マルチプレイヤーアクションゲームのクライアント/サーバー開発で意識すべき諸問題からの学びにより、実装に取り掛かるための端緒を提供できればと思います。

想像の歴史

国民(nation)の定義として、以下を提案します:  国民とは、想像された政治共同体です。そしてそれは、内在的に制約されながら同時に独立主権的でもあるものとして、想像されているのです。(中略) 国民が想像されたものであるというのは、最も小さな国民の構成員ですら、同輩のほとんどを知ること無く、会うことも無い、いわんや聞いたことすら無いにもかかわらず、各々の心の中には自分たちの交わりの場のイメージが生きているということによります。(中略) 実のところ、顔合わせの原始的村落より大きな全ての共同体(あるいは原始的村落ですらも)は、想像上のものにすぎないのです。共同体は、真偽ではなく、その想像のされ方によって区別されるべきです。 -- ベネディクト・アンダーソン 『想像の共同体』(1983:6)

アメリカの政治学者ベネディクト・アンダーソンの代表的著作の、「どこかに真の共同体なるものがあるわけではなく、全ての共同体は想像の産物以上のものではない」と喝破した、冒頭の一節です。この、20世紀末に著された一文を読んで、皆さんは何を思い浮かべるでしょうか。私としては、19世紀以来の国民国家イデオロギーのスコープを超えて、21世紀のインターネット上に存在する、様々なコミュニティの有り様を想起させられます。かつてサイバースペースと呼ばれていた想像の共同体は、今日では我々の日常生活と切り離すべからざる地続きの存在になっています。

ネット上のコミュニティの中でも、一時的に、ネットワークを介して遠く離れたメンバーたちが、リアルタイムに、まるで側にいるかのように表現された空間で一堂に会して競い合うマルチプレイヤーアクションゲーム、特にFPS(First Person Shooter:一人称視点シューティングゲーム)のジャンルは、欧米で人気の主流です。

マルチプレイヤーFPSの最も原始的な形態は、1対1でコンピューター同士を接続して行う、シンプルなP2Pの形式です。まずはLAN上で、次にインターネットを介してマッチングを行いインターネット上で通信する形で、この対戦の形式が広まりました。私のようなオールドPCゲーマーにとって、たとえば「ドワンゴ」と聞いて思い浮かべるのは、まずもってこの、1990年代半ばに流行ったインターネットを介した対戦ゲームプレイサービスです。

この、シンプルな1対1のP2P接続のつぎにやってくるのは、2台より多い複数台のコンピューターを全て相互に、フルコネクトのネットワークトポロジーで接続してプレイする、シミュレーションゲームのジャンルです。"Age of Empires"、"Star Craft"といったRTS(Real-time Strategy)ゲームは、この方式です。この方式は、「行進(lockstep)」方式とも呼ばれます。行進というのは、全ての参加メンバーのコンピュータ上で、ゲーム内部で区切られた一定時間の1単位(ターン)が同じように計算され、全てのコンピュータ上で1ターンが同じように経過したと全てのコンピュータが合意して初めて、ターンが1進むという、ターンベースでの進行を、歩調を横一列で揃えて行進していくさまになぞらえたことによります。

こういったRTSゲームは、軍勢のようなユニットをコントロールすることが多く、前述のFPSのような、激しいアクションはありません。軍勢に指示を出してからゆっくりと軍勢が動きだすアニメーションを表示するなどして、ユーザーの入力を少なくし、背後では、参加しているコンピュータ同士で時間を区切って通信を行い、ターン内で行える行動を制限しつつ、ターンを進めているのです。

この形式の利点は、実装がシンプルなことです。

反面、短所としては、

  • 全てのコンピュータが合意に達するまでターンが進まないので、ネットワーク接続が遅いコンピュータが参加していた場合、全員のゲーム体験の品質が落ちる
  • 全てのコンピュータが1ターンで同じ計算結果に達する必要が有るため、ゲーム内の挙動は全て決定論的(deterministic)である必要がある

ゲームが決定論的であるというのは、たとえば違うコンピュータで計算すると結果が違うような乱数はゲーム内で使えないということです。どうしても乱数的なものを使いたい場合、C++11のmt19937のような、シードを与えると同じ結果を得られることが保証されている擬似乱数を使う必要があります。

この、決定論的でなければならないという点が、今日のモダンなゲームをマルチプレイヤー対応に作りこむ上で、障害となりうる点です。というのは、モダンなゲームを効率的に作る上で欠かせない物理演算のエンジンは、高度なものであるほど浮動小数点演算器を活用するため、計算精度の問題から、異なるデバイス間で同一入力から同じ計算結果を保証できないものとなっているからです。

また、全員のゲーム体験の品質が、最も遅いコンピュータに引きずられて悪化するというのは、特にアクションゲームでは、致命的な問題となります。冒頭で、共同体とは、共同で想像された幻想にすぎない、という言説を挙げました。ネットワーク上で、この幻想を維持するために必須なのが、遅延(ラグ/レイテンシー)の隠蔽です。回線の向こうでゲームプレイを共にするメンバーたちは、実際には地理的・物理的に遠く離れており、その通信は言うなれば互いに遠隔地同士で手紙をやりとりしているかのような、発送から受信までの間の時間的遅延を免れないものです。通信の遅延をいかに誤魔化し、儚い幻想を維持するか、そこにマルチプレイヤーゲームの全ての工夫が詰まっているといっても過言ではありません。

マルチプレイヤーゲームの歴史に戻って、"Doom"のようなP2P方式の1対1デスマッチプレイの次にやってきたのは、"Quake"に代表される、多対多のチームプレイを可能とするクライアント-サーバー方式でした。ユーザーがアクセス出来ない領域を含むサーバー上でゲーム内の物事が全て起こり、ゲームプレイヤーが操作するクライアント群は、キーストロークをサーバに送って結果を受け取るだけのシンクライアント、ダムターミナルとなる、という方式です。

この形式の利点は、サーバーが完全に掌握している空間内で起こる現象が常に優先されるため、チートが起こりにくいという点、そしてネットワーク接続が遅いプレイヤーが居たとしても、そのプレイヤーが不利になるだけで、他のプレイヤーはそのままプレイを続行できるという点です。チートに関しては、先述の行進方式でも計算結果に全員が同意する必要があるので実際にはさほど問題になりませんが、クライアント-サーバー方式のほうがより堅牢です。また、このモデルでは、全員が計算結果に同意する必要がなく、メンバーの途中参加・退出も可能です。

この、クライアント-サーバー方式でのゲーム体験は、日本のモバイルゲームユーザーには、馴染み深いものです。何故なら、強力なweb & データベースサーバーのスタックと、非力なフィーチャーフォン端末という組み合わせによるゲームプレイは、まさにこのクライアント-サーバー方式を(主に非同期ではありますが)踏襲しているからです。

ただし、この方式でも、すべての計算をサーバー上で行ってから結果をクライアント群にブロードキャストするということをやっているだけでは、十分なゲーム体験品質を達成するには至りませんでした。そこで、id Softwareの最初期のFPSゲーム"Doom""Quake"の開発者で、FPSゲームの始祖として有名なJohn Carmack(現在はOculus VR社のCTO)が"QuakeWorld"(QW)で披露したのは、一部の計算をクライアント上で並行して行い、正しい計算結果がサーバーから届くまで、見かけ上のレイテンシーを隠蔽するという方法でした。ここではCarmackは、当時の彼のblog的な、fingerコマンドで返ってくる.planで、それまで強力なサーバーのみが世界の情報をすべて持っていれば良かったエレガントな構造が、クライアントもサーバーと同じような物理演算を行うための情報セットを持ち、計算リソースを消費する無駄が出てきた、という嘆きのポーズを表明していますが、その犠牲をユーザーが払うだけの価値をもたらす革新でした。

"Quake"のソースコードはgithub上で公開されており、レイテンシー隠蔽の代表的テクニック、クライアント予測(client prediction)部分のソースコードを閲覧可能で、解説記事もあります。クライアント予測は、クライアント上でもサーバー上で行われているシミュレーションの一部を実行することで、例えば自分が動く場合は、サーバーに自分が動くというコマンドを送ってその移動結果を待つという悠長なことをするまでもなく、クライアントローカルで先に動いたことにするという挙動になっています。また、他のプレイヤーの位置についても、情報が届く前に、手元にある情報を用いて、外挿法による補間(extrapolation)を行います。この推測方式は、Dead Reckoningとも呼ばれます。

Quakeエンジンを改造・発展させ、"Counterstrike"等のマルチプレイヤーゲームを生み出した、Valve SoftwareのGoldSrc/Sourceエンジンに、今日のモダンなマルチプレイヤーアクションゲームの代表的なレイテンシー隠蔽テクニックが網羅されており、Valve社のYahn W. Bernierによる"Latency Compensating Methods in Client/Server In-game Protocol Design and Optimization"は、"Source Multiplayer Networking"のベースとして、この分野で最も参照されるドキュメントです。ここで説明されているのは、3つのテクニックです。

  1. クライアントサイド予測 (client-side prediction)
  2. エンティティ補間 (entity interpolation)
  3. ラグ補償 (lag compensation)

まず、クライアントサイド予測は、"QuakeWorld"でも導入されていますが、Valve版では、クライアントで予測したシミュレーション結果と、サーバーから届いたシミュレーション結果に齟齬が通じた場合は、サーバーから届いた結果はクライアントから過去に送ったキー入力の結果であるので、一旦はサーバーのシミュレーション結果をクライアントで受け入れるにしても、それ以降にクライアントに入力された操作情報をサーバーからの結果に重畳して適用するという形を取ります。クライアント側ではスライディングウィンドウにキー入力履歴を保存しておいて、サーバーから届いた結果を生じさせた入力以前のタイムスタンプの履歴は破棄し、それ以降の一連のキー入力をサーバーからの結果にあらためて適用し、その時点での新たなクライアント予測の結果に反映する、という挙動です。ある意味、gitのrebaseコマンドに似ていると言えなくもありません。rebase先(サーバーという変更履歴ブランチ)に、rebase元(クライアントブランチでの変更)を適用して、1続きの歴史を再構成するというわけです。

つぎに、エンティティ補間は、Dead Reckoningの発展形です。外挿法だと、プレイヤーが急に方向を変えたりジャンプしたりと、予測を破るような急制動をしやすいアクションゲームでは都合が悪く、他のプレイヤーなどのゲームを構成する物体(エンティティ)の挙動予測に失敗することが多いところ、サーバーから送られてくるゲーム内情報を数フレーム分バッファリングすることによって、複数フレームを用いてその間を内挿法的に補間し、必要に応じて外挿法を援用することによって、プレイヤーが目にする世界が全体的に遅延したものとなることと引き換えに、より精度高くなめらかな表示を実現するものです。

最後に、ラグ補償は、ネットワークレイテンシーと、エンティティ補間によって導入された補間用バッファリングのレイテンシーの双方に、サーバーサイドで対策するもので、シューティングゲームで重要になる攻撃行動の結末を、ユーザーが自分のマシン上で目にしている情報を尊重するように補正するものです。どういうことかというと、ユーザーは他のプレイヤーを的に銃を発砲するわけですが、ユーザーが目にしているのはサーバー上で過去に起こった事象なので、そのままだと、他のプレイヤーが過去に居た位置に銃を撃っていることになってしまい、狙いを定めるにはラグを念頭に先読み発砲する必要が出てきてしまいます。これを、サーバー上で、ユーザーが銃を発砲した際には、ラグに応じた過去時点での各プレイヤーの位置履歴を参照・検索し、当たり判定を行って、ユーザー側でラグを意識せずに武器を使用できるようにするシステムを指します。弊害として、被攻撃側が攻撃を避けたはずなのに、ネットワーク遅延が激しいプレイヤーの攻撃に被弾しやすいといった問題もありますが、そのメリットの大きさから、概ね受け入れられている方策です。同じサーバーの中の同じ世界をユーザー全員が同じように視点だけを変えて見るというモデルから外れ、クライアントごとに微妙に異なる世界がレンダリングされているというモデルが、今日の支配的なマルチプレイヤーオンラインアクションゲーム環境です。

ここまで、FPSゲームのオンラインマルチプレイヤーモードで開発されてきたレイテンシー隠蔽テクニックについて紹介してきました。一方で、欧米のゲームエンジンは、ゲーム開発手法上も大きな発展を遂げてきました。上で述べたようなオンラインマルチプレイヤーゲーム開発の成果を総括しつつ、再利用可能なゲームエンジンに統合して、ゲームエンジンユーザーに対し抽象的で自然なモデルとして提供するという偉業を達成したのが、Epic GamesのUnreal Engineです。

Tim Sweeneyによる"Unreal Networking Architecture"は、オブジェクト指向の哲学的なレベルでネットワーク上のゲームモデルを整理した文献としてつとに有名です。このドキュメントは現在はUnreal Engine 3用のドキュメントとして公開されており、最新のUnreal Engine 4(UE4)用にはそのエッセンスであるレプリケーションのコンセプトを解説した文書のみの公開となっていますが、今日でもその意義は色褪せていません。

Unreal Engineでは、Sourceでエンティティと呼ばれていたゲーム内の物体はアクターと呼ばれ、ゲームのシーンを構成するレベル内にアクターが居て変数を持っている、という構造となっています。"The server is the man"(サーバーこそが主人)という有名な文句に象徴されるように、サーバーが持っている情報のみが絶対の信頼性を担保する(authoritative)、という点は当然として、クライアント側が持っている情報は、サーバーが持っている世界の情報の一部を、特定クライアントが感知する必要性のある範囲内のみをパーティショニングして再現(replication)した、いわば影のようなものにすぎない、という視座が語られています。(この、レプリケーションのコンセプト、いわゆるAOI(area of interest)のブロードキャスト戦略については、Tribesエンジンを基礎に、Haloのエンジンが掘り下げています)

Unreal Engineを使ってゲームを開発するユーザーは、UE3まではKismet/UnrealScriptでビジュアルプログラミング/スクリプティングを行い、C++で低レベルコードを書いていました。UE4ではハイレベルのビジュアルプログラミングはBlueprintで、他方(UnrealScriptを排して)リフレクション付きC++でスクリプティングから低レベルのコーディングまでを行い、標準の部品を使う限りでは、属性をon/offするだけで上述のクライアント予測などの機能を手軽に、自動的に利用できるようになっています。この、ネットワークマルチプレイ機能のゲームエンジンへの透過的・抽象的でネイティブ/ビルトインな統合が、私見では、Unityなどの新興ゲームエンジンがまだ備えていない、Unreal Engineの最重要フィーチャーであると思っています。

以上が、欧米でのマルチプレイヤーゲームエンジンの簡単な歴史です。日本からの視点については、CEDEC 2010でのSEGAの講演も参考になります。また、クライアント側での挙動計算をさらに推し進めた、"Battlefield"のDICEによるFrostbiteエンジンのような近年の方式もあります。

Cocos2d-xってどうよ

ここで、オープンソースで、かつ美しい構造と使い勝手とを持つUnreal Engine 4でマルチプレイヤーゲームを開発できれば全て万歳\(^o^)/となれば話が早いのですが、現実的にはなかなかそういうわけにもいかず、様々な大人の事情から、例えば今日のモバイルゲーム開発には、Cocos2d-xもよく用いられます。

Cocos2d-xは、無料でオープンソースで、かつそれなりに動作が軽いため、2Dゲーム開発では、日本でも利用例がかなり増えました。また、最近では、APIの元になったiOSのCocos2Dフレームワークから離れて独自に発展し、Cocosという新ブランドのもとに統合開発環境を構築し始めるなど、活発に発展しています。3D向けのAPIも追加されつつあります。今月ついにFinal版がリリースされたCocos2d-x 3.3では、サウンド周りのAPIが刷新されるなど、重要な変更もあります。

一方で、開発が活発で急速なことによる弊害もあり、バージョンアップでAPIが変わったり、時々変なバグが入っているなんてのは日常茶飯事です。また、開発の本拠地が中国なので、最新情報を探そうとすると、中国語の掲示板しかなかったりします。中国ではPCもWindowsが圧倒的に主流なので、最近までCocos StudioはMac版の開発が遅れており、先月やっとCocos Studio 2でMac版がWindows版に追い付きました。私的には、Visual Studioで開発できて楽しかったのですが、Mac万歳な方々には良好な環境とは言いがたい状態でした。

そして、Cocos Studio 2は、Cocos Studio 1と比べ、前バージョンと全然違うUIと、駆動するための全然違うコードを必要とし、最近自分で触ってみたところその学習カーブの急さに閉口したので、本記事では後ほど、使い方について簡単に解説します。

さて、Cocos2d-xで仮にネットワークゲームを作るとして、Cocos2d-xは薄いフレームワークなので、まだまだ相対的なレベルではUnityなどに比べると機能は少なく、ネットコードやバックエンドサーバーに何を用いればよいか、という問題が生じます。

最近では、バックエンドのサーバーを自前で用意せず、Photon Realtime(旧Photon Cloud)などのmBaaSを用いて、モバイルのネットワークゲームを提供する例も増えてきました。しかし、そこには留意すべき点があります。それらは、一般に、サーバー側にカスタムロジックをインストールできる機能が提供されていない限り、クライアント側でのデータ改竄の排除や、データセットのクライアント側へのマスクを行えないため、authoritativeにはなれない、ということです。それはつまり、"Cheating in online games"に列挙されるような様々な手の込んだチート手法に対し、脆弱であるということを意味します。そのあたりは、ゲームデザインを調整することで対応できる点もありますが、ユーザ間競争要素が高く、かつアプリ内購入機能を重視するゲームデザインを想定する場合は、許容できません。

よくあるmBaaSのモデルとしては、マッチングやランキングといったハイレベルな交流機能と、チャットルーム的なメッセージリレーチャンネルとを提供するというものがありますが、これだけだと、チートに対抗できません。Photon Realtimeでいえば、authoritativeなサーバーとして、Photon Serverの利用が必要になるということになります。

ネットコードについてはどうでしょうか。Cocos2d-xで使える、上記で紹介したようなクライアントサイド予測などの洗練された機能を用意してくれる組み込みライブラリは、残念ながら見つかりませんでした。

ちなみに、Unityにしても、Unityが使っている物理エンジン(PhysicsはNVIDIA PhysX、Physics2DはBox2D)のステップ実行がサポートされていないので、物理挙動シミュレーションを1フレーム内で巻き戻したり進めたりということをやりたい場合は、Unityに組み込まれた物理演算エンジンの利用を諦めて自前で挙動をC#で書くか、それが重くて許容できない場合は、C++のネイティブプラグインを作って独自実装を使わざるを得ません。(ちなみに、Unreal EngineもPhysXを使っており、PhysXのマニュアルのNetwork Physicsの項には、PhysXが非決定論的であることによる注意が記載されています) また、Unityが組み込んでいるネットワークライブラリRakNetは、最近Oculus VR社が買収してオープンソース化しましたが、これはこれで低レベルで、クライアントサイド予測などの機能はなく自前で実装する必要があったり、サーバーがあまりフレキシブルではなかったりと一筋縄では行きません。

SantaShooter

ここからは、本記事のためにでっち上げてみたサンプルプログラムSantaShooterを題材に、Cocos2d-xの基本的な使い方を見ていきます。SantaShooterは、2人のサンタクロースが向かい合って上下移動しながら、プレゼントを互いに投げつけ合うという、2Dの2プレイヤーネットワーク対戦ゲームです。

santashooter_3

遊び方は、

  1. proxyディレクトリの中で、node proxy.jsで、WebSocketプロキシサーバーを起動
  2. クライアントを起動し、プロキシサーバーのIPアドレス:ポートを記入し(無記入のデフォルトは127.0.0.1:31337)、Connectボタンをマウスクリック
  3. 3つクライアントを起動して接続すると、1番めがサーバー、2番めがプレイヤー1クライアント、3番めがプレイヤー2クライアントとなる
  4. プレイヤー1はW/S、プレイヤー2は矢印キーで上下動。相手のエリアをマウスクリックでクリック方向にプレゼントを投げつける。相手に当たると点数加算

というものです。Connectボタンを押下しない場合、スタンドアローンで動作します。

利用した環境は以下です:

  • Cocos2d-x 3.3
  • Cocos Studio 2.0.6
  • Windows: Microsoft Visual Studio Community 2013
  • Mac: Xcode 6.1.1 (Macターゲットのみ、iOS未サポート。Resourcesディレクトリの中身は全て実行ファイルから参照できる場所に置くこと)
  • node.js v0.10.33

ネットワーク機能には、Cocos2d-xに組み込まれているWebSocketを使ってみました。高度なゲームエンジンはUDPベースのプロトコルで通信し、パケットロス対策したり、といったことをやっていますが、今回はその辺りにこだわる余裕は正直ありませんでした。Cocos2d-xのWebSocketにはサーバー用のlisten機能はなさげだったので、node.jsとwsモジュールを使って簡易プロキシサーバーを用意し、そこに接続することで、最初に接続してきたクライアントがサーバーの役目を果たします。アーキテクチャ的には、クライアント-サーバー型のマルチプレイヤーオンラインゲームとなっています。

santashooter_architecture

私のPCでは、同じPC上で単純に3つつなげた状態で、ping=150ms程度でした。サーバーは、ハンドシェイク兼pingが済むと、後はひたすらプレイヤー1/2の位置・速度・スコアをクライアントに送り続けます。実際のゲームであれば無駄なデータを送らないよう差分圧縮(delta compression)を行うべきところですが、世界全体の情報量が非常に少ないため、整合性に問題が生じないよう全部のセットを1度に送りつけるという戦略はそれはそれで妥当性があります。

クライアントは、クライアントサイド予測を実装しています。クライアント側では自分の挙動とプレゼントの挙動をユーザー操作を受けて直ちに自前で物理演算して表示はしますが、サーバー上での物理挙動が真であり、当たり判定はあくまでサーバー上にしか存在しません。クライアント側の予測とサーバー上のシミュレーション結果にある程度のずれが生じた場合、クライアントにサーバー側の過去の結果を適用して世界を巻き戻し(rewind)、そこから再度クライアント上の操作を再生する(replay)時に、Chipmunk物理エンジンのステップ実行を行います(step関数に負の値を与えると動作を巻き戻せるのでこれはこれで面白いです)。物理エンジン自体を先に進めると、プレイヤーのみならず世界全体が早送りされてしまうので、プレゼントボックスは早送り前に情報を保存し、早送り後に復帰させています。また、物理演算は固定フレームレートで実行できるとサーバー・クライアント間の同期を取りやすくシミュレーション結果に差異が生じにくいですが、今回は物理エンジンのシミュレーションを可変フレームレートの1フレーム内で毎回、常に1/60秒分進め、クライアントから送るフレームシーケンス番号を使ってクライアントとサーバー間で歩調を合わせる行進方式としています。コード上は、GameScene::rewindAndReplayClientWorldState関数に実装しています。

サーバー上では、上述のラグ補償を簡易的に実装し、攻撃を行うと、攻撃中は、対象ユーザーの過去の位置にヒットボックスが現れ、対象ユーザーに追随動作します。

通信パケットは、デバッグしやすいように、Cocos2d-xに組み込まれているRapidJSONを用いて、JSONにしました。Cocos2d-x 3.3からは、Cocos Studioのcsbフォーマットのシリアライズ/デシリアライズ用にGoogleのFlatBuffersが組み込まれているので、それを用いることもできるかもしれません。ちなみにFlatBuffersはCocos2d-x 3.3 RC2/Cocos Studio 2.0.5からいきなり導入されたので、先にCocos Studioだけ2.0.5に更新してpublishするとCocos2d-x 3.3 RC0で読み込めず、エラーメッセージ無しで落ちるため、焦りました。RapidJSONについても、読み込む型が違ったりするとassertに引っかかるので、実際に使う場合は、マクロでRAPIDJSON_ASSERTをオーバーライドし、例外を投げるなどの自前のコードを仕込んでおくのを推奨します。

サーバーとクライアントのコードは渾然一体で書かれていますが、物理演算や通信部を共有できるのが良いくらいで、本来は共有部分以外は分離して書くべきです。また、今回はプレイヤー1とプレイヤー2をハードコードしていますので、かえってコードに無駄が生じてしまいました。リファクタリングするならば、任意の数のクライアントをサポートできるように、アクターにIDを振ってコントロールできるようにしておくべきです。

Cocos2d-xを使うからには、C++1xをこねくり回すのが個人的にはなかなか楽しみではあります。例えば、JSONは、不定形なデータを気楽に投げ込むためのバッグでありシンクとして利用できることを期待して使っているわけですが、JavaScript等と違って、C++は当然ながら静的型システムなので、気楽に使うにはやや扱いづらいところもあります。完全にダイナミックな処理を書くならともかく、ソースコード内に似たようなRPC関数風スタブ群を手書きで構成する場合に、引数の型や数がまちまちだったりすると、いちいちRapidJSONの手続き的コードをコピペするのも見苦しいところです。手間を軽減するために今回は、Variadic Templatesを用いてみました

createMessage関数は、先頭3つの引数は、その関数の命令コードや、ディスパッチ元など、定型的なものを指定しますが、4つ目以降は、JSON上の"a0"、"a1"、"a2"...というキーで、整数や文字列など、引数の型に応じて適切なRapidJSONのAPIを呼んで値を格納してくれます。std::tupleでも似たようなことはできますが、std::tupleはイテレーターが無いため、ここで指定されたリスト要素に対して順にaddKV関数の処理を適用するために、テンプレートエイリアスを用いて宣言した配列の初期化リストが左から順に初期化されるという性質と、複合式で返り値voidの関数含め左から順に評価(結果は無視)された上で右端の結果が返されるという性質とを用いています。10年前にはマクロによる型リストを用いたC++テンプレートメタプログラミングに熱中したものですが、C++1xの時代になっても、いまだにこういう変なテクニックの出番があるという事実に戦慄せざるを得ません。

ボタンや、文字入力ボックスなどのUIは、Cocos Studio 2で作成しています。ドキュメントの記載が乏しく、使い方に悩むのですが、メニューでNew Fileを選択し、Nodeを作成すると、Canvas上にボタンなどのControlをドラッグ&ドロップして配置できるようになります。ちなみに、Cocos2d-x的には、Layerはインターフェイスとしては残りますが、今後は廃止され、Nodeのみが用いられるようになります。Nodeは現在Layerのすべての機能をサポートしています。

また、スプライトアニメーションも、Cocos Studioを使って作成できます。既に存在するスプライトシートの一枚絵から部品画像を抽出するにはShoeboxのようなツールを使えますが、部品画像をまとめて1枚のスプライトシートを作成する機能が、Cocos Studioに内蔵されています。

santashooter_csi

  1. New FileからSpriteSheetを選択し、csiファイルを作成
  2. Canvas上のcsiファイルに、Resourcesからスプライトシートに含める画像を複数選択し、ドラッグ&ドロップ

以上で、選択した複数画像を含むスプライトシートが作成されます。Publishするとplistが生成され、SpriteFrameCacheを使って読み込むことができます。

アニメーションを作成する場合は、

  1. New FileでNodeを作成してから、Canvas上の同じ位置に使う画像を重ねた上で、Automatic Frame Recordingをチェック
  2. Propertiesの内容、例えばVisibleチェックボックスを切り替えたりすると、タイムラインの青色の線を置いた位置にキーフレームが置かれます。最初、表示・非表示を目のアイコンで切り替えるものと勘違いしていたので、気付くまでここはハマりました
  3. Visibleチェックボックスを切り替えて同時に1つの画像のみ表示されるようにしていけば、コマ送りのスプライトアニメーションを構成できます
  4. 他のスケールなどのプロパティについても同様にタイムライン上で変化させていくことができます

santashooter_csd

出来上がったら、保存されたcsdファイルからcsbファイルを生成するために、publishを実行すると、.cfg内で指定されたPublishDirectory内にcsbファイルが吐かれ、CSLoader::createNodeやCSLoader::createTimelineを使って、csbファイルからUI部品ノードやcocostudio::timeline::ActionTimelineアニメーションを読み込めるようになります。

Cocos2d-x自体は、変わった使い方はしていません。NodeをSceneにAddChildし、要らなくなったらNode自身のremoveFromParentを呼び出してautoreleaseに任せる、というだけです。今回は挙動のコントロールに物理演算を用いましたが、MOVE_WITH_PHYSICSマクロ周辺に、物理演算を使わずActionで等速運動をさせる書き方も書きましたので、参考にしてみてください。

PhysicsBodyでコリジョンの対象を規定するsetCategoryBitmask、setCollisionBitmask、setContactTestBitmaskはこれまたドキュメントがソースコード中にしか無く、また使い方もわかりにくいので、デバッガでステップ実行でもしながら制御フローを追い、ソースコードそのものを読んだほうが理解が早いかもしれません。Cocos2d-xは最終的には「ソース読め」に行き着いてしまうので、そこが辛くも有り難いところです。

2015年

ここでのサンプルはPC上での実験に留まりますが、対象をモバイルデバイスとした場合、変動するping値にどう対処するかなど、頭の痛い問題が現れてくるのもまた事実で、ネットワークプログラミングに対してクリアな回答というものはまだ世に存在しません。近年では、Playstation NowやShinra Systemといった、サーバーサイドで全ての演算とレンダリングを行い、動画をクライアントへストリーミングするというソリューションもありますが、モバイル回線に降りてくるにはまだ時間がかかりそうですし、アクションゲームに望ましい低レイテンシーに耐えうるものとなりうるかは未知数です。

一方で、Cocos2d-xはオープンソースのフレームワークで、日進月歩の様相を見せており、いわば終わらない祭り状態です。2015年、どんな愉快なことになっているか、今から楽しみです。

 


明日はService Reliabilityチームの「いわなちゃん」さんによるVarnishのお話です。お楽しみに!