• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

excaliburjs / Excalibur / 14804036802

02 May 2025 09:58PM UTC coverage: 5.927% (-83.4%) from 89.28%
14804036802

Pull #3404

github

web-flow
Merge 5c103d7f8 into 0f2ccaeb2
Pull Request #3404: feat: added Graph module to Math

234 of 8383 branches covered (2.79%)

229 of 246 new or added lines in 1 file covered. (93.09%)

13145 existing lines in 208 files now uncovered.

934 of 15759 relevant lines covered (5.93%)

4.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

1.01
/src/engine/Collision/CollisionSystem.ts
1
import { ComponentCtor, Query, SystemPriority, World } from '../EntityComponentSystem';
2
import { MotionComponent } from '../EntityComponentSystem/Components/MotionComponent';
3
import { TransformComponent } from '../EntityComponentSystem/Components/TransformComponent';
4
import { System, SystemType } from '../EntityComponentSystem/System';
5
import { CollisionEndEvent, CollisionStartEvent, ContactEndEvent, ContactStartEvent } from '../Events';
6
import { SolverStrategy } from './SolverStrategy';
7
import { ArcadeSolver } from './Solver/ArcadeSolver';
8
import { Collider } from './Colliders/Collider';
9
import { CollisionContact } from './Detection/CollisionContact';
10
import { RealisticSolver } from './Solver/RealisticSolver';
11
import { CollisionSolver } from './Solver/Solver';
12
import { ColliderComponent } from './ColliderComponent';
13
import { CompositeCollider } from './Colliders/CompositeCollider';
14
import { Engine } from '../Engine';
15
import { ExcaliburGraphicsContext } from '../Graphics/Context/ExcaliburGraphicsContext';
16
import { Scene } from '../Scene';
17
import { Side } from '../Collision/Side';
18
import { PhysicsWorld } from './PhysicsWorld';
19
import { CollisionProcessor } from './Detection/CollisionProcessor';
20
import { SeparatingAxis } from './Colliders/SeparatingAxis';
21
import { MotionSystem } from './MotionSystem';
22
import { Pair } from './Detection/Pair';
23
export class CollisionSystem extends System {
24
  static priority = SystemPriority.Higher;
1✔
25

UNCOV
26
  public systemType = SystemType.Update;
×
27
  public query: Query<ComponentCtor<TransformComponent> | ComponentCtor<MotionComponent> | ComponentCtor<ColliderComponent>>;
28

29
  private _engine: Engine;
UNCOV
30
  private _configDirty = false;
×
31
  private _realisticSolver: RealisticSolver;
32
  private _arcadeSolver: ArcadeSolver;
UNCOV
33
  private _lastFrameContacts = new Map<string, CollisionContact>();
×
UNCOV
34
  private _currentFrameContacts = new Map<string, CollisionContact>();
×
35
  private _motionSystem: MotionSystem;
36
  private get _processor(): CollisionProcessor {
UNCOV
37
    return this._physics.collisionProcessor;
×
38
  }
39

40
  private _trackCollider: (c: Collider) => void;
41
  private _untrackCollider: (c: Collider) => void;
42

43
  constructor(
44
    world: World,
UNCOV
45
    private _physics: PhysicsWorld
×
46
  ) {
UNCOV
47
    super();
×
UNCOV
48
    this._arcadeSolver = new ArcadeSolver(_physics.config.arcade);
×
UNCOV
49
    this._realisticSolver = new RealisticSolver(_physics.config.realistic);
×
UNCOV
50
    this._physics.$configUpdate.subscribe(() => (this._configDirty = true));
×
UNCOV
51
    this._trackCollider = (c: Collider) => this._processor.track(c);
×
UNCOV
52
    this._untrackCollider = (c: Collider) => this._processor.untrack(c);
×
UNCOV
53
    this.query = world.query([TransformComponent, MotionComponent, ColliderComponent]);
×
UNCOV
54
    this.query.entityAdded$.subscribe((e) => {
×
UNCOV
55
      const colliderComponent = e.get(ColliderComponent);
×
UNCOV
56
      colliderComponent.$colliderAdded.subscribe(this._trackCollider);
×
UNCOV
57
      colliderComponent.$colliderRemoved.subscribe(this._untrackCollider);
×
UNCOV
58
      const collider = colliderComponent.get();
×
UNCOV
59
      if (collider) {
×
UNCOV
60
        this._processor.track(collider);
×
61
      }
62
    });
UNCOV
63
    this.query.entityRemoved$.subscribe((e) => {
×
UNCOV
64
      const colliderComponent = e.get(ColliderComponent);
×
UNCOV
65
      const collider = colliderComponent.get();
×
UNCOV
66
      if (colliderComponent && collider) {
×
UNCOV
67
        this._processor.untrack(collider);
×
68
      }
69
    });
UNCOV
70
    this._motionSystem = world.get(MotionSystem) as MotionSystem;
×
71
  }
72

73
  initialize(world: World, scene: Scene) {
UNCOV
74
    this._engine = scene.engine;
×
75
  }
76

77
  update(elapsed: number): void {
UNCOV
78
    if (!this._physics.config.enabled) {
×
79
      return;
×
80
    }
81

82
    // TODO do we need to do this every frame?
83
    // Collect up all the colliders and update them
UNCOV
84
    let colliders: Collider[] = [];
×
UNCOV
85
    for (let entityIndex = 0; entityIndex < this.query.entities.length; entityIndex++) {
×
UNCOV
86
      const entity = this.query.entities[entityIndex];
×
UNCOV
87
      const colliderComp = entity.get(ColliderComponent);
×
UNCOV
88
      const collider = colliderComp?.get();
×
UNCOV
89
      if (colliderComp && colliderComp.owner?.isActive && collider) {
×
UNCOV
90
        colliderComp.update();
×
91

92
        // Flatten composite colliders
UNCOV
93
        if (collider instanceof CompositeCollider) {
×
UNCOV
94
          const compositeColliders = collider.getColliders();
×
UNCOV
95
          if (!collider.compositeStrategy) {
×
UNCOV
96
            collider.compositeStrategy = this._physics.config.colliders.compositeStrategy;
×
97
          }
UNCOV
98
          colliders = colliders.concat(compositeColliders);
×
99
        } else {
UNCOV
100
          colliders.push(collider);
×
101
        }
102
      }
103
    }
104

105
    // Update the spatial partitioning data structures
106
    // TODO if collider invalid it will break the processor
107
    // TODO rename "update" to something more specific
UNCOV
108
    this._processor.update(colliders, elapsed);
×
109

110
    // Run broadphase on all colliders and locates potential collisions
UNCOV
111
    let pairs = this._processor.broadphase(colliders, elapsed);
×
112

UNCOV
113
    this._currentFrameContacts.clear();
×
114

115
    // Given possible pairs find actual contacts
UNCOV
116
    let contacts: CollisionContact[] = [];
×
117

UNCOV
118
    const solver: CollisionSolver = this.getSolver();
×
119

120
    // Solve, this resolves the position/velocity so entities aren't overlapping
UNCOV
121
    const substep = this._physics.config.substep;
×
UNCOV
122
    for (let step = 0; step < substep; step++) {
×
UNCOV
123
      if (step > 0) {
×
124
        // first step is run by the MotionSystem when configured, so skip
125
        this._motionSystem.update(elapsed);
×
126
      }
127
      // Re-use pairs from previous collision
UNCOV
128
      if (contacts.length) {
×
129
        pairs = contacts.map((c) => new Pair(c.colliderA, c.colliderB));
×
130
      }
131

UNCOV
132
      if (pairs.length) {
×
UNCOV
133
        contacts = this._processor.narrowphase(pairs, this._engine?.debug?.stats?.currFrame);
×
UNCOV
134
        contacts = solver.solve(contacts);
×
135

136
        // Record contacts for start/end
UNCOV
137
        for (const contact of contacts) {
×
UNCOV
138
          if (contact.isCanceled()) {
×
UNCOV
139
            continue;
×
140
          }
141
          // Process composite ids, things with the same composite id are treated as the same collider for start/end
UNCOV
142
          const index = contact.id.indexOf('|');
×
UNCOV
143
          if (index > 0) {
×
144
            const compositeId = contact.id.substring(index + 1);
×
145
            this._currentFrameContacts.set(compositeId, contact);
×
146
          } else {
UNCOV
147
            this._currentFrameContacts.set(contact.id, contact);
×
148
          }
149
        }
150
      }
151
    }
152

153
    // Emit contact start/end events
UNCOV
154
    this.runContactStartEnd();
×
155

156
    // reset the last frame cache
UNCOV
157
    this._lastFrameContacts.clear();
×
158

159
    // Keep track of collisions contacts that have started or ended
UNCOV
160
    this._lastFrameContacts = new Map(this._currentFrameContacts);
×
161

162
    // Process deferred collider removals
UNCOV
163
    for (const entity of this.query.entities) {
×
UNCOV
164
      const collider = entity.get(ColliderComponent);
×
UNCOV
165
      if (collider) {
×
UNCOV
166
        collider.processColliderRemoval();
×
167
      }
168
    }
169
  }
170

171
  postupdate(): void {
UNCOV
172
    SeparatingAxis.SeparationPool.done();
×
173
  }
174

175
  getSolver(): CollisionSolver {
UNCOV
176
    if (this._configDirty) {
×
UNCOV
177
      this._configDirty = false;
×
UNCOV
178
      this._arcadeSolver = new ArcadeSolver(this._physics.config.arcade);
×
UNCOV
179
      this._realisticSolver = new RealisticSolver(this._physics.config.realistic);
×
180
    }
UNCOV
181
    return this._physics.config.solver === SolverStrategy.Realistic ? this._realisticSolver : this._arcadeSolver;
×
182
  }
183

184
  debug(ex: ExcaliburGraphicsContext) {
185
    this._processor.debug(ex, 0);
×
186
  }
187

188
  public runContactStartEnd() {
189
    // If composite colliders are 'together' collisions may have a duplicate id because we want to treat those as a singular start/end
UNCOV
190
    for (const [id, c] of this._currentFrameContacts) {
×
191
      // find all new contacts
UNCOV
192
      if (!this._lastFrameContacts.has(id)) {
×
UNCOV
193
        const colliderA = c.colliderA;
×
UNCOV
194
        const colliderB = c.colliderB;
×
UNCOV
195
        const side = Side.fromDirection(c.mtv);
×
UNCOV
196
        const opposite = Side.getOpposite(side);
×
UNCOV
197
        colliderA.events.emit('collisionstart', new CollisionStartEvent(colliderA, colliderB, side, c));
×
UNCOV
198
        colliderA.events.emit('contactstart', new ContactStartEvent(colliderA, colliderB, side, c) as any);
×
UNCOV
199
        colliderB.events.emit('collisionstart', new CollisionStartEvent(colliderB, colliderA, opposite, c));
×
UNCOV
200
        colliderB.events.emit('contactstart', new ContactStartEvent(colliderB, colliderA, opposite, c) as any);
×
201
      }
202
    }
203

204
    // find all contacts that have ceased
UNCOV
205
    for (const [id, c] of this._lastFrameContacts) {
×
UNCOV
206
      if (!this._currentFrameContacts.has(id)) {
×
UNCOV
207
        const colliderA = c.colliderA;
×
UNCOV
208
        const colliderB = c.colliderB;
×
UNCOV
209
        const side = Side.fromDirection(c.mtv);
×
UNCOV
210
        const opposite = Side.getOpposite(side);
×
UNCOV
211
        colliderA.events.emit('collisionend', new CollisionEndEvent(colliderA, colliderB, side, c));
×
UNCOV
212
        colliderA.events.emit('contactend', new ContactEndEvent(colliderA, colliderB, side, c) as any);
×
UNCOV
213
        colliderB.events.emit('collisionend', new CollisionEndEvent(colliderB, colliderA, opposite, c));
×
UNCOV
214
        colliderB.events.emit('contactend', new ContactEndEvent(colliderB, colliderA, opposite, c) as any);
×
215
      }
216
    }
217
  }
218
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc