В первую очередь, рекомендую вам ознакомится с первой частью - там мы написали основу нашей будущей игры(рендер, ввод, звуки, отрисовку шрифтов). На этот раз мы доделаем из демки полноценную небольшую аркаду и портируем её на Android. Ведь многие уже давно забыли, что такое писать игры с нуля, без каких либо движков, наверняка вам будет интересно попробовать, что за мини-игра получилась в итоге ;) https://habr.com/ru/post/695428/ Итак, с чем мы закончили в прошлый раз? У нас была простенькая демка, которая работала как на ПК, так и на коммуникаторах. Можно было запустить игру, пострелять врагов и... всё. Не так уж и интересно, верно? Теперь нам нужно довести демку до полноценной, пусть и неболшой игры. Само собой мало кто будет откапывать свой старый коммуникатор из кладовки, только для того, чтобы запустить игрушку из статьи. Да и мы ведь хотим, чтобы в нашу игру поиграли люди? Поэтому первая цель - порт на Android. Затем, что нам нужно ещё? Ага - меню! В менюшке у нас должно быть всё просто - игра и выход, а ещё мы должны видеть всю важную статистику(на момент написания статьи - рекорд по счёту). Теперь же, мы подошли к следующему пункту - сохранение прогресса. Ой, а интерфейс слишком мелкий на моём самсунге с 2K дисплеем! Для этого нам нужен масштабируемый UI. Так начнём же с порта! Все мы помним, что у нас есть простое ядро игры, которое ни в чём нас не ограничивает, где можно пока не реализованные модули заменить пустыми заглушками и портировать не сидя 24/7 в дебаггере? Это хорошо. В этом случае у нас уже добавляется поддержка 3-его GAPI: OpenGL ES. Поскольку я обещал, что поиграть можно будет на чём угодно, берём минимальный таргет Xamarin - Android 2.3, а версию ES - 1.1. При желании перенести игру с FFP на шейдеры не составит вообще никаких проблем. Кроме того, Metal я пока не щупал, поэтому порт на iOS будет работать на GLES - останется портировать лишь Window и звук. Контекст создаёт сама платформа. Что нам нужно портировать? Рендерер Звук Ввод Отрисовка шрифтов Кроме того, я не отлаживаю никогда на эмуляторе - я всегда запускаю игру на реальном девайсе, а их у меня, тьфу тьфу тьфу, много(и вам читателям, за это спасибо. Где бы я сейчас откопал Moto Milestone?). Девайсы разных конфигураций - x86 смартфон, смартфоны с Adreno 200, 205, 305, PowerVR SGX531/535, Mali 400 в различных конфигурациях, MSM8255, MSM7225, MT6572, MT6575, Atom Z2580. Весь этот зоопарк можно и нужно протестировать - ведь если пойдёт на них и не допущено критичных косяков по совместимости на Android 10/11+, то игра пойдёт вообще везде. Для тестов я использую недавно купленный Sony Ericsson Xperia U - на нем и андроид достаточно старый(4.0.4) и отладка очень быстрая - у меня на самсунге с 7 андроидом деплой секунд 20, на xperia u - секунд 5. Как вы помните, главная абстракция от платформы у нас это класс Window - он управляет "вычислением" физических путей к ассетам, он содержит сведения о платформе(мобильная ли она или нет, название, язык), он управляет окном, и от него можно получить размеры экрана. В случае Android, Window - это просто прослойка между главным Activity и самой игрой. MainActivity шлёт события(например, нажатие хардварной кнопки или тап по экрану) в Window, а тот выступает в роли диспетчера, переправляя это всё в подсистему ввода и.т.п. В случае Android, циклом отрисовки и созданием контекста занимается GLSurfaceView, а нам остаётся только портировать сам рендерер. Вот так выглядит MainActivity: В классе Window добавился PlatformInfo, который как я уже говорил, содержит в себе флаги о текущей платформе, и пути к различным директориям(например, где хранятся локальные данные приложения: Поскольку на Android бывают ещё и игровые приставки, и смарт телевизоры, где тачскрина нет - у нас всё так же остаётся система биндов кнопок. т.е люди как с планшетами-трансформерами, так и с геймпадами смогу поиграть в игру без проблем. Что касается механизма загрузки ассетов, то с андроидовской концепцией ассетов всё криво - Xamarin не может просканировать папку и добавить все ассеты оттуда, это муторно и неудобно. Поэтому на данный момент, все ассеты хранятся на /sdcard/GameData/WMGame3D/(sdcard в большинстве случаев - внутренняя память). Это и называется кэшем игры. Я видел ещё один вариант - распаковка ассетов на лету(Galaxy On Fire 2) в папку игры, но такой вариант мне не по душе. Поэтому если я добавил ещё одну модельку корабля - я не шерстю Visual Studio, а просто по MTP скидываю изменившиеся файлы. Просто и удобно, но доступ к таким ассетам из версий 4.4+ только read-only. Писать можно только в свою папку(/data/data/<package>/files), там же у нас будут сохранения. А ещё Android'овские ассеты не поддерживают seek - это тоже минус, приходится сразу захапывать весь файл в память, и возвращать уже MemoryStream Касательно рендерера - ничего особо не поменялось. Всё точно так же, только теперь меши рисует GLES. Порт занял около часа. Текстуры грузятся с помощью нативного Bitmap, и LockPixels. Звук я реализовал на SoundPool. В OpenTK не оказалось обертки над OpenSL(да и я им не пользовался никогда, зато пользовался OpenAL - который валился в SIGSEGV на андроиде). В целом работает нормально, но SoundPool на 2.3 не поддерживает загрузку звуков из Stream. Никакого 3D звука нет и возможности узнать, проигрывается ли ещё файл тоже, но у нас фактически весь звук - проиграть музыку и эффекты, поэтому нам хватит. Отрисовка шрифтов у нас нативная - во первых это поддержка юникода и не- моноширинных шрифтов из коробки, во вторых нативные шрифты сглажены. Для этого у нас используется Bitmap размерами с экран и Canvas, который рисует шрифты. Затем в конце кадра итоговый битмап заливается в текстуру, а текстура рисуется как квад на весь экран. Это не просаживает FPS, насколько я знаю - на стороне Android'а рендер шрифтов хардварный, поэтому даже большое количество текста должно перевариваться нормально. Если кто-то будет использовать нативный рендерер шрифтов Android, то не забываем, что координата Y начинается с нижней части текста. Не забываем получить метрики шрифта и вычитать Top из координаты текста, иначе текст "уползёт" вверх. Есть вариант рисовать текст "оверлеем" уже на уровне оконной системы. Android такие трюки умеет, а вот Windows - нет, GDI очень тормозной в этом плане. Но на Windows есть удобный DirectWrite. Есть ещё концепция "запекания" отрисованного текста в текстуру фиксированного размера(подобным образом работал TextRenderer в старых версиях Unity). Лучший вариант это конечно же запекание глифов в атласы, но тогда придётся думать над эффективным батчингом, переключением страниц с глифами, и думать над размерами(либо использовать distance field шрифты). Полная реализация: Не обращаем внимание на MeasureString. Игра запускается и работает! Отлично. Интерфейс у нас не зависит от разрешения экрана - пусть и сам по себе он примитивный. Это достигается за счёт двух очень простых концепций. Здесь нет концепций Align'ов, Docking'ов, каких либо сеток или других концепций для реализации UI. Всё по спартански просто: Размер шрифта - относительное значение того, какой шрифт смотрелся бы лучше всего на разрешении 640x480. Это очень старая концепция, я её видел ещё в GTA San Andreas. Однако всё равно размер шрифта нужно ограничивать в определённых размерах: Есть ещё одна интересная концепция: прописывать заранее весь layout и размеры шрифтов в зависимости от разрешения. Например QVGA, WVGA, VGA, SVGA, HD, FHD, UHD. Если разрешение где-то посерединке, то брать от ближайшего к нему. Что касается позиционирования UI элементов, здесь можно использовать относительную(процентную относительно размера дисплея) систему координат т.е 0.0-1.0. Тут тоже всё очень просто, и классически: По итогу UI масштабировался нормально, но на мобилках listmenu(т.е менюшки) выглядели мелковато, поэтому если мы играем на мобилке - принудительно размер шрифтов увеличивается в два раза. Все меню в игре сделаны на ListMenu - который поддерживает как ввод с кнопок, так и с тачскрина. Удобно и кроссплатформенно! Что касается главного меню, MainMenu - отдельный стейт игры. Он рисует фон, само главное меню и управляет подменюшками т.е SubMenu. Просто и компактно. Нам ведь нужно где-то хранить, сколько очков набрал пользователь, сколько врагов перебил, как его звать - верно? Для этого и нужен механизм сохранений. В нашей игре он будет до безобразия примитивным - простой бинарный файл, в который записана статистика, очки, деньги, опыт и.т.п + номер версии. Однако! Это не самый лучший подход, поскольку накладывает огромное количество ограничений: Во первых если вы пишите игру, под что-то отличное от little-endian, вам придётся в рантайме менять местами байты, иначе получите билиберду. С текстовыми форматами таких проблем нет. Во вторых, если вы на этапе разработки решили ограничить максимальный уровень до размера байта(255), а через 10 версий решили, что пользователь сможет прокачаться до уровня 1024 - вам придётся либо велосипедить второй байт уровня где-то посерединке, либо менять формат сейва - делая его несовместимым с прошлыми версиями Если у вас кардинально меняется файл сохранения(например, вы сохраняете сущность. У вас есть метаданные и захардкоженные свойства а-ля позиция. И вы захотели переместить это свойство в метаданные) - вам придётся городить всякие Version15Converter, Version16Convert и прочие костыли, либо заставлять игроков начинать новую игру. Перспектива так себе, верно? Плюсы: Малый размер файла. Это очень актуально для игр с сложными сценами, и где сохраняется фактически полная копия уровня со всем состоянием. Быстрая загрузка. В большинстве простых игр, сейв можно тупо "отразить" в память. Xml на 15 мегабайт будет парсится куда дольше. В нашей игре подход простой: статистика(пока не используется, но зарезервирована для будущих версий) в виде структуры и сами данные Для сохранения/загрузки используется BinaryReader/BinaryWriter. Использовать BinarySerializer я не стал для пущей наглядности, как это работает.   Выглядит просто и удобно. Если полей в какой-то момент станет больше - то проще завести KeyValue список, в котором хранить всё что надо, или использовать сериализацию. Вывод: для достаточно примитивной игры - это нормальное решение. Для чего-то более комплексного лучше использовать сериализацию(благо в .net она очень мощная из коробки, в т.ч есть адаптеры для бинарной сериализации). Главное меню - отдельный стейт игры, который разделен на различные подменю(ISubMenu). Сама менюшка - ListMenu, как уже отмечено выше - поддерживает ввод, как с пальца/мыши, так и с геймпада/клавиатуры. В главном меню, как стейте, есть "подменю" с основными пунктами, и потенциальные подменю для настроек/магазина(их я не успел реализовать). Где подменюхи(анонимные методы не используются в угоду поддержки древних версий С# - игра под WM затачивалась) :   Кроме того, я реализовал простенький механизм анимации интерфейса, но (пока-что) он используется только в меню. Поскольку некоторая часть геймплея у нас уже была, нам нужно найти баланс в различных параметрах геймплея. Из-за примитивности игры, фактически за сложность будет отвечать один параметр(и несколько рандомных факторов) - стадия игры. Стадия игры инкрементируется каждые 35 секунд, повышая сложность игры. Именно на стадии завязаны множители скорости игры, множитель интервала спавна астероидов. Множитель счёта планировалось сделать в зависимости от уровня игрока. Формула ну очень простая(и захардкоженная - так делать не нужно =)): В процессе игры, игрок может заскучать, если у нас просто спавнятся более быстрые астероиды. Поэтому, мы меняем тайминги спавна врагов, если стадия выше определенного показателя:   Аптечки выпадают слишком часто? Тогда, когда приходит время спавна бонуса, а здоровье игрока ыше 50 и ГПСЧ выбирает аптечку - то мы насильно заспавним бонус.   Игра строится из мелочей. Одна из таких мелочей - обратная связь, которая создаёт некоторую плавность игры. Например, для эффекта полёта - камера слегка трясётся во время игры. Однако для игрока создаётся эффект, будто бы это корабль туда-сюда витает, и им надо лучше управлять. При столкновении камера трясётся сильнее - сбивая игрока с толку. Вот мы и написали с вами небольшую игру. Я попытался расписать статью в новом для себя стиле - в первой части я разжевал архитектуру, с пояснением каждого элемента, класса и решения. Вторая часть статьи оказалась скорее неким девблогом, и отклонилась от концепции первой части статьи. Я одновременно пытаюсь усидеть на двух стульях - и с достаточным кол-вом технических подробностей, и при этом относительно понятным для людей, которые довольно далеки от разработки игр. Получается ли это у меня? Решать вам :) В любом случае, я надеюсь кому-то этот "дуэт" статей окажется полезным - конечно же исходный код игры доступен на гитхабе, который я постараюсь попозже задокументировать ещё более подробно. Кроме того, в игре всё ещё есть достаточно слабые моменты в плане архитектуры и конкретных участков кода, которые надо ещё полировать. Ниже я публикую видео с геймплеем игры, если вам лень качать. Кроме того, публикую линк на архив с бинарниками(exe под win32 и apk под Android. Под Android, распакуйте ресурсы в память телефона/WMGame3D/GameData/). https://disk.yandex.ru/d/37UqaBF1VXDU8g https://github.com/monobogdan/spaceshooterwm    Программист, железнячник. Донаты идут на контент!