Очки, множители серии и рекорды в 2D-игре: как считать награду, чтобы хотелось переиграть

Коротко

Счёт в 2D-игре складывается из базовых очков за действие и множителя серии, который растёт на 1 каждые 10 попаданий подряд, сгорает через 2 секунды без попаданий и упирается в потолок. Потолок множителя решает, насколько опытный игрок оторвётся от новичка: в нашем расчёте на 300 целях разрыв без множителя 1,2 раза, с потолком ×4 — 2,8 раза, без потолка — 3,7 раза. Таблица рекордов из 10 мест с подсказкой, сколько очков не хватило до следующего места, превращает проигрыш в повод переиграть.

Содержание

  1. Базовые очки: за что и сколько
  2. Множитель серии: шаг и потолок
  3. Окно серии: через сколько она сгорает
  4. Промах: жёсткий или мягкий сброс
  5. Расчёт: как потолок меняет разрыв между игроками
  6. Таблица рекордов и «не хватило немного»
  7. Код на JavaScript
  8. Практический сценарий: тир на 300 мишеней
  9. Когда вам это не подойдёт

Базовые очки: за что и сколько

Базовые очки дают за действие, которое вы хотите, чтобы игрок повторял, и их размер показывает, насколько действие ценно. Берите круглые числа, кратные 10 или 50: «+150» читается мгновенно, а «+137» заставляет задуматься посреди боя. Пример таблицы для тира:

ДействиеОчкиЗачем
Обычная мишень100основа счёта
Маленькая мишень250награда за точность
Мишень в движении200награда за реакцию
Время, оставшееся в конце, за секунду50награда за скорость
Попадание по «своему»−300цена ошибки

Отрицательные очки используйте осторожно: если штраф больше награды за обычное действие, осторожная игра становится выгоднее активной.

Множитель серии: шаг и потолок

Множитель серии считают по числу попаданий подряд:

множитель = min(1 + floor(серия ÷ шаг), потолок)
очки      = базовые × множитель

При шаге 10 и потолке 4 множитель ×2 включается на 10-м попадании подряд, ×3 — на 20-м, ×4 — на 30-м. 12 обычных мишеней подряд дают 9 × 100 + 3 × 200 = 1 500 очков. Показывайте множитель крупно рядом со счётом и отмечайте каждый переход на новую ступень звуком: игрок должен понимать, что сейчас рискует потерять.

Окно серии: через сколько она сгорает

Окно серии — это время, за которое нужно сделать следующее попадание, иначе серия обнуляется; мы берём 2 секунды. Без окна серия в тире не сгорает никогда, и игрок может ждать идеального выстрела сколько угодно. Окно должно быть больше, чем самый долгий промежуток между мишенями в нормальной игре, иначе серия сгорит не по вине игрока. Покажите оставшееся время полоской под множителем.

Промах: жёсткий или мягкий сброс

При промахе серию либо обнуляют (жёсткий сброс), либо снимают одну ступень множителя (мягкий сброс). Жёсткий сброс превращает каждую ошибку в событие и хорошо работает в аркаде для опытных. Мягкий прощает случайный промах, но выгоден он в первую очередь точному игроку: в нашем расчёте при потолке ×4 игрок с точностью 80% получает от мягкого сброса всего +4% (28 378 очков против 27 334), а игрок с точностью 97% — +42% (107 957 против 76 156). Разрыв между ними растёт с 2,8 до 3,8 раза. Если цель — дать новичку шанс, мягкий сброс не помогает; снижайте потолок.

Расчёт: как потолок меняет разрыв между игроками

Мы смоделировали 300 обычных мишеней по 100 очков для игроков с точностью 80%, 90% и 97%, по 500 прогонов на каждый вариант, шаг множителя 10, жёсткий сброс при промахе, если не сказано иное. Числа — средний счёт.

ПравилоТочность 80%90%97%Разрыв 97% к 80%
Без множителя23 97226 97729 0871,2 раза
Потолок ×427 33441 10276 1562,8 раза
Потолок ×827 33541 49695 1693,5 раза
Без потолка27 33541 497101 8243,7 раза
Потолок ×4, мягкий сброс28 37863 703107 9573,8 раза
Потолок ×327 31340 10565 5162,4 раза
Потолок ×227 00636 87750 4161,9 раза

Новичку с точностью 80% потолок почти не важен: он редко набирает 20 попаданий подряд. Потолок ограничивает в основном лучших игроков, и именно он решает, будет ли таблица рекордов похожа на лестницу, по которой можно подняться, или на стену.

Таблица рекордов и «не хватило немного»

Таблица рекордов из 10 мест работает лучше, если после проигрыша показывать не только место, но и сколько очков не хватило до следующего. Фраза «до 7-го места не хватило 1 200 очков» даёт конкретную цель на следующую попытку. Правила таблицы:

  1. Равный счёт — новый игрок встаёт ниже старого.
  2. Не попал в десятку — покажите, сколько не хватило до 10-го места.
  3. Хранение — массив из 10 записей с именем, счётом и датой в localStorage под ключом с версией формата; как пережить смену формата, разобрано в статье о сохранениях по ссылке в конце.
  4. Имя — до 12 символов, отображайте его как текст, а не как разметку.

Код на JavaScript

Код содержит счёт с множителем и окном серии, оба вида сброса, расчёт разрыва между игроками и таблицу рекордов. Генератор случайных чисел с зерном делает расчёт повторяемым. Код проверен в Node.js 24, вывод совпадает с комментариями.

// Детерминированный генератор для повторяемых расчётов
function rng(seed) {
  return () => {
    seed = (seed + 0x6d2b79f5) | 0;
    let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

function makeScore(rules) {
  return { score: 0, combo: 0, timer: 0, rules };
}

function multiplier(s) {
  return Math.min(1 + Math.floor(s.combo / s.rules.step), s.rules.cap);
}

function hit(s, base) {
  s.combo++;
  s.timer = s.rules.window;
  s.score += base * multiplier(s);
}

// Жёсткий сброс обнуляет серию, мягкий снимает одну ступень множителя
function miss(s) {
  s.combo = s.rules.soft ? Math.max(0, s.combo - s.rules.step) : 0;
  if (s.combo === 0) s.timer = 0;
}

// Окно серии: без попаданий дольше window секунд серия сгорает полностью
function tick(s, dt) {
  if (s.combo === 0) return;
  s.timer -= dt;
  if (s.timer <= 0) { s.combo = 0; s.timer = 0; }
}

function averageScore(rules, accuracy, runs = 500, targets = 300) {
  let sum = 0;
  for (let k = 0; k < runs; k++) {
    const rand = rng(1000 + k);
    const s = makeScore(rules);
    for (let i = 0; i < targets; i++) rand() < accuracy ? hit(s, 100) : miss(s);
    sum += s.score;
  }
  return Math.round(sum / runs);
}

// Таблица рекордов: 10 мест, при равенстве новый игрок ниже старого
function addRecord(table, entry, size = 10) {
  let place = table.findIndex((r) => entry.score > r.score);
  if (place === -1) place = table.length;
  if (place >= size) {
    return { table, place: null, missing: table[size - 1].score - entry.score + 1 };
  }
  const next = [...table.slice(0, place), entry, ...table.slice(place)].slice(0, size);
  const missing = place > 0 ? next[place - 1].score - entry.score + 1 : 0;
  return { table: next, place: place + 1, missing };
}

// ---- Проверка ----
const s = makeScore({ step: 10, cap: 4, window: 2, soft: false });
for (let i = 0; i < 12; i++) hit(s, 100);
console.log(s.score, multiplier(s)); // 1500 2
tick(s, 1.5); console.log(s.combo); // 12
tick(s, 0.6); console.log(s.combo); // 0

const RULES = {
  'без множителя': { step: 1e9, cap: 1, window: 2 },
  'потолок 4': { step: 10, cap: 4, window: 2 },
  'потолок 8': { step: 10, cap: 8, window: 2 },
  'без потолка': { step: 10, cap: Infinity, window: 2 },
  'потолок 4, мягкий сброс': { step: 10, cap: 4, window: 2, soft: true },
  'потолок 3': { step: 10, cap: 3, window: 2 },
  'потолок 2': { step: 10, cap: 2, window: 2 },
};
for (const [name, rules] of Object.entries(RULES)) {
  const a = averageScore(rules, 0.8), b = averageScore(rules, 0.9), c = averageScore(rules, 0.97);
  console.log(name, a, b, c, (c / a).toFixed(1));
}
// без множителя 23972 26977 29087 1.2
// потолок 4 27334 41102 76156 2.8
// потолок 8 27335 41496 95169 3.5
// без потолка 27335 41497 101824 3.7
// потолок 4, мягкий сброс 28378 63703 107957 3.8
// потолок 3 27313 40105 65516 2.4
// потолок 2 27006 36877 50416 1.9

let table = [9000, 8000, 7000, 6000, 5000, 4000, 3500, 3000, 2500, 2000].map((score, i) => ({ name: 'Игрок ' + (i + 1), score }));
let r = addRecord(table, { name: 'Аня', score: 3500 });
console.log(r.place, r.missing); // 8 1
r = addRecord(table, { name: 'Петя', score: 1500 });
console.log(r.place, r.missing); // null 501

Практический сценарий: тир на 300 мишеней

Возьмём тир: за 3 минуты появляются 300 мишеней, игра для школьников 10–12 лет и их родителей. Входные условия: ребёнок с точностью около 80% должен видеть свой рост, а родитель с точностью 97% — не улетать в недосягаемый отрыв. Порядок решения:

  1. Базовые очки. 100 за обычную мишень, 250 за маленькую, без штрафов за промах.
  2. Множитель. Шаг 10, потолок ×3 — по таблице выше разрыв 2,4 раза вместо 2,8 при ×4 и 3,7 без потолка.
  3. Сброс. Жёсткий: мягкий сброс в нашем расчёте увеличивает разрыв до 3,8 раза, то есть работает против цели.
  4. Окно серии. 2 секунды при интервале между мишенями в среднем 0,6 с.
  5. Рекорды. Отдельная таблица на 10 мест для каждого устройства, после игры — «до следующего места не хватило N очков».
  6. Проверка. Запустите averageScore с вашими правилами и точностями 0,8 и 0,97 и убедитесь, что разрыв не больше 2,5 раза — это наша цель для семейной игры, а не общее правило.

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

Множитель серии подходит для аркад с частыми однотипными действиями, но не для всех игр:

  • Головоломки и игры без спешки. Там награда — решённая задача, а очки за скорость портят удовольствие от размышления; лучше звёзды за число ходов.
  • Игры с сюжетом. Счёт отвлекает от истории, а таблица рекордов не нужна, если игру проходят один раз.
  • Соревнование в сети. Локальная таблица в localStorage легко правится вручную; для общей таблицы нужен сервер, который проверяет результат, а не просто принимает число.
  • Совсем маленькие дети. Для них лучше работают накопительные награды без потерь: серия, которая сгорает, может расстроить сильнее, чем мотивировать.

Мы разрабатываем J2DRU — браузерный 2D игровой движок, где хранение в браузере входит в проверенные внутри разработки основы. Движок в разработке и публично пока не открыт, заявку можно оставить на странице раннего доступа (подача заявки не гарантирует приглашение).

Источники: внешних фактов в статье нет. Таблица очков, шаг и потолок множителя, окно серии и все средние значения — наш пример и наш расчёт в Node.js 24 на 500 прогонах с фиксированным зерном.

Читайте также:

Частые вопросы

Как сделать множитель очков за серию?

Считайте попадания подряд и вычисляйте множитель как 1 + floor(серия ÷ шаг) с ограничением сверху. При шаге 10 и потолке 4 множитель ×2 включается на 10-м попадании, ×4 — на 30-м.

Нужен ли потолок у множителя?

Обычно да. Без потолка разрыв между точным и неточным игроком растёт без предела: в нашем расчёте на 300 целях он 3,7 раза против 2,8 раза при потолке ×4, и таблица рекордов становится недосягаемой для большинства.

Что делать с серией при промахе?

Жёсткий сброс обнуляет серию и наказывает каждую ошибку, мягкий снимает одну ступень множителя. Мягкий сброс почти не помогает новичку и сильнее награждает точного игрока: в нашем расчёте разрыв растёт с 2,8 до 3,8 раза, поэтому для выравнивания лучше снизить потолок.

Как хранить таблицу рекордов в браузерной игре?

Для 10 записей хватит localStorage: массив объектов с именем, счётом и датой в JSON под одним ключом с номером версии формата. Нового игрока с равным счётом ставьте ниже старого, чтобы рекорд нельзя было «отнять» повтором.