---
title: "Гейм-дизайн: механики и баланс"
description: "Сложность, награды, фидбек: почему «скучно» — вопрос гейм-дизайна. Механика и контент, петля ядра, кривая сложности и баланс игры: числа-слайдеры в Config."
course: "Ebitengine: 2D-игры на Go"
url: "https://golangmaster.ru/courses/ebitengine/lessons/24-igrovye-mehaniki/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Гейм-дизайн: механики и баланс

*Курс «Ebitengine: 2D-игры на Go», урок 24 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/ebitengine/lessons/24-igrovye-mehaniki/*

Ассеты загружены, FPS ровный, сцены переключаются — а через две минуты в игру не хочется возвращаться: виноват не движок, а несобранный баланс игры. В этом уроке — гейм дизайн для программиста: механика против контента, петля ядра, кривая сложности, числа как слайдеры и фидбек, из-за которого «скучно» почти всегда означает «не чувствуется», а не «мало уровней».

Гейм дизайн звучит как профессия про идеи, но по сути это инженерия правил: игра — набор правил, по которым данные превращаются в опыт. Хорошая новость для программиста: половина этой работы уже у вас в руках — вы двигаете числа, строите циклы и обязаны понимать, почему игрок возвращается. В этом уроке игра раскладывается на механики и контент, собирается петля ядра, растягивается кривая сложности, а настройка превращается в процедуру: менять по одному числу и играть после каждого изменения. Всё это делается кодом, который вы уже умеете писать, — структурой Config и парой функций. Художественный дар не требуется: требуется дисциплина измерений и честная запись впечатлений после каждой партии. Отдельный договор урока: слово «скучно» — не оскорбление, а диагностический симптом, и у него есть короткий список виноватых систем; мы научимся по жалобе находить виновника — петлю, кривую или фидбек — и чинить изменением одного числа, а не переписыванием проекта.

### Механика и контент: правило против данных

Механика — это правило: двойной прыжок, урон от касания врага, монета, прибавляющая единицу к счёту. Контент — данные, которые живут по правилам: уровни, типы врагов, спрайты, звуки. Двойной прыжок — одна механика и несколько строк кода; площадки для прыжков — сотни единиц контента и ни одной новой строки. Механика добавляет поведение, контент наполняет его вариациями, и путаница между ними — самая дорогая ошибка новичка в гейм-дизайне: новое правило стоит дороже нового уровня в разы.

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

**Вопрос:** Двойной прыжок героя — это что?

- Контент: ещё одна способность в списке
- ✅ Механика: правило движения, требующее кода
- Ассет: второй кадр анимации прыжка
- Данные уровня: параметр конкретной сцены

Двойной прыжок меняет правило перемещения: геймплей не меняется ни добавлением картинки, ни добавлением уровня. Способность требует кода в Update и обычно счётчика прыжков — это поведение, а не данные; ассет лишь показывает его игроку.

### Петля ядра: действие, награда, новая цель

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

Проверка петли примитивна: закройте свою игру через три минуты и честно ответьте, что тянуло вернуться. Если ответа нет — петля разомкнута: действие не даёт награды, награда не создаёт цели или цели стоят слишком далеко друг от друга. В коде петля живёт в вашем Update: движение — действие, [счёт из урока про состояние](/courses/ebitengine/lessons/16-igrovoe-sostoyanie/) — награда, следующая волна — новая цель. Если счёт растёт, но ни на что не влияет, звено разорвано: свяжите его с целью — каждые десять монет открывают волну посложнее. Обратите внимание на масштаб: петля измеряется секундами, а не миссиями. Если ваше «действие — награда» занимает пять минут, это не петля, а эпизод; найдите внутри него короткий цикл — шаг, сбор, щелчок счёта — и стройте удержание вокруг него.

### Числа игры в одной структуре

```go
type Config struct {
        EnemySpeed    float64
        EnemyHP       int
        EnemyDamage   int
        SpawnInterval int // тиков между волнами
        PlayerHP      int
}

var DefaultConfig = Config{
        EnemySpeed:    40,
        EnemyHP:       3,
        EnemyDamage:   1,
        SpawnInterval: 180, // три секунды при TPS 60
        PlayerHP:      5,
}

func spawnEnemy(cfg Config, x, y float64) Enemy {
        return Enemy{
                x:      x,
                y:      y,
                speed:  cfg.EnemySpeed,
                hp:     cfg.EnemyHP,
                damage: cfg.EnemyDamage,
        }
}
```

Все настраиваемые числа живут в одной структуре Config, а DefaultConfig фиксирует стартовую точку баланса: её можно вернуть одной командой git. spawnEnemy читает только поля конфигурации и не знает других чисел — враг рождается с параметрами текущей волны. Ни одной магической константы внутри логики: цифры переехали туда, где их видно и удобно двигать. Полями Config балансируется всё — от скорости пуль до частоты спавна: структура растёт вместе с игрой, но числа не разбредаются по функциям.

### Кривая сложности: постепенно и с передышками

Кривая сложности — график давления на игрока по времени, и первые минуты на нём обязаны быть лёгкими: лёгкая победа в начале — самый дешёвый способ удержать внимание. Дальше вводите элементы по одному: первый враг медленный, второй появляется, когда первый освоен, новый тип — тогда, когда предыдущий перестал пугать. Между волнами нужны паузы: несколько секунд тишины, за которые игрок переводит дух и замечает, что стало чуть труднее.

Сплошной рост утомляет, а резкий скачок убивает интерес: потолок скорости врага существует не из доброты, а потому что реакция человека конечна — враг, которого нельзя догнать взглядом, не создаёт вызова, а создаёт обиду. Практически кривая живёт в функциях от номера волны: скорость и HP растут умеренно, каждые несколько волн добавляется один элемент, между волнами — передышка. Пауза между волнами — тоже часть кривой: без неё игра превращается в сплошное давление, а с ней — в ритм напряжения и отдыха. Хотите проверить кривую — сыграйте чужую версию своей игры: чужой Config, подставленный на минуту, покажет разницу быстрее любых рассуждений о балансе.

### Кривая как функция от номера волны

```go
func configForWave(base Config, wave int) Config {
        cfg := base
        cfg.EnemySpeed += float64(2 * (wave - 1))
        if cfg.EnemySpeed > 80 {
                cfg.EnemySpeed = 80 // потолок: иначе не уворачиваться
        }
        if wave%4 == 0 {
                cfg.EnemyHP = base.EnemyHP + 1 // каждая четвёртая волна крепче
        }
        return cfg
}
```

Функция строит конфигурацию волны из базовой: копия, прибавка к скорости, потолок как защита от нечеловеческой реакции и бонус HP раз в четыре волны. База не мутирует — каждая волна считается от DefaultConfig, поэтому эксперименты не накапливаются. Именно так выглядит кривая сложности в коде: арифметика от номера волны вместо ручного редактирования десятков чисел. Края функции проверяются на бумаге за минуту: волна первая, волна кратная четырём, волна за потолком — кап обязан держать любое значение.

### Проблема, виноватая система и фикс

| Жалоба игрока | Какая система виновата | Фикс |
| --- | --- | --- |
| «Скучно на пятой минуте» | кривая сложности | волны с паузами, новый враг раз в пару минут |
| «Не понимаю, что убило» | фидбек | звук урона, вспышка, короткая тряска |
| «Монеты собираются в молчании» | фидбек | короткий звук и частицы на подбор |
| «Слишком легко с первых секунд» | баланс | стартовые HP и скорость врага ниже в Config |
| «К третьей волне невозможно» | баланс | потолок скорости, рост HP реже |
| «Не знаю, зачем собирать» | петля ядра | связать сбор с целью: счёт открывает новую волну |

### Какие параметры получит враг четвёртой волны?

```go
base := Config{EnemyHP: 3, EnemySpeed: 50}
cfg := configForWave(base, 4)

// configForWave: +2 к скорости за каждую волну после первой,
// потолок 80, каждая четвёртая волна: HP = base.EnemyHP + 1
```

**Что выведет программа?**

- ✅ Скорость 56, HP 4: рост сработал и бонус четвёртой волны тоже
- Скорость 56, HP 3: бонус волн применяется только к скорости
- Скорость 80, HP 4: потолок достигнут уже на четвёртой волне
- Скорость 50, HP 3: функция вернула копию без изменений

Прибавка скорости равна 2*(4-1)=6: получилось 56, потолок 80 не задет и кап не срабатывает. Волна кратна четырём, поэтому HP вырастает на единицу. Функция возвращает изменённую копию base — исходный Config остаётся нетронутым, и следующая волна снова считается от базы.

**Вопрос:** Игрок говорит: «Скучно». Что проверять в первую очередь?

- Количество контента: нужен ещё десяток уровней
- ✅ Фидбек и кривую сложности: слышны ли события и растёт ли давление
- Движок: пора переходить на более мощный
- Число ассетов: нужны новые спрайты и звуки

Скука — симптом разомкнутой петли: события не ощущаются (нет фидбека) или давление не меняется (нет кривой сложности). Контент поверх сломанной петли не помогает: новые уровни играются так же пусто, только дольше.

> **Живое место: урон, которого никто не заметил.** Частая картина: коллизия работает, HP уменьшается, в логе всё честно — а игрок продолжает бежать сквозь врагов, потому что за миг урона экран не дрогнул и звук не прозвучал. Что сломалось бы дальше: о смерти он узнаёт только по экрану поражения и решает, что умер внезапно и несправедливо, — а дальше игра закрывается. Минимальный фикс — один звук и вспышка на событии урона: два вызова, а читаемость боя меняется целиком. Обратная сторона той же ошибки — фидбек без события: вспышки и звуки на пустых триггерах учат игрока игнорировать эффекты, и настоящий урон теряется в шуме; каналы подтверждения подключаются только к реальным событиям логики.

### Баланс: числа — это слайдеры

Баланс игры — не математика равновесия, а дисциплина слайдеров: скорость врага, HP, урон, интервал спавна — числа, которые вы двигаете и слушаете результат. Правила процедуры: менять по одному числу за раз; после каждого изменения играть хотя бы пару минут; записывать впечатление одной строкой; возвращаться к DefaultConfig, если стало хуже. Два числа, сдвинутых одновременно, обнуляют выводы — вы никогда не узнаете, что именно сработало.

Держите числа в одном месте — структуре Config: прыжок в чужую функцию за константой — способ забыть, где лежит баланс. Отдельно цените точку возврата: коммит с рабочим балансом — страховка от того, что завтрашний эксперимент похоронит вчерашнюю играбельность. И помните: играбельность важнее симметрии — враг не обязан быть честным, он обязан быть понятным и преодолимым; игрок прощает сложность, но не прощает несправедливость. Отдельно про масштаб изменений: десять-двадцать процентов — не суеверие, а диапазон, в котором разница ощущается, но не ломает игру; удвоение скорости проверяет рефлексы, которых у игрока ещё нет, и выводы из такой партии ничего не говорят о балансе.

### Порядок настройки баланса одним списком

- Все игровые числа живут в структуре Config, ни одной константы в логике
- DefaultConfig — точка возврата: рабочие значения всегда восстановимы
- Одно изменение за раз: два сдвинутых числа обнуляют выводы
- После каждого изменения — играть и записывать впечатление строкой
- Кривая сложности считается от номера волны, а не правится руками по ходу
- У скорости врага есть потолок: реакция игрока конечна
- Каждое событие петли подтверждается минимум одним каналом фидбека
- «Скучно» — сигнал про фидбек и кривую, а не про количество контента

### Соберите рождение врага из конфигурации

Восстановите два пропуска: поля Config, из которых враг берёт поведение.

```go
func spawnEnemy(cfg Config) Enemy {
        return Enemy{
                speed: cfg.___,
                hp:    cfg.___,
        }
}
```

Подсказка: Имена полей Config повторяют параметры врага: с какой скоростью ходит и сколько ударов терпит.

Правильные ответы в пропусках: `EnemySpeed`, `EnemyHP`

> **Про песочницу: слайдеры без окна.** Игрового окна в песочнице сайта нет, но Config и функция кривой — чистая математика над числами: тематическая программа печатает параметры врагов по волнам и следит за потолком скорости. Те же инварианты, которые вы проверяете, играя: изменение одного числа меняет поведение предсказуемо, без сюрпризов и без накопленных правок. Числа из примеров урока — стартовые, не эталонные: ваш проект назовет им другие значения, и это нормально, потому что баланс привязан к остальным числам игры.

### Процедура изменения одного числа

Соберите дисциплину баланса по шагам.

Правильный порядок строк:

1. `Выберите одно число в Config`
2. `Измените его на десять-двадцать процентов`
3. `Соберите и сыграйте две минуты`
4. `Запишите впечатление одной строкой`
5. `Откатите или оставьте изменение`
6. `Только после этого трогайте следующее число`

### Фидбек: игрок должен чувствовать событие

Фидбек превращает событие в ощущение: без подтверждения игрок не верит, что что-то произошло. Урон герою — короткий звук из [урока про звук](/courses/ebitengine/lessons/18-zvuk/), красная вспышка и тряска камеры; сбор монеты — звон и [частицы](/courses/ebitengine/lessons/17-chasticy/); смена волны — вспышка экрана и обновлённая надпись. У каждого события петли обязан быть минимум один канал подтверждения, лучше два: слух и зрение работают в паре.

Дозируйте: тряска на каждое касание превращается в шум, частицы в тысячу экземпляров за кадр уронят частоту кадров. Фидбек — не украшение, а часть механики: урон без звука читается как «пронесло». Отсюда главный диагноз курса: «скучно» почти никогда не значит «мало контента» — это нет фидбека или нет кривой сложности. Десятый враг того же вида не спасёт; короткий звук при уроне — иногда спасает игру целиком. Иерархия каналов помогает дозировать: звук — для частых событий, вспышка — для важных, тряска — для редких и тяжёлых; чем чаще событие, тем короче и тише его подтверждение, иначе частое превращается в фоновый шум и перестаёт считываться.

После урока у игры появляется то, что не видно на скриншотах: замкнутая петля, растянутая кривая и конфигурация, которую можно крутить без страха. Это и есть гейм дизайн основы, доступные программисту без отдельной профессии: механики малочисленны, контент дёшев, числа меняются по одному, события подтверждаются фидбеком. Дальше остался [итоговый проект](/courses/ebitengine/lessons/25-itogovyj-proekt/), где петля, Config и кривая соберутся в одну мини-аркаду. Домашнее задание — первый настоящий сеанс балансировки: он займёт полчаса и расскажет о вашей игре больше, чем неделя добавления новых уровней. Помните контрольный вопрос урока: что тянуло вернуться? Ответ на него — план следующей итерации. И ещё одно умение, которое проверится в проекте: отличать «мне не нравится» от «игроку не понятно». Первое — вопрос вкуса и правится откатом числа; второе — вопрос ясности и правится фидбеком или кривой. Различать их — половина гейм-дизайна.

### Термины урока

- **Механика** — Правило поведения: двойной прыжок, урон от касания, подбор монеты. Требует кода; меняется дорого.
- **Контент** — Данные, живущие по правилам: уровни, типы врагов, спрайты. Меняется дёшево; механик должно быть мало, контента — много.
- **Петля ядра** — Действие, награда, новая цель — то, что игрок повторяет каждые секунды. Разрыв звена ощущается как скука.
- **Кривая сложности** — График давления по времени: лёгкая победа в начале, по одному новому элементу, паузы между волнами, потолок роста.
- **Слайдер баланса** — Игровое число в Config: скорость, HP, урон. Меняется по одному, после каждого изменения — игра и запись впечатления.
- **Фидбек** — Подтверждение события: звук, вспышка, частицы, тряска. Без него событие для игрока не произошло.

## Коротко: что запомнить

- Механика — правило поведения, контент — данные по правилам; механик мало, контента много.
- Петля ядра: действие, награда, новая цель; разрыв звена ощущается как скука при любом объёме контента.
- Кривая сложности начинается с лёгкой победы и вводит по одному элементу с передышками между волнами.
- Баланс — дисциплина слайдеров: одно число за раз, игра после каждого изменения, запись впечатлений.
- Config собирает числа в одном месте, DefaultConfig — точка возврата в играбельное состояние.
- Кривая считается функцией от номера волны: прибавки, потолок скорости и бонусы не накапливаются.
- «Скучно» почти всегда означает отсутствие фидбека или кривой сложности, а не мало контента.

## Домашнее задание

Проведите первый сеанс балансировки над своим проектом. Вынесите скорость, HP и урон врагов в Config с DefaultConfig и убедитесь, что spawnEnemy не видит других чисел. Напишите configForWave с потолком скорости и бонусом раз в четыре волны; между волнами добавьте паузу в пару секунд. Прогоните процедуру дисциплины: пять изменений по одному числу, после каждого — две минуты игры и строка записи в файл заметок. Проверьте фидбек всех событий петли: урон, сбор, новая волна; где нет ни звука, ни частиц, ни вспышки — добавьте хотя бы один канал из уроков про звук и эффекты. Закройте игру через три минуты и честно ответьте, что тянуло вернуться; ответ запишите — это готовый список задач. Затем сравните первую и пятую версию баланса: разница впечатлений и есть материал итогового проекта. Если после пятого изменения игра не стала интереснее, верните DefaultConfig целиком и проверяйте петлю, а не числа. Правки держите в отдельных коммитах: откат эксперимента должен стоить одной команды, а не раскопок.

## Вопросы и ответы по уроку

### Гейм-дизайн — это обязательно долго играть в собственную игру?

Да, и это самое дешёвое исследование на свете: две минуты игры после каждого изменения дают больше, чем час теоретизирования. Записи впечатлений переводят ощущения в наблюдения и делают баланс повторяемым процессом, а не везением. Больше всего пользы приносят короткие сессии сразу после изменения: память об ощущении свежее любых заметок.

### Куда девать сложность для опытных игроков?

Сначала кривая должна работать для новичка: лёгкая победа в начале — не оскорбление, а вход. Режимы сложности — те же слайдеры: три пресета Config с разной скоростью и HP, без единой новой механики. Названия пресетов говорите честно: «лёгкий» обязан быть легче обычного, иначе шкала теряет смысл.

### Как понять, что контента достаточно и пора полировать?

Когда петля замкнута и кривая держит: если с тремя видами врагов играют дольше, чем с десятью у соседа, контент хватает. Признак недобора — не скука, а «нечего открывать»: награда перестала создавать новую цель. Полировка в этот момент — звук, вспышки, частицы: она усиливает уже работающее, а не заменяет неработающее.
