diff --git a/.github/images/Arrow_Down_Key_Dark.png b/.github/images/Arrow_Down_Key_Dark.png new file mode 100644 index 0000000..9edcf58 Binary files /dev/null and b/.github/images/Arrow_Down_Key_Dark.png differ diff --git a/.github/images/Arrow_Left_Key_Dark.png b/.github/images/Arrow_Left_Key_Dark.png new file mode 100644 index 0000000..3425005 Binary files /dev/null and b/.github/images/Arrow_Left_Key_Dark.png differ diff --git a/.github/images/Arrow_Right_Key_Dark.png b/.github/images/Arrow_Right_Key_Dark.png new file mode 100644 index 0000000..929cb35 Binary files /dev/null and b/.github/images/Arrow_Right_Key_Dark.png differ diff --git a/.github/images/Arrow_Up_Key_Dark.png b/.github/images/Arrow_Up_Key_Dark.png new file mode 100644 index 0000000..025a68d Binary files /dev/null and b/.github/images/Arrow_Up_Key_Dark.png differ diff --git a/.github/images/Esc_Key_Dark.png b/.github/images/Esc_Key_Dark.png new file mode 100644 index 0000000..c363cdc Binary files /dev/null and b/.github/images/Esc_Key_Dark.png differ diff --git a/.github/images/Mouse_Left_Key_Dark.png b/.github/images/Mouse_Left_Key_Dark.png new file mode 100644 index 0000000..1b1eb86 Binary files /dev/null and b/.github/images/Mouse_Left_Key_Dark.png differ diff --git a/.github/images/Mouse_Middle_Key_Dark.png b/.github/images/Mouse_Middle_Key_Dark.png new file mode 100644 index 0000000..0fd0a2e Binary files /dev/null and b/.github/images/Mouse_Middle_Key_Dark.png differ diff --git a/.github/images/Mouse_Right_Key_Dark.png b/.github/images/Mouse_Right_Key_Dark.png new file mode 100644 index 0000000..cc24f92 Binary files /dev/null and b/.github/images/Mouse_Right_Key_Dark.png differ diff --git a/.github/images/Space_Key_Dark.png b/.github/images/Space_Key_Dark.png new file mode 100644 index 0000000..2298b4f Binary files /dev/null and b/.github/images/Space_Key_Dark.png differ diff --git a/.github/images/Z_Key_Dark.png b/.github/images/Z_Key_Dark.png new file mode 100644 index 0000000..e9a1299 Binary files /dev/null and b/.github/images/Z_Key_Dark.png differ diff --git a/.github/screenshots/screenshot01.png b/.github/screenshots/screenshot01.png new file mode 100644 index 0000000..435a038 Binary files /dev/null and b/.github/screenshots/screenshot01.png differ diff --git a/.github/screenshots/screenshot02.png b/.github/screenshots/screenshot02.png new file mode 100644 index 0000000..8ecfbe0 Binary files /dev/null and b/.github/screenshots/screenshot02.png differ diff --git a/.github/screenshots/screenshot03.png b/.github/screenshots/screenshot03.png new file mode 100644 index 0000000..276c5f6 Binary files /dev/null and b/.github/screenshots/screenshot03.png differ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe93658..574dde0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,9 +85,9 @@ jobs: - name: Copy WASM module (Ubuntu) if: matrix.target == 'web' && matrix.os == 'ubuntu-latest' run: | - cp ./build/asteroids.wasm ./src/web - cp ./build/asteroids.js ./src/web - cp ./build/asteroids.html ./src/web + cp ./build/sandbox.wasm ./src/web + cp ./build/sandbox.js ./src/web + cp ./build/sandbox.html ./src/web - name: Deploy to GitHub Pages (Ubuntu) if: matrix.target == 'web' && matrix.os == 'ubuntu-latest' diff --git a/.gitignore b/.gitignore index 759f9c1..4770cc7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,5 @@ lib/*/ imgui.ini compile_commands.json .DS_Store -src/web/asteroids.* -src/tests +src/web/sandbox.* .cache \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index e052cc5..daf204e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.18.0) -set(PROJECT_NAME asteroids) +set(PROJECT_NAME sandbox) function(dump_info) message(STATUS "CMAKE_SYSTEM: ${CMAKE_SYSTEM}") diff --git a/README.md b/README.md index e69de29..fedfb0a 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,76 @@ +# C++ Simple physics engine + +Implements a simple physics engine in C++ based on [Verlet integration](https://en.wikipedia.org/wiki/Verlet_integration) and [Verlet constraints](https://en.wikipedia.org/wiki/Verlet_integration#Verlet_constraints). + +
+

+ + +

+
+ +

+ Live demo here +

+ +## About + +You can check the WEB version [here](https://leandrosq.github.io/cpp-physics-sandbox/). It is a port using WASM generated by Emscripten using WebGL on the browser. + +* Restricted only to circles +* Supports gravity +* Supports collisions + * Implements a Quadtree for collision detection +* Supports constraints + * Implements both a circle and a rectangle world constraint +* Supports user interaction + * Dragging + * Spawning + * Explode + +## Controls + +### Desktop + +> `Left click` to spawn circles + +> `Right click` to drag circles + +> `Middle click` to explode circles + +> `Space bar` to flip the Gravity vector + +> `Z` to toggle Gravity ON/OFF + +> `Up arrow` to increase the Gravity force + +> `Down arrow` to decrease the Gravity force + +> `Left arrow` to decrease the Gravity angle + +> `Right arrow` to increase the Gravity angle + +> `ESC` to exit + +

+ Other controls included on the GUI can be used with the mouse as demonstrated below. + +

+ +## Project + +### Resources + +| Name | Description | +| -- | -- | +| [ClangD](https://clangd.llvm.org/) | Language Server for C++ | +| [CMake](https://cmake.org/) | Cross-platform open-source make system | +| [Clang-tidy](https://clang.llvm.org/extra/clang-tidy/) | A clang-based C++ “linter” tool | +| [Clang-format](https://clang.llvm.org/docs/ClangFormat.html) | A tool to format C/C++/Obj-C code | +| [Emscripten](https://emscripten.org/) | Used for the web port, generating the WASM binaries. | +| [Raylib](https://www.raylib.com/) | A simple and easy-to-use library to enjoy videogames programming | +| [Dear ImGui](https://www.github.com/ocornut/imgui) | Bloat-free Immediate Mode Graphical User interface for C++ with minimal dependencies | +| [Dear ImGui Raylib](https:://github.com/RobLoach/raylib-imgui) | Dear ImGui bindings for Raylib | +| [NES CSS](https://nostalgic-css.github.io/NES.css/) | NES.css is NES-style (8bit-like) CSS Framework. | +| Github Actions | Used for CI/CD | +| Github Pages | Used for hosting the web version | diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 256bad4..3ff5b8e 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -114,7 +114,6 @@ endfunction() # Dependencies set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) -set(SUPPORT_RPRAND_GENERATOR ON) include_library(raylib 5.0 https://github.com/raysan5/raylib/archive/refs/tags/VERSION.tar.gz) set(BUILD_RAYLIB_CPP_EXAMPLES OFF CACHE BOOL "" FORCE) @@ -122,7 +121,7 @@ include_library(raylib_cpp 5.0.1 https://github.com/RobLoach/raylib-cpp/archive/ # include_library(raygui 4.0 https://github.com/raysan5/raygui/archive/refs/tags/VERSION.tar.gz) -include_library(dear_imgui docking https://github.com/ocornut/imgui/archive/refs/heads/VERSION.tar.gz) +include_library(dear_imgui 1.90.1 https://github.com/ocornut/imgui/archive/refs/tags/vVERSION.tar.gz) include_library(rlimgui main https://github.com/raylib-extras/rlimgui/archive/refs/heads/VERSION.tar.gz) target_compile_definitions(rlimgui PRIVATE NO_FONT_AWESOME) target_link_libraries(rlimgui dear_imgui raylib) diff --git a/scripts/build.sh b/scripts/build.sh index 611fd19..69dc2b7 100644 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -7,7 +7,7 @@ cmake --build ./build -j 10 # Check if the previous command succeeded if [ $? -eq 0 ]; then echo "Build succeeded" - ./build/asteroids + ./build/sandbox else echo "Build failed" fi \ No newline at end of file diff --git a/scripts/copy-web-artifacts.sh b/scripts/copy-web-artifacts.sh index 6721aba..91d6830 100644 --- a/scripts/copy-web-artifacts.sh +++ b/scripts/copy-web-artifacts.sh @@ -2,4 +2,4 @@ set -e -find ./build -depth 1 -name 'asteroids.*' -print -exec cp {} src/web \; \ No newline at end of file +find ./build -depth 1 -name 'sandbox.*' -print -exec cp {} src/web \; \ No newline at end of file diff --git a/scripts/linter.sh b/scripts/linter.sh index 01c94fa..9b5c55b 100644 --- a/scripts/linter.sh +++ b/scripts/linter.sh @@ -7,7 +7,7 @@ cmake --build ./build -j 10 # Check if the previous command succeeded if [ $? -eq 0 ]; then echo "Build succeeded... running linter!" - cmake --build ./build --target asteroids_lint + cmake --build ./build --target sandbox_lint else echo "Build failed" fi \ No newline at end of file diff --git a/scripts/server.sh b/scripts/server.sh index 7ad41c7..1733ced 100644 --- a/scripts/server.sh +++ b/scripts/server.sh @@ -2,7 +2,7 @@ set -e -npx nodemon --watch build/asteroids.* --exec "sh scripts/copy-web-artifacts.sh" & +npx nodemon --watch build/sandbox.* --exec "sh scripts/copy-web-artifacts.sh" & npx live-server --port=3000 --no-browser ./src/web & wait \ No newline at end of file diff --git a/scripts/web.sh b/scripts/web.sh index 11e43de..94e0d66 100644 --- a/scripts/web.sh +++ b/scripts/web.sh @@ -11,4 +11,4 @@ else exit 1 fi -cp ./build/web/asteroids.* ./src/web \ No newline at end of file +cp ./build/web/sandbox.* ./src/web \ No newline at end of file diff --git a/src/core/data/dummy-list.cpp b/src/core/data/dummy-list.cpp deleted file mode 100644 index 284ceae..0000000 --- a/src/core/data/dummy-list.cpp +++ /dev/null @@ -1,42 +0,0 @@ -#include "dummy-list.hpp" -#include "../models/asteroid.hpp" - -void DummyList::clear() { - asteroids.clear(); -} - -bool DummyList::isEmpty() { - return asteroids.empty(); -} - -uint32_t DummyList::size() { - return asteroids.size(); -} - -uint16_t DummyList::getCellIndex(raylib::Vector2 position) { - return 0; -} - -raylib::Vector2 DummyList::getCellPosition(uint16_t index) { - return raylib::Vector2::Zero(); -} - -void DummyList::resize(uint16_t rows, uint16_t cols) { - // Do nothing -} - -void DummyList::insert(std::shared_ptr asteroid) { - asteroids.push_back(asteroid); -} - -void DummyList::remove(std::shared_ptr asteroid) { - asteroids.erase(std::remove(asteroids.begin(), asteroids.end(), asteroid), asteroids.end()); -} - -std::vector> DummyList::retrieve(raylib::Vector2 position, uint16_t radius) { - return asteroids; -} - -std::vector> DummyList::all() { - return asteroids; -} \ No newline at end of file diff --git a/src/core/data/dummy-list.hpp b/src/core/data/dummy-list.hpp deleted file mode 100644 index b98a307..0000000 --- a/src/core/data/dummy-list.hpp +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include "../precomp.hpp" -#include "icontainer.hpp" - -class Asteroid; - -class DummyList : public IContainer { - private: - std::vector> asteroids; - - public: - DummyList() = default; - ~DummyList() = default; - - void clear() override; - bool isEmpty() override; - uint32_t size() override; - uint16_t getCellIndex(raylib::Vector2 position) override; - raylib::Vector2 getCellPosition(uint16_t index) override; - void resize(uint16_t rows, uint16_t cols) override; - void insert(std::shared_ptr asteroid) override; - void remove(std::shared_ptr asteroid) override; - std::vector> retrieve(raylib::Vector2 position, uint16_t radius) override; - std::vector> all() override; -}; \ No newline at end of file diff --git a/src/core/data/icontainer.hpp b/src/core/data/icontainer.hpp deleted file mode 100644 index b5fbd0d..0000000 --- a/src/core/data/icontainer.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include "../precomp.hpp" - -class Asteroid; - -class IContainer { - public: - virtual void clear() = 0; - virtual bool isEmpty() = 0; - virtual uint32_t size() = 0; - virtual uint16_t getCellIndex(raylib::Vector2 position) = 0; - virtual raylib::Vector2 getCellPosition(uint16_t index) = 0; - virtual void resize(uint16_t rows, uint16_t cols) = 0; - virtual void insert(std::shared_ptr asteroid) = 0; - virtual void remove(std::shared_ptr asteroid) = 0; - virtual std::vector> retrieve(raylib::Vector2 position, uint16_t radius) = 0; - virtual std::vector> all() = 0; -}; \ No newline at end of file diff --git a/src/core/data/spatial-hash-grid.cpp b/src/core/data/spatial-hash-grid.cpp deleted file mode 100644 index 27f7c8a..0000000 --- a/src/core/data/spatial-hash-grid.cpp +++ /dev/null @@ -1,117 +0,0 @@ -#include "spatial-hash-grid.hpp" -#include "../settings.hpp" -#include "../models/asteroid.hpp" - -void SpatialHashGrid::clear() { - for (auto &cell : cells) { - cell.asteroids.resize(0); - } - count = 0; -} - -bool SpatialHashGrid::isEmpty() { - return cells.empty() || count <= 0; -} - -uint32_t SpatialHashGrid::size() { - return count; -} - -uint16_t SpatialHashGrid::getCellIndex(raylib::Vector2 position) { - uint16_t index = (uint16_t)(position.y / cellSize) * cols + (uint16_t)(position.x / cellSize); - if (index < 0) index = 0; - if (index >= cells.size()) index = cells.size() - 1; - return index; -} - -raylib::Vector2 SpatialHashGrid::getCellPosition(uint16_t index) { - return raylib::Vector2((index % cols) * cellSize, ((float)index / cols) * cellSize); -} - -void SpatialHashGrid::resize(uint16_t rows, uint16_t cols) { - const auto backup = all(); - cells.clear(); - count = 0; - cells.resize(rows * cols); - - this->rows = rows; - this->cols = cols; - - cellSize = std::min(ceilf(HEIGHT / (float)rows), ceilf(WIDTH / (float)cols)); - - for (auto &asteroid : backup) { - insert(asteroid); - } -} - -void SpatialHashGrid::insert(std::shared_ptr asteroid) { - auto index = getCellIndex(asteroid->position); - asteroid->index = index; - asteroid->color = cells[index].color; - cells[index].asteroids.push_back(asteroid); - count++; -} - -void SpatialHashGrid::remove(std::shared_ptr asteroid) { - auto index = getCellIndex(asteroid->position); - asteroid->index = -1; - cells[index].asteroids.remove(asteroid); - count--; -} - -void SpatialHashGrid::update() { - for (auto &cell : cells) { - auto it = cell.asteroids.begin(); - while (it != cell.asteroids.end()) { - uint16_t index = getCellIndex(it->get()->position); - if (index != it->get()->index) { - it->get()->index = index; - it->get()->color = cell.color; - cells[index].asteroids.push_back(*it); - it = cells[it->get()->index].asteroids.erase(it); - } else { - it++; - } - } - } -} - -std::vector> SpatialHashGrid::retrieve(raylib::Vector2 position, uint16_t radius) { - // Get the adjacent cells based on the radius - uint16_t startRow = std::max(0, (int)(position.y - radius) / cellSize); - uint16_t endRow = std::min(rows - 1, (int)(position.y + radius) / cellSize); - uint16_t startCol = std::max(0, (int)(position.x - radius) / cellSize); - uint16_t endCol = std::min(cols - 1, (int)(position.x + radius) / cellSize); - - std::vector> result; - for (uint16_t row = startRow; row <= endRow; row++) { - for (uint16_t col = startCol; col <= endCol; col++) { - uint16_t index = row * cols + col; - for (auto &asteroid : cells[index].asteroids) { - if (position.Distance(asteroid->position) <= radius) { - result.push_back(asteroid); - } - } - } - } - - return result; -} - -std::vector> SpatialHashGrid::all() { - std::vector> result; - for (auto &cell : cells) { - result.insert(result.end(), cell.asteroids.begin(), cell.asteroids.end()); - } - return result; -} - -void SpatialHashGrid::render() { - for (uint16_t row = 0; row < rows; row++) { - for (uint16_t col = 0; col < cols; col++) { - uint16_t index = row * cols + col; - DrawRectangleLines(col * cellSize, row * cellSize, cellSize, cellSize, cells[index].color); - DrawText(TextFormat("%d", index), col * cellSize + 5, row * cellSize + 5, 10, WHITE); - } - } -} diff --git a/src/core/data/spatial-hash-grid.hpp b/src/core/data/spatial-hash-grid.hpp deleted file mode 100644 index ef5e69d..0000000 --- a/src/core/data/spatial-hash-grid.hpp +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -#include "../precomp.hpp" -#include "icontainer.hpp" -#include "../utils.hpp" - -class SpatialHashGrid : public IContainer { - private: - struct Cell { - std::list> asteroids; - raylib::Color color = randomColor(); - }; - std::vector cells; - uint16_t count; - - public: - uint16_t rows; - uint16_t cols; - uint16_t cellSize; - - SpatialHashGrid() = default; - ~SpatialHashGrid() = default; - - void clear() override; - bool isEmpty() override; - uint32_t size() override; - uint16_t getCellIndex(raylib::Vector2 position) override; - raylib::Vector2 getCellPosition(uint16_t index) override; - void resize(uint16_t rows, uint16_t cols) override; - void insert(std::shared_ptr asteroid) override; - void remove(std::shared_ptr asteroid) override; - void update(); - std::vector> retrieve(raylib::Vector2 position, uint16_t radius) override; - std::vector> all() override; - void render(); -}; \ No newline at end of file diff --git a/src/core/interface/gui.cpp b/src/core/interface/gui.cpp new file mode 100644 index 0000000..c260ba9 --- /dev/null +++ b/src/core/interface/gui.cpp @@ -0,0 +1,185 @@ +#include "../precomp.hpp" +#include "../settings.hpp" +#include "../utils.hpp" +#include "../models/app.hpp" +#include "imgui.h" + +void App::setupGUI() { + rlImGuiSetup(true); + ImGuiStyle &style = ImGui::GetStyle(); + + auto primary = ImVec4(0.13f, 0.61f, 0.93f, 1.00f); + auto background = ImVec4(0.13f, 0.14f, 0.17f, 1.00f); + auto text = ImVec4(0.86f, 0.93f, 0.89f, 0.78f); + + style.WindowMinSize = ImVec2(160, 20); + style.FramePadding = ImVec2(4, 2); + style.ItemSpacing = ImVec2(6, 2); + style.ItemInnerSpacing = ImVec2(6, 4); + style.Alpha = 0.95f; + style.WindowRounding = 4.0f; + style.FrameRounding = 2.0f; + style.IndentSpacing = 6.0f; + style.ItemInnerSpacing = ImVec2(6, 4); + style.ColumnsMinSpacing = 50.0f; + style.GrabMinSize = 14.0f; + style.GrabRounding = 16.0f; + style.ScrollbarSize = 12.0f; + style.ScrollbarRounding = 16.0f; + style.SeparatorTextPadding = ImVec2(10, 10); + + style.Colors[ImGuiCol_Text] = text; + style.Colors[ImGuiCol_TextDisabled] = ImVec4(text.x, text.y, text.z, 0.28f); + style.Colors[ImGuiCol_WindowBg] = background; + style.Colors[ImGuiCol_Border] = ImVec4(0.31f, 0.31f, 1.00f, 0.13f); + style.Colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + style.Colors[ImGuiCol_FrameBg] = ImVec4(0.20f, 0.22f, 0.27f, 1.00f); + style.Colors[ImGuiCol_FrameBgHovered] = ImVec4(primary.x, primary.y, primary.z, 0.78f); + style.Colors[ImGuiCol_FrameBgActive] = primary; + style.Colors[ImGuiCol_TitleBg] = ImVec4(0.20f, 0.22f, 0.27f, 1.00f); + style.Colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.20f, 0.22f, 0.27f, 0.75f); + style.Colors[ImGuiCol_TitleBgActive] = primary; + style.Colors[ImGuiCol_MenuBarBg] = ImVec4(0.20f, 0.22f, 0.27f, 0.47f); + style.Colors[ImGuiCol_ScrollbarBg] = ImVec4(0.20f, 0.22f, 0.27f, 1.00f); + style.Colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.09f, 0.15f, 0.16f, 1.00f); + style.Colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(primary.x, primary.y, primary.z, 0.78f); + style.Colors[ImGuiCol_ScrollbarGrabActive] = primary; + style.Colors[ImGuiCol_CheckMark] = ImVec4(0.71f, 0.22f, 0.27f, 1.00f); + style.Colors[ImGuiCol_SliderGrab] = ImVec4(0.47f, 0.77f, 0.83f, 0.14f); + style.Colors[ImGuiCol_SliderGrabActive] = primary; + style.Colors[ImGuiCol_Button] = ImVec4(0.47f, 0.77f, 0.83f, 0.14f); + style.Colors[ImGuiCol_ButtonHovered] = ImVec4(primary.x, primary.y, primary.z, 0.86f); + style.Colors[ImGuiCol_ButtonActive] = primary; + style.Colors[ImGuiCol_Header] = ImVec4(primary.x, primary.y, primary.z, 0.76f); + style.Colors[ImGuiCol_HeaderHovered] = ImVec4(primary.x, primary.y, primary.z, 0.86f); + style.Colors[ImGuiCol_HeaderActive] = primary; + style.Colors[ImGuiCol_Separator] = ImVec4(0.14f, 0.16f, 0.19f, 1.00f); + style.Colors[ImGuiCol_SeparatorHovered] = ImVec4(primary.x, primary.y, primary.z, 0.78f); + style.Colors[ImGuiCol_SeparatorActive] = primary; + style.Colors[ImGuiCol_ResizeGrip] = ImVec4(0.47f, 0.77f, 0.83f, 0.04f); + style.Colors[ImGuiCol_ResizeGripHovered] = ImVec4(primary.x, primary.y, primary.z, 0.78f); + style.Colors[ImGuiCol_ResizeGripActive] = primary; + style.Colors[ImGuiCol_PlotLines] = ImVec4(text.x, text.y, text.z, 0.63f); + style.Colors[ImGuiCol_PlotLinesHovered] = primary; + style.Colors[ImGuiCol_PlotHistogram] = ImVec4(primary.x, primary.y, primary.z, 0.63f); + style.Colors[ImGuiCol_PlotHistogramHovered] = primary; + style.Colors[ImGuiCol_TextSelectedBg] = ImVec4(primary.x, primary.y, primary.z, 0.43f); + style.Colors[ImGuiCol_PopupBg] = ImVec4(0.20f, 0.22f, 0.27f, 0.9f); + style.Colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.20f, 0.22f, 0.27f, 0.73f); +} + +void App::renderGUI() { + rlImGuiBegin(); + + ImGui::Begin("Stats"); + ImGui::Text("FPS: %i", GetFPS()); + ImGui::Text("Frame time: %.2f ms", GetFrameTime() * 1000.0f); + if (ENABLE_AUTO_ADJUST_SUBSTEPS) { + ImGui::Text("Perceived time: %.2f ms", frameTimeSum / frameCounter * 1000.0f); + ImGui::Text("Sub-steps: %i", solver.substeps); + } + ImGui::Text("Objects: %i", (int)quadtree.getAll().size()); + ImGui::Text("Subdivisions: %i", quadtree.getSubdivisions()); + ImGui::Text("Collision checks: %i", solver.getAverageIterations()); + + ImGui::NewLine(); + ImGui::Separator(); + ImGui::NewLine(); + + ImGui::Checkbox("Render quadtree", &isRenderingQuadtree); + ImGui::Checkbox("Constraint to circle", &ENABLE_CIRCLE_CONSTRAINT); + if (ENABLE_CIRCLE_CONSTRAINT && ENABLE_TEMPERATURE) { + ImGui::Checkbox("Heat from circle border", &ENABLE_HEAT_FROM_CIRCLE_BORDER); + } + + ImGui::Checkbox("Auto adjust simulation quality", &ENABLE_AUTO_ADJUST_SUBSTEPS); + if (!ENABLE_AUTO_ADJUST_SUBSTEPS) { + ImGui::PushItemWidth(ImGui::GetWindowWidth() * 0.65f); + ImGui::SliderInt("Sub-steps", (int *)&solver.substeps, 1, 10); + } + + ImGui::Checkbox("Spread spawn", &ENABLE_SPAWN_SPREAD); + if (ImGui::Button("Kill objects")) { + quadtree.clear(); + } + if (quadtree.size() < SPAWN_COUNT) { + ImGui::SameLine(); + ImGui::Checkbox("Is spawning", &spawner.isSpawning); + } + + ImGui::NewLine(); + ImGui::Separator(); + ImGui::NewLine(); + ImGui::Text("Render mode"); + + if (ImGui::RadioButton("Solid color", ENABLE_SOLID_COLOR)) { + ENABLE_RAINBOW_COLORS = false; + ENABLE_FIXED_RAINBOW = false; + ENABLE_TEMPERATURE = false; + ENABLE_SOLID_COLOR = true; + } + if (ImGui::RadioButton("Rainbow on spawn", ENABLE_RAINBOW_COLORS)) { + ENABLE_FIXED_RAINBOW = false; + ENABLE_RAINBOW_COLORS = true; + ENABLE_SOLID_COLOR = false; + ENABLE_TEMPERATURE = false; + } + if (ImGui::RadioButton("Rainbow gradient", ENABLE_FIXED_RAINBOW)) { + ENABLE_RAINBOW_COLORS = false; + ENABLE_FIXED_RAINBOW = true; + ENABLE_SOLID_COLOR = false; + ENABLE_TEMPERATURE = false; + } + if (ImGui::RadioButton("Temperature", ENABLE_TEMPERATURE)) { + ENABLE_TEMPERATURE = true; + ENABLE_SOLID_COLOR = false; + ENABLE_FIXED_RAINBOW = false; + ENABLE_RAINBOW_COLORS = false; + } + + ImGui::NewLine(); + ImGui::Separator(); + ImGui::NewLine(); + + // Apply the size of the circle to the cursor + ImGui::Text("Gravity: %.2f", solver.gravity.Length()); + ImGui::SameLine(); + + // Draw custom circle with arrow + const float radius = ImGui::GetTextLineHeightWithSpacing(); + float arrowAngle = 30.0f * DEG2RAD; + const float arrowLength = radius / 2.5f; + const auto vec = solver.gravity.Normalize() * radius * 0.8f; + const auto center = raylib::Vector2( + ImGui::GetWindowPos().x + ImGui::GetContentRegionMax().x - radius, + ImGui::GetCursorScreenPos().y + radius + ); + const auto color = convert(ImGui::GetStyle().Colors[ImGuiCol_Text]); + const auto colorArrow = convert(ImGui::GetStyle().Colors[ImGuiCol_CheckMark]); + ImGui::GetWindowDrawList()->AddCircle(convert(center), radius, color); + ImGui::GetWindowDrawList()->AddLine(convert(center), convert(center + vec), colorArrow, 1.0f); + ImGui::GetWindowDrawList()->AddLine(convert(center + vec), convert(center + vec - solver.gravity.Normalize().Rotate(arrowAngle) * arrowLength), colorArrow, 1.0f); + ImGui::GetWindowDrawList()->AddLine(convert(center + vec), convert(center + vec - solver.gravity.Normalize().Rotate(-arrowAngle) * arrowLength), colorArrow, 1.0f); + + ImGui::NewLine(); + ImGui::Text("{ %.2f, %.2f }", solver.gravity.x, solver.gravity.y); + + ImGui::End(); + + if (ENABLE_TEMPERATURE) { + ImGui::Begin("Controls"); + ImGui::SliderFloat("Transfer contact", &solver.temperatureTransferContactMultiplier, 0.0f, 1.0f); + ImGui::SliderFloat("Transfer air", &solver.temperatureTransferAirMultiplier, 0.0f, 0.25f); + ImGui::SliderFloat("Transfer ground", &solver.temperatureTransferGroundMultiplier, 0.0f, 1.0f); + ImGui::SliderFloat("Floating force", &solver.temperatureFloatingForce, 0.0f, 100.0f); + ImGui::End(); + } + + if (ImGui::GetIO().WantCaptureMouse && (ImGui::IsAnyItemHovered() || ImGui::IsAnyItemActive())) { + SetMouseCursor(MOUSE_CURSOR_POINTING_HAND); + } else { + SetMouseCursor(MOUSE_CURSOR_DEFAULT); + } + + rlImGuiEnd(); +} \ No newline at end of file diff --git a/src/core/interface/palette.hpp b/src/core/interface/palette.hpp new file mode 100644 index 0000000..1a8decb --- /dev/null +++ b/src/core/interface/palette.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include "../precomp.hpp" + +const raylib::Color PALETTE_RED = raylib::Color(231, 76, 60); +const raylib::Color PALETTE_BLUE = raylib::Color(15, 188, 249); +const raylib::Color PALETTE_WHITE = raylib::Color(200, 214, 229); +const raylib::Color PALETTE_BLACK = raylib::Color(21, 21, 21); +const raylib::Color PALETTE_GREY = raylib::Color(33, 37, 41); \ No newline at end of file diff --git a/src/core/main.cpp b/src/core/main.cpp index 566d66d..1fddff9 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -1,40 +1,31 @@ #include "precomp.hpp" -#include "raylib.h" #include "settings.hpp" #include "models/app.hpp" App app; void render() { - BeginDrawing(); - ClearBackground(BLACK); - app.renderGUI(); - EndDrawing(); + if (IsWindowResized()) app.resize(); + + float start = GetTime(); + app.onFrameStart(); app.update(); - app.render(); + BeginDrawing(); + app.render(); + app.onFrameEnd(); + EndDrawing(); } int main() { SetConfigFlags(FLAG_MSAA_4X_HINT | FLAG_WINDOW_RESIZABLE | FLAG_WINDOW_HIGHDPI); #ifdef PLATFORM_WEB - InitWindow(WIDTH, HEIGHT, "Asteroids"); + InitWindow(WIDTH, HEIGHT, "Sandbox"); #else - const int monitor = GetCurrentMonitor(); - if (monitor > 0) { - WIDTH = GetMonitorWidth(monitor) / 2.0f; - HEIGHT = GetMonitorHeight(monitor) / 2.0f; - TARGET_FPS = GetMonitorRefreshRate(monitor); - - TraceLog(LOG_INFO, "Monitor: %i", monitor); - TraceLog(LOG_INFO, "Viewport: %i, %i", WIDTH, HEIGHT); - TraceLog(LOG_INFO, "Refresh Rate: %i", TARGET_FPS); - } - - InitWindow(WIDTH, HEIGHT, "Asteroids"); - SetWindowMinSize(WIDTH, HEIGHT); + InitWindow(WIDTH, HEIGHT, "Sandbox"); #endif + SetWindowMinSize(WIDTH, HEIGHT); app.setup(); diff --git a/src/core/models/app.cpp b/src/core/models/app.cpp index f759728..7a658c5 100644 --- a/src/core/models/app.cpp +++ b/src/core/models/app.cpp @@ -1,96 +1,138 @@ #include "app.hpp" +#include "../precomp.hpp" #include "../settings.hpp" -#include "RenderTexture.hpp" -#include "asteroid.hpp" -#include "imgui.h" -#include "polygon.hpp" +#include "../interface/palette.hpp" +#include "../utils.hpp" #include "raylib.h" -#include -#include + +App::App() : quadtree(raylib::Rectangle{ 0.0f, 0.0f, (float)WIDTH, (float)HEIGHT }), solver(quadtree), spawner(quadtree) { + camera.offset = raylib::Vector2{ 0.0f, 0.0f }; + camera.target = raylib::Vector2{ 0.0f, 0.0f }; + camera.zoom = 1.0f; + camera.rotation = 0.0f; +} App::~App() { - TraceLog(LOG_INFO, "App::~App()"); - rlImGuiShutdown(); + rlImGuiShutdown(); } -void App::shoot() { - Bullet bullet { - ship.position, - raylib::Vector2(cosf(ship.angle), sinf(ship.angle)) * BULLET_VELOCITY - }; - bullets.push_back(bullet); +void App::handleInput(float delta) { + if (ImGui::GetIO().WantCaptureKeyboard) return; + + // Rotate gravity vector + const float angleVelocity = 1.25f; + if (IsKeyDown(KEY_A) || IsKeyDown(KEY_LEFT)) { + solver.gravity = solver.gravity.Rotate(angleVelocity * delta); + } + if (IsKeyDown(KEY_D) || IsKeyDown(KEY_RIGHT)) { + solver.gravity = solver.gravity.Rotate(-angleVelocity * delta); + } + + // Increment/decrement gravity force + const auto steps = raylib::Vector2::One() * 2.5f * delta; + if (IsKeyDown(KEY_W) || IsKeyDown(KEY_UP)) { + solver.gravity += steps * solver.gravity.Normalize(); + } + if (IsKeyDown(KEY_S) || IsKeyDown(KEY_DOWN)) { + solver.gravity -= steps * solver.gravity.Normalize(); + } + + // Reverse gravity direction + if (IsKeyPressed(KEY_SPACE)) { + solver.gravity *= -1.0f; + } + + // Zero gravity + if (IsKeyPressed(KEY_Z)) { + if (solver.gravity.Length() == 0.0f) + solver.gravity = raylib::Vector2(0.0f, 9.8f); + else + solver.gravity = raylib::Vector2::Zero(); + } + + // Toggle rendering debug quadtree + if (IsKeyPressed(KEY_APOSTROPHE) || IsKeyPressed(KEY_GRAVE)) { + TraceLog(LOG_INFO, "app: Toggling quadtree rendering"); + isRenderingQuadtree = !isRenderingQuadtree; + } } void App::setup() { #ifdef DEBUG - SetTraceLogLevel(LOG_DEBUG); + SetTraceLogLevel(LOG_DEBUG); +#else + SetTraceLogLevel(LOG_WARNING); #endif - TraceLog(LOG_INFO, "App::setup()"); - - rlImGuiSetup(true); - ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_DockingEnable; - ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + TraceLog(LOG_INFO, "app: Starting..."); + SetRandomSeed(GetTime()); - ship.setup(); + setupGUI(); } -void App::updateAsteroids() { - Asteroid::collisionCount = 0; - for (auto &asteroid : asteroids.all()) asteroid->update(asteroids); - - asteroids.update(); -} +void App::update() { + auto delta = GetFrameTime(); -void App::updateBullets() { - if (shootTimer > BULLET_SHOOT_INTERVAL) shootTimer = BULLET_SHOOT_INTERVAL; - else shootTimer += GetFrameTime(); - if (shootTimer >= BULLET_SHOOT_INTERVAL && IsKeyDown(KEY_SPACE)) { - shoot(); - shootTimer -= BULLET_SHOOT_INTERVAL; - } + spawner.update(getRelativeMousePosition(), solver.substeps, delta, !ImGui::GetIO().WantCaptureMouse); + solver.solve(delta); - for (auto it = bullets.begin(); it != bullets.end();) { - if (it->update(asteroids)) { - it = bullets.erase(it); - } else { - it++; - } - } -} - -void App::update() { - updateAsteroids(); - updateBullets(); - ship.update(); - wave.update(asteroids, bullets, ship); + handleInput(delta); } void App::render() { - frameBuffer.BeginMode(); - ClearBackground(BLACK); - - for (auto &asteroid : asteroids.all()) asteroid->render(); - - for (auto &bullet : bullets) bullet.render(); - - ship.render(); - - // Render grid - asteroids.render(); + // Background + ClearBackground(PALETTE_GREY); + DrawCircle(GetScreenWidth() / 2, GetScreenHeight() / 2, CENTER_CIRCLE_CURRENT_RADIUS, PALETTE_BLACK); + + // Use the 2D camera to translate the simulation if it's smaller than the window + BeginMode2D(camera); + if (isRenderingQuadtree) { + quadtree.render(); + } else { + auto all = quadtree.getAll(); + for (auto object : all) { + object->render(); + } + } + EndMode2D(); + + renderGUI(); +} - frameBuffer.EndMode(); +void App::resize() { + // Ensure the simulation is centered + camera.target = raylib::Vector2( + WIDTH / 2.0f - GetScreenWidth() / 2.0f, + HEIGHT / 2.0f - GetScreenHeight() / 2.0f + ); } -void App::onResize(uint32_t width, uint32_t height) { - TraceLog(LOG_INFO, "App::onResize(%i, %i)", width, height); - frameBuffer = raylib::RenderTexture2D(width, height); +void App::onFrameStart() { + frameStartTime = GetTime(); +} - // Update global settings - WIDTH = width; - HEIGHT = height; +void App::onFrameEnd() { + if (!ENABLE_AUTO_ADJUST_SUBSTEPS) return; + + const float frameTime = GetTime() - frameStartTime; + frameCounter++; + frameTimeSum += frameTime; + + if (frameCounter <= TARGET_FPS) return; + + // Detect slowness and adjust simulation substeps accordingly + float expectedFrameTime = (1.0f / TARGET_FPS); + float averageFrameTime = (frameTimeSum / frameCounter); + if (solver.substeps > 1 && averageFrameTime > expectedFrameTime) { + TraceLog(LOG_WARNING, "Detected slowness, decreasing simulation substeps. Simulation time: %.2fms (%.2fms expected)", averageFrameTime, expectedFrameTime); + solver.substeps --; + if (solver.substeps < 1) solver.substeps = 1; + } else if (solver.substeps < 10 && expectedFrameTime - averageFrameTime >= 0.5f * expectedFrameTime) { + TraceLog(LOG_DEBUG, "Detected under utilization, increasing simulation substeps. Simulation time: %.2fms (%.2fms expected)", averageFrameTime, expectedFrameTime); + solver.substeps ++; + if (solver.substeps > 10) solver.substeps = 10; + } - // Update the grid - const auto size = (float)ASTEROID_RADIUS * 4; - asteroids.resize(ceilf(HEIGHT / size), ceilf(WIDTH / size)); -} + frameCounter = 0; + frameTimeSum = 0; +} \ No newline at end of file diff --git a/src/core/models/app.hpp b/src/core/models/app.hpp index 110459c..eec212b 100644 --- a/src/core/models/app.hpp +++ b/src/core/models/app.hpp @@ -1,34 +1,37 @@ #pragma once #include "../precomp.hpp" -#include "bullet.hpp" -#include "ship.hpp" -#include "asteroid.hpp" -#include "wave.hpp" -#include "../data/spatial-hash-grid.hpp" +#include "quadtree.hpp" +#include "solver.hpp" +#include "interation_handler.hpp" class App { private: - raylib::RenderTexture2D frameBuffer; + Solver solver; + Camera2D camera; + Quadtree quadtree; + InterationHandler spawner; - SpatialHashGrid asteroids; - std::list bullets; - float shootTimer; - Ship ship; + bool isRenderingQuadtree; - WaveController wave; + float frameStartTime; + uint8_t frameCounter; + float frameTimeSum; - void shoot(); - void updateAsteroids(); - void updateBullets(); + void handleInput(float deltaTime); + void renderGUI(); + void setupGUI(); public: - App() = default; + + App(); ~App(); + void onFrameStart(); + void onFrameEnd(); + void setup(); void update(); void render(); - void onResize(uint32_t width, uint32_t height); - void renderGUI(); + void resize(); }; \ No newline at end of file diff --git a/src/core/models/asteroid.cpp b/src/core/models/asteroid.cpp deleted file mode 100644 index cdc535e..0000000 --- a/src/core/models/asteroid.cpp +++ /dev/null @@ -1,211 +0,0 @@ -#include "asteroid.hpp" -#include "../utils.hpp" -#include "polygon.hpp" - -size_t Asteroid::idCounter = 0; -uint32_t Asteroid::collisionCount = 0; - -#pragma region SAT -struct CollisionInfo { - raylib::Vector2 normal; - float penetration; -}; - -struct Projection { - float min; - float max; -}; - -inline std::vector getGlobalVertices(const Asteroid &a) { - std::vector vertices(a.polygon.vertices.size()); - for (size_t i = 0; i < a.polygon.vertices.size(); i++) { - vertices[i] = rotateAround(a.position + a.polygon.vertices[i], a.position, a.angle); - } - return vertices; -} - -void getAxes(const std::vector &vertices, std::vector &axes) { - for (size_t i = 0; i < vertices.size(); i++) { - const auto p1 = vertices[i]; - const auto p2 = vertices[(i + 1) % vertices.size()]; - const auto edge = p1 - p2; - axes.emplace_back(-edge.y, edge.x); - } -} - -Projection project(const std::vector &vertices, const raylib::Vector2 &axis) { - float min = axis.DotProduct(vertices[0]); - float max = min; - for (size_t i = 1; i < vertices.size(); i++) { - const float p = axis.DotProduct(vertices[i]); - if (p < min) min = p; - else if (p > max) max = p; - } - return { min, max }; -} - -std::optional getCollisionInfo(const Asteroid &a, const Asteroid &b) { - auto normal = raylib::Vector2::Zero(); - float overlap = std::numeric_limits::max(); - - // Get the global world position of the vertices - const auto verticesA = getGlobalVertices(a); - const auto verticesB = getGlobalVertices(b); - - // Get the axes of the polygons - std::vector axes; - getAxes(verticesA, axes); - getAxes(verticesB, axes); - - // Iterate through the axes - for (auto it = axes.begin(); it != axes.end(); it++) { - const auto axis = *it; - const auto projectionA = project(verticesA, axis); - const auto projectionB = project(verticesB, axis); - - // Detected a gap, no collision - if (projectionA.max < projectionB.min || projectionB.max < projectionA.min) { - return std::nullopt; - } - - // Calculate the overlap - const float depth = std::min(projectionA.max, projectionB.max) - std::max(projectionA.min, projectionB.min); - if (depth < overlap) { - overlap = depth; - normal = axis; - } - } - - // Normalize the depth and normal - const auto length = normal.Length(); - normal /= length; - overlap /= length; - - // If the normal is pointing from A to B, invert it - const auto center = b.position - a.position; - if (center.DotProduct(normal) < 0) { - normal = -normal; - } - - return CollisionInfo{ normal, overlap }; -} -#pragma endregion - -void Asteroid::updatePhysics() { - const auto deltaTime = GetFrameTime(); - - position += velocity * deltaTime; - angle += angularVelocity * deltaTime; -} - -void Asteroid::wrapAroundScreen() { - if (position.x - polygon.outerRadius > WIDTH) { - position.x = -polygon.outerRadius; - } else if (position.x < -polygon.outerRadius) { - position.x = WIDTH + polygon.outerRadius; - } - - if (position.y - polygon.outerRadius > HEIGHT) { - position.y = -polygon.outerRadius; - } else if (position.y < -polygon.outerRadius) { - position.y = HEIGHT + polygon.outerRadius; - } -} - -void Asteroid::checkForCollisions(IContainer &asteroids) { - const auto others = asteroids.retrieve(position, polygon.outerRadius * 2); - for (auto &other : others) { - if (id == other->id) continue; - - // Check if asteroids are close enough to collide - // const float distanceLength = (position - other->position).Length(); - // const float minimumDistance = powf(polygon.outerRadius + other->polygon.outerRadius, 2.0f); - // if (distanceLength > minimumDistance) continue; - - // Time to bring out the big guns - const auto contact = getCollisionInfo(*this, *other.get()); - if (contact.has_value()) { - Asteroid::collisionCount++; - // Move the asteroids apart - const auto correction = contact->normal * contact->penetration; - position -= correction / 2.0f; - other->position += correction / 2.0f; - - // Calculate the new velocities - const auto relativeVelocity = velocity - other->velocity; - const auto velocityAlongNormal = relativeVelocity.DotProduct(contact->normal); - - // Use asteroid.radius / ASTEROID_RADIUS for mass - const float e = 1.0f; // Coefficient of restitution - const float j = -(1 + e) * velocityAlongNormal; - const float mass1 = polygon.outerRadius / ASTEROID_RADIUS; - const float mass2 = other->polygon.outerRadius / ASTEROID_RADIUS; - const float impulse = j / (mass1 + mass2); - velocity += contact->normal * impulse * mass1; - other->velocity -= contact->normal * impulse * mass2; - - // Ensure the asteroids are not stationary - if (velocity.Length() < 0.1f) velocity = raylib::Vector2::One() * getRandomValue(-1.0f, 1.0f) * ASTEROID_VELOCITY; - if (other->velocity.Length() < 0.1f) other->velocity = raylib::Vector2::One() * getRandomValue(-1.0f, 1.0f) * ASTEROID_VELOCITY; - - // Ensure the asteroids are not too fast - if (velocity.Length() > ASTEROID_VELOCITY) velocity = velocity.Normalize() * ASTEROID_VELOCITY; - if (other->velocity.Length() > ASTEROID_VELOCITY) other->velocity = other->velocity.Normalize() * ASTEROID_VELOCITY; - - // Calculate the new angular velocities - const float angularVelocityAlongNormal = angularVelocity - other->angularVelocity; - const float torque = angularVelocityAlongNormal * mass1; - const float angularImpulse = torque / (mass1 + mass2); - angularVelocity -= angularImpulse * mass1; - other->angularVelocity += angularImpulse * mass2; - - // Ensure the asteroids are not stationary - if (fabsf(angularVelocity) < 0.1f) angularVelocity = getRandomValue(-1.0f, 1.0f) * ASTEROID_ANGULAR_VELOCITY; - if (fabsf(other->angularVelocity) < 0.1f) other->angularVelocity = getRandomValue(-1.0f, 1.0f) * ASTEROID_ANGULAR_VELOCITY; - - // Ensure the asteroids are not too fast - if (fabsf(angularVelocity) > ASTEROID_ANGULAR_VELOCITY) angularVelocity = ASTEROID_ANGULAR_VELOCITY * (angularVelocity < 0 ? -1 : 1); - if (fabsf(other->angularVelocity) > ASTEROID_ANGULAR_VELOCITY) other->angularVelocity = ASTEROID_ANGULAR_VELOCITY * (other->angularVelocity < 0 ? -1 : 1); - - } - } -} - -void Asteroid::updateAnimations() { - // Scale up the asteroid - scale += GetFrameTime() * 2.0f; // 500ms - if (scale > 1.0f) scale = 1.0f; -} - -Asteroid::Asteroid() { - id = Asteroid::idCounter++; - polygon.generateVertices(ASTEROID_RADIUS, GetRandomValue(ASTEROID_MIN_VERTEX_COUNT, ASTEROID_MAX_VERTEX_COUNT)); - position = raylib::Vector2(GetRandomValue(0, WIDTH), GetRandomValue(0, HEIGHT)); - velocity = raylib::Vector2::One() * getRandomValue(-1.0f, 1.0f) * ASTEROID_VELOCITY; - angle = getRandomValue(0.0f, 360.0f) * DEG2RAD; - angularVelocity = getRandomValue(-1.0f, 1.0f) * ASTEROID_ANGULAR_VELOCITY; -} - -Asteroid::Asteroid(Polygon &&polygon, raylib::Vector2 position) : polygon(polygon), position(position) { - id = Asteroid::idCounter++; - angle = getRandomValue(0.0f, 360.0f) * DEG2RAD; - angularVelocity = getRandomValue(-1.0f, 1.0f) * ASTEROID_ANGULAR_VELOCITY; - scale = 1.0f; -} - -void Asteroid::update(IContainer &others) { - updatePhysics(); - if (scale >= 1.0f) checkForCollisions(others); - else updateAnimations(); - wrapAroundScreen(); -} - -void Asteroid::render() { - polygon.render(position, easeInOutBack(scale), angle, color); - -#ifdef DEBUG - auto text = TextFormat("%i", index); - auto size = MeasureTextEx(GetFontDefault(), text, 12, 1); - DrawText(text, position.x - size.x / 2.0f, position.y - size.y / 2.0f, 12, LIGHTGRAY); -#endif -} diff --git a/src/core/models/asteroid.hpp b/src/core/models/asteroid.hpp deleted file mode 100644 index 79b4dbd..0000000 --- a/src/core/models/asteroid.hpp +++ /dev/null @@ -1,40 +0,0 @@ -#pragma once - -#include "../precomp.hpp" -#include "polygon.hpp" -#include "../data/icontainer.hpp" - -class Asteroid { - - public: - static size_t idCounter; - static uint32_t collisionCount; - - uint32_t index; - size_t id; - - Polygon polygon; - raylib::Vector2 position; - raylib::Vector2 velocity; - float angle; - float angularVelocity; - raylib::Color color = raylib::Color::White(); - - // Animation - float scale; - - void updatePhysics(); - void wrapAroundScreen(); - - public: - Asteroid(); - Asteroid(Polygon &&polygon, raylib::Vector2 position); - ~Asteroid() = default; - - void update(IContainer &others); - void render(); - - private: - void checkForCollisions(IContainer &others); - void updateAnimations(); -}; \ No newline at end of file diff --git a/src/core/models/bullet.cpp b/src/core/models/bullet.cpp deleted file mode 100644 index b4eccf0..0000000 --- a/src/core/models/bullet.cpp +++ /dev/null @@ -1,47 +0,0 @@ -#include "bullet.hpp" -#include "../settings.hpp" -#include "asteroid.hpp" - -bool Bullet::update(IContainer &asteroids) { - // Update physics - position = position + velocity * GetFrameTime(); - - // Check if outside screen bounds - if (position.x < 0 || position.x > WIDTH || position.y < 0 || position.y > HEIGHT) { - return true; - } - - // Check for collisions - const auto list = asteroids.retrieve(position, ASTEROID_RADIUS * 2); - for (auto &asteroid : list) { - if (position.Distance(asteroid->position) < asteroid->polygon.outerRadius) { - if (asteroid->polygon.outerRadius >= ASTEROID_MIN_RADIUS_TO_SPLIT) { - // Split the asteroid - auto offset = raylib::Vector2::Zero(); - for (auto &poly : asteroid->polygon.split()) { - auto r = raylib::Vector2::One() * poly.outerRadius; - - const auto newAsteroid = std::make_shared(std::move(poly), asteroid->position + offset - r); - asteroids.insert(newAsteroid); - - // Calculate the velocity of the new asteroid - float angle = atan2f(newAsteroid->position.y - position.y, newAsteroid->position.x - position.x); - newAsteroid->velocity = asteroid->velocity + raylib::Vector2(cosf(angle), sinf(angle)) * ASTEROID_VELOCITY; - - offset += r; - } - } - - // Kill the asteroid - asteroids.remove(asteroid); - - return true; - } - } - - return false; -} - -void Bullet::render() { - DrawCircleV(position, BULLET_RADIUS, WHITE); -} diff --git a/src/core/models/bullet.hpp b/src/core/models/bullet.hpp deleted file mode 100644 index 82055d3..0000000 --- a/src/core/models/bullet.hpp +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#include "../precomp.hpp" -#include "../data/spatial-hash-grid.hpp" - -struct Bullet { - raylib::Vector2 position; - raylib::Vector2 velocity; - - bool update(IContainer &asteroids); - void render(); -}; \ No newline at end of file diff --git a/src/core/models/collision-handler.hpp b/src/core/models/collision-handler.hpp deleted file mode 100644 index d0c855d..0000000 --- a/src/core/models/collision-handler.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -// #include "../precomp.hpp" - -// namespace CollisionHandler { -// struct CollisionInfo { -// raylib::Vector2 normal; -// float penetration; -// }; - -// struct Shape { -// std::vector vertices; -// raylib::Vector2 position; -// float radius; -// }; - -// /** -// * @brief Implements the Separating Axis Theorem (SAT) to detect collisions between two polygons -// * -// * @param a The first polygon -// * @param b The second polygon -// * @return std::optional The collision info if a collision is detected, otherwise std::nullopt -// */ -// std::optional getCollisionInfo(const Shape &a, const Shape &b); -// } \ No newline at end of file diff --git a/src/core/models/collision-hanlder.cpp b/src/core/models/collision-hanlder.cpp deleted file mode 100644 index f0c48c1..0000000 --- a/src/core/models/collision-hanlder.cpp +++ /dev/null @@ -1,73 +0,0 @@ -// #include "collision-handler.hpp" - -// using namespace CollisionHandler; - -// struct Projection { -// float min; -// float max; -// }; - -// void getAxes(const std::vector &vertices, std::vector &axes) { -// for (size_t i = 0; i < vertices.size(); i++) { -// const auto p1 = vertices[i]; -// const auto p2 = vertices[(i + 1) % vertices.size()]; -// const auto edge = p1 - p2; -// axes.emplace_back(-edge.y, edge.x); // Perpendicular vector -// } -// } - -// Projection project(const std::vector &vertices, const raylib::Vector2 &axis) { -// float min = axis.DotProduct(vertices[0]); -// float max = min; -// for (size_t i = 1; i < vertices.size(); i++) { -// const float p = axis.DotProduct(vertices[i]); -// if (p < min) -// min = p; -// else if (p > max) -// max = p; -// } - -// return { min, max }; -// } - -// std::optional CollisionHandler::getCollisionInfo(const Shape &a, const Shape &b) { -// auto normal = raylib::Vector2::Zero(); -// float overlap = std::numeric_limits::max(); - -// // Get the axes of the polygons -// std::vector axes; -// axes.reserve(a.vertices.size() + b.vertices.size()); -// getAxes(a.vertices, axes); -// getAxes(b.vertices, axes); - -// // Iterate through the axes -// for (auto &axis : axes) { -// const auto projectionA = project(a.vertices, axis); -// const auto projectionB = project(b.vertices, axis); - -// // Detected a gap, no collision -// if (projectionA.max < projectionB.min || projectionB.max < projectionA.min) { -// return std::nullopt; -// } - -// // Calculate the overlap -// const float depth = std::min(projectionA.max, projectionB.max) - std::max(projectionA.min, projectionB.min); -// if (depth < overlap) { -// overlap = depth; -// normal = axis; -// } -// } - -// // Normalize the depth and normal -// const auto length = normal.Length(); -// normal /= length; -// overlap /= length; - -// // If the normal is pointing from A to B, invert it -// const auto center = b.position - a.position; -// if (center.DotProduct(normal) < 0) { -// normal = -normal; -// } - -// return CollisionInfo{ normal, overlap }; -// } \ No newline at end of file diff --git a/src/core/models/gui.cpp b/src/core/models/gui.cpp deleted file mode 100644 index adf6633..0000000 --- a/src/core/models/gui.cpp +++ /dev/null @@ -1,81 +0,0 @@ -#include "app.hpp" -#include "asteroid.hpp" -#include "imgui.h" -#include "raylib.h" - -void App::renderGUI() { - rlImGuiBegin(); - - ImGui::DockSpaceOverViewport(NULL, ImGuiDockNodeFlags_PassthruCentralNode); - - ImGui::Begin("Viewport"); - if (frameBuffer.IsReady()) rlImGuiImageRenderTexture(&frameBuffer); - auto min = ImGui::GetWindowContentRegionMin(); - auto max = ImGui::GetWindowContentRegionMax(); - auto size = ImVec2(max.x - min.x, max.y - min.y); - if (frameBuffer.texture.id <= 0 || size.x != frameBuffer.texture.width || size.y != frameBuffer.texture.height) { - onResize(size.x, size.y); - } - ImGui::End(); - - ImGui::Begin("Stats"); - ImGui::Text("FPS: %i", GetFPS()); - ImGui::Text("Frame Time: %.2f ms", GetFrameTime() * 1000); - ImGui::End(); - - ImGui::Begin("Ship"); - ImGui::Text("Position: { %.2f, %.2f }", ship.position.x, ship.position.y); - ImGui::Text("Velocity: { %.2f, %.2f }", ship.velocity.x, ship.velocity.y); - ImGui::Text("Acceleration: { %.2f, %.2f }", ship.acceleration.x, ship.acceleration.y); - ImGui::Text("Angle: %.2f", ship.angle * RAD2DEG); - ImGui::Text("Angular Velocity: %.2f", ship.angularVelocity * RAD2DEG); - ImGui::End(); - - ImGui::Begin("Asteroids"); - ImGui::Text("Alive: %i", asteroids.size()); - ImGui::Text("Collisions: %i", Asteroid::collisionCount); - if (ImGui::Button("Kill all")) { - asteroids.clear(); - } - int i = 0; - for (auto &asteroid : asteroids.all()) { - if (ImGui::TreeNode((void *)(intptr_t)i, "Asteroid %i", i)) { - ImGui::Text("Position: { %.2f, %.2f }", asteroid->position.x, asteroid->position.y); - ImGui::Text("Velocity: { %.2f, %.2f }", asteroid->velocity.x, asteroid->velocity.y); - ImGui::Text("Angle: %.2f", asteroid->angle * RAD2DEG); - ImGui::Text("Angular Velocity: %.2f", asteroid->angularVelocity * RAD2DEG); - ImGui::Text("Radius: %.2f", asteroid->polygon.outerRadius); - ImGui::Text("Vertices: %zu", asteroid->polygon.vertices.size()); - for (size_t j = 0; j < asteroid->polygon.vertices.size(); j++) { - ImGui::Text("Vertex %zu: { %.2f, %.2f }", j, asteroid->polygon.vertices[j].x, asteroid->polygon.vertices[j].y); - } - ImGui::TreePop(); - } - - i++; - } - ImGui::End(); - - ImGui::Begin("Bullets"); - ImGui::Text("Alive: %zu", bullets.size()); - i = 0; - for (auto &bullet : bullets) { - if (ImGui::TreeNode((void *)(intptr_t)i, "Bullet %i", i)) { - ImGui::Text("Position: { %.2f, %.2f }", bullet.position.x, bullet.position.y); - ImGui::Text("Velocity: { %.2f, %.2f }", bullet.velocity.x, bullet.velocity.y); - ImGui::TreePop(); - } - - i++; - } - ImGui::End(); - - ImGui::Begin("Wave"); - ImGui::Text("Current: %i", wave.currentWave); - ImGui::Text("Time Since Last Wave: %.2f", wave.timeSinceLastWave); - ImGui::Text("Time Since Last Spawn: %.2f", wave.timeSinceLastSpawn); - ImGui::Text("Asteroids Spawned: %i", wave.asteroidsSpawned); - ImGui::End(); - - rlImGuiEnd(); -} \ No newline at end of file diff --git a/src/core/models/interation_handler.cpp b/src/core/models/interation_handler.cpp new file mode 100644 index 0000000..3bbdf03 --- /dev/null +++ b/src/core/models/interation_handler.cpp @@ -0,0 +1,99 @@ +#include "interation_handler.hpp" +#include "../settings.hpp" +#include "../interface/palette.hpp" + +InterationHandler::InterationHandler(Quadtree &quadtree) : quadtree(quadtree) { } + +void InterationHandler::spawn(raylib::Vector2 mouse, uint8_t substeps) { + const int count = 4; + const float mass = 100.0f; + const float temperature = 0.0f; + raylib::Vector2 position; + raylib::Vector2 acceleration; + + for (int i = 0; i < count; i++) { + if (isSpawning) { + position = raylib::Vector2(WIDTH / 2.0f, CENTER_CIRCLE_RADIUS / 2.0f); + acceleration = (raylib::Vector2(cosf(angle), sinf(angle) * 0.5f) * 500.0f * mass) * (float)substeps; + } else { + position = mouse; + } + + position.x += i * (OBJECT_RADIUS * 2.5f) - (count - 1) * (OBJECT_RADIUS * 1.5f); + + raylib::Color color; + if (ENABLE_RAINBOW_COLORS) { + color = raylib::Color(raylib::Vector3((quadtree.size() % 361 / 360.0f) * 360.0f, 1.0f, 1.0f)); + } else { + color = PALETTE_BLUE; + } + + auto object = std::make_shared(position, acceleration, mass, OBJECT_RADIUS, temperature, color); + quadtree.add(object); + } +} + +void InterationHandler::update(raylib::Vector2 mouse, uint8_t substeps, float deltaTime, bool consumeInput) { + if (isSpawning) { + timer += deltaTime; + if (timer >= SPAWN_INTERVAL) { + timer -= SPAWN_INTERVAL; + if (ENABLE_SPAWN_SPREAD) + angle = Lerp(angle, sinf(GetTime()) * 0.25f * PI + 0.5f * PI, deltaTime * 10); + else + angle = Lerp(angle, 0.5f * PI, deltaTime * 10); + + spawn(raylib::Vector2::Zero(), substeps); + + if (quadtree.size() >= SPAWN_COUNT) { + isSpawning = false; + timer = 0; + } + } + + return; + } + + if (!consumeInput) return; + + // Manual spawn + if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) {// Click + spawn(mouse, substeps); + timer = 0; + } else if (IsMouseButtonDown(MOUSE_LEFT_BUTTON)) {// Hold + timer += deltaTime; + if (timer >= MANUAL_SPAWN_INTERVAL) { + timer -= MANUAL_SPAWN_INTERVAL; + spawn(mouse, substeps); + } + } + + if (IsMouseButtonDown(MOUSE_RIGHT_BUTTON)) {// Dragging + if (draggingObjects.size() == 0) { + float OBJECT_RADIUS = 50.0f; + auto objects = quadtree.query(raylib::Rectangle{ mouse.x - OBJECT_RADIUS, mouse.y - OBJECT_RADIUS, OBJECT_RADIUS * 2.0f, OBJECT_RADIUS * 2.0f }); + + for (auto object : objects) { + draggingObjects.push_back(object); + } + } + + for (auto object : draggingObjects) { + object->acceleration = (mouse - object->position).Normalize() * DRAGGING_ACCELERATION; + } + } else { + draggingObjects.clear(); + } + + if (IsMouseButtonReleased(MOUSE_BUTTON_MIDDLE)) {// Explosion + auto objects = quadtree.query(raylib::Rectangle{ mouse.x - EXPLOSION_RADIUS, mouse.y - EXPLOSION_RADIUS, EXPLOSION_RADIUS * 2.0f, EXPLOSION_RADIUS * 2.0f }); + for (auto object : objects) { + auto direction = object->position - mouse; + auto distance = direction.Length(); + if (distance < EXPLOSION_RADIUS) { + auto normal = direction / distance; + object->acceleration += normal * EXPLOSION_FORCE; + } + } + } +} \ No newline at end of file diff --git a/src/core/models/interation_handler.hpp b/src/core/models/interation_handler.hpp new file mode 100644 index 0000000..d42ec0c --- /dev/null +++ b/src/core/models/interation_handler.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "../precomp.hpp" +#include "quadtree.hpp" +#include "object.hpp" + +class InterationHandler { + private: + Quadtree &quadtree; + std::vector> draggingObjects; + + float angle = 90.0f * DEG2RAD; + float timer = 0.0f; + + void spawn(raylib::Vector2 mouse, uint8_t substeps); + + public: + bool isSpawning = true; + + InterationHandler(Quadtree &quadtree); + + void update(raylib::Vector2 mouse, uint8_t substeps, float deltaTime, bool consumeInput); +}; diff --git a/src/core/models/object.cpp b/src/core/models/object.cpp new file mode 100644 index 0000000..ead4f03 --- /dev/null +++ b/src/core/models/object.cpp @@ -0,0 +1,67 @@ +#include "object.hpp" +#include "../settings.hpp" +#include "../utils.hpp" + +Object::Object(raylib::Vector2 position, raylib::Vector2 acceleration, float mass, float radius, float temperature, raylib::Color color) { + this->position = position; + previousPosition = position; + this->acceleration = acceleration; + this->mass = mass; + this->radius = radius; + this->temperature = temperature; + this->color = color; +} + +Object::Object() { + position = raylib::Vector2::Zero(); + acceleration = raylib::Vector2::Zero(); +} + +void Object::update(float deltaTime) { + auto displacement = position - previousPosition; + previousPosition = position; + position += displacement + acceleration * deltaTime * deltaTime; + + acceleration = raylib::Vector2::Zero(); +} + +raylib::Color getColorFromTemperature(float temperature) { + // Using Kelvin scale + // https://en.wikipedia.org/wiki/Color_temperature + + // 1000K - 4000K + if (temperature < 4000.0f) { + auto t = temperature / 4000.0f; + return raylib::Color{ (uint8_t)(128.0f * t), 0, 0, 255 }; + } + + // 4000K - 7000K + if (temperature < 7000.0f) { + auto t = (temperature - 4000.0f) / 3000.0f; + return raylib::Color{ (uint8_t)(128.0f + 127.0f * t), (uint8_t)(255.0f * t), 0, 255 }; + } + + // 7000K - 10000K + if (temperature < 10000.0f) { + auto t = (temperature - 7000.0f) / 3000.0f; + return raylib::Color{ 255, 255, (uint8_t)(255.0f * t), 255 }; + } + + return WHITE; +} + +void Object::render() { + if (ENABLE_TEMPERATURE) { + position.DrawCircle(radius, getColorFromTemperature(temperature)); + } else if (ENABLE_FIXED_RAINBOW) { + const int hue = (position.y / ((float)GetScreenHeight() / 2.0f)) * 240.0f; + DrawCircleV(position, radius, raylib::Color(raylib::Vector3(hue, 1.0f, 1.0f))); + } else { + DrawCircleV(position, radius, color); + } +} + +raylib::Rectangle Object::getBounds() { + auto r = radius; + return raylib::Rectangle{ position.x - r, position.y - r, r * 2, r * 2 }; +} diff --git a/src/core/models/object.hpp b/src/core/models/object.hpp new file mode 100644 index 0000000..92585e6 --- /dev/null +++ b/src/core/models/object.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "../precomp.hpp" +#include "quadtree.hpp" + +class Quadtree; + +class Object { + private: + raylib::Vector2 previousPosition; + + public: + raylib::Vector2 position; + raylib::Vector2 acceleration; + + Quadtree *quadrant = nullptr; + + float mass; + float radius; + float temperature; + + raylib::Color color = WHITE; + + Object(raylib::Vector2 position, raylib::Vector2 acceleration, float mass, float radius, float temperature, raylib::Color color); + Object(); + + void update(float deltaTime); + void render(); + + raylib::Rectangle getBounds(); +}; \ No newline at end of file diff --git a/src/core/models/polygon.cpp b/src/core/models/polygon.cpp deleted file mode 100644 index 08c081a..0000000 --- a/src/core/models/polygon.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#include "polygon.hpp" -#include "../settings.hpp" -#include "../utils.hpp" -#include "Vector2.hpp" -#include "raylib.h" -#include - -void Polygon::generateVertices(float radius, uint8_t vertexCount) { - outerRadius = 0.0f; - innerRadius = 0.0f; - - float scale = radius / ASTEROID_RADIUS; - - for (int i = 0; i < vertexCount; i++) { - float angle = (i / (float)vertexCount) * 2.0f * PI; - - float r = radius + GetRandomValue(-ASTEROID_JAGGEDNESS * scale, ASTEROID_JAGGEDNESS * scale); - - // Keep track of the largest radius - if (outerRadius == 0.0f || r > outerRadius) outerRadius = r; - - // Keep track of the smallest radius - if (innerRadius == 0.0f || r < innerRadius) innerRadius = r; - - vertices.emplace_back(cosf(angle) * r, sinf(angle) * r); - } -} - -void Polygon::render(raylib::Vector2 center, float scale, float angle, raylib::Color color) { - for (size_t i = 0; i < vertices.size(); i++) { - size_t j = (i + 1) % vertices.size(); - DrawLineEx( - rotateAround(center + vertices[i] * scale, center, angle), - rotateAround(center + vertices[j] * scale, center, angle), - 1.5f, - color - ); - } - -// #ifdef DEBUG -// DrawCircleLinesV(center, outerRadius, LIGHTGRAY); -// DrawCircleLinesV(center, innerRadius, GRAY); -// #endif -} - -std::vector Polygon::split() { - std::vector buffer; - - float radius = ceilf(outerRadius / (float)(ASTEROID_FRAGMENTS_COUNT - 1)); - uint8_t vertexCount = GetRandomValue(ASTEROID_MIN_VERTEX_COUNT, ASTEROID_MAX_VERTEX_COUNT); - for (size_t i = 0; i < ASTEROID_FRAGMENTS_COUNT; i++) { - buffer.emplace_back(); - buffer.back().generateVertices(radius, vertexCount); - } - - return buffer; -} \ No newline at end of file diff --git a/src/core/models/polygon.hpp b/src/core/models/polygon.hpp deleted file mode 100644 index 5177c1c..0000000 --- a/src/core/models/polygon.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include "../precomp.hpp" -#include "Vector2.hpp" -#include -#include - -class Polygon { - public: - std::vector vertices; - float innerRadius; - float outerRadius; - - void generateVertices(float radius, uint8_t vertexCount); - void render(raylib::Vector2 center, float scale, float angle, raylib::Color color); - std::vector split(); -}; \ No newline at end of file diff --git a/src/core/models/quadtree.cpp b/src/core/models/quadtree.cpp new file mode 100644 index 0000000..204bd9e --- /dev/null +++ b/src/core/models/quadtree.cpp @@ -0,0 +1,217 @@ + +#if !defined(QUADTREE_DUMMY) && !defined(QUADTREE_GRID) + +#include "quadtree.hpp" +#include "../settings.hpp" + +Quadtree::Quadtree(raylib::Rectangle bounds, uint8_t depth) : bounds(bounds), depth(depth) { + color = raylib::Color { + (uint8_t) GetRandomValue(128, 255), + (uint8_t) GetRandomValue(128, 255), + (uint8_t) GetRandomValue(128, 255), + 255 + }; +} + +bool Quadtree::isLeaf() { + return quadrants.empty(); +} + +void Quadtree::split() { + auto x = bounds.x, y = bounds.y, w = bounds.width / 2, h = bounds.height / 2; + + // North-West + quadrants.push_back(std::make_shared(raylib::Rectangle{ x, y, w, h }, depth + 1)); + // North-East + quadrants.push_back(std::make_shared(raylib::Rectangle{ x + w, y, w, h }, depth + 1)); + // South-East + quadrants.push_back(std::make_shared(raylib::Rectangle{ x + w, y + h, w, h }, depth + 1)); + // South-West + quadrants.push_back(std::make_shared(raylib::Rectangle{ x, y + h, w, h }, depth + 1)); + + // Move children to quadrants + for (auto node : children) { + // bool found = false; + for (auto quadrant : quadrants) { + if (quadrant->add(node)) { + // found = true; + break; + } + } + + // if (!found) TraceLog(LOG_WARNING, "Failed to add object to quadrant"); + } + + children.clear(); +} + +void Quadtree::merge() { + if (isLeaf()) return; + + for (auto quadrant : quadrants) { + // Merge children + quadrant->merge(); + + // Move children to parent + for (auto node : quadrant->children) { + children.push_back(node); + } + quadrant->children.clear(); + } + + // Clear quadrants, make it a leaf + quadrants.clear(); +} + +bool Quadtree::add(std::shared_ptr object) { + if (!bounds.CheckCollision(object->getBounds())) return false; + + if (isLeaf()) { + if (children.size() < MAX_OBJECTS || depth >= MAX_SUBDIVISIONS) { + object->quadrant = this; + children.push_back(object); + return true; + } + + split(); + } + + if (!isLeaf()) { + for (auto quadrant : quadrants) { + if (quadrant->add(object)) return true; + } + } + + return false; +} + +bool Quadtree::remove(std::shared_ptr object) { + if (isLeaf()) { + for (auto it = children.begin(); it != children.end(); it++) { + if (*it == object) { + object->quadrant = nullptr; + children.erase(it); + return true; + } + } + + return false; + } + + for (auto quadrant : quadrants) { + if (quadrant->remove(object)) { + if (size() < MAX_OBJECTS) merge(); + + return true; + } + } + + return false; +} + +void Quadtree::update(std::shared_ptr object) { + if (object->quadrant != nullptr && object->quadrant->isLeaf() && object->quadrant->bounds.CheckCollision(object->getBounds())) return; + + if (remove(object)) { + if (!add(object)) { + TraceLog(LOG_WARNING, "Failed to add object to quadtree"); + } + } else { + TraceLog(LOG_WARNING, "Failed to remove object from quadtree"); + } +} + +std::vector> Quadtree::getAll() { + std::vector> objects; + + if (isLeaf()) { + for (auto node : children) { + objects.push_back(node); + } + } else { + for (auto quadrant : quadrants) { + auto quadrantObjects = quadrant->getAll(); + objects.insert(objects.end(), quadrantObjects.begin(), quadrantObjects.end()); + } + } + + return objects; +} + +unsigned int Quadtree::size() { + unsigned int size = 0; + + if (isLeaf()) { + size = children.size(); + } else { + for (auto quadrant : quadrants) { + size += quadrant->size(); + } + } + + return size; +} + +unsigned int Quadtree::getSubdivisions() { + unsigned int subdivisions = 0; + + if (isLeaf()) { + subdivisions = 1; + } else { + for (auto quadrant : quadrants) { + subdivisions += quadrant->getSubdivisions(); + } + } + + return subdivisions; +} + +std::vector> Quadtree::query(raylib::Rectangle range) { + std::vector> objects; + + if (!bounds.CheckCollision(range)) { + return objects; // No collision with the quadtree bounds + } + + if (isLeaf()) { + for (auto node : children) { + if (range.CheckCollision(node->getBounds())) { + objects.push_back(node); + } + } + } else { + for (auto quadrant : quadrants) { + if (quadrant->bounds.CheckCollision(range)) { + auto quadrantObjects = quadrant->query(range); + objects.insert(objects.end(), quadrantObjects.begin(), quadrantObjects.end()); + } + } + } + + + return objects; +} + +void Quadtree::render() { + DrawRectangleLinesEx(bounds, 1, color); + + if (isLeaf()) { + for (auto node : children) { + DrawCircleV(node->position, node->radius, color); + } + } else { + for (auto quadrant : quadrants) { + quadrant->render(); + } + } +} + +void Quadtree::clear() { + children.clear(); + quadrants.clear(); +} + +std::vector> Quadtree::getQuadrants() { + return quadrants; +} +#endif diff --git a/src/core/models/quadtree.hpp b/src/core/models/quadtree.hpp new file mode 100644 index 0000000..af33ff1 --- /dev/null +++ b/src/core/models/quadtree.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "../precomp.hpp" +#include "object.hpp" + +class Object; +class Quadtree; + +class Quadtree { + private: + std::vector> children; + std::vector> quadrants; + raylib::Rectangle bounds; + raylib::Color color; + uint8_t depth; + + bool isLeaf(); + void split(); + void merge(); + + public: + Quadtree(raylib::Rectangle bounds, uint8_t depth = 0); + + bool add(std::shared_ptr object); + bool remove(std::shared_ptr object); + void update(std::shared_ptr object); + std::vector> getAll(); + std::vector> getQuadrants(); + unsigned int size(); + unsigned int getSubdivisions(); + std::vector> query(raylib::Rectangle range); + void clear(); + void render(); +}; \ No newline at end of file diff --git a/src/core/models/quadtree_dummy.cpp b/src/core/models/quadtree_dummy.cpp new file mode 100644 index 0000000..6b46df6 --- /dev/null +++ b/src/core/models/quadtree_dummy.cpp @@ -0,0 +1,87 @@ +/** + * @file quadtree_dummy.cpp + * @author LeandroSQ + * @brief Dummy quadtree implementation for testing purposes + * @note This file is only compiled if QUADTREE_DUMMY is defined + * + * This emulates the behavior of a vector but exposes the same interface as the quadtree + * so it can be easily compared with the actual quadtree implementation + */ + +#ifdef QUADTREE_DUMMY + +#include "quadtree.hpp" +#include "../settings.hpp" + +Quadtree::Quadtree(raylib::Rectangle bounds, uint8_t depth) : bounds(bounds), depth(depth) { } + +bool Quadtree::isLeaf() { + return false; +} + +void Quadtree::split() { + +} + +void Quadtree::merge() { + +} + +bool Quadtree::add(std::shared_ptr object) { + children.push_back(object); + + return true; +} + +bool Quadtree::remove(std::shared_ptr object) { + for (auto it = children.begin(); it != children.end(); it++) { + if (*it == object) { + children.erase(it); + return true; + } + } + + return true; +} + +void Quadtree::update(std::shared_ptr object) { + +} + +std::vector> Quadtree::getAll() { + return children; +} + +unsigned int Quadtree::size() { + return children.size(); +} + +unsigned int Quadtree::getSubdivisions() { + return 0; +} + +std::vector> Quadtree::query(raylib::Rectangle range) { + std::vector> objects; + + for (auto node : children) { + objects.push_back(node); + } + + return objects; +} + +void Quadtree::render() { + for (auto node : children) { + DrawCircleV(node->position, node->radius, WHITE); + } +} + +void Quadtree::clear() { + children.clear(); +} + +std::vector> Quadtree::getQuadrants() { + return quadrants; +} + +#endif \ No newline at end of file diff --git a/src/core/models/quadtree_grid.cpp b/src/core/models/quadtree_grid.cpp new file mode 100644 index 0000000..0492bb2 --- /dev/null +++ b/src/core/models/quadtree_grid.cpp @@ -0,0 +1,130 @@ +/** + * @file quadtree_grid.cpp + * @author LeandroSQ + * @brief Spatial hash grid implementation for testing purposes + * @note This file is only compiled if QUADTREE_GRID is defined + * + * It implements a simple matrix of vectors, where each vector is a cell of the grid + * so queries are done by iterating over the cells that intersect the query range + * and do not require any kind of subdivision or multi-depth structure + * It exposes the same interface as the quadtree so it can be easily compared with it + */ + + +#ifdef QUADTREE_GRID + +#include "quadtree.hpp" +#include "../settings.hpp" + +// Although it says "Quadtree" this is but a spatial grid + +// Define the grid size + +int hash(int x, int y) { + auto z = x * GRID_SIZE + y; + if (z < 0) z = -z; + if (z > GRID_SIZE * GRID_SIZE) z = z % (GRID_SIZE * GRID_SIZE); + return z; +} + +Quadtree::Quadtree(raylib::Rectangle bounds, uint8_t depth) : bounds(bounds), depth(depth) { + // Initialize grid + for (int i = 0; i < GRID_SIZE * GRID_SIZE; i++) { + grid[i] = std::vector>(); + } +} + +bool Quadtree::isLeaf() { + return false; +} + +void Quadtree::split() { + +} + +void Quadtree::merge() { + +} + +bool Quadtree::add(std::shared_ptr object) { + auto x = (int) (object->position.x / GRID_SIZE); + auto y = (int) (object->position.y / GRID_SIZE); + + grid[hash(x, y)].push_back(object); + return true; +} + +bool Quadtree::remove(std::shared_ptr object) { + auto x = (int) (object->position.x / GRID_SIZE); + auto y = (int) (object->position.y / GRID_SIZE); + + auto &cell = grid[hash(x, y)]; + auto it = std::remove_if(cell.begin(), cell.end(), [object](std::shared_ptr other) { return other == object; }); + cell.erase(it, cell.end()); + + return true; +} + +void Quadtree::update(std::shared_ptr object) { + remove(object); + add(object); +} + +std::vector> Quadtree::getAll() { + std::vector> objects; + + for (int i = 0; i < GRID_SIZE * GRID_SIZE; i++) { + auto &cell = grid[i]; + objects.insert(objects.end(), cell.begin(), cell.end()); + } + + return objects; +} + +unsigned int Quadtree::size() { + return getAll().size(); +} + +unsigned int Quadtree::getSubdivisions() { + return GRID_SIZE * GRID_SIZE; +} + +std::vector> Quadtree::query(raylib::Rectangle range) { + std::vector> objects; + + auto startX = (int) (range.x / GRID_SIZE); + auto startY = (int) (range.y / GRID_SIZE); + auto endX = (int) ((range.x + range.width) / GRID_SIZE); + auto endY = (int) ((range.y + range.height) / GRID_SIZE); + + for (int x = startX; x <= endX; x++) { + for (int y = startY; y <= endY; y++) { + auto &cell = grid[hash(x, y)]; + if (cell.empty()) continue; + objects.insert(objects.end(), cell.begin(), cell.end()); + } + } + + return objects; +} + +void Quadtree::render() { + // Draw lines, grid size is actually the amount of cols and rows, not the size of the grid + for (int i = 0; i < GRID_SIZE; i++) { + DrawLineV(raylib::Vector2{ (float) i * GRID_SIZE, 0.0f }, raylib::Vector2{ (float) i * GRID_SIZE, (float) GRID_SIZE * GRID_SIZE }, WHITE); + DrawLineV(raylib::Vector2{ 0.0f, (float) i * GRID_SIZE }, raylib::Vector2{ (float) GRID_SIZE * GRID_SIZE, (float) i * GRID_SIZE }, WHITE); + } + + for (int i = 0; i < GRID_SIZE * GRID_SIZE; i++) { + auto &cell = grid[i]; + for (auto object : cell) { + DrawCircleV(object->position, object->radius, WHITE); + } + } +} + +std::vector> Quadtree::getQuadrants() { + return std::vector>(); +} + +#endif \ No newline at end of file diff --git a/src/core/models/ship.cpp b/src/core/models/ship.cpp deleted file mode 100644 index 7024fc7..0000000 --- a/src/core/models/ship.cpp +++ /dev/null @@ -1,84 +0,0 @@ -#include "ship.hpp" -#include "../utils.hpp" - -void Ship::updatePhysics() { - const auto deltaTime = GetFrameTime(); - - // Movement - velocity += acceleration * deltaTime; - position += velocity * deltaTime; - - acceleration = raylib::Vector2::Zero(); // Reset acceleration - velocity *= SHIP_DAMPING; - - // Angle - angle += angularVelocity * deltaTime; - angularVelocity *= SHIP_ANGULAR_DAMPING; - if (angle > 2 * PI) angle -= 2 * PI; - if (angle < 0) angle += 2 * PI; - - // Reset angular velocity if it's too small - if (fabs(angularVelocity) < 0.1f) angularVelocity = 0; -} - -void Ship::updateInput() { - // Rotation - if (IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_D)) angularVelocity += SHIP_ANGULAR_ACCELERATION; - if (IsKeyDown(KEY_LEFT) || IsKeyDown(KEY_A)) angularVelocity -= SHIP_ANGULAR_ACCELERATION; - - // Translation - if (IsKeyDown(KEY_UP) || IsKeyDown(KEY_W)) { - acceleration.x += cosf(angle) * SHIP_ACCELERATION; - acceleration.y += sinf(angle) * SHIP_ACCELERATION; - } - if (IsKeyDown(KEY_DOWN) || IsKeyDown(KEY_S)) { - acceleration.x -= cosf(angle) * SHIP_ACCELERATION; - acceleration.y -= sinf(angle) * SHIP_ACCELERATION; - } -} - -void Ship::wrapAroundScreen() { - if (position.x > WIDTH) { - position.x = 0; - } else if (position.x < 0) { - position.x = WIDTH; - } - if (position.y > HEIGHT) { - position.y = 0; - } else if (position.y < 0) { - position.y = HEIGHT; - } -} - -void Ship::setup() { - position = raylib::Vector2(WIDTH / 2.0f, HEIGHT / 2.0f); - velocity = raylib::Vector2::Zero(); - acceleration = raylib::Vector2::Zero(); - angle = (360.0f - 90.0f) * DEG2RAD; - angularVelocity = 0.0f; -} - -void Ship::update() { - updatePhysics(); - updateInput(); - wrapAroundScreen(); -} - -void Ship::render() { - // Render triangle with the pointy end facing the direction of the ship - const float size = 10.0f; - raylib::Vector2 p1 = position + raylib::Vector2(size, 0); - raylib::Vector2 p2 = position + raylib::Vector2(-size, -size); - raylib::Vector2 p3 = position + raylib::Vector2(-size, size); - - /* DrawCircleV(position, size, WHITE); - DrawLineV(position, position + raylib::Vector2(cosf(angle) * size, sinf(angle) * size), RED); */ - - DrawTriangleLines( - rotateAround(p1, position, angle), - rotateAround(p2, position, angle), - rotateAround(p3, position, angle), - WHITE - ); - -} diff --git a/src/core/models/ship.hpp b/src/core/models/ship.hpp deleted file mode 100644 index 14b6d6e..0000000 --- a/src/core/models/ship.hpp +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include "../precomp.hpp" -#include "Vector2.hpp" - -class Ship { - public: - raylib::Vector2 position; - raylib::Vector2 velocity; - raylib::Vector2 acceleration; - float angle; - float angularVelocity; - - private: - void updatePhysics(); - void updateInput(); - void wrapAroundScreen(); - - public: - Ship() = default; - ~Ship() = default; - - void setup(); - void update(); - void render(); -}; \ No newline at end of file diff --git a/src/core/models/solver.cpp b/src/core/models/solver.cpp new file mode 100644 index 0000000..2c23a4e --- /dev/null +++ b/src/core/models/solver.cpp @@ -0,0 +1,140 @@ +#include "solver.hpp" +#include "../settings.hpp" +#include "raylib.h" +#include "raymath.h" + +#pragma region Misc +uint32_t Solver::getAverageIterations() { + return iterationSum / iterationCount; +} + +void interpolateCenterCircleRadius() { + const float target = ENABLE_CIRCLE_CONSTRAINT ? CENTER_CIRCLE_RADIUS : std::max(GetScreenWidth(), GetScreenHeight()); + CENTER_CIRCLE_CURRENT_RADIUS = Lerp(CENTER_CIRCLE_CURRENT_RADIUS, target, 0.05f); +} +#pragma endregion + +Solver::Solver(Quadtree &quadtree) : quadtree(quadtree) { + centerCirclePosition = raylib::Vector2{ WIDTH / 2.0f, HEIGHT / 2.0f }; +} + +void Solver::applyGravity(std::shared_ptr object, float deltaTime) { + object->acceleration += gravity * object->mass; + + + if (!ENABLE_TEMPERATURE) return; + + // The more heat, the more upwards acceleration + auto temp = (object->temperature - AIR_TEMPERATURE) / (GROUND_TEMPERATURE - AIR_TEMPERATURE); + object->acceleration -= gravity * object->mass * powf(temp, 2.0f) * temperatureFloatingForce; + +} + +void Solver::applyConstraints(std::shared_ptr object, float deltaTime) { + if (ENABLE_CIRCLE_CONSTRAINT) { + const raylib::Vector2 direction = object->position - centerCirclePosition; + float distance = direction.Length(); + + if (ENABLE_TEMPERATURE) { + if (ENABLE_HEAT_FROM_CIRCLE_BORDER) { + // Transfer heat from the border of the circle + if (distance >= CENTER_CIRCLE_RADIUS - object->radius * 4) { + auto deltaTemperature = (object->temperature - GROUND_TEMPERATURE) * temperatureTransferGroundMultiplier; + object->temperature -= deltaTemperature; + } + } else { + // Transfer heat from the bottom of the circle + auto distanceFromBottom = object->position - raylib::Vector2{ centerCirclePosition.x, centerCirclePosition.y + CENTER_CIRCLE_CURRENT_RADIUS }; + if (distanceFromBottom.Length() <= object->radius * 14) { + auto deltaTemperature = (object->temperature - GROUND_TEMPERATURE) * temperatureTransferGroundMultiplier; + object->temperature -= deltaTemperature; + } + } + } + + if (distance > CENTER_CIRCLE_CURRENT_RADIUS - object->radius) { + auto normal = direction / distance; + object->position = centerCirclePosition + normal * (CENTER_CIRCLE_CURRENT_RADIUS - object->radius); + } + } + + // Keep the object inside the screen + const raylib::Vector2 screen((float) WIDTH, (float) HEIGHT); + if (object->position.x - object->radius < 0.0f) { + object->position.x = object->radius; + } else if (object->position.x + object->radius > screen.x) { + object->position.x = screen.x - object->radius; + } + + if (object->position.y - object->radius < 0.0f) { + object->position.y = object->radius; + } else if (object->position.y + object->radius > screen.y) { + object->position.y = screen.y - object->radius; + } + + // Transfer heat from the ground + if (ENABLE_TEMPERATURE && !ENABLE_CIRCLE_CONSTRAINT && object->position.y + object->radius >= screen.y - object->radius * 4) { + // Transfer temperature from the ground + auto deltaTemperature = (object->temperature - GROUND_TEMPERATURE) * temperatureTransferGroundMultiplier; + object->temperature -= deltaTemperature; + } +} + +void Solver::solveCollisions(std::shared_ptr object) { + auto padding = 3.0f * object->radius; + auto others = quadtree.query(raylib::Rectangle{ object->position.x - padding, object->position.y - padding, padding * 2.0f, padding * 2.0f }); + bool collision = false; + + // Use private scope and parallelize for loop + for (auto other : others) { + iterationSum++; + if (other == object) continue; + + const raylib::Vector2 direction = object->position - other->position; + float distance = direction.LengthSqr(); + if (distance < powf(object->radius + other->radius, 2.0f)) { + distance = sqrtf(distance); + collision = true; + auto normal = direction / distance; + auto delta = (object->radius + other->radius) - distance; + object->position += normal * 0.5f * delta; + other->position -= normal * 0.5f * delta; + + // Transfer temperature between objects slightly 10% of the time + if (ENABLE_TEMPERATURE) { + auto deltaTemperature = (object->temperature - other->temperature) * temperatureTransferContactMultiplier; + object->temperature -= deltaTemperature; + other->temperature += deltaTemperature; + } + } + } + + if (!collision && ENABLE_TEMPERATURE) { + // Cool temperature transfering to air + auto deltaTemperature = (object->temperature - AIR_TEMPERATURE) * temperatureTransferAirMultiplier; + object->temperature -= deltaTemperature; + } +} + +void Solver::solve(float deltaTime) { + interpolateCenterCircleRadius(); + + if (iterationCount >= 100) { + iterationCount = 0; + iterationSum = 0; + } + + float dt = deltaTime / float(substeps); + for (uint8_t i = 0; i < substeps; i++) { + for (auto object : quadtree.getAll()) { + applyGravity(object, dt); + solveCollisions(object); + applyConstraints(object, dt); + + object->update(dt); + quadtree.update(object); + } + } + + iterationCount++; +} \ No newline at end of file diff --git a/src/core/models/solver.hpp b/src/core/models/solver.hpp new file mode 100644 index 0000000..c65c4bf --- /dev/null +++ b/src/core/models/solver.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "../precomp.hpp" +#include "object.hpp" +#include "quadtree.hpp" + +class Solver { + private: + Quadtree &quadtree; + raylib::Vector2 centerCirclePosition; + uint64_t iterationSum = 0; + uint32_t iterationCount = 0; + + void applyGravity(std::shared_ptr object, float deltaTime); + void applyConstraints(std::shared_ptr object, float deltaTime); + void solveCollisions(std::shared_ptr object); + + public: + float temperatureTransferContactMultiplier = 0.025f; + float temperatureTransferAirMultiplier = 0.005f; + float temperatureTransferGroundMultiplier = 0.25f; + float temperatureFloatingForce = 3.0f; + unsigned int substeps = 4; + raylib::Vector2 gravity = { 0.0f, 9.8f }; + + Solver(Quadtree &quadtree); + + void solve(float deltaTime); + uint32_t getAverageIterations(); +}; \ No newline at end of file diff --git a/src/core/models/wave.cpp b/src/core/models/wave.cpp deleted file mode 100644 index a45557c..0000000 --- a/src/core/models/wave.cpp +++ /dev/null @@ -1,92 +0,0 @@ -#include "wave.hpp" -#include "asteroid.hpp" - -void WaveController::update(IContainer &asteroids, std::list &bullets, Ship &ship) { - timeSinceLastWave += GetFrameTime(); - timeSinceLastSpawn += GetFrameTime(); - - // if (asteroids.isEmpty()) { - // for (int i = 0; i < 50; i++) { - // spawn(asteroids, bullets, ship); - // } - // } - - if (timeSinceLastWave >= waveInterval || (asteroids.isEmpty() && currentWave > 0)) { - TraceLog(LOG_DEBUG, "Entering wave %i", currentWave); - - // Start new wave - timeSinceLastWave -= waveInterval; - currentWave++; - - asteroidsPerWave += LEVEL_ASTEROIDS_PER_WAVE; - - waveInterval *= LEVEL_WAVE_INTERVAL_DECREMENT; - if (waveInterval < LEVEL_WAVE_INTERVAL_MIN) waveInterval = LEVEL_WAVE_INTERVAL_MIN; - - spawnInterval *= LEVEL_ASTEROID_SPAWN_INTERVAL_DECREMENT_PER_WAVE; - if (spawnInterval < LEVEL_ASTEROID_SPAWN_INTERVAL_MIN) spawnInterval = LEVEL_ASTEROID_SPAWN_INTERVAL_MIN; - } - - if (timeSinceLastSpawn >= spawnInterval && asteroidsSpawned < asteroidsPerWave) { - // Spawn new asteroid - timeSinceLastSpawn -= spawnInterval; - asteroidsSpawned++; - spawn(asteroids, bullets, ship); - } - - if (IsKeyReleased(KEY_ENTER)) { - spawn(asteroids, bullets, ship); - } -} - -void WaveController::spawn(IContainer &asteroids, std::list &bullets, Ship &ship) { - const auto asteroid = std::make_shared(); - - bool hit = false; - uint8_t tries = 0; - do { - // Ensures it did not spawn on top of the ship - if (asteroid->position.Distance(ship.position) < asteroid->polygon.outerRadius) { - TraceLog(LOG_DEBUG, "Ship hit on %.2f, %.2f with distance %.3f - %.2f", asteroid->position.x, asteroid->position.y, ship.position.Distance(asteroid->position), asteroid->polygon.outerRadius); - hit = true; - } - - // Ensures it did not spawn on top of another asteroid - if (!hit) { - for (auto &other : asteroids.all()) { - if (&other == &asteroid) continue; - if (other->position.Distance(asteroid->position) < other->polygon.outerRadius + asteroid->polygon.outerRadius) { - TraceLog(LOG_DEBUG, "Asteroid hit on %.2f, %.2f", asteroid->position.x, asteroid->position.y); - hit = true; - break; - } - } - } - - // Ensures it did not spawn on top of bullets - if (!hit) { - for (auto &bullet : bullets) { - if (bullet.position.Distance(asteroid->position) < asteroid->polygon.outerRadius + BULLET_RADIUS) { - TraceLog(LOG_DEBUG, "Bullet hit on %.2f, %.2f", asteroid->position.x, asteroid->position.y); - hit = true; - break; - } - } - } - - // Assign new position if it hit something - if (hit) { - asteroid->position = raylib::Vector2( - GetRandomValue(0, WIDTH), - GetRandomValue(0, HEIGHT) - ); - tries++; - } - } while(hit && tries < 10); - - if (tries >= 10) { - TraceLog(LOG_INFO, "Failed to spawn asteroid after 10 tries"); - } else { - asteroids.insert(asteroid); - } -} \ No newline at end of file diff --git a/src/core/models/wave.hpp b/src/core/models/wave.hpp deleted file mode 100644 index 52544b4..0000000 --- a/src/core/models/wave.hpp +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include "../precomp.hpp" -#include "../settings.hpp" -#include "bullet.hpp" -#include "ship.hpp" -#include "../data/spatial-hash-grid.hpp" - -class WaveController { - public: - float waveInterval = LEVEL_WAVE_INTERVAL; - float spawnInterval = LEVEL_ASTEROID_SPAWN_INTERVAL; - uint8_t asteroidsPerWave = LEVEL_ASTEROIDS_PER_WAVE; - - // Current wave - uint16_t currentWave = 0; - float timeSinceLastWave = 0.0f; - float timeSinceLastSpawn = spawnInterval / 2.0f; - uint16_t asteroidsSpawned = 0; - - public: - WaveController() = default; - ~WaveController() = default; - - void update(IContainer &asteroids, std::list &bullets, Ship &ship); - -private: - void spawn(IContainer &asteroids, std::list &bullets, Ship &ship); -}; \ No newline at end of file diff --git a/src/core/precomp.hpp b/src/core/precomp.hpp index ae4f7d5..90852e5 100644 --- a/src/core/precomp.hpp +++ b/src/core/precomp.hpp @@ -1,7 +1,6 @@ #pragma once // Raylib -#define RPRAND_IMPLEMENTATION 1 #include #include @@ -22,6 +21,4 @@ #include #include #include -#include #include -#include \ No newline at end of file diff --git a/src/core/settings.cpp b/src/core/settings.cpp index 0408d49..2b0431f 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -1,5 +1,11 @@ #include "settings.hpp" -unsigned int WIDTH = 850; -unsigned int HEIGHT = 750; -unsigned int TARGET_FPS = 60; \ No newline at end of file +bool ENABLE_CIRCLE_CONSTRAINT = true; +float CENTER_CIRCLE_CURRENT_RADIUS = CENTER_CIRCLE_RADIUS; +bool ENABLE_SPAWN_SPREAD = false; +bool ENABLE_HEAT_FROM_CIRCLE_BORDER = false; +bool ENABLE_SOLID_COLOR = false; +bool ENABLE_TEMPERATURE = false; +bool ENABLE_FIXED_RAINBOW = true; +bool ENABLE_RAINBOW_COLORS = false; +bool ENABLE_AUTO_ADJUST_SUBSTEPS = true; \ No newline at end of file diff --git a/src/core/settings.hpp b/src/core/settings.hpp index 7c75af1..4d636bc 100644 --- a/src/core/settings.hpp +++ b/src/core/settings.hpp @@ -1,37 +1,48 @@ #pragma once // Constants - General -extern unsigned int WIDTH; -extern unsigned int HEIGHT; -extern unsigned int TARGET_FPS; - -// Constants - Ship -const float SHIP_ANGULAR_ACCELERATION = 30.0f * DEG2RAD; -const float SHIP_ACCELERATION = 150.0f; -const float SHIP_DAMPING = 0.98f; -const float SHIP_ANGULAR_DAMPING = 0.90f; - -// Constants - Asteroid -const float ASTEROID_RADIUS = 25.0f; -const float ASTEROID_JAGGEDNESS = 5.0f; -const float ASTEROID_ANGULAR_VELOCITY = 100.0f * DEG2RAD; -const float ASTEROID_VELOCITY = 100.0f; -const int ASTEROID_MIN_VERTEX_COUNT = 8; -const int ASTEROID_MAX_VERTEX_COUNT = 32; -const int ASTEROID_FRAGMENTS_COUNT = 3; -const float ASTEROID_RADIUS_DIFFERENCE = 10.0f; -const float ASTEROID_MIN_RADIUS_TO_SPLIT = 20.0f; - -// Constants - Bullet -const float BULLET_VELOCITY = 500.0f; -const float BULLET_RADIUS = 2.0f; -const float BULLET_SHOOT_INTERVAL = 0.250f; - -// Constants - Level -const uint8_t LEVEL_ASTEROIDS_PER_WAVE = 10; -const float LEVEL_ASTEROID_SPAWN_INTERVAL = 5.0f; -const float LEVEL_ASTEROID_SPAWN_INTERVAL_DECREMENT_PER_WAVE = 0.5f; -const float LEVEL_ASTEROID_SPAWN_INTERVAL_MIN = 0.05f; -const float LEVEL_WAVE_INTERVAL = (LEVEL_ASTEROIDS_PER_WAVE + 1) * LEVEL_ASTEROID_SPAWN_INTERVAL; -const float LEVEL_WAVE_INTERVAL_DECREMENT = 0.5f; -const float LEVEL_WAVE_INTERVAL_MIN = 2.0f; \ No newline at end of file +const unsigned int WIDTH = 850; +const unsigned int HEIGHT = 850; +const unsigned int TARGET_FPS = 60; + +// Constants - Quadtree +const unsigned int MAX_OBJECTS = 3; +const unsigned int MAX_SUBDIVISIONS = 5; + +// Constants - Spawn +#ifdef PLATFORM_WEB + const float OBJECT_RADIUS = 10.0f; + const unsigned int SPAWN_COUNT = 500; +#else + const float OBJECT_RADIUS = 10.0f; + const unsigned int SPAWN_COUNT = 800; +#endif +const float SPAWN_INTERVAL = 0.07f; +const float MANUAL_SPAWN_INTERVAL = 0.25f; + +// Constants - Temperature +const float AIR_TEMPERATURE = 4000.0f; +const float GROUND_TEMPERATURE = 10000.0f; + +// Constants - Interaction +const float CENTER_CIRCLE_RADIUS = 400.0f; +const float DRAGGING_ACCELERATION = 35000.0f; +const float EXPLOSION_FORCE = 1500000.0f; +const float EXPLOSION_RADIUS = 50.0f; + +// #define QUADTREE_DUMMY + +// Flags - Constraints +extern float CENTER_CIRCLE_CURRENT_RADIUS; +extern bool ENABLE_CIRCLE_CONSTRAINT; + +// Flags - Render mode +extern bool ENABLE_HEAT_FROM_CIRCLE_BORDER; +extern bool ENABLE_TEMPERATURE; +extern bool ENABLE_SOLID_COLOR; +extern bool ENABLE_FIXED_RAINBOW; +extern bool ENABLE_RAINBOW_COLORS; +extern bool ENABLE_AUTO_ADJUST_SUBSTEPS; + +// Flags - Spawning +extern bool ENABLE_SPAWN_SPREAD; diff --git a/src/core/utils.hpp b/src/core/utils.hpp index 316378c..b3fbaaf 100644 --- a/src/core/utils.hpp +++ b/src/core/utils.hpp @@ -1,40 +1,24 @@ #pragma once -#include "Color.hpp" #include "precomp.hpp" -#include "raylib.h" #include "settings.hpp" -#include -inline raylib::Vector2 rotateAround(raylib::Vector2 point, raylib::Vector2 center, float angle) { - return { - center.x + (point.x - center.x) * cosf(angle) - (point.y - center.y) * sinf(angle), - center.y + (point.x - center.x) * sinf(angle) + (point.y - center.y) * cosf(angle) - }; +inline raylib::Vector2 getRelativeMousePosition() { + const auto dpi = GetWindowScaleDPI(); + return raylib::Vector2( + std::clamp(GetMouseX() - GetScreenWidth() / 2.0f + WIDTH / 2.0f, 0.0f, (float)WIDTH), + std::clamp(GetMouseY() - GetScreenHeight() / 2.0f + HEIGHT / 2.0f, 0.0f, (float)HEIGHT) + ); } -inline float getRandomValue(float min, float max) { - return GetRandomValue(0, RAND_MAX) / (float)RAND_MAX * (max - min) + min; +inline ImVec2 convert(raylib::Vector2 a) { + return { a.x, a.y }; } -inline float smoothstep(float x) { - return x * x * (3 - 2 * x); +inline ImU32 convert(raylib::Color a) { + return IM_COL32(a.r, a.g, a.b, a.a); } -inline float easeInOutBack(float x) { - const float tension = 3.0158f; - const float overshoot = tension * 1.525f; - - return x < 0.5 - ? 0.5f * (x * x * ((overshoot + 1) * x - overshoot)) - : 0.5f * ((2 * x - 2) * (2 * x - 2) * ((overshoot + 1) * (2 * x - 2) + overshoot) + 2); -} - -inline raylib::Color randomColor() { - return raylib::Color( - GetRandomValue(128, 255), - GetRandomValue(128, 255), - GetRandomValue(128, 255), - 255 - ); +inline ImU32 convert(ImVec4 a) { + return IM_COL32((uint8_t)(a.x * 255.0f), (uint8_t)(a.y * 255.0f), (uint8_t)(a.z * 255.0f), (uint8_t)(a.w * 255.0f)); } \ No newline at end of file diff --git a/src/web/index.html b/src/web/index.html index ee3e998..33f4925 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -10,8 +10,8 @@ - - + +
@@ -45,6 +45,6 @@

Attention!

- + \ No newline at end of file