/* Copyright (C) 2015 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ /** * @file * Vertex-based algorithm for CCmpPathfinder. * Computes paths around the corners of rectangular obstructions. * * Useful search term for this algorithm: "points of visibility". * * Since we sometimes want to use this for avoiding moving units, there is no * pre-computation - the whole visibility graph is effectively regenerated for * each path, and it does A* over that graph. * * This scales very poorly in the number of obstructions, so it should be used * with a limited range and not exceedingly frequently. */ #include "precompiled.h" #include "CCmpPathfinder_Common.h" #include "lib/timer.h" #include "ps/Profile.h" #include "simulation2/components/ICmpObstructionManager.h" #include "simulation2/helpers/PriorityQueue.h" #include "simulation2/helpers/Render.h" /* Quadrant optimisation: * (loosely based on GPG2 "Optimizing Points-of-Visibility Pathfinding") * * Consider the vertex ("@") at a corner of an axis-aligned rectangle ("#"): * * TL : TR * : * ####@ - - - * ##### * ##### * BL ## BR * * The area around the vertex is split into TopLeft, BottomRight etc quadrants. * * If the shortest path reaches this vertex, it cannot continue to a vertex in * the BL quadrant (it would be blocked by the shape). * Since the shortest path is wrapped tightly around the edges of obstacles, * if the path approached this vertex from the TL quadrant, * it cannot continue to the TL or TR quadrants (the path could be shorter if it * skipped this vertex). * Therefore it must continue to a vertex in the BR quadrant (so this vertex is in * *that* vertex's TL quadrant). * * That lets us significantly reduce the search space by quickly discarding vertexes * from the wrong quadrants. * * (This causes badness if the path starts from inside the shape, so we add some hacks * for that case.) * * (For non-axis-aligned rectangles it's harder to do this computation, so we'll * not bother doing any discarding for those.) */ static const u8 QUADRANT_NONE = 0; static const u8 QUADRANT_BL = 1; static const u8 QUADRANT_TR = 2; static const u8 QUADRANT_TL = 4; static const u8 QUADRANT_BR = 8; static const u8 QUADRANT_BLTR = QUADRANT_BL|QUADRANT_TR; static const u8 QUADRANT_TLBR = QUADRANT_TL|QUADRANT_BR; static const u8 QUADRANT_ALL = QUADRANT_BLTR|QUADRANT_TLBR; typedef __m128 m128; // A vertex around the corners of an obstruction // (paths will be sequences of these vertexes) struct Vertex { enum { UNEXPLORED, OPEN, CLOSED, }; CFixedVector2D p; fixed g, h; u16 pred; u8 status; u8 quadInward : 4; // the quadrant which is inside the shape (or NONE) u8 quadOutward : 4; // the quadrants of the next point on the path which this vertex must be in, given 'pred' }; // Obstruction edges (paths will not cross any of these). // Defines the two points of the edge. struct Edge { CFixedVector2D p0, p1; }; // Axis-aligned obstruction squares (paths will not cross any of these). // Defines the opposing corners of an axis-aligned square // (from which four individual edges can be trivially computed), requiring p0 <= p1 struct Square { CFixedVector2D p0, p1; }; // Axis-aligned obstruction edges. // p0 defines one end; c1 is either the X or Y coordinate of the other end, // depending on the context in which this is used. struct EdgeAA { CFixedVector2D p0; fixed c1; }; // When computing vertexes to insert into the search graph, // add a small delta so that the vertexes of an edge don't get interpreted // as crossing the edge (given minor numerical inaccuracies) static const entity_pos_t EDGE_EXPAND_DELTA = entity_pos_t::FromInt(1)/4; /** * Check whether a ray from 'a' to 'b' crosses any of the edges. * (Edges are one-sided so it's only considered a cross if going from front to back.) */ inline static bool CheckVisibility(CFixedVector2D a, CFixedVector2D b, const std::vector& edges, size_t size) { CFixedVector2D abn = (b - a).Perpendicular(); // Edges of general non-axis-aligned shapes for (size_t i = 0; i < size; ++i) { CFixedVector2D p0 = edges[i].p0; CFixedVector2D p1 = edges[i].p1; CFixedVector2D d = (p1 - p0).Perpendicular(); // If 'a' is behind the edge, we can't cross fixed q = (a - p0).Dot(d); if (q < fixed::Zero()) continue; // If 'b' is in front of the edge, we can't cross fixed r = (b - p0).Dot(d); if (r > fixed::Zero()) continue; // The ray is crossing the infinitely-extended edge from in front to behind. // Check the finite edge is crossing the infinitely-extended ray too. // (Given the previous tests, it can only be crossing in one direction.) fixed s = (p0 - a).Dot(abn); if (s > fixed::Zero()) continue; fixed t = (p1 - a).Dot(abn); if (t < fixed::Zero()) continue; return false; } return true; } /* * MARIJN VERSIE SIMD 4 Vectors -> 2 Dot Products */ __m128 MarijnDotSIMDV4(const CFixedVector2D& vectorA1, const CFixedVector2D& vectorA2, const CFixedVector2D& vectorB1, const CFixedVector2D& vectorB2) { __m128 vector1 = _mm_set_ps(vectorA1.X.GetInternalValue(), vectorA1.Y.GetInternalValue(), vectorB1.X.GetInternalValue(), vectorB1.Y.GetInternalValue()); // A1x, A1y, B1x, B1y __m128 vector2 = _mm_set_ps(vectorA2.X.GetInternalValue(), vectorA2.Y.GetInternalValue(), vectorB2.X.GetInternalValue(), vectorB2.Y.GetInternalValue()); // A2x, A2y, B2x, B2y __m128 multiplied = _mm_mul_ps(vector1, vector2); //A1x*A2x, A1y*A2y, B1x*B2x, B1y*B2y __m128 added = _mm_hadd_ps(multiplied, multiplied); // DotA, DotB, DotA, DotB return added; } /** * Compute the dot product of four vectors using SIMD */ __m128 DotSIMD(const CFixedVector2D& v1, const CFixedVector2D& u1, const CFixedVector2D& v2, const CFixedVector2D& u2, const CFixedVector2D& v3, const CFixedVector2D& u3, const CFixedVector2D& v4, const CFixedVector2D& u4) { __m128 vx4 = _mm_set_ps(v1.X.GetInternalValue(), v2.X.GetInternalValue(), v3.X.GetInternalValue(), v4.X.GetInternalValue()); __m128 ux4 = _mm_set_ps(u1.X.GetInternalValue(), u2.X.GetInternalValue(), u3.X.GetInternalValue(), u4.X.GetInternalValue()); __m128 vy4 = _mm_set_ps(v1.Y.GetInternalValue(), v2.Y.GetInternalValue(), v3.Y.GetInternalValue(), v4.Y.GetInternalValue()); __m128 uy4 = _mm_set_ps(u1.Y.GetInternalValue(), u2.Y.GetInternalValue(), u3.Y.GetInternalValue(), u4.Y.GetInternalValue()); __m128 sum = _mm_add_ps(_mm_mul_ps(vx4, ux4), _mm_mul_ps(vy4, uy4)); return sum; } /** * Compute the dot product of two vectors using SIMD //*/ //__m128 DotSIMD(const CFixedVector2D& v1, const CFixedVector2D& u1, const CFixedVector2D& v2, const CFixedVector2D& u2) //{ // __m128 v4 = _mm_set_ps(v1.X.GetInternalValue(), v2.X.GetInternalValue(), v1.Y.GetInternalValue(), v2.Y.GetInternalValue()); // __m128 u4 = _mm_set_ps(u1.X.GetInternalValue(), u2.X.GetInternalValue(), u1.Y.GetInternalValue(), u2.Y.GetInternalValue()); // __m128 mul = _mm_mul_ps(v4, u4); // //__m128 sum = // // //return sum; //} // Handle the axis-aligned shape edges separately (for performance): // (These are specialised versions of the general unaligned edge code. // They assume the caller has already excluded edges for which 'a' is // on the wrong side.) inline static bool CheckVisibilityLeft(CFixedVector2D a, CFixedVector2D b, const std::vector& edges, size_t size) { if (a.X >= b.X) return true; CFixedVector2D abn = (b - a).Perpendicular(); //EdgeAA checkedEdges[1024]; /* size_t first = (size >> 2) << 2; bool a1; bool a2; bool a3; bool a4; for (size_t i = 0; i < first; i += 4) { a1 = b.X < edges[i].p0.X; a2 = b.X < edges[i + 1].p0.X; a3 = b.X < edges[i + 2].p0.X; a4 = b.X < edges[i + 3].p0.X; if (a1 && a2 && a3 && a4) continue; CFixedVector2D p01 (edges[i].p0.X, edges[i].c1); CFixedVector2D p02 (edges[i + 1].p0.X, edges[i + 1].c1); CFixedVector2D p03 (edges[i + 2].p0.X, edges[i + 2].c1); CFixedVector2D p04 (edges[i + 3].p0.X, edges[i + 3].c1); //__m128i px4 = _mm_set_epi32(p01.X.GetInternalValue(), p02.X.GetInternalValue(), p03.X.GetInternalValue(), p04.X.GetInternalValue()); //__m128i ax4 = _mm_set_epi32(a.X.GetInternalValue(), a.X.GetInternalValue(), a.X.GetInternalValue(), a.X.GetInternalValue()); //__m128i py4 = _mm_set_epi32(p01.Y.GetInternalValue(), p02.Y.GetInternalValue(), p03.Y.GetInternalValue(), p04.Y.GetInternalValue()); //__m128i ay4 = _mm_set_epi32(a.Y.GetInternalValue(), a.Y.GetInternalValue(), a.Y.GetInternalValue(), a.Y.GetInternalValue()); __m128 dot0 = DotSIMD(p01 - a, abn, p02 - a, abn, p03 - a, abn, p04 - a, abn); fixed s1; s1.SetInternalValue((i32)dot0.m128_i32[0]); fixed s2; s2.SetInternalValue((i32)dot0.m128_i32[1]); fixed s3; s3.SetInternalValue((i32)dot0.m128_i32[2]); fixed s4; s4.SetInternalValue((i32)dot0.m128_i32[3]); a1 = a1 || s1 > fixed::Zero(); a2 = a2 || s2 > fixed::Zero(); a3 = a3 || s3 > fixed::Zero(); a4 = a4 || s4 > fixed::Zero(); if (a1 && a2 && a3 && a4) continue; CFixedVector2D p11 (edges[i].p0.X, edges[i].p0.Y); CFixedVector2D p12 (edges[i + 1].p0.X, edges[i + 1].p0.Y); CFixedVector2D p13 (edges[i + 2].p0.X, edges[i + 2].p0.Y); CFixedVector2D p14 (edges[i + 3].p0.X, edges[i + 3].p0.Y); __m128 dot1 = DotSIMD(p11 - a, abn, p12 - a, abn, p13 - a, abn, p14 - a, abn); fixed t1; t1.SetInternalValue((i32)dot1.m128_i32[0]); fixed t2; t2.SetInternalValue((i32)dot1.m128_i32[1]); fixed t3; t3.SetInternalValue((i32)dot1.m128_i32[2]); fixed t4; t4.SetInternalValue((i32)dot1.m128_i32[3]); a1 = a1 || t1 < fixed::Zero(); a2 = a2 || t2 < fixed::Zero(); a3 = a3 || t3 < fixed::Zero(); a4 = a4 || t4 < fixed::Zero(); if (a1 && a2 && a3 && a4) continue; return false; } */ /* int k = 0; for (size_t i = 0; i < size; ++i) { if (b.X < edges[i].p0.X) continue; checkedEdges[k] = edges[i]; ++k; } int l = 0; for (int i = 0; i < k; i += 2) { CFixedVector2D p01(checkedEdges[i].p0.X, checkedEdges[i].c1); CFixedVector2D p02(checkedEdges[i + 1].p0.X, checkedEdges[i + 1].c1); CFixedVector2D p11(checkedEdges[i].p0.X, checkedEdges[i].p0.Y); CFixedVector2D p12(checkedEdges[i + 1].p0.X, checkedEdges[i + 1].p0.Y); __m128 dot = DotSIMD((p01 - a), abn, (p02 - a), abn, (p11 - a), abn, (p12 - a), abn); fixed s0; s0.SetInternalValue(dot.m128_i32[0]); fixed s1; s1.SetInternalValue(dot.m128_i32[1]); fixed t0; t0.SetInternalValue(dot.m128_i32[2]); fixed t1; t1.SetInternalValue(dot.m128_i32[3]); if ((s0 > fixed::Zero() || t0 < fixed::Zero()) && (s1 > fixed::Zero() || t1 < fixed::Zero())) return false; }*/ //for (size_t i = 0; i < size; ++i) //{ // if (b.X < edges[i].p0.X) // continue; // CFixedVector2D p0(edges[i].p0.X, edges[i].c1); // CFixedVector2D p1(edges[i].p0.X, edges[i].p0.Y); // m128 dot = MarijnDotSIMDV4((p0 - a), abn, (p1 - a), abn); // // fixed s; // s.SetInternalValue(dot.m128_i32[0]); // fixed t; // t.SetInternalValue(dot.m128_i32[1]); // if (s > fixed::Zero()) // continue; // if (t < fixed::Zero()) // continue; // return false; //} for (size_t i = 0; i < size; ++i) { if (b.X < edges[i].p0.X) continue; CFixedVector2D p0(edges[i].p0.X, edges[i].c1); fixed s = (p0 - a).Dot(abn); if (s > fixed::Zero()) continue; CFixedVector2D p1(edges[i].p0.X, edges[i].p0.Y); fixed t = (p1 - a).Dot(abn); if (t < fixed::Zero()) continue; return false; } return true; } inline static bool CheckVisibilityRight(CFixedVector2D a, CFixedVector2D b, const std::vector& edges, size_t size) { if (a.X <= b.X) return true; CFixedVector2D abn = (b - a).Perpendicular(); //size_t first = (size >> 2) << 2; //for (size_t i = 0; i < size; i += 4) //{ // if (b.X > edges[i].p0.X && b.X > edges[i + 1].p0.X && b.X > edges[i + 2].p0.X && b.X > edges[i + 3].p0.X) // continue; // CFixedVector2D p01 (edges[i].p0.X, edges[i].c1); // CFixedVector2D p02 (edges[i + 1].p0.X, edges[i + 1].c1); // CFixedVector2D p03 (edges[i + 2].p0.X, edges[i + 2].c1); // CFixedVector2D p04 (edges[i + 3].p0.X, edges[i + 3].c1); // fixed s1 = (p01 - a).Dot(abn); // fixed s2 = (p02 - a).Dot(abn); // fixed s3 = (p03 - a).Dot(abn); // fixed s4 = (p04 - a).Dot(abn); // if (s1 > fixed::Zero() && s2 > fixed::Zero() && s3 > fixed::Zero() && s4 > fixed::Zero()) // continue; // CFixedVector2D p11 (edges[i].p0.X, edges[i].p0.Y); // CFixedVector2D p12 (edges[i + 1].p0.X, edges[i + 1].p0.Y); // CFixedVector2D p13 (edges[i + 2].p0.X, edges[i + 2].p0.Y); // CFixedVector2D p14 (edges[i + 3].p0.X, edges[i + 3].p0.Y); // fixed t1 = (p11 - a).Dot(abn); // fixed t2 = (p12 - a).Dot(abn); // fixed t3 = (p13 - a).Dot(abn); // fixed t4 = (p14 - a).Dot(abn); // if (t1 < fixed::Zero() && t2 < fixed::Zero() && t3 < fixed::Zero() && t4 < fixed::Zero()) // continue; // return false; //} for (size_t i = 0; i < size; ++i) { if (b.X > edges[i].p0.X) continue; CFixedVector2D p0 (edges[i].p0.X, edges[i].c1); fixed s = (p0 - a).Dot(abn); if (s > fixed::Zero()) continue; CFixedVector2D p1 (edges[i].p0.X, edges[i].p0.Y); fixed t = (p1 - a).Dot(abn); if (t < fixed::Zero()) continue; return false; } return true; } inline static bool CheckVisibilityBottom(CFixedVector2D a, CFixedVector2D b, const std::vector& edges, size_t size) { if (a.Y >= b.Y) return true; CFixedVector2D abn = (b - a).Perpendicular(); //size_t first = (size >> 2) << 2; //for (size_t i = 0; i < size; i += 4) //{ // if (b.Y < edges[i].p0.Y && b.Y < edges[i + 1].p0.Y && b.Y < edges[i + 2].p0.Y && b.Y < edges[i + 3].p0.Y) // continue; // CFixedVector2D p01 (edges[i].p0.X, edges[i].p0.Y); // CFixedVector2D p02 (edges[i + 1].p0.X, edges[i + 1].p0.Y); // CFixedVector2D p03 (edges[i + 2].p0.X, edges[i + 2].p0.Y); // CFixedVector2D p04 (edges[i + 3].p0.X, edges[i + 3].p0.Y); // fixed s1 = (p01 - a).Dot(abn); // fixed s2 = (p02 - a).Dot(abn); // fixed s3 = (p03 - a).Dot(abn); // fixed s4 = (p04 - a).Dot(abn); // if (s1 > fixed::Zero() && s2 > fixed::Zero() && s3 > fixed::Zero() && s4 > fixed::Zero()) // continue; // CFixedVector2D p11 (edges[i].c1, edges[i].p0.Y); // CFixedVector2D p12 (edges[i + 1].c1, edges[i + 1].p0.Y); // CFixedVector2D p13 (edges[i + 2].c1, edges[i + 2].p0.Y); // CFixedVector2D p14 (edges[i + 3].c1, edges[i + 3].p0.Y); // fixed t1 = (p11 - a).Dot(abn); // fixed t2 = (p12 - a).Dot(abn); // fixed t3 = (p13 - a).Dot(abn); // fixed t4 = (p14 - a).Dot(abn); // if (t1 < fixed::Zero() && t2 < fixed::Zero() && t3 < fixed::Zero() && t4 < fixed::Zero()) // continue; // return false; //} for (size_t i = 0; i < size; ++i) { if (b.Y < edges[i].p0.Y) continue; CFixedVector2D p0 (edges[i].p0.X, edges[i].p0.Y); fixed s = (p0 - a).Dot(abn); if (s > fixed::Zero()) continue; CFixedVector2D p1 (edges[i].c1, edges[i].p0.Y); fixed t = (p1 - a).Dot(abn); if (t < fixed::Zero()) continue; return false; } return true; } inline static bool CheckVisibilityTop(CFixedVector2D a, CFixedVector2D b, const std::vector& edges, size_t size) { if (a.Y <= b.Y) return true; CFixedVector2D abn = (b - a).Perpendicular(); //size_t first = (size >> 2) << 2; //for (size_t i = 0; i < size; i += 4) //{ // if (b.Y > edges[i].p0.Y && b.Y > edges[i + 1].p0.Y && b.Y > edges[i + 2].p0.Y && b.Y > edges[i + 3].p0.Y) // continue; // CFixedVector2D p01 (edges[i].p0.X, edges[i].p0.Y); // CFixedVector2D p02 (edges[i + 1].p0.X, edges[i + 1].p0.Y); // CFixedVector2D p03 (edges[i + 2].p0.X, edges[i + 2].p0.Y); // CFixedVector2D p04 (edges[i + 3].p0.X, edges[i + 3].p0.Y); // fixed s1 = (p01 - a).Dot(abn); // fixed s2 = (p02 - a).Dot(abn); // fixed s3 = (p03 - a).Dot(abn); // fixed s4 = (p04 - a).Dot(abn); // if (s1 > fixed::Zero() && s2 > fixed::Zero() && s3 > fixed::Zero() && s4 > fixed::Zero()) // continue; // CFixedVector2D p11 (edges[i].c1, edges[i].p0.Y); // CFixedVector2D p12 (edges[i + 1].c1, edges[i + 1].p0.Y); // CFixedVector2D p13 (edges[i + 2].c1, edges[i + 2].p0.Y); // CFixedVector2D p14 (edges[i + 3].c1, edges[i + 3].p0.Y); // fixed t1 = (p11 - a).Dot(abn); // fixed t2 = (p12 - a).Dot(abn); // fixed t3 = (p13 - a).Dot(abn); // fixed t4 = (p14 - a).Dot(abn); // if (t1 < fixed::Zero() && t2 < fixed::Zero() && t3 < fixed::Zero() && t4 < fixed::Zero()) // continue; // return false; //} for (size_t i = 0; i < size; ++i) { if (b.Y > edges[i].p0.Y) continue; CFixedVector2D p0 (edges[i].p0.X, edges[i].p0.Y); fixed s = (p0 - a).Dot(abn); if (s > fixed::Zero()) continue; CFixedVector2D p1 (edges[i].c1, edges[i].p0.Y); fixed t = (p1 - a).Dot(abn); if (t < fixed::Zero()) continue; return false; } return true; } typedef PriorityQueueHeap VertexPriorityQueue; /** * Add edges and vertexes to represent the boundaries between passable and impassable * navcells (for impassable terrain). * Navcells i0 <= i <= i1, j0 <= j <= j1 will be considered. */ static void AddTerrainEdges(std::vector& edges, std::vector& vertexes, int i0, int j0, int i1, int j1, pass_class_t passClass, const Grid& grid) { PROFILE("AddTerrainEdges"); // Clamp the coordinates so we won't attempt to sample outside of the grid. // (This assumes the outermost ring of navcells (which are always impassable) // won't have a boundary with any passable navcells. TODO: is that definitely // safe enough?) i0 = clamp(i0, 1, grid.m_W-2); j0 = clamp(j0, 1, grid.m_H-2); i1 = clamp(i1, 1, grid.m_W-2); j1 = clamp(j1, 1, grid.m_H-2); for (int j = j0; j <= j1; ++j) { for (int i = i0; i <= i1; ++i) { if (IS_PASSABLE(grid.get(i, j), passClass)) continue; if (IS_PASSABLE(grid.get(i+1, j), passClass) && IS_PASSABLE(grid.get(i, j+1), passClass) && IS_PASSABLE(grid.get(i+1, j+1), passClass)) { Vertex vert; vert.status = Vertex::UNEXPLORED; vert.quadOutward = QUADRANT_ALL; vert.quadInward = QUADRANT_BL; vert.p = CFixedVector2D(fixed::FromInt(i+1)+EDGE_EXPAND_DELTA, fixed::FromInt(j+1)+EDGE_EXPAND_DELTA).Multiply(Pathfinding::NAVCELL_SIZE); vertexes.push_back(vert); } if (IS_PASSABLE(grid.get(i-1, j), passClass) && IS_PASSABLE(grid.get(i, j+1), passClass) && IS_PASSABLE(grid.get(i-1, j+1), passClass)) { Vertex vert; vert.status = Vertex::UNEXPLORED; vert.quadOutward = QUADRANT_ALL; vert.quadInward = QUADRANT_BR; vert.p = CFixedVector2D(fixed::FromInt(i)-EDGE_EXPAND_DELTA, fixed::FromInt(j+1)+EDGE_EXPAND_DELTA).Multiply(Pathfinding::NAVCELL_SIZE); vertexes.push_back(vert); } if (IS_PASSABLE(grid.get(i+1, j), passClass) && IS_PASSABLE(grid.get(i, j-1), passClass) && IS_PASSABLE(grid.get(i+1, j-1), passClass)) { Vertex vert; vert.status = Vertex::UNEXPLORED; vert.quadOutward = QUADRANT_ALL; vert.quadInward = QUADRANT_TL; vert.p = CFixedVector2D(fixed::FromInt(i+1)+EDGE_EXPAND_DELTA, fixed::FromInt(j)-EDGE_EXPAND_DELTA).Multiply(Pathfinding::NAVCELL_SIZE); vertexes.push_back(vert); } if (IS_PASSABLE(grid.get(i-1, j), passClass) && IS_PASSABLE(grid.get(i, j-1), passClass) && IS_PASSABLE(grid.get(i-1, j-1), passClass)) { Vertex vert; vert.status = Vertex::UNEXPLORED; vert.quadOutward = QUADRANT_ALL; vert.quadInward = QUADRANT_TR; vert.p = CFixedVector2D(fixed::FromInt(i)-EDGE_EXPAND_DELTA, fixed::FromInt(j)-EDGE_EXPAND_DELTA).Multiply(Pathfinding::NAVCELL_SIZE); vertexes.push_back(vert); } } } // XXX rewrite this stuff for (int j = j0; j < j1; ++j) { std::vector segmentsR; std::vector segmentsL; for (int i = i0; i <= i1; ++i) { bool a = IS_PASSABLE(grid.get(i, j+1), passClass); bool b = IS_PASSABLE(grid.get(i, j), passClass); if (a && !b) segmentsL.push_back(i); if (b && !a) segmentsR.push_back(i); } if (!segmentsR.empty()) { segmentsR.push_back(0); // sentinel value to simplify the loop u16 ia = segmentsR[0]; u16 ib = ia + 1; int segmentSize = segmentsR.size(); for (size_t n = 1; n < segmentSize; ++n) { if (segmentsR[n] == ib) ++ib; else { CFixedVector2D v0 = CFixedVector2D(fixed::FromInt(ia), fixed::FromInt(j+1)).Multiply(Pathfinding::NAVCELL_SIZE); CFixedVector2D v1 = CFixedVector2D(fixed::FromInt(ib), fixed::FromInt(j+1)).Multiply(Pathfinding::NAVCELL_SIZE); edges.emplace_back(Edge{ v0, v1 }); ia = segmentsR[n]; ib = ia + 1; } } } if (!segmentsL.empty()) { segmentsL.push_back(0); // sentinel value to simplify the loop u16 ia = segmentsL[0]; u16 ib = ia + 1; int segmentSize = segmentsL.size(); for (size_t n = 1; n < segmentSize; ++n) { if (segmentsL[n] == ib) ++ib; else { CFixedVector2D v0 = CFixedVector2D(fixed::FromInt(ib), fixed::FromInt(j+1)).Multiply(Pathfinding::NAVCELL_SIZE); CFixedVector2D v1 = CFixedVector2D(fixed::FromInt(ia), fixed::FromInt(j+1)).Multiply(Pathfinding::NAVCELL_SIZE); edges.emplace_back(Edge{ v0, v1 }); ia = segmentsL[n]; ib = ia + 1; } } } } for (int i = i0; i < i1; ++i) { std::vector segmentsU; std::vector segmentsD; for (int j = j0; j <= j1; ++j) { bool a = IS_PASSABLE(grid.get(i+1, j), passClass); bool b = IS_PASSABLE(grid.get(i, j), passClass); if (a && !b) segmentsU.push_back(j); if (b && !a) segmentsD.push_back(j); } if (!segmentsU.empty()) { segmentsU.push_back(0); // sentinel value to simplify the loop u16 ja = segmentsU[0]; u16 jb = ja + 1; int segmentSize = segmentsU.size(); for (size_t n = 1; n < segmentSize; ++n) { if (segmentsU[n] == jb) ++jb; else { CFixedVector2D v0 = CFixedVector2D(fixed::FromInt(i+1), fixed::FromInt(ja)).Multiply(Pathfinding::NAVCELL_SIZE); CFixedVector2D v1 = CFixedVector2D(fixed::FromInt(i+1), fixed::FromInt(jb)).Multiply(Pathfinding::NAVCELL_SIZE); edges.emplace_back(Edge{ v0, v1 }); ja = segmentsU[n]; jb = ja + 1; } } } if (!segmentsD.empty()) { segmentsD.push_back(0); // sentinel value to simplify the loop u16 ja = segmentsD[0]; u16 jb = ja + 1; int segmentSize = segmentsD.size(); for (size_t n = 1; n < segmentSize; ++n) { if (segmentsD[n] == jb) ++jb; else { CFixedVector2D v0 = CFixedVector2D(fixed::FromInt(i+1), fixed::FromInt(jb)).Multiply(Pathfinding::NAVCELL_SIZE); CFixedVector2D v1 = CFixedVector2D(fixed::FromInt(i+1), fixed::FromInt(ja)).Multiply(Pathfinding::NAVCELL_SIZE); edges.emplace_back(Edge{ v0, v1 }); ja = segmentsD[n]; jb = ja + 1; } } } } } static void SplitAAEdges(CFixedVector2D a, const std::vector& edges, const std::vector& squares, std::vector& edgesUnaligned, std::vector& edgesLeft, std::vector& edgesRight, std::vector& edgesBottom, std::vector& edgesTop) { edgesLeft.reserve(squares.size()); edgesRight.reserve(squares.size()); edgesBottom.reserve(squares.size()); edgesTop.reserve(squares.size()); for (const Square& square : squares) { if (a.X <= square.p0.X) edgesLeft.emplace_back(EdgeAA{ square.p0, square.p1.Y }); if (a.X >= square.p1.X) edgesRight.emplace_back(EdgeAA{ square.p1, square.p0.Y }); if (a.Y <= square.p0.Y) edgesBottom.emplace_back(EdgeAA{ square.p0, square.p1.X }); if (a.Y >= square.p1.Y) edgesTop.emplace_back(EdgeAA{ square.p1, square.p0.X }); } for (const Edge& edge : edges) { if (edge.p0.X == edge.p1.X) { if (edge.p1.Y < edge.p0.Y) { if (!(a.X <= edge.p0.X)) continue; edgesLeft.emplace_back(EdgeAA{ edge.p1, edge.p0.Y }); } else { if (!(a.X >= edge.p0.X)) continue; edgesRight.emplace_back(EdgeAA{ edge.p1, edge.p0.Y }); } } else if (edge.p0.Y == edge.p1.Y) { if (edge.p0.X < edge.p1.X) { if (!(a.Y <= edge.p0.Y)) continue; edgesBottom.emplace_back(EdgeAA{ edge.p0, edge.p1.X }); } else { if (!(a.Y >= edge.p0.Y)) continue; edgesTop.emplace_back(EdgeAA{ edge.p0, edge.p1.X }); } } else edgesUnaligned.push_back(edge); } } /** * Functor for sorting edge-squares by approximate proximity to a fixed point. */ struct SquareSort { CFixedVector2D src; SquareSort(CFixedVector2D src) : src(src) { } bool operator()(const Square& a, const Square& b) { if ((a.p0 - src).CompareLength(b.p0 - src) < 0) return true; return false; } }; void CCmpPathfinder::ComputeShortPath(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t clearance, entity_pos_t range, const PathGoal& goal, pass_class_t passClass, WaypointPath& path) { PROFILE3("ComputeShortPath"); m_DebugOverlayShortPathLines.clear(); if (m_DebugOverlay) { // Render the goal shape m_DebugOverlayShortPathLines.push_back(SOverlayLine()); m_DebugOverlayShortPathLines.back().m_Color = CColor(1, 0, 0, 1); switch (goal.type) { case PathGoal::POINT: { SimRender::ConstructCircleOnGround(GetSimContext(), goal.x.ToFloat(), goal.z.ToFloat(), 0.2f, m_DebugOverlayShortPathLines.back(), true); break; } case PathGoal::CIRCLE: case PathGoal::INVERTED_CIRCLE: { SimRender::ConstructCircleOnGround(GetSimContext(), goal.x.ToFloat(), goal.z.ToFloat(), goal.hw.ToFloat(), m_DebugOverlayShortPathLines.back(), true); break; } case PathGoal::SQUARE: case PathGoal::INVERTED_SQUARE: { float a = atan2f(goal.v.X.ToFloat(), goal.v.Y.ToFloat()); SimRender::ConstructSquareOnGround(GetSimContext(), goal.x.ToFloat(), goal.z.ToFloat(), goal.hw.ToFloat()*2, goal.hh.ToFloat()*2, a, m_DebugOverlayShortPathLines.back(), true); break; } } } // List of collision edges - paths must never cross these. // (Edges are one-sided so intersections are fine in one direction, but not the other direction.) std::vector edges; std::vector edgeSquares; // axis-aligned squares; equivalent to 4 edges // Create impassable edges at the max-range boundary, so we can't escape the region // where we're meant to be searching fixed rangeXMin = x0 - range; fixed rangeXMax = x0 + range; fixed rangeZMin = z0 - range; fixed rangeZMax = z0 + range; // (The edges are the opposite direction to usual, so it's an inside-out square) edges.emplace_back(Edge{ CFixedVector2D(rangeXMin, rangeZMin), CFixedVector2D(rangeXMin, rangeZMax) }); edges.emplace_back(Edge{ CFixedVector2D(rangeXMin, rangeZMax), CFixedVector2D(rangeXMax, rangeZMax) }); edges.emplace_back(Edge{ CFixedVector2D(rangeXMax, rangeZMax), CFixedVector2D(rangeXMax, rangeZMin) }); edges.emplace_back(Edge{ CFixedVector2D(rangeXMax, rangeZMin), CFixedVector2D(rangeXMin, rangeZMin) }); // List of obstruction vertexes (plus start/end points); we'll try to find paths through // the graph defined by these vertexes std::vector vertexes; // Add the start point to the graph CFixedVector2D posStart(x0, z0); fixed hStart = (posStart - goal.NearestPointOnGoal(posStart)).Length(); Vertex start = { posStart, fixed::Zero(), hStart, 0, Vertex::OPEN, QUADRANT_NONE, QUADRANT_ALL }; vertexes.push_back(start); const size_t START_VERTEX_ID = 0; // Add the goal vertex to the graph. // Since the goal isn't always a point, this a special magic virtual vertex which moves around - whenever // we look at it from another vertex, it is moved to be the closest point on the goal shape to that vertex. Vertex end = { CFixedVector2D(goal.x, goal.z), fixed::Zero(), fixed::Zero(), 0, Vertex::UNEXPLORED, QUADRANT_NONE, QUADRANT_ALL }; vertexes.push_back(end); const size_t GOAL_VERTEX_ID = 1; // Add terrain obstructions { u16 i0, j0, i1, j1; Pathfinding::NearestNavcell(rangeXMin, rangeZMin, i0, j0, m_MapSize*Pathfinding::NAVCELLS_PER_TILE, m_MapSize*Pathfinding::NAVCELLS_PER_TILE); Pathfinding::NearestNavcell(rangeXMax, rangeZMax, i1, j1, m_MapSize*Pathfinding::NAVCELLS_PER_TILE, m_MapSize*Pathfinding::NAVCELLS_PER_TILE); AddTerrainEdges(edges, vertexes, i0, j0, i1, j1, passClass, *m_TerrainOnlyGrid); } // Find all the obstruction squares that might affect us CmpPtr cmpObstructionManager(GetSystemEntity()); std::vector squares; cmpObstructionManager->GetObstructionsInRange(filter, rangeXMin - clearance, rangeZMin - clearance, rangeXMax + clearance, rangeZMax + clearance, squares); // Precalculate squares, vertexes and edge squares size int squaresSize = squares.size(); int vertexesSize = vertexes.size(); // Change array capacities to reduce reallocations vertexes.reserve(vertexesSize + squaresSize << 2); edgeSquares.reserve(edgeSquares.size() + squaresSize); // (assume most squares are AA) // Convert each obstruction square into collision edges and search graph vertexes for (size_t i = 0; i < squaresSize; ++i) { CFixedVector2D center(squares[i].x, squares[i].z); CFixedVector2D u = squares[i].u; CFixedVector2D v = squares[i].v; // Expand the vertexes by the moving unit's collision radius, to find the // closest we can get to it CFixedVector2D hd0(squares[i].hw + clearance + EDGE_EXPAND_DELTA, squares[i].hh + clearance + EDGE_EXPAND_DELTA); CFixedVector2D hd1(squares[i].hw + clearance + EDGE_EXPAND_DELTA, -(squares[i].hh + clearance + EDGE_EXPAND_DELTA)); // Check whether this is an axis-aligned square bool aa = (u.X == fixed::FromInt(1) && u.Y == fixed::Zero() && v.X == fixed::Zero() && v.Y == fixed::FromInt(1)); Vertex vert; vert.status = Vertex::UNEXPLORED; vert.quadInward = QUADRANT_NONE; vert.quadOutward = QUADRANT_ALL; vert.p.X = center.X - hd0.Dot(u); vert.p.Y = center.Y + hd0.Dot(v); if (aa) vert.quadInward = QUADRANT_BR; vertexes.push_back(vert); vert.p.X = center.X - hd1.Dot(u); vert.p.Y = center.Y + hd1.Dot(v); if (aa) vert.quadInward = QUADRANT_TR; vertexes.push_back(vert); vert.p.X = center.X + hd0.Dot(u); vert.p.Y = center.Y - hd0.Dot(v); if (aa) vert.quadInward = QUADRANT_TL; vertexes.push_back(vert); vert.p.X = center.X + hd1.Dot(u); vert.p.Y = center.Y - hd1.Dot(v); if (aa) vert.quadInward = QUADRANT_BL; vertexes.push_back(vert); // Add the edges: CFixedVector2D h0(squares[i].hw + clearance, squares[i].hh + clearance); CFixedVector2D h1(squares[i].hw + clearance, -(squares[i].hh + clearance)); CFixedVector2D ev0(center.X - h0.Dot(u), center.Y + h0.Dot(v)); CFixedVector2D ev1(center.X - h1.Dot(u), center.Y + h1.Dot(v)); CFixedVector2D ev2(center.X + h0.Dot(u), center.Y - h0.Dot(v)); CFixedVector2D ev3(center.X + h1.Dot(u), center.Y - h1.Dot(v)); if (aa) edgeSquares.emplace_back(Square{ ev1, ev3 }); else { edges.emplace_back(Edge{ ev0, ev1 }); edges.emplace_back(Edge{ ev1, ev2 }); edges.emplace_back(Edge{ ev2, ev3 }); edges.emplace_back(Edge{ ev3, ev0 }); } // TODO: should clip out vertexes and edges that are outside the range, // to reduce the search space } ENSURE(vertexesSize < 65536); // we store array indexes as u16 // Render the debug overlay if (m_DebugOverlay) { #define PUSH_POINT(p) STMT(xz.push_back(p.X.ToFloat()); xz.push_back(p.Y.ToFloat())) // Render the vertexes as little Pac-Man shapes to indicate quadrant direction for (size_t i = 0; i < vertexes.size(); ++i) { m_DebugOverlayShortPathLines.emplace_back(); m_DebugOverlayShortPathLines.back().m_Color = CColor(1, 1, 0, 1); float x = vertexes[i].p.X.ToFloat(); float z = vertexes[i].p.Y.ToFloat(); float a0 = 0, a1 = 0; // Get arc start/end angles depending on quadrant (if any) if (vertexes[i].quadInward == QUADRANT_BL) { a0 = -0.25f; a1 = 0.50f; } else if (vertexes[i].quadInward == QUADRANT_TR) { a0 = 0.25f; a1 = 1.00f; } else if (vertexes[i].quadInward == QUADRANT_TL) { a0 = -0.50f; a1 = 0.25f; } else if (vertexes[i].quadInward == QUADRANT_BR) { a0 = 0.00f; a1 = 0.75f; } if (a0 == a1) SimRender::ConstructCircleOnGround(GetSimContext(), x, z, 0.5f, m_DebugOverlayShortPathLines.back(), true); else SimRender::ConstructClosedArcOnGround(GetSimContext(), x, z, 0.5f, a0 * ((float)M_PI*2.0f), a1 * ((float)M_PI*2.0f), m_DebugOverlayShortPathLines.back(), true); } // Render the edges for (size_t i = 0; i < edges.size(); ++i) { m_DebugOverlayShortPathLines.emplace_back(); m_DebugOverlayShortPathLines.back().m_Color = CColor(0, 1, 1, 1); std::vector xz; PUSH_POINT(edges[i].p0); PUSH_POINT(edges[i].p1); // Add an arrowhead to indicate the direction CFixedVector2D d = edges[i].p1 - edges[i].p0; d.Normalize(fixed::FromInt(1)/8); CFixedVector2D p2 = edges[i].p1 - d*2; CFixedVector2D p3 = p2 + d.Perpendicular(); CFixedVector2D p4 = p2 - d.Perpendicular(); PUSH_POINT(p3); PUSH_POINT(p4); PUSH_POINT(edges[i].p1); SimRender::ConstructLineOnGround(GetSimContext(), xz, m_DebugOverlayShortPathLines.back(), true); } #undef PUSH_POINT // Render the axis-aligned squares for (size_t i = 0; i < edgeSquares.size(); ++i) { m_DebugOverlayShortPathLines.push_back(SOverlayLine()); m_DebugOverlayShortPathLines.back().m_Color = CColor(0, 1, 1, 1); std::vector xz; Square s = edgeSquares[i]; xz.push_back(s.p0.X.ToFloat()); xz.push_back(s.p0.Y.ToFloat()); xz.push_back(s.p0.X.ToFloat()); xz.push_back(s.p1.Y.ToFloat()); xz.push_back(s.p1.X.ToFloat()); xz.push_back(s.p1.Y.ToFloat()); xz.push_back(s.p1.X.ToFloat()); xz.push_back(s.p0.Y.ToFloat()); xz.push_back(s.p0.X.ToFloat()); xz.push_back(s.p0.Y.ToFloat()); SimRender::ConstructLineOnGround(GetSimContext(), xz, m_DebugOverlayShortPathLines.back(), true); } } // Do an A* search over the vertex/visibility graph: // Since we are just measuring Euclidean distance the heuristic is admissible, // so we never have to re-examine a node once it's been moved to the closed set. // To save time in common cases, we don't precompute a graph of valid edges between vertexes; // we do it lazily instead. When the search algorithm reaches a vertex, we examine every other // vertex and see if we can reach it without hitting any collision edges, and ignore the ones // we can't reach. Since the algorithm can only reach a vertex once (and then it'll be marked // as closed), we won't be doing any redundant visibility computations. PROFILE_START("Short pathfinding - A*"); VertexPriorityQueue open; VertexPriorityQueue::Item qiStart = { START_VERTEX_ID, start.h, start.h }; open.push(qiStart); u16 idBest = START_VERTEX_ID; fixed hBest = start.h; while (!open.empty()) { // Move best tile from open to closed VertexPriorityQueue::Item curr = open.pop(); vertexes[curr.id].status = Vertex::CLOSED; // If we've reached the destination, stop if (curr.id == GOAL_VERTEX_ID) { idBest = curr.id; break; } // Sort the edges so ones nearer this vertex are checked first by CheckVisibility, // since they're more likely to block the rays //std::sort(edgeSquares.begin(), edgeSquares.end(), SquareSort(vertexes[curr.id].p)); std::vector edgesUnaligned; std::vector edgesLeft; std::vector edgesRight; std::vector edgesBottom; std::vector edgesTop; SplitAAEdges(vertexes[curr.id].p, edges, edgeSquares, edgesUnaligned, edgesLeft, edgesRight, edgesBottom, edgesTop); // Precalculate all sizes so its not done for loop iteration size_t unalignedSize = edgesUnaligned.size(); size_t leftSize = edgesLeft.size(); size_t rightSize = edgesRight.size(); size_t bottomSize = edgesBottom.size(); size_t topSize = edgesTop.size(); // Precalc vertexes size vertexesSize = vertexes.size(); // Check the lines to every other vertex for (size_t n = 0; n < vertexesSize; ++n) { if (vertexes[n].status == Vertex::CLOSED) continue; // If this is the magical goal vertex, move it to near the current vertex CFixedVector2D npos; if (n == GOAL_VERTEX_ID) { npos = goal.NearestPointOnGoal(vertexes[curr.id].p); // To prevent integer overflows later on, we need to ensure all vertexes are // 'close' to the source. The goal might be far away (not a good idea but // sometimes it happens), so clamp it to the current search range npos.X = clamp(npos.X, rangeXMin, rangeXMax); npos.Y = clamp(npos.Y, rangeZMin, rangeZMax); } else { npos = vertexes[n].p; } // Work out which quadrant(s) we're approaching the new vertex from u8 quad = 0; if (vertexes[curr.id].p.X <= npos.X && vertexes[curr.id].p.Y <= npos.Y) quad |= QUADRANT_BL; if (vertexes[curr.id].p.X >= npos.X && vertexes[curr.id].p.Y >= npos.Y) quad |= QUADRANT_TR; if (vertexes[curr.id].p.X <= npos.X && vertexes[curr.id].p.Y >= npos.Y) quad |= QUADRANT_TL; if (vertexes[curr.id].p.X >= npos.X && vertexes[curr.id].p.Y <= npos.Y) quad |= QUADRANT_BR; // Check that the new vertex is in the right quadrant for the old vertex if (!(vertexes[curr.id].quadOutward & quad)) { // Hack: Always head towards the goal if possible, to avoid missing it if it's // inside another unit if (n != GOAL_VERTEX_ID) { continue; } } bool visible = CheckVisibilityLeft(vertexes[curr.id].p, npos, edgesLeft, leftSize) && CheckVisibilityRight(vertexes[curr.id].p, npos, edgesRight, rightSize) && CheckVisibilityBottom(vertexes[curr.id].p, npos, edgesBottom, bottomSize) && CheckVisibilityTop(vertexes[curr.id].p, npos, edgesTop, topSize) && CheckVisibility(vertexes[curr.id].p, npos, edgesUnaligned, unalignedSize); /* // Render the edges that we examine m_DebugOverlayShortPathLines.push_back(SOverlayLine()); m_DebugOverlayShortPathLines.back().m_Color = visible ? CColor(0, 1, 0, 0.5) : CColor(1, 0, 0, 0.5); std::vector xz; xz.push_back(vertexes[curr.id].p.X.ToFloat()); xz.push_back(vertexes[curr.id].p.Y.ToFloat()); xz.push_back(npos.X.ToFloat()); xz.push_back(npos.Y.ToFloat()); SimRender::ConstructLineOnGround(GetSimContext(), xz, m_DebugOverlayShortPathLines.back(), false); //*/ if (visible) { fixed g = vertexes[curr.id].g + (vertexes[curr.id].p - npos).Length(); // If this is a new tile, compute the heuristic distance if (vertexes[n].status == Vertex::UNEXPLORED) { // Add it to the open list: vertexes[n].status = Vertex::OPEN; vertexes[n].g = g; vertexes[n].h = goal.DistanceToPoint(npos); vertexes[n].pred = curr.id; // If this is an axis-aligned shape, the path must continue in the same quadrant // direction (but not go into the inside of the shape). // Hack: If we started *inside* a shape then perhaps headed to its corner (e.g. the unit // was very near another unit), don't restrict further pathing. if (vertexes[n].quadInward && !(curr.id == START_VERTEX_ID && g < fixed::FromInt(8))) vertexes[n].quadOutward = ((~vertexes[n].quadInward) & quad) & 0xF; if (n == GOAL_VERTEX_ID) vertexes[n].p = npos; // remember the new best goal position VertexPriorityQueue::Item t = { (u16)n, g + vertexes[n].h, vertexes[n].h }; open.push(t); // Remember the heuristically best vertex we've seen so far, in case we never actually reach the target if (vertexes[n].h < hBest) { idBest = (u16)n; hBest = vertexes[n].h; } } else // must be OPEN { // If we've already seen this tile, and the new path to this tile does not have a // better cost, then stop now if (g >= vertexes[n].g) continue; // Otherwise, we have a better path, so replace the old one with the new cost/parent fixed gprev = vertexes[n].g; vertexes[n].g = g; vertexes[n].pred = curr.id; // If this is an axis-aligned shape, the path must continue in the same quadrant // direction (but not go into the inside of the shape). if (vertexes[n].quadInward) vertexes[n].quadOutward = ((~vertexes[n].quadInward) & quad) & 0xF; if (n == GOAL_VERTEX_ID) vertexes[n].p = npos; // remember the new best goal position open.promote((u16)n, gprev + vertexes[n].h, g + vertexes[n].h, vertexes[n].h); } } } } // Reconstruct the path (in reverse) for (u16 id = idBest; id != START_VERTEX_ID; id = vertexes[id].pred) path.m_Waypoints.emplace_back(Waypoint{ vertexes[id].p.X, vertexes[id].p.Y }); PROFILE_END("Short pathfinding - A*"); }