← Puzzle Breakers

ТЕХНИЧЕСКИЙ КЕЙС · Производительность и надёжность

Стабильность и производительность боя

Как диагностируемые ограничения и архитектурные изменения снижали риск ошибок в боевой системе.

UnityC#ПрофилированиеТестированиеДетерминизмLiveOps

Проблема

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

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

Моя ответственность

Я занимался профилированием и оптимизацией, исправлением ошибок и модульным тестированием, а также проектировал изменения в боевой архитектуре, которые повышали наблюдаемость и надёжность системы.

Реализация

Контроль детерминированного RNG

В бою использовался детерминированный генератор случайных чисел с серверным seed. Обращение к нему вне разрешённой фазы могло незаметно рассинхронизировать результат между клиентом и сервером.

Я спроектировал и внедрил runtime-guard, который контролировал допустимые фазы обращения к генератору и фиксировал нарушения. Guard обрамлял легитимные детерминированные фазы боя; нарушение не останавливало матч, но становилось аналитическим событием с контекстом для последующей диагностики.

Ввод игрока или старт боя
            │
            ▼
  разрешённая фаза Random
            │
            ▼
  вызов Random вне фазы ──► аналитическое событие + дебаг-контекст

Диспетчеризация боевых событий через Visitor

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

Вместо ручной диспетчеризации по типу я ввёл интерфейс Visitor с явным методом для каждого типа боевого события и перевёл на него существующие менеджеры визуализации. Это примерный псевдокод, показывающий принцип без внутреннего API проекта:

// До: новая ветка могла не появиться в одном из менеджеров
if (battleEvent is DamageEvent damage) ShowDamage(damage);
else if (battleEvent is EffectAppliedEvent effect) ShowEffect(effect);
// После: контракт перечисляет поддерживаемые события явно
interface IBattleEventVisitor
{
    void Visit(DamageEvent battleEvent);
    void Visit(EffectAppliedEvent battleEvent);
    // … остальные типы событий
}

// Каждому менеджеру необходимо реализовать весь контракт.
До:     новое событие → ручные проверки в 5 менеджерах → пропуск → runtime-ошибка
После:  новое событие → Visitor-контракт → компилятор требует обработку во всех менеджерах

Это изменение сделало добавление нового типа события проверяемой задачей на этапе сборки, а не источником скрытой ошибки в релизе.

Сокращение объёма данных при сохранении input

Профилирование показало, что заметная часть времени и выделений памяти в горячем сценарии приходилась на сохранение input: данные сериализовались и проходили через сжатие, чтобы их можно было использовать далее в боевом процессе.

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

Unity Profiler: цепочка ProcessInput → BattleStorage.Save до оптимизации

Профилирование цепочки обработки input: сохранение данных было одним из основных потребителей времени и выделений памяти в рассматриваемом сценарии.

Результат

Тихий и трудно воспроизводимый класс десинхронизаций стал наблюдаемым: вместо расследования постфактум команда получала конкретное аналитическое событие с дебаг-контекстом. А обработка примерно 111 типов боевых событий в пяти подсистемах стала контролироваться компилятором, что исключило класс ошибок «забыли обработать новое событие».

Сокращение объёма сжимаемых данных сделало обработку input легче для CPU и памяти в профилируемом сценарии. Вместе с архитектурными ограничениями, профилированием, оптимизацией и тестированием это снижало стоимость поддержки развивающейся боевой системы и помогало стабильнее выпускать новый контент.