Cocos2d-JS 触り始めた5

前回の作業中ホーミングミッソーを試しにたくさん飛ばしてみると、一時的にフレームレートが低下し、それに伴いゲームの進行速度が遅くなることがあった。特にフレームスキップなどの実装をしていないしこれは想定通りの動作ではあるものの、update メソッドの第一引数に渡されてくる値は前回のフレームからの経過時間(秒)なので、これを適切に扱うようにすればフレーム落ちも加味して計算できる。



これによって少しぐらいのフレームレート低下なら何もしないよりはマシに振る舞うことができる可能性がある。ただゲームによってはフレームレート低下でゲームの進行速度が遅くなるのも許容できることもあるだろうし(例えばターン制のゲームなど時間に追われていない場合とか)、実装の面倒臭さも合わせて上手い落としを見つけるのがいい。
diff --git a/src/app.js b/src/app.js
index e0d3a27..03ccee6 100644
--- a/src/app.js
+++ b/src/app.js
@@ -119,8 +119,11 @@ var GameOverLayer = cc.Layer.extend({

 var MyShipLayer = cc.Layer.extend({
     sprite:null,
-    vy: 3,
+    vy: 140,
     barrier:false,
+    d:0,
+    origY: 0,
+    G: -280,
     ctor:function () {
         this._super();
         this.sprite = cc.Sprite.create(res.nc_png);
@@ -132,11 +135,12 @@ var MyShipLayer = cc.Layer.extend({
         return true;
     },
     update:function(dt) {
-        this.y += this.vy;
-        this.vy -= 0.1;
+      var d = this.d += dt;
+      this.y = 0.5*this.G*d*d + this.vy*d + this.origY;
     },
     jump:function() {
-      this.vy = 3;
+      this.d = 0;
+      this.origY = this.y;
     }
 });

@@ -157,8 +161,8 @@ var BlockLayer = cc.Layer.extend({
         return true;
     },
     update:function(dt) {
-        this.x += this.vx;
-        this.y += this.vy;
+        this.x += this.vx * dt;
+        this.y += this.vy * dt;
     }
 });

@@ -167,13 +171,17 @@ var HomingMissoLayer = cc.Layer.extend({
     particle:null,
     d:null,
     a:0,
+    acos:0,
+    asin:0,
     vx:0,
     vy:0,
-    speed:6,
+    speed:300,
+    trackInterval: 0.032,
     angle:0.3,
     ctor:function () {
       this._super();
       this.scheduleUpdate();
+      this.schedule(this.track, this.trackInterval);
       this.d = cc.DrawNode.create();
       this.d.drawRect(cc.p(-8, -2), cc.p(8, 2), cc.color(255,255,255,255));
       this.addChild(this.d, 0);
@@ -184,26 +192,29 @@ var HomingMissoLayer = cc.Layer.extend({
       this.addChild(this.particle);
       return true;
     },
-    update:function(dt) {
+    track:function(dt) {
       var bb = this.target.getBoundingBox();
       var dx = this.d.x, dy = this.d.y;
       var vx = this.vx, vy = this.vy;
       var tx = bb.x + bb.width*0.5 - dx, ty = bb.y + bb.height*0.5 - dy;
-      var tl = Math.sqrt(tx*tx+ty*ty), a = this.a;
-      if ((tx*vx+ty*vy)/(tl*Math.sqrt(vx*vx+vy*vy)) < 0) {
-        a += this.angle;
+      if ((tx*vx+ty*vy)/(vx*vx+vy*vy) < 0) {
+        this.a += this.angle;
       } else {
-        a += this.angle * (vx*ty - vy*tx)/tl/this.speed;
+        this.a += this.angle * (vx*ty - vy*tx)/Math.sqrt(tx*tx+ty*ty)/this.speed;
       }
-      this.a = a;
-      var cosa = Math.cos(a), sina = Math.sin(a), r = a*180/Math.PI;
-      this.vx = vx = this.speed * cosa;
-      this.vy = vy = this.speed * sina;
-      this.d.x = dx += vx;
-      this.d.y = dy += vy;
+      this.acos = Math.cos(this.a);
+      this.asin = Math.sin(this.a);
+      this.vx = this.speed * this.acos;
+      this.vy = this.speed * this.asin;
+      var r = this.a*180/Math.PI;
       this.d.rotation = -r;
-      this.particle.setSourcePosition(cc.p(dx + -6 * cosa, dy + -6 * sina));
       this.particle.angle = 180+r;
+    },
+    update:function(dt) {
+      var x = this.vx * dt + this.d.x, y = this.vy * dt + this.d.y;
+      this.d.x = x;
+      this.d.y = y;
+      this.particle.setSourcePosition(cc.p(x + -6 * this.acos, y + -6 * this.asin));
     }
 });

@@ -222,6 +233,7 @@ var HelloWorldScene = cc.Scene.extend({
           x: 40,
           y: size.height / 2
         });
+        this.myShip.jump();

         this.blocks = [];
         for (var i = 0, bl; i < 16; ++i) {
@@ -238,8 +250,8 @@ var HelloWorldScene = cc.Scene.extend({
           bl.attr({
             x: size.width+Math.random()*size.width,
             y: -bl.height+Math.random()*(size.height+bl.height),
-            vx: -0.5-Math.random()*3,
-            vy: -0.25+Math.random()*0.5
+            vx: -60-Math.random()*60,
+            vy: -2.5+Math.random()*5
           });
           this.addChild(bl);
           this.blocks.push(bl);
@@ -298,8 +310,8 @@ var HelloWorldScene = cc.Scene.extend({
           bl.attr({
             x: size.width,
             y: -bl.height+Math.random()*(size.height+bl.height),
-            vx: -0.5-Math.random()*3,
-            vy: -0.25+Math.random()*0.5
+            vx: -60-Math.random()*60,
+            vy: -2.5+Math.random()*5
           });
         }
       }
なお面倒だったので速度などは前と全く同じではなく適当に似た振る舞いになるように合わせているだけ。今までは例えばブロックは毎フレーム this.vx を足していたが変更後は this.vx*dt になっていて、dt は秒単位なので 0.0167 などの小さい値が入ってくる。前は60フレーム想定の速度で書いていたので、this.vx/0.0167 ぐらいの値を指定すれば大体同じくらいの速度で動くことにはなるとは思う。

まあでもこの手のものは今どきっぽい実装をするなら物理エンジンに丸投げしてしまえばそれで済むことも多いんだろうし予め最適化されたエンジンが使えるので、下手に自分で作るより頑張らないという選択肢も悪くなさそうだと思った。作るものの複雑さ次第かも。

Cocos Code IDE を使ってみる

つい先日 Cocos Code IDE というものがリリースされたらしい。

これは Eclipse 系列のよくある開発環境のようなのだが、どうやら Cocos2d-x の Windows 用コンパイル済みバイナリが付属していて手軽に実行して確認できる上、実行中にソースコードを書き換えるとすぐに反映して実行しなおしてくれた。

既存のプロジェクトを Cocos Code IDE で使える状態に移行するには新規プロジェクトを作って必要なファイルを突っ込めばいいようで、Cocos2d-x の新しいバージョンも出たっぽいし試しに使ってみることにした。作業中のリポジトリにはランタイムなども含んでいるのでこれによる変更ファイル数は相当多いものの基本的な振る舞いに変更はないのでここでの diff は省略する。


デバッグボタンを押すとほとんどラグなしですぐにウィンドウが出て動き始めるのでテストまでのストレスが少ない。Haxe で NME で開発していた時、Flash が動き出すのも丁度こんな感じだったので作業しやすい(とはいえ元々ブラウザをリロードするだけで試せていたので前もそんなには遅くなかった)。

今後はブラウザ上でのテストも Run In Browser ボタンでいける。

CocoStudio UI Editor を使ってスコア表示機能を作る

今度は CocoStudio に付属している UI Editor を使ってインターフェイスを作り、それを利用してスコア表示をしてみる。

単に画面にスコアを出すという目的だけを達成するなら最初から HelloWorldLayer に存在しているラベルを応用するなどでできるとは思うものの、デザインに関わる部分は長い目で見ればプログラマーとデザイナーで分業できる形になっていたほうがいい。分業相手は居ないけど。

ビットマップフォントを作る

スコアには点数しか出さないし、多種多様な実行環境に依存せず使えるように数字が格納されたビットマップフォントがあるといい。これには ShoeBox というツールが使えるらしいので、試しに M+ フォントで数字を書いただけの画像を適当に作ってみた。


これを ShoeBox に投げつけていくつか設定をするだけで余白を元に文字の切り分けポイントをは別して簡単にビットマップフォントは生成できたのだが、どうやら Cocos2d-x にはプロポーショナルフォントとして解釈されてしまうようだ(まあ描画したフォント自体もプロポーショナルフォントなので正しくはあるのだが……)。どんどんカウントアップされている点数が左右にガチャガチャ動くのは辛いので、モノスペースフォントになるように回避策を考える必要が出てきた。

自分の手で fnt ファイルを編集するとか生成用ツールを作るとかアプローチは色々ありそうではあるものの、Littera というツールでモノスペースフォントで吐けば綺麗に出ることがわかったので、手間も掛からないし今回はこの方法で行くことにした。

数字を書いて適当にポチポチやって保存すると zip ファイルで書き出される。

UI Editor でインターフェイスを組み立てる

スコア表示周りの素材を作り、その上に作成したビットマップフォントでスコアを表示させるような感じに素材を作った。UI Editor のインターフェイスは若干独特な部分があって操作性が微妙に良くない感じもあるけど、試行錯誤しているうちに目的の形に落ち着いた。


なお作った素材がダサいことについては重々承知しているのでこれ以上の追撃は必要ない。
作成したインターフェイスはエクスポートで res ディレクトリ内にサブディレクトリを作って配置した。

プログラム上でインターフェイスを読み込む

早速作った素材を読み込むレイヤーを追加し、そのレイヤーを HelloWorldScene に追加する。インターフェイス用の JSON ファイルは project.json の modules"cocostudio" を追加すると、ccs.uiReader.widgetFromJsonFile というメソッドを通じて読み込むことができるようになるらしい。
diff --git a/project.json b/project.json
index f0c0298..b1cc345 100644
--- a/project.json
+++ b/project.json
@@ -8,7 +8,7 @@
     "renderMode" : 2,
     "engineDir":"frameworks/cocos2d-html5",
 
-    "modules" : ["menus", "cocos2d"],
+    "modules" : ["menus", "cocos2d", "cocostudio"],
 
     "jsList" : [
         "src/resource.js",
diff --git a/res/main/font.fnt b/res/main/font.fnt
new file mode 100644
index 0000000..e0b6c39
--- /dev/null
+++ b/res/main/font.fnt
@@ -0,0 +1,15 @@
+info face=font size=30 bold=1 italic=0 charset= unicode= stretchH=100 smooth=1 aa=1 padding=5,5,5,5 spacing=0,0 outline=0
+common lineHeight=45 base=32 scaleW=128 scaleH=64 pages=1 packed=0
+page id=0 file="font.png"
+chars count=10
+char id=48 x=5 y=5 width=13 height=23 xoffset=1 yoffset=0 xadvance=15 page=0 chnl=15
+char id=49 x=23 y=5 width=10 height=22 xoffset=2 yoffset=0 xadvance=15 page=0 chnl=15
+char id=50 x=38 y=5 width=12 height=23 xoffset=2 yoffset=0 xadvance=15 page=0 chnl=15
+char id=51 x=55 y=5 width=12 height=23 xoffset=2 yoffset=0 xadvance=15 page=0 chnl=15
+char id=52 x=72 y=5 width=14 height=22 xoffset=1 yoffset=0 xadvance=15 page=0 chnl=15
+char id=53 x=91 y=5 width=12 height=23 xoffset=2 yoffset=0 xadvance=15 page=0 chnl=15
+char id=54 x=72 y=32 width=13 height=23 xoffset=1 yoffset=0 xadvance=15 page=0 chnl=15
+char id=55 x=5 y=33 width=12 height=22 xoffset=2 yoffset=0 xadvance=15 page=0 chnl=15
+char id=56 x=22 y=33 width=14 height=23 xoffset=1 yoffset=0 xadvance=15 page=0 chnl=15
+char id=57 x=41 y=33 width=13 height=23 xoffset=1 yoffset=0 xadvance=15 page=0 chnl=15
+char id=32 x=0 y=0 width=0 height=0 xoffset=1 yoffset=0 xadvance=15 page=0 chnl=15
\ No newline at end of file
diff --git a/res/main/font.png b/res/main/font.png
new file mode 100644
index 0000000..a30c2e2
Binary files /dev/null and b/res/main/font.png differ
diff --git a/res/main/main.json b/res/main/main.json
new file mode 100644
index 0000000..e512a7e
--- /dev/null
+++ b/res/main/main.json
@@ -0,0 +1,183 @@
+{
+  "classname": null,
+  "name": null,
+  "animation": {
+    "classname": null,
+    "name": "AnimationManager",
+    "actionlist": []
+  },
+  "dataScale": 1,
+  "designHeight": 450,
+  "designWidth": 800,
+  "textures": [],
+  "texturesPng": [
+    "score.png"
+  ],
+  "version": "1.5.0.0",
+  "widgetTree": {
+    "classname": "Panel",
+    "name": null,
+    "children": [
+      {
+        "classname": "ImageView",
+        "name": null,
+        "children": [],
+        "options": {
+          "__type": "ImageViewSurrogate:#EditorCommon.JsonModel.Component.GUI",
+          "classname": "ImageView",
+          "name": "Image_6",
+          "ZOrder": 0,
+          "actiontag": 45502051,
+          "anchorPointX": 1,
+          "anchorPointY": 1,
+          "classType": "CocoStudio.EngineAdapterWrap.CSImageView",
+          "colorB": 255,
+          "colorG": 255,
+          "colorR": 255,
+          "customProperty": "",
+          "flipX": false,
+          "flipY": false,
+          "height": 34,
+          "ignoreSize": true,
+          "layoutParameter": null,
+          "opacity": 255,
+          "positionPercentX": 1.0025,
+          "positionPercentY": 1.00444448,
+          "positionType": 1,
+          "rotation": 0,
+          "scaleX": 1,
+          "scaleY": 1,
+          "sizePercentX": 0.31875,
+          "sizePercentY": 0.0755555555,
+          "sizeType": 0,
+          "tag": 10,
+          "touchAble": false,
+          "useMergedTexture": false,
+          "visible": true,
+          "width": 255,
+          "x": 802,
+          "y": 452,
+          "capInsetsHeight": 1,
+          "capInsetsWidth": 1,
+          "capInsetsX": 0,
+          "capInsetsY": 0,
+          "fileName": null,
+          "fileNameData": {
+            "path": "score.png",
+            "plistFile": "",
+            "resourceType": 0
+          },
+          "scale9Enable": false,
+          "scale9Height": 34,
+          "scale9Width": 255
+        }
+      },
+      {
+        "classname": "LabelBMFont",
+        "name": null,
+        "children": [],
+        "options": {
+          "__type": "LabelBMFontSurrogate:#EditorCommon.JsonModel.Component.GUI",
+          "classname": "LabelBMFont",
+          "name": "scoreValue",
+          "ZOrder": 0,
+          "actiontag": 52610097,
+          "anchorPointX": 1,
+          "anchorPointY": 1,
+          "classType": "CocoStudio.EngineAdapterWrap.CSLabelBMFont",
+          "colorB": 255,
+          "colorG": 255,
+          "colorR": 255,
+          "customProperty": "",
+          "flipX": false,
+          "flipY": false,
+          "height": 45,
+          "ignoreSize": true,
+          "layoutParameter": null,
+          "opacity": 255,
+          "positionPercentX": 0.99625,
+          "positionPercentY": 0.9911111,
+          "positionType": 0,
+          "rotation": 0,
+          "scaleX": 1,
+          "scaleY": 1,
+          "sizePercentX": 0.285,
+          "sizePercentY": 0.25111112,
+          "sizeType": 0,
+          "tag": 12,
+          "touchAble": false,
+          "useMergedTexture": false,
+          "visible": true,
+          "width": 90,
+          "x": 797,
+          "y": 446,
+          "fileNameData": {
+            "path": "font.fnt",
+            "plistFile": "",
+            "resourceType": 0
+          },
+          "text": "112266"
+        }
+      }
+    ],
+    "options": {
+      "__type": "PanelSurrogate:#EditorCommon.JsonModel.Component.GUI",
+      "classname": "Panel",
+      "name": "Panel_14",
+      "ZOrder": 0,
+      "actiontag": -1,
+      "anchorPointX": 0,
+      "anchorPointY": 0,
+      "classType": "CocoStudio.EngineAdapterWrap.CSPanel",
+      "colorB": 255,
+      "colorG": 255,
+      "colorR": 255,
+      "customProperty": "",
+      "flipX": false,
+      "flipY": false,
+      "height": 450,
+      "ignoreSize": false,
+      "layoutParameter": null,
+      "opacity": 255,
+      "positionPercentX": 0,
+      "positionPercentY": 0,
+      "positionType": 0,
+      "rotation": 0,
+      "scaleX": 1,
+      "scaleY": 1,
+      "sizePercentX": 1,
+      "sizePercentY": 1,
+      "sizeType": 0,
+      "tag": 3,
+      "touchAble": false,
+      "useMergedTexture": false,
+      "visible": true,
+      "width": 800,
+      "x": 0,
+      "y": 0,
+      "adaptScreen": false,
+      "backGroundImage": null,
+      "backGroundImageData": null,
+      "backGroundScale9Enable": false,
+      "bgColorB": 255,
+      "bgColorG": 200,
+      "bgColorOpacity": 0,
+      "bgColorR": 150,
+      "bgEndColorB": 255,
+      "bgEndColorG": 200,
+      "bgEndColorR": 150,
+      "bgStartColorB": 255,
+      "bgStartColorG": 255,
+      "bgStartColorR": 255,
+      "capInsetsHeight": 1,
+      "capInsetsWidth": 1,
+      "capInsetsX": 0,
+      "capInsetsY": 0,
+      "clipAble": false,
+      "colorType": 1,
+      "layoutType": 0,
+      "vectorX": 0,
+      "vectorY": -0.5
+    }
+  }
+}
\ No newline at end of file
diff --git a/res/main/score.png b/res/main/score.png
new file mode 100644
index 0000000..c51951a
Binary files /dev/null and b/res/main/score.png differ
diff --git a/src/app.js b/src/app.js
index f7454fe..468a0df 100644
--- a/src/app.js
+++ b/src/app.js
@@ -62,6 +62,26 @@ var HelloWorldLayer = cc.Layer.extend({
  }
 });
 
+var StatusLayer = cc.Layer.extend({
+ _score:0,
+ ui:null,
+ scoreValue:null,
+ getScore: function () {
+  return this._score;
+ },
+ setScore: function (newScore) {
+  this._score = newScore;
+  this.scoreValue.string = newScore;
+ },
+ ctor:function () {
+  this._super();
+  cc.defineGetterSetter(this, "score", this.getScore, this.setScore);
+  this.ui = ccs.uiReader.widgetFromJsonFile(res.main_json);
+  this.scoreValue = ccui.helper.seekWidgetByName(this.ui, "scoreValue");
+  this.addChild(this.ui);
+ }
+});
+
 var GameOverLayer = cc.Layer.extend({
  ctor:function () {
   this._super();
@@ -223,6 +243,8 @@ var HelloWorldScene = cc.Scene.extend({
  myShip:null,
  blocks:null,
  el:null,
+ status:null,
+ live:0,
  onEnter:function () {
   this._super();
   var size = cc.director.getWinSize();
@@ -271,6 +293,10 @@ var HelloWorldScene = cc.Scene.extend({
   ml.attr({target: this.blocks[3]});
   this.addChild(ml);
 
+  this.status = new StatusLayer();
+  this.addChild(this.status);
+  this.status.score = 0;
+
   this.el = cc.EventListener.create({
    event: cc.EventListener.TOUCH_ONE_BY_ONE,
    swallowTouches: true,
@@ -316,6 +342,9 @@ var HelloWorldScene = cc.Scene.extend({
     });
    }
   }
+  
+  this.live += dt;
+  this.status.score = Math.floor(this.live * 1000);
  },
  gameover:function() {
   this.unscheduleUpdate();
diff --git a/src/resource.js b/src/resource.js
index 756e58d..f392785 100644
--- a/src/resource.js
+++ b/src/resource.js
@@ -12,6 +12,10 @@ var res = {
  return_to_title_selected_png : "res/return_to_title_selected.png",
  FireParticle_plist : "res/FireParticle.plist",
  stripes_png : "res/stripes.png",
+ font_png : "res/main/font.png",
+ main_json : "res/main/main.json",
+ score_png : "res/main/score.png",
+ font_fnt : "res/main/font.fnt",
 };
 
 var g_resources = [
@@ -28,11 +32,14 @@ var g_resources = [
  res.return_to_title_normal_png,
  res.return_to_title_selected_png,
  res.stripes_png,
+ res.score_png,
+ res.font_png,
 
  // plist
  res.FireParticle_plist,
  
  // fnt
+ res.font_fnt,
 
  // tmx
  
@@ -40,4 +47,6 @@ var g_resources = [
  
  // effect
 
+ // json
+ res.main_json,
 ];
今回の StatusLayer クラスには、プロパティに setter/getter を定義できる cc.defineGetterSetter を使って score というプロパティを定義してみた。これによって setScore のようなメソッドを呼ばない単なる代入に対しても処理を介入させられるようになるので、間違えて setScore を呼ばずに score に代入してしまうといったケースを減らせる。

これでひとまず動くようになった。


はずだったのだが、コンパイル済みの Windows 版で出ている画像がブラウザ上だと出ない。この手の問題は自分が原因でないケースも考えられるので調べてみたらそれっぽい報告がある模様。
寝て待つとそのうち果報が来そうな予感がするしこの問題にはこれ以上対処しないことにする。

効果音をつける

オーディオ周りは多数の環境で動かすことを考えるとどのフォーマットで用意すればいいのか悩ましい。一応ここに簡単な表と、ブラウザの場合はこっちを見るとどの辺が使えるのかがわかる。ただここまで多種多様なものを覚えるのは結構きついので、本当は *.wav とか *.aiff 辺りで用意しておくと書き出し先の動作環境に合わせて適切にエンコードしてくれたりくれると嬉しいと個人的には思う。

今回は各ブラウザへの対応を考えて ogg と mp3、更にその元データになっている wav も用意した。また、チュートリアルを見る限りでは「And you don't need to list both in resource.js, please just list "mp3" format which has better compatibility with iOS and Mac when you run your game on devices.」とのことなので resource.js には mp3 だけを書くことにする。

あとはこれを cc.audioEngine.playEffect(res.jump_mp3) として再生すればいいだけなのだが、どうもブラウザでは5回以降音がでない状態に陥る。挙動的に再生完了を自動的に検知せず再生数上限か何かに引っかかっているんだろうと思いちょっと見てみる_maxAudioInstance: 5 というのがあったので、どうやらこれっぽい。再生前に前回再生時のオーディオIDを停止させるようにすることで問題なく動くようになった。
diff --git a/res/jump.mp3 b/res/jump.mp3
new file mode 100644
index 0000000..a2d3a5e
Binary files /dev/null and b/res/jump.mp3 differ
diff --git a/res/jump.ogg b/res/jump.ogg
new file mode 100644
index 0000000..d15fc28
Binary files /dev/null and b/res/jump.ogg differ
diff --git a/res/jump.wav b/res/jump.wav
new file mode 100644
index 0000000..fd5c63a
Binary files /dev/null and b/res/jump.wav differ
diff --git a/src/app.js b/src/app.js
index 468a0df..202de44 100644
--- a/src/app.js
+++ b/src/app.js
@@ -244,6 +244,7 @@ var HelloWorldScene = cc.Scene.extend({
  blocks:null,
  el:null,
  status:null,
+ jumpAudioId:null,
  live:0,
  onEnter:function () {
   this._super();
@@ -297,16 +298,21 @@ var HelloWorldScene = cc.Scene.extend({
   this.addChild(this.status);
   this.status.score = 0;
 
+  var that = this;
   this.el = cc.EventListener.create({
    event: cc.EventListener.TOUCH_ONE_BY_ONE,
    swallowTouches: true,
-   onTouchBegan: this.onTap,
+   onTouchBegan: function(touch, event){ return that.onTap(touch, event); },
   });
   cc.eventManager.addListener(this.el, this);
   this.scheduleUpdateWithPriority(10);
  },
  onTap:function(touch, event) {
-  event.getCurrentTarget().myShip.jump();
+  this.myShip.jump();
+  if (this.jumpAudioId !== null) {
+   cc.audioEngine.stopEffect(this.jumpAudioId);
+  }
+  this.jumpAudioId = cc.audioEngine.playEffect(res.jump_mp3);
   return true;
  },
  update:function(dt) {
diff --git a/src/resource.js b/src/resource.js
index f392785..1bcb3f0 100644
--- a/src/resource.js
+++ b/src/resource.js
@@ -16,6 +16,7 @@ var res = {
  main_json : "res/main/main.json",
  score_png : "res/main/score.png",
  font_fnt : "res/main/font.fnt",
+ jump_mp3 : "res/jump.mp3",
 };
 
 var g_resources = [
@@ -46,6 +47,7 @@ var g_resources = [
  // bgm
  
  // effect
+ res.jump_mp3,
 
  // json
  res.main_json,
同じ流れで BGM も流すようにしてみる。
diff --git a/res/bgm.mp3 b/res/bgm.mp3
new file mode 100644
index 0000000..218012a
Binary files /dev/null and b/res/bgm.mp3 differ
diff --git a/res/bgm.ogg b/res/bgm.ogg
new file mode 100644
index 0000000..6eb0b1b
Binary files /dev/null and b/res/bgm.ogg differ
diff --git a/src/app.js b/src/app.js
index 202de44..8174010 100644
--- a/src/app.js
+++ b/src/app.js
@@ -298,6 +298,7 @@ var HelloWorldScene = cc.Scene.extend({
   this.addChild(this.status);
   this.status.score = 0;
 
+  cc.audioEngine.playMusic(res.bgm_mp3, true);
   var that = this;
   this.el = cc.EventListener.create({
    event: cc.EventListener.TOUCH_ONE_BY_ONE,
@@ -353,6 +354,10 @@ var HelloWorldScene = cc.Scene.extend({
   this.status.score = Math.floor(this.live * 1000);
  },
  gameover:function() {
+  if (this.jumpAudioId !== null) {
+   cc.audioEngine.stopEffect(this.jumpAudioId);
+  }
+  cc.audioEngine.stopMusic(true);
   this.unscheduleUpdate();
   cc.eventManager.removeListener(this.el);
 
diff --git a/src/resource.js b/src/resource.js
index 1bcb3f0..0edd300 100644
--- a/src/resource.js
+++ b/src/resource.js
@@ -17,6 +17,7 @@ var res = {
  score_png : "res/main/score.png",
  font_fnt : "res/main/font.fnt",
  jump_mp3 : "res/jump.mp3",
+ bgm_mp3 : "res/bgm.mp3",
 };
 
 var g_resources = [
@@ -45,6 +46,7 @@ var g_resources = [
  // tmx
  
  // bgm
+ res.bgm_mp3,
  
  // effect
  res.jump_mp3,
Windows / Android / Web どの環境でもそれなりに鳴るけど、Windows はループ再生してもギャップレスにはならずループ時にノイズが混じり、Android は曲の終端が切れ、Web(Chrome) は一番理想には近いもののギャップレスにはならなかった。

もし実際にゲームを作る時に BGM を入れる場合は1ループは長めに確保し、曲の頭には少しの無音を入れ、かつ曲の終わりはフェードアウトか無音部分を入れて終わらせる感じだろうか。

ゲームと言っても日常の生活の中で開いた時間にちょっとずつプレイするようなスタイルだと重厚な臨場感溢れる BGM とかそもそもあまりお呼びではない気もするし、取りあえずそこそこに音が出せれば十分であまり洗練されていないのかもしれないと思った。

加速度センサーの値に応じて挙動を変える

端末を傾けるとブロックの速度と得点の上がり方が変わるようにしてみる。これによってゲームプレイ方法に幅が広がる。

加速度センサーからの情報を得るには cc.inputManager.setAccelerometerEnabled で有効化した上で cc.eventManager.addListener でイベントリスナーを登録しておけばいいようだ。
diff --git a/src/app.js b/src/app.js
index 8174010..d4ff88c 100644
--- a/src/app.js
+++ b/src/app.js
@@ -169,6 +169,7 @@ var BlockLayer = cc.Layer.extend({
  d:null,
  vx: 0,
  vy: 0,
+ accel:null,
  ctor:function (width, height, fillcolor) {
   this._super();
   this.d = cc.DrawNode.create();
@@ -182,7 +183,7 @@ var BlockLayer = cc.Layer.extend({
   return true;
  },
  update:function(dt) {
-  this.x += this.vx * dt;
+  this.x += this.vx * dt * (1-Math.min(0.5, Math.max(-0.5, this.accel.x)));
   this.y += this.vy * dt;
  }
 });
@@ -243,14 +244,19 @@ var HelloWorldScene = cc.Scene.extend({
  myShip:null,
  blocks:null,
  el:null,
+ ela:null,
  status:null,
  jumpAudioId:null,
+ accel:null,
  live:0,
  onEnter:function () {
   this._super();
   var size = cc.director.getWinSize();
   var layer = new HelloWorldLayer();
   this.addChild(layer);
+  
+  this.accel={x:0, y:0, z:0};
+
   this.myShip = new MyShipLayer();
   this.addChild(this.myShip);
   this.myShip.attr({
@@ -275,7 +281,8 @@ var HelloWorldScene = cc.Scene.extend({
     x: size.width+Math.random()*size.width,
     y: -bl.height+Math.random()*(size.height+bl.height),
     vx: -60-Math.random()*60,
-    vy: -2.5+Math.random()*5
+    vy: -2.5+Math.random()*5,
+    accel: this.accel,
    });
    this.addChild(bl);
    this.blocks.push(bl);
@@ -306,6 +313,13 @@ var HelloWorldScene = cc.Scene.extend({
    onTouchBegan: function(touch, event){ return that.onTap(touch, event); },
   });
   cc.eventManager.addListener(this.el, this);
+
+  cc.inputManager.setAccelerometerEnabled(true);  
+  this.ela = cc.EventListener.create({
+   event: cc.EventListener.ACCELERATION,
+   callback: function(accel, event){ return that.onAccel(accel, event); },
+  });
+  cc.eventManager.addListener(this.ela, this);
   this.scheduleUpdateWithPriority(10);
  },
  onTap:function(touch, event) {
@@ -316,6 +330,12 @@ var HelloWorldScene = cc.Scene.extend({
   this.jumpAudioId = cc.audioEngine.playEffect(res.jump_mp3);
   return true;
  },
+ onAccel:function(accel, event) {
+  this.accel.x = accel.x;
+  this.accel.y = accel.y;
+  this.accel.z = accel.z;
+  return true;
+ },
  update:function(dt) {
   var myShipBB = this.myShip.getBoundingBox();
   var barrierBB = cc.rect(myShipBB.x-40,myShipBB.y-40,myShipBB.width+80,myShipBB.height+80);
@@ -350,7 +370,7 @@ var HelloWorldScene = cc.Scene.extend({
    }
   }
   
-  this.live += dt;
+  this.live += dt * (1-Math.min(0.5, Math.max(-0.5, this.accel.x)));
   this.status.score = Math.floor(this.live * 1000);
  },
  gameover:function() {
@@ -360,6 +380,7 @@ var HelloWorldScene = cc.Scene.extend({
   cc.audioEngine.stopMusic(true);
   this.unscheduleUpdate();
   cc.eventManager.removeListener(this.el);
+  cc.eventManager.removeListener(this.ela);
 
   var layer = new GameOverLayer();
   this.addChild(layer);
簡単に動くようにはなったのだが、端末の持ち方を上下逆さまにした時ゲーム画面はそれに合わせて回転するもののセンサーの反応はそのままなので、結果的に傾きに対するリアクションが正反対になってしまう。orientation 的なものを取得できれば挙動も反転させられそうだけどどこから取得すればいいのかわからない。

その他

Cocos Code IDE で新規プロジェクトを作成した時、画面サイズのデフォルト値が 960x640 になっていたので、スクリーンサイズをそれに変更した。これによって画面サイズが広くなったが、自機やブロックのサイズは変わらないままなので画面に余裕ができてかなり間延びした状態になった。

また、インターフェイスでビットマップフォントのラベルはインターフェイス用背景画像とは別の階層に居たが、これはインターフェイス用背景画像の子としてビットマップフォンのトラベルを配置したほうが正しいと思ったのでそうなるように変更した。変更したところ、数字だけは見えていたブラウザ上でのインターフェイス表示は数字も含めて見えなくなった。

今回までの成果物はこちら。また、今回はインターフェイスの追加が Web から確認できないので Android 用の apk ファイルも公開する。作業に使っているリポジトリはこちら