1. Введение

Данной статьей я открываю серию дневников разработки уже третьей реинкарнации моей будущей игры KARC. Первая версия дошла до минимально играбельного вида и выложена в открытый доступ. Однако, мне не понравилось, как я спроектировал структуру приложения. Поэтому, ошибочно полагая, что я понял, как правильно спроектировать архитектуру приложения, я сделал с нуля вторую версию. В ней мне удалось прогрузить сцену с машинкой игрока, которая реагирует на клавиатуру. Однако, развивать эту версию дальше я не посчитал целесообразным, потому что, увлекшись, накрутил слишком много всего и, самое главное, неправильно. В результате в коде стало очень трудно ориентироваться. Так бы и остался проект в заморозке на неопределенный срок, как и моя мечта делать игры, если бы я в очередной раз не решил попытаться понять архитектурные паттерны, в частности, MVP, и у меня это, внезапно не получилось бы. И так мне MVP понравился, что я понял, как хочу переписать архитектуру KARC. В этот раз я решил фиксировать все свои шаги на бумаге, а потом выложить в широкий доступ. Дело в том, что я делаю игру на фреймворке Monogame, но в сети я не нашел внятного руководства, как сделать на нем что-нибудь законченное. Есть много роликов и статей о том, как сделать какие-то отдельные элементы (например, какой метод подгружает спрайт, но не как лучше это осуществить, если их у вас много и под каждый игровой объект их несколько) или мелкие игры, которые тянут в лучшем случае на технодемку, но не создать какую-либо законченную игру целиком (кое-что на самом деле нашел, и ссылки в последующих статьях будут, но в целом мне не понравилось). Поэтому, возможно, описание моего пути кому-нибудь пригодится.

2. Материалы и методы

Игра KARC будет представлять из себя простейшую аркаду с видом сверху, где машинка игрока двигается по прямой дороге, уворачиваясь от других машинок. Игра будет разрабатываться на языке C# с использованием фреймворка Monogame.

Почему Monogame? Я очень люблю язык C# и разрабатывать игру хочу на нем. Однако, сколько раз ни подступался к Unity, он неизменно вызывал у меня отторжение – Unity является полноценным универсальным движком, и моему мозгу всегда тяжело было охватить его устройство. А, если я не понимаю от начала и до конца, как что-то работает хотя бы в общих чертах, то мне крайне некомфортно этим пользоваться. Я копировал на Unity несколько проектов, которые находил в уроках в сети, но понимания не было, и я забрасывал. Когда-нибудь вернусь просто потому, что интересно, но не потому, что нужно - для воплощения моих скромных геймдевелоперских аппетитов Monogame пока хватает с лихвой.

Однако, я забегаю вперед. Так бы моя детская мечта разрабатывать свои собственные игрушки и осталась нереализованной, но как-то на Метаните я наткнулся на руководство по Monogame, впервые узнав о его существовании. Прочитав первые несколько уроков, я понял, что это именно то, что мне нужно. Дело в том, что Monogame не является игровым движком – это фреймворк типа pygame, написанный на языке C#. По сути, это просто набор библиотек, которые содержат классы и методы для подгрузки ресурсов, запуска простейшего игрового цикла и работы с графикой и геометрией. Как это все скомпоновать и пользоваться этим каждый решает сам. Хочешь, чтобы у тебя в игре была физика? Напиши ее, друг! Monogame позволит создать тебе экземпляры класса Rectangle, для которых есть статический метод, который отвечает на вопрос – пересекаются ли два указанных прямоугольника? Дальше – сам. Никто тебя не ограничивает! Для меня это стало настоящим спасением, потому что, спустившись на уровень ниже движка, я наконец-то понял, как работают игры. Отсутствие возможностей для меня стало плюсом, потому что, как следствие, отсутствовала перегруженность – если мне было что-то нужно, я писал это сам. В процессе работы над первой и второй версиями KARC я даже стал лучше понимать устройство Unity, потому что для реализации своих идей необходимо было создавать подобный ему функционал.

Позднее я даже понял, что мне не нужен фреймворк, чтобы сделать игру – для забавы я пару раз написал простенькие проекты типа модели «Хищник-жертва» с двухмерной графикой и обсчетом столкновений на связке C# + Windows Forms. И, подняв уровень своего навыка, решил завершить реализацию своей первой идеи, потому что дело надо доводить до конца. Так как вся игра будет написана на C#  , то данная серия статей рассчитана на людей, которые уже знают синтаксис и возможности этого языка, представляют, что такое классы, методы, свойства и события. Однако, автор не претендует на то, что пишет идеальный код, а предлагаемые им решения являются наиболее оптимальными, поэтому всегда будет рад конструктивной критике.

3. Теория

3.1 Подготовка рабочей среды

Как я уже сказал, разработка будет вестись на фрейморке Monogame. Проект я буду писать в Visual Studio 2019 (забегая вперед - потом будет 2022). Для того, чтобы использовать Monogame последней на данный момент версии 3.8, нужно скачать соответствующее расширение.

Можно пойти другим путем – создать пустой проект и докачать к нему соответствующие пакеты через NuGet, но я не советую – придется вручную качать пакеты для каждого проекта (а там не один пакет), а потом вручную же писать начальный код для того, чтобы это счастье работало как нужно (подключение графики и так далее). В то время как установка через расширения создаст в Студии шаблон нового проекта, в котором все, что нужно, написано сразу.

Создание нового проекта Monogame
Создание нового проекта Monogame
Запуск нового проекта Monogame
Запуск нового проекта Monogame

Давайте, подробно пройдемся по структуре автоматически созданного кода.

3.2 Структура проекта

Точкой входа в программу, как всегда, в C # , является статический метод Main статического класса Program. Если заглянуть в соответствующую вкладку, то мы увидим несколько скучных строк, подобных тем, которые можно увидеть, например, в Windows Forms:

Этот код нам недвусмысленно намекает, что метод Main создает экземпляр некоего класса Game1, из которого вызывает метод Run (который как раз и запускает нашу игру). Можно сделать вывод, что внутри Game1 как раз и происходит все самое интересное, и, забегая вперед, это действительно так. Пройдемся по структуре этого класса. Начнем с верха.

С самого начала (1) мы видим то, что класс использует три библиотеки, названия которых начинаются с Microsoft.XNA. Не стоит удивляться – Monogame вырос из майкрософтовского фреймворка, который те перестали поддерживать. Люди с этим не смирились – так и появился Monogame. Далее (2) мы видим, что наш класс Game1 наследуется от Game. Если интересно, можно перейти к определению родительского класса и посмотреть, какие там есть еще поля и методы помимо тех, что встретим далее. После этого (3) видим два приватных поля, которые отвечают за графику, но пока не используются. Скоро мы это поправим. Конструктор (4) подключает графику, задает корневую директорию для ресурсов игры и делает мышку видимой. Дальше идет переопределенный метод Initialize (5), который вызывается при инициализации нашей игры. По задумке авторов, сюда мы должны помещать всякие ресурсы, которые не имеют отношения к графике. Сейчас там вызывается только родительский метод Initialize, который запускает следующий переопределенный метод – LoadContent (6). Здесь, как раз, следует загружать графические ресурсы типа спрайтов и всего остального, что не имеет прямого отношения к коду. Технически, шарп, разумеется, не различает, какой метод для чего нужен. Ваши ресурсы типа спрайтов, звуков и музыки будут храниться после загрузки в обычных полях, поэтому вам никто не мешает подгружать ресурсы хоть внутри метода Initialize, хоть внутри игрового цикла, хоть написать собственный метод и вызывать его в конструкторе кроме того, что в большинстве случаев это просто глупо. Поэтому советую придерживаться этого деления. Затем начинается самое интересное.

Если все, что было до этого, вызывалось один раз, то методы Update и Draw прогоняются каждый игровой цикл. В Update должна обновляться «логика» игры, а Draw каждый цикл рисует игровое поле. Опять же, деление относительно условное, поэтому если вы особый эстет, то можете запихнуть логику в метод Draw, а отрисовку – в метод Update, и оно даже будет работать. Относительно условное деление потому, что в игровом цикле сначала вызывается Update, а уже потом, что логично, Draw, поэтому если вы, например, попытаетесь отрисовать объект, который еще не создан, то программе это не понравится.

Если подытожить, то структурные отличия того, что мы увидели, от обычного C# проекта небольшие: по сути, это необходимость использования методов Update и Draw, которые вместе и составляют игровой цикл. Во всем остальном мы видим обычный C# - код с некоторыми специфическими инструментами, которых коснемся в следующий раз. В моей следующей статье мы подумаем, как вместить так понравившийся мне паттерн MVP в прокрустово ложе игрового цикла, а также создадим что-то осязаемое, помимо голубого экрана.

Комментарии (10)


  1. aegoroff
    14.07.2022 08:17

    OFFTOPIC: Бросилось в глаза - а почему кАраван через О пишется?


    1. EVolans
      14.07.2022 08:46
      +13

      Потому что это отсылка к древнему мему. Это его начало:

      Здраствуйте. Я, Кирилл. Хотел бы чтобы вы сделали игру, 3Д-экшон суть такова... Пользователь может играть лесными эльфами, охраной дворца и злодеем. И если пользователь играет эльфами то эльфы в лесу, домики деревяные набигают солдаты дворца и злодеи. Можно грабить корованы... И эльфу раз лесные то сделать так что там густой лес...


      1. soltpain
        14.07.2022 10:36
        +10

        Я джва года жду эту игру


        1. NebulusPrima
          14.07.2022 16:36

          Задумка то неплоха! Тянет на целый AAA


  1. bratuha
    14.07.2022 11:53
    +1

    Спасибо за статью, когда-то начинал с этого фремворка, когда он еще был XNA (не совсем корректно сказать, что это одно и то же, но подробности опустим). Для 2D прекрасно подходит, новичкам, хоть чуть-чуть знакомым с шарпом, для старта подойдет. Можно выпустить релиз на мобильных платформах, главное, заморочиться с динамическим ресайзом спрайтов под параметры экрана и вообще проработать все нюансы, связанные с разрешением и соотношением сторон.

    По поводу draw и update уже не вспомню подробностей, но на моей памяти update-фичи спокойно работали внутри draw.


    1. KayAltos Автор
      14.07.2022 13:00

      Большое спасибо!
      Да, согласен. Кроме того, из-за того, что много приходится писать самому, лучше понимаешь, как все устроено. Идея с мобилками хорошая, когда доделаю работу, то хочу попробовать.
      3D здесь тоже можно делать, но это удовольствие, прямо скажем, не для всех. По основной работе я моделирую структуры материалов, и мне удалось сделать простейший визуализатор пористых структур, где области с материалом заполняются кубиками. Но если нужно что-то более сложное, то лучше уже на нормальные движки перейти.

      Да, Update-фичи работают внутри Draw. Для проекта это просто два метода, которые вызываются каждый цикл, писать там можно абсолютно любой код. Разграничение, скорее, для читаемости и удобства.


      1. 0vZ
        14.07.2022 13:20
        +1

        Если я правильно помню, еще во времена XNA было такое отличие: в случае падения framerate, вызовы Draw могли пропускаться. Поэтому, если засунуть часть логики в Draw, то есть риск того, что в каких-то ситуациях этот метод не будет вызван.


        1. KayAltos Автор
          14.07.2022 13:31

          Вот этого не знал, спасибо!


  1. OneManStudio
    15.07.2022 09:51

    Просто интересно, почему такой выбор? Почему не взяли например тот же Godot, где все в разы проще и структуру можно поменять в два клика и переиспользовать не создавая проект с нуля?


    1. KayAltos Автор
      15.07.2022 10:00

      Godot в первую очередь заточен под gdscript, а я хочу писать именно на C#, а еще да, чтобы многие вещи писать самому и понять, как они работают.