Решил посмотреть на самые простые способы отображения графических примитивов в Windows и нашел библиотеку memake. В использовании достаточно простая и удобная, но зависит от библиотеки SDL. В примере для Visual Studio (я использую VS 2019 и всем советую использовать эту или новее) сразу идут собранные бинари SDL, так что все работает "из коробки", но настроено там все только для дебажной x86 версии, так что я пытался настроить для остальных (Release x86, Debug x64, Release x64), но не преуспел (слишком много всяких параметров менять руками) и решил сделать все через cmake, попутно разбираясь, а что же там не работает. Получил вот такой CMakeLists.txt:
#
cmake_minimum_required (VERSION 3.8)
project ("MemakePrj")
# Add source to this project's executable.
add_executable (MemakePrj "main.cpp" "Memake/Memake.cpp" "Memake/Vector2d.cpp")
# SDL2 headers
target_include_directories(MemakePrj PRIVATE "SDL2-2.0.14/include")
# add SDL_MAIN_HANDLED definition to avoid
# "LNK2019 unresolved external symbol SDL_main referenced in function main_getcmdline"
add_definitions( -DSDL_MAIN_HANDLED )
# SDL library folder
set(SDL2_lib_folder "${PROJECT_SOURCE_DIR}/SDL2-2.0.14/lib")
message(${CMAKE_BUILD_TYPE})
# check 32 or 64 bits
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
# 64 bits
set(SDL2_lib_folder "${SDL2_lib_folder}/x64")
elseif(CMAKE_SIZEOF_VOID_P EQUAL 4)
# 32 bits
set(SDL2_lib_folder "${SDL2_lib_folder}/x86")
endif()
# link SDL2 static lib
target_link_libraries(MemakePrj ${SDL2_lib_folder}/SDL2.lib)
target_link_libraries(MemakePrj ${SDL2_lib_folder}/SDL2main.lib)
# copy dynamic lib to folder with executable file
file(COPY ${SDL2_lib_folder}/SDL2.dll DESTINATION ${PROJECT_BINARY_DIR})
Теперь по порядку. В строке 7 добавляю к main.cpp еще два cpp-файла из библиотеки memake (остальное там - заголовочные файлы) и вся библиотека будет таким образом включена в исполнимый файл. Можно было сделать отдельный CMakeLists.txt для папки memake, чтобы вся библиотека подтягивалась и собиралась отдельно, но мне было лень и теперь все у меня одним куском. В строке 10 добавляю заголовки для SDL, а в строке 14 добавляю специальный дефинишен SDL_MAIN_HANDLED, потому что без него будет ошибка компиляции "LNK2019 unresolved external symbol SDL_main ...". В строке 17 указываем папку с бинарями библиотеки SDL, а в строках 22-28 определяем, какую версию бинарных файлов SDL нам надо использовать x86 или x64. В строках 31 и 32 собственно подключаем эти библиотеки к проекту. В строке 35 очень интересный момент - копирование SDL2.dll в папку с исполняемым файлом. Оказывается, без этой dll оно все не будет работать и в солюшене для Visual Studio был просто добавлен путь в переменную PATH для проекта. Я не нашел как сделать что-то такое же для cmake, поэтому просто скопировал файл библиотеки в папку с exe-файлом (строка 35). На самом деле составлять CMakeLists.txt для проекта - это целое дело, сопоставимое с написанием кода, но это все же вспомогательная задача, от которой нужно только одно: чтобы все собиралось и работало. Так что я не упорствовал в поиске каких-то сильно красивых, изящных и правильных решений: работает, выглядит понятно - ну и хорошо.
То ли дело посмотреть как работает "коллижн менеджер" в оригинальном примере - а он работает очень просто и можно даже сказать примитивно, но справляется со своей задачей (демонстрация работы библиотеки):
void checkCollision(Ball& b)
{
float distX = x - b.x;
float distY = y - b.y;
float distance = sqrt((distX * distX) + (distY * distY));
if (distance < r + b.r)
{
dx *= -1;
dy *= -1;
}
}
Если расстояниеот одного до другого шарика меньше, чем сумма радиусов обоих, то шарик отлетает в противоположную сторону (направления движения по обоим осям умножаются на -1). Простенько и со вкусом. В результате все шарики летают под углом, кратным 45 градусов:
Я решил заморочиться и все же учесть правило "угол падения равен углу отражения", для чего даже написал "библиотеку" операций для двумерного пространства. В результате вот такой получился код:
void checkCollision(Ball& b)
{
float dist = pos.distanceTo(b.pos);
if (dist < r + b.r)
{
// direction to other ball
Point2f toB = b.pos - pos;
// calculate dot product
float dotProd = f.dotProduct(toB);
// if dot product is negative then force directed away from B ball
// and we do nothing
if (dotProd > 0)
{
// angle between normal and force (moving) vectors
float angle = f.angleTo(toB);
// the angle of incidence is equal to the angle of reflection
f = Mat2x2f().rot(angle * 2) * f;
f = f * (-1.f);
}
}
}
Если вкратце, то мы тут тоже сначала измеряем расстояние от этого до другого шарика, а потом вычисляем вектор
toB от центра этого до центра другого шарика, после чего в строке 9 вычисляем dot product вектора движения
f на
toB (что есть проекция
f на
toB) и в случае, если он больше нуля (то есть направлен на другой шарик), то происходит "отскок", а если меньше нуля, то значит наш шарик и так двигается прочь от другого шарика и делать ничего не надо. Отскок мы вычисляем следующим образом: вычисляем угол между вектором движения и вектором направления на центр другого шарика (строка 15), после чего доворачиваем (путем умножения на матрицу поворота) вокруг оси направления вектор движения на этот двойной угол и потом умножаем его на -1 и таким образом реализуем правило "угол падения равен углу отражения". Для большей наглядности вот картинка:
Вот тут вертикальная ось - это как раз вектор
toB на центр другого шарика, а горизонтальная линия - это касательная к точке, где произошел контакт. Получилось вот так:
Уже гораздо более интересно! Но пришлось, конечно, вспомнить векторную и матричную математику.