---
title: "Спавн и удаление объектов в 2D-игре: время жизни, отложенное удаление и пулы"
description: "Как создавать и удалять снаряды, монеты и частицы в 2D-игре без ошибок и лишней нагрузки: формула «одновременно на экране = время жизни ÷ интервал создания», почему нельзя удалять объект из массива во время обхода, отложенное удаление с флагом и swap-and-pop, пул объектов со списком свободных и проверенный в Node.js код на JavaScript."
slug: spavn-i-udalenie-obektov-v-2d-igre-vremya-zhizni-i-puly
date: 2026-11-04
updated: 2026-11-04
scheduled_at: "2026-11-04T19:00:00+03:00"
rubric: praktika-i-uroki
tags: ["igrovoj-cikl", "pul-obektov", "javascript", "optimizaciya", "novichkam"]
reading_time: 9
draft: false
summary: "Объекты, которые появляются во время игры, — снаряды, монеты, частицы — нужно создавать и удалять по трём правилам. Каждому объекту задают время жизни, и тогда число одновременно живущих объектов считается заранее: время жизни ÷ интервал создания. Удаление откладывают до конца обхода, иначе цикл пропускает соседние объекты. Если объекты создаются десятками в секунду, их берут из пула и возвращают туда, а не создают заново. В статье — код на JavaScript для всех трёх правил и симуляция 10 секунд стрельбы."
---

## Коротко

Снаряды, монеты и частицы появляются во время выполнения игры и должны так же надёжно исчезать. Три правила закрывают почти все ошибки: у каждого объекта есть время жизни, поэтому максимум одновременно живущих объектов считается заранее; удаление откладывается до конца обхода, чтобы цикл не пропускал соседей; частые объекты берутся из пула вместо создания заново. Ниже — формула, таблица ошибок и код на JavaScript, проверенный в Node.js.

## Содержание

1. Время жизни: сколько объектов будет одновременно
2. Почему удаление во время обхода ломает игру
3. Отложенное удаление: флаг, уборка и swap-and-pop
4. Пул объектов: когда нужен и как устроен
5. Практический сценарий: 10 секунд стрельбы
6. Когда вам это не подойдёт

## Время жизни: сколько объектов будет одновременно

Число одновременно живущих объектов определяется двумя числами: временем жизни и интервалом создания. Если объекты появляются равномерно, а исчезают по таймеру, формула такая:

```text
максимум одновременно = ⌈ время жизни ÷ интервал создания ⌉

время жизни снаряда, который должен улететь за экран:
время жизни = расстояние ÷ скорость
```

Таблица: Примеры расчёта

| Объект | Время жизни | Интервал создания | Максимум одновременно |
|---|---|---|---|
| Снаряд героя | `2 с` | `0,25 с` (4 выстрела в секунду) | `8` |
| Монета из сундука | `5 с` | `0,5 с` | `10` |
| Снаряд, пересекающий экран `1280 px` со скоростью `600 px/с` | `1280 ÷ 600 ≈ 2,13 с` | `0,25 с` | `9` |
| Искры от удара, `12` за раз | `0,4 с` | `0,3 с` между ударами | `2 × 12 = 24` |

Время жизни нужно даже тем объектам, которые «и так» исчезают при столкновении: снаряд, улетевший мимо всех врагов, без таймера будет жить вечно за пределами экрана. Расчёт заодно даёт размер пула из раздела 4.

## Почему удаление во время обхода ломает игру

Удаление элемента из массива внутри цикла по этому же массиву заставляет цикл пропустить следующий элемент. Нистром разбирает это в главе *Update Method*: после удаления все элементы сдвигаются на одну позицию назад, а индекс цикла всё равно увеличивается (*Game Programming Patterns*, проверено: ноябрь 2026).

```js
// Ошибка: удаление через splice прямо в цикле обновления.
function updateNaive(list, dt) {
  const updated = [];
  for (let i = 0; i < list.length; i++) {
    const o = list[i];
    o.life -= dt;
    updated.push(o.id);
    if (o.life <= 0) list.splice(i, 1); // следующий объект сдвинулся на место i
  }
  return updated;
}

const objs = [
  { id: "a", life: 1 }, { id: "b", life: 0.01 },
  { id: "c", life: 1 }, { id: "d", life: 1 },
];
console.log(updateNaive(objs, 0.016)); // [ 'a', 'b', 'd' ] — объект c пропущен
```

В этом примере объект `c` в кадре не получил обновления: его таймер не уменьшился, он не сдвинулся и не проверил столкновения. В игре это выглядит как редкое «подрагивание» или пролёт снаряда сквозь врага, и такую ошибку трудно поймать, потому что она случается только в кадр, когда кто-то исчез.

## Отложенное удаление: флаг, уборка и swap-and-pop

Надёжное решение — во время обхода только помечать объект как мёртвый, а убирать мёртвые после обхода. Это второй способ из главы Нистрома; первый (обход с конца) тоже работает, но ломается, если объект во время своего обновления удаляет другой объект.

```js
// Отложенное удаление: в кадре — только флаг, уборка — после обхода.
function update(list, dt, onDie) {
  for (let i = 0; i < list.length; i++) {
    const o = list[i];
    if (o.dead) continue;           // помеченные в этом кадре больше не участвуют
    o.life -= dt;
    if (o.life <= 0) {
      o.dead = true;
      if (onDie) onDie(o);          // последнее слово объекта: вспышка, бонус, звук
    }
  }
  sweep(list);
}

// Уборка за O(1) на объект: последний элемент переезжает на место мёртвого.
function sweep(list) {
  let i = 0;
  while (i < list.length) {
    if (list[i].dead) {
      list[i] = list[list.length - 1];
      list.pop();                   // i не увеличиваем: на месте i теперь новый объект
    } else {
      i++;
    }
  }
}

const list2 = [
  { id: "a", life: 1 }, { id: "b", life: 0.01 },
  { id: "c", life: 1 }, { id: "d", life: 1 },
];
const effects = [];
update(list2, 0.016, function (o) { effects.push("вспышка на месте " + o.id); });
console.log(list2.map(function (o) { return o.id; })); // [ 'a', 'd', 'c' ]
console.log(effects);                                  // [ 'вспышка на месте b' ]
```

У swap-and-pop одна цена: порядок элементов меняется. Если порядок важен (например, объекты в массиве отсортированы по слою отрисовки), вместо него используют сдвиг: живые объекты переписывают в начало массива по порядку и укорачивают массив один раз.

Новые объекты тоже лучше добавлять отложенно. Нистром замечает, что объект, добавленный в конец списка во время обхода, успеет обновиться в том же кадре, ещё до того, как игрок его увидит. Простое решение — складывать созданные в кадре объекты в отдельный массив и переносить в основной после уборки.

Таблица: Типичные ошибки создания и удаления

| Ошибка | Как проявляется | Исправление |
|---|---|---|
| Удаление через `splice` в цикле | Соседний объект пропускает кадр | Флаг `dead` и уборка после обхода |
| Нет времени жизни | Число объектов растёт, игра постепенно замедляется | Таймер у каждого создаваемого объекта |
| Создание в каждом кадре без интервала | При 60 кадр/с — 60 объектов в секунду вместо 4 | Накопитель времени и интервал создания |
| Действие с уже удалённым объектом | Бонус или вспышка появляются в точке (0, 0) | Читать координаты до пометки `dead`, в `onDie` |
| Объект из пула не очищен | Новый снаряд «помнит» скорость или цвет прошлого | Сбрасывать все поля в `init` |

## Пул объектов: когда нужен и как устроен

Пул — это заранее созданный набор объектов, которые не удаляются, а выключаются и потом используются снова. В главе *Object Pool* Нистром объясняет, зачем это нужно: в C++ на консолях и мобильных устройствах главная проблема — фрагментация памяти, а в языках со сборкой мусора пулы встречаются реже, но всё равно полезны, чтобы не платить за создание и освобождение объектов, особенно на мобильных устройствах (*Game Programming Patterns*, проверено: ноябрь 2026).

Там же названы три подводных камня, которые полностью применимы к JavaScript:

- пул занимает память даже тогда, когда объекты не нужны, поэтому его размер считают по формуле из раздела 1;
- переиспользованный объект не очищается сам — каждое поле нужно задать заново при выдаче;
- пул может закончиться, и решение «что тогда» принимают заранее: пропустить создание, заменить самый старый объект или увеличить пул.

Чтобы не перебирать весь пул в поисках свободного места, Нистром предлагает хранить список свободных объектов. В JavaScript это обычный массив-стек:

```js
// Пул со списком свободных объектов. Размер — из формулы «время жизни ÷ интервал».
function createPool(size, makeObject) {
  const all = [];
  const free = [];
  for (let i = 0; i < size; i++) {
    const o = makeObject();
    o.active = false;
    all.push(o);
    free.push(o);
  }
  return {
    all: all,
    acquire: function () {
      const o = free.pop();          // undefined, если пул пуст
      if (o) o.active = true;
      return o;
    },
    release: function (o) {
      if (!o.active) return;         // защита от двойного возврата
      o.active = false;
      free.push(o);
    },
    freeCount: function () { return free.length; },
  };
}

function makeBullet() { return { x: 0, y: 0, vx: 0, life: 0, active: false }; }

// init задаёт ВСЕ поля: переиспользованный объект не очищается сам.
function initBullet(b, x, y, vx, life) {
  b.x = x; b.y = y; b.vx = vx; b.life = life;
  return b;
}
```

## Практический сценарий: 10 секунд стрельбы

Задача: герой стреляет `4` раза в секунду, снаряд летит `600 px/с` и живёт `2 с`, игра идёт `60` кадров в секунду. Нужно убедиться, что за `10` секунд одновременно живёт не больше `8` снарядов, пул на `8` мест ни разу не заканчивается и новые объекты не создаются. Код ниже использует `createPool`, `makeBullet` и `initBullet` из предыдущего раздела.

```js
const DT = 1 / 60;
const FIRE_INTERVAL = 0.25;
const LIFE = 2;
const POOL_SIZE = Math.ceil(LIFE / FIRE_INTERVAL); // 8

let created = 0;
const pool = createPool(POOL_SIZE, function () { created++; return makeBullet(); });
const createdAtStart = created;

const live = [];
let fireTimer = 0;
let maxAlive = 0;
let misses = 0;

for (let frame = 0; frame < 600; frame++) {   // 600 кадров = 10 секунд
  // 1. создание по интервалу, а не в каждом кадре
  fireTimer += DT;
  while (fireTimer >= FIRE_INTERVAL - 1e-9) {
    fireTimer -= FIRE_INTERVAL;
    const b = pool.acquire();
    if (b) live.push(initBullet(b, 100, 360, 600, LIFE));
    else misses++;
  }
  // 2. обновление: только пометка
  for (let i = 0; i < live.length; i++) {
    const b = live[i];
    b.x += b.vx * DT;
    b.life -= DT;
    if (b.life <= 1e-9) b.dead = true;
  }
  // 3. уборка после обхода: swap-and-pop и возврат в пул
  let i = 0;
  while (i < live.length) {
    if (live[i].dead) {
      live[i].dead = false;
      pool.release(live[i]);
      live[i] = live[live.length - 1];
      live.pop();
    } else i++;
  }
  if (live.length > maxAlive) maxAlive = live.length;
}

console.log("размер пула:", POOL_SIZE);                         // 8
console.log("максимум одновременно:", maxAlive);                 // 8
console.log("отказов пула:", misses);                            // 0
console.log("создано объектов во время игры:", created - createdAtStart); // 0
```

В Node.js симуляция выдаёт: размер пула `8`, максимум одновременно `8`, отказов `0`, новых объектов во время игры `0`. Если уменьшить пул до `7`, счётчик отказов становится больше нуля — так проверяют, что формула размера верна для вашей частоты стрельбы. Маленькие допуски `1e-9` нужны из-за того, что `1/60` не представляется в двоичной арифметике точно и сумма шагов накапливает погрешность.

## Когда вам это не подойдёт

Три правила нужны не в каждой игре, и иногда они только усложняют код:

- **Объектов мало и они создаются редко.** Если на уровне десяток врагов, которые появляются при входе в комнату, обычного создания и удаления достаточно; пул тут лишний.
- **Объекты сильно различаются по составу.** Пул хорош для одинаковых объектов (снаряды, частицы); если каждый враг собран из разного набора свойств, переиспользование превращается в долгую очистку полей.
- **Порядок объектов в массиве важен.** swap-and-pop меняет порядок; если массив одновременно служит порядком отрисовки, используйте сдвиг с сохранением порядка или отдельный список для рисования.
- **Нужна сохранённая история объектов.** Если игра пишет повтор прохождения, удалённый объект нельзя просто вернуть в пул без записи его последнего состояния.

Мы разрабатываем **J2DRU** — браузерный 2D игровой движок с событийной логикой; он в разработке и публично пока не открыт, заявку можно оставить на [странице раннего доступа](/early-access?utm_source=blog&utm_medium=article&utm_campaign=spavn-i-udalenie-obektov-v-2d-igre-vremya-zhizni-i-puly) (подача заявки не гарантирует приглашение).

**Источники (проверено: ноябрь 2026):** Robert Nystrom, *Game Programming Patterns*, главы *Update Method* (удаление и добавление объектов во время обхода, флаг «мёртв» и уборка после обхода) и *Object Pool* (фрагментация, пулы в языках со сборкой мусора, очистка переиспользованных объектов, поведение при заполненном пуле, список свободных объектов), `gameprogrammingpatterns.com`. Формула максимума, расчёты в таблицах и код — наши, код проверен в Node.js.

**Читайте также:**
- [Игровой цикл и физика столкновений AABB](/blog/igrovoj-cikl-i-fizika-stolknovenij-aabb)
- [От Breakout до Arkanoid: ускорение мяча, прочные блоки и капсулы](/blog/istoriya-arkanoidov-ot-breakout-do-arkanoid-1986)
- [Конечный автомат персонажа в платформере](/blog/konechnyj-avtomat-personazha-platformera)
