Skip to content

Commit 4e3c2bd

Browse files
author
Your Name
committed
feat: optimize collision detection with spatial grid
1 parent 72f3c78 commit 4e3c2bd

File tree

6 files changed

+287
-14
lines changed

6 files changed

+287
-14
lines changed

Math/SpatialGrid.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
2+
import type { Collider } from '../Parts/Children/Collider';
3+
import type { Vector } from './Vector';
4+
5+
export class SpatialGrid {
6+
private cells: Map<string, Collider[]>;
7+
private cellSize: number;
8+
9+
constructor(cellSize: number) {
10+
this.cells = new Map();
11+
this.cellSize = cellSize;
12+
}
13+
14+
private getKey(x: number, y: number): string {
15+
return `${Math.floor(x / this.cellSize)}_${Math.floor(y / this.cellSize)}`;
16+
}
17+
18+
clear() {
19+
this.cells.clear();
20+
}
21+
22+
insert(collider: Collider) {
23+
const start = collider.realWorldStart;
24+
const end = collider.realWorldEnd;
25+
26+
const startX = Math.floor(start.x / this.cellSize);
27+
const startY = Math.floor(start.y / this.cellSize);
28+
const endX = Math.floor(end.x / this.cellSize);
29+
const endY = Math.floor(end.y / this.cellSize);
30+
31+
for (let x = startX; x <= endX; x++) {
32+
for (let y = startY; y <= endY; y++) {
33+
const key = `${x}_${y}`;
34+
if (!this.cells.has(key)) {
35+
this.cells.set(key, []);
36+
}
37+
this.cells.get(key)!.push(collider);
38+
}
39+
}
40+
}
41+
42+
query(collider: Collider): Collider[] {
43+
const candidates = new Set<Collider>();
44+
const start = collider.realWorldStart;
45+
const end = collider.realWorldEnd;
46+
47+
const startX = Math.floor(start.x / this.cellSize);
48+
const startY = Math.floor(start.y / this.cellSize);
49+
const endX = Math.floor(end.x / this.cellSize);
50+
const endY = Math.floor(end.y / this.cellSize);
51+
52+
for (let x = startX; x <= endX; x++) {
53+
for (let y = startY; y <= endY; y++) {
54+
const key = `${x}_${y}`;
55+
if (this.cells.has(key)) {
56+
for (const other of this.cells.get(key)!) {
57+
candidates.add(other);
58+
}
59+
}
60+
}
61+
}
62+
return Array.from(candidates);
63+
}
64+
}

Parts/Children/Collider.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,12 @@ export abstract class Collider extends Part {
129129

130130

131131
evaluateMerging() {
132-
const layer = this.registrations["layer"];
132+
const layer = this.registrations["layer"] as import("../Layer").Layer;
133133
if (!layer) return;
134134

135-
const fellowColliders: Collider[] = layer.flats.colliders.filter((c: Collider) => c.tag == this.tag && c.id !== this.id && c.allowMerge && c.active);
135+
const candidates = layer.spatialGrid.query(this);
136+
const fellowColliders: Collider[] = candidates.filter((c: Collider) => c.tag == this.tag && c.id !== this.id && c.allowMerge && c.active);
137+
136138
if (fellowColliders.length == 0) return;
137139

138140
for (const fellow of fellowColliders) {
@@ -210,9 +212,10 @@ export abstract class Collider extends Part {
210212
this.collidingWith.clear();
211213

212214

213-
const layer = this.registrations.layer as Part;
214-
const colliders = layer.flats.colliders as Collider[];
215-
for (const other of colliders) {
215+
const layer = this.registrations.layer as import("../Layer").Layer;
216+
const candidates = layer.spatialGrid.query(this);
217+
218+
for (const other of candidates) {
216219
if (other === this) continue;
217220
if (this.checkCollision(other)) {
218221
this.colliding = true;

Parts/Layer.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,54 @@
11
import { GameObject } from "./GameObject";
22
import { generateUID } from "../helpers";
33
import { Part } from "./Part";
4+
import { SpatialGrid } from "../Math/SpatialGrid";
5+
import type { Collider } from "./Children/Collider";
46

57
export class Layer extends Part {
8+
spatialGrid: SpatialGrid;
9+
610
constructor({ name }: { name: string }) {
711
super({ name });
812
this.type = "Layer";
913
this.id = generateUID();
1014
this.debugEmoji = "🗂️"; // Default emoji for debugging the layer
15+
this.spatialGrid = new SpatialGrid(100);
1116
}
1217

1318
addChild(part: Part) {
1419
part.setAll("layer", this);
1520
super.addChild(part);
1621
}
22+
1723
addChildren(...parts: Part[]) {
1824
parts.forEach((part) => this.addChild(part));
1925
}
26+
2027
removeChild(part: Part) {
2128
part.onUnregister("layer", this);
2229
super.removeChild(part);
2330
}
31+
2432
act(delta: number) {
2533
if (!this.ready) {
2634
return;
2735
}
36+
37+
this.spatialGrid.clear();
38+
const colliders = this.flats.colliders as Collider[];
39+
for (const collider of colliders) {
40+
if (collider.active) {
41+
this.spatialGrid.insert(collider);
42+
}
43+
}
44+
2845
this.ties.forEach(tie => {
2946
if (tie.target && tie.target.hasOwnProperty(tie.targetAttribute)) {
3047
const value = this.attr(tie.localAttribute);
3148
tie.target.attr(tie.targetAttribute, value);
3249
}
3350
});
51+
3452
this.childrenArray.forEach(child => {
3553
child.act(delta);
3654
});

engine/bundle.js

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11238,13 +11238,68 @@ class Game extends Part {
1123811238
}
1123911239
}
1124011240
}
11241+
// Math/SpatialGrid.ts
11242+
class SpatialGrid {
11243+
cells;
11244+
cellSize;
11245+
constructor(cellSize) {
11246+
this.cells = new Map;
11247+
this.cellSize = cellSize;
11248+
}
11249+
getKey(x, y) {
11250+
return `${Math.floor(x / this.cellSize)}_${Math.floor(y / this.cellSize)}`;
11251+
}
11252+
clear() {
11253+
this.cells.clear();
11254+
}
11255+
insert(collider) {
11256+
const start = collider.realWorldStart;
11257+
const end = collider.realWorldEnd;
11258+
const startX = Math.floor(start.x / this.cellSize);
11259+
const startY = Math.floor(start.y / this.cellSize);
11260+
const endX = Math.floor(end.x / this.cellSize);
11261+
const endY = Math.floor(end.y / this.cellSize);
11262+
for (let x = startX;x <= endX; x++) {
11263+
for (let y = startY;y <= endY; y++) {
11264+
const key = `${x}_${y}`;
11265+
if (!this.cells.has(key)) {
11266+
this.cells.set(key, []);
11267+
}
11268+
this.cells.get(key).push(collider);
11269+
}
11270+
}
11271+
}
11272+
query(collider) {
11273+
const candidates = new Set;
11274+
const start = collider.realWorldStart;
11275+
const end = collider.realWorldEnd;
11276+
const startX = Math.floor(start.x / this.cellSize);
11277+
const startY = Math.floor(start.y / this.cellSize);
11278+
const endX = Math.floor(end.x / this.cellSize);
11279+
const endY = Math.floor(end.y / this.cellSize);
11280+
for (let x = startX;x <= endX; x++) {
11281+
for (let y = startY;y <= endY; y++) {
11282+
const key = `${x}_${y}`;
11283+
if (this.cells.has(key)) {
11284+
for (const other of this.cells.get(key)) {
11285+
candidates.add(other);
11286+
}
11287+
}
11288+
}
11289+
}
11290+
return Array.from(candidates);
11291+
}
11292+
}
11293+
1124111294
// Parts/Layer.ts
1124211295
class Layer extends Part {
11296+
spatialGrid;
1124311297
constructor({ name }) {
1124411298
super({ name });
1124511299
this.type = "Layer";
1124611300
this.id = generateUID();
1124711301
this.debugEmoji = "\uD83D\uDDC2️";
11302+
this.spatialGrid = new SpatialGrid(100);
1124811303
}
1124911304
addChild(part) {
1125011305
part.setAll("layer", this);
@@ -11261,6 +11316,13 @@ class Layer extends Part {
1126111316
if (!this.ready) {
1126211317
return;
1126311318
}
11319+
this.spatialGrid.clear();
11320+
const colliders = this.flats.colliders;
11321+
for (const collider of colliders) {
11322+
if (collider.active) {
11323+
this.spatialGrid.insert(collider);
11324+
}
11325+
}
1126411326
this.ties.forEach((tie) => {
1126511327
if (tie.target && tie.target.hasOwnProperty(tie.targetAttribute)) {
1126611328
const value = this.attr(tie.localAttribute);
@@ -12020,7 +12082,8 @@ class Collider extends Part {
1202012082
const layer = this.registrations["layer"];
1202112083
if (!layer)
1202212084
return;
12023-
const fellowColliders = layer.flats.colliders.filter((c) => c.tag == this.tag && c.id !== this.id && c.allowMerge && c.active);
12085+
const candidates = layer.spatialGrid.query(this);
12086+
const fellowColliders = candidates.filter((c) => c.tag == this.tag && c.id !== this.id && c.allowMerge && c.active);
1202412087
if (fellowColliders.length == 0)
1202512088
return;
1202612089
for (const fellow of fellowColliders) {
@@ -12081,8 +12144,8 @@ class Collider extends Part {
1208112144
this.colliding = false;
1208212145
this.collidingWith.clear();
1208312146
const layer = this.registrations.layer;
12084-
const colliders = layer.flats.colliders;
12085-
for (const other of colliders) {
12147+
const candidates = layer.spatialGrid.query(this);
12148+
for (const other of candidates) {
1208612149
if (other === this)
1208712150
continue;
1208812151
if (this.checkCollision(other)) {

engine/editor.js

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5045,6 +5045,58 @@ class SoundManagerController {
50455045
}
50465046
}
50475047

5048+
class SpatialGrid {
5049+
cells;
5050+
cellSize;
5051+
constructor(cellSize) {
5052+
this.cells = new Map;
5053+
this.cellSize = cellSize;
5054+
}
5055+
getKey(x, y) {
5056+
return `${Math.floor(x / this.cellSize)}_${Math.floor(y / this.cellSize)}`;
5057+
}
5058+
clear() {
5059+
this.cells.clear();
5060+
}
5061+
insert(collider) {
5062+
const start = collider.realWorldStart;
5063+
const end = collider.realWorldEnd;
5064+
const startX = Math.floor(start.x / this.cellSize);
5065+
const startY = Math.floor(start.y / this.cellSize);
5066+
const endX = Math.floor(end.x / this.cellSize);
5067+
const endY = Math.floor(end.y / this.cellSize);
5068+
for (let x = startX;x <= endX; x++) {
5069+
for (let y = startY;y <= endY; y++) {
5070+
const key = `${x}_${y}`;
5071+
if (!this.cells.has(key)) {
5072+
this.cells.set(key, []);
5073+
}
5074+
this.cells.get(key).push(collider);
5075+
}
5076+
}
5077+
}
5078+
query(collider) {
5079+
const candidates = new Set;
5080+
const start = collider.realWorldStart;
5081+
const end = collider.realWorldEnd;
5082+
const startX = Math.floor(start.x / this.cellSize);
5083+
const startY = Math.floor(start.y / this.cellSize);
5084+
const endX = Math.floor(end.x / this.cellSize);
5085+
const endY = Math.floor(end.y / this.cellSize);
5086+
for (let x = startX;x <= endX; x++) {
5087+
for (let y = startY;y <= endY; y++) {
5088+
const key = `${x}_${y}`;
5089+
if (this.cells.has(key)) {
5090+
for (const other of this.cells.get(key)) {
5091+
candidates.add(other);
5092+
}
5093+
}
5094+
}
5095+
}
5096+
return Array.from(candidates);
5097+
}
5098+
}
5099+
50485100
class Vector {
50495101
x;
50505102
y;
@@ -24275,11 +24327,13 @@ Defaulting to 2020, but this will stop working in the future.`);
2427524327
}
2427624328
};
2427724329
Layer = class Layer extends Part {
24330+
spatialGrid;
2427824331
constructor({ name }) {
2427924332
super({ name });
2428024333
this.type = "Layer";
2428124334
this.id = generateUID();
2428224335
this.debugEmoji = "\uD83D\uDDC2️";
24336+
this.spatialGrid = new SpatialGrid(100);
2428324337
}
2428424338
addChild(part) {
2428524339
part.setAll("layer", this);
@@ -24296,6 +24350,13 @@ Defaulting to 2020, but this will stop working in the future.`);
2429624350
if (!this.ready) {
2429724351
return;
2429824352
}
24353+
this.spatialGrid.clear();
24354+
const colliders = this.flats.colliders;
24355+
for (const collider of colliders) {
24356+
if (collider.active) {
24357+
this.spatialGrid.insert(collider);
24358+
}
24359+
}
2429924360
this.ties.forEach((tie) => {
2430024361
if (tie.target && tie.target.hasOwnProperty(tie.targetAttribute)) {
2430124362
const value = this.attr(tie.localAttribute);
@@ -24952,7 +25013,8 @@ Defaulting to 2020, but this will stop working in the future.`);
2495225013
const layer = this.registrations["layer"];
2495325014
if (!layer)
2495425015
return;
24955-
const fellowColliders = layer.flats.colliders.filter((c) => c.tag == this.tag && c.id !== this.id && c.allowMerge && c.active);
25016+
const candidates = layer.spatialGrid.query(this);
25017+
const fellowColliders = candidates.filter((c) => c.tag == this.tag && c.id !== this.id && c.allowMerge && c.active);
2495625018
if (fellowColliders.length == 0)
2495725019
return;
2495825020
for (const fellow of fellowColliders) {
@@ -25013,8 +25075,8 @@ Defaulting to 2020, but this will stop working in the future.`);
2501325075
this.colliding = false;
2501425076
this.collidingWith.clear();
2501525077
const layer = this.registrations.layer;
25016-
const colliders = layer.flats.colliders;
25017-
for (const other of colliders) {
25078+
const candidates = layer.spatialGrid.query(this);
25079+
for (const other of candidates) {
2501825080
if (other === this)
2501925081
continue;
2502025082
if (this.checkCollision(other)) {

0 commit comments

Comments
 (0)