优化四叉树性能

This commit is contained in:
gongxh 2025-05-27 18:19:41 +08:00
parent 91f1dbc355
commit 1b73738c39
6 changed files with 159 additions and 86 deletions

View File

@ -4,6 +4,7 @@
* @Description: * @Description:
*/ */
import { v2, Vec2 } from "cc"; import { v2, Vec2 } from "cc";
import { ShapeType } from "./IShape";
import { Polygon } from "./Polygon"; import { Polygon } from "./Polygon";
// 3|2 // 3|2
@ -12,6 +13,11 @@ import { Polygon } from "./Polygon";
// 矩形的四个点 // 矩形的四个点
export class Box extends Polygon { export class Box extends Polygon {
public get shapeType(): ShapeType {
return ShapeType.BOX;
}
constructor(x: number, y: number, width: number, height: number, tag: number = -1) { constructor(x: number, y: number, width: number, height: number, tag: number = -1) {
let points: Vec2[] = new Array(4); let points: Vec2[] = new Array(4);
points[0] = v2(x, y); points[0] = v2(x, y);

View File

@ -5,20 +5,26 @@
*/ */
import { Rect } from "cc"; import { Rect } from "cc";
import { ShapeType } from "./IShape";
import { Shape } from "./Shape"; import { Shape } from "./Shape";
export class Circle extends Shape { export class Circle extends Shape {
public radius: number; // 半径 public radius: number; // 半径
public get shapeType(): ShapeType {
return ShapeType.CIRCLE;
}
constructor(radius: number, tag: number = -1) { constructor(radius: number, tag: number = -1) {
super(tag); super(tag);
this.radius = radius; this.radius = radius;
this.boundingBox.x = -this.radius; this._boundingBox.x = -this.radius;
this.boundingBox.y = -this.radius; this._boundingBox.y = -this.radius;
this.boundingBox.width = this.radius * 2; this._boundingBox.width = this.radius * 2;
this.boundingBox.height = this.radius * 2; this._boundingBox.height = this.radius * 2;
} }
public getBoundingBox(): Rect { public getBoundingBox(): Rect {
return this.boundingBox; return this._boundingBox;
} }
} }

32
src/quadtree/IShape.ts Normal file
View File

@ -0,0 +1,32 @@
/**
* @Author: Gongxh
* @Date: 2025-05-27
* @Description:
*/
import { Rect, Vec2 } from "cc";
export enum ShapeType {
CIRCLE = 1,
BOX = 2,
POLYGON = 3,
}
export interface IShape {
/** 形状类型 */
get shapeType(): ShapeType;
/** 形状掩码 @internal */
get mask(): number;
/** 是否有效 @internal */
get isValid(): boolean;
get position(): Vec2;
get scale(): number;
get rotation(): number;
/** 获取包围盒 */
getBoundingBox(): Rect;
setPosition(x: number, y: number): void;
setScale(value: number): void;
setRotation(angle: number): void;
destroy(): void;
}

View File

@ -5,6 +5,7 @@
*/ */
import { Rect, v2, Vec2 } from "cc"; import { Rect, v2, Vec2 } from "cc";
import { ShapeType } from "./IShape";
import { Shape } from "./Shape"; import { Shape } from "./Shape";
@ -30,6 +31,11 @@ function rotate(radians: number, x: number, y: number): Vec2 {
export class Polygon extends Shape { export class Polygon extends Shape {
protected _points: Vec2[] = []; // 多边形 protected _points: Vec2[] = []; // 多边形
protected _realPoints: Vec2[]; protected _realPoints: Vec2[];
public get shapeType(): ShapeType {
return ShapeType.POLYGON;
}
constructor(points: Vec2[], tag: number = -1) { constructor(points: Vec2[], tag: number = -1) {
super(tag); super(tag);
this._points = points; this._points = points;
@ -41,7 +47,7 @@ export class Polygon extends Shape {
} }
public getBoundingBox(): Rect { public getBoundingBox(): Rect {
if (this.isDirty) { if (this._isDirty) {
let minX = Number.MAX_VALUE; let minX = Number.MAX_VALUE;
let maxX = Number.MIN_VALUE; let maxX = Number.MIN_VALUE;
let minY = Number.MAX_VALUE; let minY = Number.MAX_VALUE;
@ -53,13 +59,13 @@ export class Polygon extends Shape {
maxX = Math.max(maxX, a.x); maxX = Math.max(maxX, a.x);
maxY = Math.max(maxY, a.y); maxY = Math.max(maxY, a.y);
} }
this.boundingBox.x = minX; this._boundingBox.x = minX;
this.boundingBox.y = minY; this._boundingBox.y = minY;
this.boundingBox.width = maxX - minX; this._boundingBox.width = maxX - minX;
this.boundingBox.height = maxY - minY; this._boundingBox.height = maxY - minY;
this.isDirty = false; this._isDirty = false;
} }
return this.boundingBox; return this._boundingBox;
} }
public get points(): Vec2[] { public get points(): Vec2[] {

View File

@ -5,10 +5,9 @@
*/ */
import { Graphics, Intersection2D, rect, Rect } from "cc"; import { Graphics, Intersection2D, rect, Rect } from "cc";
import { Box } from "./Box";
import { Circle } from "./Circle"; import { Circle } from "./Circle";
import { IShape, ShapeType } from "./IShape";
import { Polygon } from "./Polygon"; import { Polygon } from "./Polygon";
import { Shape } from "./Shape";
// 1|0 // 1|0
// --- // ---
@ -25,18 +24,18 @@ const circleCircle = Intersection2D.circleCircle;
const polygonCircle = Intersection2D.polygonCircle; const polygonCircle = Intersection2D.polygonCircle;
const polygonPolygon = Intersection2D.polygonPolygon; const polygonPolygon = Intersection2D.polygonPolygon;
/** 两个形状是否碰撞 */ /** 两个形状是否碰撞 */
function isCollide(shape1: Shape, shape2: Shape): boolean { function isCollide(shape1: IShape, shape2: IShape): boolean {
if (shape1 instanceof Circle) { if (shape1.shapeType === ShapeType.CIRCLE) {
if (shape2 instanceof Circle) { if (shape2.shapeType === ShapeType.CIRCLE) {
return circleCircle(shape1.position, shape1.radius * shape1.scale, shape2.position, shape2.radius * shape2.scale); return circleCircle(shape1.position, (shape1 as Circle).radius * shape1.scale, shape2.position, (shape2 as Circle).radius * shape2.scale);
} else if (shape2 instanceof Box || shape2 instanceof Polygon) { } else if (shape2.shapeType === ShapeType.BOX || shape2.shapeType === ShapeType.POLYGON) {
return polygonCircle(shape2.points, shape1.position, shape1.radius * shape1.scale); return polygonCircle((shape2 as Polygon).points, shape1.position, (shape1 as Circle).radius * shape1.scale);
} }
} else if (shape1 instanceof Box || shape1 instanceof Polygon) { } else if (shape1.shapeType === ShapeType.BOX || shape1.shapeType === ShapeType.POLYGON) {
if (shape2 instanceof Circle) { if (shape2.shapeType === ShapeType.CIRCLE) {
return polygonCircle(shape1.points, shape2.position, shape2.radius * shape2.scale); return polygonCircle((shape1 as Polygon).points, shape2.position, (shape2 as Circle).radius * shape2.scale);
} else if (shape2 instanceof Box || shape2 instanceof Polygon) { } else if (shape2.shapeType === ShapeType.BOX || shape2.shapeType === ShapeType.POLYGON) {
return polygonPolygon(shape2.points, shape1.points); return polygonPolygon((shape2 as Polygon).points, (shape1 as Polygon).points);
} }
} }
return false; return false;
@ -53,7 +52,7 @@ export class QuadTree {
/** @internal */ /** @internal */
private _graphics: Graphics; private _graphics: Graphics;
/** @internal */ /** @internal */
private _shapes_map: Map<number, Shape[]>; // 根据类型存储形状对象 private _shapes_map: Map<number, IShape[]>; // 根据类型存储形状对象
/** @internal */ /** @internal */
private _trees: QuadTree[] = []; // 存储四个子节点 private _trees: QuadTree[] = []; // 存储四个子节点
/** @internal */ /** @internal */
@ -61,7 +60,7 @@ export class QuadTree {
/** @internal */ /** @internal */
private _bounds: Rect; // 树的外框 private _bounds: Rect; // 树的外框
/** @internal */ /** @internal */
private _ignore_shapes: Shape[] = []; // 不在树中的形状 private _ignore_shapes: IShape[] = []; // 不在树中的形状
/** /**
* *
* @param rect * @param rect
@ -82,7 +81,7 @@ export class QuadTree {
* *
* *
*/ */
public insert(shape: Shape): void { public insert(shape: IShape): void {
// 如果该节点下存在子节点 // 如果该节点下存在子节点
if (this._trees.length > 0) { if (this._trees.length > 0) {
let quadrant = this._getQuadrant(shape); let quadrant = this._getQuadrant(shape);
@ -114,28 +113,28 @@ export class QuadTree {
} }
/** @internal */ /** @internal */
private _insert(shape: Shape): void { private _insert(shape: IShape): void {
if (!this._shapes_map.has(shape.tag)) { if (!this._shapes_map.has(shape.mask)) {
this._shapes_map.set(shape.tag, []); this._shapes_map.set(shape.mask, []);
} }
this._shapes_map.get(shape.tag).push(shape); this._shapes_map.get(shape.mask).push(shape);
} }
/** /**
* *
* ... * ...
*/ */
public collide(shape: Shape, tag: number = -1): Shape[] { public collide(shape: IShape, tag: number = -1): IShape[] {
let result: any[] = []; let result: IShape[] = [];
if (this._trees.length > 0) { if (this._trees.length > 0) {
let quadrant = this._getQuadrant(shape); let quadrant = this._getQuadrant(shape);
if (quadrant === Quadrant.MORE) { if (quadrant === Quadrant.MORE) {
let len = this._trees.length - 1; let len = this._trees.length - 1;
for (let i = len; i >= 0; i--) { for (let i = len; i >= 0; i--) {
result = result.concat(this._trees[i].collide(shape, tag)); result.push(...this._trees[i].collide(shape, tag));
} }
} else { } else {
result = result.concat(this._trees[quadrant].collide(shape, tag)); result.push(...this._trees[quadrant].collide(shape, tag));
} }
} }
@ -145,7 +144,7 @@ export class QuadTree {
} }
let shapes = this._shapes_map.get(key); let shapes = this._shapes_map.get(key);
for (const other_shape of shapes) { for (const other_shape of shapes) {
if (other_shape.valid && shape !== other_shape && isCollide(shape, other_shape)) { if (other_shape.isValid && shape !== other_shape && isCollide(shape, other_shape)) {
result.push(other_shape); result.push(other_shape);
} }
} }
@ -185,7 +184,7 @@ export class QuadTree {
} }
/** 当前形状是否包含在象限内 @internal */ /** 当前形状是否包含在象限内 @internal */
private _isInner(shape: Shape, bounds: Rect): boolean { private _isInner(shape: IShape, bounds: Rect): boolean {
let rect = shape.getBoundingBox(); let rect = shape.getBoundingBox();
return ( return (
rect.xMin * shape.scale + shape.position.x > bounds.xMin && rect.xMin * shape.scale + shape.position.x > bounds.xMin &&
@ -204,7 +203,7 @@ export class QuadTree {
* *
* @internal * @internal
*/ */
private _getQuadrant(shape: Shape): Quadrant { private _getQuadrant(shape: IShape): Quadrant {
let bounds = this._bounds; let bounds = this._bounds;
let rect = shape.getBoundingBox(); let rect = shape.getBoundingBox();
let center = bounds.center; let center = bounds.center;
@ -262,42 +261,66 @@ export class QuadTree {
/** 更新忽略掉的形状 @internal */ /** 更新忽略掉的形状 @internal */
private _updateIgnoreShapes(root: QuadTree): void { private _updateIgnoreShapes(root: QuadTree): void {
let len = this._ignore_shapes.length; let shapes = this._ignore_shapes;
if (len <= 0) { let lastIndex = shapes.length - 1;
return; let index = 0;
while (index < lastIndex) {
let shape = shapes[index];
if (!shape.isValid) {
if (index !== lastIndex) {
shapes[index] = shapes[lastIndex];
} }
for (let i = len - 1; i >= 0; i--) { shapes.pop();
let shape = this._ignore_shapes[i]; lastIndex--;
if (!shape.valid) {
this._ignore_shapes.splice(i, 1);
continue; continue;
} }
if (!this._isInner(shape, this._bounds)) { if (this._isInner(shape, this._bounds)) {
continue; if (index !== lastIndex) {
[shapes[index], shapes[lastIndex]] = [shapes[lastIndex], shapes[index]];
}
root.insert(shapes.pop());
} else {
index++;
} }
root.insert(this._ignore_shapes.splice(i, 1)[0]);
} }
} }
/** 更新有效的形状 @internal */ /** 更新有效的形状 @internal */
private _updateShapes(root: QuadTree): void { private _updateShapes(root: QuadTree): void {
for (const shapes of this._shapes_map.values()) { for (const shapes of this._shapes_map.values()) {
let len = shapes.length; let lastIndex = shapes.length - 1;
for (let i = len - 1; i >= 0; i--) { let index = 0;
let shape = shapes[i]; while (index <= lastIndex) {
if (!shape.valid) { let shape = shapes[index];
shapes.splice(i, 1); if (!shape.isValid) {
if (index !== lastIndex) {
shapes[index] = shapes[lastIndex];
}
shapes.pop();
lastIndex--;
continue; continue;
} }
if (!this._isInner(shape, this._bounds)) { if (!this._isInner(shape, this._bounds)) {
// 如果矩形不属于该象限,则将该矩形重新插入根节点 // 如果矩形不属于该象限,则将该矩形重新插入根节点
root.insert(shapes.splice(i, 1)[0]); if (index !== lastIndex) {
[shapes[index], shapes[lastIndex]] = [shapes[lastIndex], shapes[index]];
}
root.insert(shapes.pop());
lastIndex--;
} else if (this._trees.length > 0) { } else if (this._trees.length > 0) {
// 如果矩形属于该象限且该象限具有子象限,则将该矩形安插到子象限中 // 如果矩形属于该象限且该象限具有子象限,则将该矩形安插到子象限中
let quadrant = this._getQuadrant(shape); let quadrant = this._getQuadrant(shape);
if (quadrant !== Quadrant.MORE) { if (quadrant !== Quadrant.MORE) {
this._trees[quadrant].insert(shapes.splice(i, 1)[0]); if (index !== lastIndex) {
[shapes[index], shapes[lastIndex]] = [shapes[lastIndex], shapes[index]];
} }
this._trees[quadrant].insert(shapes.pop());
lastIndex--;
} else {
index++;
}
} else {
index++;
} }
} }
} }

View File

@ -5,23 +5,20 @@
*/ */
import { Rect, Vec2 } from "cc"; import { Rect, Vec2 } from "cc";
import { IShape, ShapeType } from "./IShape";
export abstract class Shape { export abstract class Shape implements IShape {
/** /**
* * & -1
* &
* -1
*/ */
public tag: number = -1; private _mask: number = -1;
protected _scale: number;
/** 缩放 */
public scale: number; // 缩放
/** 脏标记 用来重置包围盒 @internal */ /** 脏标记 用来重置包围盒 @internal */
protected isDirty: boolean; protected _isDirty: boolean;
/** 包围盒 @internal */ /** 包围盒 @internal */
protected boundingBox: Rect; protected _boundingBox: Rect;
/** 位置 @internal */ /** 位置 @internal */
protected _position: Vec2; protected _position: Vec2;
@ -32,12 +29,20 @@ export abstract class Shape {
/** 是否有效 下次更新时删除 @internal */ /** 是否有效 下次更新时删除 @internal */
private _valid: boolean = true; private _valid: boolean = true;
constructor(tag: number) { public abstract get shapeType(): ShapeType;
this.tag = tag;
this.scale = 1.0; public get mask(): number { return this._mask; }
public get position(): Vec2 { return this._position; }
public get scale(): number { return this._scale; }
public get rotation(): number { return this._rotation; }
public get isValid(): boolean { return this._valid; }
constructor(mask: number) {
this._mask = mask;
this._scale = 1.0;
this._rotation = 0; this._rotation = 0;
this.isDirty = true; this._isDirty = true;
this.boundingBox = new Rect(); this._boundingBox = new Rect();
this._position = new Vec2(); this._position = new Vec2();
} }
@ -46,30 +51,25 @@ export abstract class Shape {
this._position.y = y; this._position.y = y;
} }
get position(): Vec2 { public setRotation(angle: number) {
return this._position;
}
set rotation(angle: number) {
if (this._rotation !== angle) { if (this._rotation !== angle) {
this._rotation = angle; this._rotation = angle;
this.isDirty = true; this._isDirty = true;
} }
} }
get rotation(): number { public setScale(value: number) {
return this._rotation; if (this._scale !== value) {
this._scale = value;
this._isDirty = true;
} }
public get valid(): boolean {
return this._valid;
} }
/** 包围盒 子类重写 */ /** 包围盒 子类重写 */
public abstract getBoundingBox(): Rect; public abstract getBoundingBox(): Rect;
public destroy(): void { public destroy(): void {
this._valid = false; this._valid = false;
} }
} }