Something went wrong. Try again.
Experimental canvas 2D engine for tile-based sidescroller/sandbox games, created strictly for educational purposes.
entity-component-system game-engine canvas-2d
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130import { Entity, System } from "@cosmic/core";import { Transform, Collider } from "@cosmic/kit/components";
export class CollisionSystem extends System { public readonly requiredComponents = new Set([Transform.name, Collider.name]);
public update(entities: Entity[], _deltaTime: number) { // Reset collision flag for all first for (const entity of entities) { const transform = entity.getComponent(Transform)!; transform.isCollidingWithEntity = false; }
// Check each pair once for (let i = 0; i < entities.length; i++) { const entityA = entities[i]; const aTransform = entityA.getComponent(Transform)!; const aCollider = entityA.getComponent(Collider)!;
for (let j = i + 1; j < entities.length; j++) { const entityB = entities[j]; const bTransform = entityB.getComponent(Transform)!; const bCollider = entityB.getComponent(Collider)!;
if (this.areColliding(aTransform, bTransform)) { aTransform.isCollidingWithEntity = true; bTransform.isCollidingWithEntity = true;
this.resolveOverlap(aTransform, bTransform, aCollider, bCollider); } } } }
protected areColliding(a: Transform, b: Transform): boolean { // AABB vs AABB return ( a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y ); }
protected resolveOverlap(a: Transform, b: Transform, colA: Collider, colB: Collider) { // Centers const centerAx = a.x + a.width / 2; const centerAy = a.y + a.height / 2; const centerBx = b.x + b.width / 2; const centerBy = b.y + b.height / 2;
const dx = centerAx - centerBx; const dy = centerAy - centerBy;
const combinedHalfWidths = (a.width + b.width) / 2; const combinedHalfHeights = (a.height + b.height) / 2;
const overlapX = combinedHalfWidths - Math.abs(dx); const overlapY = combinedHalfHeights - Math.abs(dy);
if (overlapX <= 0 || overlapY <= 0) return; // just in case
const aStatic = colA.isStatic; const bStatic = colB.isStatic;
// Decide which axis to resolve along (least penetration) if (overlapX < overlapY) { // Horizontal if (dx > 0) { // A is to the right of B this.pushApartX(a, b, overlapX, +1, aStatic, bStatic); } else { // A is to the left of B this.pushApartX(a, b, overlapX, -1, aStatic, bStatic); } } else { // Vertical if (dy > 0) { // A is below B this.pushApartY(a, b, overlapY, +1, aStatic, bStatic); } else { // A is above B this.pushApartY(a, b, overlapY, -1, aStatic, bStatic); } } }
private pushApartX( a: Transform, b: Transform, overlap: number, direction: 1 | -1, // +1: A right of B, -1: A left of B aStatic: boolean, bStatic: boolean, ) { if (aStatic && bStatic) return;
if (aStatic && !bStatic) { // Only move B b.x -= direction * overlap; } else if (!aStatic && bStatic) { // Only move A a.x += direction * overlap; } else { // Both dynamic → split const half = overlap / 2; a.x += direction * half; b.x -= direction * half; } }
private pushApartY( a: Transform, b: Transform, overlap: number, direction: 1 | -1, // +1: A below B, -1: A above B aStatic: boolean, bStatic: boolean, ) { if (aStatic && bStatic) return;
if (aStatic && !bStatic) { b.y -= direction * overlap; } else if (!aStatic && bStatic) { a.y += direction * overlap; } else { const half = overlap / 2; a.y += direction * half; b.y -= direction * half; } }}