LOCKYOUで学ぶ

パラフドーム後日談

戻る

こんにちワン!
というわけで今回も4分木空間分割によるオブジェクトの衝突判定の効率化講座やってくよ!
前回変な感じに終わったけど
今回はあれだろ?衝突予想のペアのリスト
長くなりそうって言ってたが
いえあぁぁ
この衝突予想ペアのリストを作るのには再帰ってやつが必要になってくるんだよね
グーグルで再帰って検索すると もしかして: 再帰 って出るやつ
これについて今回はやってくよ
確かに出るけど……
再帰自体は衝突予想特有のものじゃないだろ?
ここで具体的に説明する必要あるか?
そうだね、衝突判定とは直接は関係ないけど
そもそも再帰はちょっとややこしいから
作者的に理解を深めたくてやってるってのがあるね
ふーん
それじゃ改めて具体的に再帰ってなんだ?
本格的♂な定義はネットで調べてもらうとして、
作者なりの考えてる再帰っていうのは「『モノ』の中に、その『モノ』と同じようなものがある状態」
かな
随分と抽象的な……
まあそうなるよね、
具体例をあげると「フォルダ」かな
windowsとかの
フォルダの中にファイルが入っているけど、
フォルダもあってその中にまたファイルとフォルダが……
みたいな感じ
あ、ショートカットは面倒だから無しでね
あとはwikipediaとかも再帰的な感じに表現できるね
ページの中に文章とリンクがあるけど、リンクをクリックしたらまたページに文章とリンクがあって……って感じで
人物であれば「暦」から日付(誕生日)をたどるほうが速かったりします(六曜→暦→11月6日→松岡修造)
ウィキペディアというかウェブサイト全体に言えるかもしれんが……
これを再帰って言っていいのか?
いやまあそうっすね
フォルダは階層構造になってて親子関係があるって感じでウィキペディアの場合はないでしょ?
だけどまあ、階層構造になってるっていうことはあんまり重要じゃなくて
さっき言ったように「『モノ』の中に、その『モノ』と同じようなものがある状態」っていう抽象的なことに当てはまってさえいれば
割と何にだって適用できちゃうんだよね
なんというか、そういうもんなんだな
そーなんでちゅ……
んじゃ、試しに一個再帰をつかった計算式をやってみよう
ああ、階乗とかフィボナッチ数とかがよく挙げられてるな
いや、足し算

function add(m, n) {
	if(m == 0) {
		return n;
	} else {
		return add(m-1, n)+1;
	}
};
console.log(add(4,6)); // 10
足し算かよ
何の問題ですか?
注意点としては自然数の範囲だけしか計算できないけどね
ちなみに再帰の動きを図で表すとこんな感じ

console.log(add(4,6));              console.log(   10   );
              │                                   ↑
              ↓                                   └───┐
function add(4, 6) {                function add(4, 6) {   │
    if(4 == 0) {                        if(4 == 0) {       │
        return n;                           return n;      │
    } else {                            } else {           │
        return add(4-1, 6)+1;               return     9     +1;
    }                 │                }              ↑
};            ┌───┘            };                 └─┐
              ↓                                           │
function add(3, 6) {                function add(3, 6) {   │
    if(3 == 0) {                        if(3 == 0) {       │
        return n;                           return n;      │
    } else {                            } else {           │
        return add(3-1, 6)+1;               return     8     +1;
    }                 │                }              ↑
};            ┌───┘            };                 └─┐
              ↓                                           │
function add(2, 6) {                function add(2, 6) {   │
    if(2 == 0) {                        if(2 == 0) {       │
        return n;                           return n;      │
    } else {                            } else {           │
        return add(2-1, 6)+1;               return     7     +1;
    }                 │                }              ↑
};            ┌───┘            };                 └─┐
              ↓                                           │
function add(1, 6) {                function add(1, 6) {   │
    if(1 == 0) {                        if(1 == 0) {       │
        return n;                           return n;      │
    } else {                            } else {           │
        return add(1-1, 6)+1;               return     6     +1;
    }                 │                }              ↑
};            ┌───┘            };                 │
              ↓                                       │
function add(0, 6) {                                   │
    if(0 == 0) {                                       │
        return 6;───────────────────┘
    } else {
        return add(m-1, n)+1;
    }
};
この関数の内容は、引数の前者(m)が0のとき、というか0になったら引数の後者(n)を返して、
そうじゃない時はmから1を引いて同じ関数を呼ぶって形になってるよね
だから呼ばれるごとに1が引かれていってmが0になったときに関数呼び出しじゃないもの(n)を返して、
そこで再帰呼び出しは打ち止めになるよ
そして返り値が返されたら今度は呼ばれた順にだんだんと値が戻っていくんだな
add(1-1, 6)+1の部分はadd(1-1, 6)が6になるから6+1となって呼び出し元の関数に返されていって
7+1、8+1と遡って処理されて行って最終的に10になると
ちなみにadd関数だけに注目するとこんな感じになるね

console.log(    add(4,6)            )
console.log(    add(3,6)+1          )
console.log(   {add(2,6)+1}+1       )
console.log(  {[add(1,6)+1]+1}+1    )
console.log( {[<add(0,6)+1>+1]+1}+1 )
console.log( {[<    6   +1>+1]+1}+1 )
console.log(  {[    7   +1]+1}+1    )
console.log(   {    8   +1}+1       )
console.log(        9   +1          )
console.log(       10               )

階層が深くなって関数の数が多くなるとややこしいことになるな……
だからこそ作者もこうしてじっくりこってりやることにしてるし……
それじゃ衝突リスト作成……って言いたいところだけどもう一個だけ
さっき言ったwindowsのフォルダの中にあるファイルの数を数えるプログラムを作ってみよう
作ってみようって言ったって、javascriptでどうやってやるんだ?
さすがにこれはjavascriptじゃないよ
こんな疑似コード的な感じで日本語で書いてくよ

ファイル数取得(){
	// TODO
	return ファイル数
}
フォルダにあるファイル数っていうんだから
どのフォルダが対象かを示さないとダメだな
そうだね、対象フォルダは引数として入れることにして……

ファイル数取得(フォルダパス){
	変数(数値) ファイル数 = 0
	変数(配列) ファイルリスト = フォルダパスのファイルをすべて取得
	変数(配列) フォルダリスト = フォルダパスのフォルダをすべて取得
	
	ファイル数 += ファイルリストの要素の数
	
	//TODO
	
	return ファイル数
}
フォルダパスのファイルをすべて取得って部分はまあそういうメソッドとかあらかじめ用意されてるだろう機能を使う感じ
コマンドプロンプトでやる場合、ファイルなら「dir /a:-D」、フォルダなら「dir /a:D」で取得できるね
そして「dir /s」で子孫のフォルダ含めた全ファイルが取得できる
そのあと、取り出したフォルダリストもおんなじことをやれば子孫のファイル数も数えれるって感じ
実際に動かしたわけじゃないから動作するのかどうかちょっとわかんないけど大体こんな感じだね

ファイル数取得(フォルダパス){
	変数(数値) ファイル数 = 0
	変数(配列) ファイルリスト = フォルダパスのファイルをすべて取得
	変数(配列) フォルダリスト = フォルダパスのフォルダをすべて取得
	
	ファイル数 += ファイルリストの要素の数
	
	for(フォルダリストの要素の数分回す){
		変数(文字列) サブフォルダパス = フォルダリストの要素
		ファイル数 += ファイル数取得(サブフォルダパス)
	}
	
	return ファイル数
}
for文の中でファイル数を取得するために同じ関数を使って再帰してるんだな
自分の中にフォルダがあったらファイル数取得処理をサブフォルダに移して……を繰り返して
自分の中にフォルダがなかったらfor文の中には入らないからreturnで自分のファイル数を親に帰す
そうして子から渡されたファイル数を加算してフォルダリストの要素全部で処理を終えたらfor文から抜けて親に帰す
を繰り返して全部のファイルを取得してるって感じだね
確かに複雑だけど使いこなせればだいぶ便利って感じか
一応再帰つかわなくてもできるけどね

ファイル数取得(フォルダパス){
	
	変数(数値) ファイル数 = 0
	変数(配列) 未検査フォルダリスト = []
	
	未検査フォルダリスト push フォルダパス
	
	while(未検査フォルダリストが空になるまで) {
		変数(文字列) 検査フォルダパス pop 未検査フォルダリスト
		変数(配列) ファイルリスト = 検査フォルダパスのファイルをすべて取得
		変数(配列) フォルダリスト = 検査フォルダパスのフォルダをすべて取得
		
		ファイル数 += ファイルリストの要素の数
		未検査フォルダリスト push フォルダリスト
	}
	
	return ファイル数
}
pushは配列の最後に要素を入れる操作
popは配列の最後から一つ要素を取り出す操作 取り出した要素は変数に入れている
再帰を使いこなせれば便利って話をした後に……
理論上は確か再帰で書いてあるところは
再帰をつかわなくても書ける 冒頭で「衝突予想ペアのリストを作るのには再帰ってやつが必要になってくる」
って言ったな あれは嘘だ
ようになってるんだけど
まあ実装するときは自分が読みやすいとか書きやすいとか好きなほう選べばいいと思うよ
今回は勉強もかねて再帰つかうけど
そんじゃここから本題として衝突予想ペアのリストやってくよ
前回までどういう感じで進めてたっけ……
(3年前だしな……)
仕方ないね……
とりあえず一旦ここまでの全コードを乗せよう
// phina.js をグローバル領域に展開
phina.globalize();

// MainScene クラスを定義
phina.define('MainScene', {
	superClass: 'DisplayScene',

	// 初期処理
	init: function() {

		// 親クラス初期化
		this.superInit({
			// 画面サイズ
			width: 600,
			height: 600,
		});
		// 背景色を指定
		this.backgroundColor = '#EEE';

		// ラベルを生成
		this.label = Label('Hello, phina.js!').addChildTo(this);
		this.label.x = this.gridX.center(); // x 座標
		this.label.y = this.gridY.center(); // y 座標
		this.label.fill = 'black'; // 塗りつぶし色
		
		// グリッド線作成
		(9).times(function(i){
			var path1 = PathShape()
				.addPath(0, 600/8*i)
				.addPath(600, 600/8*i)
				.addChildTo(this);
			var path2 = PathShape()
				.addPath(600/8*i, 0)
				.addPath(600/8*i, 600)
				.addChildTo(this);
		}, this);
		
		// ラベルを生成
		this.kaisou = Label('test').addChildTo(this);
		this.kaisou.setPosition(this.gridX.span(10), this.gridY.span(1));
		this.heya = Label('test').addChildTo(this);
		this.heya.setPosition(this.gridX.span(10), this.gridY.span(2));
		
		// 初期処理部分
		this.hierarchyLevel = 3; // 階層
		this.unitCount = Math.pow(2, this.hierarchyLevel); // 部屋の縦(横)の個数
		this.unitWidth = this.width / this.unitCount; // 部屋の横のサイズ
		this.unitHeight = this.height / this.unitCount; // 部屋の縦のサイズ

		// 衝突オブジェクトリストを作成
		this.colObjList = [];
		this.colObjCount = 0;
		// 四角を複数個作成
		for(; this.colObjCount<31; this.colObjCount++) {
			let rect = RectangleShape().addChildTo(this);
			rect.setPosition(Math.randint(0, 600),Math.randint(0, 600)); // 位置はランダムにする
			rect.setSize(30,30);
			rect.draggable;

			rect.id = this.colObjCount; // ID
			rect.headRoomNo = 0; // 左上の部屋番号(最下層)
			rect.tailRoomNo = 0; // 右下の部屋番号(最下層)
			rect.hierNo = -1; // 階層番号
			rect.roomNo = 0; // 部屋番号
			
			// それぞれの四角にIDを表示
			let label = Label(rect.id).addChildTo(rect);
			label.fill = 'white'; // 塗りつぶし色

			this.colObjList[rect.id] = rect;
		}
		
		// オブジェクトの衝突の木をSetで埋める
		this.colObjBelongTree = new Array(this.hierarchyLevel + 1);
		for (let i=0; i < this.colObjBelongTree.length; i++) {
			this.colObjBelongTree[i] = new Array(Math.pow(4, i));
			for(let j=0; j < this.colObjBelongTree[i].length; j++) {
				this.colObjBelongTree[i][j] = new Set();
			}
		}
		this.colObjBelongTree[-1] = [new Set()];

	},

	// 毎フレーム処理
	update: function(app) {
		// ここに処理を記述
		let colObjList = this.colObjList;
		for(let colObj of colObjList) {
			
			// 旧番号を保持
			let oldHierNo = colObj.hierNo;
			let oldRoomNo = colObj.roomNo;
			
			// 左上の部屋番号を割り出す
			let headRoomX = Math.trunc(colObj.left / this.unitWidth);
			headRoomX = Math.clamp(headRoomX, 0, this.unitCount-1);
			let headRoomY = Math.trunc(colObj.top / this.unitHeight);
			headRoomY = Math.clamp(headRoomY, 0, this.unitCount-1);
			colObj.headRoomNo = this.bitSeparate32(headRoomX) | (this.bitSeparate32(headRoomY)<<1);

			// 右下の部屋番号を割り出す
			let tailRoomX = Math.trunc(colObj.right / this.unitWidth);
			tailRoomX = Math.clamp(tailRoomX, 0, this.unitCount-1);
			let tailRoomY = Math.trunc(colObj.bottom / this.unitHeight);
			tailRoomY = Math.clamp(tailRoomY, 0, this.unitCount-1);
			colObj.tailRoomNo = this.bitSeparate32(tailRoomX) | (this.bitSeparate32(tailRoomY)<<1);
			
			// 全体の階層をもとに自分のオブジェクトがいる階層を割り出す
			let xorRoom = colObj.headRoomNo ^ colObj.tailRoomNo;
			xorRoom = (xorRoom & 0xaaaaaaaa) | (xorRoom << 1 & 0xaaaaaaaa);
			xorRoom |= (xorRoom >>> 1);
			xorRoom |= (xorRoom >>> 2);
			xorRoom |= (xorRoom >>> 4);
			xorRoom |= (xorRoom >>> 8);
			xorRoom |= (xorRoom >>> 16);
			
			xorRoom = (xorRoom & 0x55555555) + ((xorRoom >>> 1) & 0x55555555);
			xorRoom = (xorRoom & 0x33333333) + ((xorRoom >>> 2) & 0x33333333);
			xorRoom = (xorRoom & 0x0f0f0f0f) + ((xorRoom >>> 4) & 0x0f0f0f0f);
			xorRoom = (xorRoom & 0x00ff00ff) + ((xorRoom >>> 8) & 0x00ff00ff);
			xorRoom = (xorRoom & 0x0000ffff) + ((xorRoom >>> 16) & 0x0000ffff);
			
			colObj.hierNo = (this.hierarchyLevel - (xorRoom/2));

			// 部屋番号を割り出す
			colObj.roomNo = colObj.headRoomNo >>> xorRoom;

			// 旧番号と異なる場合は部屋を移し替える
			if (oldHierNo != colObj.hierNo || oldRoomNo != colObj.roomNo) {
				this.colObjBelongTree[oldHierNo][oldRoomNo].delete(colObj.id);
				this.colObjBelongTree[colObj.hierNo][colObj.roomNo].add(colObj.id);
			}
		}
		
		//ここに衝突予想ペアのリストを作成する処理を記述する 
		
		// 階層と部屋を表示
		this.kaisou.text = "階層:" + colObjList[0].hierNo;
		this.heya.text = "部屋:" + colObjList[0].roomNo;

	}, 
	
	bitSeparate32: function(n) {
		n = (n|(n<<8)) & 0x00ff00ff;
		n = (n|(n<<4)) & 0x0f0f0f0f;
		n = (n|(n<<2)) & 0x33333333;
		return (n|(n<<1)) & 0x55555555;
	},
	
	makeCombiList: function(paramList) {
		let result = [];
		for (let i=0; i < paramList.length-1; i++){
			for(let j=i+1; j < paramList.length; j++) {
				result.push([paramList[i], paramList[j]]);
			}
		}
		return result;
	},
	
	makeCrossJoinList: function(list1, list2) {
		let result = [];
		for (let i=0; i < list1.length; i++){
			for(let j=0; j < list2.length; j++) {
				result.push([list1[i], list2[j]]);
			}
		}
		return result;
	},

});


// メイン処理
phina.main(function() {
	// アプリケーション生成
	let app = GameApp({
		startLabel: 'main',
		fit: false,
		query: '#sample01',
		width: 600, //縦のサイズ
		height: 600, //横のサイズ
	});
	// アプリケーション実行
	app.run();
});
お~なるほどね( ^ω^)
this.colObjBelongTreeがオブジェクトの所属するツリーってわけね
前回から持ってきたこの下の画像みたいな感じ
全コード乗っているプログラムはhierarchyLevelは3となっていますが
その場合階層の数は0,1,2,3の4つとなります
なのでhierarchyLevel3を表すにはこの図にはもう一つ階層が下に必要です
正確にはツリーじゃなく二次元配列だからこうなるな
この図で言えば
H0R0に入ってる6, 10, 20は全部のオブジェクトと当たる可能性があって
H1R2に入ってる13, 15は上の6,10,20に加えて13と15同士、あとはその下のH2R8~11の12,16と当たる可能性があるって感じだね
それじゃいよいよ衝突予想ペアのリストの関数を書いていこう

ファイル数取得(フォルダパス){
	変数(数値) ファイル数 = 0
	変数(配列) ファイルリスト = フォルダパスのファイルをすべて取得
	変数(配列) フォルダリスト = フォルダパスのフォルダをすべて取得
	
	ファイル数 += ファイルリストの要素の数
	
	for(フォルダリストの要素の数分回す){
		変数(文字列) サブフォルダパス = フォルダリストの要素
		ファイル数 += ファイル数取得(サブフォルダパス)
	}
	
	return ファイル数
}

let self = this;
function collisionPairPullOut(){
	
	let collisionPairList = [];
	
	
	return collisionPairList;
}
ファイル数を取得する疑似コードも再掲したけど
衝突予想ペアのリストを抜き出すの関数もおんなじ要領で作れるから参考にできるね
疑似コードで言うフォルダパスは衝突予想ペアリストで言えば……
階層番号と部屋番号か
この二つで
this この場合のthisはMainSceneクラスを指しますが
collisionPairPullOut関数の中に書かれたthisは別のもの(この場合window)を指してしまうようになるため
self変数を作ってMainSceneクラスへの参照を退避させています
アロー関数使えって?確かに……
.colObjBelongTreeからその部屋に含まれているオブジェクトの集合を取得できるな

let self = this;
function collisionPairPullOut(hierNo, roomNo){
	
	let collisionPairList = [];
	
	// 自分の部屋のオブジェクトリストを取得
	let objSet = self.colObjBelongTree[hierNo][roomNo];
	objSet.length = objSet.size;
	let objList = Array.from(objSet)
	
	return collisionPairList;
}
オブジェクトのペアを作る関数はリストで受け取る形になっているので
Set→リストへ変換しています
objSet.length = objSet.size;という処理については妙なことしてると思う方もいるかもしれませんが
ちょっとした事情で入れてます(端的にいえばphina.jsの不具合)
で、自分のところのオブジェクトのリストは
前回作っておいた
makeCombiList こちらもMainSceneクラスの中にあるためthis(self)を前につける必要があります
でできるね

let self = this;
function collisionPairPullOut(hierNo, roomNo){
	
	let collisionPairList = [];
	
	// 自分の部屋のオブジェクトリストを取得
	let myObjSet = self.colObjBelongTree[hierNo][roomNo];
	myObjSet.length = myObjSet.size;
	let myObjList = Array.from(myObjSet)
	
	// 祖先と自分の部屋のオブジェクト同士
	// TODO
	
	// 自分の部屋のオブジェクト同士
	let myPairList = self.makeCombiList(myObjList);
	collisionPairList = collisionPairList.concat(myPairList);
	
	// 自分の子孫に対して同じ処理を繰り返す
	// TODO
	
	return collisionPairList;
}
自分の部屋のオブジェクト同士はできたが祖先とのオブジェクト同士はどうすればいいんだ?
その前に自分の子孫に対して同じ処理を繰り返す部分を書こう
ただ、ファイル数取得の場合は自分の中にあるフォルダを取得する処理があったからいいけど
今回は取得できるのはただのSetで自分の子孫に関する情報はないんだよね
でもまあ自分の子には固定値の4つだけ部屋があるって決まってるから
固定のfor文でぐるぐるまわしちゃおう
ちなみに子の部屋番号は自分の部屋番号を4倍+0~3で求められるよ
例によって使いまわし
H2R9なら自分の一つ下の部屋はH3のR36からR39、9×4+0~3になってるね
9は二進数で[10][01]、4倍は左に2シフトということだから[10][01][00]=36
一番右のデュエットに[00]~[11]の4つを当てはめるっていうことになるね

let self = this;
function collisionPairPullOut(hierNo, roomNo){
	
	let collisionPairList = [];
	
	// 自分の部屋のオブジェクトリストを取得
	let myObjSet = self.colObjBelongTree[hierNo][roomNo];
	myObjSet.length = myObjSet.size;
	let myObjList = Array.from(myObjSet)
	
	// 祖先と自分の部屋のオブジェクト同士
	// TODO
	
	// 自分の部屋のオブジェクト同士
	let myPairList = self.makeCombiList(myObjList);
	collisionPairList = collisionPairList.concat(myPairList);
	
	// 自分の子孫に対して同じ処理を繰り返す
	for(let i=0; i<4; i++){
		let childPairList = collisionPairPullOut(hierNo+1, roomNo*4+i);
		collisionPairList = collisionPairList.concat(childPairList);
	}
	
	return collisionPairList;
}
for文の中でcollisionPairPullOutを呼び出すと
その時は階層に+1、部屋は大体4倍……
……これ、呼び出すたびに階層も部屋も無限に増えていかねえか?
ファイル数取得の場合だと中にあるフォルダが0件ならそのままそのフォルダは終わりだったんだけど
今回はぐるぐる回って(ちゃんと指定した階層で止めて)出ていけぇ!しないと無限ループになっちゃうから
ちゃんと停止条件を指定してあげよう

let self = this;
function collisionPairPullOut(hierNo, roomNo){
	
	let collisionPairList = [];
	
	// 自分の部屋のオブジェクトリストを取得
	let myObjSet = self.colObjBelongTree[hierNo][roomNo];
	myObjSet.length = myObjSet.size;
	let myObjList = Array.from(myObjSet)
	
	// 祖先と自分の部屋のオブジェクト同士
	// TODO
	
	// 自分の部屋のオブジェクト同士
	let myPairList = self.makeCombiList(myObjList);
	collisionPairList = collisionPairList.concat(myPairList);
	
	// 自分の子孫に対して同じ処理を繰り返す
	if(hierNo >= self.hierarchyLevel) {return collisionPairList;}
	for(let i=0; i<4; i++){
		let childPairList = collisionPairPullOut(hierNo+1, roomNo*4+i);
		collisionPairList = collisionPairList.concat(childPairList);
	}
	
	return collisionPairList;
}
さて、ファイル数取得と衝突予想ペア取得の最大の違いは
自分のとこのオブジェクトのリストを子孫にちゃんと渡してあげないとダメっていう点なんだけど
これは引数に入れてあげよう

let self = this;
function collisionPairPullOut(hierNo, roomNo){
	
	let collisionPairList = [];
	
	// 自分の部屋のオブジェクトリストを取得
	let myObjSet = self.colObjBelongTree[hierNo][roomNo];
	myObjSet.length = myObjSet.size;
	let myObjList = Array.from(myObjSet)
	
	// 祖先と自分の部屋のオブジェクト同士
	// TODO
	
	// 自分の部屋のオブジェクト同士
	let myPairList = self.makeCombiList(myObjList);
	collisionPairList = collisionPairList.concat(myPairList);
	
	// 自分の子孫に対して同じ処理を繰り返す
	if(hierNo >= self.hierarchyLevel) {return collisionPairList;}
	for(let i=0; i<4; i++){
		let childPairList = collisionPairPullOut(hierNo+1, roomNo*4+i, myObjList);
		collisionPairList = collisionPairList.concat(childPairList);
	}
	
	return collisionPairList;
}
再帰の呼び出し部分に引数を追加したから
定義のほうでも引数を追加する必要があるな
で、こっちのほうは親のほうからくるオブジェクトのリストだからparentとなると

let self = this;
function collisionPairPullOut(hierNo, roomNo, parentObjList){
	
	let collisionPairList = [];
	
	// 自分の部屋のオブジェクトリストを取得
	let myObjSet = self.colObjBelongTree[hierNo][roomNo];
	myObjSet.length = myObjSet.size;
	let myObjList = Array.from(myObjSet)
	
	// 祖先と自分の部屋のオブジェクト同士
	// TODO
	
	// 自分の部屋のオブジェクト同士
	let myPairList = self.makeCombiList(myObjList);
	collisionPairList = collisionPairList.concat(myPairList);
	
	// 自分の子孫に対して同じ処理を繰り返す
	if(hierNo >= self.hierarchyLevel) {return collisionPairList;}
	for(let i=0; i<4; i++){
		let childPairList = collisionPairPullOut(hierNo+1, roomNo*4+i, myObjList);
		collisionPairList = collisionPairList.concat(childPairList);
	}
	
	return collisionPairList;
}
そして祖先と自分の部屋のオブジェクト同士を作る
前回用意したmakeCrossJoinListをつかおう

let self = this;
function collisionPairPullOut(hierNo, roomNo, parentObjList){
	
	let collisionPairList = [];
	
	// 自分の部屋のオブジェクトリストを取得
	let myObjSet = self.colObjBelongTree[hierNo][roomNo];
	myObjSet.length = myObjSet.size;
	let myObjList = Array.from(myObjSet)
	
	// 祖先と自分の部屋のオブジェクト同士
	let myAndParentPairList = self.makeCrossJoinList(parentObjList, myObjList);
	collisionPairList = collisionPairList.concat(myAndParentPairList);
	
	// 自分の部屋のオブジェクト同士
	let myPairList = self.makeCombiList(myObjList);
	collisionPairList = collisionPairList.concat(myPairList);
	
	// 自分の子孫に対して同じ処理を繰り返す
	if(hierNo >= self.hierarchyLevel) {return collisionPairList;}
	for(let i=0; i<4; i++){
		let childPairList = collisionPairPullOut(hierNo+1, roomNo*4+i, myObjList);
		collisionPairList = collisionPairList.concat(childPairList);
	}
	
	return collisionPairList;
}
そして再帰の部分で引数に渡すオブジェクトのリストは
自分の部屋の分だけじゃなくて祖先の部屋のオブジェクトも入れてあげないといけないから
concatで結合したものを引数にしたよ

let self = this;
function collisionPairPullOut(hierNo, roomNo, parentObjList){
	
	let collisionPairList = [];
	
	// 自分の部屋のオブジェクトリストを取得
	let myObjSet = self.colObjBelongTree[hierNo][roomNo];
	myObjSet.length = myObjSet.size;
	let myObjList = Array.from(myObjSet)
	
	// 祖先と自分の部屋のオブジェクト同士
	let myAndParentPairList = self.makeCrossJoinList(parentObjList, myObjList);
	collisionPairList = collisionPairList.concat(myAndParentPairList);
	
	// 自分の部屋のオブジェクト同士
	let myPairList = self.makeCombiList(myObjList);
	collisionPairList = collisionPairList.concat(myPairList);
	
	// 自分の子孫に対して同じ処理を繰り返す
	if(hierNo >= self.hierarchyLevel) {return collisionPairList;}
	let mixObjList = parentObjList.concat(myObjList);
	for(let i=0; i<4; i++){
		let childPairList = collisionPairPullOut(hierNo+1, roomNo*4+i, mixObjList);
		collisionPairList = collisionPairList.concat(childPairList);
	}
	
	return collisionPairList;
}
これで関数は完成だな
なんか回りくどい形で組み上げたけど
ジュラル星人の仕業に違いない!!
まあそれは置いといて、関数宣言の後ろで関数の呼び出しをしてあげれば
すべての衝突ペアのリストが取得できるようになるね

let self = this;
function collisionPairPullOut(hierNo, roomNo, parentObjList){
	
	中略
	
	return collisionPairList;
}

let collisionPairList = collisionPairPullOut(0, 0, []);
ちなみにこのコードの動きの流れはこんな感じになるよ
原寸大のgifはこちら→GIF
SVGはこちら→SVG
gifだと勝手に流れが進んでっちゃうけど
SVGのほうは進めるボタンをクリックで一回ずつ流れを追えるからこっちのほうがいいと思うよ
ウィンドウに合わせて画像の大きさを変えられるしね
最後まで見るには242回クリックする必要あるけど……
ちなみに初期状態は上のやつで固定だけどリセット押すたびに配置は変わるようになってるよ
今回はだいぶ長かったな……
いやまあそうっすね……
というわけで無事衝突予想ペアのリストが出来上がったから
次はこのリストを使って実際に衝突判定して、第2回に載せたアレを完成させよう
それじゃ、またね~




とはいっても4分木空間分割の話に関してはもう終わってるんだけどね
オブジェクトが31個ある場合
本来なら465回(ペアが465個ある)ってものを100とか200位にどうやって抑えるか?っていう話で、
それが今回の衝突予想ペアのリスト作成の処理で完成しちゃってるから
ああそうかい