読者です 読者をやめる 読者になる 読者になる

フニゲの開発日記

Electronとか...

Spineのショートカット

 最近Spineの仕事をしたので自分用のまとめ。

f:id:funige:20151225143745p:plain

 ショートカットを覚えると劇的に効率が上がるんだけど、日本語の情報があんまり無いのでメモ。ごめんたくさんあった。
 使っているのは、Mac用の2.1.27Proです。


一番良く使うTransformの切り替え
  • Cで回転
  • Vで平行移動
  • Xでスケール
画面狭いので「Alt+ビューの頭文字」を使うだけでもだいぶ楽になる。
  • Alt+Dで、Dopesheet(タイムライン)ビューの表示・非表示。
  • Alt+Tで、Treeビューの表示・非表示。

 以下同様。ただしGhostingビューはAlt+H。Alt+GはGraphビューに割り当てられている。

フレーム間の移動。
  • Fで直前のフレーム、Rで直後のフレームに移動。
  • Sで前のキーフレーム、Wで次のキーフレームに移動。
  • Qで最初のフレーム、Eで最後のフレームに移動。

 ショートカット使ってフレームを移動していると、Dopesheetの表示範囲から飛び出してしまうことがある。そういう時は左上の「Current」と書いてあるところをダブルクリックすると、編集中のフレームが中央に来るようにスクロールしてくれる。

アニメーションの再生。
  • @で開始フレームのセット、[で終了フレームのセット。
  • Dでモーションを再生、もう一回Dを押すと停止。Aは逆再生。Ctrl+Rでリピート再生のオン・オフ。
  • Tで下のアニメーションに移動。Yで上のアニメーションに移動。

 自分はTranslateのつもりでTを押して迷子になってしまう癖があったので、慌てずにYを押せば編集中のアニメーションに戻ってこれるのを発見して、だいぶ楽になった。俺だけかなあ。

その他
  • J+ドラッグで作業画面のPan(RMBドラッグが使えない環境で便利)
  • モーション再生中にCtrlを押すと一時停止

 まだいろいろ隠しショートカットがありそうだ。
 Spineはたまにバグって、スペース押しても選択が解除されなくなったりするのだが、これも未知のショートカットを押しちゃったのが原因なんじゃないかと思っている。

Pixi.jsの継承パターン

pixi.js

 大きなプログラムを書こうとすると、どうしても必要になるクラスの継承。
 正直JavaScriptのprototype継承はよくわからないのだが、あまり深く考えずに丸暗記していいと思う。

 javascript - three.js inheritence pattern - Stack Overflow

 Three.jsで新しいHogeクラスを定義する場合は

var Hoge = function(){
    THREE.Object3D.call(this);
    ... // 追加のプロパティなど
};

Hoge.prototype = Object.create(THREE.Object3D.prototype);
Hoge.prototype.constructor = Hoge;
...

 Pixi.jsはThree.jsを参考にして作られているので、継承もだいたい同じ書き方になる。

var Funi = function () {
    PIXI.Container.call(this);
    ... // 追加のプロパティなど
};

Funi.prototype = Object.create(PIXI.Container.prototype);
Funi.constructor = Funi;
...

Three.jsも使ってみよう

Three.js Pixi.js

 3Dも使う必要があって、いまさらな感じもするがThree.jsもやってみた。
 2013年のPixi.jsがリリースされたときのアナウンスを見ると「Pixi.jsはThree.jsの2D版」という考え方が示されていて、まあ確かに似ている部分が多いと思う。片方勉強していれば、もう片方も習得しやすい。

 じゃあPixi.jsとThree.jsを同じcanvas共存できるようにしたらいいと思うのだが、どうもうまくいかないらしい。検索して見つけたサンプルでは、2Dと3Dを別々のcanvasに描画して、2DのcanvasにPixi.jsで描画したものをThree.jsの3D空間にテクスチャとして貼り付ける形になっている。

Combining a Pixi.js canvas and a three.js canvas · Issue #1366 · GoodBoyDigital/pixi.js · GitHub
How to use a Pixi.js Canvas as a Three.js Texture? - Stack Overflow

<html>
<head>
<script src="pixi.js"></script>
<script src="three.js"></script>
</head>

<body>
<script>
    width = window.innerWidth;
    height = window.innerHeight;

    
    // 3DはThree.jsで描画する
    var scene_3D = new THREE.Scene();
    var canvas_3D = new THREE.WebGLRenderer({antialias: true});
    canvas_3D.setSize(width, height);
    document.body.appendChild(canvas_3D.domElement);

    var camera = new THREE.PerspectiveCamera(75, width / height, 1, 10000);
    camera.position.set(0, 0, 700);
    camera.updateProjectionMatrix();

    var geometry = new THREE.BoxGeometry(500, 500, 500);
    var texture = THREE.ImageUtils.loadTexture("res/funige.png");
    var material = new THREE.MeshBasicMaterial({ map: texture });
    var cube = new THREE.Mesh(geometry, material);
    cube.position.z = -500;
    cube.rotation.z = -45;
    scene_3D.add(cube);

    
    // 2DはPixi.jsで描画する
    var scene_UI = new PIXI.Container();
    var canvas_UI = PIXI.autoDetectRenderer(256, 256, {transparent:true});
    // document.body.appendChild(canvas_UI.view);

    var sprite = new PIXI.Sprite.fromImage("res/funige.png")
    sprite.scale.set(0.3);
    sprite.anchor.set(0.5);
    sprite.position.set(128);
    scene_UI.addChild(sprite);

    
    // canvas_UIの内容を512x512の板に貼り付けて3D空間上に表示する
    var texture_UI = new THREE.Texture(canvas_UI.view) 
    var material_UI = new THREE.MeshBasicMaterial({ map:texture_UI, transparent:true });

    var mesh_UI = new THREE.Mesh(new THREE.PlaneBufferGeometry(512, 512), material_UI);
    mesh_UI.position.set(0,0,0);
    scene_3D.add(mesh_UI);

    
    // アニメーション
    function animate() {
        requestAnimationFrame(animate);

        cube.rotation.y += 0.01;
        sprite.rotation += 0.01;
        mesh_UI.material.map.needsUpdate = true; // 2Dが更新されるたびに呼ぶ

        canvas_UI.render(scene_UI);
        canvas_3D.render(scene_3D, camera);
    }
    animate();
</script>

</body>
</html>

 うーん、だいたい動くんだけど、ブラウザによって表示がばらつくなあ。AndroidはまあChromeだけ動けばいいか……。


f:id:funige:20150531231048p:plain

サンプル

PIXI.loader

Pixi.js

 画像やjsonファイルなどを最初にまとめて読み込むときはPIXI.loaderを使う。
 前回のサンプルをPIXI.loaderを使って書き直してみる。

 頂点データをjsonファイルに分離して、例えばmodel.jsonという名前で読み込むことにする。まあ実際のゲームではそうなるだろう。

// model.json
{
"verts": [0, 0, 300, 0, 0, 300, 400, 400],
"uvs": [0, 0, 1, 0, 0, 1, 1, 1],
"triangles": [0, 1, 2, 3, 2, 1]
}

 PIXI.loaderに読み込みたいファイルを好きなだけaddして、PIXI.loader.loadを呼び出す。

var stage = new PIXI.Container();
...

PIXI.loader
    .add('model', 'res/model.json')
    .add('ship', 'res/ship.png')
    .on('progress', function (loader) {
        console.log("*progress* " + loader.progress); // 途中経過も取れる
    })
    .load(onAssetsLoaded);

 ロードが終わった後に行う処理はonAssetsLoadedの中に移動。

...
function onAssetsLoaded(loader, res) 
{
    var verts = new Float32Array(res.model.data.verts);
    var uvs = new Float32Array(res.model.data.uvs);
    var triangles = new Uint16Array(res.model.data.triangles);

    var ship = new PIXI.mesh.Mesh(
        res.ship.texture,
        verts, uvs, triangles, 
        PIXI.mesh.Mesh.DRAW_MODES.TRIANGLES);
    stage.addChild(ship);
}

animate();
...

 こんな感じでしょうか。
 読み込んだデータを解放するメソッドは無さそう。
 

メッシュ変形

Pixi.js

 Pixi.jsのメッシュ操作は気が抜けるぐらい簡単だ。昔Cocos2d-html5でこれと同じ事をやろうとして何日かかったことか。

var renderer = PIXI.autoDetectRenderer(800, 600);
//var renderer = new PIXI.CanvasRenderer(800, 600); // Canvasでも動くぞ
document.body.appendChild(renderer.view);

var stage = new PIXI.Container();

var verts = new Float32Array([0, 0, 300, 0, 0, 300, 400, 400]);
var uvs = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]);
var triangles = new Uint16Array([0, 1, 2, 3, 2, 1]);

var texture = PIXI.Texture.fromImage('res/ship.png');
var ship = new PIXI.mesh.Mesh(
    texture,
    verts, uvs, triangles, 
    PIXI.mesh.Mesh.DRAW_MODES.TRIANGLES);

var touchMove = function (event) {
    // 矩形の右下の点を移動するテスト
    verts[6] = event.data.global.x;
    verts[7] = event.data.global.y;
    ship.dirty = true;
};

ship.on("mousemove", touchMove);
ship.on("touchmove", touchMove);
ship.interactive = true;
stage.addChild(ship);

animate();

function animate() {
    requestAnimationFrame(animate);

    renderer.render(stage);
}

 PIXI.mesh.MeshはPIXI.Spriteと同じように移動・回転・スケールもできる。anchorの設定はできない。頂点データに触ったらdirtyフラグをセットするだけで再計算してくれる。
 あと、長細いものをうねうね動かしたいときは、専用のPIXI.mesh.Ropeというクラスが用意されているのでそっちを使えばいいはず。

f:id:funige:20150527122319p:plain

サンプル

Pixi.jsはじめました

Pixi.js

 Appleのリジェクトにウンザリしたというわけではないのだが、スマホのブラウザで動くゲームを作ることになって、いいフレームワークを探している。

 最終的にネイティブアプリにしてリリースする予定があるなら、今でもCocos2d-JSをおすすめするのだが、ブラウザがターゲットの場合は、現状のCocos2d-JSがベストとは言えない。WebGLの機能を十分に活かしたゲームが作れないからだ。分家のCocos2d-html5 V4も一応調べてみたけど、まだドキュメントも何も無い状態で、残念ながら使えない。

 本当にたくさんあるんだけど、あまり複雑なのは頭に入らないので、とりあえず簡単そうなPixi.js。

 Pixi.jsはロンドンのGoodBoy Digitalが開発している2D描画用のフレームワークで、前に書いたCreatureSpineなどのツールも対応しているし、開発も活発に行われているので、まあ間違いないと思う。

 使用法は、Githubからbinフォルダの中にあるpixi.js(またはpixi.min.js)をダウンロードしてきてindex.htmlの最初の方に追加するだけだ。(たぶんbower使うのが正式なんだと思う。なんかコンソールにエラーが出てるけど気にしない)


 公式のサンプルがわかりやすいので、上から順番にソースコード読みながら試していけば、概要はつかめると思う。

 ……とりあえず習うより慣れろだな。

f:id:funige:20150527065114p:plain

<!DOCTYPE HTML>
<html>
<head>
  <script src="pixi.js"></script>
</head>

<body>
  <script>
    var renderer = PIXI.autoDetectRenderer(800, 600);
    document.body.appendChild(renderer.view);

    var stage = new PIXI.Container();

    // 背景
    var bg = PIXI.Sprite.fromImage('res/bg.jpg');
    bg.width = 800;
    bg.height = 600;
    stage.addChild(bg);

    // 宇宙船
    var ship = PIXI.Sprite.fromImage('res/ship.png');
    ship.anchor.set(0.5);
    ship.position.x = 400;
    ship.position.y = 0; // Pixi.jsは左上が原点なのでy=0は上端
    stage.addChild(ship);

   // 隕石をたくさん飛ばす
    var enemies = [];
    for (var i = 0; i < 10; i++) {
      var enemy = PIXI.Sprite.fromImage('res/enemy.png');
      enemy.anchor.set(0.5);
      enemy.position.x = Math.random() * 800 + 800;
      enemy.position.y = Math.random() * 600;
      stage.addChild(enemy);
      enemies.push(enemy);
    }

    // 宇宙船がタップに反応するように
    stage.interactive = true;
    stage.on('mousedown', function (event) { event.target.touched = true; })
    stage.on('mouseup', function (event) { event.target.touched = false; })
    stage.on('touchstart', function (event) { event.target.touched = true; })
    stage.on('touchend', function (event) { event.target.touched = false; })

    // animate()をタイマーで定期的に呼ぶことでシーンを動かす。
    animate();

    function animate() {
      requestAnimationFrame(animate);

      // 隕石の移動 
      for (var i = 0; i < enemies.length; i++) {
        var enemy = enemies[i];
        enemy.position.x -= 5;
        if (enemy.position.x < 0) {
          enemy.position.x += 800;
        }
      }

      // 宇宙船の移動 
      if (stage.touched) {
        ship.position.y -= 3; // 上昇
      } else {
        ship.position.y += 3; // 下降 
      }

      renderer.render(stage);
   }

  </script>
</body>
</html>

サンプル

 スマホブラウザゲームの市場は今も昔も残念な感じだが、そろそろWebGLが普通に使えるようになって、ここからが面白いところだと思う。

広告SDKでリジェクトの話

 iOSでリジェクトされた件をツイートしたら結構RTされたので、正確な情報が知りたい人もいると思うので、いちおう全文載せときますね。

 2年ぐらい前に作ったゲームで、その頃からアイモバイルのバナーとアスタのアイコン広告は入っていました。久しぶりにバグ修正して、ついでにappCのアイテムSTOREで広告削除の機能をつけようと思ったのですが……

May 18, 2015 at 9:48 AM
差出人: Apple
2.25 - Apps that display Apps other than your own for purchase or promotion in a manner similar to or confusing with the App Store will be rejected
3.10 - Developers who attempt to manipulate or cheat the user reviews or chart ranking in the App Store with fake or paid reviews, or any other inappropriate methods will be removed from the iOS Developer Program
2.25 Details

Your app includes content or feature that displays or promotes third-party apps, which violates the App Store Review Guidelines.

3.10 Details

Your app includes content or features that can manipulate the user reviews or chart ranking in the App Store.

Next Steps

Please remove the following third party libraries from your app:

 今回のリジェクトの前に、10日ぐらい前に一回リジェクトされていまして、2.25はその時のリジェクト理由(アイコン広告)と同じです。3.10の方は一回目のリジェクトの時はありませんでした。
 アスタのSDKの設定画面に「特定バージョンの配信停止設定」(いわゆる審査避け)がありまして、それを使って配信を停止した状態で再提出したですが、どうもそれが疑われた理由じゃないかなと思います。
 ウォール広告とかブーストとか、まあ以前に仕事で関わったことはありますが、このアプリに関しては3.10に引っかかるようなことは何もしてないのです。

 広告はWebビューで入れ直せばいいとして、アプリ内課金はどうしたものか。