Skip to content

Commit ee3b060

Browse files
committed
added loads of cool stuff
1 parent 1b3b9e4 commit ee3b060

File tree

15 files changed

+1538
-714
lines changed

15 files changed

+1538
-714
lines changed

Math/Vector.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ export class Vector {
4343
length(): number {
4444
return Math.sqrt(this.x * this.x + this.y * this.y);
4545
}
46+
magnitude(): number {
47+
return this.length();
48+
}
49+
equals(other: Vector): boolean {
50+
return this.x === other.x && this.y === other.y;
51+
}
4652
toString(): string {
4753
return `[${this.x}, ${this.y}]`;
4854
}
@@ -69,6 +75,11 @@ export class Vector {
6975
}
7076
return this;
7177
}
78+
addInPlace(other: Vector): Vector {
79+
this.x += other.x;
80+
this.y += other.y;
81+
return this;
82+
}
7283
static From(scalar: number): Vector {
7384
return new Vector(scalar, scalar);
7485
}

Parts/Children/BoxCollider.ts

Lines changed: 97 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { drawBox } from "../../helpers";
21
import { Vector } from "../../Math/Vector";
32
import { Game } from "../Game";
43
import { Part } from "../Part";
@@ -9,151 +8,128 @@ import { PolygonCollider } from "./PolygonCollider";
98
export class BoxCollider extends Collider {
109
start: Vector;
1110
end: Vector;
12-
realWorldStart: Vector;
13-
realWorldEnd: Vector;
14-
rotatedCorners: Vector[] = []; // Store the four corners of the rotated box
15-
constructor({ width, height }: { width: number, height: number }) {
16-
super();
11+
rotatedCorners: Vector[] = [];
12+
width: number;
13+
height: number;
14+
15+
private cachedAxes: Vector[] = [];
16+
private lastRotation: number = NaN;
17+
private lastScale: Vector = new Vector(NaN, NaN);
18+
19+
constructor({ width, height, tag }: { width: number; height: number; tag?: string }) {
20+
super({ tag });
1721
this.name = "BoxCollider";
22+
this.width = width;
23+
this.height = height;
1824
this.start = new Vector(-width / 2, -height / 2);
1925
this.end = new Vector(width / 2, height / 2);
20-
this.realWorldStart = this.start; // Will be updated in act(), onMount()
21-
this.realWorldEnd = this.end; // Will be updated in act(), onMount()
26+
this.radius = Math.sqrt((width / 2) ** 2 + (height / 2) ** 2);
2227
this.type = "BoxCollider";
23-
this.base = "Collider";
24-
}
25-
onMount(parent: Part) {
26-
super.onMount(parent);
27-
const transform = this.sibling<Transform>("Transform")
28-
if (!transform) {
29-
this.top?.warn(
30-
`BoxCollider <${this.name}> (${this.id}) does not have Transform sibling. Skipping`
31-
);
32-
return;
28+
29+
for (let i = 0; i < 4; i++) {
30+
this.rotatedCorners.push(new Vector(0, 0));
3331
}
32+
}
3433

35-
// Initialize realWorldStart and realWorldEnd based on parent's Transform
36-
this.updateRotatedBounds(transform);
34+
get worldVertices(): Vector[] {
35+
return this.rotatedCorners;
36+
}
3737

38+
updateCollider(transform: Transform) {
39+
const cos = Math.cos(transform.rotation);
40+
const sin = Math.sin(transform.rotation);
3841

39-
}
42+
const halfW = (this.width * transform.scale.x) / 2;
43+
const halfH = (this.height * transform.scale.y) / 2;
4044

41-
// Helper method to calculate rotated bounding box
42-
updateRotatedBounds(transform: Transform) {
43-
const scaledStart = this.start.multiply(transform.scale);
44-
const scaledEnd = this.end.multiply(transform.scale);
45-
46-
// Calculate the center of the box (in local space)
47-
const center = new Vector(
48-
(scaledStart.x + scaledEnd.x) / 2,
49-
(scaledStart.y + scaledEnd.y) / 2
50-
);
51-
52-
// Corners in local space (top-left, top-right, bottom-right, bottom-left)
53-
const corners = [
54-
scaledStart, // top-left
55-
new Vector(scaledEnd.x, scaledStart.y), // top-right
56-
scaledEnd, // bottom-right
57-
new Vector(scaledStart.x, scaledEnd.y), // bottom-left
45+
const localCorners = [
46+
new Vector(-halfW, -halfH),
47+
new Vector(halfW, -halfH),
48+
new Vector(halfW, halfH),
49+
new Vector(-halfW, halfH),
5850
];
5951

60-
// Rotate each corner around the center, then translate to world position
61-
this.rotatedCorners = corners.map(corner => {
62-
const rel = corner.subtract(center);
63-
if (transform.rotation === 0) {
64-
return transform.worldPosition.add(corner);
65-
}
66-
const cos = Math.cos(transform.rotation);
67-
const sin = Math.sin(transform.rotation);
68-
const rotated = new Vector(
69-
rel.x * cos - rel.y * sin,
70-
rel.x * sin + rel.y * cos
71-
);
72-
73-
return transform.worldPosition.add(rotated.add(center));
74-
});
52+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
7553

76-
// Calculate axis-aligned bounding box from rotated corners
77-
const xs = this.rotatedCorners.map(corner => corner.x);
78-
const ys = this.rotatedCorners.map(corner => corner.y);
54+
for (let i = 0; i < 4; i++) {
55+
const c = localCorners[i];
56+
const x = c.x * cos - c.y * sin + transform.worldPosition.x;
57+
const y = c.x * sin + c.y * cos + transform.worldPosition.y;
7958

80-
this.realWorldStart = new Vector(Math.min(...xs), Math.min(...ys));
81-
this.realWorldEnd = new Vector(Math.max(...xs), Math.max(...ys));
82-
}
59+
this.rotatedCorners[i].set(x, y);
8360

61+
if (x < minX) minX = x;
62+
if (x > maxX) maxX = x;
63+
if (y < minY) minY = y;
64+
if (y > maxY) maxY = y;
65+
}
8466

85-
checkCollision(other: Collider): boolean {
67+
this.realWorldStart.set(minX, minY);
68+
this.realWorldEnd.set(maxX, maxY);
8669

70+
if (transform.rotation !== this.lastRotation || !transform.scale.equals(this.lastScale)) {
71+
this.cachedAxes = this.getAxes(this.rotatedCorners);
72+
this.lastRotation = transform.rotation;
73+
this.lastScale = transform.scale.clone();
74+
}
75+
}
76+
77+
narrowPhaseCheck(other: Collider): boolean {
8778
if (other instanceof BoxCollider) {
88-
return !(
89-
this.realWorldEnd.x < other.realWorldStart.x || this.realWorldStart.x > other.realWorldEnd.x ||
90-
this.realWorldEnd.y < other.realWorldStart.y || this.realWorldStart.y > other.realWorldEnd.y
91-
);
79+
return this.checkBoxVsBox(this, other);
9280
} else if (other instanceof PolygonCollider) {
93-
// Delegate to PolygonCollider's checkCollision method
94-
return other.checkCollision(this);
81+
return other.narrowPhaseCheck(this);
9582
}
96-
this.top?.warn("Collision checks are only supported between BoxColliders and PolygonColliders.");
83+
84+
this.top?.warn(`Collision with unsupported collider type: ${other.type}`);
9785
return false;
9886
}
99-
get vertices(): Vector[] {
100-
const width = this.end.x - this.start.x;
101-
const height = this.end.y - this.start.y;
102-
const halfWidth = width / 2;
103-
const halfHeight = height / 2;
104-
105-
const vertices = [
106-
new Vector(-halfWidth, -halfHeight),
107-
new Vector(halfWidth, -halfHeight),
108-
new Vector(halfWidth, halfHeight),
109-
new Vector(-halfWidth, halfHeight),
110-
];
111-
return vertices;
87+
88+
private checkBoxVsBox(box1: BoxCollider, box2: BoxCollider): boolean {
89+
const axes = box1.cachedAxes.concat(box2.cachedAxes);
90+
for (const axis of axes) {
91+
const proj1 = this.project(box1.rotatedCorners, axis);
92+
const proj2 = this.project(box2.rotatedCorners, axis);
93+
if (!this.overlap(proj1, proj2)) return false;
94+
}
95+
return true;
11296
}
97+
11398
act(delta: number) {
114-
super.act(delta); // Pass to children
115-
const transform = this.sibling<Transform>("Transform");
116-
if (!transform) {
117-
throw new Error(`BoxCollider <${this.name}> (${this.id}) does not have a Transform sibling. Ensure it is mounted to a GameObject with a Transform component.`);
99+
super.act(delta);
100+
}
101+
102+
drawDebug(ctx: CanvasRenderingContext2D): void {
103+
ctx.save();
104+
ctx.strokeStyle = this.colliding ? "rgba(255,0,0,0.5)" : "rgba(0,255,0,0.5)";
105+
ctx.lineWidth = 1;
106+
ctx.beginPath();
107+
ctx.moveTo(this.rotatedCorners[0].x, this.rotatedCorners[0].y);
108+
for (let i = 1; i < 4; i++) {
109+
ctx.lineTo(this.rotatedCorners[i].x, this.rotatedCorners[i].y);
118110
}
111+
ctx.closePath();
112+
ctx.stroke();
113+
ctx.restore();
114+
}
119115

120-
if (transform.rotation !== 0) {
121-
// If rotated, replace with a PolygonCollider
122-
const vertices = this.vertices;
116+
override clone(memo = new Map()): this {
117+
if (memo.has(this)) return memo.get(this);
123118

124-
const polygonCollider = new PolygonCollider({ vertices });
125-
this.parent?.addChild(polygonCollider);
126-
this.parent?.removeChild(this);
127-
return; // Stop further execution for this BoxCollider instance
128-
}
129-
this.updateRotatedBounds(transform);
130-
131-
this.colliding = false;
132-
133-
const layer = this.registrations?.layer as Part | undefined;
134-
const colliders = layer?.flats.colliders as Collider[] || [];
135-
this.collidingWith = new Set<Collider>();; // Reset collidingWith list for this frame
136-
for (const other of colliders) {
137-
if (other === this) continue;
138-
if (this.checkCollision(other)) {
139-
this.colliding = true;
140-
this.collidingWith.add(other);
141-
}
142-
}
143-
if (this.top instanceof Game && this.top.devmode) {
144-
if (transform.rotation === 0) {
145-
// Draw regular box if not rotated
146-
drawBox(
147-
this.top.context,
148-
this.realWorldStart.x,
149-
this.realWorldStart.y,
150-
this.realWorldEnd.x - this.realWorldStart.x,
151-
this.realWorldEnd.y - this.realWorldStart.y,
152-
this.colliding
153-
? "rgba(255, 0, 0, 0.5)"
154-
: "rgba(0, 255, 0, 0.5)"
155-
);
156-
}
157-
}
119+
const cloned = new BoxCollider({
120+
width: this.width,
121+
height: this.height,
122+
tag: this.tag,
123+
});
124+
memo.set(this, cloned);
125+
126+
this._cloneProperties(cloned, memo);
127+
128+
cloned.colliding = false;
129+
cloned.base = this.base;
130+
cloned.type = this.type;
131+
cloned.collidingWith = new Set<Collider>();
132+
133+
return cloned as this;
158134
}
159135
}

0 commit comments

Comments
 (0)