• 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

0.0
/src/engine/Collision/Solver/ArcadeSolver.ts
1
import { PostCollisionEvent, PreCollisionEvent } from '../../Events';
2
import { CollisionContact } from '../Detection/CollisionContact';
3
import { CollisionType } from '../CollisionType';
4
import { Side } from '../Side';
5
import { CollisionSolver } from './Solver';
6
import { BodyComponent } from '../BodyComponent';
7
import { ContactBias, ContactSolveBias, HorizontalFirst, None, VerticalFirst } from './ContactBias';
8
import { PhysicsConfig } from '../PhysicsConfig';
9
import { DeepRequired } from '../../Util/Required';
10

11
/**
12
 * ArcadeSolver is the default in Excalibur. It solves collisions so that there is no overlap between contacts,
13
 * and negates velocity along the collision normal.
14
 *
15
 * This is usually the type of collisions used for 2D games that don't need a more realistic collision simulation.
16
 *
17
 */
18
export class ArcadeSolver implements CollisionSolver {
UNCOV
19
  directionMap = new Map<string, 'horizontal' | 'vertical'>();
×
UNCOV
20
  distanceMap = new Map<string, number>();
×
21

UNCOV
22
  constructor(public config: DeepRequired<Pick<PhysicsConfig, 'arcade'>['arcade']>) {}
×
23

24
  public solve(contacts: CollisionContact[]): CollisionContact[] {
25
    // Events and init
UNCOV
26
    this.preSolve(contacts);
×
27

28
    // Remove any canceled contacts
UNCOV
29
    contacts = contacts.filter((c) => !c.isCanceled());
×
30

31
    // Locate collision bias order
32
    let bias: ContactBias;
UNCOV
33
    switch (this.config.contactSolveBias) {
×
34
      case ContactSolveBias.HorizontalFirst: {
×
35
        bias = HorizontalFirst;
×
36
        break;
×
37
      }
38
      case ContactSolveBias.VerticalFirst: {
UNCOV
39
        bias = VerticalFirst;
×
UNCOV
40
        break;
×
41
      }
42
      default: {
UNCOV
43
        bias = None;
×
44
      }
45
    }
46

47
    // Sort by bias (None, VerticalFirst, HorizontalFirst) to avoid artifacts with seams
48
    // Sort contacts by distance to avoid artifacts with seams
49
    // It's important to solve in a specific order
UNCOV
50
    contacts.sort((a, b) => {
×
UNCOV
51
      const aDir = this.directionMap.get(a.id);
×
UNCOV
52
      const bDir = this.directionMap.get(b.id);
×
UNCOV
53
      const aDist = this.distanceMap.get(a.id);
×
UNCOV
54
      const bDist = this.distanceMap.get(b.id);
×
UNCOV
55
      return bias[aDir] - bias[bDir] || aDist - bDist;
×
56
    });
57

UNCOV
58
    for (const contact of contacts) {
×
59
      // Solve position first in arcade
UNCOV
60
      this.solvePosition(contact);
×
61

62
      // Solve velocity second in arcade
UNCOV
63
      this.solveVelocity(contact);
×
64
    }
65

66
    // Events and any contact house-keeping the solver needs
UNCOV
67
    this.postSolve(contacts);
×
68

UNCOV
69
    return contacts;
×
70
  }
71

UNCOV
72
  private _compositeContactsIds = new Set<string>();
×
73
  public preSolve(contacts: CollisionContact[]) {
UNCOV
74
    const epsilon = 0.0001;
×
UNCOV
75
    for (let i = 0; i < contacts.length; i++) {
×
UNCOV
76
      const contact = contacts[i];
×
77

78
      // Cancel dup composite together strategy contacts
UNCOV
79
      const index = contact.id.indexOf('|');
×
UNCOV
80
      if (index > 0) {
×
UNCOV
81
        const compositeId = contact.id.substring(index + 1);
×
UNCOV
82
        if (this._compositeContactsIds.has(compositeId)) {
×
UNCOV
83
          contact.cancel();
×
UNCOV
84
          continue;
×
85
        }
UNCOV
86
        this._compositeContactsIds.add(compositeId);
×
87
      }
88

89
      // Cancel near 0 mtv collisions
UNCOV
90
      if (Math.abs(contact.mtv.x) < epsilon && Math.abs(contact.mtv.y) < epsilon) {
×
UNCOV
91
        contact.cancel();
×
UNCOV
92
        continue;
×
93
      }
94

95
      // Record distance/direction for sorting
UNCOV
96
      const side = Side.fromDirection(contact.mtv);
×
UNCOV
97
      const mtv = contact.mtv.negate();
×
98

UNCOV
99
      const distance = Math.abs(contact.info.separation);
×
UNCOV
100
      this.distanceMap.set(contact.id, distance);
×
101

UNCOV
102
      this.directionMap.set(contact.id, side === Side.Left || side === Side.Right ? 'horizontal' : 'vertical');
×
103

104
      // Publish collision events on both participants
UNCOV
105
      contact.colliderA.events.emit('precollision', new PreCollisionEvent(contact.colliderA, contact.colliderB, side, mtv, contact));
×
UNCOV
106
      contact.colliderB.events.emit(
×
107
        'precollision',
108
        new PreCollisionEvent(contact.colliderB, contact.colliderA, Side.getOpposite(side), mtv.negate(), contact)
109
      );
110
    }
UNCOV
111
    this._compositeContactsIds.clear();
×
112
  }
113

114
  public postSolve(contacts: CollisionContact[]) {
UNCOV
115
    for (let i = 0; i < contacts.length; i++) {
×
UNCOV
116
      const contact = contacts[i];
×
UNCOV
117
      if (contact.isCanceled()) {
×
UNCOV
118
        continue;
×
119
      }
UNCOV
120
      const colliderA = contact.colliderA;
×
UNCOV
121
      const colliderB = contact.colliderB;
×
UNCOV
122
      const bodyA = colliderA.owner?.get(BodyComponent);
×
UNCOV
123
      const bodyB = colliderB.owner?.get(BodyComponent);
×
UNCOV
124
      if (bodyA && bodyB) {
×
UNCOV
125
        if (bodyA.collisionType === CollisionType.Passive || bodyB.collisionType === CollisionType.Passive) {
×
UNCOV
126
          continue;
×
127
        }
128
      }
129

UNCOV
130
      const side = Side.fromDirection(contact.mtv);
×
UNCOV
131
      const mtv = contact.mtv.negate();
×
132
      // Publish collision events on both participants
UNCOV
133
      contact.colliderA.events.emit('postcollision', new PostCollisionEvent(contact.colliderA, contact.colliderB, side, mtv, contact));
×
UNCOV
134
      contact.colliderB.events.emit(
×
135
        'postcollision',
136
        new PostCollisionEvent(contact.colliderB, contact.colliderA, Side.getOpposite(side), mtv.negate(), contact)
137
      );
138
    }
139
  }
140

141
  public solvePosition(contact: CollisionContact) {
UNCOV
142
    const epsilon = 0.0001;
×
143
    // if bounds no longer intersect skip to the next
144
    // this removes jitter from overlapping/stacked solid tiles or a wall of solid tiles
UNCOV
145
    if (!contact.colliderA.bounds.overlaps(contact.colliderB.bounds, epsilon)) {
×
146
      // Cancel the contact to prevent and solving
UNCOV
147
      contact.cancel();
×
UNCOV
148
      return;
×
149
    }
150

UNCOV
151
    if (Math.abs(contact.mtv.x) < epsilon && Math.abs(contact.mtv.y) < epsilon) {
×
152
      // Cancel near 0 mtv collisions
153
      contact.cancel();
×
154
      return;
×
155
    }
156

UNCOV
157
    let mtv = contact.mtv;
×
UNCOV
158
    const colliderA = contact.colliderA;
×
UNCOV
159
    const colliderB = contact.colliderB;
×
UNCOV
160
    const bodyA = colliderA.owner?.get(BodyComponent);
×
UNCOV
161
    const bodyB = colliderB.owner?.get(BodyComponent);
×
UNCOV
162
    if (bodyA && bodyB) {
×
UNCOV
163
      if (bodyA.collisionType === CollisionType.Passive || bodyB.collisionType === CollisionType.Passive) {
×
UNCOV
164
        return;
×
165
      }
166

UNCOV
167
      if (bodyA.collisionType === CollisionType.Active && bodyB.collisionType === CollisionType.Active) {
×
168
        // split overlaps if both are Active
UNCOV
169
        mtv = mtv.scale(0.5);
×
170
      }
171

172
      // Resolve overlaps
UNCOV
173
      if (bodyA.collisionType === CollisionType.Active) {
×
UNCOV
174
        bodyA.globalPos.x -= mtv.x;
×
UNCOV
175
        bodyA.globalPos.y -= mtv.y;
×
UNCOV
176
        colliderA.update(bodyA.transform.get());
×
177
      }
178

UNCOV
179
      if (bodyB.collisionType === CollisionType.Active) {
×
UNCOV
180
        bodyB.globalPos.x += mtv.x;
×
UNCOV
181
        bodyB.globalPos.y += mtv.y;
×
UNCOV
182
        colliderB.update(bodyB.transform.get());
×
183
      }
184
    }
185
  }
186

187
  public solveVelocity(contact: CollisionContact) {
UNCOV
188
    if (contact.isCanceled()) {
×
UNCOV
189
      return;
×
190
    }
191

UNCOV
192
    const colliderA = contact.colliderA;
×
UNCOV
193
    const colliderB = contact.colliderB;
×
UNCOV
194
    const bodyA = colliderA.owner?.get(BodyComponent);
×
UNCOV
195
    const bodyB = colliderB.owner?.get(BodyComponent);
×
196

UNCOV
197
    if (bodyA && bodyB) {
×
UNCOV
198
      if (bodyA.collisionType === CollisionType.Passive || bodyB.collisionType === CollisionType.Passive) {
×
UNCOV
199
        return;
×
200
      }
201

UNCOV
202
      const normal = contact.normal;
×
UNCOV
203
      const opposite = normal.negate();
×
204

UNCOV
205
      if (bodyA.collisionType === CollisionType.Active) {
×
206
        // only adjust velocity if the contact normal is opposite to the current velocity
207
        // this avoids catching edges on a platform when sliding off
UNCOV
208
        if (bodyA.vel.normalize().dot(opposite) < 0) {
×
209
          // Cancel out velocity opposite direction of collision normal
UNCOV
210
          const velAdj = normal.scale(normal.dot(bodyA.vel.negate()));
×
UNCOV
211
          bodyA.vel = bodyA.vel.add(velAdj);
×
212
        }
213
      }
214

UNCOV
215
      if (bodyB.collisionType === CollisionType.Active) {
×
216
        // only adjust velocity if the contact normal is opposite to the current velocity
217
        // this avoids catching edges on a platform
UNCOV
218
        if (bodyB.vel.normalize().dot(normal) < 0) {
×
UNCOV
219
          const velAdj = opposite.scale(opposite.dot(bodyB.vel.negate()));
×
UNCOV
220
          bodyB.vel = bodyB.vel.add(velAdj);
×
221
        }
222
      }
223
    }
224
  }
225
}
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