Как стать автором
Обновить

Комментарии 128

  1. А почему у вас в основном const T в аргументах функций? Это имеет какое-то преимущество для POD? Для std::string, вообще не понятен смысл данной манипуляции - уже или просто по значению, или по константной ссылке, если нет желания погружаться в move и т.п.

  2. Конструкции типа : for (int j=0; j<h; j++) for (int i=0; i<w; i++) {

    Очень error-prone, понятно, что экономится 1 строка, но стоит ли оно того?

  3. int(x) а не (int) x? Зачем?

  4. if (dt>=.3)?

    Статья, в принципе, интресная, но вот обучать людей надо начинать с нормального стиля и подхода, особенно, если плюсам, ИМХО.

А так, полно мелочей, которые попахивают и портят впечатление от отличной задумки для первого проекта в учебном году.

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

Да и вообще, пулл реквест в студию :)

Теперь по пунктам:

  1. Я привык ставить const в аргументах, чтобы только по заголовку было понятно, что есть вход, что есть выход. Конечно же, никаких особых преимуществ это для аргумента get(i, j) не даёт (кроме читабельности).

  2. Ой, ставить скобки или нет - это см. чувство прекрасного. Особой экономии строк я не добивался.

  3. А зачем писать (int)x? Почему не static_cast<int>(x)

  4. Не понял в чём проблема с ифом. Поясните?

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

uint64_t - и запись короче, и длина в битвах указана.

Ну вот у вас то есть скобки, то нет.

Отсутствие скобок - ведет к усложнению чтения кода, не более того. Ну и, возможно, к проблемам с анализом кода в некоторых IDE.

Как пример - CLion с питоновской «ошибкой» в плюсовом проекте.

Про int - вы сами попробуйте объяснить почему изначально не cast (c-style или static), мне интересно какие преимущества оно даёт? Такой вопрос студенты не задают?

Насколько я знаю, static_cast<> в теории преимущества даёт, но в данном случае никаких. Что из трёх вариантов выбирать - это чисто вопрос стиля, и ваш си-каст ничем не выделяется.

Я что-то не понимаю, почему я должен отбиваться от вашего требования использовать си-каст. Может, вообще переписать код, чтобы не возникало приведения типов? Почему именно си-каст?

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

Ну и последнее, почему я должен писать свой код так, чтобы не нарываться на баги CLion? :)

"Я привык ставить const в аргументах, чтобы только по заголовку было понятно, что есть вход, что есть выход. Конечно же, никаких особых преимуществ это для аргумента get(i, j) не даёт (кроме читабельности)."

Отличная мотивация, а для меня - удобный момент снова разобрать элементы общих рекомендации по типам аргументов функций, заглянув в раздел Functions C++ Core Guidelines (и конкретно пересмотрев рекомендации F.15 и F.16).

Начнём с того, что наиболее простой способ передачи данных в функцию и получения результатов - передача по значению (тип аргумента T) и получение всех выходов функции только в возвращаемом значении (результате) - который при множественности результатов становится композитным, например, пара, кортеж (tuple) или простая структура (лучший вариант, на мой взгляд, благодаря содержательным названиям полей). Современный C++ предоставляет удобную деконструкцию композитного результата в вызывающем контексте, нампример auto [ x, y, z ] = get_coordinates(a); (этот приём используется в фрагментах кода к C++ Core Guidelines I.10, F.21, ES.10, ES.11, ES.20).

Недостаток: если тип T - достаточно крупного размера, копирование значения при передаче по значению может сказаться на производительности (что всегда не мешает проверить замерами). В этом случае помогает ссылка на константное значение, const T& - заметьте присутствие '&'. Только при наличии & появляется риск изменить внутри функции внешнее значение, и const помогает это предотвратить, а также просигналить о намереньях в сигнатуре функции: этот const T& параметр - входной, с той же подразумеваемой семантикой, как передача по значению (просто T), а ссылочность заведена для оптимизации производительности.

Поставив const T (без '&') при передаче по значению вы не меняете семантику отношений вызывающего кода с функцией - и так и так параметр, передаваемый по значению, только входной (in). В этом случае const ограничивает набор действий с параметром в реализации функции (вы не сможете обновить его значение, это сходно константным локальным переменный вроде sprite_w и sprite_h в одном из фрагментов вашего кода. Лично я рассматриваю такую типизацию как неполезную - добавляет ненужный шум в сигнатуру, накладывает ограничение на реализующий функцию код (иногда приводящее к более длинной и менее читабельной форме этого кода).

При передаче по ссылке ('&') спецификатор const действительно важен! Без него функция имеет право поменять значение переданной переменной в вызывающем контексте, таким образом параметр становится входным/выходным (in-out, описанный в рекомендации F.17) -- чего лично я стараюсь избегать, предпочитая все выходы функции возвращать в результате.

Кроме этого усложнения передача по ссылке может привнести подводные камни, так что прибегать к ней имеет смысл только убедившись, что копирование крупного значения T действительно влияет на производительность. Здесь я имею в виду возможные проблемы с aliasing. При получинии параметра const T& код функции может резонно положиться на соглашение, что данный параметр - входной, его значение в момент вызова функции таковым и остаётся до момента выхода из функции. Однако, если функция обращается к другим функциям, имеющим (другим путём) доступ к той же переменной в вызывающем контексте, это значение неожиданно может поменяться. С этой проблемой можно бороться, предотвращая aliasing с помощью &&, но, увы, такой подход усложняет код.

Комментарий длинный и полезный, но в рамках текущей беседы малоприменимый. Чем мешает конкретно в моём случае объявление const? Доведём до абсурда: нужно ли переменную-член width класса спрайт обозначать как const?

Касательно structural binding auto [x,y,z] = foo(), это фича C++17, что, конечно, хорошо, но есть два тонких момента:

  1. Не везде есть компилятор, поддерживащий этот стандарт (да, я знаю, что на дворе 2021й год, и что конкретно в данном проекте у меня зачем-то c++17 прописан в cmakelists :) )

  2. Мы сильно полагаемся на то, что оптимизатор компилятора уберёт все эти tuple, и запихнёт возвращаемое значение в регистры какие-нибудь. Чем это лучше усложения сигнатуры при добавлении constвходному параметру? (при этом я не уверен, что сигнатура усложняется, и оптимизатор уж точно любит const и особенно constexpr)

Я достаточно наработался с кодом, где объект передаётся по значению, а внутри там спрятан какой-нибудь хитрый shared_ptr,поэтому у меня просто рефлекс ставить const на то, что изменяться не должно. Да и вообще не вижу смысла писать код, который изменяет значение целочисленного аргумента, переданного по значению. Это точно ухудшение читабельности...

Жду претензий к названиям переменных :)

Нет у меня претензий к названиям переменных (мне даже в голову не пришло бы начинать разговоры на эту тему). Я рад, что мой длинный комментарий показался вам полезным. Не уверен, что вы сочтёте ещё один относительно длинный комментарий ниже применимым в рамках текущей беседы; печатаю его, опять же, чтобы ещё немного порефлексировать.

Моё отношение к coding guidelines довольно прагматично. Я открыт к кодификации в coding guidelines сложившегося по тем или иным причинам стиля, но мне важна внутренняя непротиворечивость такого стиля. Обсуждая с коллегами аспекты вроде рассматриваемого здесь (про желательность / нежелательность спецификатора const при объявлении локальных переменных: параметры -- локальные переменные в реализации функции) я выясняю их мотивы и подталкиваю к рефлексии на тему, насколько логичны их привычки? В случае с вашей привычкой некая поименованная величина в теле функции может оказаться констатной только из-за того, что она -- параметр, а не локальная переменная. Вы не находите сочетание строгости в отношении одних локальных переменных (параметров функции) и расслабленность в отношении других локальных переменных (не-параметров) нелогичным?

Я несколько лет назад задал самому себе такой вопрос и решил, что у меня, как у сторонника логичных самосогласованных практик, два варианта -- писать const по возможности везде (в том числе при объявлении локальных переменных и да, членов классов -- во имя самосогласованности данного правила) или писать const только там, где этот спецификатор реально детализирует контракт (как в типах параметров const T&). Порефлексировал и принял выбор в пользу второго варианта. Подтверждение нахожу в стандартной библиотеке и других пакетах, коде коллег -- поэтому удивился, увидев типа параметров const T у вас (для меня это -- ухудшение читабельности, глаз запнулся), после чего напечатал предыдущий длинный комментарий.

Упомянутый вами случай с shared_ptr, конечно, прискорбен (я, безусловно, предпочитаю unique_ptr и проработку дизайна компонент так, чтобы необходимости использования shared_ptr не было). Опять же, порефлексировав некоторое время назад, я пришёл к практике инкапсуляции деталей реализации сколь-либо сложного класса в pimpl (последнее время использую zero-cost "fast pimpl" после того, как написал поддерживающий fast impl темплейтный код, до этого использовал pimpl на unique_ptr) и запрете (= delete;) у него copy costructor, copy assignment operator. Тогда передача по значению такого сколь-нибудь сложного класса оказывается намеренно запрещена (чтобы не платить за копирование), для использования класса в функции передаётся const T& (отлично) или T& (что сигналит в сигнатуре-контракте о мутациях параметра, выполняемых в функции, о коде становится сложнее рассуждать -- цена за предполагаемую или доказанную замерами оптимизацию производительности... лучше бы доказанную). Просто T -- передача по значению (отличный выбор для лёгких "скалярных" типов, небольших struct-ов, POD). Добавление в этом случае const T -- ненужный шум в контракте и нелогичное ограничение на стороне реализации функции (нелогичное - при использовании достаточно широко принятой практике не вставлять const в объявление почти каждой локальной переменной, это и ваша практика тоже).

Возвращаясь к coding guidelines. С другой стороны, я не хочу фиксировать в coding guidelines чьи-то (мои в том числе) идеосинкразии, обусловленные индивидуальным профессиональным опытом (как ваша печальная история с shared_ptr внутри переданного по значению параметра). Вместо этого я пытаюсь отслеживать тренды в развитии языка, пытаюсь делать coding guidelines, к которым прикладываю руки, как можно более future-proof. С++ Core Guidelines -- хороший источник информации на этот счёт, на мой взгляд. Движение в сторону более частого использования практик функционального программирования (пример из Core Guidelines: "F.8: Prefer pure functions") прослеживается и там, и в современных UI библиотеках, и в практиках безопасного многопоточного программирования, поэтому я стараюсь сам и подталкиваю коллег использовать типы параметров T и const T& - избегая T& (предпочитая стиль чистых функций с немутирующими аргументами и возвратом всего через результат -- пока выигрыш в производительности от in-out стиля с T& не доказан замерами).

Вдогонку добавлю (заглянув в другую ветку комментариев) про преимущество static_cast. Для меня очевидное преимущество -- в том, что такая практика согласована с C++ Core Guidelines, конкретно -- с "ES.49: If you must use a cast, use a named cast".

Вы не находите сочетание строгости в отношении одних локальных переменных (параметров функции) и расслабленность в отношении других локальных переменных (не-параметров) нелогичным?

А я, кстати, зачастую следую Con. 4 (Use const to define objects with values that do not change after construction), и объявляю неизменяемыми и локальные переменные. Тут местами поленился, да.

У меня ровно сейчас, к сожалению, нет времени проверить легкодоступные статистические анализаторы, но, кажется, проверка на отсутствие модификации локальной переменной (если нет модификаций - диагностика "добавьте const!") может существовать, она технически нетрудна. Обращу на это внимание между делом - но в моём текущем проекте всё-таки практика применять const только в public APIs. Спасибо за дискуссию! По существу - продолжайте, пожалуйста, такие публикации, полезное для сообщества дело!

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

А как от этого поможет добавление к параметру const? Реализация функции точно так же может сколько угодно инкрементировать счётчик или пройти по этому указателю и что-то изменить.


и оптимизатор уж точно любит const

В случае с параметром семантика никак не меняется и компилятору дополнительных возможностей для оптимизации не открывается. По крайней мере в теории.


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

В некоторых (пусть и нечастых) случаях может и улучшить.
Например, вся функция — это цикл со счётчиком, начинающимся с параметра. Особенно если for по семантике не очень подходит. Тогда константность параметра заставит начинать функцию с auto value = param;, что читаемость ну никак не улучшит.

А как от этого поможет добавление к параметру const?

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

В случае простого POD аргумента constне даёт ничего, кроме чувства удовлетворения :)

А я со временем пришел к выводу, что const нужно ставить везде, где только можно. Что по умолчанию переменные должны быть константами. Что программист должен явно декларировать свое намерение изменять значение переменной после ее объявления. Я могу ошибаться, но в Rust вроде бы так и сделано, есть модификатор mut. Тоже касается методов класса, по умолчанию они должны быть const. И т.д. Помогало ли это отловить ошибки в реальном уже? Да, помогало, когда в результате копипасты значение присваивалось не той переменной, что задумывалось.

Отличная практика в проекте, в котором вы владеете coding guidelines и можете в одночасье расставить везде const, да ещё и поддержать практику статической проверкой (такие инструменты есть). В Rust то, что неизменяемость идёт по дефолту (ничего не надо писать, наоборот, надо писать mut для изменяемости) - замечательно. А в C++ дефолт, увы, противоположный. Согласен с спецификацией const для не-мутирующих функций-членов, но вот, const для каждой локальной переменной (и параметров, переданных по значению) - редко встречаю такое, так что ваш тезис мне понятен, но сам на практике предпочёл в данный период не применять (пока - из-за отсутствия технической возможности поддержать статическим анализом). Когда под рукой будет на готове опция статической проверки (что все немутирующие переменные и методы помечены const) - обсужу с коллегами и, возможно, начнём внедрять. В соло (хобби-)проектах - тоже за! Но, в коллективном проекте с большим объёмом кода без соблюдения этого правила и без возможности поддержки правила статическим анализом - взвесив за и против, решил пока так. Первый приоритет для меня - получить возможность обеспечить такую проверку статическим анализом (до этого - нет смысла и обсуждать с коллегами!), потом - достичь консенсуса с коллегами, что они согласны - эта новая строгость пойдёт на пользу в перспективе, а потом уже - внедрять. Внедрять на существующем коде, где такого правила нет, без поддержки автоматической проверкой - гиблый номер.

Насчёт переменных не уверен, но по крайней мере для методов в clang-tidy есть readability-make-member-function-const.

Не совсем понимаю откуда столько минусов? Вопрос задан абсолютно по существу. При прочтении статьи у меня были аналогичные вопросы. Если ещё этот проект используется в учебных целях, то аргумент автора «про измеритель прекрасного» заучит более чем странно. Существует множество описаний хороших практик при написании кода на плюсах и именно на таких примерах стоит и рассказывать и показывать их. Ещё мешанина из С и С++ стилей прям очень сильно бросается в глаза.

В общем, вам плюс за замечания:)

Минусы обычно ставят не за вопросы по существу, а за грубость. Ну и надо ещё хорошо понимать, что:

  1. ваше чувство прекрасного и моё могут не совпадать

  2. если хотеть всегда только идеальный код (это невозможно), то можно ни одного проекта не сделать.

И вообще, давайте меряться гитхабами :)

А про меряться чем-то там, это не грубость? Не весь код, к вашему сведению, выкладывается в публичный доступ.

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

Вам по пунктам предложили варианты, как можно сделать немного лучше.

А я по пунктам ответил, что лучше не получится. И что дальше? Пока что единственное приличное предложение - это чуть переоформить вызов chrono, да и это предложение пока не оформилось во что-то окончательное.

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

Минусы обычно ставят не за вопросы по существу, а за грубость

Гхм. Да я не собирался авторитетом давить, что ж вы все такие нервные. Я говорю о том, что я делаю проекты, и выкладываю в общий доступ. И даже неидеальные, они находят свою публику. И да, мне не стыдно выкладывать код как есть. Я в своей жизни кода читаю много, и точно знаю, что мой не самый хороший, но и не самый плохой. Зато он есть и доступен.

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

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

Подскажите, пожалуйста, на какой вопрос я не ответил? У меня сложилось впечатление, что отвечал я сразу же и по пунктам :)

Не хотелось писать про код, особенно после того, как по нему потоптались другие комментаторы, но…
double dt = std::chrono::duration<double>(Clock::now() - timestamp).count();
if (dt<.02)

Зачем так глумиться над chrono? Это хорошая библиотека, она такого не заслуживает.

if (Clock::now() - timestamp < 20ms)

Тут смешно в том, что все комментарии с критикой были мимо кассы, ни один не улучшал моего кода (который, ещё раз, не претендует на идеальность).

А теперь ближе посмотрим на предложенный вами кусочек кода.

if (Clock::now() - timestamp < 20ms) 

Ой, а 20ms прям-таки скомпилируется? Ладно, давайте представим, что 20ms это опечатка, вы хотели 20. Что я делаю не так? Вот этот код не компилируется.

#include <chrono>
#include <thread>
#include <iostream>

using Clock = std::chrono::high_resolution_clock;
using TimeStamp = std::chrono::time_point<Clock>;

int main() {
    TimeStamp timestamp = Clock::now();
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    if (Clock::now() - timestamp < 20)
        std::cerr << "foo" << std::endl;
    return 0;
}

Ну а про интегратор мы поговорим потом.

Ой, а 20ms прям-таки скомпилируется?
Да.

О, я узнал новую для себя штуку (chrono_literals), за это спасибо, я люблю учиться. А теперь мне нужно получить длительность dtпосле этого условия. Вы предлагаете вот такой код? Или что-то более читаемое?

    if (sc::now() - timestamp < 10ms)
        std::cerr << "foo" << std::endl;
    double dt = (sc::now() - timestamp).count()/1e6;
О, я узнал новую для себя штуку (chrono_literals)
Суть не в литералах, они просто для удобства — с тем же успехом можно написать milliseconds(20). Суть в том, что chrono предлагает type safety и полный набор операций над временем. Rule of thumb: если хочется написать .count(), то, наверное, вы что-то делаете не так.

Или что-то более читаемое?
(now() - timestamp) / 1.0s

Итак, в сухом остатке. Вы предлагаете вот эту строчку

double dt = std::chrono::duration<double>(Clock::now() - timestamp).count();

заменить на

double dt = (Clock::now() - timestamp)/std::literals::chrono_literals::1.0s;

И вот тут во мне начинает агонировать физик. Секунды делить на секунды - это безразмерная величина, а вовсе не секунды. Почему и как вторая запись должна быть безопаснее (ведь претензия только к этому?) первой?

using std::literals::chrono_literals;

в начале срр-файла спасает очень многих.

Секунды делить на секунды

chrono::duration хранит время не в секундах же, а в попугаях, заданных на этапе компиляции.

The only data stored in a duration is a tick count of type Rep

Так что эта строка как раз и переводит из "попугаев" в секунды.

the tick period is a compile-time rational fraction representing the time in seconds from one tick to the next.

Да пофиг на попугаи/секунды, now()-ts имеет тип duration, равно как и 1.0s. Мне кажется диким одно делить на другое, и говорить, что так и надо. Величина получается безразмерная. Короче, чем моя строчка с непосредственным кастом в секунды хуже этого странного деления?

Да пофиг на попугаи/секунды, now()-ts имеет тип duration

Конкретно в данном случае оно имеет тип std::chrono::high_resolution_clock::duration, который является std::chrono::duration<rep, period>, а какие там у него rep и period — а хрен это знает, это определяется реализацией.

chrono::duration хранит время не в секундах же, а в попугаях, заданных на этапе компиляции.

Таки вы не правы:
https://en.cppreference.com/w/cpp/chrono/duration


В случае std::duration<T> вторым шаблонным параметром будет std::ratio<1>, а значит, внутренним представлением будут секунды.


Но вот в каком формате нам придёт ***clock::now(), стандарт не определяет.

Да не нужны там дикие неймспейсы. Можно сразу писать 10ms

А как он найдёт 10ms без указания неймспейса? Нужно либо явное указание std::literals::chrono_literals::10ms или using std::literals::chrono_literals. При этом второй вариант вполне может быть нежелательным.

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

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

как вторая запись должна быть безопаснее первой?
Вторая запись явно говорит читателю, что автор кода хочет получить количество секунд. В первой же берется .count() каких-то внутренних попугаев и надо помнить (или идти читать), как устроен duration, какие там параметры шаблона и какой период по умолчанию.

Ну вот смотрите, я новичок в использовании chrono. Когда я вижу запись duration/1.0s, мне совершенно неочевидно, что это приведение интервала к секундам. Никогда и нигде в моей жизни такого не было. В то же время как явный каст из моего кода сразу подсвечивается в IDE, и там всё чётко видно.

sizeof(array) / sizeof(array[0]), наверное, встречали хоть раз и диссонанса от деления размера на размер не возникало?
Концептуально это то же самое.

Не, тут диссонанса нет никакого. Количество байт/массив поделённое на количество байт/элемент это размерность колво элементов / в массиве.

Ровно так же у меня не возникает никаких вопросов к делению на CLOCKS_PER_SEC из time.h. А вот к делению на одну секунду... Очень неудачный выбор имени, как мне кажется.

А я вообще не понимаю, зачем делить. Есть вполне удобная альтернатива в виде std::chrono::duration_cast. Да, она очень многословная, но читаемость с ней намного лучше.

О, так ведь об этом и разговор. Я использовал прямой каст, а меня за это ругают :)

А вот к делению на одну секунду

К делению величины <попугаев в 1 секунду>, а не секунду.

Это проект для себя или для студентов?

В записи CLOCKS_PER_SEC я чётко вижу попугаи в секунду, в записи 1.0s не вижу. Кажется ли вам это удачным выбором? Вы от моего кода-то отвлекитесь на секундочку, мы сейчас про STL говорим, и о том, почему вдруг прямой каст в секунды, рекомендуемый на том же cppreference, вдруг стал хуже деления на какой-то литерал с неудачным именем.

В записи CLOCKS_PER_SEC я чётко вижу попугаи в секунду, в записи 1.0s не вижу

потому что не понимаете как устроен std::chrono, при этом упорно всем доказываете, что понимаете, что происходит и ругаете, что другие "делят секунды на секунды". Вообще интересно как вы с такой позиции смотрите на "кванты процессорного времени" :)

Ну расслабьтесь вы, я понял, что вы меня не любите. Вы лучше по делу ответьте, а не на личности переходите. Напоминаю вопрос: вся эта ветка сводится к тому, что вместо вот этой моей строчки

double dt = duration<double>(Clock::now() - timestamp).count();

настойчиво предлагается использовать вот такую:

double dt = (Clock::now() - timestamp)/1.0s;

Ни одного разумного аргумента при этом я не услышал (ну, помимо того, что ".count() это нельзя-нельзя" - почему???)

Мои аргументы: первая строчка интуитивно понятнее, и мой IDE мне выдаёт конкретно этот конструктор, и говорит, в каких единицах выдаётся результат. Со второй же строчкой единицы нужно угадывать.

А теперь ответьте, пожалуйста, по существу: почему нужно использовать именно вторую запись, и для чего вообще придуман duration_cast?

для чего вообще придуман duration_cast

Что бы в time_point можно было поместить что угодно, хоть int, хоть double (если говорить упрощённо) и не было нужды писать

double dt = ...

а по итогу можно было в месте использования взять тот самый count()

auto dt = std::chrono::duration_cast<
        std::chrono:milliseconds
    >( sc.now() - timestamp ).count()
if( dt > 200 ) {
    ...

 читая этот код, я вожу, что тут речь идёт о 200 миллисекндах, а читая

double dt = duration<double>(
    Clock::now() - timestamp
).count();
if( dt > 0.02 ) {
    ...

константа 0.02 требует пояснений

".count() это нельзя-нельзя" — почему???
Потому что count — это не секунды, это тики, которые в каждом duration свои.
То, что в duration<double>они совпадают с секундами — частный случай, который из типа этого duration не слишком очевиден.

Это, однако, совсем не означает, что так делать нельзя. Кастите на здоровье. Мой изначальный комментарий был о сравнении c фиксированной величиной, для чего переводить все в даблы не было никакой необходимости.

для чего вообще придуман duration_cast?
— Для преобразований с потерей информации (1337ms -> 1s)
— Для шаблонов.

О, наконец-то у нас забрезжила надежда взаимопонимания. Итак, я действительно согласен с тем, что явное указание ms в сравнении if (now() - ts < 20ms) это нагляднее, нежели if (dt<0.020). Спасибо за указание существования литералов, это симпатично.

Другой вопрос, что double dt мне нужен по-любому для интеграторов типа x += vx * dt, а оттуда уже не является очевидным, насколько нужно вычислять этот временной интервал дважды, один раз в ифе, а второй раз для интегратора.

В сухом остатке: я так и не понял, где я так глумился над славной библиотекой chrono, и нужно ли вообще править мой код.

А в чём проблема сохранить интервал в переменную типа duration?


const auto dt = Clock::now() - timestamp;

А приводить к double тем или иным способом уже только по необходимости.

НЛО прилетело и опубликовало эту надпись здесь

Большое спасибо @haqreu.

Всегда с удовольствием читаю ваши статьи!

Пишите еще!

Небольшой момент, который хотелось бы уточнить, header guards специально проигнорированы или случайно?

#pragma once

занимает одну строку, поддерживается всеми современными компиляторами, если уж не хочется использовать классические #ifndef/#define/#endif

А зачем ставить header guards там, где они не нужны? Конкретно в этом проекте они не нужны. Ну и #pragma once нестандартный, я предпочитаю

#define ...

#ifdef ...

...
#endif

`#pragma once` уже давно стал стандартом де факто. Глупо писать по-старинке.

Хм. Ну тут я только могу сказать, что глупо делать утверждения, не подкреплённые аргументацией :)

Да и вообще, надо на модули переходить.

Вам какая аргументация по какому из моментов нужна?

1. h-файл без include guard (в любом его варианте) — это плохая практика, потому что код вы пишете не только для себя, но и для других. Где гарантии, что по мере разрастания кода не получится ситуация, что файл будет включён более одного раза со всеми вытекающими эффектами?

2. `#pragma once` vs `#ifdef` — типа, на самом деле, холиварная. Но первый вариант я считаю более удобным, т.к. меньше риск допустить ошибку. Ну а про поддержку `#pragma once` всеми современными компиляторами вы и сами можете погуглить.

3. Модули в данном случае — это оверкилл.
  1. В этом проекте меня нет заголовочных файлов как таковых, я просто цпп код раскидал по разным файлам.

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

  3. Да, оверкилл. Так же, как было бы оверкиллом делать .h+.cpp на каждую структуру, у меня описанную. Они просто не нужны на проекте такого размера.

Тогда просто не давайте им расширение `.h`, не вводите других в заблуждение.
В целом же всё это костыльные решения для преодоления недостатка C/C++: 1 .cpp файл = 1 TU.
Так у автора это и не заголовочные файлы, а просто включение фрагментов кода в cpp-файл.

просто включения должны иметь расширение .inl, а не .h

Хм. А STL файлы зачастую вообще расширения не имеют. Раз уж на то пошло, то скорее .inc, т.к. .inl скорее для инлайненных функций, чтобы они были доступны во всех модулях. Но это уже настолько нестандартные договорённости, что даже странно такое критиковать.

Супер. А можно то же самое для людей из смежных областей, для которых программирование - не профессия я полезный инструмент, и словосочетание Cmake звучит как что-то очень таинственное. Вот скажем я - умею создавать (довольно объемные по 5-10 тысяч строк кода) консольные приложения в VisualStudio и нажимать кнопку RUN в дебаг-режиме. Есть ли способ с такими знаниями работать с окошками и графикой на рабочем столе, а не в черно-белом текстовом терминале? Желательно чтобы этот способ был не сложнее проставления флажков при создании проекта и нажатии каких-то кнопов в самой VS. Буду благодарен за дельный совет.

Ну собственно, поставьте утилиту cmake, натравите её на мой репозиторий, и она вам сгенерирует проект .sln под visual studio, а дальше вы уже умеете. Cmake это такая фиговина, которая под разные ОС/компиляторы делает сборочные файлы. В вашем случае под вижл студию.

Чтобы не париться с гитом, скачайте просто репозиторий как .zip, распакуйте его, запустите cmake, укажите ему папку с проектом, и папку, в которую он будет собирать бинарники. Затем нажмите кнопочку "сконфигурировать", а затем "сгенерировать", и у вас появится .sln.

Примерно также автослесарь объясняет блондинке по телефону, что та должна покрутить в своем автомобиле, чтобы тот поехал. Для автослесаря, конечно, все просто, но вот для блондинки ...

Вы готовы объяснить человеку, не разбирающемуся в С/С++ почему в сгенерированном .sln содержатся "непонятные" цели ALL_BUILD и ZERO_CHECK?

Не проще ли тогда просто попросить открыть CMakeLists.txt непосредственно в студии (в VSCode тоже работает), так как студия уже давно умеет обрабатывать CMake скрипты без этапа "генерация sln"?

https://docs.microsoft.com/en-us/cpp/build/cmake-projects-in-visual-studio

НЛО прилетело и опубликовало эту надпись здесь

Коллеги, давно ломаю голову над одним вопросом. SDL2 поддерживает отрисовку спрайтов по координатам с плавающей запятой, но я никак не могу избавиться от бага плавающих пикселей в моем пет-движке.

Как он выглядит, можно посмотреть тут: https://www.youtube.com/watch?v=CYhqpEiUcdE (да, это не артефакты кодирования видео, это реальный эффект при работе приложения).

Если кто нибудь подскажет, в какую сторону копать, буду очень благодарен.

Трудно сказать, нужно смотреть код. Возможно, фреймрейт плавает, а возможно, есть баг с округлением чего-то.

Не совсем понял, что вы подразумеваете под плавающим фреймрейтом.

Я имел в виду, насколько ровно у вас идут перерисовки экрана по времени.

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

Да, это первое, что приходит в голову. Но я, все перепроверив с десяток раз, в упор не вижу, в каком месте косяк.

А возьмите один "дрожащий" спрайт, и для каждой перерисовки выведите в файл метку времени с точностью до миллисекунды и его текущую координату, которую вы отправляете в SDL. А потом в экселе просто постройте график по этому .csv файлу. Насколько он гладкий? Просто ли там ступеньки? Если да, то насколько большие? А он точно монотонный?

Взял спрайт дома. Взял участок данных за промежуток времени, в который происходит самая заметная "рябь" (209 строк с 29с:67мс по 31с:12мс). График получился не ступенчатый и не совсем ровный. Именно эти координаты передаются в функцию SDL_RenderCopyExF.

График и данные

Красиво, спасибо. А время как интерпретируется? Почему есть дубликаты меток времени?

Просто отрисовка происходит чаще

"59:21:6927","1780,8"

"59:21:6987","1780,8"

"59:21:7047","1780,8"

"59:21:7108","1780,8"

"59:21:7173","1780,8"

Подождите, 59 - это минуты, 21 - секунды, а ещё четыре цифры потом? Десятитысячные секунды? То есть, отрисовка раз в 60 миллисекунд?

Или 6 мс?!

А, ну да, я считать разучился. 60 десятитысячных, если это десятитысячные. Но тогда вопрос @EduardGuschin: если смотреть на список, который вы привели под графиком (давайте возьмём конец):

53:30:99

53:30:99 <- перерисовка меньше чем за одну сотую секунды

53:31:00 <- перерисовка меньше чем за одну сотую секунды

53:31:00 <- перерисовка меньше чем за одну сотую секунды

51:31:12 <- сто двадцать миллисекунд при цели в шесть??

А можете теперь построить ещё второй график (прямо по тому же csv, что у вас есть), где будет в миллисекундах время между двумя отрисовками спрайта? По оси абсцисс номер отрисовки спрайта, по оси ординат время между отрисовками.

Вертикальная синхронизация отключена, верхний лок FPS - 144, просто чтобы впустую не гонять GPU, поэтому время между кадрами может быть очень маленьким. Попробовал сделать лок FPS на 15, наблюдается та же проблема с рябью.

Для вычисления времени между двумя кадрами я, по аналогии с Unity, сделал свойство DeltaTime, которое можно получить в любой момент времени. Его удобно юзать, чтобы открепить скорость воспроизведения анимации от FPS.

Для этого графика пришлось взять другие данные, так как в старых метка времени в мс была не больше двух знаков.

Получилось как-то так:

В целом график относительно приличный, но вопрос со 120мс из предыдущего скриншота остался. Но как бы то ни было, fps чуть-чуть да плавает. Используете ли вы эти настоящие значения для обновления координаты спрайта, или она вычисляется при помощи константы какой-нибудь?

Только сейчас дошло, о чем речь с 120мс. Вообще при рендере таких заметных просадок по FPS нет, скорее всего так получилось, потому что участок с рябью я ловлю выводом в консоль и закрытием окна (чтобы не терять время на запись в файл). В момент, когда ЛКМ зажимает кнопку закрытия окна отрисовка останавливается. В общем, скорее всего 120мс - это скорость моего клика.

По поводу значений - да. Я сделал функцию, которая заставляет камеру плавно следовать за персонажем. Там для указания скорости слежения используется DeltaTime, чтобы она не зависела от FPS.

var camera_pos =  Math.Lerp(Camera.Transform.Position.X, _playerAnimator.Transform.Position.X, 1 * Time.DeltaTime);
Camera.Transform.Position = new Point(Math.Clamp(camera_pos, -3.5, 3.9), 0);

Но так как в SDL нет понятия камеры, приходится, по сути, двигать все остальные спрайты в игровом мире при движении персонажа. Поэтому DeltaTime влияет на вообще все спрайты при вычислении координат.

Я могу выслать демку, чтобы вы смогли своими глазами взглянуть на то, как это выглядит в реальном времени (видео не все передает, к сожалению), если вам интересно, конечно. Так же у движка открытый исходный код. В любом случае вы уже очень сильно помогли, за что вам большое спасибо. Баг ищу уже очень долго, обидно, что разработка встала из-за такой мелочи.

UPD: При локе в 15 FPS отчетливо видно, что этот эффект происходит из-за того, что в определенный момент времени все пиксели по оси Y меняют свою фактическую ширину. https://youtu.be/dLct7Rsk0BU

UPD2: На уровне SDL это можно решить включением линейного или анизатропного сглаживания, но тогда можно забыть про тайловый pixelart, потому что картинка сильно мылится и и становятся видны края тайлов

Скриншот

Так, стоп. Полный назад. Это что за вертикальные дырки в земле? Появляются они крайне регулярно.

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

Да, это тоже проблема. Их природа, скорее всего, такая же как и у остальных плавающих спрайтов. Земля - тайловый спрайт размером 15x15, и чтобы все эти маленькие пиксельартные спрайты отобразить на FullHD и выше, пришлось поработать над игровым масштабированием. Происходит оно следующим образом:

При загрузке спрайта земли ему назначается pixelPerUnit = 15; (это на верхнем уровне дает возможность упростить работу с координатной плоскостью, благодаря этому Size спрайта земли равен 1x1, а значит по оси X его можно расставлять на целочисленных координатах без стыков) Область видимости камеры по умолчанию устанавливается на 5.

При отрисовке спрайта над его размером и положением проводятся следующие манипуляции:

  1. Вычисляем Unit (единицу измерения) мира

    1.1. Берем две точки, (1;0) и (0;0), конвертируем их из мировых в экранные координаты, и вычитаем первую из второй. В итоге у нас получится размер мировой единицы измерения в экранных пикселях.

  2. Получаем Высоту и Ширину объекта:

    _size.Width = _bounds.Width * transformTo.LocalScale.X;
    _size.Height = _bounds.Height * transformTo.LocalScale.Y;
  3. Вычисляем draw_rect для передачи в SDL:

    var unit = Camera.MainCamera.WorldUnit;
    
    _draw_rect.w = (float)(unit * (_size.Width / PixelPerUnit));
    _draw_rect.h = (float)(unit * (_size.Height / PixelPerUnit));
    
    _draw_rect.x = (float)(point.X - (_draw_rect.w * transformTo.Achor.X));
    _draw_rect.y = (float)(point.Y - (_draw_rect.h * transformTo.Achor.Y));
    
    center.x = (float)(_draw_rect.w * transformTo.Achor.X);
    center.y = (float)(_draw_rect.h * transformTo.Achor.Y);
  4. И на основе этих данных рисуем:

    SDL.SDL_RenderCopyExF(Game.RenderContext, _texture.Instance, ref _bounds.SDLRect, draw_rect, transformTo.Degrees, ref center, flip);

В итоге размер тайла в экранных координатах при окне рендера в 1024х768 становится равен 76.8x76.8, и не меняется на протяжении всего рендера.

Можно было бы объяснить разрывы в тайлах, если бы в какой-то момент времени SDL на отрисовку передавалась разная ширина тайла, но это не так.

Вы навели меня на одну мысль. По идее, расстояние между двумя тайлами должно быть статично (при моих параметрах это 76,8). Чтобы это проверить, я убрал все тайлы земли кроме двух, поставил их рядом, и начал вычислять их фактическое расстояние между собой. Логи показали интересный результат

Лог
№ спрайта: (ширина; высота) (x; y)


2: (76,8; 76,8) (806,1994; 652,8)
1: (76,8; 76,8) (729,3994; 652,8)
Result: 76,79999

2: (76,8; 76,8) (806,1991; 652,8)
1: (76,8; 76,8) (729,39905; 652,8)
Result: 76,80005

2: (76,8; 76,8) (806,1987; 652,8)
1: (76,8; 76,8) (729,3987; 652,8)
Result: 76,80005

2: (76,8; 76,8) (806,19836; 652,8)
1: (76,8; 76,8) (729,3984; 652,8)
Result: 76,79999

2: (76,8; 76,8) (806,198; 652,8)
1: (76,8; 76,8) (729,398; 652,8)
Result: 76,79999

2: (76,8; 76,8) (806,1977; 652,8)
1: (76,8; 76,8) (729,3977; 652,8)
Result: 76,79999

2: (76,8; 76,8) (806,1974; 652,8)
1: (76,8; 76,8) (729,39734; 652,8)
Result: 76,80005

UPD: Я округлил экранные координаты до двух знаков после запятой перед вычислением расстояния, и результат стал еще интереснее

Лог
2: (76,8; 76,8) (857,1; 652,8)
1: (76,8; 76,8) (780,3; 652,8)
Result: 76,79999

2: (76,8; 76,8) (857,09; 652,8)
1: (76,8; 76,8) (780,29; 652,8)
Result: 76,80005

Если считать данные вручную, получается всегда 76,8, но во время выполнения это не так. Логика float в данном случае мне не понятна.

Да, я и хотел проверить постоянность расстояния между тайлами. Но ничего криминального не вижу в выводе. Что вас смущает? 76.79999 и 76.80005? Это совершенно нормально для float, это 76.8 с машинной точностью.

Проблему дырок между квадратными тайлами решать проще, чем проблему дрожащего дома, и наверняка это приведёт к решению и для дома.

Вопрос: а почему субпиксельная точность, почему не целые SDL_Rect? Давайте попробуем передать округлённые значения координат?
_draw_rect.x = (float)((int)(point.X - (_draw_rect.w * transformTo.Achor.X) + 0.5));

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

Можно округлить в сторону отрицательной бесконечности через Floor: Можно через Round или (int), эффект будет один и тот же.

_draw_rect.w = unit * (_size.Width / PixelPerUnit);
_draw_rect.h = unit * (_size.Height / PixelPerUnit);

_draw_rect.x = Math.Floor(point.X - _draw_rect.w * transformTo.Achor.X);
_draw_rect.y = Math.Floor(point.Y - _draw_rect.h * transformTo.Achor.Y);

В таком случае спрайты начинают дергаться целиком: https://youtu.be/56L5giUZL9A

Почему так происходит, я показал вот здесь: https://youtu.be/kjoDMnizsdM (здесь для примера не используется ничего, кроме попиксельного перемещения слоев с разной скоростью, чтобы исключить ошибки в DeltaTime и математических функциях отвечающих за плавное перемещение. Нет даже масштабирования текстур, они представлены как есть)

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

Можно даже вместо SDL_RenderCopyExF заюзать SDL_RenderCopyEx, чтобы вообще исключить возможность косяков в коде, но эффект тот-же.

Как раз floor не надо, надо в сторону ближайшего целого, т.е. floor(val + 0.5). Первое видео не видно, оно private.

Попробовал в сторону ближайшего целого через упаковку в int +0.5, эффект тот же, что и на первом видео (сделал public)

При этом расстояние между тайлами так же скачет

Лог
2: (77; 77) (558; 653)
1: (77; 77) (482; 653)
Result: 76, DeltaTime: 0,01708

2: (77; 77) (558; 653)
1: (77; 77) (482; 653)
Result: 76, DeltaTime: 0,0164386

2: (77; 77) (558; 653)
1: (77; 77) (482; 653)
Result: 76, DeltaTime: 0,016708

2: (77; 77) (558; 653)
1: (77; 77) (481; 653)
Result: 77, DeltaTime: 0,016612

2: (77; 77) (558; 653)
1: (77; 77) (481; 653)
Result: 77, DeltaTime: 0,0167341

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

К сожалению, разрыв есть, даже когда размер блока целый.

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

Я привязал камеру к кусту, а сам куст начал двигать каждый кадр на 0.0001 по оси X без умножения на DeltaTime.

Сначала видно, как плывет куст, но когда камера начинает двигаться за кустом - плыть начинает все остальное окружение. (https://www.youtube.com/watch?v=JJCuxeHgyrQ).

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

Проблема в функции SDL_RenderCopyExF, или в враппере SDL для C#.

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

ух, а что это вертикальные волны на 1:29??

https://youtu.be/JJCuxeHgyrQ?t=89

У вас растительность на заднем плане - это один спрайт или тоже составлен из кусочков, как и земля? А табличка с черепом?

Растительность на заднем плане из двух спрайтов. Табличка - один спрайт. Вертикальные волны - это тот самый эффект, который можно заметить и на других видео. Он же дает и пробелы в земле. Я экспериментировал с демкой написанной с нуля, и понял, что эти волны можно получить, если прибавлять к _draw_rect.w любое число или умножать на не целое. Если умножить на целое, волн не будет, но тогда получится эффект, как на видео с параллаксом, где спрайт перемещается не плавно, а заметными рывками, хоть и согласно пиксельной сетке монитора. Плюс ко всему, при перемещении камеры спрайты переходят на следующий пиксель не одновременно, а в разнобой, из за чего складывается ощущение, что спрайты между собой дергаются.

подождите, а если ширина целая, а координата нецелая? Тоже прыжками движется?

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

  1. Ширина не целая, координата не целая: движется волнами

  2. Ширина целая, координата не целая: движется прыжком

  3. Ширина не целая, координата целая: движется прыжком

  4. Ширина целая, координата целая: движется прыжком

Так. А теперь следующий вопрос: если спрайт не зумится, то есть, размер прямоугольника отрисовки равен размеру прямоугольника спрайта, координаты плавающие, и включен анизо фильтр, как это всё выглядит?

Спрайт передвигается так же рывками по одному экранному пикселю.

На всякий, скину код в 200 строк. Ничего C#-по зависимого, просто SDL2

Код
using System;
using static SDL2.SDL;
using static SDL2.SDL_image;

namespace SDLTest
{
    internal static class Program
    {
        private static IntPtr _windowContext, _renderContext;
        private static bool _mainLoop;

        private static Texture _treeTexture;
        private static Sprite _treeSprite;

        private static void Main(string[] args)
        {

            _mainLoop = true;

            CreateRenderer();

            RenderLoop();
        }
        
        private static void CreateRenderer()
        {
            SDL_Init(SDL_INIT_EVERYTHING);
            IMG_Init(IMG_InitFlags.IMG_INIT_PNG);
            
            
            
            _windowContext = SDL_CreateWindow("test", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 
                1024, 768, SDL_WindowFlags.SDL_WINDOW_SHOWN);
            
            const SDL_RendererFlags renderFlags = SDL_RendererFlags.SDL_RENDERER_ACCELERATED | 
                                                  SDL_RendererFlags.SDL_RENDERER_TARGETTEXTURE | 
                                                  SDL_RendererFlags.SDL_RENDERER_PRESENTVSYNC;
            
            _renderContext = SDL_CreateRenderer(_windowContext, -1, renderFlags);
            
            SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "2");
        }
        
        private static void RenderLoop()
        {
            LoadTexture();
            
            while (_mainLoop)
            {
                SDL_RenderClear(_renderContext);

                PoolEvents();

                RenderSprite();

                SDL_RenderPresent(_renderContext);
            }
        }

        private static void LoadTexture()
        {
            _treeTexture = new Texture("tree.png", _renderContext);
            _treeSprite = new Sprite(_treeTexture, _renderContext)
            {
                Width = _treeTexture.Width,
                Height = _treeTexture.Height,

                //X = 0,
                //Y = 0
            };
            _treeSprite.X = 1024 / 2 - (_treeSprite.Width / 2);
            _treeSprite.Y = 768 / 2 - (_treeSprite.Width / 2);
        }

        private static void RenderSprite()
        {
            _treeSprite.Draw();
            _treeSprite.X += 0.001f;
        }

        private static void PoolEvents()
        {
            while (SDL_PollEvent(out SDL_Event e) == 1)
            {
                PoolWindowEvent(e);
                PoolKeyEvent(e);
            }
        }

        private static void PoolKeyEvent(SDL_Event e)
        {
            if (e.key.type == SDL_EventType.SDL_KEYUP)
            {
                if (e.key.keysym.sym == SDL_Keycode.SDLK_ESCAPE)
                    _mainLoop = false;
            }
        }

        private static void PoolWindowEvent(SDL_Event e)
        {
            if (e.window.windowEvent == SDL_WindowEventID.SDL_WINDOWEVENT_CLOSE)
                _mainLoop = false;
        }
    }

    public struct Texture
    {
        public Texture(string path, IntPtr renderContext)
        {
            
            Instance = IMG_LoadTexture(renderContext, path);
            SDL_QueryTexture(Instance, out var format, out var access, out var width, out var height);
            Width = width;
            Height = height;
        }
        
        public int Width { get; }
        
        public int Height { get; }

        public IntPtr Instance { get; }
    }

    public struct Sprite
    {
        private readonly Texture _instance;
        private readonly IntPtr _renderInstance;
        private SDL_Rect _bounds;

        public Sprite(Texture texture, IntPtr renderContext)
        {
            _instance = texture;
            _renderInstance = renderContext;
            X = 0;
            Y = 0;
            Width = texture.Width;
            Height = texture.Height;

            _bounds = new SDL_Rect()
            {
                x = 0,
                y = 0,
                w = _instance.Width,
                h = _instance.Height
            };
        }

        public float X { get; set; }
        
        public float Y { get; set; }
        
        public float Width { get; set; }
        
        public float Height { get; set; }

        public void Draw()
        {
            var w = Width;
            var h = Height;
            var drawRect = new SDL_FRect()
            {
                x = X,
                y = Y,
                w = w,
                h = h
            };
            
            Console.WriteLine($"{w} {X}");

            SDL_RenderCopyExF(_renderInstance, _instance.Instance, ref _bounds, ref drawRect, 0, IntPtr.Zero,
                SDL_RendererFlip.SDL_FLIP_NONE);
        }
    }
}

На самом деле стало плавнее, но для пиксельарта сглаживание не вариант.

Насколько я понимаю, для пиксель арта сглаживание губительно при масштабировании, просто при движении должно быть нормально.

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

У меня получилось сделать такой же эффект волн на SFML. Получается он, если масштабировать спрайт на нецелое число.

Резюмируя: проблема не в SDL и не в коде. Просто так устроен механизм отрисовки. Если цель сделать красивый pixelart с плавным движением на современных разрешениях, лучше использовать спрайты больших размеров в стилистике pixelart и включать сглаживание, а не каноничные pixelart спрайты, и не в коем случае не использовать масштабирование спрайтов на нецелое число, если есть хоть-какая ни будь анимация.

Большое вам спасибо за уделенное время, вы очень помогли.

Для демонстрации возможностей фреймворка SDL, наверное, круто. Для внедрения в мозг студента базовых возможностей С++, стандартной библиотеки и структурирования программ на С++, по-моему, не очень. Извините, только частное мнение ведущего курс "SQL/noSQL СУБД за 30 часов" и практикующего разработчика на С++.

А можете раскрыть, что именно и почему плохо для C++?

Слово "плохо" я не говорил :) Подход интересный и по сути своей "обучать играя" правильный. Я бы, наверное, построил курс на создании простой консольной игры с использованием только возможностей стандартной библиотеки с выгрузкой/загрузкой состояния в файл. Студентам, как минимум, пришлось бы изучить все основные структурные блоки (if, for, while, switch), создать свои классы, использовать iostream, концепт << и базовые контейнеры типа списка, массива или даже словаря.

Так ведь именно всё это и придётся студентам сделать. Тот код, что я показал в этой статье - это совсем не конечная цель, а чисто заготовка, чтобы получить открывающееся окно, обработку клавиатуры, и отрисовку спрайтов. Грубо говоря, чисто чтобы студенты не тратили время на написание CMakeLists.txt.

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

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

Зато если не добавить графики, то получается делать только скучные проекты из разряда бронирования книги в каталоге местной библиотеки (зевает).

Ну и к тому же, не обязательно ставить себе целью объять ВСЮ STL, это совершенно не нужно и абсолютно нереально в рамках любого университетского курса.

Не скажите :) Помню, у нас на ДВК-2 была консольная динамическая игра Road. Границы трассы выводились символами "/", "|" и "\", препятствия "солнышком", машина управлялась стрелками. Программировать пошаговые игрушки не менее интересно.

Угу, только нынче для такого программирования в консоли понадобится какой-нибудь ncurses, что ничем не отличается от SDL.

Зато если не добавить графики, то получается делать только скучные проекты из разряда бронирования книги в каталоге местной библиотеки (зевает).

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

Следующий этап - модульность операций и препарсинг через сервер, то есть клиент видит в выражении exp(... и запрашивает у сервера правила парсинга, в простейшем случае - regex.

Третий этап - "взломать" (отказ в обслуживании или некорректный результат вместо сообщения об ошибке) сервер (можно ваш, но можно и собственный) некорректным запросом, по типу классического "my name is) DROP TABLE users;" или незакрытым XML/Json тэгом и предложить решение по защите (экранирование).

Из "лишних" вещей тут только сокеты будут, а не целая SDL, хотя можно клиент на Qt сделать - дополнительный нужный опыт.

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

А какая итоговая специальность у студентов?

Как-то С++ для первого курса мне кажется избыточным, я думал речь идёт о третьем (примерно курсе), Python лучше бы подошёл для старта, да даже чистый Си, который тоже вполне себе в ООП может если правильно покрутить.

Про первый курс у меня прямо в первом предложении статьи написано ;)

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

Про первый курс у меня прямо в первом предложении статьи написано ;)

Пропустил, извиняюсь. Почему-то в голове засел третий.

Специализация - информатика в самом широком смысле

Это немного меняет дело. Тут действительно визуальная составляющая играет огромную роль.

Тогда следующий вопрос, почему не что-то готовое, я бы посмотрел в сторону LÖVE, правда основано на Lua, но гораздо проще для освоения.

А что может быть готовое помимо LÖVE? Мне нужен ц/цпп, тут у меня вариантов нет (см. жёсткая программа, аккредитованная министерством).

SDL написан на Си, код для работы с ним чисто сишный, на плюсах так не пишут. Графической библиотекой для C++ является, например, SFML.

Ой, несколько сишных вызовов это не страшно. Главное-то всё равно не сама библиотека, а внутреннее представление игры.

Спасибо большое. SDL приятный в работе, и вы тщательно объясняете всё. Если у меня найдётся время (увы, увы, не факт), я попытаюсь портировать ваш код на Rust (который с SDL работает плюс-минус так же).

У вас забавная лицензия, но если бы вы её сделали под MIT, то остальным было бы проще и понятнее работать с вашим кодом и ресурсами.

Отдельный вопрос про спрайты. Вы их сами рисовали, или это скриншоты откуда-то? Если сами, лицензия на картинки распространяется?

Спасибо на добром слове. Спрайты рисовал сам, специально, чтобы не зависеть ни от кого. Лицензия общая на всё :)

Вообще я за то, чтобы отдавать подобное просто в public domain, так что даже MIT мне не очень подходит.

Если что, у меня где-то векторные черновики спрайтов остались ещё.

Сделайте под cc-0 (https://creativecommons.org/share-your-work/public-domain/cc0/) Это специальная лицензия, позволяющая обойти отсутствие понятия добровольной передачи в public domain в некоторых юрисдикциях.

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории