Article Physics & Mechanics · ≈ ⏱ 11 min read

Event-driven simulation: exact collisions without stepping through time

A billiard ball moving at 40 m/s can pass straight through a thin cushion between two discrete time steps — a bug known as tunneling. Event-driven simulation eliminates it entirely by computing the exact time of every future collision analytically.

TL;DR: Event-driven simulation avoids the tunneling bug of fixed time-steps by solving a quadratic equation for the exact collision time between objects, then processing collisions in order via a priority queue with lazy invalidation. This gives exact, physically consistent collisions (momentum and energy conserved) at O(N² log N) cost, ideal for billiards and hard-sphere molecular dynamics.

1. The tunneling problem

У покроковій (time-stepped) симуляції позиція частинки оновлюється дискретно: x(t + dt) = x(t) + v·dt. Якщо частинка рухається досить швидко відносно dt і товщини перешкоди, вона може «перестрибнути» перешкоду цілком — на кроці t вона перед стінкою, на кроці t+dt — вже за нею, і жодна перевірка перетину не спрацьовує. Це явище називають тунелюванням (tunneling).

Tunneling condition v · dt > wall_thickness    ⟹   collision missed

Зменшення dt «вирішує» проблему ціною квадратичного зростання вартості (менший dt → більше кроків на ту саму тривалість симуляції). Для більярду, ігор з високою швидкістю снарядів чи молекулярної динаміки з жорсткими зіткненнями це неприйнятно.

2. Time-stepped vs. event-driven

Event-driven simulation інвертує підхід: замість «просувати всіх на dt і перевіряти перетини» вона обчислює точний аналітичний момент часу наступної колізії для кожної пари об'єктів, кладе всі майбутні події в чергу з пріоритетом за часом і просуває симуляцію рівно до наступної події — не менше, не більше.

Time-stepped

  • Фіксований або адаптивний dt
  • Може пропустити швидкі зіткнення (tunneling)
  • Вартість O(steps · N²) для перевірки перетинів
  • Простіше реалізувати для м'яких тіл

Event-driven

  • Час зіткнення — точне аналітичне рішення
  • Тунелювання неможливе за конструкцією
  • Вартість O(N² log N) — залежить від N подій, не dt
  • Ідеально для жорстких тіл, більярду, MD hard-spheres

3. Deriving time-of-impact analytically

Для двох куль радіусів r₁, r₂ з позиціями p₁, p₂ і швидкостями v₁, v₂ момент зіткнення — це найменше t > 0, за якого відстань між центрами дорівнює сумі радіусів. Позначимо відносну позицію Δp = p₂ − p₁ і відносну швидкість Δv = v₂ − v₁:

Collision condition |Δp + Δv·t|² = (r₁ + r₂)²

Розкриваючи квадрат, отримуємо квадратне рівняння відносно t:

Quadratic in t a·t² + b·t + c = 0
a = Δv · Δv
b = 2 · (Δp · Δv)
c = Δp · Δp − (r₁ + r₂)²

Розв'язок: t = (−b − √(b² − 4ac)) / (2a) — беремо менший корінь (перший момент дотику, а не другий, коли кулі вже пройшли одна одну). Колізія реальна лише якщо:

  • Дискримінант ≥ 0 — траєкторії взагалі перетинаються.
  • t > 0 — зіткнення в майбутньому, не в минулому.
  • Δp · Δv < 0 — кулі зближуються, а не віддаляються.

4. The priority queue algorithm

Алгоритм (класична схема, описана Sedgewick & Wayne для симуляції молекулярної динаміки твердих сфер) підтримує мін-купу подій, впорядковану за часом зіткнення:

Event-driven main loop 1. Обчислити всі майбутні зіткнення (пара-пара, пара-стіна)
2. Покласти у пріоритетну чергу за часом
3. Витягти найближчу подію (t_min, i, j)
4. Просунути ВСІ частинки на t_min (прямолінійний рух)
5. Розв'язати зіткнення (i, j) — оновити швидкості
6. Перерахувати нові можливі зіткнення для i, j
7. Повторити з кроку 3

Складність O(N² log N) для початкового заповнення черги (усі пари) плюс O(N log N) на кожну подію (перерахунок і вставка нових зіткнень для двох змінених частинок). Для N ~ 100–1000 куль це швидше і завжди точніше за time-stepped підхід.

5. Event invalidation

Найтонший момент реалізації: коли частинка i зіштовхується з j, усі раніше заплановані події за участю i чи j стають недійсними — швидкість i чи j змінилась, і старий прогноз зіткнення більше не вірний. Наївний підхід — видаляти застарілі події з черги (дорого для бінарної купи). Практичний підхід — залишати їх у черзі, але зберігати лічильник зіткнень кожної частинки на момент створення події; при витягуванні події з черги перевіряти, чи лічильники досі співпадають.

Lazy invalidation trick

event.valid = (particle.collisionCount === event.countAtCreation). Якщо ні — подія застаріла, просто пропускаємо її без перерахунку черги. Це уникає дорогого видалення довільного елемента з бінарної купи (O(N)) ціною зберігання «мертвих» подій, які згодом просто ігноруються (O(log N) для pop).

6. Elastic collision response

Для пружного (elastic) зіткнення двох куль мас m₁, m₂ у момент дотику, імпульс передається вздовж лінії центрів (нормаль n̂ = Δp / |Δp|):

Impulse-based elastic collision J = 2 · m₁ · m₂ / (m₁ + m₂) · (Δv · n̂)
v₁' = v₁ + (J / m₁) · n̂
v₂' = v₂ − (J / m₂) · n̂

Ця формула точно зберігає і імпульс, і кінетичну енергію системи двох тіл — на відміну від пружинної моделі контакту, де енергія неминуче трохи «просочується» через дискретизацію dt.

7. Method comparison

Method Tunneling Collision time Cost model Used for
Discrete time-step Possible Rounded to dt O(steps · N²) Soft bodies, general games
Continuous CCD (swept) Prevented per-step Exact within step O(steps · N² · solve) Fast projectiles in game engines
Event-driven Impossible by construction Exact, analytic O(N² log N) Billiards, hard-sphere MD

8. Псевдокод

Time-of-impact + priority queue (JS)

// Час до зіткнення двох куль (найменший додатний корінь)
function timeToCollision(a, b) {
  const dp = sub(b.pos, a.pos);
  const dv = sub(b.vel, a.vel);
  const R = a.radius + b.radius;

  const A = dot(dv, dv);
  if (A === 0) return Infinity;   // однакова швидкість — ніколи не зіткнуться

  const B = 2 * dot(dp, dv);
  if (B >= 0) return Infinity;    // віддаляються

  const C = dot(dp, dp) − R * R;
  const disc = B * B − 4 * A * C;
  if (disc < 0) return Infinity;    // траєкторії не перетинаються

  return (−B − Math.sqrt(disc)) / (2 * A);  // перший момент дотику
}

// Головний цикл: витягти найближчу подію, просунути час, розв'язати
function runEventDriven(particles, pq, tEnd) {
  let tNow = 0;
  while (tNow < tEnd) {
    const event = pq.popMin();
    if (!event.isValid()) continue;    // lazy invalidation

    const dt = event.time − tNow;
    for (const p of particles) p.advance(dt);  // прямолінійний рух
    tNow = event.time;

    resolveCollision(event.a, event.b);          // оновити швидкості
    predictNewCollisions(event.a, event.b, pq); // нові події в чергу
  }
}
▶ Live Demo

See exact collisions in action

Симуляція більярду обчислює точний час кожного удару кулі — жодна швидка куля не «протуне» крізь бортик чи іншу кулю.

🎱 Billiards Physics 🔵 Molecular Dynamics

🔗 Related Simulations

🎱Billiards 🔵Molecular Dynamics N-Body