-
Notifications
You must be signed in to change notification settings - Fork 22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implements first draft of quadtree lookup implemented via *BoundingBox* #12
Implements first draft of quadtree lookup implemented via *BoundingBox* #12
Conversation
We can't add a I imagine the object we'd be adding to the quad tree would be a pair of a struct Thing {
bounds: BoundingBox,
entity: Entity,
}
EDIT: I had another look. We're trying to say any object implementing our I don't think we'd be able to add that generic impl for |
With this it is now possible to insert *Entities* from *World* into a quadtree And query for them using *BoundingBox* queries
Using an intermediate |
and keep track of changes to entities within it
Oof this commit got quite wordy Would it make sense to implement an API similar to let space = world.read_resource::<SpaceTree>();
space.query_point();
space.query_region(); instead of the hacky methods on the system itself? |
arcs/src/systems/spatial_relation.rs
Outdated
} | ||
} | ||
|
||
pub fn query_point(&self, pt: Vector) -> Option<Vec<Entity>> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably be possible to be called from somewhere else
with a similar structure to the *NameTableBookkeeping*
…Relation* system now
I think I found a way around the problem of my branch being dependency-locked to
We should be able to reference different versions using this:
|
I'm about to land #13 which uses |
It's going to break all the tests and some methods on |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for getting the ball rolling on this.
After looking at the code I'm not overly satisfied with aabb-quadtree
's API. It feels a bit restrictive and doesn't necessarily match the way I've needed to use these sorts of data structures in the past.
What did it feel like trying to wrap the QuadTree
when writing Space
?
fn default_tree() -> SpatialTree{ | ||
// Initialize quadtree | ||
let size = BoundingBox::new( | ||
Vector::new(-Self::WORLD_RADIUS, -Self::WORLD_RADIUS), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a massive fan of telling the SpatialTree
the size of the world up-front... Will the code panic if we choose the wrong number and try to place something outside of the space? And what happens if the space is a lot bigger than it needs to be, will we consume an unnecessary amount of memory?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As i commented here, the trees bounding_box
is hard-coded on initialization. If we try to insert something outside of those bounds afterwards it will panic.
I'm not a fan of how the aabb_quadtree
handles this and I only see 3 options:
- Check all insertions in our API and rebuild the tree with a big enough bounding box every time (seems really inefficient)
- Allocate a very big world size (also inefficient)
- Use another
Quadtree
implementation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did some more thinking on this:
Space
needs to implementDefault
so we can use it as aResource
, this inevitely means we have to define some default starting parameters, as we can not supply arguments toSpace::default()
.- Memory-wise we should still be quite efficient, as very large empty space in a quad-tree should take the same amount of memory as small, filled spaces
- To be extra-safe I could write a wrapper around
aabb_quadtree.insert()
which detects objects outside the bounds of the tree-root and resize the tree if needed - Resizing basically means re-building the whole tree again, but with a large enough BoundingBox so the newly added
SpatialEntity
fits in
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Space needs to implement Default so we can use it as a Resource, this inevitely means we have to define some default starting parameters, as we can not supply arguments to Space::default().
Could we add Space
as a resource in the bookkeeping system's setup()
method then use ReadExpect
when trying to access it?
Resizing basically means re-building the whole tree again, but with a large enough BoundingBox so the newly added SpatialEntity fits in
That's annoying.
I was kinda hoping you could implement a resize operation by taking the quad tree's "head" node and wrapping it in a larger node, almost like the tree equivalent of adding an item to the front of a linked list. You'd be left with a really unbalanced tree (the entire previous tree is in one quadrant of the tree's head node), but it turns a resize into an O(1)
operation where we just copy some pointers around.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we add Space as a resource in the bookkeeping system's setup() method then use ReadExpect when trying to access it?
This seems like an unnecessary mix of two unrelated systems. So I'd rather stick to Space
being it's own system
almost like the tree equivalent of adding an item to the front of a linked list.
There is nothing in the API that woudl allow for this.
It really looks like we need a better backend for our Space
).aabb(); | ||
let quadtree: SpatialTree = QuadTree::new( | ||
size, | ||
true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably pull these out into constants, because true, 4, 16, 8, 4
doesn't really mean anything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
} | ||
|
||
pub fn insert(&mut self, spatial: SpatialEntity) { | ||
let entity_id = spatial.entity.id(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we using id()
here instead of the full Entity
? An Entity
also contains a Generation
so you can't get the equivalent of a use-after-free when an entity gets deleted and specs
allocates the same Index
to an entity created later on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually that makes more sense!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
let mut system = SpatialRelation::new(&world); | ||
System::setup(&mut system, &mut world); | ||
|
||
// make some changes after the initial setup |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As well as object creation, we may want to make sure object modification and deletion are detected too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit stuck on that part @Michael-F-Bryan ,
Similar as to the test for name_table_bookkeeping
after we read a resource from world we can no longer borrow world as mutable.
let space = world.read_resource::<Space>(); // <- world is immutably borrowed here
world.delete_entity(first).unwrap(); // <- We would mutably borrow here
I can't seem to find a nice pattern/api for either dropping space or another way of letting us do queries but also still being able to manipulate the world afterwards.
How would you implement/test this for NameTable
the implementation for Space
should be similar
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... I guess you could fetch the space
every time you need it instead of putting it in a local variable.
We should only need the space
after the second system.run_now()
when we do our assertions though, shouldn't we?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, never storing Space
in a variable solves this limitation.
arcs/src/systems/spatial_relation.rs
Outdated
let space = world.read_resource::<Space>(); | ||
|
||
let query = space.query_point(Vector::new(3.0, -0.5)); | ||
assert!(query != None); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be written as assert_ne!(query, None)
or assert!(query.is_some())
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
arcs/src/systems/spatial_relation.rs
Outdated
|
||
let query = space.query_point(Vector::new(3.0, -0.5)); | ||
assert!(query != None); | ||
assert_eq!(query.unwrap().len(), 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... After seeing how a typical caller will be using the query, does it actually make sense to return an Option<Vec<_>>
?
If we return an empty Vec
(note: it's guaranteed that empty vecs don't allocate) when the query has no results, the query_*()
methods should be a lot more ergonomic.
Also, semantically returning an empty Vec
means your query completed but nothing was found. Which kinda makes sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True that makes more sense!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
…rnung *impl Iterator*
added panics for unexpected outcomes
… implement-quadtree-lookup
…om space when encountering *ComponentEvent::Removed*
out-of-bounds at a later stage
… implement-quadtree-lookup
I Should have taken care of all comments, some further thought:
|
I agree with you there, but I'd like to see how far we can get without implementing our own. The longer we wait, the more we'll know what sort of API we want. |
I found an interesting article called A dive into spatial search algorithms. They propose a nice way of retrieving query results in order...
|
That is indeed an interesing query algorithm! I wrote a toy quadtree alongside implementing |
In the first draft for this I implemented the quadtree insertion via
BoundingBox
. I would have liked to implement theSpatial
trait fromaabb
forBounded
but i could not get it to work without usingBox<dyn Bounded>
It would probably make more sense to implement
Spatial
forDrawingObject
as this is for now the main thing we want to query for.