parent
dbeabb8ffc
commit
51900bb3af
@ -0,0 +1,493 @@ |
|||||||
|
/*
|
||||||
|
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 <typename OBJECT_TYPE> |
||||||
|
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<StaticQuadTree<OBJECT_TYPE>>(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<OBJECT_TYPE> search(const olc::rect& rArea) const |
||||||
|
{ |
||||||
|
std::list<OBJECT_TYPE> 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<OBJECT_TYPE>& 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<OBJECT_TYPE>& 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<OBJECT_TYPE> items() const |
||||||
|
{ |
||||||
|
// No questions asked, just return child items
|
||||||
|
std::list<OBJECT_TYPE> 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<olc::rect, 4> m_rChild{}; |
||||||
|
|
||||||
|
// 4 potential children of this StaticQuadTree
|
||||||
|
std::array<std::shared_ptr<StaticQuadTree<OBJECT_TYPE>>, 4> m_pChild{}; |
||||||
|
|
||||||
|
// Items which belong to this StaticQuadTree
|
||||||
|
std::vector<std::pair<olc::rect, OBJECT_TYPE>> m_pItems; |
||||||
|
}; |
||||||
|
|
||||||
|
|
||||||
|
template <typename OBJECT_TYPE> |
||||||
|
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<OBJECT_TYPE>; |
||||||
|
|
||||||
|
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<typename QuadTreeContainer::iterator> 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<typename QuadTreeContainer::iterator> search(const olc::rect& rArea) const |
||||||
|
{ |
||||||
|
std::list<typename QuadTreeContainer::iterator> 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<SomeObjectWithArea> vecObjects; |
||||||
|
|
||||||
|
// An equivalent quad tree of the objects
|
||||||
|
StaticQuadTreeContainer<SomeObjectWithArea> 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<float> 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<float> 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; |
||||||
|
} |
Loading…
Reference in new issue