Experimental canvas 2D engine for tile-based sidescroller/sandbox games, created strictly for educational purposes.
entity-component-system game-engine canvas-2d
at develop 3.7 kB view raw
1import { Entity, System } from "@cosmic/core"; 2import { Transform, Collider } from "@cosmic/kit/components"; 3 4export class CollisionSystem extends System { 5 public readonly requiredComponents = new Set([Transform.name, Collider.name]); 6 7 public update(entities: Entity[], _deltaTime: number) { 8 // Reset collision flag for all first 9 for (const entity of entities) { 10 const transform = entity.getComponent(Transform)!; 11 transform.isCollidingWithEntity = false; 12 } 13 14 // Check each pair once 15 for (let i = 0; i < entities.length; i++) { 16 const entityA = entities[i]; 17 const aTransform = entityA.getComponent(Transform)!; 18 const aCollider = entityA.getComponent(Collider)!; 19 20 for (let j = i + 1; j < entities.length; j++) { 21 const entityB = entities[j]; 22 const bTransform = entityB.getComponent(Transform)!; 23 const bCollider = entityB.getComponent(Collider)!; 24 25 if (this.areColliding(aTransform, bTransform)) { 26 aTransform.isCollidingWithEntity = true; 27 bTransform.isCollidingWithEntity = true; 28 29 this.resolveOverlap(aTransform, bTransform, aCollider, bCollider); 30 } 31 } 32 } 33 } 34 35 protected areColliding(a: Transform, b: Transform): boolean { 36 // AABB vs AABB 37 return ( 38 a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y 39 ); 40 } 41 42 protected resolveOverlap(a: Transform, b: Transform, colA: Collider, colB: Collider) { 43 // Centers 44 const centerAx = a.x + a.width / 2; 45 const centerAy = a.y + a.height / 2; 46 const centerBx = b.x + b.width / 2; 47 const centerBy = b.y + b.height / 2; 48 49 const dx = centerAx - centerBx; 50 const dy = centerAy - centerBy; 51 52 const combinedHalfWidths = (a.width + b.width) / 2; 53 const combinedHalfHeights = (a.height + b.height) / 2; 54 55 const overlapX = combinedHalfWidths - Math.abs(dx); 56 const overlapY = combinedHalfHeights - Math.abs(dy); 57 58 if (overlapX <= 0 || overlapY <= 0) return; // just in case 59 60 const aStatic = colA.isStatic; 61 const bStatic = colB.isStatic; 62 63 // Decide which axis to resolve along (least penetration) 64 if (overlapX < overlapY) { 65 // Horizontal 66 if (dx > 0) { 67 // A is to the right of B 68 this.pushApartX(a, b, overlapX, +1, aStatic, bStatic); 69 } else { 70 // A is to the left of B 71 this.pushApartX(a, b, overlapX, -1, aStatic, bStatic); 72 } 73 } else { 74 // Vertical 75 if (dy > 0) { 76 // A is below B 77 this.pushApartY(a, b, overlapY, +1, aStatic, bStatic); 78 } else { 79 // A is above B 80 this.pushApartY(a, b, overlapY, -1, aStatic, bStatic); 81 } 82 } 83 } 84 85 private pushApartX( 86 a: Transform, 87 b: Transform, 88 overlap: number, 89 direction: 1 | -1, // +1: A right of B, -1: A left of B 90 aStatic: boolean, 91 bStatic: boolean, 92 ) { 93 if (aStatic && bStatic) return; 94 95 if (aStatic && !bStatic) { 96 // Only move B 97 b.x -= direction * overlap; 98 } else if (!aStatic && bStatic) { 99 // Only move A 100 a.x += direction * overlap; 101 } else { 102 // Both dynamic → split 103 const half = overlap / 2; 104 a.x += direction * half; 105 b.x -= direction * half; 106 } 107 } 108 109 private pushApartY( 110 a: Transform, 111 b: Transform, 112 overlap: number, 113 direction: 1 | -1, // +1: A below B, -1: A above B 114 aStatic: boolean, 115 bStatic: boolean, 116 ) { 117 if (aStatic && bStatic) return; 118 119 if (aStatic && !bStatic) { 120 b.y -= direction * overlap; 121 } else if (!aStatic && bStatic) { 122 a.y += direction * overlap; 123 } else { 124 const half = overlap / 2; 125 a.y += direction * half; 126 b.y -= direction * half; 127 } 128 } 129}