diff --git a/.github/images/Arrow_Down_Key_Dark.png b/.github/images/Arrow_Down_Key_Dark.png deleted file mode 100644 index 9edcf58..0000000 Binary files a/.github/images/Arrow_Down_Key_Dark.png and /dev/null differ diff --git a/.github/images/Arrow_Left_Key_Dark.png b/.github/images/Arrow_Left_Key_Dark.png deleted file mode 100644 index 3425005..0000000 Binary files a/.github/images/Arrow_Left_Key_Dark.png and /dev/null differ diff --git a/.github/images/Arrow_Right_Key_Dark.png b/.github/images/Arrow_Right_Key_Dark.png deleted file mode 100644 index 929cb35..0000000 Binary files a/.github/images/Arrow_Right_Key_Dark.png and /dev/null differ diff --git a/.github/images/Arrow_Up_Key_Dark.png b/.github/images/Arrow_Up_Key_Dark.png deleted file mode 100644 index 025a68d..0000000 Binary files a/.github/images/Arrow_Up_Key_Dark.png and /dev/null differ diff --git a/.github/images/Esc_Key_Dark.png b/.github/images/Esc_Key_Dark.png deleted file mode 100644 index c363cdc..0000000 Binary files a/.github/images/Esc_Key_Dark.png and /dev/null differ diff --git a/.github/images/Mouse_Left_Key_Dark.png b/.github/images/Mouse_Left_Key_Dark.png deleted file mode 100644 index 1b1eb86..0000000 Binary files a/.github/images/Mouse_Left_Key_Dark.png and /dev/null differ diff --git a/.github/images/Mouse_Middle_Key_Dark.png b/.github/images/Mouse_Middle_Key_Dark.png deleted file mode 100644 index 0fd0a2e..0000000 Binary files a/.github/images/Mouse_Middle_Key_Dark.png and /dev/null differ diff --git a/.github/images/Mouse_Right_Key_Dark.png b/.github/images/Mouse_Right_Key_Dark.png deleted file mode 100644 index cc24f92..0000000 Binary files a/.github/images/Mouse_Right_Key_Dark.png and /dev/null differ diff --git a/.github/images/Space_Key_Dark.png b/.github/images/Space_Key_Dark.png deleted file mode 100644 index 2298b4f..0000000 Binary files a/.github/images/Space_Key_Dark.png and /dev/null differ diff --git a/.github/images/Z_Key_Dark.png b/.github/images/Z_Key_Dark.png deleted file mode 100644 index e9a1299..0000000 Binary files a/.github/images/Z_Key_Dark.png and /dev/null differ diff --git a/.github/screenshots/screenshot01.png b/.github/screenshots/screenshot01.png deleted file mode 100644 index 435a038..0000000 Binary files a/.github/screenshots/screenshot01.png and /dev/null differ diff --git a/.github/screenshots/screenshot02.png b/.github/screenshots/screenshot02.png deleted file mode 100644 index 8ecfbe0..0000000 Binary files a/.github/screenshots/screenshot02.png and /dev/null differ diff --git a/.github/screenshots/screenshot03.png b/.github/screenshots/screenshot03.png deleted file mode 100644 index 276c5f6..0000000 Binary files a/.github/screenshots/screenshot03.png and /dev/null differ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 574dde0..fe93658 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/sandbox.wasm ./src/web - cp ./build/sandbox.js ./src/web - cp ./build/sandbox.html ./src/web + cp ./build/asteroids.wasm ./src/web + cp ./build/asteroids.js ./src/web + cp ./build/asteroids.html ./src/web - name: Deploy to GitHub Pages (Ubuntu) if: matrix.target == 'web' && matrix.os == 'ubuntu-latest' diff --git a/.gitignore b/.gitignore index 4770cc7..759f9c1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,6 @@ lib/*/ imgui.ini compile_commands.json .DS_Store -src/web/sandbox.* +src/web/asteroids.* +src/tests .cache \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index daf204e..e052cc5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.18.0) -set(PROJECT_NAME sandbox) +set(PROJECT_NAME asteroids) function(dump_info) message(STATUS "CMAKE_SYSTEM: ${CMAKE_SYSTEM}") diff --git a/README.md b/README.md index fedfb0a..e69de29 100644 --- a/README.md +++ b/README.md @@ -1,76 +0,0 @@ -# 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 3ff5b8e..256bad4 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -114,6 +114,7 @@ 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) @@ -121,7 +122,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 1.90.1 https://github.com/ocornut/imgui/archive/refs/tags/vVERSION.tar.gz) +include_library(dear_imgui docking https://github.com/ocornut/imgui/archive/refs/heads/VERSION.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 69dc2b7..611fd19 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/sandbox + ./build/asteroids 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 91d6830..6721aba 100644 --- a/scripts/copy-web-artifacts.sh +++ b/scripts/copy-web-artifacts.sh @@ -2,4 +2,4 @@ set -e -find ./build -depth 1 -name 'sandbox.*' -print -exec cp {} src/web \; \ No newline at end of file +find ./build -depth 1 -name 'asteroids.*' -print -exec cp {} src/web \; \ No newline at end of file diff --git a/scripts/linter.sh b/scripts/linter.sh index 9b5c55b..01c94fa 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 sandbox_lint + cmake --build ./build --target asteroids_lint else echo "Build failed" fi \ No newline at end of file diff --git a/scripts/server.sh b/scripts/server.sh index 1733ced..7ad41c7 100644 --- a/scripts/server.sh +++ b/scripts/server.sh @@ -2,7 +2,7 @@ set -e -npx nodemon --watch build/sandbox.* --exec "sh scripts/copy-web-artifacts.sh" & +npx nodemon --watch build/asteroids.* --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 94e0d66..11e43de 100644 --- a/scripts/web.sh +++ b/scripts/web.sh @@ -11,4 +11,4 @@ else exit 1 fi -cp ./build/web/sandbox.* ./src/web \ No newline at end of file +cp ./build/web/asteroids.* ./src/web \ No newline at end of file diff --git a/src/core/data/dummy-list.cpp b/src/core/data/dummy-list.cpp new file mode 100644 index 0000000..284ceae --- /dev/null +++ b/src/core/data/dummy-list.cpp @@ -0,0 +1,42 @@ +#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 new file mode 100644 index 0000000..b98a307 --- /dev/null +++ b/src/core/data/dummy-list.hpp @@ -0,0 +1,26 @@ +#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 new file mode 100644 index 0000000..b5fbd0d --- /dev/null +++ b/src/core/data/icontainer.hpp @@ -0,0 +1,19 @@ +#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 new file mode 100644 index 0000000..27f7c8a --- /dev/null +++ b/src/core/data/spatial-hash-grid.cpp @@ -0,0 +1,117 @@ +#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 new file mode 100644 index 0000000..ef5e69d --- /dev/null +++ b/src/core/data/spatial-hash-grid.hpp @@ -0,0 +1,36 @@ +#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 deleted file mode 100644 index c260ba9..0000000 --- a/src/core/interface/gui.cpp +++ /dev/null @@ -1,185 +0,0 @@ -#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 deleted file mode 100644 index 1a8decb..0000000 --- a/src/core/interface/palette.hpp +++ /dev/null @@ -1,9 +0,0 @@ -#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 1fddff9..566d66d 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -1,31 +1,40 @@ #include "precomp.hpp" +#include "raylib.h" #include "settings.hpp" #include "models/app.hpp" App app; void render() { - if (IsWindowResized()) app.resize(); - - float start = GetTime(); - app.onFrameStart(); - - app.update(); BeginDrawing(); - app.render(); - app.onFrameEnd(); + ClearBackground(BLACK); + app.renderGUI(); EndDrawing(); + + app.update(); + app.render(); } int main() { SetConfigFlags(FLAG_MSAA_4X_HINT | FLAG_WINDOW_RESIZABLE | FLAG_WINDOW_HIGHDPI); #ifdef PLATFORM_WEB - InitWindow(WIDTH, HEIGHT, "Sandbox"); + InitWindow(WIDTH, HEIGHT, "Asteroids"); #else - InitWindow(WIDTH, HEIGHT, "Sandbox"); -#endif + 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); +#endif app.setup(); diff --git a/src/core/models/app.cpp b/src/core/models/app.cpp index 7a658c5..f759728 100644 --- a/src/core/models/app.cpp +++ b/src/core/models/app.cpp @@ -1,138 +1,96 @@ #include "app.hpp" -#include "../precomp.hpp" #include "../settings.hpp" -#include "../interface/palette.hpp" -#include "../utils.hpp" +#include "RenderTexture.hpp" +#include "asteroid.hpp" +#include "imgui.h" +#include "polygon.hpp" #include "raylib.h" - -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; -} +#include +#include App::~App() { - rlImGuiShutdown(); + TraceLog(LOG_INFO, "App::~App()"); + rlImGuiShutdown(); } -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::shoot() { + Bullet bullet { + ship.position, + raylib::Vector2(cosf(ship.angle), sinf(ship.angle)) * BULLET_VELOCITY + }; + bullets.push_back(bullet); } void App::setup() { #ifdef DEBUG - SetTraceLogLevel(LOG_DEBUG); -#else - SetTraceLogLevel(LOG_WARNING); + SetTraceLogLevel(LOG_DEBUG); #endif - TraceLog(LOG_INFO, "app: Starting..."); - SetRandomSeed(GetTime()); + TraceLog(LOG_INFO, "App::setup()"); - setupGUI(); -} + rlImGuiSetup(true); + ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_DockingEnable; + ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; -void App::update() { - auto delta = GetFrameTime(); + ship.setup(); +} - spawner.update(getRelativeMousePosition(), solver.substeps, delta, !ImGui::GetIO().WantCaptureMouse); - solver.solve(delta); +void App::updateAsteroids() { + Asteroid::collisionCount = 0; + for (auto &asteroid : asteroids.all()) asteroid->update(asteroids); - handleInput(delta); + asteroids.update(); } -void App::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(); +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; + } + + for (auto it = bullets.begin(); it != bullets.end();) { + if (it->update(asteroids)) { + it = bullets.erase(it); + } else { + it++; + } + } } -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::update() { + updateAsteroids(); + updateBullets(); + ship.update(); + wave.update(asteroids, bullets, ship); } -void App::onFrameStart() { - frameStartTime = GetTime(); +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(); + + frameBuffer.EndMode(); } -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; - } +void App::onResize(uint32_t width, uint32_t height) { + TraceLog(LOG_INFO, "App::onResize(%i, %i)", width, height); + frameBuffer = raylib::RenderTexture2D(width, height); - frameCounter = 0; - frameTimeSum = 0; -} \ No newline at end of file + // Update global settings + WIDTH = width; + HEIGHT = height; + + // Update the grid + const auto size = (float)ASTEROID_RADIUS * 4; + asteroids.resize(ceilf(HEIGHT / size), ceilf(WIDTH / size)); +} diff --git a/src/core/models/app.hpp b/src/core/models/app.hpp index eec212b..110459c 100644 --- a/src/core/models/app.hpp +++ b/src/core/models/app.hpp @@ -1,37 +1,34 @@ #pragma once #include "../precomp.hpp" -#include "quadtree.hpp" -#include "solver.hpp" -#include "interation_handler.hpp" +#include "bullet.hpp" +#include "ship.hpp" +#include "asteroid.hpp" +#include "wave.hpp" +#include "../data/spatial-hash-grid.hpp" class App { private: - Solver solver; - Camera2D camera; - Quadtree quadtree; - InterationHandler spawner; + raylib::RenderTexture2D frameBuffer; - bool isRenderingQuadtree; + SpatialHashGrid asteroids; + std::list bullets; + float shootTimer; + Ship ship; - float frameStartTime; - uint8_t frameCounter; - float frameTimeSum; + WaveController wave; - void handleInput(float deltaTime); - void renderGUI(); - void setupGUI(); + void shoot(); + void updateAsteroids(); + void updateBullets(); public: - - App(); + App() = default; ~App(); - void onFrameStart(); - void onFrameEnd(); - void setup(); void update(); void render(); - void resize(); + void onResize(uint32_t width, uint32_t height); + void renderGUI(); }; \ No newline at end of file diff --git a/src/core/models/asteroid.cpp b/src/core/models/asteroid.cpp new file mode 100644 index 0000000..cdc535e --- /dev/null +++ b/src/core/models/asteroid.cpp @@ -0,0 +1,211 @@ +#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 new file mode 100644 index 0000000..79b4dbd --- /dev/null +++ b/src/core/models/asteroid.hpp @@ -0,0 +1,40 @@ +#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 new file mode 100644 index 0000000..b4eccf0 --- /dev/null +++ b/src/core/models/bullet.cpp @@ -0,0 +1,47 @@ +#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 new file mode 100644 index 0000000..82055d3 --- /dev/null +++ b/src/core/models/bullet.hpp @@ -0,0 +1,12 @@ +#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 new file mode 100644 index 0000000..d0c855d --- /dev/null +++ b/src/core/models/collision-handler.hpp @@ -0,0 +1,25 @@ +#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 new file mode 100644 index 0000000..f0c48c1 --- /dev/null +++ b/src/core/models/collision-hanlder.cpp @@ -0,0 +1,73 @@ +// #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 new file mode 100644 index 0000000..adf6633 --- /dev/null +++ b/src/core/models/gui.cpp @@ -0,0 +1,81 @@ +#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 deleted file mode 100644 index 3bbdf03..0000000 --- a/src/core/models/interation_handler.cpp +++ /dev/null @@ -1,99 +0,0 @@ -#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 deleted file mode 100644 index d42ec0c..0000000 --- a/src/core/models/interation_handler.hpp +++ /dev/null @@ -1,23 +0,0 @@ -#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 deleted file mode 100644 index ead4f03..0000000 --- a/src/core/models/object.cpp +++ /dev/null @@ -1,67 +0,0 @@ -#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 deleted file mode 100644 index 92585e6..0000000 --- a/src/core/models/object.hpp +++ /dev/null @@ -1,31 +0,0 @@ -#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 new file mode 100644 index 0000000..08c081a --- /dev/null +++ b/src/core/models/polygon.cpp @@ -0,0 +1,57 @@ +#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 new file mode 100644 index 0000000..5177c1c --- /dev/null +++ b/src/core/models/polygon.hpp @@ -0,0 +1,17 @@ +#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 deleted file mode 100644 index 204bd9e..0000000 --- a/src/core/models/quadtree.cpp +++ /dev/null @@ -1,217 +0,0 @@ - -#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 deleted file mode 100644 index af33ff1..0000000 --- a/src/core/models/quadtree.hpp +++ /dev/null @@ -1,34 +0,0 @@ -#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 deleted file mode 100644 index 6b46df6..0000000 --- a/src/core/models/quadtree_dummy.cpp +++ /dev/null @@ -1,87 +0,0 @@ -/** - * @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 deleted file mode 100644 index 0492bb2..0000000 --- a/src/core/models/quadtree_grid.cpp +++ /dev/null @@ -1,130 +0,0 @@ -/** - * @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 new file mode 100644 index 0000000..7024fc7 --- /dev/null +++ b/src/core/models/ship.cpp @@ -0,0 +1,84 @@ +#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 new file mode 100644 index 0000000..14b6d6e --- /dev/null +++ b/src/core/models/ship.hpp @@ -0,0 +1,26 @@ +#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 deleted file mode 100644 index 2c23a4e..0000000 --- a/src/core/models/solver.cpp +++ /dev/null @@ -1,140 +0,0 @@ -#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 deleted file mode 100644 index c65c4bf..0000000 --- a/src/core/models/solver.hpp +++ /dev/null @@ -1,30 +0,0 @@ -#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 new file mode 100644 index 0000000..a45557c --- /dev/null +++ b/src/core/models/wave.cpp @@ -0,0 +1,92 @@ +#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 new file mode 100644 index 0000000..52544b4 --- /dev/null +++ b/src/core/models/wave.hpp @@ -0,0 +1,29 @@ +#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 90852e5..ae4f7d5 100644 --- a/src/core/precomp.hpp +++ b/src/core/precomp.hpp @@ -1,6 +1,7 @@ #pragma once // Raylib +#define RPRAND_IMPLEMENTATION 1 #include #include @@ -21,4 +22,6 @@ #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 2b0431f..0408d49 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -1,11 +1,5 @@ #include "settings.hpp" -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 +unsigned int WIDTH = 850; +unsigned int HEIGHT = 750; +unsigned int TARGET_FPS = 60; \ No newline at end of file diff --git a/src/core/settings.hpp b/src/core/settings.hpp index 4d636bc..7c75af1 100644 --- a/src/core/settings.hpp +++ b/src/core/settings.hpp @@ -1,48 +1,37 @@ #pragma once // Constants - General -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; +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 diff --git a/src/core/utils.hpp b/src/core/utils.hpp index b3fbaaf..316378c 100644 --- a/src/core/utils.hpp +++ b/src/core/utils.hpp @@ -1,24 +1,40 @@ #pragma once +#include "Color.hpp" #include "precomp.hpp" +#include "raylib.h" #include "settings.hpp" +#include -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 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 ImVec2 convert(raylib::Vector2 a) { - return { a.x, a.y }; +inline float getRandomValue(float min, float max) { + return GetRandomValue(0, RAND_MAX) / (float)RAND_MAX * (max - min) + min; } -inline ImU32 convert(raylib::Color a) { - return IM_COL32(a.r, a.g, a.b, a.a); +inline float smoothstep(float x) { + return x * x * (3 - 2 * x); } -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)); +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 + ); } \ No newline at end of file diff --git a/src/web/index.html b/src/web/index.html index 33f4925..ee3e998 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