Ответы пользователя по тегу C++
  • Почему не срабатывает смещение при инстансинге?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Что такое инстансинг по своей сути?
    Вот у нас есть геометрия и параметры ее обработки. Речь только о стадии обработки вершин.
    Самая суть инстансинга в том, что геометрия у нас как бы находится в состоянии суперпозиции. Примитив у нас один, DIP тоже один, но в атрибутах примитива заложено что-то особенное, что заставляет увидеть его сразу во многих местах.
    Это самое особенное - это атрибуты инстанса.

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

    Поэтому самым первым этапом включения инстансинга будет именно новый буфер атрибутов в шейдере.
    layout (location = 0) in vec3 aPos;
    layout (location = 1) in vec2 aOffset;


    aOffset не должен быть uniform-ом. Это должен быть именно атрибут инстанса.
    Uniform-ами стоит делать другие вещи, не уникальные между инстансами. Uniform-ы для инстанса имеются сразу и все. Поэтому рационально в них держать только то, что разные инстансы используют совместно.
    Например - это могут быть цвета фракций или параметры костей скелетов в разных позах.
    Примерный код вершинного шейдера
    #version 460 core
    
    layout (location = 0) in vec3 aPos;
    layout (location = 1) in vec2 aOffset;
    
    void main()
    {
       gl_Position = vec4(aPos + aOffset, 0.0, 1.0);
    }


    Для указания того, что в буфере находятся атрибуты инстанса, следует использовать glVertexAttribDivisor[?].
    У тебя, скорее всего, это будет выглядеть так: glVertexAttribDivisor( 1, 6 );.
    А внутри буфера у тебя должно быть то, что сейчас записывается в glm::vec2 translations[100].
    Как примерно может заполняться буфер атрибутов инстанса
    glm::vec2 translations[100];
    int index = 0;
    float offset = 0.1f;
    for (int y = -10; y < 10; y += 2)
    {
    	for (int x = -10; x < 10; x += 2)
    	{
    		glm::vec2 translation;
    		translation.x = (float)x / 10.0f + offset;
    		translation.y = (float)y / 10.0f + offset;
    		translations[index++] = translation;
    	}
    }
    
    glBindBuffer(GL_ARRAY_BUFFER, VBOs[3]);
    glBufferData(GL_ARRAY_BUFFER, sizeof(translations), translations, GL_STATIC_DRAW);
    
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(glm::vec2), (GLvoid*)0);
    glVertexAttribDivisor(1, 6);
    glEnableVertexAttribArray(1);


    С этого момента инстансинг заведется как положено. Просто убираем uniform, меняем подготовку его значений на подготовку еще одного буфера вершин, добавляем код установки этого нового буфера, задействуем его значения в шейдере и все. Эту теорию тебе остается только адаптировать для своего кода.
    Чтобы разобраться почему у тебя конкретно сейчас рисуется неправильно, тебе нужно установить графический отладчик и проанализировать через него презентацию одного кадра. Советую поставить Renderdoc и научиться полноценно работать с ним. Без запуска твоего кода и анализа кадра выяснить причину неправильной работы практически невозможно.
    Ответ написан
  • Почему при арифметических операциях bool конвертируется в int?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    В данном случае int правильно выбран в качестве типа для result.

    Чтобы разобраться в логике этого поведения, нужно изучить т.н. технику продвижения целочисленных типов - Integral Promotion.
    В выражении x + y работает только она.

    В частности. Именно продвижение целых подразумевает неявное преобразование булева значения к целому со строго однозначным результатом: [expr.conv.6].
    true всегда в таких случаях будет преобразован в 1 с типом int.

    Конкретно для кода x + y типом результата значения будет именно int, потому что ранг short int ниже ранга int в правилах продвижения. Арифметические операции с значениями всех типов, чей ранг ниже int, всегда подвергаются продвижению и выполняются на значениях с типом int.

    Дело довершает auto result в левой части выражения. Обобщение принимает тип результата выражения справа от знака равенства, коим и является int.
    Иными словами, все это выражение было отдано на откуп продвижению целочисленных типов.
    Ответ написан
    Комментировать
  • Как работает определение дружественной функции внутри класса?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Перегрузка большинства операторов может быть произведена любым из двух способов: внешней функцией или методом класса. Лишь некоторые операторы невозможно перегрузить в виде внешней функции, их перегрузка осуществляется только в виде метода класса.
    Иными словами, все операторы можно перегрузить в виде метода и только некоторые - в виде внешней функции.
    Разница между перегрузкой в виде внешней функции и перегрузкой в виде метода состоит в том, что метод является частью интерфейса класса и имеет доступ к его непубличным объявлениям, а внешняя функция частью интерфейса не является и доступ имеет только к публичным объявлениям класса.

    Дружественность к перегрузке операторов не имеет никакого отношения. Перегрузка оператора в виде внешней функции и добавление дружественности для этой перегрузки выглядит как признак незнания стандарта в плане перегрузки операторов. Потому что в этом случае перегрузку нужно проводить в виде метода.
    Исключением являться может только определение перегрузки оператора по месту объявления дружественности. Я уже объяснял почему это может быть удобно.
    Такая перегрузка становится только ADL-доступной и не может быть вызвана через полную квалификацию имени.

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

    Это не так. У тебя неверное понимание. Наверное ты уже подзабыл мое прошлое объяснение по этому вопросу. Я рекомендую повторно обратиться к тому вопросу и ответу.
    friend std::ostream& operator<<(std::ostream& os, const Object & r) {/**/ return os;}

    Тут operator << все так же остается глобальной функцией, определенной в том же пространстве имен, где определен и тип Object, но не в пространстве имен типа Object. Но, будучи определенным по месту объявления дружественности, оператор стал только ADL-доступным. Обращение к этому оператору может быть найдено только тогда, когда в конструкции std::cout << obj; этот obj имеет тип Object.

    Но почему такая же логика не работает с обычной функцией?

    Минимально, потому что операторы никак нельзя сравнивать ни с глобальными функциями, ни с методами. У операторов своя отдельная методика вызова, отличная от функций.
    friend void fOUT (void) { }, опять же, является определением по месту объявления дружественности и доступна только через ADL. Но у нее нет аргументов чтобы ADL смог найти ее при обращении. Поэтому такая конструкция является бесполезной.
    Ответ написан
    Комментировать
  • [OpenGL]Почему получается такая фигура?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    GL_POLYGON предназначен для вывода только выпуклых многоугольников, а у тебя он выпуклым не является.

    Фигурки для тетриса рисовать стоит или в режиме GL_QUAD, или в режиме GL_TRIANGLES, явным образом выделяя отдельные квадратные участки.
    Так проще ориентироваться в точках.
    Ответ написан
    Комментировать
  • Как сделать простое окно Vulkan с отображением какого либо примитива?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    На gamedev.ru есть статьи по вулкану. В частности, там есть демонстрация самого минимального примера.
    На хабре есть много статей по вулкану. Среди них есть и уроки от самого начала работы, и некоторые минимальные примеры.
    У GLFW есть свои собственные материалы по работе с вулканом. А на их гитхабе есть прямо готовый пример для треугольника.
    Ответ написан
    5 комментариев
  • Где можно найти курс по разработке 3д игры на c++ и vulkan?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Низкий порог вхождения для C++ и Vulkan означает продвинутый уровень владения инструментом C++, экспертные навыки обработки графики, работы с шейдерами и управления памятью GPU (да, там все иначе). Для входа в работу с Vulkan нужно быть, как минимум, Middle Graphics Engineer и уже уметь уверенно работать с DirectX11 или OpenGL4.5. Без этих знаний вулкан будет очень сложно понять, а правильно работать с ним получится только через десятки и сотни полностью неудачных итераций написать одно и то же.

    Vulkan является очень низкоуровневым GAPI и требует от пользователя изначально серьезной подготовки. У этого GAPI много точек привязки к системной памяти, содержимое которой трактуется как на GPU, так и на CPU. Поэтому работать с памятью в C++ правильно нужно уметь с самого начала. Поэтому, еще до начала работы с вулканом от пользователя требуются экспертные знания языка. В противном случае вместо обучения работе с довольно сложным GAPI получится блуждание по полю граблей, где ничего не понятно.

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

    И тем не менее, вулкан легко не дастся. Для его понимания нужна база, нужно знать устройство GPU, принципы коммуникации с ним, принципы его работы и всю теорию обработки графики. Нужно уже уметь быстро писать много стабильного и сложного кода на C++, нужно уметь безошибочно писать на GLSL или SPIR-V. Нужно уметь пользоваться графическими отладчиками, профилировщиками, разбираться в диагностике проблем при работе с графикой.
    Все это приобрести можно в процессе практики с DirectX11 и OpenGL4.5.
    Ответ написан
    6 комментариев
  • Какая разница на практике между clang и gcc?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Однажды я на подобный вопрос уже отвечал, правда там вопрос был о кроссплатформенности, а не о трансляторах.

    Какая разница между clang и GCC.
    Разница большая. GCC обладает обширной поддержкой наследия идиом и конструкций из языка C, которые, вынужденно или по своей воле, поддерживает в современном C++.
    VLA, тип по умолчанию, всевозможные изыски синтаксиса C. Это все GCC не глядя принимает за C++ код и позволяет трансляцию.
    GCC даже сегодня многократно нарушает стандарты C++ просто потому что выбрал стратегию поддержки экзотической функциональности C в коде C++. Так же GCC не хвастается и скоростью поддержки стандартов C++.
    В 2016 году Google полностью отказались от поддержки GCC в Android NDK из-за слишком плохой поддержки стандартов и слишком свободного следования стандартам C++. В этот момент GCC стал неконкурентоспособным относительно оставшихся двух самых широко используемых трансляторов.
    Clang же, наоборот, сегодня считается, буквально, бастионом идеального следования стандартам C++. Clang точно поддерживает стандарты во всех деталях, максимально быстро интегрирует изменения и добавления стандартов, позволяет в самых первых рядах поиграться с функциональностью из драфтов следующего стандарта C++.
    Clang обладает обширной системой статической и динамической проверки кода: богатый статический анализ, возможность подключения санитайзеров, поддержка C++ Core Guidelines, очень качественные отчеты об ошибках трансляции, хорошая скорость трансляции.
    Это все ставит clang в предпочтение перед GCC на третьих для GCC платформах.

    О полной совместимости между трансляторами.
    Полная совместимость между трансляторами есть. Иначе я бы не мог делать то, что я делаю. А дело мое заключается в создании полностью кроссплатформенного кода, который однозначно собирается на всех целевых платформах и на всех них выполняется так же однозначно.
    Полная совместимость между трансляторами заключается в строгом соответствии кода выбранному стандарту C++. Всё, точка. На этом к трансляторам требования заканчиваются.
    Только тут есть небольшая проблема. Каждый транслятор по-своему поддерживает стандарт и по-своему реализует неоговоренные стандартом механики. Каждый транслятор имеет свои ошибки трансляции. И вскрывается это все именно в процессе работы над кроссплатформенным кодом.

    Я в своей работе видел многое. Я видел как при смене GCC на clang люди хватались за голову и отказывались от последнего просто потому что он нашел горы нарушений стандарта, которые молча принимал GCC. Я видел как группа из 5 человек 3 месяца рефакторила код при переходе с MSVS2015 на MSVS2017 (т.е. просто при смене версии транслятора) просто потому что разработчики из рук вон плохо знают используемый ими стандарт C++.
    Я видел ошибки в clang, приводящие к неверной генерации кода. Я видел ошибки в GCC, не позволяющие использовать его для кроссплатформенной сборки. Я видел ошибки в MSCL, в результате которых последний явно нарушает стандарт, а команда его разработки отказывается это исправлять потому что "иди нафиг".

    И, тем не менее, конкретно у меня есть возможность писать код ровно один раз и собирать его на 5 совершенно разных целевых платформ совершенно разными трансляторами, на которых этот код работает абсолютно равнозначно. Просто потому что я знаю стандарт и то, как этот стандарт поддерживают выбранные мной трансляторы.
    Ответ написан
    3 комментария
  • Является ли хорошим решением разбивать большой класс на несколько .cpp файлов (C++)?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Если дать короткий ответ, то всегда следует задуматься о декомпозиции класса в такой ситуации.

    Подобный твоему класс представляет из себя монолит - довольно распространенный примитив проектирования, попутно именуемый как "God Object". Объект, который может всё и от которого все вокруг зависят.
    Если появляется желание разбить реализацию интерфейса класса на несколько файлов, значит уже есть понимание того, как тематически декомпозировать этот класс и, вероятно, проблема остается только в том, чтобы правильно декомпозировать состояние класса.

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

    Если говорить развернуто.

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

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

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

    Общий шаблон такого разделения выглядит так. Чаще всего разработчики именуют файлы именем класса. Например MyClass.h и MyClass.cpp. Когда нужно тематически разделить определение интерфейса, к имени класса после точки и перед расширением файла добавляется суффикс, говорящий о тематике определения. Например MyClass.serialization.cpp, MyClass.crud.cpp или MyClass.callbacks.cpp.
    Ответ написан
    5 комментариев
  • Как отключить определение функции через шаблоны?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Не так важно просто включить или выключить метод исходя из аргументов шаблона типа, как важно объяснить пользователю типа, почему там метод доступен, а тут - нет.

    Для начала стоит разобраться со SFINAE, ведь именно этот механизм позволяет "выключать" определения в коде при особых условиях.
    Провал вывода шаблона не должен приводить к ошибке трансляции кода. Это - тонкий механизм, который всегда балансирует на грани между трансляцией и ошибкой. Когда нужно выключить определение функции, для нее всегда нужно предоставить альтернативу при иных условиях.
    В этой связи очень важно разделять вывод шаблона самой матрицы и вывод шаблонов методов матрицы.
    Проваливаться должны именно попытки вывода шаблонов методов матрицы. В самом буквальном смысле, метод будет выключаться для неквадратной матрицы потому что его не удалось вывести из шаблона.

    И вот как это должно выглядеть в идеале.
    template< size_t ROWS, size_t COLUMNS >
    struct Matrix final
    {
    	template< size_t R = ROWS, size_t C = COLUMNS >
    	inline std::enable_if_t<R == C, int> GetDeterminant() const
    	{
    		return 0;
    	}
    };


    Важно гарантировать зависимость выражения SFINAE от параметров шаблона метода. Поэтому объявление шаблона выглядит именно так, а не как-то еще.

    Теперь, что будет если GetDeterminant не удалось вывести из шаблона? Будет ошибка трансляции, говорящая о том, что метод не найден. Это ничего не говорит пользователю. Это просто инструктирует транслятор остановить трансляцию. Пользователь, особенно если он не искушен знаниями о SFINAE, не сможет понять причину ошибки трансляции. Такая ситуация создаст риск излишней траты времени на дознание причин ошибки.
    Под конец выяснится что матрица просто не квадратная.
    Пользователю нужно объяснить причину отсутствия метода.

    Есть простой способ сделать это.
    template< size_t ROWS, size_t COLUMNS >
    struct Matrix final
    {
    	template< size_t R = ROWS, size_t C = COLUMNS >
    	inline std::enable_if_t<R != C, int> GetDeterminant() const = delete;
    
    	template< size_t R = ROWS, size_t C = COLUMNS >
    	inline std::enable_if_t<R == C, int> GetDeterminant() const
    	{
    		return 0;
    	}
    };


    Альтернативный путь вывода всегда должен быть. Его отсутствие или приведет к ошибке трансляции, или усложнит понимание для пользователя. В данном случае явно удаленный метод должен подтолкнуть пользователя к верной причине. Сообщение об ошибке будет говорить о том, что пользователь пытается использовать удаленный GetDeterminant.

    Но есть способ вообще уйти от всех этих усложнений и крайне доходчиво донести до пользователя суть его ошибки.
    Дело в том, что методы выведенного из шаблона типа выводятся по мере их использования. Если никто не брал детерминант матрицы, то и метод взятия детерминанта для нее выведен не будет.
    А если матрица не является квадратной, сама попытка вывода метода взятия детерминанта должна быть пресечена.

    И делается это крайне просто.
    template< size_t ROWS, size_t COLUMNS >
    struct Matrix final
    {
    	inline int GetDeterminant() const
    	{
    		static_assert( ROWS == COLUMNS, "Matrix should be square to calculate the determinant." );
    		return 0;
    	}
    };


    SFINAE в решении вопроса вообще не нужен. Он только больше усложнит конструкцию и будет запутывать.
    Достаточно простого статического утверждения с максимально детальным пояснением для пользователя.
    Для всех квадратных матриц метод детерминанта будет выведен без проблем. Для любой неквадратной матрицы вывод метода детерминанта провалится на проверке статического утверждения, а пользователь получит максимально конкретную причину провала.
    Ответ написан
    Комментировать
  • Как правильно освобождать память?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Давай рассмотрим, для начала, строку под комментарием.

    ArrayArray[0] = *new Array<int>{10};
    Что тут происходит.
    ArrayArray[0] вернет ссылку на Array<int>.
    *new Array<int>{10} выделяет память в куче под Array<int>, вызывает инициализатор Array<int>::Array(int length), после чего делает разыменование получившегося указателя на Array<int>.
    После этого для подготовленной ссылки на Array<int> будет выполнен оператор копирования по умолчанию, функциональность которого просто скопирует поля из объекта справа в объект слева от присвоения.
    После этого результат new Array<int>{10} становится утекшим, т.к. указатель на него сразу становится потерян и освободить занимаемую им память становится невозможно.
    Именно поэтому с этой строчкой у тебя "всё отрабатывает нормально". И нет, всё у тебя не отрабатывает нормально потому что память утекла.

    Давай рассмотрим следующую строку.

    ArrayArray[0] = Array<int>{10};
    Что тут происходит.
    ArrayArray[0] вернет ссылку на Array<int>.
    Array<int>{10} инициализирует безымянный локальный объект на стеке, используя инициализатор Array<int>::Array(int length).
    После этого выполняется уже оператор перемещения по умолчанию, суть которого снова сводится к копированию полей из объекта справа в объект слева. После этого оба объекта ссылаются на одну и ту же выделенную память через поле T *m_data.
    Далее безымянный локальный объект уничтожается, освобождая недавно выделенную память. А ArrayArray[0] в этот момент начинает ссылаться на освобожденную память.

    Double deletion происходит тогда, когда ArrayArray в конце работы программы пытается удалить уже освобожденную память в ArrayArray[0].

    В твоем коде на лицо нарушение инварианта типа.
    Что нужно сделать.
    Тебе должно уже стать понятно что проблема в твоем коде связана с поведением операторов копирования и перемещения по умолчанию. Но проблема у тебя, на самом деле, значительно шире. Потому что завтра ты ведь точно захочешь еще и инициализацию копией провести или вернуть объект по значению из функции. В этом случае тебя тоже ждут проблемы.
    Решением твоих проблем будет соблюдение правила 3/5/0.
    Тебе нужно полноценно описать поведение объектов при копировании, перемещении, а так же при инициализации копией и инициализацией через перемещение.
    Ответ написан
    2 комментария
  • Внесение данных в std::vector< GLfloat >?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Вектор перед работой с его индексами необходимо привести к требуемому размеру.
    Делается это с помощью метода resize[?].
    После этого можно обращаться к значению по индексу напрямую.

    Если размер вектора уже определен и необходимо именно вставить данные по индексу, то воспользоваться можно методом insert[?].
    Однако, первым параметром метод требует не индекс, а итератор внутри вектора, куда требуется выполнить вставку. Этот итератор можно получить через смещение итератора начала вектора на требуемый индекс.
    vertexBuffer.insert( vertexBuffer.begin() + 1, x );

    При этом важно контролировать чтобы индекс вставки не выходил за пределы размера вектора.
    Забывать не стоит и о том, что при вставке велика вероятность реаллокации памяти под элементы вектора, в следствии чего уже все итераторы и ссылки на элементы вектора будут инвалидированы.
    Ответ написан
    1 комментарий
  • Можно ли объявить переменную-член класса с помощью метапрограммирования?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Соберем немного конкретики из комментариев.

    Отсюда:
    Только хотелось бы без специализации, чтобы код не дублировать

    Единственным дублированием кода тут будет только заголовок частичной специализации шаблона. В силах писателя определить общий между специализациями код и вынести его в родительский для всех специализаций класс.
    А с подобным дублированием справляется шаблон std::enable_if[?]. Часто его используют для выбора поведения шаблона исходя из черт аргумента шаблона.

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

    Отсюда:
    соответственно во всех методах, где этот член используется можно будет проверку if constexpr (someCondition)

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

    За чистотой кода нужно следить. Подобные вещи, как вот такие блоки if constexpr, нужно убирать в функции. А где эти функции лучше расположить? Верно - прямо там, где для их работы определены данные. Поэтому на самом деле if constexpr не нужен. Нужно определить набор функций с поведением там где оно возможно. А где нет - определить заглушки чтобы клиентский, относительно вариативного поведения, код не нуждался в проверках и мог просто обращаться к вариативному поведению так, как будто оно не вариативно и есть всегда.
    Опять же, тут хорошо подходит пример с DebugName[?], в котором такие заглушки реализованы.
    Ответ написан
    1 комментарий
  • Assignment operator VS Destructor + Placement new, где аргумент placement new - prvalue?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    К современному коду предъявляется немало требований. В частности, код должен отвечать определенным критериям качества чтобы иметь шансы на добавление в основной репозиторий в качестве решения задачи.
    Среди таких критериев качества нередко бывают: соответствие общему стилю кода, соответствие общему стилю форматирования, наличие документации или самодокументируемость, а так же - элементарная понятность для читателя.

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

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

    *reinterpret_cast<T*>( &buffer[ sizeof( T ) * head ] ) = T{ std::forward<Args>( args )... };

    Да, тут присутствует Value Initialization и оператор перемещения. Однако, оптимизация этого кода сведет все к довольно короткому и как можно более быстрому коду. Поэтому не стоит беспокоиться об этом на текущем этапе. Лучше беспокоиться о понятности кода для читателя.
    Столь же понятным будет и такой код:
    auto T vicar{ std::forward<Args>( args )... };
    std::swap( *reinterpret_cast<T*>( &buffer[ sizeof( T ) * head ] ), vicar );

    Он не будет вызывать много вопросов, разве только вопрос относительно использования std::swap, но только от людей со слабой привычкой пользоваться STL.

    Однако, такой код может привести к ошибке трансляции в том случае, если T является неперемещаемым. В твоем ЦБ должны присутствовать проверки на перемещаемость, копируемость и возможность размена состояний.
    Если T можно копировать, но не перемещать, использовать стоит оператор копирования.
    И только если ни копировать, ни перемещать T нельзя, следует пользоваться деструктором и размещающим конструктором.

    Я бы рекомендовал все три действия оформить в виде перегрузок шаблона функции со SFINAE, чтобы для любого T включалась только одна конкретная перегрузка шаблона.
    Ответ написан
    6 комментариев
  • Как разрабатывать игру на c++ под Android?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Самым простым решением для разработки именно игры будет взять Cocos2d-x, Defold или Godot.
    Это уже готовые решения для разработки игр на различные платформы, в том числе и на Android.
    Все движки предоставляют инструменты именно для С++, в отличие от SDL.

    SDL выполнен на языке C и не является подходящим в контексте разработки на C++.

    Если есть желание заняться именно разработкой движка, а не игры, то вместо SDL стоит взять SFML, который выполнен уже на C++ и является довольно хорошим фреймворком. На базе SFML можно довольно быстро сделать небольшой движок для небольшой игры.
    Ответ написан
    2 комментария
  • Какое связывание у namespace, определённёго в области видимости файла .cpp?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Файлы с расширением .cpp обычно являются точками сборки модулей трансляции [?].
    Характеристики связывания имеют свой эффект только между модулями трансляции. Компоновщик занимается связыванием кода и работает с результатами обработки именно модулей трансляции - объектными файлами.
    Поэтому, если в одном .cpp подключить другой .cpp (исключенный из сборки иными способами), то все элементы с внутренним связыванием любого из этих .cpp будут доступны в них обоих.
    Это будет справедливо и для файлов с расширением .h. Файлы .cpp обычно включают .h и все вместе своим кодом формируют модуль трансляции, в котором доступны все элементы с внутренним связыванием. Даже в коде файлов .h.
    Это важно учитывать чтобы не совершать некоторых ошибок.

    По существу вопроса. Все нестатические (без спецификатора static) элементы именованных пространств имен по умолчанию имеют внешнее связывание [?]. Глобальное пространство является тоже именованным (и доступно через :: без имени слева) и ровно так же наделяет все свои нестатические элементы характеристикой внешнего связывания.

    В противоположность этому, абсолютно все элементы анонимных пространств имен (даже элементы вложенных именованных пространств) имеют характеристику внутреннего связывания [?].
    Довольно распространенной ошибкой является определение анонимных пространств и статических элементов пространств в заголовках, после чего сразу множество модулей трансляции пополняются кодом с внутренним связыванием. Это приводит к разбуханию бинарного кода и усложнению сборки.

    Отдельно хотелось бы обозначить inline.
    Спецификатор inline[?] дает пометку слабого внешнего связывания для любой сущности. Что константа или глобальная переменная, что функция или метод (даже статический), помечаются как сущности с внешним связыванием, которое не нарушает ODR в случае если все определения цели связывания во всех модулях трансляции являются полностью одинаковыми. Если хоть одно определение цели связывания отличается - будет нарушение ODR.
    Компоновщик подберет самое первое определение и встроит его в бинарный код. После этого компоновщик будет только проверять другие встреченные определения на предмет соответствия первому, а линковку кода будет производить только относительно первого встреченного определения.
    Поэтому, если в любом именованном пространстве, в нескольких .cpp определить inline функции с одним именем и одной сигнатурой, но разными телами, то проблем со сборкой будет не избежать.

    Все это можно более детально изучить в статье на хабре.
    Ответ написан
    Комментировать
  • В разных IDE код выдаёт разный ответ, как так?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Это тот самый случай, когда с виду простой код заставляет разобраться во множестве тонкостей языка.
    Для лучшего понимания проходящих в коде процессов сперва требуется внимательно присмотреться к стандарту языка.

    Что стандарт говорит нам о перегрузке операторов?
    A declaration whose declarator-id is an operator-function-id shall declare a function or function template or an explicit instantiation or specialization of a function template. A function so declared is an operator function.

    cout << a.get() << b.get();
    Данный код маскирует два вызова одной функции - std::ostream& operator << ( std::ostream&, int ).

    Тут очень важно уточнить форму перегрузки оператора, т.к. результат поведения этого кода сильно зависит от применяемой формы перегрузки.
    Относительно стандартной формы перегрузки operator << () стандарт говорит что это перегрузка в форме внешней функции.

    Значит приведенный код можно записать как:
    operator<<( operator<<( cout, a.get() ), b.get() );


    И именно с этого момента начинается самое интересное.

    Что стандарт говорит нам о вызове функций? А говорит он совсем разные вещи.
    C++14 [expr.call#5.2.2.8] заявляет, что:
    The evaluations of the postfix expression and of the arguments are all unsequenced relative to one another. All side effects of argument evaluations are sequenced before the function is entered (see 1.9).

    C++17 [expr.call#8.2.2.5] утверждает, что:
    If an operator function is invoked using operator notation, argument evaluation is sequenced as specified for the built-in operator; see 16.3.1.2.

    В результате, если транслировать данный код как код 14-го (или старших) стандарта, поведение у этого кода будет одно. Если же код транслировать как код 17-го (и моложе) стандарта, его поведение будет будет уже другим.

    А что же там с вероятным неопределенным поведением? Ведь неупорядоченная модификация состояния является UB. И, вроде как, cout << a.get() << b.get(); можно упростить до cout << ++i << ++i;, что уже более явно должно показывать наличие UB.
    UB в этом коде нет. И вот почему.

    Для определения порядка вычисления участков выражения следует руководствоваться правилами упорядочивания выражений.
    Среди прочих правил там записаны важные для нас сейчас. Я приведу цитаты.
    2) The value computations (but not the side-effects) of the operands to any operator are sequenced before the value computation of the result of the operator (but not its side-effects).

    3) When calling a function (whether or not the function is inline, and whether or not explicit function call syntax is used), every value computation and side effect associated with any argument expression, or with the postfix expression designating the called function, is sequenced before execution of every expression or statement in the body of the called function.

    5) The side effect of the built-in pre-increment and pre-decrement operators is sequenced before its value computation (implicit rule due to definition as compound assignment)


    16) Every overloaded operator obeys the sequencing rules of the built-in operator it overloads when called using operator notation. (since C++17)

    19) In a shift operator expression E1<<E2 and E1>>E2, every value computation and side-effect of E1 is sequenced before every value computation and side effect of E2. (since C++17)


    До C++17 порядок вычисления операндов cout << a.get() << b.get(); не определен, но поведение этого кода определено. Поэтому при трансляции по стандарту C++14 этот код может выдать или 12, или 21. Но не 11.
    Начиная с C++17 порядок вычисления операндов строго определен и является интуитивным, а результат выполнения cout << a.get() << b.get(); всегда однозначен. При трансляции этого кода по стандарту C++17 (и дальше) в консоль будет выведено всегда и только 12.
    До C++11 поведение кода cout << a.get() << b.get(); не определено.

    Сегодня мы уже не задумываемся о жизни до стандарта C++11, поэтому я не скажу что в общем смысле в этом коде присутствует UB. Я скажу что UB тут нет. Но тем не менее, я бы рекомендовал избегать присутствия подобного кода в проектах даже если используется стандарт C++17 и дальше.
    Ответ написан
    Комментировать
  • В чем смысл определения const int &ref=1;?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Для начала стоит немного пробежаться по категориям выражений в современном C++.
    Из статьи будет видно что 1 имеет категорию prvalue. 1 является литералом и не является строковым литералом. Это - литерал с типом int по умолчанию.

    Если снова обратиться к статье и посмотреть свойства prvalue, то будет видно, в частности, что prvalue:
    • имеют всегда полный тип;
    • не имеют размещения, а следовательно не имеют адреса в памяти;
    • могут использоваться для инициализации cvq-lvalue-ref и rvalue-ref.

    Последний пункт говорит о том, что код const int& ref = 1; или int&& ref = 1; является полностью стандартным.
    ref в этом случае будет ссылаться на переданный литерал и отсутствие размещения литерала этому не помеха.

    Смысл подобных выражений в том чтобы позволить написать такой код.
    void foo( const int& ref );
    
    void bar()
    {
      foo( 1 );
    }


    Смыл конкретно кода const int &ref=1; можно найти в том, чтобы не писать магические константы в коде. ref - очень плохое имя. Но голая 1 в коде еще хуже.
    Инженер ПО свой код пишет не для транслятора, а для своих сотрудников. Транслятор разберется в любом коде и по любому синтаксически верному коду всегда произведет работающий исполняемый код. Но другие люди в коде смогут разобраться только тогда, когда этот код написан доступным для понимания образом.

    Чтобы код было легче понять, его документируют. Документация бывает разная. Это могут быть комментарии, это может быть отдельный документ. Но лучше всего код понимается тогда, когда он "самодоукментируется" [1], [2], т.е. когда сам код однозначно поясняет цели своего существования и принципы своей работы.
    Чтобы код сам себя пояснял, имена функций, переменных и констант, имена типов и прочих рукописных конструкций должны как можно более ясно отражать цели своего существования. В частности, чтобы код не пестрил безликими магическими числами [?], эти числа принято обличать в т.н. поясняющие константы.
    Такую константу можно определить как cvq-lvalue-ref, назначить понятное в контексте кода имя и присвоить ей требуемый литерал.
    Ответ написан
    Комментировать
  • Что значит ссылка без типа?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    const &y = x;
    Это не ссылка без типа, а синтаксически неверный код, который не пройдет трансляцию за пределами GCC.

    Т.к. ты пользуешься GCC, тебе стоит принять во внимание то, что он не соблюдает стандарт в некоторых случаях.
    В частности - в данном случае.

    В C++ нет типа по умолчанию, в отличие от С, где типом по умолчанию является int. Если в C код const y = x является синтаксически верным и подразумевает const int y = x, то в C++ этот же код является уже синтаксически неверным и не пройдет трансляцию.
    GCC в твоем коде отходит от стандарта C++ в пользу поведения как в C.
    Ответ написан
    2 комментария
  • Почему сплайны OpenGL прорисовываются не на всех компьютерах?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Дело в том, что функция glMap1[?] имеет довольно узкий коридор поддержки. Она была введена в OpenGL 1.0 и удалена из поддержки в OpenGL 3.2 Core Profile.

    Т.к. ты пользуешься wglCreateContext, система сама решает какой контекст ей для тебя создавать. Это может быть и контекст с версией 4.6, в котором уже нет поддержки функции glMap1f.
    Тебе стоит более точно указывать версию создаваемого контекста. Это можно сделать с помощью расширения WGL_ARB_create_context. Функция wglCreateContextAttribsARB позволяет задавать атрибуты для создаваемого контекста, среди которых ты можешь обозначить и требуемую версию.

    В качестве примера использования этого расширения можно взять такой код.
    Код примера
    // Sample code showing how to create a modern OpenGL window and rendering context on Win32.
    
    #include <windows.h>
    #include <gl/gl.h>
    #include <stdbool.h>
    
    typedef HGLRC WINAPI wglCreateContextAttribsARB_type(HDC hdc, HGLRC hShareContext,
            const int *attribList);
    wglCreateContextAttribsARB_type *wglCreateContextAttribsARB;
    
    // See https://www.khronos.org/registry/OpenGL/extensions/ARB/WGL_ARB_create_context.txt for all values
    #define WGL_CONTEXT_MAJOR_VERSION_ARB             0x2091
    #define WGL_CONTEXT_MINOR_VERSION_ARB             0x2092
    #define WGL_CONTEXT_PROFILE_MASK_ARB              0x9126
    
    #define WGL_CONTEXT_CORE_PROFILE_BIT_ARB          0x00000001
    
    typedef BOOL WINAPI wglChoosePixelFormatARB_type(HDC hdc, const int *piAttribIList,
            const FLOAT *pfAttribFList, UINT nMaxFormats, int *piFormats, UINT *nNumFormats);
    wglChoosePixelFormatARB_type *wglChoosePixelFormatARB;
    
    // See https://www.khronos.org/registry/OpenGL/extensions/ARB/WGL_ARB_pixel_format.txt for all values
    #define WGL_DRAW_TO_WINDOW_ARB                    0x2001
    #define WGL_ACCELERATION_ARB                      0x2003
    #define WGL_SUPPORT_OPENGL_ARB                    0x2010
    #define WGL_DOUBLE_BUFFER_ARB                     0x2011
    #define WGL_PIXEL_TYPE_ARB                        0x2013
    #define WGL_COLOR_BITS_ARB                        0x2014
    #define WGL_DEPTH_BITS_ARB                        0x2022
    #define WGL_STENCIL_BITS_ARB                      0x2023
    
    #define WGL_FULL_ACCELERATION_ARB                 0x2027
    #define WGL_TYPE_RGBA_ARB                         0x202B
    
    static void
    fatal_error(char *msg)
    {
        MessageBoxA(NULL, msg, "Error", MB_OK | MB_ICONEXCLAMATION);
        exit(EXIT_FAILURE);
    }
    
    static void
    init_opengl_extensions(void)
    {
        // Before we can load extensions, we need a dummy OpenGL context, created using a dummy window.
        // We use a dummy window because you can only set the pixel format for a window once. For the
        // real window, we want to use wglChoosePixelFormatARB (so we can potentially specify options
        // that aren't available in PIXELFORMATDESCRIPTOR), but we can't load and use that before we
        // have a context.
        WNDCLASSA window_class = {
            .style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC,
            .lpfnWndProc = DefWindowProcA,
            .hInstance = GetModuleHandle(0),
            .lpszClassName = "Dummy_WGL_djuasiodwa",
        };
    
        if (!RegisterClassA(&window_class)) {
            fatal_error("Failed to register dummy OpenGL window.");
        }
    
        HWND dummy_window = CreateWindowExA(
            0,
            window_class.lpszClassName,
            "Dummy OpenGL Window",
            0,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            0,
            0,
            window_class.hInstance,
            0);
    
        if (!dummy_window) {
            fatal_error("Failed to create dummy OpenGL window.");
        }
    
        HDC dummy_dc = GetDC(dummy_window);
    
        PIXELFORMATDESCRIPTOR pfd = {
            .nSize = sizeof(pfd),
            .nVersion = 1,
            .iPixelType = PFD_TYPE_RGBA,
            .dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
            .cColorBits = 32,
            .cAlphaBits = 8,
            .iLayerType = PFD_MAIN_PLANE,
            .cDepthBits = 24,
            .cStencilBits = 8,
        };
    
        int pixel_format = ChoosePixelFormat(dummy_dc, &pfd);
        if (!pixel_format) {
            fatal_error("Failed to find a suitable pixel format.");
        }
        if (!SetPixelFormat(dummy_dc, pixel_format, &pfd)) {
            fatal_error("Failed to set the pixel format.");
        }
    
        HGLRC dummy_context = wglCreateContext(dummy_dc);
        if (!dummy_context) {
            fatal_error("Failed to create a dummy OpenGL rendering context.");
        }
    
        if (!wglMakeCurrent(dummy_dc, dummy_context)) {
            fatal_error("Failed to activate dummy OpenGL rendering context.");
        }
    
        wglCreateContextAttribsARB = (wglCreateContextAttribsARB_type*)wglGetProcAddress(
            "wglCreateContextAttribsARB");
        wglChoosePixelFormatARB = (wglChoosePixelFormatARB_type*)wglGetProcAddress(
            "wglChoosePixelFormatARB");
    
        wglMakeCurrent(dummy_dc, 0);
        wglDeleteContext(dummy_context);
        ReleaseDC(dummy_window, dummy_dc);
        DestroyWindow(dummy_window);
    }
    
    static HGLRC
    init_opengl(HDC real_dc)
    {
        init_opengl_extensions();
    
        // Now we can choose a pixel format the modern way, using wglChoosePixelFormatARB.
        int pixel_format_attribs[] = {
            WGL_DRAW_TO_WINDOW_ARB,     GL_TRUE,
            WGL_SUPPORT_OPENGL_ARB,     GL_TRUE,
            WGL_DOUBLE_BUFFER_ARB,      GL_TRUE,
            WGL_ACCELERATION_ARB,       WGL_FULL_ACCELERATION_ARB,
            WGL_PIXEL_TYPE_ARB,         WGL_TYPE_RGBA_ARB,
            WGL_COLOR_BITS_ARB,         32,
            WGL_DEPTH_BITS_ARB,         24,
            WGL_STENCIL_BITS_ARB,       8,
            0
        };
    
        int pixel_format;
        UINT num_formats;
        wglChoosePixelFormatARB(real_dc, pixel_format_attribs, 0, 1, &pixel_format, &num_formats);
        if (!num_formats) {
            fatal_error("Failed to set the OpenGL 3.3 pixel format.");
        }
    
        PIXELFORMATDESCRIPTOR pfd;
        DescribePixelFormat(real_dc, pixel_format, sizeof(pfd), &pfd);
        if (!SetPixelFormat(real_dc, pixel_format, &pfd)) {
            fatal_error("Failed to set the OpenGL 3.3 pixel format.");
        }
    
        // Specify that we want to create an OpenGL 3.3 core profile context
        int gl33_attribs[] = {
            WGL_CONTEXT_MAJOR_VERSION_ARB, 3,
            WGL_CONTEXT_MINOR_VERSION_ARB, 3,
            WGL_CONTEXT_PROFILE_MASK_ARB,  WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
            0,
        };
    
        HGLRC gl33_context = wglCreateContextAttribsARB(real_dc, 0, gl33_attribs);
        if (!gl33_context) {
            fatal_error("Failed to create OpenGL 3.3 context.");
        }
    
        if (!wglMakeCurrent(real_dc, gl33_context)) {
            fatal_error("Failed to activate OpenGL 3.3 rendering context.");
        }
    
        return gl33_context;
    }
    
    static LRESULT CALLBACK
    window_callback(HWND window, UINT msg, WPARAM wparam, LPARAM lparam)
    {
        LRESULT result = 0;
    
        switch (msg) {
            case WM_CLOSE:
            case WM_DESTROY:
                PostQuitMessage(0);
                break;
            default:
                result = DefWindowProcA(window, msg, wparam, lparam);
                break;
        }
    
        return result;
    }
    
    static HWND
    create_window(HINSTANCE inst)
    {
        WNDCLASSA window_class = {
            .style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC,
            .lpfnWndProc = window_callback,
            .hInstance = inst,
            .hCursor = LoadCursor(0, IDC_ARROW),
            .hbrBackground = 0,
            .lpszClassName = "WGL_fdjhsklf",
        };
    
        if (!RegisterClassA(&window_class)) {
            fatal_error("Failed to register window.");
        }
    
        // Specify a desired width and height, then adjust the rect so the window's client area will be
        // that size.
        RECT rect = {
            .right = 1024,
            .bottom = 576,
        };
        DWORD window_style = WS_OVERLAPPEDWINDOW;
        AdjustWindowRect(&rect, window_style, false);
    
        HWND window = CreateWindowExA(
            0,
            window_class.lpszClassName,
            "OpenGL",
            window_style,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            rect.right - rect.left,
            rect.bottom - rect.top,
            0,
            0,
            inst,
            0);
    
        if (!window) {
            fatal_error("Failed to create window.");
        }
    
        return window;
    }
    
    int WINAPI
    WinMain(HINSTANCE inst, HINSTANCE prev, LPSTR cmd_line, int show)
    {
        HWND window = create_window(inst);
        HDC gldc = GetDC(window);
        HGLRC glrc = init_opengl(gldc);
    
        ShowWindow(window, show);
        UpdateWindow(window);
    
        bool running = true;
        while (running) {
            MSG msg;
            while (PeekMessageA(&msg, 0, 0, 0, PM_REMOVE)) {
                if (msg.message == WM_QUIT) {
                    running = false;
                } else {
                    TranslateMessage(&msg);
                    DispatchMessageA(&msg);
                }
            }
    
            glClearColor(1.0f, 0.5f, 0.5f, 1.0f);
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
            // Do OpenGL rendering here
    
            SwapBuffers(gldc);
        }
    
        return 0;
    }
    Ответ написан
    Комментировать
  • Выбор игрового движка для C++?

    @MarkusD Куратор тега C++
    все время мелю чепуху :)
    Много решений подходит под такие критерии. Смотри, изучай, выбирай.

    Cocos2d-x является одним из самых популярных открытых движков. У него большое сообщество и масса поклонников. Есть документация и все нужное для старта.

    Defold является еще одним очень популярным кроссплатформенным и открытым решением для разработки 2D игр. Сообщество тоже очень большое, документация и уроки для начинающих тоже на месте.
    Defold часто выбирают в качестве решения для своей первой игры. И этот выбор далеко не случаен.

    Godot Engine не менее популярен и не менее поднят по возможностям. В чем-то Godot даже будет лучше чем Cocos. Сообщество у него тоже большое. Документация тоже присутствует.

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

    Дальше пойдут не такие популярные решения, однако и проходить мимо них тоже не стоит.

    Urho3D является нареченной Open-Source альтернативой Unity. Движок используется многими энтузиастами. По разным уголкам сети раскиданы многочисленные группы обсуждения этого движка. Документация и примеры у него на месте.

    GDevelop - это довольно популярное решение для небольших игр. Документация на месте.

    Panda3D - тоже довольно популярное решение со своим сообществом. Документация имеется.

    Hazel Engine - один разработчик - один движок. Полностью вся разработка изложена в видео на youtube. Пользоваться можно... на свой страх и риск.

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

    GZDoom - современная инкарнация движка DOOM.

    Дальше решения пойдут или сложные, или экзотические. Всё на свой страх и риск.

    CryEngine - от Crytek.
    X-Ray - движок S.T.A.L.K.E.R.
    UE 3 - для коммерческих проектов использовать нельзя.
    Lumberyard - от Amazon. Да-да, тот самый.
    Banshee Engine - он просто существует.
    Diligent Engine - у него есть свое сообщество.
    Atomic Engine - на нем тоже выпускают игры.
    Lumix Engine - тоже что-то может.
    Horde 3D - просто существует и этого уже достаточно.
    Ответ написан
    Комментировать