Коротко
Счёт в 2D-игре складывается из базовых очков за действие и множителя серии, который растёт на 1 каждые 10 попаданий подряд, сгорает через 2 секунды без попаданий и упирается в потолок. Потолок множителя решает, насколько опытный игрок оторвётся от новичка: в нашем расчёте на 300 целях разрыв без множителя 1,2 раза, с потолком ×4 — 2,8 раза, без потолка — 3,7 раза. Таблица рекордов из 10 мест с подсказкой, сколько очков не хватило до следующего места, превращает проигрыш в повод переиграть.
Содержание
- Базовые очки: за что и сколько
- Множитель серии: шаг и потолок
- Окно серии: через сколько она сгорает
- Промах: жёсткий или мягкий сброс
- Расчёт: как потолок меняет разрыв между игроками
- Таблица рекордов и «не хватило немного»
- Код на JavaScript
- Практический сценарий: тир на 300 мишеней
- Когда вам это не подойдёт
Базовые очки: за что и сколько
Базовые очки дают за действие, которое вы хотите, чтобы игрок повторял, и их размер показывает, насколько действие ценно. Берите круглые числа, кратные 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 972 | 26 977 | 29 087 | 1,2 раза |
| Потолок ×4 | 27 334 | 41 102 | 76 156 | 2,8 раза |
| Потолок ×8 | 27 335 | 41 496 | 95 169 | 3,5 раза |
| Без потолка | 27 335 | 41 497 | 101 824 | 3,7 раза |
| Потолок ×4, мягкий сброс | 28 378 | 63 703 | 107 957 | 3,8 раза |
| Потолок ×3 | 27 313 | 40 105 | 65 516 | 2,4 раза |
| Потолок ×2 | 27 006 | 36 877 | 50 416 | 1,9 раза |
Новичку с точностью 80% потолок почти не важен: он редко набирает 20 попаданий подряд. Потолок ограничивает в основном лучших игроков, и именно он решает, будет ли таблица рекордов похожа на лестницу, по которой можно подняться, или на стену.
Таблица рекордов и «не хватило немного»
Таблица рекордов из 10 мест работает лучше, если после проигрыша показывать не только место, но и сколько очков не хватило до следующего. Фраза «до 7-го места не хватило 1 200 очков» даёт конкретную цель на следующую попытку. Правила таблицы:
- Равный счёт — новый игрок встаёт ниже старого.
- Не попал в десятку — покажите, сколько не хватило до 10-го места.
- Хранение — массив из 10 записей с именем, счётом и датой в
localStorageпод ключом с версией формата; как пережить смену формата, разобрано в статье о сохранениях по ссылке в конце. - Имя — до 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% — не улетать в недосягаемый отрыв. Порядок решения:
- Базовые очки. 100 за обычную мишень, 250 за маленькую, без штрафов за промах.
- Множитель. Шаг 10, потолок ×3 — по таблице выше разрыв 2,4 раза вместо 2,8 при ×4 и 3,7 без потолка.
- Сброс. Жёсткий: мягкий сброс в нашем расчёте увеличивает разрыв до 3,8 раза, то есть работает против цели.
- Окно серии. 2 секунды при интервале между мишенями в среднем 0,6 с.
- Рекорды. Отдельная таблица на 10 мест для каждого устройства, после игры — «до следующего места не хватило N очков».
- Проверка. Запустите
averageScoreс вашими правилами и точностями 0,8 и 0,97 и убедитесь, что разрыв не больше 2,5 раза — это наша цель для семейной игры, а не общее правило.
Когда вам это не подойдёт
Множитель серии подходит для аркад с частыми однотипными действиями, но не для всех игр:
- Головоломки и игры без спешки. Там награда — решённая задача, а очки за скорость портят удовольствие от размышления; лучше звёзды за число ходов.
- Игры с сюжетом. Счёт отвлекает от истории, а таблица рекордов не нужна, если игру проходят один раз.
- Соревнование в сети. Локальная таблица в
localStorageлегко правится вручную; для общей таблицы нужен сервер, который проверяет результат, а не просто принимает число. - Совсем маленькие дети. Для них лучше работают накопительные награды без потерь: серия, которая сгорает, может расстроить сильнее, чем мотивировать.
Мы разрабатываем J2DRU — браузерный 2D игровой движок, где хранение в браузере входит в проверенные внутри разработки основы. Движок в разработке и публично пока не открыт, заявку можно оставить на странице раннего доступа (подача заявки не гарантирует приглашение).
Источники: внешних фактов в статье нет. Таблица очков, шаг и потолок множителя, окно серии и все средние значения — наш пример и наш расчёт в Node.js 24 на 500 прогонах с фиксированным зерном.
Читайте также: