モーリーのメモ

アプリ開発等(プログラミング、CG作成)、興味を持ったことを実践してまとめるブログです。

モーリーのメモ

物理演算エンジン(Chipmunk)の使い方:Cocos2d-x v3.7(JavaScript)

 物理演算エンジンは、物が落下したり衝突したりしたとき等の動きを、それっぽく作るのを手助けしてくれるツールです。それっぽくと書いたのは、現実の動きは複雑すぎるので、実行する環境の処理能力に合わせて、形状や力等の条件を簡略化されているからですが、うまく物理演算エンジンを取り入れられれば面白い効果を得ることが出来ます。
 
 『Cocos2d-x』には『Box2D』と『Chipmunk』という2つの物理演算エンジンが含まれていますが、『JavaScript Bindngs』でサポートされているのは『Chipmunk』です。
 つまり『JavaScript』の場合、iOSAndroid向けのアプリで物理演算エンジンを使いたい場合は『Chipmunk』を使用することになります。
 
 私は『1つのソースコードで、iOSAndroid、web向けのアプリを作る』という利点に惹かれて『JavaScript』を選んでいるで、まずは『Chipmunk』の使い方を調べました。
 
 実際に物理演算エンジン(Chipmunk)を使用したプログラムを作成します。

目次

作成する内容

 物理演算エンジンによって物体を動かす方法と、物体の衝突イベントの設定方法を確認するため、下記の内容を作成しました。

  • 画面内に重力が働いています。
  • 画面のタッチした位置に物体を出現させます。
  • 出現する物体は、円、四角、三角の3つの形状で、タッチする度に切り替わります。
  • 円形状の物体は、キャラクターで簡単なアニメーションを設定します。
  • 円形状と三角形状の物体が衝突したら、円形状の物体を削除します。
  • チェックボックスを作成し、デバッグ用の表示と非表示を切り替えます。

『Cocos Studio 2』での作業

新規プロジェクトの作成

新規プロジェクトから作成します。

  1. 『Cocos Studio2』を起動
  2. アプリケーションメニューの『File』-『New Project...』をクリック
  3. 開いたウィンドウで『All』の『Cocos Project』を選択し、『Next』をクリック
  4. 次のように設定を行った後、『Finished』をクリック
    • 『Project Name(プロジェクト名)』:HelloCocos(任意のプロジェクト名)
    • 『Orientation(画面の向き)』:『縦向きの画面のアイコン』を選択
    • 『Engine Version(cocos2d-xのバージョン)』:『cocos2d-x-3.7』を選択
    • 『Project Language(使用するプログラミング言語)』:『JavaScript』を選択

使用する画像

 画像を右クリックして、画像の右に書いた名前を付けて保存して下さい。

地面用画像 f:id:mmorley:20151016114557p:plain:w200 ground.png
円形状用画像1 f:id:mmorley:20151016114605p:plain:w50 chara_0.png
円形状用画像2 f:id:mmorley:20151016114637p:plain:w50 chara_1.png
円形状用画像3 f:id:mmorley:20151016114645p:plain:w50 chara_2.png
四角形状用画像 f:id:mmorley:20151016114707p:plain:w50 box.png
三角形状用画像 f:id:mmorley:20151016114716p:plain:w50 poly.png

 チェックボックス用の画像の取得と設定方法については、『Cocos Studio 2 - CheckBoxの使い方:Cocos2d-x v3.7(JavaScript) - モーリーのメモ』を御覧ください。

プロジェクトへの画像の追加

  1. 『Cocos Studio 2』の画面左下の『Resourcesウインドウ』内で右クリックし、『Import Resources...』をクリック
  2. 先ほど保存した画像(チェックボックス用の画像も含む)を選択し、『Open』をクリック

スプライトシートの追加

インポートした画像をスプライトシートにまとめます。

  1. アプリケーションメニューの『File』-『New File...』をクリック
  2. 開いたウインドウで、下記のように設定し、『New』をクリック
    • 『Type』:SpriteSheet
    • 『Name』:Plist
  3. 『Resourcesウインドウ』でインポートした画像を選択し、『Canvas(画面作成等の作業をするウインドウ)』の『Plist.csi』にドラッグアンドドロップ

『Spriteオブジェクト』の配置とプロパティの設定

 地面用の画像を配置します。

  1. Canvas』上部の『MainScene.csd』のタブをクリック
    『MainScene.csd』の編集に戻ります。
  2. 『Cocos Studio 2』の画面左上の『Objectsウインドウ』の『Basic Objects』の『Spriteオブジェクト』を『Canvas』にドラッグアンドドロップ
  3. 画面右の『Propertiesウインドウ』で『Spriteオブジェクト』のプロパティを下記のように変更
    *書いていないパラメータは変更していません。
    • 『Name』:SpriteGround
    • 『Position & Size』-『Position』:X 320、Y 25
    • 『Feature』
      • 『Image Resource』:ground.png

 『Resourcesウインドウ』から画像ファイルをドラッグアンドドロップして登録します。

『CheckBoxオブジェクト』の配置とプロパティの設定

  1. 『Cocos Studio 2』の画面左上の『Objectsウインドウ』の『Widgets』の『CheckBox』を『Canvas(アプリの画面を作成するウインドウ)』にドラッグアンドドロップ
  2. 画面右の『Propertiesウインドウ』で『CheckBox』のプロパティを下記のように変更
    • 『Name』:CheckBoxDebugNode
    • 『Position & Size』-『Position』:X 50、Y 910
    • 『Feature』
      • 『Background』-『Normal』:checkbox_normal.png
      • 『Background』-『Pressed』:checkbox_pressed.png
      • 『Background』-『Disabled』:checkbox_disabled.png
      • 『Logo Style』-『Pressed』:check_normal.png
      • 『Logo Style』-『Disabled』:check_disabled.png

プロジェクトの保存とパブリッシュ

  1. アプリケーションメニューの『File』-『Save All』をクリック
  2. アプリケーションメニューの『Project』-『Publish and Package...』をクリック
  3. 『Publish』- 『Publish Type』- 『Publish To Code IDE』をクリック
  4. 『OK』をクリック

 作成したプロジェクトがパブリッシュされて、『Cocos Code IDE』が起動します。

『Cocos Code IDE』での作業

『project.json』の編集

  1. 『Cocos Code IDE』の画面左の『Explorer』で、『project.json』を右クリックし、『Open With』-『テキストエディタ』をクリック
  2. 下記のように、『"modules"』に、『"chipmunk"』を追加
"modules" : ["cocos2d", "cocostudio", "chipmunk"],

『resource.js』の編集

スプライトシートを登録します。
スプライトシートのファイルは、『res』フォルダ内の『Plist.png』と『Plist.plist』です。

  1. 『Cocos Code IDE』の画面左の『Explorer』で、『srcフォルダ』にある『resource.js』をダブルクリックして開く
  2. 『resource.js』に、下記のようにリソースファイルを登録
var res = {
    HelloWorld_png : "res/HelloWorld.png",
    MainScene_json : "res/MainScene.json",
    Plist_png : "res/Plist.png", // スプライトシートの画像ファイル
    Plist_plist : "res/Plist.plist" // スプライトシートのplistファイル
};

『app.js』の編集

『HelloWorldLayer』に、下記のようにコードを書き加えます。

var HelloWorldLayer = cc.Layer.extend({
    sprite:null,
    space:null, // 物理演算をする空間
    flg:0, // 出現する物体を切り替えるためのフラグ
    charas:[], // キャラクターのPhysicsSpriteを保持(削除時に使用)
    ctor:function () {
        //////////////////////////////
        // 1. super init first
        this._super();

        /////////////////////////////
        // 2. add a menu item with "X" image, which is clicked to quit the program
        //    you may modify it.
        // ask the window size
        var size = cc.winSize;

        var mainscene = ccs.load(res.MainScene_json);
        this.addChild(mainscene.node);
        
        cc.spriteFrameCache.addSpriteFrames(res.Plist_plist); // スプライトシートをキャッシュに登録
  
        this.space = new cp.Space(); // Space(物理演算する空間)を作成
        this.space.gravity = cp.v(0, -98); // 重力を設定

        this._debugNode = new cc.PhysicsDebugNode(this.space); // 物体の形状を表示するデバッグノードを作成
        this._debugNode.setVisible(true); // 表示する
        this.addChild(this._debugNode, 10); // 自ノード(レイヤー)の最前面に追加
        
        var checkBoxDebugNode = // デバッグノードの表示を切り替えるためのチェックボックス
        	ccui.helper.seekWidgetByName(mainscene.node, "CheckBoxDebugNode"); 
        checkBoxDebugNode.addEventListener(function(sender, type){ // チェックイベントを登録
        	switch (type) {
        	case ccui.CheckBox.EVENT_SELECTED: // チェック状態になった場合
        		this._debugNode.setVisible(true); // デバッグノードの表示を表示
        		break;
        	case ccui.CheckBox.EVENT_UNSELECTED: // 非チェック状態になった場合
        		this._debugNode.setVisible(false); // デバッグノードを隠す
        		break;
        	}
        }, this); 
        
        var shapeBottom = new cp.SegmentShape( // 底面として、線の形状を作成
        		new cp.StaticBody(), // 静体(動かない物体)
        		cp.v(0, 50), // 始点
        		cp.v(size.width, 50), // 終点
        		0); // 線の太さ
        shapeBottom.setElasticity(0.8); // 弾性係数を設定
        shapeBottom.setFriction(0.8); // 摩擦係数を設定
        this.space.addStaticShape(shapeBottom); // 空間に物体を追加
        
        cc.eventManager.addListener({ // タッチイベントを登録
        	event: cc.EventListener.TOUCH_ONE_BY_ONE, // シングルタッチのみ対応
        	swallowTouches:false, // 以降のノードにタッチイベントを渡す
        	onTouchBegan:function(touch, event){ // タッチ開始時
        		switch (this.flg) { 
        		case 0: // 円形状の物体を作成
        			var animFrames = []; // アニメーション用の画像の配列
        			for (var i = 0; i < 3; i++) {
        				var str = "chara_" + i + ".png"; // 画像のファイル名を作成
        				var frame = cc.spriteFrameCache.getSpriteFrame(str); // スプライトシートから画像(SpriteFrame)を取得
        				animFrames.push(frame); // 配列に画像を追加
        			}
        			var animation = new cc.Animation.create(animFrames, 0.2); // 1フレーム0.2秒のアニメーションを作成
        			var action = cc.repeatForever(cc.animate(animation)); // ループ再生する

        			var spriteFrame = cc.spriteFrameCache.getSpriteFrame("chara_0.png"); // スプライトシートから画像(SpriteFrame)を取得
        			var sprite = new cc.PhysicsSprite(spriteFrame); // 画像(PhysicsSprite)を作成
        			var contentSize = sprite.getContentSize(); // 画像のサイズを取得
        			var body = new cp.Body(1, cp.momentForCircle(1, 0, contentSize.width / 2, cp.v(0, 0))); // Bodyを作成
        			body.setPos(touch.getLocation()); // タッチ位置にBodyを配置
        			body.applyImpulse(cp.v(0, 200), cp.v(0, 0)); // 垂直上向きの力を加える
        			this.space.addBody(body); // Spaceに追加
        			var shape = new cp.CircleShape(body, contentSize.width / 2, cp.v(0, 0)); // Shapeを作成
        			shape.setElasticity(1.0); // 弾性係数を設定
        			shape.setFriction(1.0); // 摩擦係数を設定
        			shape.setCollisionType(1); // 衝突タイプ(衝突イベントで物体を識別するための番号)を設定
        			this.space.addShape(shape); // Shapeを追加
        			sprite.setBody(body); // 画像(PhysicsSprite)にBodyを設定
        			this.addChild(sprite); // 自ノード(レイヤー)に追加
        			sprite.runAction(action); // 作成したアニメーションを実行
        			this.charas.push(sprite); // キャラクターの配列に追加
        			this.flg ++; // フラグを加算
        			break;
				case 1: // 四角形状の物体を作成
					var spriteFrame = cc.spriteFrameCache.getSpriteFrame("box.png"); // スプライトシートから画像(SpriteFrame)を取得
					var sprite = new cc.PhysicsSprite(spriteFrame); // 画像(PhysicsSprite)を作成
					var contentSize = sprite.getContentSize(); // 画像のサイズを取得
					var body = new cp.Body(1, cp.momentForBox(1, contentSize.width, contentSize.height)); // Bodyを作成
					body.setPos(touch.getLocation()); // Bodyをタッチ位置に配置
					this.space.addBody(body); // Spaceに追加
					var shape = new cp.BoxShape(body, contentSize.width, contentSize.height); // Shapeを作成
					shape.setElasticity(0.2); // 弾性係数を設定
					shape.setFriction(0.2); // 摩擦係数を設定
					this.space.addShape(shape); // Shapeを追加
					sprite.setBody(body); // 画像(PhysicsSprite)にBodyを設定
					this.addChild(sprite); // 自ノード(レイヤー)に追加
					this.flg ++; // フラグを加算
					break;
				default: // 三角形状の物体を作成
					var spriteFrame = cc.spriteFrameCache.getSpriteFrame("poly.png"); // スプライトシートから画像(SpriteFrame)を取得
					var sprite = new cc.PhysicsSprite(spriteFrame); // 画像(PhysicsSprite)を作成
					var contentSize = sprite.getContentSize(); // 画像のサイズを取得
					var verts = [ // ポリゴンの頂点の配列、時計回りで記述
					             0, contentSize.height / 2,
					             contentSize.width / 2, -contentSize.height / 2,
					             -contentSize.width / 2, -contentSize.height / 2
					             ];
					var body = new cp.Body(1, cp.momentForPoly(1, verts, cp.v(0, 0))); // Bodyを作成
					body.setPos(touch.getLocation()); // Bodyをタッチ位置に配置
					this.space.addBody(body); // Spaceに追加
					var shape = new cp.PolyShape(body, verts, cp.v(0, 0)); // Shapeを作成
					shape.setElasticity(0.4); // 弾性係数を設定
					shape.setFriction(0.4); // 摩擦係数を設定
					shape.setCollisionType(2); // 衝突タイプ(衝突イベントで物体を識別するための番号)を設定
					this.space.addShape(shape); // Shapeを追加
					sprite.setBody(body); // 衝突イベントで物体を識別するための番号を設定
					this.addChild(sprite); // 自ノード(レイヤー)に追加
					this.flg = 0; // フラグ0に戻す
					break;
				}
        		return true;
        	}.bind(this) // レイヤーのthisを使えるようにする
        }, this);
        
        this.space.addCollisionHandler( // 衝突イベントを設定
        		1, 2, // 衝突タイプ1と2(キャラクターと三角形状)の物体の衝突時のイベント
        		function(arbiter, space){ // begin
        			var shapes = arbiter.getShapes(); // 衝突したShapeの配列を取得
        			var shape = shapes[0]; // Shapeを取得(配列の順序は、引数で衝突タイプを指定した順序です。)
        			if (shape.collision_type == 1) { // 衝突タイプが1の場合
        				//this.removeShapes.push(shape);
        				var body = shape.getBody(); // ShapeからBodyを取得
        				
        				for (var int = 0; int < this.charas.length; int++) {
        					if (this.charas[int].getBody() == body) { // 衝突したBodyがキャラクターであるか確認
        						this.space.addPostStepCallback(function(){ // ステップ処理終了時に実行
        							this.space.removeBody(body); // Bodyを物理空間から削除
        							body = null; // nullを割り当てて開放
        							this.space.removeShape(shape); // Shapeを物理空間から削除
        							this.charas[int].removeFromParent(true); // 画像を親ノード(レイヤー)から削除
        							this.charas.splice(int, 1); // キャラクター配列から削除
        						}.bind(this)); // レイヤーのthisを使えるようにする
        						break;
							}
        				}
        			}
        			return true;
        		}.bind(this),  // レイヤーのthisを使えるようにする
        		null, // preSolve
        		null, // postSolve
        		null); // separate
        
        this.scheduleUpdate(); // 周期処理を開始
 
        return true;
    },
    update:function(dt){
    	this.space.step(dt); // dt時間分の物理演算を行う
    }
});

物理演算エンジン(Chipmunk)の使い方

 物理演算を行う空間を作成し、その中に物体を追加します。
 物体は『Body』と『Shape』、そして『画像』で構成されています。

  • Space
     物理演算を行う空間です。空間に追加された物体は物理演算エンジンによって動きが計算されます。
  • Body
     物体の物理的特性(質量、位置、回転角度、速度等)を保持します。形状は持っていません。
  • Shape
     物体の形状データと表面特性(摩擦、弾力性)を保持します。『Body』と結びつけて物体を作成します。
  • 画像
     物体の視覚データです。『Body』の持つ位置、角度の情報と同期して動きます。

『Chipmunk』のメソッド

cc.p(x, y)とcp.v(x, y)は同じです。

『cp.Space()』
機能 物理演算を行う空間を作成します。
『space.gravity = cp.v(x, y)』
機能 『space』に空間全体に作用する重力を設定します。
『space.addBody(Body)』
機能 『space』に『Body』を追加します。
引数 Body:cp.Body
『space.addBody(Shape)』
機能 『space』に『Shape』を追加します。
引数 Shape:cp.Shape
『space.removeBody(Body)』
機能 『space』から『Body』を削除します。
引数 Body:cp.Body
『space.removeShape(Shape)』
機能 『space』から『Shape』を削除します。
引数 Shape:cp.Shape
『space.step(経過時間)』
機能 物理空間の時間を経過時間分進めます。
周期処理で実行することで連続して時間を進めます。
引数 経過時間:Number値
『space.addCollisionHandler(衝突タイプ1, 衝突タイプ2, begin, preSolve, postSolve, separate)』
機能 『space』に衝突タイプ1と2の物体の衝突イベントを設定します。
衝突タイプは『shape.setCollisionType(衝突タイプ)』で
設定しておきます。
引数1 衝突タイプ1:Number値
引数2 衝突タイプ2:Number値
引数3 begin:function(arbiter, space){}
2つの物体(Shape)が接触し始めた時に実行される関数です。
falseを返した場合、preSolveとpostSolveは実行されません。
引数4 preSolve:function(arbiter, space){}
接触中で、衝突処理がされる前に呼び出される関数です。
falseを返した場合はpostSolveは実行されません。
引数5 postSolve:function(arbiter, space){}
接触中で、衝突処理がされた後に呼び出される関数です。
引数6 separate:function(arbiter, space){}
2つの物体(Shape)が離れた時に実行される関数です。
補足 『arbiter』により衝突の情報を取得・変更できます。
『arbiter.getShapes()』:衝突した『Shape』の配列を取得します。
配列にShapeが格納される順序は『addCollisionHandler』の引数で
衝突タイプを指定した順序です。
『space.addPostStepCallback(関数)』
機能 『space.step』の処理後に実行する関数を設定します。
関数は設定されてから一度だけ実行されます。
引数 関数:function(){}
『cp.Body(質量, 慣性モーメント)』
機能 『Body』を作成します。慣性モーメントは、回転しにくさです。
引数1 質量:Number値
引数2 慣性モーメント:Number値
『body.setPos(座標)』
機能 『body』の位置を設定します。
引数 座標:cp.v(x, y)
『body.setAngle(角度)』
機能 『body』の角度を設定します。角度の単位はラジアンです。
引数 角度:Number値
『body.applyImpulse(力, オフセット)』
機能 『body』に力を加えます。
引数1 力:cp.v(x, y)
引数2 オフセット:cp.v(x, y)
『cp.momentForCircle(質量, 内径, 外径, オフセット) 』
機能 中空の円の慣性モーメントを返します。
穴がない円の場合は内径を0にします。
引数1 質量:Number値
引数2 内径:Number値
引数3 外径:Number値
引数4 オフセット:cp.v(x, y)
『cp.momentForSegment(質量, 始点, 終点)』
機能 線形状の慣性モーメントを返します。
始点と終点は物体の中心を原点とした座標です。
引数1 質量:Number値
引数2 始点:cp.v(x, y)
引数3 終点:cp.v(x, y)
『cp.momentForPoly(質量, ポリゴンの頂点の配列, オフセット) 』
機能 中心を重心としたポリゴン形状の固体の慣性モーメントを返します。
オフセットは各頂点に加算されます。
引数1 質量:Number値
引数2 ポリゴンの頂点の配列:cc.Array()
頂点の配列は[ x1, y1, x2, y2, x3, y3]のようにX座標とY座標の値を
交互に並べます。頂点は時計回りの順番で記述します。
引数3 オフセット:cp.v(x, y)
『cp.momentForBox(質量, 幅, 高さ) 』
機能 箱型の固体の中心の慣性モーメントを返します。
引数1 質量:Number値
引数2 幅:Number値
引数3 高さ:Number値
『cp.StaticBody()』
機能 静的ボディを返します。
『cp.CircleShape(Body, 半径, オフセット)』
機能 円形状の『Shape』を作成します。
引数1 Body:cp.Body
引数2 半径:Number値
引数3 オフセット:cp.v(x, y)
『cp.SegmentShape(Body, 始点, 終点, 線の太さ) 』
機能 線形状の『Shape』を作成します。
引数1 Body:cp.Body
引数2 始点:Number値
引数3 終点:Number値
引数4 線の太さ:Number値
『cp.PolyShape(Body, ポリゴンの頂点の配列, オフセット)』
機能 ポリゴン形状の『Shape』を作成します。
引数1 Body:cp.Body
引数2 ポリゴンの頂点の配列:cc.Array()
頂点の配列は[ x1, y1, x2, y2, x3, y3]のようにX座標とY座標の値を
交互に並べます。頂点は時計回りの順番で記述します。
引数3 オフセット:cp.v(x, y)
重心に対するオフセットです。
『cp.BoxShape(Body, 幅, 高さ)』
機能 箱(四角)形状の『Shape』を作成します。
引数1 Body:cp.Body
引数2 幅:Number値
引数3 高さ:Number値
『shape.setElasticity(弾性係数)』
機能 『shape』に弾性係数を設定します。
引数 弾性係数:Number値
『shape.setFriction(摩擦係数)』
機能 『shape』に摩擦係数を設定します。
引数 摩擦係数:Number値
『shape.setCollisionType(衝突タイプ)』
機能 『shape』に衝突タイプを設定します。
衝突タイプは衝突イベントで物体を識別するための番号です。
引数 衝突タイプ:Number値
『cc.PhysicsSprite(画像ファイル)』
機能 『画像を『Body』に合わせて動かす機能を持ったスプライトを作成します。
引数 画像ファイル:cc.SpriteFrame
画像ファイルを指定する場合はファイルパスの文字列
『physicsSprite.setBody(Body)』
機能 『physicsSprite』にBodyを設定します。
引数 Body:cp.Body

デモ

 今回作成したものです。
http://githubmorley.github.io/cocosprojects-pages/hellococos05/

  • 画面をクリックすると、クリックした位置に物体が出現します。
  • クリックする度に形状が変わります。
  • 円形状のキャラクターと三角形状の物体が衝突するとキャラクターが消えます。
  • 左上のチェックボックスデバッグ表示が切り替わります。

 
以上です。

あとがき

 質量や力等の単位に関する記述は見つかりませんでした。
 それぞれの物体の比率や動きを見て調整する事になりそうです。
 また、たまに物体がはじけ飛んだりするなど設定外っぽい動きをすることがあります。
 処理の負荷も考慮にいれないと行けないですね。