Предыстория
Несколько лет назад, просматривая темы Лолза, наткнулся на пост, где парень сделал гонки за несколько часов. Никогда не доводилось реализовывать подобное, поэтому решил создать "ремастер" его проекта. Поскольку, автор оригинальной игры использовал C# Windows Forms, которые более-менее неплохо знал на тот момент, так же решил их использовать.
Давайте поэтапно расскажу, что у меня в итоге вышло за несколько вечеров уже далёкого 2020 года.
Ссылка на мою оригинальную тему https://lolz.guru/threads/2065540/
Сурсы и билд
Основные асеты взял из оригинальной игры. Всё остальное: музыку, пропы, меню нашел в интернете, либо сделал сам.
Код писал полностью самостоятельно. Копипаст с форумов нет. Про нейросети вообще молчу, тогда никаких GPT не существовало. Код морально устарел и имеет много проблем, как с неймингом, так и с архитектурой, но решил оставить всё как есть, чтобы сохранить вайб.
Ссылка на гитхаб
Архив с билдом
Ссылка на VirusTotal
Баги и тесты
Тестировал на Win 10, Win 11, WinServer 2012, проблем с производительностью или багами окружения не наблюдалось, кроме бесконечной загрузки, которая иногда возникает при первом запуске и лечится перезапуском приложения.
DevLog
Не буду расписывать каждую строчку стенами кода, лучше расскажу в общих чертах, что тогда сделал и какие были интересные моменты.
Структура проекта
Основным классом проекта является Game он же компонент UserControl.
В нем описана общая логика поведения игры. Есть таймер на 16 тиков (16 миллисекунд таймера, дают 60 фпс). Дальше логику отдельных элементов реализуют контроллеры и менеджеры.
Вторым основным классом является сама форма, в которой прописаны первичные инициализации и управление меню пункты которого вызывают юзер контролы.
Смотря на проект спустя 5 лет, понимаю, что многое можно сделать эффективнее. В проекте почти не используется наследование. Можно было применить, как минимум, "шаблонный метод" и "стратегию". Более грамотно реализовать стейт машины и работу с ИИ противника. Сделать нормальный менеджер для юзер контролов. Использовать общепринятую нотацию для нейминга и оформления кода. Хранить все настройки объектов в конфигах. Сжать музыку. Оптимизировать алгоритмическую сложность и сделать много чего ещё. Например, в проекте не используется дельта тайм из-за чего игра имеет фиксированный фпс в качестве костыля. Визуальных багов нет, только из-за многослойности алгоритмов отрисовки Windows Forms. Так же, для ренедра графики используется CPU, а не GPU. Поскольку игра простая в просчётах, это не критично, но в теории при развитии и масштабировании вызывло много проблем.
Отрисовка и анимации
Столкнулся с проблемой анимирования спрайтов при отрисовке. Фреймы нужно отрисовывать непосредственно в общем апдейте (тике таймера) из-за особенностей компонента Graphics в WinForms. В качестве хоть какого-то архитектурного решения, тогда создал 2 простых класса: объект анимированного спрайта (AnimationSprite) и их менеджер (AnimationManager). Сделал поддержку z-index для спрайтов. По итогу, функуия репэйнта сократилась до одного цикла с linq запросом на сортировку.
Так же, решил немного поработать с пост обработкой. В фотошопе нарисовал 2 пост эффекта. Вроде пустяк, но разница чувствуется. Создавать анимации было мутороно, т.к. пришлось их делать покадрово отдельными изображениями. Очередная, особенность работы с компонентом Graphics.
Поскольку не использовал графические библиотеки, то ни о каких шейдерах речи идти не могло.
Искусственный интеллект
Тогда впервые довелось делать ИИ с нуля, еще и для гонок.
Ранее, делал ИИ только для пошаговых игр или использовал готовые решения.
Логика противника примерно следующая:
* Если игрок сзади, то всегда перекрывать проезд с собой и быть на одной линии.
* Если игрок впереди, то включать буст для того, чтобы догнать и пытаться быть не на одной линии.
* Если противник обгоняет, то уступать ему первенство через раз.
Вроде ничего сложного, но алгоритмы поведения, у меня получились кривые. В своё оправдание могу сказать, что кривость придаёт больше фана и непредсказуймости, поэтому не баг, а фича.) За ИИ отвечает класс EnemyAi.
Сейчас код выглядит ужасно. Огромное количество вложенных if else, много магических чисел (что тоже частая проблема проекта). Сейчас бы код класса, разбил на функции, ввёл константы, подумал в сторону паттерна стратегии, а так же в сторону разбиения функционала ИИ на команды.
Работа с музыкой
Первый раз, делал игру на WinForm с полноценными звуками, поэтому столкнулся с множеством подводных камней. Из-за текущей реализации подгрузки звуков, как раз может случиться баг с бесконечной загрузкой.
Со звуками в качестве файлов работать не сложно, основная сложность была в их параллельном проигрывании.
В WinForm нет корутин, а обертывать WindowsMediaPlayer в отдельный поток учитывая, что он сам по себе работает в отдельном потоке было бы странным решением. Поэтому в проекте создал 2 полностью одинаковых класса MusicManager и VoiceManager, которые обеспечиваают многоканальность звука.
Звуки машин пришлось подгружать в оперативную память сразу при запуске игры, что привело к созданию первоначального окна загрузки. Иначе они слишком долго подгружались с диска и приводили к стартерам.
Тогда, моя лень не позволила сделать нормальное управление звуком в меню игры, поэтому громкость и отключение звука придётся регулировать в микшере громкости. :З
Как оказалось, в C# есть много библиотек для решения проблемы менеджмента звуков, например NAudio. Так же, создавать 2 идентичных класса - плохая идея. Даже, используя однопоточный WindowsMediaPlayer, можно было сделать, что-то вроде менеджера потоков, тем-более, в C# для этого был готовый функционал.
Прочее
Систему коллизий, менеджеры объектов, стэйт машины и т.д. мне на тот момент, доводилось реализовывать ни один раз. Как на WinForms, так и на других стеках, игровых движках. Однако, в текущей реализации, нашлось пара забавных моментов.
Например, в игре точкой отсчета является авто игрока. Поэтому выходит игрок стоит всё время на месте, а все элементы игры движутся относительно него, путем вычитания и сложения скоростей. Да, да, машина игрока прямо, как корабль из Футурамы, который стоял на месте и двигал вселенную.
Сделал парочку пропов. Например следы от торможения. Работает неплохо, деталей визуалу добавляет.
Зачем всё это?
Зачем делать игры на WinForm, когда можно их создать при помощи готовых движков?
Делать игру на движке, где многое работает из коробки не сложно, а создавать ёё на непредназначенных для этого технологиях, на самом деле крутой опыт. Позволяет лучше понять, как устроены игры под капотом. Подобные проекты что-то вроде челленджа, который делается ради фана.
Вывод
По итогу, получился самобытный и немного странный проект, который, 5 лет назад, помог мне лучше понять устройство игр под капотом и получить опыт работы с отрисовкой графики в Windows Forms, что в будущем пригодилось при создании своих кастомных компонентов. Если говорить современным сленгом, то занимался vibe кодингом, когда это ещё не было мейнстримом. 😎