-
Notifications
You must be signed in to change notification settings - Fork 557
Add 2D raycast #1021
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
Add 2D raycast #1021
Conversation
9344d7c
to
fc0bfd3
Compare
I guess finding the closest instance of a particular type of component will be something that's useful, which would require doing the check inside the raycasting? I wish there was a way to avoid that, though the only other option would to basically have the raycaster be an iterator of some sort, maintaining it's internal state and allowing you to grab the next intersection through a method call. It might be worth thinking about how this would interact with some sort of compiled asm.js collision engine. I believe that the first approach would be tough (the compiled code couldn't interact with general crafty code, and so it would be tricky for it to inspect entities for the presence of components), while the latter approach would perhaps be more natural in such a context? |
I'd expect that it's not worth it -- if used properly (i.e. don't put 100s entities in the same cell) the spatial map is itself the broadphase check. This is probably more true of raycasting than regular collisions, since SAT is more expensive than the simpler checks here. |
I wonder if the best approach for Canvas redrawing would be to have it flag cells of the hashmap as dirty, rather than carefully keeping track of the bounding areas of individual entities. |
IIRC calling boundaries is actually pretty expensive, maybe not something we want to do every time raycast is called. A possible optimization is caching them, and only recalculating the boundaries if the hashmap has been modified? (i.e. have a dirty flag on the hashmap) |
2610556
to
ef405bc
Compare
837a759
to
d8d68fa
Compare
@starwed Thanks for the feedback!
Added
Removed
Added What's still missing:
If you're ok with it, I'd include the latter 2 points into a follow-up PR (with some additional proposals in regards to bounding rectangle handling). I feel confident about the implementation now, so if you have time and are up for it, take a look at it, I've included extensive implementation-specific documentation. |
I don't quite understand what the 1st approach and 2nd approach is :)
In an 'Asteroids' like game, where you have small entities scattered all around the scene, it might perhaps require redrawing all cells? Might work well in the general case though. Maybe some additional metrics / statistics could be considered here, like heuristical cluster analysis? Might be overkill though :) |
No worries, I was just trying to think through how an underlying optimized algorithm would work, and probably wasn't very clear. In the end the API the user calls would be unchanged.
Well, it would work the same way it does now -- if a large fraction of entities need redrawing, just do them all.
I might be forgetting how our existing code works, but what's the change here? |
Ah, I'm glad you asked, there is no problem at all. Render code calls Side-note:
True, I was just wondering about the edge case where percentage of dirty entities < 50% and their dimension << cellsize. I like this proposal, as it could potentially improve performance for Canvas rendering. Drawing dirty entities requires merging their dirty rectangles, which sounds like an expensive operation. Drawing dirty cells could be done by just clearing each individual cell and drawing its entities. |
From what I understand, you always want your cellsize to be some small multiple of the typical entity size. If you have lots of small entities moving around inside a single cell, collision performance will suffer because the broad-phase pass will leave too many candidates for the more expensive check. We kind of support setting the cellsize before init, but it should probably be an actual option exposed through the api. |
10ba225
to
48fe40f
Compare
Ok, added the missing stuff (api docs, test file organization and some minor tweaks). |
9910d9d
to
c7506cb
Compare
@starwed |
@mucaho I haven't had time to fully look through it, but so long as it doesn't seem likely to affect performance I don't have any problem with landing it. Like you say, we can always tune it up before release. (And if we're concerned, add a warning to the docs that the api is new and possibly unstable.) Feel free to merge once the conflicts are resolved. |
Let the parameter be an entry, like in Crafty_map_refresh.
Add spatial map iteration. Add polygon - ray intersection. Add raycast playground. Add tests. fixes craftyjs#729.
There may be certainly some things that could get optimized. |
Development finished. Pending review.
Fixes outstanding issue #729 .
Screenshot taken from playground example:

There is still lots to do, but I would appreciate a little input on the API design:
Added a separate, private methodCrafty.map.boundaries(findKeys)
was extended to find only hashmap cell keys if optional parameter is provided. Should this stay here (introduces some branching in code) or make an explicit methodCrafty.map.keyBoundaries()
(code duplication)?Crafty.map._keyBoundaries()
.Crafty.map.iterateMap(origin, direction, entryCallback)
iterates the hashmap in the direction of the ray. CallsentryCallback
for each hashmap entry (obj). IfentryCallback
returnstrue
, aborts iteration immediately.Removed from implementationCollision.approximateDistanceFromRay(origin, direction)
returns the distance to nearest corner of the entity's bounding rectangle (if there is an intersection).Crafty.polygon.distanceFromRay(origin, direction)
returns the distance to the closest intersection with any of the polygon's segements (if there is at least one intersection).Crafty.raycast(origin, direction, maxDistance, comp, sort)
uses the above described methods to find an intersection. Returns an array of raycast results, each raycast result looks like{obj: obj, distance: distance, x: intersection_point_x, y: intersection_point_y}
.maxDistance
is Infinity, finds all intersections (this is the default case). If its negative, finds only first intersection. If its a positive value, finds intersection up to that distance.comp
string parameter is supplied, returns only entities that have the specified componentsort
boolean parameter is supplied and is set tofalse
, then the results are not sorted by distance (opt-out, slight performance gain)removed from implementationcheckBroadphase
dictates whether an approximate broadphase intersection check (approximateDistanceFromRay
) should be used to filter objects, before checking for exact intersection (withdistanceFromRay
)origin
&direction
describe the ray.direction
must be normalized. Should this be mentioned in the docs, or should input sanitation (and thus this normalization) be done?AddedCrafty.raycast
returns an array of raycast results. Duplicates are weeded out, but the array is not sorted by distance. Should there be an additional parametersort
(similar to thefilter
parameter inCrafty.map.search
) that optionally sorts the results (and has performance impact)?Notes:
I still have to test whether the approximate broadphase intersection check is worth it, and how many segments a polygon must have (increases complexity of the exact intersection check) to be worth it.Removed from implementationCollision (and raycast) logic needs the collision bounding rectangle (Nope, render code just uses plain rectangles for searching the spatial map, so it doesn't matter.this._cbr || this._mbr || this
). Rendering stuff needs the minimum bounding rectangle (this._mbr || this
).The considered bounding rectangle is hardcoded in the hashmap's functions (currently the collision one is used). This can possibly lead to (and possibly has already lead to; haven't fully tested) problems with drawing dirty regions on Canvas, when there exists a collision hitbox outside of the entity's natural bounds (
_cbr != null
).Having separate hashmaps for collision stuff and for each layer would help in configuring each hashmap so that the correct property gets accessed for the bounding rectangle. A more immediate fix would be to provide an optional argument to each of the hashmaps methods, which dictates which bounding rectangle property to use.