I'm creating a simple 2D game and got a little bit stuck with a visibility cone algorithm, influenced by walls and other obstacles on the map.
The only issue I'm currently having, is that it's very slow.
On a map with ~100 square obstacles, an UpdateVision
call takes up to 60ms without compiler optimizations (up to 15ms with, so still very long), though depending on how many obstacles are in the cone. According to the profiler, most of the time is spent in find_intersection
.
I know one of the main causes: the algorithm actually checks all the obstacles in the 120 degree cone, ignoring how far away they are. The small cone is actually a masked version of this:
So ignoring far points early should help a lot. Except I can't simply ignore them, because then stuff like this happens:
How can I improve on this?
Here is the commented relevant part of my code:
struct IntersectionData {
float T1, T2;
};
/*
* For two lines, (p11, p12) and (p21, p22)
* Finds their intersection point.
* The point coordinate ca be calculated with: p11 + T1 * (p12 - p11) OR p21 + T2 * (p22 - p21)
*
* Formula from: http://ncase.me/sight-and-light/
*/
IntersectionData find_intersection(const sf::Vector2f &p11, const sf::Vector2f &p12, const sf::Vector2f &p21, const sf::Vector2f &p22)
{
float r_px = p11.x, r_py = p11.y;
float r_dx = p12.x-p11.x, r_dy = p12.y-p11.y;
float s_px = p21.x, s_py = p21.y;
float s_dx = p22.x-p21.x, s_dy = p22.y-p21.y;
float T1, T2;
T2 = (r_dx*(s_py-r_py) + r_dy*(r_px-s_px))/(s_dx*r_dy - s_dy*r_dx);
if (r_dx != 0)
T1 = (s_px+s_dx*T2-r_px)/r_dx;
else
T1 = (s_py+s_dy*T2-r_py)/r_dy;
return IntersectionData{T1, T2};
}
/*
* Finds the closest intersection of a ray with origin (x, y) and angle a
* With a wall
* A wall contains a vector of points, making a convex shape.
*/
sf::Vector2f Enemy::CastRay(float a, const std::vector<std::shared_ptr<Wall>> &walls) {
float best_T1 = 1000000;
const float cos_a = cosf(a), sin_a = sinf(a);
for(const auto &wall : walls){
for(size_t i = 0; i < wall->points.size(); ++i){
const auto &p1 = wall->points[i];
const auto &p2 = wall->points[(i+1)%4];
const auto intersection = find_intersection({x, y}, {x+cos_a, y+sin_a}, p1, p2);
const float &T1 = intersection.T1, &T2 = intersection.T2;
if ((T1 > 0) && (T2 > 0) && (T2 < 1))
if(T1 < best_T1)
best_T1 = T1;
}
}
return {x + best_T1 * cos_a, y + best_T1 * sin_a};
}
/*
* Algorithm from: http://ncase.me/sight-and-light/
*/
void Enemy::UpdateVision(const std::vector<std::shared_ptr<Wall>> &walls)
{
// first, let's get all the vertices of all the walls
std::vector<sf::Vector2f> points;
for(const auto &wall : walls)
for(const auto &p : wall->points)
points.push_back(p);
std::vector<sf::Vector2f> visPoints;
for(const auto& point : points) {
float angle = atan2(point.y-y, point.x-x);
float normalized = NormalizeAngle(angle);
// if the point is outside of the cone of vision, ignore it
if(normalized > visibilityAngle || normalized < -visibilityAngle)
continue;
// analyze two slightly off angles, to see both a wall the point belongs to and possibly a wall just behind it
float a1 = angle + 0.0001, a2 = angle - 0.0001;
auto p1 = CastRay(a1, walls);
auto p2 = CastRay(a2, walls);
visPoints.push_back(p1);
visPoints.push_back(p2);
}
// also add two rays just on the edges of cone of vision
visPoints.push_back(CastRay(direction - visibilityAngle, walls));
visPoints.push_back(CastRay(direction + visibilityAngle, walls));
// sort the points based on their angle, so they can be correctly drawn with OpenGL TriangleFan mode
std::sort(visPoints.begin(), visPoints.end(),
[&](const sf::Vector2f &p1, const sf::Vector2f &p2) {
return NormalizeAngle(atan2(p2.y-y, p2.x-x)) < NormalizeAngle(atan2(p1.y-y, p1.x-x));
});
float mid = texture.getSize().x / 2;
// vision is a vector of sf::Vertex and is used for actual rendering
// the third argument is the UV coordinate on the texture
vision.clear();
vision.push_back(sf::Vertex({x, y}, sf::Color::Green, {mid, mid}));
for(auto &p : visPoints)
vision.push_back(sf::Vertex(p, sf::Color::Green, {p.x - x + mid, p.y - y + mid}));
}