モーリーのメモ

プログラミングやCG作成等、アプリ開発を中心に情報を収集中!

Tiledのマップデータから物理空間を作成する!:Cocos Creator

 Cocos Creatorは、物理エンジンのChipmunkを使用できます。
 物理エンジンとは、物体が落下したり衝突したりした時の動きを計算してくれるものです。
 アクションゲーム等を作る際に利用出来ます。
 Tiledのマップデータから、アクションゲームのステージを作成するには、壁・床・ブロック等がそれぞれ物理的に認識される必要があります。
 
 今回は、Tiledで壁・床・ブロックとしてオブジェクトを配置し、Chipmunkを利用して、これらがプログラム上で物理的に認識されるようにします。
 f:id:mmorley:20160620173045p:plain:w500
 下図はプロジェクトの実行画面です。
 タッチした位置に丸い物体が生成されるようにしています。
 壁・床・ブロックに対して、跳ねたり、転がったり、留まったりします。
 f:id:mmorley:20160620174258p:plain:w300
 下記に動くデモがあります。

使用環境

 私が使用している環境です。

  • Mac OS X El Capitan Version 10.11.4
  • Tiled Version 0.16.1
  • Cocos Creator Version 1.1.0
  • ブラウザ:Google Chrome Version 51.0.2704.103 (64-bit)

Tiledによるマップデータの作成

 下図のようにオブジェクトを配置したマップデータを作成しました。
 Cocos Creatorに読み込んだ際に、オブジェクトの形状を判別するために『種類』を設定しています。
 尚、『polygon』は、時計回りに頂点を描かないとChipmunkの仕様でエラーになります。
 Chipmunk内で物体の形状(境界)がわかりやすいようにマップは背景のみにしています。
f:id:mmorley:20160602152859p:plain:w550
 Tiledによるタイルマップの作成方法については、下記の記事で説明しています。
mmorley.hatenablog.com

Cocos Creatorで、Tiledのマップデータを取得

 下記の手順で、HelloWorldスクリプト内でマップデータを扱えるようにします。

  1. HelloWorldスクリプトのPropertiesにtiledMap(名前は任意)を追加
  2. Assetsパネルのマップデータファイル(.tmx)をNode TreeパネルのCanvasノードにサブノードとして追加
  3. Node Treeパネルに追加したマップデータのノードを、HelloWorldスクリプトコンポーネントのプロパティに登録
    f:id:mmorley:20160604083030p:plain:w550
  4. マップデータのノードのアンカーポイントを(0, 0)に設定
    f:id:mmorley:20160604105923p:plain:w550
  5. マップデータの位置が中央からズレるので、Positionを修正

Chipmunkの実装について

 Cocos Creatorでは、Chipmunkはすぐに使えます。ライブラリの設定等はありません。
 ごくごく簡単に実装の流れを説明すると、下記のようになります。

  1. cp.Spaceで物理空間を作成
  2. cp.Bodyとcp.Shapeで物体を定義して、物理空間に追加
    • cp.Body:位置、角度、速度、力等の情報を管理
    • cp.Shape:形状、摩擦係数、弾性係数等の情報を管理
  3. ループ処理
    1. cp.Space.step(dt)でdt時間後の物体の動きを計算
    2. cp.Bodyから物体の位置・角度を取得して、対応するスプライトと同期(今回、省略)

 必要に応じてcp.Bodyから運動の情報を取得したり、力を加えて動きに変化を付けたりします。
 使用したChipmunkの関数については、下記の記事で説明しています。
mmorley.hatenablog.com

Tiledのマップデータから物理空間を作成

 Tiledのオブジェクトのデータについては、下記の記事で調べました。
mmorley.hatenablog.com
 Chipmunkの物体を作成する関数に、オブジェクトのデータを渡して、物体を作成します。
 下記は、今回作成したHelloScriptスクリプトの全コードです。
 HelloScriptスクリプトにコピペします。

cc.Class({
    extends: cc.Component,

    properties: {
        tiledMap: { // マップデータを保持
            default: null, // 初期値
            type: cc.TiledMap, // データの型
        },
    },

    // 初期化処理
    onLoad: function () {
        // 物理空間の設定
        this.space = new cp.Space(); // Space(物理空間)を作成
        this.space.gravity = cp.v(0, -350); // 重力を設定
        this.setupDebugNode(); // デバッグ用の表示(物理演算上の物体の境界を表示)

        // タイルマップのオフセットを計算
        this.tiledMapOffset = this.tiledMap.node.position;
        
        // オブジェクトの読込
        let objects = this.tiledMap.getObjectGroup("object1").getObjects(); // マップからオブジェクトの配列を取得
        for(let i = 0; i < objects.length; i++){ // オブジェクトの配列数分ループ
            let curObject = objects[i]; // オブジェクトを取得
            // オブジェクトの基準点の位置を取得
            let objectPos = cp.v.add(
                cp.v(Number(curObject.x), Number(curObject.y)), // 文字から数値に変換
                this.tiledMapOffset); // オフセット値を加算
            // オブジェクトのサイズを取得
            let objectSize = cp.v(Number(curObject.width), Number(curObject.height)); // 文字から数値に変換
            let objectBody = new cp.StaticBody(); // 静的ボディを生成
            let objectElasticity = 0.5; // 弾性係数
            let objectFriction = 0.5; // 摩擦係数
            switch (curObject.type) { // タイプ別に処理を行う
                case "polyline":
                    for(let j = 0; j < curObject.polylinePoints.length - 1; j++){ // 頂点数分ループ
                        // 取得した座標値を文字列から数値に変換
                        let start = cp.v(Number(curObject.polylinePoints[j].x),
                                         -Number(curObject.polylinePoints[j].y)); // yは正負反転
                        let end = cp.v(Number(curObject.polylinePoints[j + 1].x),
                                         -Number(curObject.polylinePoints[j + 1].y)); // yは正負反転
                        start.add(objectPos); // Canvasの座標に変換
                        end.add(objectPos); // Canvasの座標に変換
                        let segmentShape = new cp.SegmentShape( // 線形状のShapeを作成
		                    objectBody, // 静的ボディを設定
		                    start,  // 始点座標
		                    end, // 終点座標
		                    0); // 線の太さ
		                segmentShape.setElasticity(objectElasticity); // 弾性係数を設定
			            segmentShape.setFriction(objectFriction); // 摩擦係数を設定
                        this.space.addStaticShape(segmentShape); // Spaceに地面(静的ボディ)を追加
                    }
                    break;
                case "box":
                    //objectBody.setPos(objectPos.add(cp.v.mult(objectSize, 0.5))); // Boxの中心座標(左下の座標+サイズの1/2)
		        	objectBody.p = cp.v.add(objectPos, cp.v(objectSize.x * 0.5, objectSize.y * 0.5));
		        	let boxShape = new cp.BoxShape( // Box形状のShapeを作成
		        	    objectBody, 
		        	    objectSize.x, 
		        	    objectSize.y); // Shapeを作成
			        boxShape.setElasticity(objectElasticity); // 弾性係数を設定
			        boxShape.setFriction(objectFriction); // 摩擦係数を設定
			        this.space.addShape(boxShape); // Shapeを追加
                    break;
                case "circle":
		        	objectBody.p = cp.v.add(objectPos, cp.v(objectSize.x * 0.5, objectSize.y * 0.5));
		        	let circleShape = new cp.CircleShape( // Circle形状のShapeを作成
		        	    objectBody, 
		        	    objectSize.x * 0.5, 
		        	    cp.vzero); // Shapeを作成
			        circleShape.setElasticity(objectElasticity); // 弾性係数を設定
			        circleShape.setFriction(objectFriction); // 摩擦係数を設定
			        this.space.addShape(circleShape); // Shapeを追加
                    break;
                case "polygon":
                    let verts = [];
                    for(let j = 0; j < curObject.points.length; j++){
                        // 取得した座標値を文字列から数値に変換
                        let point = cp.v(Number(curObject.points[j].x),
                                         -Number(curObject.points[j].y)); // yは正負反転
                        point.add(objectPos); // Canvasの座標に変換
                        verts.push(point.x); // 配列に追加
                        verts.push(point.y); // 配列に追加
                    }
		        	let polyShape = new cp.PolyShape( // Polygon形状のShapeを作成
		        	    objectBody, 
		        	    verts,
		        	    cp.vzero); // Shapeを作成
			        polyShape.setElasticity(objectElasticity); // 弾性係数を設定
			        polyShape.setFriction(objectFriction); // 摩擦係数を設定
			        this.space.addShape(polyShape); // Shapeを追加
                    break;
            }
        }
        
        // タッチイベントを追加
        this.node.on(cc.Node.EventType.TOUCH_START, function (touch, event) {
            // タッチした位置にボールを追加
            let body = new cp.Body(1, cp.momentForCircle(1, 0, 16, cp.vzero)); // Bodyを作成
            body.setPos(this.node.convertTouchToNodeSpaceAR(touch)); // touchオブジェクトからタッチ位置を取得
            this.space.addBody(body); // Spaceに追加
            let shape = new cp.CircleShape(body, 16, cp.vzero); //Circle形状のShapeを作成
        	shape.setElasticity(0.5); // 弾性係数を設定
        	shape.setFriction(0.5); // 摩擦係数を設定
        	this.space.addShape(shape); // Shapeを追加
        }, this);
    },

    // ループ処理
    update: function (dt) {
        this.space.step(dt); // dt時間分の物理演算を行う
    },
    
    // デバッグ用の表示
    setupDebugNode: function (){ // 物理空間のデバッグ表示
        this.debugNode = cc.PhysicsDebugNode.create(this.space);
        this.debugNode.visible = true;
        this.debugNode.setPosition(0, 0);
        this.node._sgNode.addChild(this.debugNode, 100);
    },
});
  • switch文でオブジェクトの種類ごと処理を分けて、物体を作成しています。
  • 壁・床・ブロックだけだと動きがないので、タッチイベントでタッチ位置に丸い物体を生成しています。
  • 物体を視覚的に表示するためデバッグ表示を利用しています。

デモ

 上記を実装したデモです。
 タッチした位置に丸い物体を生成します。

あとがき

 今回作成したプロジェクトは、iOSシミュレータ上で動作することを確認しました。
 なので基本的にはChipmunkは使用できると思います。
 ただ、cp.v.mult(v, s)とVect.mult(s)という関数を使うと、Web上では動きましたが、iOSシミュレータ上では動きませんでした。
 今回わかったのは上記2つの関数です。他にダメなものがあるかは分かりません。ちなみに、どちらも座標の成分をs倍する関数で、なくても特別困ることはありません。
 私の環境では、iOS向けにビルド→コンパイルiOSシミュレータで実行を行うのにけっこう時間がかかるので、原因を突き止めるのがけっこう手間がかかりました。