Блок-схемы на практике без формалина

Блок-схемы — это наглядный инструмент, который помогает превратить сложный алгоритм в понятную и структурированную последовательность действий. От программирования до управления бизнес-процессами, они служат универсальным языком для визуализации, анализа и оптимизации самых сложных систем.

Представьте себе карту, где вместо дорог — логика, а вместо городов — действия. Это и есть блок-схема — незаменимый инструмент для навигации по самым запутанным процессам.

Пример 1: Упрощённая схема запуска игры
Чтобы понять принцип работы, давайте представим простую схему запуска игры.

Эта схема показывает идеальный сценарий, когда всё происходит без сбоев. Но в реальной жизни всё гораздо сложнее.

Пример 2: Расширенная схема запуска игры с загрузкой данных
Современные игры часто требуют подключения к интернету для загрузки данных пользователя, сохранений или настроек. Давайте добавим эти шаги в нашу схему.

Эта схема уже более реалистична, но что произойдёт, если что-то пойдёт не так?

Как было: Игра, которая “ломалась” при потере интернета

На старте проекта разработчики могли не учесть всех возможных сценариев. Например, они сосредоточились на основной логике игры и не подумали, что произойдет, если у игрока пропадет интернет-соединение.

В такой ситуации блок-схема их кода выглядела бы так:

В этом случае, вместо того чтобы выдать ошибку или корректно закрыться, игра замирала на этапе ожидания данных, которых не получала из-за отсутствия соединения. Это приводило к “чёрному экрану” и зависанию приложения.

Как стало: Исправление по жалобам пользователей

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

Вот как выглядит исправленная блок-схема, где учтены оба сценария:

Благодаря такому подходу, игра теперь корректно информирует пользователя о проблеме, а в некоторых случаях даже может перейти в офлайн-режим, позволяя продолжить игру. Это наглядный пример того, почему блок-схемы так важны: они заставляют разработчика думать не только об идеальном пути выполнения, но и о всех возможных сбоях, делая итоговый продукт гораздо более стабильным и надёжным.

Неопределенное поведение

Зависания и ошибки — это лишь один из примеров непредсказуемого поведения программы. В программировании существует понятие неопределённого поведения (Undefined Behavior) — это ситуация, когда стандарт языка не описывает, как должна вести себя программа в определённом случае.

Это может привести к чему угодно: от случайного “мусора” в выводе до сбоя программы или даже серьёзной уязвимости безопасности. Неопределённое поведение часто возникает при работе с памятью, например, со строками в языке C.

Пример из языка C:

Представьте, что разработчик скопировал строку в буфер, но забыл добавить в конец нулевой символ (`\0`), который отмечает конец строки.

Вот как выглядит код:

#include 

int main() {
char buffer[5];
char* my_string = "hello";

memcpy(buffer, my_string, 5);

printf("%s\n", buffer);
return 0;
}

Ожидаемый результат: “hello”
Реальный результат Непредсказуем.

Почему так происходит? Функция `printf` с спецификатором `%s` ожидает, что строка завершается нулевым символом. Если его нет, она продолжит читать память за пределами выделенного буфера.

Вот блок-схема этого процесса с двумя возможными исходами:

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

Pixel Perfect: Миф или реальность в эпоху декларативности?

В мире разработки интерфейсов существует расхожее понятие – “pixel perfect вёрстка”. Оно подразумевает максимально точное воспроизведение дизайн-макета до мельчайшего пикселя. Долгое время это было золотым стандартом, особенно в эру классического веб-дизайна. Однако с приходом декларативной вёрстки и стремительным ростом разнообразия устройств, принцип “pixel perfect” становится всё более эфемерным. Попробуем разобраться, почему.

Императивный WYSIWYG vs. Декларативный Код: В чём разница?

Традиционно многие интерфейсы, особенно десктопные, создавались с помощью императивных подходов или WYSIWYG (What You See Is What You Get) редакторов. В таких инструментах дизайнер или разработчик напрямую манипулирует элементами, располагая их на холсте с точностью до пикселя. Это похоже на работу с графическим редактором – вы видите, как ваш элемент выглядит, и можете точно его позиционировать. В этом случае достижение “pixel perfect” было вполне реальной целью.

Однако современная разработка всё чаще опирается на декларативную вёрстку. Это означает, что вы не говорите компьютеру “помести эту кнопку сюда”, а описываете, что вы хотите получить. Например, вместо того чтобы указывать конкретные координаты элемента, вы описываете его свойства: “эта кнопка должна быть красной, иметь отступы 16px со всех сторон и находиться в центре контейнера”. Фреймворки вроде React, Vue, SwiftUI или Jetpack Compose как раз и используют этот принцип.

Почему “Pixel Perfect” не работает с декларативной вёрсткой для множества устройств

Представьте себе, что вы создаёте приложение, которое должно одинаково хорошо выглядеть на iPhone 15 Pro Max, Samsung Galaxy Fold, iPad Pro и телевизоре с разрешением 4K. Каждое из этих устройств имеет разное разрешение экрана, плотность пикселей, соотношение сторон и физические размеры.

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

* Адаптивность и Отзывчивость: Основная цель декларативной вёрстки — создать адаптивные и отзывчивые интерфейсы. Это значит, что ваш интерфейс должен автоматически подстраиваться под размер и ориентацию экрана, не ломаясь и сохраняя читаемость. Если бы мы стремились к “pixel perfect” на каждом устройстве, нам пришлось бы создавать бесчисленное количество вариантов одного и того же интерфейса, что полностью нивелирует преимущества декларативного подхода.
* Плотность Пикселей (DPI/PPI): Устройства имеют разную плотность пикселей. Один и тот же элемент, имеющий размер 100 “виртуальных” пикселей, на устройстве с высокой плотностью будет выглядеть гораздо меньше, чем на устройстве с низкой плотностью, если не учитывать масштабирование. Декларативные фреймворки абстрагируются от физических пикселей, работая с логическими единицами.
* Динамический Контент: Контент в современных приложениях часто бывает динамическим – его объём и структура могут меняться. Если бы мы жёстко привязывались к пикселям, любое изменение текста или изображения привело бы к “разваливанию” макета.
* Различные Платформы: Помимо разнообразия устройств, существуют и разные операционные системы (iOS, Android, Web, Desktop). Каждая платформа имеет свои гайдлайны по дизайну, стандартные элементы управления и шрифты. Попытка сделать абсолютно идентичный, “pixel perfect” интерфейс на всех платформах привела бы к неестественному виду и плохому пользовательскому опыту.

Старые Подходы Не Ушли, А Эволюционировали

Важно понимать, что подход к вёрстке интерфейсов не является бинарным выбором между “императивным” и “декларативным”. Исторически для каждой платформы существовали свои инструменты и подходы к созданию интерфейсов.

* Нативные Интерфейсные Файлы: Для iOS это были XIB/Storyboards, для Android – XML-файлы разметки. Эти файлы представляют собой pixel-perfect WYSIWYG верстку, которая затем отображается в рантайме также как и в редакторе. Этот подход никуда не исчез, он продолжает развиваться, интегрируясь с современными декларативными фреймворками. Например, SwiftUI в Apple и Jetpack Compose в Android ступили на путь чисто декларативного кода, но при этом сохранили возможность использовать классическую верстку.
* Гибридные Решения: Часто в реальных проектах используется комбинация подходов. Например, базовая структура приложения может быть реализована декларативно, а для специфических, требующих точного позиционирования элементов, могут применяться более низкоуровневые, императивные методы или же подключаться нативные компоненты, разработанные с учётом специфики платформы.

От Монолита к Адаптивности: Как Эволюция Устройств Сформировала Декларативную Вёрстку

Мир цифровых интерфейсов претерпел колоссальные изменения за последние десятилетия. От стационарных компьютеров с фиксированными разрешениями мы пришли к эпохе экспоненциального роста разнообразия пользовательских устройств. Сегодня наши приложения должны одинаково хорошо работать на:

* Смартфонах всех форм-факторов и размеров экрана.
* Планшетах с их уникальными режимами ориентации и разделенного экрана.
* Ноутбуках и десктопах с различными разрешениями мониторов.
* Телевизорах и медиацентрах, управляемых дистанционно. Примечательно, что даже для телевизоров, пульты которых могут быть простыми, как Apple TV Remote с минимумом кнопок, или наоборот, перегруженными множеством функций, современные требования к интерфейсам таковы, что код не должен требовать специфической адаптации под эти особенности ввода. Интерфейс должен работать “как бы сам собой”, без дополнительного описания того, “как” именно взаимодействовать с конкретным пультом.
* Умных часах и носимых устройствах с минималистичными экранами.
* Шлемах виртуальной реальности (VR), требующих совершенно нового подхода к пространственному интерфейсу.
* Устройствах дополненной реальности (AR), накладывающих информацию на реальный мир.
* Автомобильных информационно-развлекательных системах.
* И даже бытовой технике: от холодильников с сенсорными экранами и стиральных машин с интерактивными дисплеями до умных духовок и систем “умного дома”.

Каждое из этих устройств имеет свои уникальные особенности: физические размеры, соотношение сторон, плотность пикселей, методы ввода (сенсорный экран, мышь, контроллеры, жесты, голосовые команды) и, что немаловажно, тонкости пользовательского окружения. Например, VR-шлем требует глубокого погружения, а смартфон — быстрой и интуитивной работы на ходу, тогда как интерфейс холодильника должен быть максимально простым и крупным для быстрой навигации.

Классический Подход: Бремя Поддержки Отдельных Интерфейсов

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

* Разработка под iOS часто требовала использования Storyboards или XIB-файлов в Xcode, написания кода на Objective-C или Swift.
* Для Android создавались XML-файлы разметки и код на Java или Kotlin.
* Веб-интерфейсы верстались на HTML/CSS/JavaScript.
* Для C++ приложений на различных десктопных платформах использовались свои специфические фреймворки и инструментарии:
* В Windows это были MFC (Microsoft Foundation Classes), Win32 API с ручной отрисовкой элементов или с использованием ресурсных файлов для диалоговых окон и элементов управления.
* В macOS применялись Cocoa (Objective-C/Swift) или старые Carbon API для прямого управления графическим интерфейсом.
* В Linux/Unix-подобных системах часто использовались библиотеки вроде GTK+ или Qt, которые предоставляли свой набор виджетов и механизмы для создания интерфейсов, нередко через XML-подобные файлы разметки (например, .ui файлы в Qt Designer) или прямое программное создание элементов.

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

Декларативная Вёрстка: Единый Язык для Разнообразия

Именно в ответ на это стремительное усложнение и появилась декларативная вёрстка как доминирующая парадигма. Фреймворки вроде React, Vue, SwiftUI, Jetpack Compose и других представляют собой не просто новый способ написания кода, а фундаментальный сдвиг в мышлении.

Основная идея декларативного подхода: вместо того чтобы говорить системе “как” отрисовывать каждый элемент (императивно), мы описываем “что” мы хотим увидеть (декларативно). Мы задаём свойства и состояние интерфейса, а фреймворк сам решает, как наилучшим образом отобразить его на конкретном устройстве.

Это стало возможно благодаря следующим ключевым преимуществам:

1. Абстракция от Деталей Платформы: Декларативные UI фреймворки специально разработаны, чтобы забыть о низкоуровневых деталях отрисовки и специфике каждой платформы. Разработчик описывает компоненты и их взаимосвязи на более высоком уровне абстракции, используя единый, переносимый код.
2. Автоматическая Адаптация и Отзывчивость: Фреймворки берут на себя ответственность за автоматическое масштабирование, изменение макета и адаптацию элементов под различные размеры экранов, плотности пикселей и методы ввода. Это достигается за счёт использования гибких систем компоновки, таких как Flexbox или Grid, и концепций, подобных “логическим пикселям” или “dp” (density-independent pixels).
3. Согласованность Пользовательского Опыта: Несмотря на внешние различия, декларативный подход позволяет поддерживать единую логику поведения и взаимодействия по всему семейству устройств. Это упрощает процесс тестирования и обеспечивает более предсказуемый пользовательский опыт.
4. Ускорение Разработки и Снижение Затрат: С одним и тем же кодом, способным работать на множестве платформ, значительно снижаются время и стоимость разработки и поддержки. Команды могут сосредоточиться на функциональности и дизайне, а не на многократном переписывании одного и того же интерфейса.
5. Готовность к Будущему: Способность абстрагироваться от специфики текущих устройств делает декларативный код более устойчивым к появлению новых типов устройств и форм-факторов. Фреймворки могут быть обновлены для поддержки новых технологий, а ваш уже написанный код получит эту поддержку относительно бесшовно.

Заключение

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

Программистам, дизайнерам и пользователям необходимо научиться жить в этом новом мире. Лишние детали “pixel perfect” дизайна, привязанные к конкретному устройству или разрешению, приводят к ненужным временным затратам на разработку и поддержку. Более того, такие жёсткие макеты могут просто не отработать на устройствах с нестандартными интерфейсами, таких как телевизоры с ограниченным вводом, VR- и AR-шлемы, а также другие устройства будущего, о которых мы сегодня ещё даже не догадываемся. Гибкость и адаптивность – вот ключи к созданию успешных интерфейсов в современном мире.

Vibe-кодерские трюки: почему LLM пока не работают с SOLID, DRY и CLEAN

С развитием больших языковых моделей (LLM), таких как ChatGPT, всё больше разработчиков используют их для генерации кода, проектирования архитектуры и ускорения интеграции. Однако при практическом применении становится заметно: классические принципы архитектуры — SOLID, DRY, CLEAN — плохо уживаются с особенностями кодогенерации LLM.

Это не значит, что принципы устарели — напротив, они прекрасно работают при ручной разработке. Но с LLM подход приходится адаптировать.

Почему LLM не справляются с архитектурными принципами

Инкапсуляция

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

Абстракции и интерфейсы

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

DRY (Don’t Repeat Yourself)

LLM не стремятся минимизировать повторяющийся код — напротив, им проще дублировать блоки, чем выносить общую логику. Хотя они могут предложить рефакторинг по запросу, по умолчанию модели склонны генерировать «самодостаточные» фрагменты, даже если это приводит к избыточности.

CLEAN Architecture

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

Что работает лучше при работе с LLM

WET вместо DRY
Подход WET (Write Everything Twice) оказывается практичнее в работе с LLM. Дублирование кода не требует от модели удержания контекста, а значит — результат предсказуемее и легче исправляется вручную. Это также снижает вероятность появления неочевидных связей и багов.

Кроме того, дублирование помогает компенсировать короткую память модели: если определённый фрагмент логики встречается в нескольких местах, LLM с большей вероятностью будет его учитывать при дальнейшей генерации. Это упрощает сопровождение и увеличивает устойчивость к “забыванию”.

Простые структуры вместо инкапсуляции

Избегая сложной инкапсуляции и полагаясь на прямую передачу данных между частями кода, можно значительно упростить как генерацию, так и отладку. Это особенно актуально при быстрой итеративной разработке или создании MVP.

Упрощённая архитектура

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

Интеграция SDK — вручную надёжнее

Большинство языковых моделей обучены на устаревших версиях документации. Поэтому при генерации инструкций по установке SDK часто появляются ошибки: устаревшие команды, неактуальные параметры или ссылки на недоступные ресурсы. Практика показывает: лучше всего использовать официальную документацию и ручную настройку, оставляя LLM вспомогательную роль — например, генерацию шаблонного кода или адаптацию конфигураций.

Почему принципы всё же работают — но при ручной разработке

Важно понимать, что сложности с SOLID, DRY и CLEAN касаются именно кодогенерации через LLM. Когда разработчик пишет код вручную, эти принципы продолжают демонстрировать свою ценность: снижают связанность, упрощают сопровождение, повышают читаемость и гибкость проекта.

Это связано с тем, что человеческое мышление склонно к обобщению. Мы ищем закономерности, выносим повторяющуюся логику в отдельные сущности, создаём паттерны. Вероятно, такое поведение имеет эволюционные корни: сокращение объёма информации экономит когнитивные ресурсы.

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

Вывод

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

Принципы SOLID, DRY и CLEAN по-прежнему актуальны — но наилучший эффект они дают в руках человека. При работе с LLM разумно использовать упрощённый, практичный стиль, позволяющий получать надёжный и понятный код, который легко доработать вручную. А где LLM забывает — дублирование кода помогает ему вспомнить.

Портирование Surreal Engine C++ на WebAssembly

В этой заметке я опишу то как я портировал игровой движок Surreal Engine на WebAssembly.

Surreal Engine – игровой движок который реализует большую часть функционала движка Unreal Engine 1, известные игры на этом движке – Unreal Tournament 99, Unreal, Deus Ex, Undying. Он относится к классическим движкам, которые работали преимущественно в однопоточной среде выполнения.

Изначально у меня была идея взяться за проект который я не смогу выполнить в какой-либо разумный срок, таким образом показав своим подписчикам на Twitch, что есть проекты которые не могу сделать даже я. В первый же стрим я внезапно понял что задача портирования Surreal Engine C++ на WebAssembly с помощью Emscripten выполнима.

Surreal Engine Emscripten Demo

Спустя месяц я могу продемонстрировать свой форк и сборку движка на WebAssembly:
https://demensdeum.com/demos/SurrealEngine/

Управление как и в оригинале, осуществляется на стрелках клавиатуры. Далее планирую адаптацию под мобильное управление (тачи), добавление корректного освещения и прочие графические фишки рендера Unreal Tournament 99.

С чего начать?

Первое о чем хочется сказать, это то что любой проект можно портировать с C++ на WebAssembly с помощью Emscripten, вопрос лишь в том насколько полным получится функционал. Выбирайте проект порты библиотек которого уже доступны для Emscripten, в случае Surreal Engine очень сильно повезло, т.к. движок использует библиотеки SDL 2, OpenAL – они обе портированы под Emscripten. Однако в качестве графического API используется Vulkan, который на данный момент не доступен для HTML5, ведутся работы по реализации WebGPU, но он также находится в стадии черновика, также неизвестно насколько простым будет дальнейший порт из Vulkan на WebGPU, после полной стандартизации оного. Поэтому пришлось написать свой собственный базовый OpenGL-ES / WebGL рендер для Surreal Engine.

Сборка проекта

Система сборки в Surreal Engine – CMake, что тоже упрощает портирование, т.к. Emscripten предоставляет свои нативные сборщики – emcmake, emmake.
За основу порта Surreal Engine брался код моей последней игры на WebGL/OpenGL ES и C++ под названием Death-Mask, из-за этого разработка шла гораздо проще, все необходимые флаги сборки были с собой, примеры кода.

Один из важнейших моментов в CMakeLists.txt это флаги сборки для Emscripten, ниже пример из файла проекта:


-s MAX_WEBGL_VERSION=2 \

-s EXCEPTION_DEBUG \

-fexceptions \

--preload-file UnrealTournament/ \

--preload-file SurrealEngine.pk3 \

--bind \

--use-preload-plugins \

-Wall \

-Wextra \

-Werror=return-type \

-s USE_SDL=2 \

-s ASSERTIONS=1 \

-w \

-g4 \

-s DISABLE_EXCEPTION_CATCHING=0 \

-O3 \

--no-heap-copy \

-s ALLOW_MEMORY_GROWTH=1 \

-s EXIT_RUNTIME=1")

Сам сборочный скрипт:


emmake make -j 16

cp SurrealEngine.data /srv/http/SurrealEngine/SurrealEngine.data

cp SurrealEngine.js /srv/http/SurrealEngine/SurrealEngine.js

cp SurrealEngine.wasm /srv/http/SurrealEngine/SurrealEngine.wasm

cp ../buildScripts/Emscripten/index.html /srv/http/SurrealEngine/index.html

cp ../buildScripts/Emscripten/background.png /srv/http/SurrealEngine/background.png

Далее подготовим index.html, который включает в себя прелоадер файловой системы проекта. Для выкладывания в веб я использовал Unreal Tournament Demo версии 338. Как можно увидеть из файла CMake, распакованная папка игры была добавлена с сборочную директорию и прилинкована как preload-file для Emscripten.

Основные изменения кода

Затем предстояло поменять игровой цикл игры, запускать бесконечный цикл нельзя, это приводит к зависанию браузера, вместо этого нужно использовать emscripten_set_main_loop, об этой особенности я писал в своей заметке 2017 года “Портирование SDL C++ игры на HTML5 (Emscripten)
Код условия выхода из цикла while меняем на if, далее выводим основной класс игрового движка, который содержит игровой луп, в глобальный скоп, и пишем глобальную функцию которая будет вызывать шаг игрового цикла из глобального объекта:


#include <emscripten.h>

Engine *EMSCRIPTEN_GLOBAL_GAME_ENGINE = nullptr;

void emscripten_game_loop_step() {

	EMSCRIPTEN_GLOBAL_GAME_ENGINE->Run();

}

#endif

После этого нужно убедиться что в приложении отсутствуют фоновые потоки, если они есть, то приготовьтесь к переписываю их на однопоточное выполнение, либо использование библиотеки phtread в Emscripten.
Фоновый поток в Surreal Engine используется для проигрывания музыки, из главного потока движка приходят данные о текущем треке, о необходимости проигрывания музыки, либо ее отсутствии, затем фоновый поток по мьютексу получает новое состояние и начинает проигрывать новую музыку, либо приостанавливает. Фоновый поток также используется для буферизации музыки во время проигрывания.
Мои попытки собрать Surreal Engine под Emscripten с pthread не увенчались успехом, по той причине что порты SDL2 и OpenAL были собраны без поддержки pthread, а их пересборкой ради музыки я заниматься не хотел. Поэтому перенес функционал фонового потока музыки в однопоточное выполнение с помощью цикла. Удалив вызовы pthread из C++ кода, я перенес буферизацию, проигрывание музыки в основной поток, чтобы не было задержек я увеличил буфер на несколько секунд.

Далее я опишу уже конкретные реализации графики и звука.

Vulkan не поддерживается!

Да Vulkan не поддерживается в HTML5, хотя все рекламные брошюры выдают кроссплатформенность и широкую поддержку на платформах как основное преимущество Vulkan. По этой причине пришлось написать свой базовый рендер графики для упрощенного типа OpenGL – ES, он используется на мобильных устройствах, иногда не содержит модных фишек современного OpenGL, зато он очень хорошо переносится на WebGL, именно это реализует Emscripten. Написание базового рендера тайлов, bsp рендеринга, для простейшего отображения GUI, и отрисовки моделей + карт, удалось за две недели. Это, пожалуй, была самая сложная часть проекта. Впереди еще очень много работы по имплементации полного функционала рендеринга Surreal Engine, поэтому любая помощь читателей приветствуется в виде кода и pull request’ов.

OpenAL поддерживается!

Большим везением стало то, что Surreal Engine использует OpenAL для вывода звука. Написав простой hello world на OpenAL, и собрав его на WebAssembly c помощью Emscripten, мне стало ясно насколько все просто, и я отправился портировать звук.
После нескольких часов дебага, стало очевидно что в OpenAL реализации Emscripten есть несколько багов, например при инициализации считывания количества моно каналов, метод возвращал бесконечную цифру, а после попытки инициализации вектора бесконечного размера, падает уже C++ с исключением vector::length_error.
Это удалось обойти сделав хардкод количества моно каналов на 2048:


		alcGetIntegerv(alDevice, ALC_STEREO_SOURCES, 1, &stereoSources);



#if __EMSCRIPTEN__

		monoSources = 2048; // for some reason Emscripten's OpenAL gives infinite monoSources count, bug?

#endif



А сеть есть?

Surreal Engine сейчас не поддерживает сетевую игру, игра с ботами поддерживается, но нужен кто-то кто напишет ИИ для этих ботов. Теоретически реализовать сетевую игру на WebAssembly/Emscripten можно с помощью Websockets.

Заключение

В заключение хочется сказать что портирование Surreal Engine получилось достаточно гладким из-за использования библиотек для которых есть порты Emscripten, также мой прошлый опыт реализации игры на C++ для WebAssembly на Emscripten. Ниже ссылки на источники знаний, репозиториев по теме.
M-M-M-MONSTER KILL!

Также если вы хотите помочь проекту, желательно кодом рендера WebGL/OpenGL ES, то пишите мне в Telegram:
https://t.me/demenscave

Ссылки

https://demensdeum.com/demos/SurrealEngine/
https://github.com/demensdeum/SurrealEngine-Emscripten

https://github.com/dpjudas/SurrealEngine

Включаем подсветку USB клавиатуры на macOS

Недавно купил очень недорогую USB-клавиатуру Getorix GK-45X, с RGB подсветкой. Подключив ее к Макбуку Pro на процессоре M1 стало понятно что RGB подсветка не работает. Даже нажимая волшебную комбинацию Fn + Scroll Lock включить подсветку не удалось, менялся только уровень подсветки экрана макбука.
Решений этой проблемы несколько, а именно OpenRGB (не работает), HID LED Test (не работает). Cработала только утилита kvmswitch:
https://github.com/stoutput/OSX-KVM

Надо ее скачать из гитхаба и разрешить для запуска из терминала в Security панели System Settings.
Как я понял из описания, после запуска утилита отправляет нажатие Fn + Scroll Lock, таким образом включая/выключая подсветку на клавиатуре.

Tree sort

Tree sort – сортировка двоичным деревом поиска. Временная сложность – O(n²). В таком дереве у каждой ноды слева числа меньше ноды, справа больше ноды, при приходе от корня и распечатке значений слева направо, получаем отсортированный список чисел. Удивительно да?

Рассмотрим схему двоичного дерева поиска:

Derrick Coetzee (public domain)

Попробуйте вручную прочитать числа начиная с предпоследней левой ноды нижнего левого угла, для каждой ноды слева – нода – справа.

Получится так:

  1. Предпоследняя нода слева внизу – 3.
  2. У нее есть левая ветвь – 1.
  3. Берем это число (1)
  4. Дальше берем саму вершину 3 (1, 3)
  5. Справа ветвь 6, но она содержит ветви. Поэтому ее прочитываем таким же образом.
  6. CЛева ветвь ноды 6 число 4 (1, 3, 4)
  7. Сама нода 6 (1, 3, 4, 6)
  8. Справа 7 (1, 3, 4, 6, 7)
  9. Идем наверх к корневой ноде – 8 (1,3, 4 ,6, 7, 8)
  10. Печатаем все что справа по аналогии
  11. Получаем итоговый список – 1, 3, 4, 6, 7, 8, 10, 13, 14

Чтобы реализовать алгоритм в коде потребуются две функции:

  1. Сборка бинарного дерева поиска
  2. Распечатка бинарного дерева поиска в правильно порядке

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

Пример на Lua:


function Node:new(value, lhs, rhs)
    output = {}
    setmetatable(output, self)
    self.__index = self  
    output.value = value
    output.lhs = lhs
    output.rhs = rhs
    output.counter = 1
    return output  
end

function Node:Increment()
    self.counter = self.counter + 1
end

function Node:Insert(value)
    if self.lhs ~= nil and self.lhs.value > value then
        self.lhs:Insert(value)
        return
    end

    if self.rhs ~= nil and self.rhs.value < value then
        self.rhs:Insert(value)
        return
    end

    if self.value == value then
        self:Increment()
        return
    elseif self.value > value then
        if self.lhs == nil then
            self.lhs = Node:new(value, nil, nil)
        else
            self.lhs:Insert(value)
        end
        return
    else
        if self.rhs == nil then
            self.rhs = Node:new(value, nil, nil)
        else
            self.rhs:Insert(value)
        end
        return
    end
end

function Node:InOrder(output)
    if self.lhs ~= nil then
       output = self.lhs:InOrder(output)
    end
    output = self:printSelf(output)
    if self.rhs ~= nil then
        output = self.rhs:InOrder(output)
    end
    return output
end

function Node:printSelf(output)
    for i=0,self.counter-1 do
        output = output .. tostring(self.value) .. " "
    end
    return output
end

function PrintArray(numbers)
    output = ""
    for i=0,#numbers do
        output = output .. tostring(numbers[i]) .. " "
    end    
    print(output)
end

function Treesort(numbers)
    rootNode = Node:new(numbers[0], nil, nil)
    for i=1,#numbers do
        rootNode:Insert(numbers[i])
    end
    print(rootNode:InOrder(""))
end


numbersCount = 10
maxNumber = 9

numbers = {}

for i=0,numbersCount-1 do
    numbers[i] = math.random(0, maxNumber)
end

PrintArray(numbers)
Treesort(numbers)

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

Ссылки

https://gitlab.com/demensdeum/algorithms/-/tree/master/sortAlgorithms/treesort

Источники

TreeSort Algorithm Explained and Implemented with Examples in Java | Sorting Algorithms | Geekific – YouTube

Tree sort – YouTube

Convert Sorted Array to Binary Search Tree (LeetCode 108. Algorithm Explained) – YouTube

Sorting algorithms/Tree sort on a linked list – Rosetta Code

Tree Sort – GeeksforGeeks

Tree sort – Wikipedia

How to handle duplicates in Binary Search Tree? – GeeksforGeeks

Tree Sort | GeeksforGeeks – YouTube

Bucket Sort

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

Временная сложность алгоритма O(nk). Алгоритм работает за линейное время для данных которые подчиняются равномерному закону распределения. Если говорить проще, то элементы должны быть в каком-то определенном диапазоне, без «вспесков», например числа от 0.0 до 1.0. Если среди таких чисел есть 4 или 999, то такой ряд по дворовым законам «ровным» уже не считается.

Пример реализации на Julia:

    buckets = Vector{Vector{Int}}()
    
    for i in 0:bucketsCount - 1
        bucket = Vector{Int}()
        push!(buckets, bucket)
    end

    maxNumber = maximum(numbers)

    for i in 0:length(numbers) - 1
        bucketIndex = 1 + Int(floor(bucketsCount * numbers[1 + i] / (maxNumber + 1)))
        push!(buckets[bucketIndex], numbers[1 + i])
    end

    for i in 0:length(buckets) - 1
        bucketIndex = 1 + i
        buckets[bucketIndex] = sort(buckets[bucketIndex])
    end

    flat = [(buckets...)...]
    print(flat, "\n")

end

numbersCount = 10
maxNumber = 10
numbers = rand(1:maxNumber, numbersCount)
print(numbers,"\n")
bucketsCount = 10
bucketSort(numbers, bucketsCount)

На производительность алгоритма также влияет число ведер, для большего количества чисел лучше взять большее число ведер (Algorithms in a nutshell by George T. Heineman)

Ссылки

https://gitlab.com/demensdeum/algorithms/-/tree/master/sortAlgorithms/bucketSort

Источники

https://www.youtube.com/watch?v=VuXbEb5ywrU
https://www.youtube.com/watch?v=ELrhrrCjDOA
https://medium.com/karuna-sehgal/an-introduction-to-bucket-sort-62aa5325d124
https://www.geeksforgeeks.org/bucket-sort-2/
https://ru.wikipedia.org/wiki/%D0%91%D0%BB%D0%BE%D1%87%D0%BD%D0%B0%D1%8F_%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0
https://www.youtube.com/watch?v=LPrF9yEKTks
https://en.wikipedia.org/wiki/Bucket_sort
https://julialang.org/
https://www.oreilly.com/library/view/algorithms-in-a/9780596516246/ch04s08.html