版本:2.4.10
参考:
一 演示效果
碰撞红色,未碰撞蓝色。
二 二叉树、四叉树、八叉树
二叉树:树形结构,每个节点最多2个子树。
四叉树:树状数据结构,每个节点有四个子区块。
八叉树:描述三维空间的树状结构,任意子节点有0或8个。
二叉树 四叉树 八叉树
三 普通碰撞检测
场景中有500个矩形小球,要进行碰撞检测,普通的检测就是遍历一遍小球数组,两两进行碰撞检测。
遍历小球数组,使用函数rectRect()进行碰撞检测。
for (let i = 0; i < this.ballList.length; i++) { for (let j = i + 1; j < this.ballList.length; j++) { count++; let ballA = this.ballList[i]; let ballB = this.ballList[j]; if (this.rectRect(ballA.node, ballB.node)) { console.log("碰撞"); } } }
这种方式需要遍历每一个小球,造成大量的计算。500个小球需要进行499500次碰撞检测,消耗时间大约10-11毫秒。
三 划分区域进行碰撞检测(四叉树)
将场景划为为不同区域,小球只和自己同一区域的小球进行碰撞检测,这样可以大量减少碰撞次数。
这种方式需要额外计算每个小球属于哪一个区域,但是对比碰撞检测的消耗,还是比较节省效率的。
遍历小球列表ballList,计算小球所在格子的行列值,将小球保存到对应的格子列表中。
for (let i = 0; i < this.ballList.length; i++) { let ball = this.ballList[i]; let row = Math.floor((ball.node.y + this.maxHeight) / this.gridSize); let col = Math.floor((ball.node.x + this.maxWidth) / this.gridSize); ball.row = row; ball.col = col; this.gridList[row][col].push(ball); }
遍历小球列表ballList,根据小球的行列值row、col可以从格子gridLsit中获取同一区域的小球。每个小球只和自己同一区域的小球进行碰撞检测。
for (let i = 0; i < this.ballList.length; i++) { let ballA = this.ballList[i]; let list = this.gridList[ballA.row][ballA.col]; for (let j = 0; j < list.length; j++) { let ballB = list[j]; if (ballA != ballB) { if (this.rectRect(ballA.node, ballB.node)) { console.log("碰撞"); } } } }
检测次数大约67364次,耗时4毫秒。对比普通检测,检测次数从499500变成了67364,耗时从10毫秒变成了4毫秒。
四 完整代码
Ball.ts:
const { ccclass, property } = cc._decorator; /** * 小球 * @author chenkai 2022.9.9 */ @ccclass export default class Ball extends cc.Component { /**行 */ public row: number = 0; /**列 */ public col: number = 0; /**移动速度 */ public speed: number = 2; /**x轴速度 */ public xSpeed: number = 0; /**y轴速度 */ public ySpeed: number = 0; }
MainScene.ts:
const { ccclass, property } = cc._decorator; /** * 主场景 * @author chenkai 2022.9.9 */ @ccclass export default class MainScene extends cc.Component { @property({ type: cc.Prefab, tooltip: "球(矩形)" }) pb_ball: cc.Prefab = null; /**舞台宽度/2 */ private maxWidth: number; /**舞台高度/2 */ private maxHeight: number; /**小球列表 */ private ballList: Ball[] = []; /**格子区域二位数组 */ private gridList = []; /**格子行数 */ private gridRow: number = 4; /**格子列数 */ private gridCol: number = 4; /**格子高宽 */ private gridSize: number = 400; /**检查类型 1普通检测 2划为区域检测 */ private checkType: number = 2; onLoad() { //舞台边缘值 this.maxWidth = cc.view.getVisibleSize().width / 2; this.maxHeight = cc.view.getVisibleSize().height / 2; //格子列表 for (let i = 0; i < this.gridRow; i++) { this.gridList[i] = []; for (let j = 0; j < this.gridCol; j++) { this.gridList[i][j] = []; } } //创建小球 this.createBall(); } update(dt) { this.updateBallMove(); let startTime = new Date().getTime(); //普通碰撞检测 if (this.checkType == 1) { this.checkNormalCollision(); //划分格子碰撞检测 } else { this.updateBallGrid(); this.checkCollision(); } console.log("消耗时间:", new Date().getTime() - startTime); } /**普通的碰撞检测 */ private checkNormalCollision() { //将小球置蓝色 for (let i = 0; i < this.ballList.length; i++) { this.ballList[i].node.color = new cc.Color().fromHEX("#0000ff"); } //碰撞检测 let count = 0; for (let i = 0; i < this.ballList.length; i++) { for (let j = i + 1; j < this.ballList.length; j++) { count++; let ballA = this.ballList[i]; let ballB = this.ballList[j]; if (this.rectRect(ballA.node, ballB.node)) { ballA.node.color = new cc.Color().fromHEX("#ff0000"); ballB.node.color = new cc.Color().fromHEX("#ff0000"); } } } console.log("计算次数:", count); } /**创建小球 */ private createBall() { for (let i = 0; i < 1000; i++) { //随机位置 let node: cc.Node = cc.instantiate(this.pb_ball); node.parent = this.node; node.x = Math.random() * this.maxWidth * 2 - this.maxWidth; node.y = Math.random() * this.maxHeight * 2 - this.maxHeight; //随机速度 let ball: Ball = node.getComponent(Ball); ball.xSpeed = Math.random() * ball.speed; ball.ySpeed = Math.random() * ball.speed; this.ballList.push(ball); } } /**刷新小球移动 */ private updateBallMove() { let len = this.ballList.length; let ball: Ball; for (let i = 0; i < len; i++) { ball = this.ballList[i]; //移动 ball.node.x += ball.xSpeed; ball.node.y += ball.ySpeed; //边缘检测 达到边缘后速度取反 if (ball.node.x + ball.node.width / 2 > this.maxWidth) { ball.node.x = this.maxWidth - ball.node.width / 2; ball.xSpeed = -ball.speed; } else if (ball.node.x - ball.node.width / 2 < -this.maxWidth) { ball.node.x = - this.maxWidth + ball.node.width / 2; ball.xSpeed = ball.speed; } if (ball.node.y + ball.node.height / 2 > this.maxHeight) { ball.node.y = this.maxHeight - ball.node.height / 2; ball.ySpeed = -ball.speed; } else if (ball.node.y - ball.node.height / 2 < -this.maxHeight) { ball.node.y = -this.maxHeight + ball.node.height / 2; ball.ySpeed = ball.speed; } } } /**刷新小球所在格子 */ private updateBallGrid() { //清理格子 for (let i = 0; i < this.gridRow; i++) { for (let j = 0; j < this.gridCol; j++) { this.gridList[i][j].length = 0; } } //将小球置蓝色,重新计算小球所属行列的格子 for (let i = 0; i < this.ballList.length; i++) { let ball = this.ballList[i]; let row = Math.floor((ball.node.y + this.maxHeight) / this.gridSize); let col = Math.floor((ball.node.x + this.maxWidth) / this.gridSize); ball.row = row; ball.col = col; this.gridList[row][col].push(ball); ball.node.color = new cc.Color().fromHEX("#0000ff"); } } /**碰撞检测 */ private checkCollision() { let count = 0; for (let i = 0; i < this.ballList.length; i++) { let ballA = this.ballList[i]; let list = this.gridList[ballA.row][ballA.col]; for (let j = 0; j < list.length; j++) { count++; let ballB = list[j]; if (ballA != ballB) { if (this.rectRect(ballA.node, ballB.node)) { ballA.node.color = new cc.Color().fromHEX("#ff0000"); ballB.node.color = new cc.Color().fromHEX("#ff0000"); } } } } console.log("检查次数:", count); } /** * cc.Intersection.rectRect * @param a * @param b * @returns true碰撞 false未碰撞 */ private rectRect(a: cc.Node, b: cc.Node) { var a_min_x = a.x - a.width / 2; var a_min_y = a.y - a.height / 2; var a_max_x = a.x + a.width / 2; var a_max_y = a.y + a.height / 2; var b_min_x = b.x - b.width / 2; var b_min_y = b.y - b.height / 2; var b_max_x = b.x + b.width / 2; var b_max_y = b.y + b.height / 2; return a_min_x <= b_max_x && a_max_x >= b_min_x && a_min_y <= b_max_y && a_max_y >= b_min_y; } }