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!
-
+
|