├── .gitignore ├── .gitmodules ├── modules ├── physics │ ├── physics.ixx │ ├── physics-utils.ixx │ ├── physics-engine.ixx │ ├── physics-ball.ixx │ └── quad-tree.ixx ├── util │ ├── enum-utils.ixx │ ├── util.ixx │ ├── basic-types.ixx │ ├── stopwatch.ixx │ └── random-generator.ixx ├── world │ └── world.ixx └── bridges │ └── pge-bridge.ixx ├── Readme.md ├── src ├── 3rd_party │ └── olcPixelGameEngine.cpp ├── physics │ ├── physics-ball.cpp │ └── physics-engine.cpp └── ball-pit.cpp ├── .vscode ├── c_cpp_properties.json └── settings.json ├── LICENSE └── CMakeLists.txt /.gitignore: -------------------------------------------------------------------------------- 1 | ##### PLEASE KEEP THIS FILE SORTED! ##### 2 | /build/ 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "3rd_party/olcPixelGameEngine"] 2 | path = 3rd_party/olcPixelGameEngine 3 | url = https://github.com/OneLoneCoder/olcPixelGameEngine 4 | -------------------------------------------------------------------------------- /modules/physics/physics.ixx: -------------------------------------------------------------------------------- 1 | export module Physics; 2 | 3 | export import Physics.Ball; 4 | export import Physics.Engine; 5 | export import Physics.QuadTree; 6 | export import Physics.Utils; -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Ball Pit C++20 modules 2 | 3 | Build the ball pit simulation with C++20 modules. 4 | 5 | At this time, even CMake 3.22 requires Visual Studio 2019/2022 with Visual Studio generator. 6 | 7 | ```sh 8 | cmake -B build -G "Visual Studio 16 2019" 9 | 10 | cmake --build build --config Release 11 | ``` 12 | 13 | creates build/Release/ball_pit.exe 14 | -------------------------------------------------------------------------------- /modules/util/enum-utils.ixx: -------------------------------------------------------------------------------- 1 | module; 2 | #include 3 | export module Util.EnumUtils; 4 | 5 | export 6 | { 7 | 8 | template 9 | concept Enum = std::is_enum_v; 10 | 11 | template 12 | using PrimitiveType = std::underlying_type_t; 13 | 14 | template 15 | constexpr auto rep(E e) { return PrimitiveType(e); } 16 | 17 | } // export -------------------------------------------------------------------------------- /modules/util/util.ixx: -------------------------------------------------------------------------------- 1 | module; 2 | #include 3 | export module Util; 4 | 5 | export import Util.BasicTypes; 6 | export import Util.EnumUtils; 7 | export import Util.RandomGenerator; 8 | export import Util.Stopwatch; 9 | 10 | export 11 | using std::to_string; 12 | 13 | export 14 | using std::uint8_t; 15 | 16 | // Expose the allocation and deallocation functions (since they would be type dependent in template bodies and not bound). 17 | export 18 | using ::operator new; 19 | using ::operator new[]; 20 | using ::operator delete; 21 | using ::operator delete[]; -------------------------------------------------------------------------------- /src/3rd_party/olcPixelGameEngine.cpp: -------------------------------------------------------------------------------- 1 | #pragma warning(push) 2 | #pragma warning(disable: 4100) // 'fElapsedTime': unreferenced formal parameter 3 | #pragma warning(disable: 4201) // nonstandard extension used: nameless struct/union 4 | #pragma warning(disable: 4244) // '=': conversion from 'int' to 'char', possible loss of data 5 | #pragma warning(disable: 4245) // 'argument': conversion from 'int' to 'uint8_t', possible loss of data 6 | #pragma warning(disable: 4458) // declaration of 'nativeCap' hides class member 7 | #pragma warning(disable: 4706) // assignment within conditional expression 8 | 9 | #define OLC_PGE_APPLICATION 10 | #include "olcPixelGameEngine.h" 11 | 12 | #pragma warning(pop) -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Win32", 5 | "includePath": [ 6 | "${workspaceFolder}/**" 7 | ], 8 | "defines": [ 9 | "_DEBUG", 10 | "UNICODE", 11 | "_UNICODE" 12 | ], 13 | "windowsSdkVersion": "10.0.17763.0", 14 | "compilerPath": "C:/Program Files (x86)/Microsoft Visual Studio/2019/Preview/VC/Tools/MSVC/14.29.30030/bin/Hostx64/x64/cl.exe", 15 | "cStandard": "c17", 16 | "cppStandard": "c++17", 17 | "intelliSenseMode": "windows-msvc-x64", 18 | "configurationProvider": "ms-vscode.cmake-tools" 19 | } 20 | ], 21 | "version": 4 22 | } -------------------------------------------------------------------------------- /modules/util/basic-types.ixx: -------------------------------------------------------------------------------- 1 | module; 2 | #include 3 | export module Util.BasicTypes; 4 | 5 | import Bridges.PGE; 6 | 7 | export 8 | { 9 | 10 | enum class Width : std::int32_t { }; 11 | enum class Height : std::int32_t { }; 12 | enum class Radius : std::int32_t { }; 13 | enum class Weight : std::int32_t { }; 14 | enum class PixelWidth : std::int32_t { }; 15 | enum class PixelHeight : std::int32_t { }; 16 | enum class Row : std::int32_t { }; 17 | enum class Column : std::int32_t { }; 18 | 19 | struct ScreenInfo 20 | { 21 | Width width; 22 | Height height; 23 | PixelWidth px_width; 24 | PixelHeight px_height; 25 | }; 26 | 27 | using PixelPoint = olc::vi2d; 28 | using PhysicsPoint = olc::vf2d; 29 | using Color = olc::Pixel; 30 | 31 | } // export -------------------------------------------------------------------------------- /modules/util/stopwatch.ixx: -------------------------------------------------------------------------------- 1 | module; 2 | #include 3 | export module Util.Stopwatch; 4 | 5 | export 6 | class Stopwatch 7 | { 8 | public: 9 | using Clock = std::chrono::high_resolution_clock; 10 | 11 | void start() 12 | { 13 | start_ = Clock::now(); 14 | } 15 | 16 | void stop() 17 | { 18 | stop_ = Clock::now(); 19 | } 20 | 21 | Clock::duration ticks() const 22 | { 23 | return stop_ - start_; 24 | } 25 | 26 | // helpers 27 | template 28 | Tick to_ticks() const 29 | { 30 | return std::chrono::duration_cast(ticks()); 31 | } 32 | 33 | std::chrono::milliseconds to_ms() const 34 | { 35 | return to_ticks(); 36 | } 37 | 38 | private: 39 | Clock::time_point start_ = { }; 40 | Clock::time_point stop_ = { }; 41 | }; -------------------------------------------------------------------------------- /modules/world/world.ixx: -------------------------------------------------------------------------------- 1 | export module World; 2 | 3 | import Util.BasicTypes; 4 | import Util.EnumUtils; 5 | 6 | export 7 | class World 8 | { 9 | public: 10 | void set(Width width, Height height) 11 | { 12 | world_width = width; 13 | world_height = height; 14 | } 15 | 16 | Width width() const 17 | { 18 | return world_width; 19 | } 20 | 21 | Height height() const 22 | { 23 | return world_height; 24 | } 25 | 26 | bool bounded(const PixelPoint& pos) const 27 | { 28 | return bounded(Row(pos.y), Column(pos.x)); 29 | } 30 | 31 | bool bounded(Row row, Column col) const 32 | { 33 | if (rep(row) < 0) 34 | { 35 | return false; 36 | } 37 | 38 | if (rep(col) < 0) 39 | { 40 | return false; 41 | } 42 | 43 | return rep(row) < rep(height()) && rep(col) < rep(width()); 44 | } 45 | private: 46 | Width world_width = Width{}; 47 | Height world_height = Height{}; 48 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Cameron DaCamara 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /modules/bridges/pge-bridge.ixx: -------------------------------------------------------------------------------- 1 | module; 2 | #pragma warning(push) 3 | #pragma warning(disable: 4201) // nonstandard extension used: nameless struct/union 4 | #pragma warning(disable: 4245) // 'argument': conversion from 'int' to 'uint8_t', possible loss of data 5 | #include "olcPixelGameEngine.h" 6 | #pragma warning(pop) 7 | export module Bridges.PGE; 8 | 9 | export 10 | namespace olc 11 | { 12 | // For game. 13 | using olc::PixelGameEngine; 14 | using olc::Key; 15 | 16 | // Note: Because these color constants are defined to be static in the header they cannot be 17 | // directly exported. Instead we export their values through a module-owned variable. 18 | namespace ModuleColors 19 | { 20 | auto Black() 21 | { 22 | return olc::BLACK; 23 | } 24 | 25 | auto White() 26 | { 27 | return olc::WHITE; 28 | } 29 | 30 | auto Blue() 31 | { 32 | return olc::BLUE; 33 | } 34 | 35 | auto Red() 36 | { 37 | return olc::RED; 38 | } 39 | } 40 | 41 | // For basic types. 42 | using olc::Pixel; 43 | using olc::vf2d; 44 | using olc::vi2d; 45 | 46 | // Allow using the multiply operator from olc::v2d_generic. 47 | using olc::operator*; 48 | } -------------------------------------------------------------------------------- /modules/physics/physics-utils.ixx: -------------------------------------------------------------------------------- 1 | export module Physics.Utils; 2 | 3 | import Util.BasicTypes; 4 | import Util.EnumUtils; 5 | 6 | export 7 | { 8 | 9 | struct AABBBox 10 | { 11 | PixelPoint center = { }; 12 | Radius radius = { }; 13 | }; 14 | 15 | bool overlap_AABB(const AABBBox& first, const AABBBox& second) 16 | { 17 | return first.center.x + rep(first.radius) + rep(second.radius) > second.center.x 18 | && first.center.x < second.center.x + rep(first.radius) + rep(second.radius) 19 | && first.center.y + rep(first.radius) + rep(second.radius) > second.center.y 20 | && first.center.y < second.center.y + rep(first.radius) + rep(second.radius); 21 | } 22 | 23 | class Box 24 | { 25 | public: 26 | Box(const PixelPoint& upper_left, Width width, Height height): 27 | upper_left{ upper_left }, w{ width }, h{ height } { } 28 | 29 | int left() const 30 | { 31 | return upper_left.x; 32 | } 33 | 34 | int top() const 35 | { 36 | return upper_left.y; 37 | } 38 | 39 | int bottom() const 40 | { 41 | return top() + rep(height()); 42 | } 43 | 44 | int right() const 45 | { 46 | return left() + rep(width()); 47 | } 48 | 49 | Width width() const 50 | { 51 | return w; 52 | } 53 | 54 | Height height() const 55 | { 56 | return h; 57 | } 58 | private: 59 | PixelPoint upper_left; 60 | Width w; 61 | Height h; 62 | }; 63 | 64 | bool point_in(const PixelPoint& point, const Box& box) 65 | { 66 | return box.left() + rep(box.width()) > point.x 67 | && box.left() <= point.x 68 | && box.top() + rep(box.height()) > point.y 69 | && box.top() <= point.y; 70 | } 71 | 72 | } // export -------------------------------------------------------------------------------- /modules/physics/physics-engine.ixx: -------------------------------------------------------------------------------- 1 | module; 2 | #include 3 | #include 4 | #include 5 | export module Physics.Engine; 6 | 7 | import Physics.Ball; 8 | import Physics.QuadTree; 9 | import World; 10 | 11 | export 12 | class PhysicsEngine 13 | { 14 | static constexpr int d_time = 16; 15 | static constexpr float d_time_s = static_cast(d_time) / 1000.f; 16 | static constexpr float gravity_constant = 980.f; 17 | public: 18 | void add(const PhysicsBall& ball) 19 | { 20 | ball_collection.push_back(ball); 21 | } 22 | 23 | void update(float dt, World* world); 24 | 25 | void remove_all() 26 | { 27 | ball_collection.clear(); 28 | } 29 | 30 | const auto& objects() const 31 | { 32 | return ball_collection; 33 | } 34 | 35 | const QuadTree* current_quad_tree() const 36 | { 37 | return quad_tree.get(); 38 | } 39 | 40 | const auto& update_times() const 41 | { 42 | return current_update_times; 43 | } 44 | 45 | const auto& tree_build_times() const 46 | { 47 | return current_tree_build_times; 48 | } 49 | 50 | const auto& collision_times() const 51 | { 52 | return current_collision_times; 53 | } 54 | private: 55 | void init_timer_collections(int steps); 56 | 57 | void clear_quadtree(const World* world); 58 | 59 | void intersect_objects_static_response(PhysicsBall* ball); 60 | 61 | void dynamic_collision_responses(); 62 | 63 | std::vector ball_collection; 64 | std::unique_ptr quad_tree; 65 | std::vector> dynamic_collision_pairs; 66 | std::vector current_update_times; 67 | std::vector current_tree_build_times; 68 | std::vector current_collision_times; 69 | }; -------------------------------------------------------------------------------- /modules/util/random-generator.ixx: -------------------------------------------------------------------------------- 1 | module; 2 | #include 3 | #include 4 | export module Util.RandomGenerator; 5 | 6 | import Util.EnumUtils; 7 | 8 | enum class RandomSeed : decltype(std::random_device{}()) { }; 9 | 10 | export 11 | template 12 | using IntDistribution = std::uniform_int_distribution; 13 | 14 | export 15 | template 16 | using RealDistribution = std::uniform_real_distribution; 17 | 18 | class RandomNumberGenerator 19 | { 20 | public: 21 | RandomNumberGenerator() = default; 22 | RandomNumberGenerator(RandomSeed seed): 23 | seed{ seed } { } 24 | 25 | template 26 | auto generate(T&& distribution) 27 | { 28 | return std::forward(distribution)(generator); 29 | } 30 | 31 | template 32 | auto from_1_to_100() 33 | { 34 | IntDistribution dis{1, 100}; 35 | return generate(dis); 36 | } 37 | 38 | template 39 | auto from(I min, I max) 40 | { 41 | IntDistribution dis{ min, max }; 42 | return generate(dis); 43 | } 44 | 45 | template 46 | auto from_0_to_1() 47 | { 48 | RealDistribution dis{ 0, 1 }; 49 | return generate(dis); 50 | } 51 | 52 | template 53 | auto from(F min, F max) 54 | { 55 | RealDistribution dis{ min, max }; 56 | return generate(dis); 57 | } 58 | 59 | auto initial_seed() const 60 | { 61 | return seed; 62 | } 63 | 64 | std::mt19937& raw() 65 | { 66 | return generator; 67 | } 68 | private: 69 | RandomSeed seed = RandomSeed(std::random_device{}()); 70 | std::mt19937 generator{ rep(seed) }; 71 | }; 72 | 73 | export 74 | RandomNumberGenerator& random_generator() 75 | { 76 | // The random number generator in is HUGE and expensive to construct, 77 | // so we will only have one. 78 | static RandomNumberGenerator generator{ }; 79 | return generator; 80 | } -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.28) 2 | 3 | if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR) 4 | message(FATAL_ERROR "Do not build in-source. Please remove CMakeCache.txt and the CMakeFiles/ directory. Then build out-of-source.") 5 | endif() 6 | 7 | project( 8 | "ball_pit" 9 | VERSION 0.1.0 10 | LANGUAGES CXX 11 | ) 12 | 13 | # It is always easier to navigate in an IDE when projects are organized in folders. 14 | set_property(GLOBAL PROPERTY USE_FOLDERS ON) 15 | 16 | include_directories( 17 | 3rd_party/olcPixelGameEngine 18 | include 19 | ) 20 | 21 | message("CMAKE_CXX_SOURCE_FILE_EXTENSIONS ${CMAKE_CXX_SOURCE_FILE_EXTENSIONS}") 22 | 23 | add_library(ball_pit_mod) 24 | target_sources(ball_pit_mod 25 | PUBLIC FILE_SET CXX_MODULES 26 | FILES 27 | modules/bridges/pge-bridge.ixx 28 | modules/physics/physics-ball.ixx 29 | modules/physics/physics-engine.ixx 30 | modules/physics/physics-utils.ixx 31 | modules/physics/physics.ixx 32 | modules/physics/quad-tree.ixx 33 | modules/util/basic-types.ixx 34 | modules/util/enum-utils.ixx 35 | modules/util/random-generator.ixx 36 | modules/util/stopwatch.ixx 37 | modules/util/util.ixx 38 | modules/world/world.ixx) 39 | 40 | add_executable(ball_pit 41 | src/3rd_party/olcPixelGameEngine.cpp 42 | src/ball-pit.cpp 43 | src/physics/physics-ball.cpp 44 | src/physics/physics-engine.cpp 45 | ) 46 | 47 | target_link_libraries(ball_pit ball_pit_mod) 48 | 49 | # Only support statically linking for now. 50 | target_compile_definitions(ball_pit PRIVATE 51 | SEQUENTIAL_PROCESSING=0) 52 | 53 | # Require c++20, this is better than setting CMAKE_CXX_STANDARD since it won't pollute other targets 54 | # note : cxx_std_* features were added in CMake 3.8.2 55 | target_compile_features(ball_pit_mod PRIVATE cxx_std_20) 56 | target_compile_features(ball_pit PRIVATE cxx_std_20) 57 | 58 | target_compile_options(ball_pit PRIVATE 59 | $<$:/Bt+>) 60 | 61 | set_target_properties(ball_pit PROPERTIES INTERPROCEDURAL_OPTIMIZATION ON) -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.ixx": "cpp", 4 | "chrono": "cpp", 5 | "algorithm": "cpp", 6 | "array": "cpp", 7 | "atomic": "cpp", 8 | "bit": "cpp", 9 | "cctype": "cpp", 10 | "clocale": "cpp", 11 | "cmath": "cpp", 12 | "compare": "cpp", 13 | "concepts": "cpp", 14 | "cstddef": "cpp", 15 | "cstdint": "cpp", 16 | "cstdio": "cpp", 17 | "cstdlib": "cpp", 18 | "cstring": "cpp", 19 | "ctime": "cpp", 20 | "cwchar": "cpp", 21 | "exception": "cpp", 22 | "filesystem": "cpp", 23 | "fstream": "cpp", 24 | "functional": "cpp", 25 | "initializer_list": "cpp", 26 | "iomanip": "cpp", 27 | "ios": "cpp", 28 | "iosfwd": "cpp", 29 | "iostream": "cpp", 30 | "istream": "cpp", 31 | "iterator": "cpp", 32 | "limits": "cpp", 33 | "list": "cpp", 34 | "locale": "cpp", 35 | "map": "cpp", 36 | "memory": "cpp", 37 | "new": "cpp", 38 | "ostream": "cpp", 39 | "random": "cpp", 40 | "ratio": "cpp", 41 | "sstream": "cpp", 42 | "stdexcept": "cpp", 43 | "stop_token": "cpp", 44 | "streambuf": "cpp", 45 | "string": "cpp", 46 | "system_error": "cpp", 47 | "thread": "cpp", 48 | "tuple": "cpp", 49 | "type_traits": "cpp", 50 | "typeinfo": "cpp", 51 | "unordered_map": "cpp", 52 | "utility": "cpp", 53 | "vector": "cpp", 54 | "xfacet": "cpp", 55 | "xhash": "cpp", 56 | "xiosbase": "cpp", 57 | "xlocale": "cpp", 58 | "xlocbuf": "cpp", 59 | "xlocinfo": "cpp", 60 | "xlocmes": "cpp", 61 | "xlocmon": "cpp", 62 | "xlocnum": "cpp", 63 | "xloctime": "cpp", 64 | "xmemory": "cpp", 65 | "xstddef": "cpp", 66 | "xstring": "cpp", 67 | "xtr1common": "cpp", 68 | "xtree": "cpp", 69 | "xutility": "cpp" 70 | } 71 | } -------------------------------------------------------------------------------- /modules/physics/physics-ball.ixx: -------------------------------------------------------------------------------- 1 | export module Physics.Ball; 2 | 3 | import Physics.Utils; 4 | import Util.BasicTypes; 5 | import Util.EnumUtils; 6 | import World; 7 | 8 | struct PhysicalProperties 9 | { 10 | float stickyness = 150.f; 11 | float friction = .85f; 12 | }; 13 | 14 | export 15 | class PhysicsBall 16 | { 17 | public: 18 | PhysicsBall(const PhysicsPoint& pos, 19 | const PhysicsPoint& velocity, 20 | Color color, 21 | Radius radius, 22 | Weight weight = Weight(1), 23 | PhysicalProperties properties = { }): 24 | pos{ pos }, 25 | vel{ velocity }, 26 | pixel_color{ color }, 27 | r{ radius }, 28 | w{ weight }, 29 | props{ properties } { } 30 | 31 | void dead(bool b) 32 | { 33 | exploded = b; 34 | } 35 | 36 | bool dead() const 37 | { 38 | return exploded; 39 | } 40 | 41 | const PhysicsPoint& position() const 42 | { 43 | return pos; 44 | } 45 | 46 | PhysicsPoint& position() 47 | { 48 | return pos; 49 | } 50 | 51 | PhysicsPoint& old_position() 52 | { 53 | return prev_pos; 54 | } 55 | 56 | const PhysicsPoint& old_position() const 57 | { 58 | return prev_pos; 59 | } 60 | 61 | const PhysicsPoint& velocity() const 62 | { 63 | return vel; 64 | } 65 | 66 | PhysicsPoint& velocity() 67 | { 68 | return vel; 69 | } 70 | 71 | Color color() const 72 | { 73 | return pixel_color; 74 | } 75 | 76 | Radius radius() const 77 | { 78 | return r; 79 | } 80 | 81 | Weight weight() const 82 | { 83 | return w; 84 | } 85 | 86 | const PhysicalProperties& properties() const 87 | { 88 | return props; 89 | } 90 | 91 | float physics_time_remaining() const 92 | { 93 | return time_remaining; 94 | } 95 | 96 | void physics_time_remaining(float dt) 97 | { 98 | time_remaining = dt; 99 | } 100 | 101 | bool single_point() const 102 | { 103 | return rep(radius()) == 0; 104 | } 105 | 106 | AABBBox bounding_box() const 107 | { 108 | return { position(), radius() }; 109 | } 110 | 111 | static void static_collision_response(PhysicsBall* a, PhysicsBall* b); 112 | 113 | static void dynamic_collision_response(PhysicsBall* a, PhysicsBall* b); 114 | 115 | static bool collides_with(const PhysicsBall& a, const PhysicsBall& b); 116 | 117 | enum class ImpactWorldResult 118 | { 119 | None, 120 | Left, 121 | Right, 122 | Top, 123 | Bottom 124 | }; 125 | 126 | static ImpactWorldResult impacts_world_bounds(const PhysicsBall& a, const World& world); 127 | 128 | static void world_collision_response(ImpactWorldResult result, PhysicsBall* ball, const World& world); 129 | 130 | private: 131 | bool can_stick() const 132 | { 133 | return velocity().mag2() < (props.stickyness * props.stickyness); 134 | } 135 | 136 | Color pixel_color; 137 | PhysicsPoint pos; 138 | PhysicsPoint vel; 139 | Radius r; 140 | Weight w; 141 | PhysicsPoint prev_pos = pos; 142 | PhysicalProperties props; 143 | float time_remaining = 0.f; 144 | bool exploded = false; 145 | }; -------------------------------------------------------------------------------- /src/physics/physics-ball.cpp: -------------------------------------------------------------------------------- 1 | module; 2 | #include 3 | module Physics.Ball; 4 | 5 | void PhysicsBall::static_collision_response(PhysicsBall* a, PhysicsBall* b) 6 | { 7 | // Displace the balls apart from each other. 8 | auto collision_vector = a->position() - b->position(); 9 | float distance = collision_vector.mag(); 10 | if (distance == 0.f) 11 | { 12 | // Dynamic collision will drift these apart. 13 | return; 14 | } 15 | float overlap = .5f * (distance - static_cast(rep(b->radius())) - static_cast(rep(a->radius()))); 16 | a->position() -= overlap * (collision_vector / distance); 17 | b->position() += overlap * (collision_vector / distance); 18 | } 19 | 20 | void PhysicsBall::dynamic_collision_response(PhysicsBall* a, PhysicsBall* b) 21 | { 22 | auto collision_vector = b->position() - a->position(); 23 | float distance = collision_vector.mag(); 24 | if (distance == .0f) 25 | { 26 | distance = .1f; 27 | } 28 | auto collision_normal = collision_vector / distance; 29 | auto relative_velocity = a->velocity() - b->velocity(); 30 | float speed = relative_velocity.dot(collision_normal); 31 | // These objects are moving away from each other already. 32 | if (speed < 0) 33 | { 34 | return; 35 | } 36 | float impulse = 2 * speed / (rep(a->weight()) + rep(b->weight())); 37 | a->velocity() -= collision_normal * impulse * static_cast(rep(b->weight())); 38 | b->velocity() += collision_normal * impulse * static_cast(rep(a->weight())); 39 | } 40 | 41 | bool PhysicsBall::collides_with(const PhysicsBall& a, const PhysicsBall& b) 42 | { 43 | auto angle = a.position() - b.position(); 44 | float distance_2 = angle.mag2(); 45 | return distance_2 <= ((rep(a.radius()) + rep(b.radius())) * (rep(a.radius()) + rep(b.radius()))); 46 | } 47 | 48 | auto PhysicsBall::impacts_world_bounds(const PhysicsBall& ball, const World& world) -> ImpactWorldResult 49 | { 50 | using enum ImpactWorldResult; 51 | if (ball.position().x + rep(ball.radius()) > rep(world.width())) 52 | { 53 | return Right; 54 | } 55 | if (ball.position().x - rep(ball.radius()) < 0) 56 | { 57 | return Left; 58 | } 59 | if (ball.position().y + rep(ball.radius()) > rep(world.height())) 60 | { 61 | return Bottom; 62 | } 63 | if (ball.position().y - rep(ball.radius()) < 0) 64 | { 65 | return Top; 66 | } 67 | return None; 68 | } 69 | 70 | void PhysicsBall::world_collision_response(ImpactWorldResult result, PhysicsBall* ball, const World& world) 71 | { 72 | assert(impacts_world_bounds(*ball, world) == result && result != ImpactWorldResult::None); 73 | PhysicsPoint normal{ }; 74 | switch (result) 75 | { 76 | case ImpactWorldResult::Left: 77 | normal = { 1.f, 0.f }; 78 | ball->position().x += -(ball->position().x - rep(ball->radius())); 79 | break; 80 | case ImpactWorldResult::Right: 81 | normal = { -1.f, 0.f }; 82 | ball->position().x += rep(world.width()) - (ball->position().x + rep(ball->radius())); 83 | break; 84 | case ImpactWorldResult::Top: 85 | normal = { 0.f, -1.f }; 86 | ball->position().y += -(ball->position().y - rep(ball->radius())); 87 | break; 88 | case ImpactWorldResult::Bottom: 89 | normal = { 0.f, 1.f }; 90 | ball->position().y += rep(world.height()) - (ball->position().y + rep(ball->radius())); 91 | break; 92 | default: 93 | return; 94 | } 95 | float d = 2 * ball->velocity().dot(normal); 96 | ball->velocity() -= (normal * d) * ball->properties().friction; 97 | } -------------------------------------------------------------------------------- /modules/physics/quad-tree.ixx: -------------------------------------------------------------------------------- 1 | module; 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | export module Physics.QuadTree; 8 | 9 | import Physics.Ball; 10 | import Physics.Utils; 11 | import Util.BasicTypes; 12 | import Util.EnumUtils; 13 | 14 | export 15 | enum class Level : int { }; 16 | 17 | export 18 | class QuadTree 19 | { 20 | static constexpr int max_depth = 5; 21 | static constexpr int split_factor = 20; 22 | public: 23 | using BoundingBox = Box; 24 | 25 | QuadTree(const PixelPoint& upper_left, Width width, Height height, Level level): 26 | rect{ upper_left, width, height }, level{ level } { } 27 | 28 | void clear() 29 | { 30 | objects.clear(); 31 | trees = { }; // clear the existing trees. 32 | } 33 | 34 | void insert(PhysicsBall* ball) 35 | { 36 | internal_insert(box_for(*ball), ball); 37 | } 38 | 39 | template F> 40 | void for_each_in(const BoundingBox& box, F&& invocable) const 41 | { 42 | int i = index(box); 43 | if (i != -1 && trees[i]) 44 | { 45 | trees[i]->for_each_in(box, std::forward(invocable)); 46 | } 47 | for (PhysicsBall* ball : objects) 48 | { 49 | std::forward(invocable)(ball); 50 | } 51 | } 52 | 53 | static BoundingBox box_for(const PhysicsBall& ball) 54 | { 55 | int r = rep(ball.radius()); 56 | return { ball.position(), Width(r), Height(r) }; 57 | } 58 | 59 | std::vector all_boxes() const 60 | { 61 | std::vector boxes; 62 | internal_all_boxes(&boxes); 63 | return boxes; 64 | } 65 | private: 66 | void internal_all_boxes(std::vector* boxes) const 67 | { 68 | boxes->push_back(rect); 69 | 70 | for (const auto& tree : trees) 71 | { 72 | if (tree) 73 | { 74 | tree->internal_all_boxes(boxes); 75 | } 76 | } 77 | } 78 | 79 | void internal_insert(const BoundingBox& box, PhysicsBall* ball) 80 | { 81 | if (trees[0]) 82 | { 83 | int i = index(box); 84 | if (i != -1) 85 | { 86 | trees[i]->internal_insert(box, ball); 87 | return; 88 | } 89 | } 90 | 91 | objects.push_back(ball); 92 | 93 | if (objects.size() > split_factor && rep(level) < max_depth) 94 | { 95 | if (!trees[0]) 96 | { 97 | split(); 98 | } 99 | 100 | int i = 0; 101 | while (i < static_cast(objects.size())) 102 | { 103 | BoundingBox object_box = box_for(*objects[i]); 104 | int idx = index(object_box); 105 | if (idx != -1) 106 | { 107 | trees[idx]->internal_insert(object_box, objects[i]); 108 | objects.erase(begin(objects) + i); 109 | } 110 | else 111 | { 112 | ++i; 113 | } 114 | } 115 | } 116 | } 117 | 118 | void split() 119 | { 120 | int sub_width = rep(rect.width()) / 2; 121 | int sub_height = rep(rect.height()) / 2; 122 | int x = rect.left(); 123 | int y = rect.top(); 124 | int next_level = rep(level) + 1; 125 | 126 | trees[0] = std::make_unique(PixelPoint{ x + sub_width, y }, Width(sub_width), Height(sub_height), Level(next_level)); 127 | trees[1] = std::make_unique(PixelPoint{ x, y }, Width(sub_width), Height(sub_height), Level(next_level)); 128 | trees[2] = std::make_unique(PixelPoint{ x, y + sub_height }, Width(sub_width), Height(sub_height), Level(next_level)); 129 | trees[3] = std::make_unique(PixelPoint{ x + sub_width, y + sub_height }, Width(sub_width), Height(sub_height), Level(next_level)); 130 | } 131 | 132 | int index(const BoundingBox& box) const 133 | { 134 | int index = -1; 135 | float vert_mid = rect.left() + static_cast(rep(rect.width())) / 2.f; 136 | float horiz_mid = rect.top() + static_cast(rep(rect.height())) / 2.f; 137 | 138 | auto is_top = [&] 139 | { 140 | return box.top() < horiz_mid && box.bottom() < horiz_mid; 141 | }; 142 | auto is_bottom = [&] 143 | { 144 | return box.top() > horiz_mid; 145 | }; 146 | 147 | if (box.left() <= vert_mid && box.right() <= vert_mid) 148 | { 149 | if (is_top()) 150 | { 151 | index = 1; 152 | } 153 | else if (is_bottom()) 154 | { 155 | index = 2; 156 | } 157 | } 158 | else if (box.left() >= vert_mid) 159 | { 160 | if (is_top()) 161 | { 162 | index = 0; 163 | } 164 | else if (is_bottom()) 165 | { 166 | index = 3; 167 | } 168 | } 169 | 170 | return index; 171 | } 172 | 173 | std::array, max_depth> trees; 174 | std::vector objects; 175 | BoundingBox rect; 176 | const Level level; 177 | }; -------------------------------------------------------------------------------- /src/physics/physics-engine.cpp: -------------------------------------------------------------------------------- 1 | module; 2 | #include 3 | 4 | #include 5 | #include 6 | module Physics.Engine; 7 | 8 | import Util.Stopwatch; 9 | 10 | void PhysicsEngine::update(float dt, World* world) 11 | { 12 | constexpr int updates = 4; 13 | float sim_elapsed_time = dt / updates; 14 | constexpr int max_steps = 4; 15 | int dead_count = 0; 16 | dynamic_collision_pairs.reserve(ball_collection.size()); 17 | 18 | // Track timing. 19 | init_timer_collections(updates * max_steps); 20 | Stopwatch stopwatch_all; 21 | Stopwatch stopwatch; 22 | 23 | for (int i = 0; i != updates; ++i) 24 | { 25 | stopwatch_all.start(); 26 | for (PhysicsBall& ball : ball_collection) 27 | { 28 | ball.physics_time_remaining(sim_elapsed_time); 29 | } 30 | 31 | dead_count = 0; 32 | bool early_break = true; 33 | for (int j = 0; j != max_steps; ++j) 34 | { 35 | clear_quadtree(world); 36 | 37 | for (PhysicsBall& ball : ball_collection) 38 | { 39 | if (ball.dead()) 40 | { 41 | ++dead_count; 42 | continue; 43 | } 44 | 45 | // This ball has exhausted its maximum allotted time for this 46 | // epoch. 47 | if (ball.physics_time_remaining() <= 0.f) 48 | { 49 | continue; 50 | } 51 | early_break = false; 52 | 53 | ball.old_position() = ball.position(); 54 | 55 | // Add gravity. 56 | ball.velocity().y += gravity_constant * ball.physics_time_remaining(); 57 | 58 | if (ball.velocity().mag2() < 0.005f) 59 | { 60 | ball.velocity() = { }; 61 | } 62 | 63 | // Always add x velocity. 64 | ball.position() += ball.velocity() * ball.physics_time_remaining(); 65 | 66 | auto result = PhysicsBall::impacts_world_bounds(ball, *world); 67 | if (result != PhysicsBall::ImpactWorldResult::None) 68 | { 69 | PhysicsBall::world_collision_response(result, &ball, *world); 70 | } 71 | } 72 | 73 | if (early_break) 74 | { 75 | break; 76 | } 77 | 78 | // If we are doing extra interactions, do them. 79 | stopwatch.start(); 80 | for (PhysicsBall& ball : ball_collection) 81 | { 82 | if (!ball.dead()) 83 | { 84 | quad_tree->insert(&ball); 85 | } 86 | } 87 | stopwatch.stop(); 88 | current_tree_build_times.push_back(stopwatch.to_ms()); 89 | 90 | stopwatch.start(); 91 | dynamic_collision_pairs.clear(); 92 | for (PhysicsBall& ball : ball_collection) 93 | { 94 | if (ball.dead()) 95 | { 96 | continue; 97 | } 98 | intersect_objects_static_response(&ball); 99 | float intended_speed = ball.velocity().mag(); 100 | //float intended_distance = intended_speed * ball.physics_time_remaining(); 101 | float actual_distance = (ball.position() - ball.old_position()).mag(); 102 | float actual_time = 0.f; 103 | if (intended_speed > 0.f) 104 | { 105 | actual_time = actual_distance / intended_speed; 106 | } 107 | ball.physics_time_remaining(ball.physics_time_remaining() - actual_time); 108 | } 109 | 110 | dynamic_collision_responses(); 111 | 112 | stopwatch.stop(); 113 | current_collision_times.push_back(stopwatch.to_ms()); 114 | } 115 | stopwatch_all.stop(); 116 | current_update_times.push_back(stopwatch_all.to_ms()); 117 | } 118 | 119 | constexpr int cull_dead_threshold = 50; 120 | if (dead_count >= cull_dead_threshold) 121 | { 122 | std::erase_if(ball_collection, 123 | [](const PhysicsBall& ball) 124 | { 125 | return ball.dead(); 126 | }); 127 | } 128 | } 129 | 130 | void PhysicsEngine::init_timer_collections(int steps) 131 | { 132 | current_update_times.clear(); 133 | current_tree_build_times.clear(); 134 | current_collision_times.clear(); 135 | int reserve_quantity = std::max(0, steps); // 'steps' can be negative. 136 | current_update_times.reserve(reserve_quantity); 137 | current_tree_build_times.reserve(reserve_quantity); 138 | current_collision_times.reserve(reserve_quantity); 139 | } 140 | 141 | void PhysicsEngine::clear_quadtree(const World* engine) 142 | { 143 | quad_tree = nullptr; 144 | quad_tree = std::make_unique(PixelPoint{ 0, 0 }, engine->width(), engine->height(), Level(0)); 145 | } 146 | 147 | void PhysicsEngine::intersect_objects_static_response(PhysicsBall* ball) 148 | { 149 | assert(quad_tree != nullptr); 150 | quad_tree->for_each_in(QuadTree::box_for(*ball), 151 | [&](PhysicsBall* other) 152 | { 153 | if (other == ball) 154 | { 155 | return; 156 | } 157 | 158 | if (other->dead()) 159 | { 160 | return; 161 | } 162 | 163 | if (overlap_AABB(ball->bounding_box(), other->bounding_box()) 164 | && PhysicsBall::collides_with(*ball, *other)) 165 | { 166 | PhysicsBall::static_collision_response(ball, other); 167 | dynamic_collision_pairs.emplace_back(ball, other); 168 | } 169 | }); 170 | } 171 | 172 | void PhysicsEngine::dynamic_collision_responses() 173 | { 174 | for (auto [ball_a, ball_b] : dynamic_collision_pairs) 175 | { 176 | if (!ball_a->dead() && !ball_b->dead()) 177 | { 178 | PhysicsBall::dynamic_collision_response(ball_a, ball_b); 179 | } 180 | } 181 | } -------------------------------------------------------------------------------- /src/ball-pit.cpp: -------------------------------------------------------------------------------- 1 | import Bridges.PGE; 2 | 3 | import Physics; 4 | import Util; 5 | import World; 6 | 7 | class BallPit : olc::PixelGameEngine 8 | { 9 | using Base = olc::PixelGameEngine; 10 | 11 | public: 12 | BallPit() 13 | { 14 | sAppName = "Ball Pit"; 15 | } 16 | 17 | bool OnUserCreate() override 18 | { 19 | return true; 20 | } 21 | 22 | bool OnUserUpdate(float dt) override 23 | { 24 | bool quit = false; 25 | bool clear = false; 26 | bool show_help = false; 27 | // Poll input 28 | if (GetKey(olc::Key::ESCAPE).bReleased) 29 | { 30 | quit = true; 31 | } 32 | 33 | if (GetKey(olc::Key::TAB).bHeld) 34 | { 35 | show_help = true; 36 | } 37 | 38 | if (GetKey(olc::Key::C).bReleased) 39 | { 40 | clear = true; 41 | } 42 | 43 | // Toggle showing the quad tree. 44 | if (GetKey(olc::Key::Q).bReleased) 45 | { 46 | should_draw_quad_tree = !should_draw_quad_tree; 47 | } 48 | 49 | if (GetMouse(0).bHeld) 50 | { 51 | drop(); 52 | } 53 | 54 | // Update 55 | if (clear) 56 | { 57 | physics_engine.remove_all(); 58 | } 59 | physics_engine.update(dt, &world); 60 | 61 | // Draw 62 | Clear(olc::ModuleColors::Black()); 63 | 64 | // Let anything overlap this. 65 | DrawString({ 10, rep(world.height()) - 10 }, "TAB to show help", olc::ModuleColors::White()); 66 | 67 | draw_physics(); 68 | 69 | if (show_help) 70 | { 71 | draw_help(); 72 | } 73 | 74 | return !quit; 75 | } 76 | 77 | auto Construct(ScreenInfo info) 78 | { 79 | screen_info = info; 80 | world.set(info.width, info.height); 81 | return Base::Construct(rep(info.width), rep(info.height), rep(info.px_width), rep(info.px_height), false, true); 82 | } 83 | 84 | using Base::Start; 85 | private: 86 | PixelPoint mouse_position() const 87 | { 88 | return { GetMouseX(), GetMouseY() }; 89 | } 90 | 91 | void drop() 92 | { 93 | RealDistribution dis_velocity_x{ -1500.f, 1500.f }; 94 | RealDistribution dis_velocity_y{ -500.f, 500.f }; 95 | RealDistribution dis_stickyness{ 0.f, 0.f }; 96 | RealDistribution dis_friction{ .1f, .85f }; 97 | IntDistribution dis_color{ 0, 255 }; 98 | IntDistribution dis_radius{ 2, 10 }; 99 | for (int i = 0; i != 10; ++i) 100 | { 101 | PhysicsPoint velocity{ random_generator().generate(dis_velocity_x), random_generator().generate(dis_velocity_y) }; 102 | Color color { 103 | static_cast(random_generator().generate(dis_color)), 104 | static_cast(random_generator().generate(dis_color)), 105 | static_cast(random_generator().generate(dis_color)) }; 106 | auto radius = random_generator().generate(dis_radius); 107 | auto weight = radius * 2 + 1; 108 | physics_engine.add(PhysicsBall{ { mouse_position() }, 109 | velocity, 110 | color, 111 | Radius(radius), 112 | Weight(weight), 113 | { .stickyness = random_generator().generate(dis_stickyness), 114 | .friction = random_generator().generate(dis_friction) } }); 115 | } 116 | } 117 | 118 | void draw_help() 119 | { 120 | constexpr int num_options = 5; 121 | constexpr int text_height = 10; 122 | constexpr int total_height = 5 * 2 + num_options * text_height; 123 | constexpr int starting_point = 20; 124 | 125 | // Background 126 | FillRect({ 10, 10 }, { rep(world.width()) - starting_point, total_height + 10 }, olc::ModuleColors::Blue()); 127 | 128 | PixelPoint pos = { starting_point, starting_point }; 129 | // Quit 130 | DrawString(pos, "ESC: Quit", olc::ModuleColors::White()); 131 | pos.y += text_height; 132 | 133 | // Help 134 | DrawString(pos, "TAB: Show this help", olc::ModuleColors::White()); 135 | pos.y += text_height; 136 | 137 | // Clear balls 138 | DrawString(pos, "C: Clear balls", olc::ModuleColors::White()); 139 | pos.y += text_height; 140 | 141 | // Draw quad tree 142 | DrawString(pos, "Q: Draw quad trees", olc::ModuleColors::White()); 143 | pos.y += text_height; 144 | 145 | // Drop balls 146 | DrawString(pos, "Mouse 1: Drop balls!", olc::ModuleColors::White()); 147 | } 148 | 149 | void draw_physics() 150 | { 151 | int alive_balls = 0; 152 | for (const PhysicsBall& ball : physics_engine.objects()) 153 | { 154 | if (!ball.dead()) 155 | { 156 | ++alive_balls; 157 | 158 | if (ball.single_point()) 159 | { 160 | Draw(ball.position(), ball.color()); 161 | } 162 | else 163 | { 164 | FillCircle(ball.position(), rep(ball.radius()), ball.color()); 165 | } 166 | } 167 | } 168 | 169 | if (should_draw_quad_tree) 170 | { 171 | draw_quad_tree(); 172 | } 173 | 174 | DrawString({ 10, 10 }, to_string(alive_balls), olc::ModuleColors::White()); 175 | } 176 | 177 | void draw_quad_tree() 178 | { 179 | if (const QuadTree* tree = physics_engine.current_quad_tree()) 180 | { 181 | auto boxes = tree->all_boxes(); 182 | for (const QuadTree::BoundingBox& box : boxes) 183 | { 184 | const Color color = olc::ModuleColors::Red(); 185 | // Top line 186 | DrawLine({ box.left(), box.top() }, { box.right(), box.top() }, color); 187 | // Right line 188 | DrawLine({ box.right(), box.top() }, { box.right(), box.bottom() }, color); 189 | // Bottom line 190 | DrawLine({ box.right(), box.bottom() }, { box.left(), box.bottom() }, color); 191 | // Left line 192 | DrawLine({ box.left(), box.bottom() }, { box.left(), box.top() }, color); 193 | } 194 | } 195 | } 196 | 197 | ScreenInfo screen_info; 198 | World world; 199 | PhysicsEngine physics_engine; 200 | 201 | bool should_draw_quad_tree = false; 202 | }; 203 | 204 | int main() 205 | { 206 | BallPit ball_pit_game; 207 | if (ball_pit_game.Construct({ Width(640), Height(480), PixelWidth(2), PixelHeight(2) })) 208 | { 209 | ball_pit_game.Start(); 210 | } 211 | } --------------------------------------------------------------------------------