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

excaliburjs / Excalibur / 15354777440

30 May 2025 08:03PM UTC coverage: 87.858% (-1.5%) from 89.344%
15354777440

Pull #3385

github

web-flow
Merge a00f57733 into e6ec66358
Pull Request #3385: updated Meet action to add tolerance

5002 of 6948 branches covered (71.99%)

3 of 5 new or added lines in 2 files covered. (60.0%)

872 existing lines in 83 files now uncovered.

13661 of 15549 relevant lines covered (87.86%)

25187.01 hits per line

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

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

27
  public systemType = SystemType.Update;
1,351✔
28
  public query: Query<ComponentCtor<TransformComponent> | ComponentCtor<MotionComponent> | ComponentCtor<ColliderComponent>>;
29

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

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

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

74
  initialize(world: World, scene: Scene) {
75
    this._engine = scene.engine;
739✔
76
  }
77

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

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

93
        // Flatten composite colliders
94
        if (collider instanceof CompositeCollider) {
1,242✔
95
          const compositeColliders = collider.getColliders();
12✔
96
          if (!collider.compositeStrategy) {
12✔
97
            collider.compositeStrategy = this._physics.config.colliders.compositeStrategy;
10✔
98
          }
99
          colliders = colliders.concat(compositeColliders);
12✔
100
        } else {
101
          colliders.push(collider);
1,230✔
102
        }
103
      }
104
    }
105

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

111
    // Run broadphase on all colliders and locates potential collisions
112
    let pairs = this._processor.broadphase(colliders, elapsed);
1,791✔
113

114
    this._currentFrameContacts.clear();
1,791✔
115

116
    // Given possible pairs find actual contacts
117
    let contacts: CollisionContact[] = [];
1,791✔
118

119
    const solver: CollisionSolver = this.getSolver();
1,791✔
120

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

133
      if (pairs.length) {
1,791✔
134
        contacts = this._processor.narrowphase(pairs, this._engine?.debug?.stats?.currFrame);
112!
135
        contacts = solver.solve(contacts);
112✔
136

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

154
    // Emit contact start/end events
155
    this.runContactStartEnd();
1,791✔
156

157
    // reset the last frame cache
158
    this._lastFrameContacts.clear();
1,791✔
159

160
    // Keep track of collisions contacts that have started or ended
161
    this._lastFrameContacts = new Map(this._currentFrameContacts);
1,791✔
162

163
    // Process deferred collider removals
164
    for (const entity of this.query.entities) {
1,791✔
165
      const collider = entity.get(ColliderComponent);
2,180✔
166
      if (collider) {
2,180!
167
        collider.processColliderRemoval();
2,180✔
168
      }
169
    }
170
  }
171

172
  postupdate(): void {
173
    SeparatingAxis.SeparationPool.done();
1,789✔
174
  }
175

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

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

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

205
    // find all contacts that have ceased
206
    for (const [id, c] of this._lastFrameContacts) {
1,791✔
207
      if (!this._currentFrameContacts.has(id)) {
101✔
208
        const colliderA = c.colliderA;
10✔
209
        const colliderB = c.colliderB;
10✔
210
        const side = Side.fromDirection(c.mtv);
10✔
211
        const opposite = Side.getOpposite(side);
10✔
212
        colliderA.events.emit('collisionend', new CollisionEndEvent(colliderA, colliderB, side, c));
10✔
213
        colliderA.events.emit('contactend', new ContactEndEvent(colliderA, colliderB, side, c) as any);
10✔
214
        colliderB.events.emit('collisionend', new CollisionEndEvent(colliderB, colliderA, opposite, c));
10✔
215
        colliderB.events.emit('contactend', new ContactEndEvent(colliderB, colliderA, opposite, c) as any);
10✔
216
      }
217
    }
218
  }
219
}
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