Сложность и кривая обучения
Хорошая игра ведёт тебя за руку: чуть-чуть тяжелее на каждом шаге — ровно настолько, чтобы было азартно, но не хотелось закрыть вкладку.
Кривая сложности — это график того, насколько игра трудна в каждый момент времени. Твоя задача как геймдизайнера — рисовать эту кривую так, чтобы игрок всё время был в потоке: между скукой и злостью.
В прошлом уроке про кор-луп мы разобрались, ради какого повторяющегося действия в игру хочется возвращаться. Но даже самый сочный кор-луп можно убить одной ошибкой — выкрутить сложность не туда. Сегодня учимся настраивать вызов так, чтобы наш цыплёнок честно вёл игрока от первых неуклюжих прыжков до момента, когда тот чувствует себя мастером.
Зачем вообще думать о сложности
Представь: ты скачал новую мобильную игру. Первый уровень — три врага ползут на тебя со скоростью улитки, ты их сносишь и зеваешь. Удаляешь. Или наоборот: на первом же экране в тебя летит десяток снарядов, ты умираешь четыре раза за минуту, ничего не понимаешь — и тоже удаляешь. В обоих случаях разработчик потерял тебя не из-за плохой графики, а из-за неправильной сложности.
Сложность — это не «много врагов» и не «мало здоровья». Это ощущение вызова, которое игрок испытывает прямо сейчас. И это ощущение можно настраивать так же точно, как громкость в наушниках. К концу урока у тебя будет понятный набор рычагов и план: как через нашего цыплёнка провести игрока от лёгкого старта к настоящему испытанию — и ни разу его не потерять.
Поток: коридор между скукой и фрустрацией
Психолог Михай Чиксентмихайи описал состояние, которое он назвал потоком (flow) — это когда ты так увлечён, что забываешь про время. Ты наверняка его ловил: засел в игру «на пять минут», а поднял глаза — уже стемнело.
Поток возникает в узком коридоре. Представь две стены. Левая — скука: задача слишком простая для твоих навыков, мозг засыпает. Правая — фрустрация (злость и бессилие): задача слишком сложная, ты бьёшься головой и проигрываешь. Между ними — тоннель потока. Игра должна вести игрока ровно по этому тоннелю.
Фокус в том, что навык игрока растёт. То, что было сложным на первом уровне, к десятому станет скучным. Значит, сложность тоже должна расти — иначе игрок упрётся в левую стену скуки.
Как это выглядит на графике
Если по горизонтали отложить время игры, а по вертикали — сложность, то идеальная кривая обучения похожа на лестницу, которая ползёт вверх. Каждая ступенька — новый вызов. После каждого подъёма — маленькая площадка, где игрок осваивается и чувствует себя крутым. А потом — снова чуть выше.
| Что чувствует игрок | Где он на кривой | Что делать дизайнеру |
| «Скучно, я уже всё умею» | Слишком низко, левая стена | Поднять вызов: новый враг, меньше времени |
| «Азартно, но справляюсь» | В тоннеле потока — идеально | Ничего не трогать, держать ритм |
| «Бесит, я постоянно умираю» | Слишком высоко, правая стена | Снизить вызов или подучить механике |
Из чего складывается сложность: рычаги
Хорошая новость: сложность не приходится регулировать «на глаз». Это набор конкретных чисел в коде, которые ты крутишь как ручки на пульте. Вот главные рычаги для нашего платформера про цыплёнка.
- Скорость — как быстро летят враги и снаряды. Чем быстрее, тем меньше времени на реакцию.
- Количество — сколько врагов или препятствий одновременно на экране.
- Точность — насколько узкие платформы и проёмы, в которые надо попасть прыжком.
- Ресурсы — сколько у игрока жизней, времени, патронов. Меньше ресурсов = больнее ошибаться.
- Новизна — незнакомые механики, которые надо понять прямо в бою.
Пример 1: уровень сложности как набор чисел
Главная идея — не разбрасывать «магические числа» по всему коду, а собрать настройки сложности в один объект. Тогда поднять вызов на следующем уровне — это поменять пару цифр, а не переписывать игру.
// Профили сложности для цыплёнка-платформера.
// Один объект — один уровень вызова.
const LEVELS = {
easy: { enemySpeed: 1.5, enemyCount: 2, gapWidth: 120, lives: 5 },
normal: { enemySpeed: 2.5, enemyCount: 4, gapWidth: 90, lives: 3 },
hard: { enemySpeed: 4.0, enemyCount: 7, gapWidth: 60, lives: 1 },
};
let difficulty = LEVELS.easy; // стартуем с лёгкого
function spawnEnemies() {
for (let i = 0; i < difficulty.enemyCount; i++) {
enemies.push({
x: canvas.width + i * 80,
y: groundY,
vx: -difficulty.enemySpeed, // враги ползут влево, на цыплёнка
sprite: chickenEnemySprite,
});
}
}Результат: на лёгком уровне навстречу цыплёнку медленно ползут два врага, проёмы между платформами широкие, и у игрока пять жизней — промахнуться не страшно. Стоит переключить difficulty на LEVELS.hard — и тех же врагов становится семь, летят они почти втрое быстрее, проёмы сужаются, а жизнь всего одна.
Разберём по шагам. Мы держим три профиля в LEVELS — каждый это просто мешочек чисел. Переменная difficulty указывает на текущий профиль. Функция spawnEnemies не знает никаких конкретных цифр: она спрашивает их у difficulty. Захотел сделать игру злее — поменял одну строчку, и весь спавн, скорость и количество подстроились сами.
Пример 2: плавный рост вместо ступенек
Иногда не хочется делить игру на «уровни» — хочется, чтобы вызов рос непрерывно, как в бесконечном раннере. Тогда сложность привязывают к прогрессу: чем дольше живёшь, тем горячее.
let score = 0;
let elapsed = 0; // секунд с начала забега
function update(dt) {
elapsed += dt;
// Скорость растёт плавно: каждые 10 секунд +0.5,
// но не выше потолка, чтобы не стало нечестно.
const speed = Math.min(2 + elapsed / 10 * 0.5, 8);
for (const enemy of enemies) {
enemy.x -= speed * dt; // двигаем с учётом дельта-времени
}
// Интервал спавна сжимается: враги появляются чаще.
spawnTimer -= dt;
if (spawnTimer <= 0) {
spawnEnemies();
spawnTimer = Math.max(2 - elapsed / 30, 0.6); // не чаще, чем раз в 0.6 с
}
}Результат: первые секунды забега цыплёнок бодро бежит, враги редкие и медленные — игрок осваивается. Через минуту враги несутся заметно быстрее и появляются чаще, экран наполняется. Но и скорость, и частота упираются в потолок (Math.min и Math.max), поэтому игра не превращается в нечестную мясорубку, где увернуться физически невозможно.
Ключевая деталь — потолки. Без Math.min скорость росла бы до бесконечности и через пять минут стала бы физически непроходимой. С потолком кривая выходит на плато: сложность высокая, но честная. Игрок проигрывает из-за своей ошибки, а не потому что игра жульничает.
Пример 3: невидимый помощник (резиновая лента)
Профи используют хитрый приём — динамическую подстройку сложности. Игра тихо следит, как идут дела у игрока, и незаметно помогает или поддаёт жару. Это как резиновая лента: отстал — игра чуть притормаживает, разогнался — натягивает поводок.
let deathsInARow = 0; // подряд проигрышей
function onChickenDeath() {
deathsInARow++;
// Игрок застрял? Тихо снижаем вызов, ничего ему не говорим.
if (deathsInARow >= 3) {
difficulty.enemySpeed *= 0.9; // враги чуть медленнее
difficulty.gapWidth += 10; // проёмы чуть шире
}
}
function onLevelClear() {
deathsInARow = 0; // справился — сбрасываем счётчик
}Результат: если игрок умирает третий раз подряд на одном месте, враги почти незаметно замедляются, а пропасти между платформами чуть расширяются — и на следующей попытке цыплёнок проскакивает сложный участок. Игрок не видит цифр и думает, что наконец-то приноровился сам. Стоит ему пройти уровень — счётчик обнуляется, и игра возвращается к обычному ритму.
Тут важна незаметность. Если на экране всплывёт «Мы сделали полегче, не расстраивайся» — игрок обидится. А тихая подстройка просто удерживает его в потоке. Это честный приём: ты не обманываешь игрока, ты подгоняешь кривую под его реальный навык.
Кстати, резиновая лента работает и в другую сторону. Если игрок проходит уровни без единой смерти, можно тихо поддать жару: чуть ускорить врагов, добавить ещё одного. Так ты ловишь и тех, кому слишком тяжело, и тех, кому слишком легко, не заставляя их лезть в меню настроек. Гонки вроде Mario Kart этим знамениты: отстающим подкидывают мощные бонусы, а лидеру — слабенькие, чтобы гонка до финиша оставалась напряжённой для всех. Это и есть динамическая подстройка в чистом виде, просто завёрнутая в красивую обёртку.
Постепенный ввод механик
Сложность — это не только числа. Это ещё и то, сколько всего игрок должен держать в голове. Главная ошибка новичков-дизайнеров — вывалить все механики сразу. Хорошие игры вводят их по одной, как буквы в букваре.
Вспомни любой платформер, в который залипал. Первый экран: только прыжок, прыгай через ямку. Второй: появился враг — теперь прыгай ещё и на него. Третий: монетки за рискованный маршрут. И только потом — комбинация всего сразу. Каждая новая механика сначала подаётся в безопасной песочнице, где ошибиться почти невозможно, и только освоенная — добавляется в общий замес.
Правило одной новой вещи: на каждом новом отрезке вводи максимум одну незнакомую механику. Дай игроку потрогать её там, где цена ошибки нулевая, прежде чем требовать применять под давлением.
Как это выглядит в коде уровней
Удобно описывать уровни данными: какие механики на нём «включены». Тогда видно с первого взгляда, что нового приносит каждый уровень.
const STAGES = [
{ name: 'Учимся прыгать', features: ['jump'] },
{ name: 'Появились ямы', features: ['jump', 'gaps'] },
{ name: 'Первый враг', features: ['jump', 'gaps', 'enemy'] },
{ name: 'Монетки и риск', features: ['jump', 'gaps', 'enemy', 'coins'] },
];
function has(feature) {
return currentStage.features.includes(feature);
}
function update(dt) {
if (has('enemy')) updateEnemies(dt); // враги работают только там, где введены
if (has('coins')) updateCoins(dt);
}Результат: на первом этапе цыплёнок просто учится прыгать на ровной площадке — ни ям, ни врагов, можно спокойно почувствовать высоту прыжка. На втором добавляются пропасти, на третьем — первый враг, и только на четвёртом игрок жонглирует прыжками, врагами и сбором монеток одновременно. Каждый этап честно добавляет ровно одну новую вещь.
Заметь: функция update проверяет has('enemy') перед тем, как двигать врагов. Это значит, что один и тот же код уровня обслуживает все этапы — мы просто включаем механики флажком в данных, а не плодим четыре разных файла. Удобно для нас и предсказуемо для игрока.
Честность для разных игроков
Твою игру откроет и младший брат, который впервые держит клавиатуру, и хардкорщик, который проходит платформеры с закрытыми глазами. Невозможно угодить обоим одной кривой. Поэтому хорошие игры дают выбор.
- Уровни сложности на старте (easy/normal/hard) — самый простой способ, мы его уже закодили в примере 1.
- Необязательные испытания — основной путь проходим для всех, а спрятанные секретные комнаты с монетками — для тех, кто хочет хардкора.
- Доступность — опции вроде «больше времени на реакцию» или «нельзя умереть на обучении». Это не «читы», это уважение к игроку.
Главный принцип честности: игрок должен проигрывать из-за своей ошибки, которую он понимает, а не из-за того, что игра подсунула невидимый шип или потребовала реакцию быстрее человеческой. Проигрыш в честной игре злит на себя («сейчас пройду!»), а в нечестной — на разработчика («да сколько можно!»). Разница огромная: первый игрок жмёт «Заново» и пробует снова, второй закрывает вкладку навсегда.
Есть простой тест на честность: посмотри запись своей смерти в игре и спроси себя — «Мог ли игрок предотвратить это, если бы играл лучше?». Если да — сложность честная. Если нет, если игра убила его без предупреждения и шанса на реакцию, — это твоя вина как дизайнера, а не его как игрока. Дай заранее сигнал: мигающий значок, звук, тень приближающегося врага. Игрок должен видеть угрозу и иметь хотя бы долю секунды, чтобы среагировать.
Частые ошибки и подводные камни
На этих граблях прыгает почти каждый, кто впервые настраивает сложность. Сверься со списком, прежде чем давать игру друзьям.
- Стена сложности вместо лестницы. Первые уровни легко проходятся, а потом резкий скачок — и игрок упирается в непробиваемую стену. Кривая должна расти плавно, без обрывов. Если многие бросают на одном и том же месте — там у тебя стена.
- «Мне же легко» — синдром разработчика. Ты прошёл свой уровень сто раз и знаешь, где будет враг. Для тебя игра лёгкая, для новичка — нет. Никогда не балансируй сложность по своим ощущениям: давай играть тем, кто видит игру впервые, и смотри, где они спотыкаются.
- Сложность без обучения. Ты вводишь механику двойного прыжка и сразу требуешь её в опасном месте. Игрок не успел понять, как она работает, и злится. Сначала песочница, потом испытание.
- Нечестная сложность. Враг появляется внезапно за краем экрана, увернуться нельзя. Игрок умирает не по своей вине. Такое бесит сильнее всего — давай игроку информацию заранее (предупреждающий значок, звук), чтобы у него был шанс среагировать.
- Сложность ради сложности. «Сделаю-ка я мегахардкор!» — и в игру никто не играет дольше минуты. Сложность — это средство держать игрока в потоке, а не самоцель. Высокий вызов хорош только тогда, когда он по-прежнему проходим.
Мини-практика: спроектируй кривую
Возьми наш платформер про цыплёнка и спланируй для него первые четыре отрезка. Сделай так, чтобы каждый добавлял ровно одну новую вещь и чтобы вызов плавно рос.
- Распиши массив
STAGESиз примера на четыре этапа. Для каждого укажиname, списокfeaturesи числа сложности (скорость врага, ширину проёмов). - Сделай так, чтобы скорость врага росла от этапа к этапу, но не больше чем на 30% за шаг — это и есть плавность.
- Добавь динамическую подстройку из примера 3: после трёх смертей подряд чуть снижай вызов на текущем этапе.
- Самое важное: дай поиграть кому-то, кто не видел твою игру. Запиши, на каком этапе он спотыкается. Если бросает на одном месте — там у тебя стена, сглаживай кривую.
Бонус: добавь на старте экран выбора easy / normal / hard и переключай объект difficulty. Так одной игрой ты накроешь и младшего брата, и хардкорщика.
Итоги
Сегодня ты научился думать о сложности не как о «много врагов», а как об инструменте, который держит игрока в потоке — узком коридоре между скукой и злостью. Главное, что стоит унести:
- Кривая сложности должна плавно расти вслед за навыком игрока — лестницей, а не стеной.
- Сложность — это конкретные числа в одном объекте (скорость, количество, ресурсы), которые удобно крутить как ручки на пульте.
- Новые механики вводи по одной и сначала в безопасной песочнице.
- Давай игрокам выбор и держи сложность честной: проигрыш должен быть виной игрока, а не подставой игры.
В следующем уроке мы займёмся обратной связью — звуками, тряской экрана и частицами, которые превращают сухое «попал по врагу» в сочное «БАМ!». Ведь даже идеально настроенная кривая сложности скучна, если удар по врагу ощущается как нажатие на выключенную кнопку. Погнали делать игру вкусной на ощупь.