はじめに
本セクションではゲーム開発に必要なモジュールを実際に使い、「忍者ロワイヤル」から一部を抜き出したコードを使って、最終的には図1のようなサンプルゲームを作ることを目指したいと思います。
サンプルアプリを開発するために必要なSDKのダウンロード手順は連載第1回を参照ください。
Hello ngCore!
まず、最小のアプリケーションであるHello worldアプリを作成しましょう。Hello worldは、画面上に文字列を出力するだけの非常に簡単なプログラムです。構造がシンプルなので、利用するシステムの動作を確認するのにはぴったりです。
最小のngCoreコード
CやPerlで書かれたHello worldと、ngCoreを使って作るHello worldは、基本的に何も変わりません。ゴールは「“Hello world”という文字列を画面上で確認すること」です。ngCoreでは、画面に文字列を表示する方法は2つあります。
1つは、ログ出力関数です。ngCoreにはNgLogD、NgLogEなどのログ出力関数が用意されていて、文字列を引数に呼び出すと、コンソール上に文字列を出力します。2つめは、GL2.Textというクラスを使い、OpenGLのフレームバッファに文字列を描画する方法です。今回はこの2つの方法を試してみましょう。
全てのはじまりはmainから
それでは、LIST1のソースコードを見てみましょう。
//----- 起動時に呼ばれる function main() { NgLogD("--------------"); NgLogD("Hello ngCore !"); NgLogD("--------------"); }
NgLogDはログを出力する関数です。第1引数に出力させたい文字列を指定します。このコードを適当なディレクトリに配置して、ブラウザからアプリのURLを開いてみましょう。問題がなければ、JavaScriptコンソールにログが吐かれているはずです。図2のようなログが流れていればOKです。
このように、NgLogD関数を設定するだけで、とても簡単にログを出力できます。
new UI.GLViewではじまるOpenGLの世界
続いて、OpenGLを使ってテキストを表示してみましょう。LIST2のソースコードを見てください。
var UI = require('../NGCore/Client/UI').UI; var GL2 = require('../NGCore/Client/GL2').GL2; //----- 初期化処理 function initialize() { var helloText = new GL2.Text("Hello ngCore"); helloText.setPosition(UI.Window.getWidth() / 2, UI.Window.getHeight() / 2); helloText.setColor(0, 0, 0); GL2.Root.addChild(helloTtext); } //----- 起動時に呼ばれる function main() { NgLogD("--------------"); NgLogD("Hello ngCore !"); NgLogD("--------------"); // GLView を初期化する new UI.GLView({ frame: [0, 0, UI.Window.getWidth(), UI.Window.getHeight()], onLoad: initialize }).setActive(true); }
少し複雑になってきました。
new UI.GLView({
という行があります。これは一体何をしているのでしょうか? UI.GLViewはOpenGLを使うために必要なクラスです。ここではUI.GLViewのインスタンスを作成することで、OpenGLの初期化を行うことが可能となります。
コンストラクタはOpenGLの初期化に必要な情報をUI.GLViewにObject型として渡します。
frame: [0, 0, UI.Window.getWidth(), UI.Window.getHeight()], onLoad: initialize
frameには、OpenGLで描画する大きさを指定します。ここでは、UI.WindowのgetWidth、getHeightというメソッドで、画面の全てを描画に使うように設定しています。
onLoadには、OpenGLの初期化処理が終了したタイミングで呼び出してほしい関数を指定します。ここでは、initializeという初期化のための関数を指定しています。
initializeの中ではGL2.Textのインスタンスを作成し、表示させる座標・文字列の色を指定した後に、そのインスタンスをGL2.RootのaddChildというメソッドに渡しています。
ngCoreではシーングラフをベースとした描画を行います。シーングラフとは描画に必要なオブジェクトを木構造で表現するものです。GL2.Rootは名前の通り、描画の根元です。この根元にGL2.Textのインスタンスを追加すると、そのインスタンスはシーングラフの一部になり、画面に描画されることになります。
では、実行してみましょう。図3のように、ブラウザのページ部分とJavaScriptコンソールに「Hello ngCore」という文字列が出力されたでしょうか?
タッチを使って簡単なアプリを作る
スマートフォンのアプリならではの操作といえば、みなさん何を思い浮かべるでしょうか。タップ、フリック、ピンチイン・ピンチアウト、スワイプなど、たくさんあります。ここでは一番基本となる「タップ」を使って、簡単なアプリを作成します。画面をタップすると、タップした位置に指紋が表示される「指紋ぺたぺたアプリ」を作ってみましょう(図4)。
まずソースコード全体を見てください(LIST3)。次から個々の部分を説明していきます。
var GL2 = require('../NGCore/Client/GL2').GL2; var UI = require('../NGCore/Client/UI').UI; var Util = require('./lib/Util').Util; //----- 初期化処理 function initialize() { //----- var width = UI.Window.getWidth(); var height = UI.Window.getHeight(); //----- var bg = Util.makePrimitive(0, 0, width, height, [1.0, 1.0, 1.0]); GL2.Root.addChild(bg); //----- var target = new GL2.TouchTarget(); target.setSize([width, height]); GL2.Root.addChild(target); //----- var listener = new Core.MessageListener(); listener.onTouch = function(touch) { var fp = new GL2.Sprite(); fp.setImage("./Content/fingerprint.png", [80, 103], [0.5, 0.5]); fp.setPosition(touch.getPosition()); GL2.Root.addChild(fp); }; target.getTouchEmitter().addListener(listener, listener.onTouch); } //----- 起動時に呼ばれる function main() { new UI.GLView({ frame: [0, 0, UI.Window.getWidth(), UI.Window.getHeight()], onLoad: initialize }).setActive(true); }
TouchTarget
main関数は、Hello worldと同じです。実際のアプリに関係するコードはinitializeにあります。
var bg = Util.makePrimitive( 0, 0, width, height, [1.0, 1.0, 1.0]);
この行に関しては、今は本質ではないのでスキップします。ここでは、背景を白く塗りつぶしている、とだけ覚えておいてください。次を見てみましょう。
var target = new GL2.TouchTarget(); target.setSize([width, height]); GL2.Root.addChild(target);
GL2.TouchTargetはタッチを検出するためのクラスで、タッチの範囲を設定できます。ここでは画面全体がタッチの範囲となるようにsetSizeメソッドで値を設定しています。GL2.TouchTargetも描画オブジェクトの一種です。addChildメソッドでシーングラフに追加します。
Core.MessageListenerとエミッター
実際にユーザーがタッチ操作を行った際、その操作をハンドリングする必要があります。そこで使用するのがCore.MessageListenerというクラスです。名前の通り、このクラスはイベントリスナーとして使用します。
var listener = new Core.MessageListener(); listener.onTouch = function(touch) { // ここにタッチされた時の処理を書く };
Core.MessageListenerを使う方法は2通りあります。1つは、直接Core.MessageListenerをインスタンス化して使う方法です。この方法では生成したインスタンスのプロパティに関数を代入して、ハンドラを定義します。もう1つは、Core.MessageListenerのサブクラスを定義して、そのメソッドをハンドラとする方法です。
ここでは、前者のやり方で実装しています。まず、newCore.MessageListener()でインスタンスを生成し、その後、onTouchというプロパティに無名関数を登録しています。
さて、このままではまだイベントをハンドリングできません。イベントが発生した際にそのイベントをイベントリスナーに伝える存在が必要です。ngCoreではそれを“エミッター“と呼んでいます。では、エミッターにイベントを伝えてもらうため、イベントリスナーを登録しましょう。
target.getTouchEmitter(). addListener(listener, listener.onTouch);
エミッターは先ほど生成したGL2.TouchTargetから取得できます。getTouchEmitterというメソッドで取得したエミッターに対してaddListenerメソッドを呼び出します。addListenerの第1引数はリスナー自身、第2引数はイベントをハンドリングするメソッドです。
これでタッチをハンドリングする準備は整いました。次は“指紋”を描画してみましょう。
GL2.Spriteで画像を表示
画面に画像を描画するにはGL2.Spriteを使用します。
var fp = new GL2.Sprite(); fp.setImage("./Content/fingerprint.png", [80, 103], [0.5, 0.5]); fp.setPosition(touch.getPosition()); GL2.Root.addChild(fp);
インスタンスを生成した後、画像へのパスと位置を設定し、シーングラフに追加しています。ひとつずつ見ていきましょう。setImageのシグネチャはLIST4のようになります。
setImage(image, size, anchor, uvs) Parameters: {String} image The directory path to an image resource. {Core.Size} size Optional The size of the image to display (in pixels). {Core.Point} anchor Optional The anchor coordinates that indicate the image center in the animation. {Core.Rect} uvs Optional The UV coordinates used to specify the subset of an image. Returns: This function returns this to support method invocation chaining.
ここでは、anchor に [0.5, 0.5] を指定しています。これは、タッチしたポイントに指紋画像の中心がきてほしいからです。
次に、画像の位置を指定します。タッチイベントをハンドリングした際にGL2.Touchのインスタンスが渡ってきています。このtouchオブジェクトは、タッチイベントが発生したときの情報が入っています。
listener.onTouch = function(touch) {
getPositionメソッドでタッチイベントが発生した位置を取得します。この位置をそのまま指紋のGL2.Spriteに設定しましょう。
fp.setPosition(touch.getPosition());
最後に、シーングラフに追加して完成です。
GL2.Root.addChild(fp);
では、実行してみましょう。画面をタップすると、その位置に指紋が表示されます。次は、いよいよ忍者ロワイヤルのソースから抜き出した“剣の軌跡”の表示を実装します。
剣の軌跡を表示しよう
DeNAがスマートフォン向けに提供しているゲームに「忍者ロワイヤル」というものがあります。「忍者ロワイヤル」はスマートフォンならではの操作がウリのアクションRPGで、特にバトルはスワイプやタップを駆使して遊ぶものとなっています。今回はそのバトルの中の一要素である“剣の軌跡”の表示を実装します。今まで説明してきたタッチの仕組みに加え、GL2.Primitive Core.UpdateEmitterというクラスを使います。少し複雑になりますが、1つずつ見ていきましょう。
LIST5のソースコードを見てみましょう。
var GL2 = require('../NGCore/Client/GL2').GL2; var UI = require('../NGCore/Client/UI').UI; var Device = require('../NGCore/Client/Device').Device; var Util = require('./lib/Util').Util; var Trajectory = require('./entity/Trajectory').Trajectory; //----- 初期化処理 function initialize() { //----- 画面の向きを横に Device.OrientationEmitter.setInterfaceOrientation( Device.OrientationEmitter.Orientation.LandscapeLeft); //----- var width = UI.Window.getWidth(); var height = UI.Window.getHeight(); //----- var bg = Util.makePrimitive(0, 0, height, width, [0.0, 0.0, 0.0]); GL2.Root.addChild(bg); //----- var target = new GL2.TouchTarget(); target.setSize([height, width]); GL2.Root.addChild(target); //----- 剣の軌跡 var trajectory = new Trajectory(target.getTouchEmitter()); bg.addChild( this._trajectory._node ); } //----- 起動時に呼ばれる function main() { new UI.GLView({ frame: [0, 0, UI.Window.getWidth(), UI.Window.getHeight()], onLoad: initialize }).setActive(true); }
mainは今までと同じ内容ですね。initializeの中を見ていきましょう。
//----- 画面の向きを横に Device.OrientationEmitter.setInterfaceOrientation( Device.OrientationEmitter.Orientation.LandscapeLeft);
新しいクラスDevice.OrientationEmitterが出てきました。Device.OrientationEmitterは端末の向きの変化を通知したり、端末の向きを設定するためのクラスです。ここではsetInterfaceOrientationメソッドを呼び出し、横向きの描画を行うように設定しています。これで横長の画面でアプリが動くことになります。
//----- 剣の軌跡 var trajectory = new Trajectory(target.getTouchEmitter()); bg.addChild( this._trajectory._node );
次に出てきた新しいクラスはTrajectoryです。これは「忍者ロワイヤル」で切り出した“剣の軌跡を表示する”ためのクラスです。ここではコンストラクタにTouchEmitterを引き渡しています。
Trajectoryクラスの中身は、LIST6のようになります。今まで見てきたものより複雑なコードですが、1つずつ見ていきましょう。
var Core = require('../../NGCore/Client/Core').Core; var GL2 = require('../../NGCore/Client/GL2').GL2; var utils = require('../../DnLib/Dn/utils').utils; exports.Trajectory = Core.MessageListener.subclass({ initialize: function(touchEmitter, length, colorHead, colorTail) { //----- this._emitter = touchEmitter; this._maxLength = length || 20; this._colorHead = colorHead || [1, 1, 0]; this._colorTail = colorTail || [1, 0, 0]; this._bladePower = 1.0; // 1.0 が MAX. 刀を折り返すたびに威力が減って軌跡が細くなる this._vertexList = []; this._isMoving = false; //----- this._emitter.addListener( this, this.onTouch ); //----- this._node = new GL2.Node(); //----- var p = new GL2.Primitive(); p.setType( GL2.Primitive.Type.TriangleStrip ); for (var i=0; i < this._maxLength * 2; i++) { var v = new GL2.Primitive.Vertex([0, 0], [0, 0], [1, 0, 0]); p.pushVertex(v); } this._prim = p; this._node.addChild( this._prim ); this._node.setDepth( 1 ); //----- Core.UpdateEmitter.addListener( this, this.onUpdate ); }, //-------------------------------------------------------------------------- destroy: function() { Core.UpdateEmitter.removeListener(this); this._emitter.removeListener(this); this._prim.destroy(); }, //------------------------------------------------------------------------- getVertexList: function() { return this._vertexList; }, getNode: function() { return this._node; }, //-------------------------------------------------------------------------- onUpdate: function( delta ) { var v = this._vertexList; var p = this._prim; if (v.length >= 2) { p.setVisible( true ); var bladeWidth = 10 * this._bladePower; for (var i=0; i < v.length - 1; i++) { var grad = 1 - (i / v.length); var r = (this._colorHead[0] * grad) + (this._colorTail[0] * (1-grad)); var g = (this._colorHead[1] * grad) + (this._colorTail[1] * (1-grad)); var b = (this._colorHead[2] * grad) + (this._colorTail[2] * (1-grad)); var w = (i < 4) ? (i * 0.22) : g; var bw = w * bladeWidth; var vx = v[i ].x; var vpx = v[i+1].x; var vy = v[i ].y; var vpy = v[i+1].y; //----- 進行方向と直角に軌跡の太さの方向を算出 if (! v[i].dx) { var theta = Math.atan2( vy - vpy, vx - vpx ) * 180 / Math.PI; v[i].dx = utils.cos( theta + 90 ); v[i].dy = utils.sin( theta + 90 ); } p.setVertex( i*2+0, new GL2.Primitive.Vertex( [v[i].x - v[i].dx * bw, v[i].y - v[i].dy * bw], [0, 0], [r, g, b])); p.setVertex( i*2+1, new GL2.Primitive.Vertex( [v[i].x + v[i].dx * bw, v[i].y + v[i].dy * bw], [0, 0], [r, g, b])); } //----- 余った頂点は末尾にまとめる var lastVertex = new GL2.Primitive.Vertex( [v[ v.length - 1 ].x, v[ v.length - 1 ].y], [0, 0], [0, 0, 0]); for (var i = v.length - 1; i < this._maxLength; i++) { p.setVertex( i*2+0, lastVertex ); p.setVertex( i*2+1, lastVertex ); } } else { p.setVisible(false); } if (v.length > 0 && !this._isMoving) { v.pop(); } else { this._isMoving = false; } }, //-------------------------------------------------------------------------- onTouch: function( touch ) { var x = touch.getPosition().getX(); var y = touch.getPosition().getY(); var vList = this._vertexList; this._isMoving = false; switch (touch.getAction()) { case touch.Action.Start: break; case touch.Action.End: break; case touch.Action.Move: vList.unshift( {x:x, y:y} ); this._isMoving = true; break; default: break; } if (vList.length > this._maxLength) { vList.pop(); } return touch.getAction() === touch.Action.Start; } });
ngCoreでクラスを定義
ぱっと見て気づくのは、今まで書いてきた関数ベースのソースコードとは違うということです。今回は“剣の軌跡を表示する”という部分をクラス化してあります。
exports.Trajectory = Core.MessageListener.subclass({
この行でTrajectoryクラスを定義しています。ここでは、TrajectoryクラスはCore.MessageListenerクラスを継承します、ということを宣言しています。ngCoreでは独自クラスシステムを構築していて、あるクラスを継承する場合、そのクラスのsubclassメソッドを呼び出すことで継承を実現します。subclassの引数には新しいクラスの定義そのものをオブジェクト型で渡します。
ngCoreのクラスシステムには、いくつか決まりがあります。initializeという名前のメソッドはコンストラクタとして扱われます。initializeの中身を見ていきましょう。
this._emitter.addListener( this, this.onTouch );
これは、先ほど紹介したタッチのハンドリングです。エミッターにリスナーを登録しています。TrajectoryクラスはCore.MessageListenerを継承しているので、自身をリスナーとして登録できます。ここではonTouchメソッドをハンドラとして登録しています。これでTrajectoryクラスはアプリケーション上でタッチが発生した場合、その通知を受け取ることができます。
GL2.Primitiveで多角形を描画する
LIST7は、描画に関する部分です。
var p = new GL2.Primitive(); p.setType( GL2.Primitive.Type.TriangleStrip ); for (var i=0; i < this._maxLength * 2; i++) { var v = new GL2.Primitive.Vertex([0, 0], [0, 0], [1, 0, 0]); p.pushVertex(v); } this._prim = p; this._node.addChild( this._prim );
新しいクラスが2つも出てきました。GL2.PrimitiveとGL2.Primitive.Vertexです。この2つのクラスは“剣の軌跡を表示する”クラスの中で最も重要なクラスになります。
GL2.Primitiveはポリゴンを描画するためのクラスです。OpenGLの低レベルな部分を使い、2Dの点(Vertex)のリストをもとに画面にポリゴンを描画します。GL2.Spriteでは表現できない特殊な形状や部分的な色合いの変化を実現できます。“剣の軌跡を表示する”というような、矩形ではない形をリアルタイムで、スムーズに動かしたいという時にはぴったりのクラスと言えます。
使い方はとても簡単です。pushVertexというメソッドが用意されているので、このメソッドに点(Vertex)の情報をどんどん追加していくだけです。
var v = new GL2.Primitive.Vertex([0, 0], [0, 0], [1, 0, 0]); p.pushVertex(v);
この時に、点の情報を表すクラスとしてGL2.Primitive.Vertexが用意されています。LIST8はGL2.Primitive.Vertexのコンストラクタのシグネチャです。
GL2.Primitive.Vertex(position, uv, color) The default constructor. Parameters: {Core.Vector|float[2]} position A location. {Core.Vector|float[2]} uv A texture coordinate. {Core.Color|float[3]} color An RGB color triplet.
このVertexのリストは何をもとに変化していくのでしょうか? そう、ユーザーのタッチ操作によって変化していきます。次は先ほどタッチエミッターに登録したonTouchの中身を見てみましょう(LIST9)。
onTouch: function( touch ) { var x = touch.getPosition().getX(); var y = touch.getPosition().getY(); this._isMoving = false; switch (touch.getAction()) { case touch.Action.Move: this._vertexList.unshift( {x:x, y:y} ); this._isMoving = true; break; case touch.Action.Start: case touch.Action.End: default: break; } if (this._vertexList.length > this._maxLength) { this._vertexList.pop(); } return touch.getAction() === touch.Action.Start; }
さて、onTouchの中では何をやっているのでしょうか? switch文の中身がメインの処理です。注目してみましょう。
エミッターから送られてきたtouchインスタンスのgetActionメソッドの返り値でスイッチしています。このメソッドはタッチがどのような状態で行われたのかを値として返します。ここではその値がAction.Moveだった時に処理を行っています。Action.Moveは画面上で指をスワイプするような操作をした場合に使われます。
this._vertexList.unshift( {x:x, y:y} );
処理の内容はとても単純です。this._vertexはコンストラクタで初期化している配列ですが、この配列にタッチされた位置を追加しているだけです。配列の中身を変更しているだけなので、まだ描画に影響はありません。実際に描画に影響のある処理を行っている部分は、Trajactoryクラスの中にあるonUpdateメソッドです。
UpdateEmitterとは
ここで一度コンストラクタに戻ってコードを見てみましょう。
Core.UpdateEmitter.addListener( this, this.onUpdate );
この行でonUpdateへの参照が使われています。新しいエミッターがでてきました。このエミッターは何をするものなのでしょうか。
Core.UpdateEmitterクラスは登録されたリスナーに対して1フレームごとに通知を行うクラスです。つまり、Trajectoryクラスはこのエミッターに登録しているので、毎フレーム通知を受け取ることになります。ここでは、onUpdateメソッドがそのハンドラとなります。
onUpdateの中ではonTouchで更新したthis._vertexListをもとに描画するポリゴンの更新を行っています(LIST10)。
var v = this._vertexList; var p = this._prim; ~省略~ p.setVertex( i*2+0, new GL2.Primitive.Vertex( [v[i].x - v[i].dx * bw, v[i].y - v[i].dy * bw], [0, 0], [r, g, b])); p.setVertex( i*2+1, new GL2.Primitive.Vertex( [v[i].x + v[i].dx * bw, v[i].y + v[i].dy * bw], [0, 0], [r, g, b]));
setVertexは最初にpushVertexで登録したGL2.Primitive.Vertexを更新するためのメソッドです。様々な計算をしていますが、this._vertexListの内容をもとにsetVertexするというのが本質になります。setVertexでVertexのリストを更新することにより、画面に描画される内容が変化します。サンプルを用意したので確認してください。
ゲームを組み立てる
今回紹介したモジュールやTrajectoryクラスを使用し、サンプルゲームを作成しました(図5)。
サンプルの構成
-
Sample1.zip
Hello ngCore
-
Sample2.zip
指紋ぺたぺたアプリ
-
Sample3.zip
“剣の軌跡を表示する”サンプル
-
Sample4.zip
「忍者ロワイヤル」をアクションパートをもとにしたサンプルゲーム
最後に
駆け足でしたが、説明は以上です。
今回はHello worldからはじまり、タッチ、スプライト、ポリゴンの描画、一定時間ごとにアップデートされるコードの説明までできました。サンプルゲームのようなアプリケーションが非常に少ないコードで動くことがお解かりいただけたと思います。本セクションで説明したモジュール以外にもngCoreには有用なモジュールがまだまだあります。Network、Social、Bankなど、ソーシャルゲームを開発する際に必要なものは全て揃っていると言ってよいでしょう。
これから、スマートフォンで動くソーシャルゲームの分野はますます伸びていくと我々は考えています。我々と一緒にこの分野を盛り上げていきませんか? 本記事がみなさんのngCoreを理解する際のお役に立てれば幸いです。ありがとうございました。
ngCoreにて開発された忍者アクションRPG。タップで手裏剣を投げる、スラッシュで敵を斬るなどスマフォならではの操作をゲーム内に組み込み、忍者としてミッションを実行し、ボスを倒して世界に散らばるお宝を集め、忍者としての格を上げていくゲームです(※iOS/Androidの両OSに対応しています)。