Cocos2d-x で 3Dフライトアクションゲームを作ってみた - アメリカでの研修を通して

GREE Advent Calendar 2015, 13日目です!

whoami

はじめまして、奥畑と申します。
新卒入社からの2年半で、プラットフォームの運用、Webゲームの新規開発・運用に1年ずつ携わり、現在は北米支社と共同で Nativeゲームの運用に携わっています。

私はこの夏、グリーが資本業務提携している米Make School社によるアプリ開発者養成講座に、新卒1~4年目の社員8人で参加してきました。世界中から300人ほどの高校生・大学生が集まり、2ヶ月間のカリキュラムの中で、前半3週間は iOSアプリやゲームの作り方を学び、 後半5週間で各々オリジナルのアプリやゲームを作り App Store にリリースするというもので、我々はグリー専用コースとして Cocos2d-x によるゲーム制作を行いました。

その中で私は 3Dフライトアクションゲームを作りました。iOS (ついでに Android も) 開発経験ゼロ、当然 Cocos も Unity も知らず、3Dの知識も経験も皆無という状態から、一応5週間でリリースするところまでできてとてもよい勉強になったので、この研修を通して学んだことや、現地の学生と交流して感じたことなどをご紹介できればと思います。

作ったもの

Sky High!!
https://itunes.apple.com/jp/app/sky-high!!/id1028886804

Sky High!!

画面は開発中のものです

指一本で操作できます。課金・広告等ありませんので、よろしければ触ってみてください。

ソースコードはこちらにあります。
https://github.com/aokht/Flight

なぜ Cocos2d-x?

と、作る前も作ってる最中も作った後も言われ続けましたw
なんとなく 3D やりたいと考えて、研修内容が Cocos2d-x だったから、じゃあ Cocos で 3D やってみようとなっただけで特に深い理由はありません。仕組みからしっかり勉強してみたかったので、わからない部分はひたすらコードを読みつつ、ライブラリとして足りていない部分は自分で実装していくことで、とてもよい勉強になったと振り返ってみて思っています。
(Make School のカリキュラムは 2D のゲーム制作を基本としているので 3D をやろうと思うと割と独力で頑張らなくてはいけませんでした)

ということで本記事では、3D に興味はあるけど Cocos しか知らないんだよねーみたいな方に、Cocos でも 3D できるよ!!ということをお伝えできればと思っています。とはいえ、3Dゲームとしての出来はまだまだお遊びみたいなものですので、ベテランの諸先輩方におかれましては、ネイティブ・3D 初心者のどたばた奮闘記くらいの温度感で見守って頂ければ幸いです。笑

学んだこと

前提

以下の環境・要件で制作を行いました。

  • Cocos2d-x 3.6 (2015.07 当時の最新版)
  • C++
  • ロビー → ゲームプレイ → ロビーに戻る、を基本とするゲームサイクル
  • 60FPS を基本とし最低でも 30FPS を保つこと(iPhone 4S を目安とする)
  • ネットワークを介したリアルタイム同期によるマルチプレイヤーモードの実装
  • 適宜パーティクル表現を使い見た目にもこだわること

3Dモデルの表示と移動

まずはモデルを表示して動かしてみます。

Cocos2d-x は 3Dモデルを扱うクラスとして Sprite3D を提供しています。
いくつかのフォーマットに対応していますが、今回は Wavefront OBJ (*.obj) 形式を使うことにしました。人間が読むこともできるシンプルなフォーマットです。

3Dモデルのデータは、Cocos のテストに付属している 宇宙船っぽいオブジェクト や、TurboSquid で terrain や desert など適当に検索して用意しました。

これを画面上に表示するサンプルは下記の通りで、見慣れた Sprite と同じように扱うことができます。

Sprite3D に対する各種操作は、set*3D 系のメソッドで Sprite と似たように行うことができます。

このあたりまでは公式チュートリアルなどでも説明されていますね。

実際、3Dモデル(地形・飛行機・空など)を配置し、飛行機が一定速度で前進するようにし(飛行機ではなく地面を動かしています)、ドラッグ量に応じて進行方向を変える EventListener を仕込んでやれば、簡単にそれっぽいものを作ることができます。

モックを作ってみたところ

モックを作ってみたところ

ここからそれなりのゲームにしていくうえで、以下の機能がなかったり、不十分だったり、作ってみたかったりしたので自作していきました。なお前述のとおり、Cocos のバージョンは 3.6 です。

  • 衝突検知
  • シェーダ
  • バッチ描画
  • ステージ作成
  • 地形が無限に続くように見せる
  • 飛行機のプロペラアニメーション

それぞれ簡単に紹介していこうとおもいます。

衝突検知

正直、3Dモデルの衝突検知がサポートされていないなんて思っていませんでした・・・orz
「Cocos で 3D」というテーマを選んだことを若干後悔しかけました。

仕方ないのでとりあえず自作してみたら、振り返ってみるととてもよい勉強になったので結果おーらいでした。そんなお話です。

下準備

Cocos は、3Dモデルの頂点や面に関する情報はデフォルトでは iOS では関知せずOpenGL に渡すだけで破棄してしまいます。まずはこれらを破棄せず保持し、そこから扱いやすい形に取り出してあげるよう、Sprite3Dを拡張しました。結構力技で乗り切っています。。

なお、この辺りのコードを読んでいる時点で既に OpenGL? VertexBuffer? IndexBuffer? VBO? などと知らない子たちが沢山出てきたので、一旦立ち止まって以下の資料や書籍等を読んで勉強しました。

  • 床井研究室
    • 入門講座がたくさんまとまっていて本当に勉強になりました。
  • 明解WebGL
    • WebGLの本ではあるものの、本質は同じだし、ブラウザで気軽に試せるのでとても実用的でした。

続いて、取り出した頂点や面の情報を衝突検知に使えるように加工します。
ここでは「ゲームプログラマになる前に覚えておきたい技術」を参考に、kd木を生成するようにしました。kd木とその用途については次で説明します。なお 3D に関する基礎的な知識の多くは、この本を熟読して学びました。色んな面で「ちゃんとした」ゲームプログラミングの基礎が網羅されていて、本当に素晴らしい本でした。

衝突検知処理

実際の衝突検知処理は、毎フレーム飛行機の前方と下方に長さ20(プレイしながら微調整)の線分を出し、これと交差する地形モデルの面があるかどうかで判定します。1万数千の面と全て交差判定するわけにはいかないので、ここで予め作っておいた kd木を活用します。

kd木は、k次元空間を分割するデータ構造で、今回の場合は 3次元空間をXYZいずれかの軸に垂直な面で再帰的に分割しながら、衝突判定の対象である地形モデルの全ての面がいずれかの部分空間に格納されるような木を生成し、これによって 3次元空間の 2分探索をできるようにします。

使い方としては、現在の飛行機の座標(から出ている線分)が属する部分空間を2分探索で高速に割り出し、その周辺の部分空間に属する面についてのみ交差判定を行うことで、現実的な計算量で交差判定ができるようになります。

面と線分の交差判定処理についても「ゲームプログラマになる前に覚えておきたい技術」を参考に、クラメルの公式を用いて連立一次方程式を解く実装を行いました。

かくして衝突検知が完成し、今まで地面を突き抜けていた飛行機がちゃんと爆発した時の喜びと達成感はそれはそれは大きなものでした。

bomb

z-index がおかしいのはご愛嬌

実際ここが全工程のなかでもっとも大変だった箇所の1つで、同時に、頂点バッファとは・インデックスバッファとはなど 3Dモデルを扱う上での基礎的な知識、kd木などのアルゴリズムの知識、線形代数の知識(高校・大学数学の復習)とその応用など、もっとも多くのことを学べたところでした。衝突検知のコードはいわゆる写経ではあるのですが、1行1行計算の意味を理解しながら自分の言葉(コード、コメント)で作っていくことでとても良い勉強になりました。

なお、(見てないのですが) v3.7 で 3D physics が入ったようですね。

シェーダ

Cocos は基本的なライティング機能をサポートしていますが、せっかくなので勉強しながら自分で作ってみることにしました。

そもそもシェーダについて何も知らない状態からのスタートだったので、シェーダとはなんぞやというところから、行列演算による Model / View / Projection 変換などの基礎的な概念、頂点シェーダ・フラグメントシェーダの基本的な実装、ライティングの基礎などを、以下の資料や書籍で勉強しました。

  • (再掲) 床井研究室
    • シェーダについてもわかりやすい記事がたくさん。本当にありがたいです。
  • (再掲) 明解WebGL
    • こちらもシェーダについても基本的な実装が載っていてとても実用的でした。
  • DirectX 9 シェーダプログラミングブック
    • 豊富なサンプルと数学的にもコード的にも充実した解説で、理解を何段階も深められました。
    • 研修期間中に Amazon(.co.jp) で購入しました。アメリカ西海岸まで送料900円でたったの5日。便利な時代になったものですね。

Cocos でのシェーダ

Cocos のシェーダインタフェースは、シェーダプログラムを扱う GLProgram と、attribute や uniform 変数を扱う GLProgramState によって提供されています。

使い方としては、下記のように、シェーダプログラムを読み込み、GLProgramState にセットしたうえで、シェーダを適用したい Node にセットする流れになります。

その後、定型文的に頂点属性をセットすれば準備完了です。

uniform 変数に関しては、GLProgramState に対してデータ型にあわせたメソッドでセットしてあげたり、描画の度に呼ばれるコールバック(重そう)を用いてセットすることもできます。

フォグ

「フォグかけると手っ取り早くそれっぽくなるよ」と友人に言われて早速やってみました。

頂点シェーダで距離を計算し、フラグメントシェーダで適当な割合で白っぽい色を乗せて、遠くを霞ませています。

fog before

before


fog after

after

山肌

3Dモデルは全て TurboSquid で購入したものですが、10万頂点ちかくあるものを1万頂点程度に減らして使っているため、山肌の表現が残念な感じになりがちでした。
一部のモデルには法線マップが付いていたので、これを使って山肌の凸凹感を出してみました。

フラグメントシェーダで法線マップを用いて明るさを計算し、ポリゴンの粗さ以上の明暗をつけています。

normal before

before


normal after

after

飛行機

飛行機はこのゲームの主役なので出来ればかっこよく反射させてあげたいところです。
が、如何せん知識も経験も時間もないので、とりあえず基本的なフォンシェーディングを実装してみました。

phong before

before

phong after

after

と、入門から1週間程度でできることはこの程度が限界でした。ちまちまとパラメータや計算式を変えながらいい感じの光り方を探っていく作業は、見た目にダイレクトに反応があってとても楽しいものでしたが、学べば学ぶほどに、理想で思い描くかっこいい画像と手元のお遊びみたいな画像の差に愕然とし、3D の世界に生きるエンジニアの方々への尊敬の念を改めて強く抱いたのでした。

バッチ描画

コイン(空に浮いてるやつのこと)を集めるゲームなので、沢山のコインを浮かべなければならないわけで、現在1ステージに最大2000個ほど浮かんでいます。これらを描画するにあたり、2Dの Sprite だったらそれなりの最適化がされるようですが、Sprite3D にそのような機能は見当たらず、どうにかして一気に描画する仕組みが必要でした。

instanced drawing

やり方はいくつかあると思いますが、今回は研修ということで、対象が iPhone 4S 以上のみと限られていること、OpenGL の勉強にもちょうど良さそうなどの理由から、インスタンス描画法 を使ってみました。
これは OpenGL ES 2.0 では拡張機能としてサポートされており、対象機種がある程度限られてしまう代わりに、1回の描画コマンドで、座標等の属性のみが異なる同じモデルを大量に描画する事ができるようになります。

実装としては、Sprite3D を継承して Sprite3DBatchNode としています。
使い方は、Sprite3DBatchNode を継承したコインクラス(当初は球の予定だったので名前が Sphere です…) を一つだけ生成し、大量に表示する分は座標のみを追加していき、あとは普通に addChild するだけです。

描画は drawメソッドをオーバーライドし Cocos の カスタム描画コマンドを実装しています。
描画の度に各個体の座標情報と表示/非表示フラグを全部まとめて uniform変数に格納してシェーダに渡し、シェーダでは gl_InstanceIDEXT という変数に今描こうとしている個体のインデックスが入っているので、これを用いて uniform変数から座標と表示/非表示フラグを取り出して実際の座標を計算し描画します。
書きながら気づきましたが、座標は動かないので描画のたびに更新する必要は無いですね…

この手法で一度に何個まで描けるかは uniform 変数の容量に依存し、現在は実機で確認して 250個程度を想定して使っています。この値は glGet*関数で取得できるほか、OpenGL Extensions Viewer などで調べることができます。Apple の資料によると 128個までのようですね、、あれ…?

ともあれ、これで約2000個のコインを 8回の描画命令(見えない位置のものは描かれないので実際はもっと少ない)で描けるようになり、余裕で60FPS付近を達成できました。

performance

ステージ作成

3Dモデルの調整(頂点数減らしたり、ランディングギアのような見えない不要なパーツ消したり)はすべて Blender で行っていました。それなりに操作にクセのあるソフトだとは思いますが、チュートリアルやドキュメントも豊富なのでしばらく使っているうちに慣れるようになりました。
Blender の特長のひとつに、python スクリプトで様々な操作を行えることがありますが、ステージ作成はこれを活用して行いました。

具体的には、Blender 上でコインの位置を表すオブジェクトを適当に配置し、これらの座標を python スクリプトでCSVファイルに出力、アプリ側でこれを読み取り、間を適当に補間しながら大量のコインを配置するようにしました。ちょっとした微調整も視覚的に行えるので、比較的簡単にステージ作りの仕組みを整えられたと思っています。

stage

あきらめたこと

頂点シェーダによる地形のループ化

地形が無限に続いているように見せるために、地形の端が見えてきたらその先に反対側の端を描く必要があります。そこで、視界から外れて描く必要の無い領域を端の向こう側に描く処理を、頂点シェーダで実装しました。しかし、これはあくまで見えない部分を見える位置に移動させるものであり、地形モデルの大きさの都合上見えない部分が無い状態(高いところに行くと地形の全幅以上が見え、全幅の外側に表示するものがなくなる)がどうしても発生してしまうため諦めました。

仕方ないので、安直ですが地形モデルを3行3列に9つ並べることにしました。見えない部分は描画されないので頂点数的なパフォーマンスは問題ないのですが、地形以外にコインの座標や表示/非表示も全9つの地形モデルで共有する必要があり、かなり強引な実装で乗り切ってしまっているのが心残りです。頂点シェーダであれこれ計算して頂点を動かしてみるいい勉強にはなったので、もっと広大な地形モデルとかで試せたら再挑戦してみたいです。

duplicate

プロペラのアニメーション

これが最大の心残りです・・・飛んでる間ずっとプロペラ止まってるってどう見てもあほらしい。。orz

とはいえそれっぽく見えるプロペラアニメーションってきっと適当に回すだけではできないと思うので、知識も経験も時間もない中ではこれは無理だったなと素直に諦めています。

その他

マルチプレイモード(ちなみにシングルプレイヤーは2単語、マルチプレイヤーは1単語なのでキャメルケースにすると singlePlayer, multiplayer になるらしい。英語的には。)とかミニマップつけてみたりとか、楽しい機能は他にもあったけれど時間(?)がないので割愛させてくださいごめんなさい。 是非お近くの方と bluetooth 対戦してみてください!

感じたこと

ここでは少し技術的な内容から離れて、この研修を通して現地の学生たちと交流する中で感じたことを手短に書いてみたいと思います。個人的・主観的な内容です。

なお、ドキュメント読むくらいの英語力はありましたが、喋るのと(現地英語を)聞くのは絶望的にだめだめだったので、ノリで乗り切ってます。

現地の高校生・大学生について

アメリカの高校生・大学生とワークショップやディスカッション、普段の雑談などを通して話し合う中で、彼らも日本と同じように普通の高校生・大学生なんだな、と言葉にしてみれば当たり前のことを実感しました。

高校生は「プログラミング大好き!」から「なんとなく参加してみた」までいろんなモチベーションの人がいました。大学生になると、より将来を視野にいれ、就職先やインターンシップ先を迷いながら、実績作りを目標にする人を多くみかけました。

共通していたのは、プログラミングとゲームデザインに(あるいは遊びにも)精通し、各々のレベルに合わせて真剣にレクチャー・ディスカッションしてくれる魅力的な講師陣から、自分の求める技術や知識・経験を積極的に貪欲に得ようとする姿勢でした。
生徒と講師がとても良い関係で、定性的ではありますがこれが講座の成果を引き上げることに大きく寄与していると感じ、日本でも増えつつあるこのようなサービスにおいても、子どもたちの積極性を引き出せるようにすることが大切だし実際力を入れているんだろうなと思いました。

アプリ開発者養成学校について

現地の大学生の将来観について話を聞く中で、このような講座で実践的なプログラミングの経験や小さな実績を得て、その後インターンシップへとつなげることで、短中期的な講座ではなかなか難しい「仕事としてのプログラミング」へのステップアップを自然にできる流れができているなと感じました。

日本においても都市部ではこのような学校が広まりつつあるようで、プログラミングに対する門戸が開かれることはとても素晴らしいことだと思います。今までこういうことは企業が初心者向けインターンシップ等として行っていた印象がありましたが、入門から一人でアプリ開発ができるようになるところまでのカリキュラムがこのような学校から提供されることで、企業はより仕事へのステップアップとしてのインターンシップに注力できるのではないかと思います。

実践的な学びを得る場としての学校、職業体験としてのインターンシップ、そしてもちろん計算機科学をきちんと教育する場としての大学・専門学校等が密に連携することによって、理論と実践を併せ持つ情報技術者を育てる素晴らしい土壌ができるのではないかと思いました。

終わりに

ということで、長くなってしまいましたが、アメリカでの研修を通して学んだこと、感じたことを書いてみました。最後まで読んでくださりありがとうございました。

明日は同期の山田くんです。お楽しみに!