first commit

This commit is contained in:
宫欣海
2025-02-20 11:27:28 +08:00
commit 68090ca38d
91 changed files with 9915 additions and 0 deletions

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

@@ -0,0 +1,32 @@
/**
* @Author: Gongxh
* @Date: 2024-12-21
* @Description: 矩形
*/
import { v2, Vec2 } from "cc";
import { Polygon } from "./Polygon";
// 3|2
// --
// 0|1
// 矩形的四个点
export class Box extends Polygon {
constructor(x: number, y: number, width: number, height: number, tag: number = -1) {
let points: Vec2[] = new Array(4);
points[0] = v2(x, y);
points[1] = v2(x + width, y);
points[2] = v2(x + width, y + height);
points[3] = v2(x, y + height);
super(points, tag);
}
public resetPoints(x: number, y: number, width: number, height: number): void {
let points: Vec2[] = new Array(4);
points[0] = v2(x, y);
points[1] = v2(x + width, y);
points[2] = v2(x + width, y + height);
points[3] = v2(x, y + height);
this.points = points;
}
}

28
src/quadtree/Circle.ts Normal file
View File

@@ -0,0 +1,28 @@
/**
* @Author: Gongxh
* @Date: 2024-12-21
* @Description: 原型
*/
import { Graphics, Rect } from "cc";
import { Shape } from "./Shape";
export class Circle extends Shape {
public radius: number; // 半径
constructor(radius: number, tag: number = -1) {
super(tag);
this.radius = radius;
this.boundingBox.x = -this.radius;
this.boundingBox.y = -this.radius;
this.boundingBox.width = this.radius * 2;
this.boundingBox.height = this.radius * 2;
}
public getBoundingBox(): Rect {
return this.boundingBox;
}
public drawShape(draw: Graphics): void {
draw && draw.circle(this.position.x, this.position.y, this.radius * this.scale);
}
}

96
src/quadtree/Polygon.ts Normal file
View File

@@ -0,0 +1,96 @@
/**
* @Author: Gongxh
* @Date: 2024-12-21
* @Description: 多边形
*/
import { Graphics, Rect, v2, Vec2 } from "cc";
import { Shape } from "./Shape";
const vec2 = new Vec2();
/** 点绕原点旋转 radians 弧度后的新点 */
function rotate(radians: number, x: number, y: number): Vec2 {
let sin = Math.sin(radians);
let cos = Math.cos(radians);
vec2.x = x * cos - y * sin;
vec2.y = y * cos + x * sin;
return vec2;
}
// /** 点绕点旋转 radians 弧度后的新点 */
// export function rotateByPoint(radians: number, x: number, y: number, cx: number, cy: number): Vec2 {
// let sin = Math.sin(radians);
// let cos = Math.cos(radians);
// vec2.x = (x - cx) * cos - (y - cy) * sin + cx;
// vec2.y = (y - cy) * cos + (x - cx) * sin + cy;
// return vec2;
// }
export class Polygon extends Shape {
protected _points: Vec2[] = []; // 多边形
protected _realPoints: Vec2[];
constructor(points: Vec2[], tag: number = -1) {
super(tag);
this._points = points;
this._realPoints = new Array(points.length);
for (let i = 0, len = points.length; i < len; i++) {
this._realPoints[i] = v2(points[i].x, points[i].y);
}
this.getBoundingBox();
}
public getBoundingBox(): Rect {
if (this.isDirty) {
let minX = Number.MAX_VALUE;
let maxX = Number.MIN_VALUE;
let minY = Number.MAX_VALUE;
let maxY = Number.MIN_VALUE;
for (const point of this._points) {
let a = rotate(Math.PI / 180 * this._rotation, point.x, point.y);
minX = Math.min(minX, a.x);
minY = Math.min(minY, a.y);
maxX = Math.max(maxX, a.x);
maxY = Math.max(maxY, a.y);
}
this.boundingBox.x = minX;
this.boundingBox.y = minY;
this.boundingBox.width = maxX - minX;
this.boundingBox.height = maxY - minY;
this.isDirty = false;
}
return this.boundingBox;
}
public get points(): Vec2[] {
let points = this._points;
let len = points.length;
for (let i = 0; i < len; i++) {
let m = points[i];
this._realPoints[i] = m.rotate(Math.PI / 180 * this.rotation);
let a = this._realPoints[i];
a.x = a.x * this.scale + this.position.x;
a.y = a.y * this.scale + this.position.y;
}
return this._realPoints;
}
public set points(pts: Vec2[]) {
this._points = pts;
this._realPoints = new Array(pts.length);
for (let i = 0, len = pts.length; i < len; i++) {
this._realPoints[i] = v2(pts[i].x, pts[i].y);
}
}
public drawShape(draw: Graphics): void {
if (draw) {
let points = this.points;
draw.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
draw.lineTo(points[i].x, points[i].y);
}
draw.lineTo(points[0].x, points[0].y);
}
}
}

341
src/quadtree/QuadTree.ts Normal file
View File

@@ -0,0 +1,341 @@
/**
* @Author: Gongxh
* @Date: 2024-12-21
* @Description: 树节点
*/
import { Color, Graphics, Intersection2D, rect, Rect } from "cc";
import { Box } from "./Box";
import { Circle } from "./Circle";
import { Polygon } from "./Polygon";
import { Shape } from "./Shape";
// 1|0
// ---
// 2|3
const enum Quadrant {
ONE = 0,
TWO,
THREE,
FOUR,
MORE, // 多个象限
}
const circleCircle = Intersection2D.circleCircle;
const polygonCircle = Intersection2D.polygonCircle;
const polygonPolygon = Intersection2D.polygonPolygon;
/** 两个形状是否碰撞 */
function isCollide(shape1: Shape, shape2: Shape): boolean {
if (shape1 instanceof Circle) {
if (shape2 instanceof Circle) {
return circleCircle(shape1.position, shape1.radius * shape1.scale, shape2.position, shape2.radius * shape2.scale);
} else if (shape2 instanceof Box || shape2 instanceof Polygon) {
return polygonCircle(shape2.points, shape1.position, shape1.radius * shape1.scale);
}
} else if (shape1 instanceof Box || shape1 instanceof Polygon) {
if (shape2 instanceof Circle) {
return polygonCircle(shape1.points, shape2.position, shape2.radius * shape2.scale);
} else if (shape2 instanceof Box || shape2 instanceof Polygon) {
return polygonPolygon(shape2.points, shape1.points);
}
}
return false;
}
export const QTConfig = {
/** 每个节点(象限)所能包含物体的最大数量 */
MAX_SHAPES: 12,
/** 四叉树的最大深度 */
MAX_LEVELS: 5,
}
export class QuadTree {
private _draw: Graphics;
private _shapes_map: Map<number, Shape[]>; // 根据类型存储形状对象
private _trees: QuadTree[] = []; // 存储四个子节点
private _level: number; // 树的深度
private _bounds: Rect; // 树的外框
private _ignore_shapes: Shape[] = []; // 不在树中的形状
/**
* 创建一个四叉树
* @param rect 该节点对应的象限在屏幕上的范围
* @param level 该节点的深度根节点的默认深度为0
* @param draw cc中用于绘制树的绘制组件
*/
constructor(rect: Rect, level: number = 0, draw: Graphics = undefined) {
this._shapes_map = new Map();
this._trees = [];
this._level = level || 0;
this._bounds = rect;
this._draw = draw;
}
/**
* 插入形状
* @param shape 形状数据
* 如果当前节点存在子节点,则检查物体到底属于哪个子节点,如果能匹配到子节点,则将该物体插入到该子节点中
* 如果当前节点不存在子节点,将该物体存储在当前节点。随后,检查当前节点的存储数量,如果超过了最大存储数量,则对当前节点进行划分,划分完成后,将当前节点存储的物体重新分配到四个子节点中。
*/
public insert(shape: Shape): void {
// 如果该节点下存在子节点
if (this._trees.length > 0) {
let quadrant = this._getQuadrant(shape);
if (quadrant !== Quadrant.MORE) {
this._trees[quadrant].insert(shape);
return;
}
}
if (this._level == 0 && !this._isInner(shape, this._bounds)) {
// 插入跟节点并且形状不在根节点的框内,则把形状放入忽略列表中
this._ignore_shapes.push(shape);
} else {
// 存储在当前节点下
this._insert(shape);
// 如果当前节点存储的数量超过了 MAX_OBJECTS并且深度没超过 MAX_LEVELS则继续拆分
if (!this._trees.length && this._size() > QTConfig.MAX_SHAPES && this._level < QTConfig.MAX_LEVELS) {
this._split();
for (const shapes of this._shapes_map.values()) {
let length = shapes.length - 1;
for (let i = length; i >= 0; i--) {
let quadrant = this._getQuadrant(shapes[i]);
if (quadrant !== Quadrant.MORE) {
this._trees[quadrant].insert(shapes.splice(i, 1)[0]);
}
}
}
}
}
}
private _insert(shape: Shape): void {
if (!this._shapes_map.has(shape.tag)) {
this._shapes_map.set(shape.tag, []);
}
this._shapes_map.get(shape.tag).push(shape);
}
/**
* 检索功能:
* 给出一个物体对象,该函数负责将该物体可能发生碰撞的所有物体选取出来。该函数先查找物体所属的象限,该象限下的物体都是有可能发生碰撞的,然后再递归地查找子象限...
*/
public collide(shape: Shape, tag: number = -1): Shape[] {
let result: any[] = [];
if (this._trees.length > 0) {
let quadrant = this._getQuadrant(shape);
if (quadrant === Quadrant.MORE) {
let len = this._trees.length - 1;
for (let i = len; i >= 0; i--) {
result = result.concat(this._trees[i].collide(shape, tag));
}
} else {
result = result.concat(this._trees[quadrant].collide(shape, tag));
}
}
for (const key of this._shapes_map.keys()) {
if (!(tag & key)) {
continue;
}
let shapes = this._shapes_map.get(key);
for (const other_shape of shapes) {
if (!other_shape.invalid && shape !== other_shape && isCollide(shape, other_shape)) {
result.push(other_shape);
}
}
}
return result;
}
/**
* 动态更新
*/
public update(root?: QuadTree): void {
root = root || this;
let isRoot = (root === this);
isRoot && this._strokeClear()
this._updateIgnoreShapes(root);
this._updateShapes(root);
// 递归刷新子象限
for (const tree of this._trees) {
tree.update(root);
}
this._removeChildTree();
this._drawTreeBound(root);
if (isRoot && this._draw) {
this._draw.stroke();
}
}
public clear(): void {
this._level = 0;
this._ignore_shapes.length = 0;
this._shapes_map.clear();
for (const tree of this._trees) {
tree.clear();
}
this._trees.length = 0;
}
/** 当前形状是否包含在象限内 */
private _isInner(shape: Shape, bounds: Rect): boolean {
let rect = shape.getBoundingBox();
return (
rect.xMin * shape.scale + shape.position.x > bounds.xMin &&
rect.xMax * shape.scale + shape.position.x < bounds.xMax &&
rect.yMin * shape.scale + shape.position.y > bounds.yMin &&
rect.yMax * shape.scale + shape.position.y < bounds.yMax
);
}
/**
* 获取形状对应的象限序号,以中心为界限切割:
* @param {Shape} shape 形状
* 右上:象限一
* 左上:象限二
* 左下:象限三
* 右下:象限四
*/
private _getQuadrant(shape: Shape): Quadrant {
let bounds = this._bounds;
let rect = shape.getBoundingBox();
let center = bounds.center;
let onTop = rect.yMin * shape.scale + shape.position.y > center.y;
let onBottom = rect.yMax * shape.scale + shape.position.y < center.y;
let onLeft = rect.xMax * shape.scale + shape.position.x < center.x;
let onRight = rect.xMin * shape.scale + shape.position.x > center.x;
if (onTop) {
if (onRight) {
return Quadrant.ONE;
} else if (onLeft) {
return Quadrant.TWO;
}
} else if (onBottom) {
if (onLeft) {
return Quadrant.THREE;
} else if (onRight) {
return Quadrant.FOUR;
}
}
return Quadrant.MORE; // 跨越多个象限
}
/**
* 划分函数
* 如果某一个象限节点内存储的物体数量超过了MAX_OBJECTS最大数量
* 则需要对这个节点进行划分
* 它的工作就是将一个象限看作一个屏幕,将其划分为四个子象限
*/
private _split(): void {
let bounds = this._bounds;
let x = bounds.x;
let y = bounds.y;
let halfwidth = bounds.width * 0.5;
let halfheight = bounds.height * 0.5;
let nextLevel = this._level + 1;
this._trees.push(
new QuadTree(rect(bounds.center.x, bounds.center.y, halfwidth, halfheight), nextLevel, this._draw),
new QuadTree(rect(x, bounds.center.y, halfwidth, halfheight), nextLevel, this._draw),
new QuadTree(rect(x, y, halfwidth, halfheight), nextLevel, this._draw),
new QuadTree(rect(bounds.center.x, y, halfwidth, halfheight), nextLevel, this._draw)
);
}
/** 删除子树 */
private _removeChildTree(): void {
if (this._trees.length > 0) {
if (this._totalSize() <= 0) {
this._trees.length = 0;
}
}
}
/** 更新忽略掉的形状 */
private _updateIgnoreShapes(root: QuadTree): void {
let len = this._ignore_shapes.length;
if (len <= 0) {
return;
}
for (let i = len - 1; i >= 0; i--) {
let shape = this._ignore_shapes[i];
if (shape.invalid) {
this._ignore_shapes.splice(i, 1);
continue;
}
if (!this._isInner(shape, this._bounds)) {
continue;
}
root.insert(this._ignore_shapes.splice(i, 1)[0]);
}
}
/** 更新有效的形状 */
private _updateShapes(root: QuadTree): void {
for (const shapes of this._shapes_map.values()) {
let len = shapes.length;
for (let i = len - 1; i >= 0; i--) {
let shape = shapes[i];
if (shape.invalid) {
shapes.splice(i, 1);
continue;
}
if (!this._isInner(shape, this._bounds)) {
// 如果矩形不属于该象限,则将该矩形重新插入根节点
root.insert(shapes.splice(i, 1)[0]);
} else if (this._trees.length > 0) {
// 如果矩形属于该象限且该象限具有子象限,则将该矩形安插到子象限中
let quadrant = this._getQuadrant(shape);
if (quadrant !== Quadrant.MORE) {
this._trees[quadrant].insert(shapes.splice(i, 1)[0]);
}
}
shape.drawShape(this._draw);
}
}
}
/** 当前树以及子树中所有的形状数量 */
private _totalSize(): number {
let size = this._size();
for (const tree of this._trees) {
size += tree._totalSize();
}
return size;
}
private _size(): number {
let size = 0;
for (const shapes of this._shapes_map.values()) {
size += shapes.length;
}
return size + this._ignore_shapes.length;
}
/** 画出当前树的边界 */
private _drawTreeBound(root: QuadTree): void {
if (!this._draw) {
return;
}
this._draw.lineWidth = 4;
this._draw.strokeColor = Color.BLUE;
if (this._trees.length > 0) {
this._draw.moveTo(this._bounds.x, this._bounds.center.y);
this._draw.lineTo(this._bounds.x + this._bounds.width, this._bounds.center.y);
this._draw.moveTo(this._bounds.center.x, this._bounds.y);
this._draw.lineTo(this._bounds.center.x, this._bounds.y + this._bounds.height);
}
if (this == root) {
this._draw.moveTo(this._bounds.xMin, this._bounds.yMin);
this._draw.lineTo(this._bounds.xMax, this._bounds.yMin);
this._draw.lineTo(this._bounds.xMax, this._bounds.yMax);
this._draw.lineTo(this._bounds.xMin, this._bounds.yMax);
this._draw.lineTo(this._bounds.xMin, this._bounds.yMin);
}
}
private _strokeClear(): void {
this._draw && this._draw.clear();
}
}

71
src/quadtree/Shape.ts Normal file
View File

@@ -0,0 +1,71 @@
/**
* @Author: Gongxh
* @Date: 2024-12-21
* @Description: 四叉树的 形状基类
*/
import { Graphics, Rect, Vec2 } from "cc";
export abstract class Shape {
/**
* 形状的标记 用来过滤不需要检测的形状
* 通过 & 来匹配形状是否需要被检测
* -1 表示和所有物体碰撞
*/
public tag: number = -1;
/** 被标记为无效 下次更新时删除 */
public invalid: boolean = false;
/** 缩放 */
public scale: number; // 缩放
/** 脏标记 用来重置包围盒 */
protected isDirty: boolean;
/** 包围盒 */
protected boundingBox: Rect;
/** 位置 */
protected _position: Vec2;
/** 旋转角度 */
protected _rotation: number;
constructor(tag: number) {
this.tag = tag;
this.scale = 1.0;
this._rotation = 0;
this.isDirty = true;
this.boundingBox = new Rect();
this._position = new Vec2();
}
set position(pos: Vec2) {
this._position.x = pos.x;
this._position.y = pos.y;
}
get position(): Vec2 {
return this._position;
}
set rotation(angle: number) {
if (this._rotation !== angle) {
this._rotation = angle;
this.isDirty = true;
}
}
get rotation(): number {
return this._rotation;
}
/** 包围盒 子类重写 */
public abstract getBoundingBox(): Rect;
public drawShape(draw: Graphics): void {
}
}