/* Quirky Quad Trees Part #1 - Static Quad Tree Implementation "War... huh... What is it good for? Absolutely nothin..." - javidx9 License (OLC-3) ~~~~~~~~~~~~~~~ Copyright 2018 - 2022 OneLoneCoder.com Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions or derivations of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions or derivative works in binary form must reproduce the above copyright notice. This list of conditions and the following disclaimer must be reproduced in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Video: ~~~~~~ https://youtu.be/ASAowY6yJII Pan & Zoom with middle mouse, TAB to switch between methods Links ~~~~~ YouTube: https://www.youtube.com/javidx9 https://www.youtube.com/javidx9extra Discord: https://discord.gg/WhwHUMV Twitter: https://www.twitter.com/javidx9 Twitch: https://www.twitch.tv/javidx9 GitHub: https://www.github.com/onelonecoder Homepage: https://www.onelonecoder.com Author ~~~~~~ David Barr, aka javidx9, ŠOneLoneCoder 2019, 2020, 2021, 2022 */ #define OLC_PGE_APPLICATION #include "olcPixelGameEngine.h" #define OLC_PGEX_TRANSFORMEDVIEW #include "olcPGEX_TransformedView.h" namespace olc { struct rect { olc::vf2d pos; olc::vf2d size; rect(const olc::vf2d& p = { 0.0f, 0.0f }, const olc::vf2d& s = { 1.0f, 1.0f }) : pos(p), size(s) { } constexpr bool contains(const olc::vf2d& p) const { return !(p.x < pos.x || p.y < pos.y || p.x >= (pos.x + size.x) || p.y >= (pos.y + size.y)); } constexpr bool contains(const olc::rect& r) const { return (r.pos.x >= pos.x) && (r.pos.x + r.size.x < pos.x + size.x) && (r.pos.y >= pos.y) && (r.pos.y + r.size.y < pos.y + size.y); } constexpr bool overlaps(const olc::rect& r) const { return (pos.x < r.pos.x + r.size.x && pos.x + size.x >= r.pos.x && pos.y < r.pos.y + r.size.y && pos.y + size.y >= r.pos.y); } }; }; // Constrain depth of Quad Tree. Since its floating point, it could in principle sub-divide for // a very long time, consuming far more time and memory than is sensible constexpr size_t MAX_DEPTH = 8; template class StaticQuadTree { public: StaticQuadTree(const olc::rect& size = { {0.0f, 0.0f}, {100.0f, 100.0f} }, const size_t nDepth = 0) { m_depth = nDepth; resize(size); } // Force area change on Tree, invalidates this and all child layers void resize(const olc::rect& rArea) { // Erase this layer clear(); // Recalculate area of children m_rect = rArea; olc::vf2d vChildSize = m_rect.size / 2.0f; // Cache child areas local to this layer m_rChild = { // Top Left olc::rect(m_rect.pos, vChildSize), // Top Right olc::rect({m_rect.pos.x + vChildSize.x, m_rect.pos.y}, vChildSize), // Bottom Left olc::rect({m_rect.pos.x, m_rect.pos.y + vChildSize.y}, vChildSize), // Bottom Right olc::rect(m_rect.pos + vChildSize, vChildSize) }; } // Clears the contents of this layer, and all child layers void clear() { // Erase any items stored in this layer m_pItems.clear(); // Iterate through children, erase them too for (int i = 0; i < 4; i++) { if (m_pChild[i]) m_pChild[i]->clear(); m_pChild[i].reset(); } } // Returns a count of how many items are stored in this layer, and all children of this layer size_t size() const { size_t nCount = m_pItems.size(); for (int i = 0; i < 4; i++) if (m_pChild[i]) nCount += m_pChild[i]->size(); return nCount; } // Inserts an object into this layer (or appropriate child layer), given the area the item occupies void insert(const OBJECT_TYPE& item, const olc::rect& itemsize) { // Check each child for (int i = 0; i < 4; i++) { // If the child can wholly contain the item being inserted if (m_rChild[i].contains(itemsize)) { // Have we reached depth limit? if (m_depth + 1 < MAX_DEPTH) { // No, so does child exist? if (!m_pChild[i]) { // No, so create it m_pChild[i] = std::make_shared>(m_rChild[i], m_depth + 1); } // Yes, so add item to it m_pChild[i]->insert(item, itemsize); return; } } } // It didnt fit, so item must belong to this quad m_pItems.push_back({ itemsize, item }); } // Returns a list of objects in the given search area std::list search(const olc::rect& rArea) const { std::list listItems; search(rArea, listItems); return listItems; } // Returns the objects in the given search area, by adding to supplied list void search(const olc::rect& rArea, std::list& listItems) const { // First, check for items belonging to this area, add them to the list // if there is overlap for (const auto& p : m_pItems) { if (rArea.overlaps(p.first)) listItems.push_back(p.second); } // Second, recurse through children and see if they can // add to the list for (int i = 0; i < 4; i++) { if (m_pChild[i]) { // If child is entirely contained within area, recursively // add all of its children, no need to check boundaries if (rArea.contains(m_rChild[i])) m_pChild[i]->items(listItems); // If child overlaps with search area then checks need // to be made else if (m_rChild[i].overlaps(rArea)) m_pChild[i]->search(rArea, listItems); } } } void items(std::list& listItems) const { // No questions asked, just return child items for (const auto& p : m_pItems) listItems.push_back(p.second); // Now add children of this layer's items for (int i = 0; i < 4; i++) if (m_pChild[i]) m_pChild[i]->items(listItems); } std::list items() const { // No questions asked, just return child items std::list listItems; items(listItems); return listItems; } // Returns area of this layer const olc::rect& area() { return m_rect; } protected: // Depth of this StaticQuadTree layer size_t m_depth = 0; // Area of this StaticQuadTree olc::rect m_rect; // 4 child areas of this StaticQuadTree std::array m_rChild{}; // 4 potential children of this StaticQuadTree std::array>, 4> m_pChild{}; // Items which belong to this StaticQuadTree std::vector> m_pItems; }; template class StaticQuadTreeContainer { // Using a std::list as we dont want pointers to be invalidated to objects stored in the // tree should the contents of the tree change using QuadTreeContainer = std::list; protected: // The actual container QuadTreeContainer m_allItems; // Use our StaticQuadTree to store "pointers" instead of objects - this reduces // overheads when moving or copying objects StaticQuadTree root; public: StaticQuadTreeContainer(const olc::rect& size = { {0.0f, 0.0f}, { 100.0f, 100.0f } }, const size_t nDepth = 0) : root(size, nDepth) { } // Sets the spatial coverage area of the quadtree // Invalidates tree void resize(const olc::rect& rArea) { root.resize(rArea); } // Returns number of items within tree size_t size() const { return m_allItems.size(); } // Returns true if tree is empty bool empty() const { return m_allItems.empty(); } // Removes all items from tree void clear() { root.clear(); m_allItems.clear(); } // Convenience functions for ranged for loop typename QuadTreeContainer::iterator begin() { return m_allItems.begin(); } typename QuadTreeContainer::iterator end() { return m_allItems.end(); } typename QuadTreeContainer::const_iterator cbegin() { return m_allItems.cbegin(); } typename QuadTreeContainer::const_iterator cend() { return m_allItems.cend(); } // Insert item into tree in specified area void insert(const OBJECT_TYPE& item, const olc::rect& itemsize) { // Item is stored in container m_allItems.push_back(item); // Pointer/Area of item is stored in quad tree root.insert(std::prev(m_allItems.end()), itemsize); } // Returns a std::list of pointers to items within the search area std::list search(const olc::rect& rArea) const { std::list listItemPointers; root.search(rArea, listItemPointers); return listItemPointers; } }; // The Example! class Example_StaticQuadTree : public olc::PixelGameEngine { public: Example_StaticQuadTree() { sAppName = "Static QuadTree"; } protected: olc::TransformedView tv; // An example object of something in 2D space struct SomeObjectWithArea { olc::vf2d vPos; olc::vf2d vVel; olc::vf2d vSize; olc::Pixel colour; }; // A regular list of the objects std::list vecObjects; // An equivalent quad tree of the objects StaticQuadTreeContainer treeObjects; // The "length" of one side of the "world" the objects reside in float fArea = 100000.0f; bool bUseQuadTree = true; public: bool OnUserCreate() override { // Transform View - enables Pan & Zoom tv.Initialise({ ScreenWidth(), ScreenHeight() }); // Create the tree, and size it to the world treeObjects.resize(olc::rect({ 0.0f, 0.0f }, { fArea, fArea })); // Dirty random float generator auto rand_float = [](const float a, const float b) { return float(rand()) / float(RAND_MAX) * (b - a) + a; }; // Create 1,000,000 objects, push into both containers (so 2,000,000 I suppose :P ) for (int i = 0; i < 1000000; i++) { SomeObjectWithArea ob; ob.vPos = { rand_float(0.0f, fArea), rand_float(0.0f, fArea) }; ob.vSize = { rand_float(0.1f, 100.0f), rand_float(0.1f, 100.0f) }; ob.colour = olc::Pixel(rand() % 256, rand() % 256, rand() % 256); treeObjects.insert(ob, olc::rect(ob.vPos, ob.vSize)); vecObjects.push_back(ob); } return true; } bool OnUserUpdate(float fElapsedTime) override { // Tab switches between modes if (GetKey(olc::Key::TAB).bPressed) bUseQuadTree = !bUseQuadTree; tv.HandlePanAndZoom(); // Get rectangle that equates to screen in world space olc::rect rScreen = { tv.GetWorldTL(), tv.GetWorldBR() - tv.GetWorldTL() }; size_t nObjectCount = 0; if (bUseQuadTree) { // QUAD TREE MODE auto tpStart = std::chrono::system_clock::now(); // Use search function to return list of pointers to objects in that area for (const auto& object : treeObjects.search(rScreen)) { tv.FillRectDecal(object->vPos, object->vSize, object->colour); nObjectCount++; } std::chrono::duration duration = std::chrono::system_clock::now() - tpStart; std::string sOutput = "Quadtree " + std::to_string(nObjectCount) + "/" + std::to_string(vecObjects.size()) + " in " + std::to_string(duration.count()); DrawStringDecal({ 4, 4 }, sOutput, olc::BLACK, { 4.0f, 8.0f }); DrawStringDecal({ 2, 2 }, sOutput, olc::WHITE, { 4.0f, 8.0f }); } else { // LINEAR SEARCH MODE auto tpStart = std::chrono::system_clock::now(); // Blindly check all objects to see if they overlap with screen for (const auto& object : vecObjects) { if (rScreen.overlaps({ object.vPos, object.vSize })) { tv.FillRectDecal(object.vPos, object.vSize, object.colour); nObjectCount++; } } std::chrono::duration duration = std::chrono::system_clock::now() - tpStart; std::string sOutput = "Linear " + std::to_string(nObjectCount) + "/" + std::to_string(vecObjects.size()) + " in " + std::to_string(duration.count()); DrawStringDecal({ 4, 4 }, sOutput, olc::BLACK, { 4.0f, 8.0f }); DrawStringDecal({ 2, 2 }, sOutput, olc::WHITE, { 4.0f, 8.0f }); } return true; } }; int main() { Example_StaticQuadTree demo; if (demo.Construct(1280, 960, 1, 1, false, false)) demo.Start(); return 0; }