1
0

PathfindingSystem.Grid.cs 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830
  1. using System.Diagnostics.CodeAnalysis;
  2. using System.Numerics;
  3. using System.Threading;
  4. using System.Threading.Tasks;
  5. using Content.Server.Destructible;
  6. using Content.Shared.Access.Components;
  7. using Content.Shared.Climbing.Components;
  8. using Content.Shared.Doors.Components;
  9. using Content.Shared.NPC;
  10. using Content.Shared.Physics;
  11. using Robust.Shared.Collections;
  12. using Robust.Shared.Map;
  13. using Robust.Shared.Map.Components;
  14. using Robust.Shared.Physics;
  15. using Robust.Shared.Physics.Events;
  16. using Robust.Shared.Timing;
  17. using Robust.Shared.Utility;
  18. namespace Content.Server.NPC.Pathfinding;
  19. public sealed partial class PathfindingSystem
  20. {
  21. private static readonly TimeSpan UpdateCooldown = TimeSpan.FromSeconds(0.45);
  22. // What relevant collision groups we track for pathfinding.
  23. // Stuff like chairs have collision but aren't relevant for mobs.
  24. public const int PathfindingCollisionMask = (int) CollisionGroup.MobMask;
  25. public const int PathfindingCollisionLayer = (int) CollisionGroup.MobLayer;
  26. /// <summary>
  27. /// If true, UpdateGrid() will not process grids.
  28. /// </summary>
  29. /// <remarks>
  30. /// Useful if something like a large explosion is in the process of shredding the grid, as it avoids unneccesary
  31. /// updating.
  32. /// </remarks>
  33. public bool PauseUpdating = false;
  34. private readonly Stopwatch _stopwatch = new();
  35. // Probably can't pool polys as there might be old pathfinding refs to them.
  36. private void InitializeGrid()
  37. {
  38. SubscribeLocalEvent<GridInitializeEvent>(OnGridInit);
  39. SubscribeLocalEvent<GridRemovalEvent>(OnGridRemoved);
  40. SubscribeLocalEvent<GridPathfindingComponent, ComponentShutdown>(OnGridPathShutdown);
  41. SubscribeLocalEvent<CollisionChangeEvent>(OnCollisionChange);
  42. SubscribeLocalEvent<CollisionLayerChangeEvent>(OnCollisionLayerChange);
  43. SubscribeLocalEvent<PhysicsBodyTypeChangedEvent>(OnBodyTypeChange);
  44. SubscribeLocalEvent<TileChangedEvent>(OnTileChange);
  45. _transform.OnGlobalMoveEvent += OnMoveEvent;
  46. }
  47. private void OnTileChange(ref TileChangedEvent ev)
  48. {
  49. if (ev.OldTile.IsEmpty == ev.NewTile.Tile.IsEmpty)
  50. return;
  51. DirtyChunk(ev.Entity, Comp<MapGridComponent>(ev.Entity).GridTileToLocal(ev.NewTile.GridIndices));
  52. }
  53. private void OnGridPathShutdown(EntityUid uid, GridPathfindingComponent component, ComponentShutdown args)
  54. {
  55. foreach (var chunk in component.Chunks)
  56. {
  57. // Invalidate all polygons in case there's portals or the likes.
  58. foreach (var poly in chunk.Value.Polygons)
  59. {
  60. ClearTilePolys(poly);
  61. }
  62. }
  63. component.DirtyChunks.Clear();
  64. component.Chunks.Clear();
  65. }
  66. private void UpdateGrid(ParallelOptions options)
  67. {
  68. if (PauseUpdating)
  69. return;
  70. var curTime = _timing.CurTime;
  71. #if DEBUG
  72. var updateCount = 0;
  73. #endif
  74. _stopwatch.Restart();
  75. // We defer chunk updates because rebuilding a navmesh is hella costly
  76. // Still run even when paused.
  77. var query = AllEntityQuery<GridPathfindingComponent>();
  78. while (query.MoveNext(out var uid, out var comp))
  79. {
  80. var pathfinding = new Entity<GridPathfindingComponent>(uid, comp);
  81. // TODO: Dump all this shit and just do it live it's probably fast enough.
  82. if (comp.DirtyChunks.Count == 0 ||
  83. curTime < comp.NextUpdate ||
  84. !_gridQuery.TryGetComponent(uid, out var mapGridComp))
  85. {
  86. continue;
  87. }
  88. var dirtyPortals = comp.DirtyPortals;
  89. dirtyPortals.Clear();
  90. // TODO: Often we invalidate the entire chunk when it might be something as simple as an airlock change
  91. // Would be better to handle that though this was safer and max it's taking is like 1-2ms every half-second.
  92. var dirt = new GridPathfindingChunk[comp.DirtyChunks.Count];
  93. var idx = 0;
  94. foreach (var origin in comp.DirtyChunks)
  95. {
  96. var chunk = GetChunk(origin, uid, pathfinding);
  97. dirt[idx] = chunk;
  98. idx++;
  99. }
  100. // We force clear portals in a single-threaded context to be safe
  101. // as they may not be thread-safe to touch.
  102. foreach (var chunk in dirt)
  103. {
  104. foreach (var (_, poly) in chunk.PortalPolys)
  105. {
  106. ClearPoly(poly);
  107. }
  108. chunk.PortalPolys.Clear();
  109. foreach (var portal in chunk.Portals)
  110. {
  111. dirtyPortals.Add(portal);
  112. }
  113. }
  114. // TODO: Inflate grid bounds slightly and get chunks.
  115. // This is for map <> grid pathfinding
  116. // Without parallel this is roughly 3x slower on my desktop.
  117. Parallel.For(0, dirt.Length, options, i =>
  118. {
  119. BuildBreadcrumbs(dirt[i], (uid, mapGridComp));
  120. });
  121. const int Division = 4;
  122. // You can safely do this in parallel as long as no neighbor chunks are being touched in the same iteration.
  123. // You essentially do bottom left, bottom right, top left, top right in quadrants.
  124. // For each 4x4 block of chunks.
  125. // i.e. first iteration: 0,0; 2,0; 0,2
  126. // second iteration: 1,0; 3,0; 1;2
  127. // third iteration: 0,1; 2,1; 0,3 etc
  128. for (var it = 0; it < Division; it++)
  129. {
  130. var it1 = it;
  131. Parallel.For(0, dirt.Length, options, j =>
  132. {
  133. var chunk = dirt[j];
  134. // Check if the chunk is safe on this iteration.
  135. var x = Math.Abs(chunk.Origin.X % 2);
  136. var y = Math.Abs(chunk.Origin.Y % 2);
  137. var index = x * 2 + y;
  138. if (index != it1)
  139. return;
  140. ClearOldPolys(chunk);
  141. });
  142. }
  143. // TODO: You can probably skimp on some neighbor chunk caches
  144. for (var it = 0; it < Division; it++)
  145. {
  146. var it1 = it;
  147. Parallel.For(0, dirt.Length, options, j =>
  148. {
  149. var chunk = dirt[j];
  150. // Check if the chunk is safe on this iteration.
  151. var x = Math.Abs(chunk.Origin.X % 2);
  152. var y = Math.Abs(chunk.Origin.Y % 2);
  153. var index = x * 2 + y;
  154. if (index != it1)
  155. return;
  156. BuildNavmesh(chunk, pathfinding);
  157. #if DEBUG
  158. Interlocked.Increment(ref updateCount);
  159. #endif
  160. });
  161. }
  162. // Handle portals at the end after having cleared their neighbors above.
  163. // We do this because there's no guarantee of where these are for chunks.
  164. foreach (var portal in dirtyPortals)
  165. {
  166. var polyA = GetPoly(portal.CoordinatesA);
  167. var polyB = GetPoly(portal.CoordinatesB);
  168. if (polyA == null || polyB == null)
  169. continue;
  170. DebugTools.Assert((polyA.Data.Flags & PathfindingBreadcrumbFlag.Invalid) == 0x0);
  171. DebugTools.Assert((polyB.Data.Flags & PathfindingBreadcrumbFlag.Invalid) == 0x0);
  172. var chunkA = GetChunk(polyA.ChunkOrigin, polyA.GraphUid);
  173. var chunkB = GetChunk(polyB.ChunkOrigin, polyB.GraphUid);
  174. chunkA.PortalPolys.TryAdd(portal, polyA);
  175. chunkB.PortalPolys.TryAdd(portal, polyB);
  176. AddNeighbors(polyA, polyB);
  177. }
  178. comp.DirtyChunks.Clear();
  179. }
  180. }
  181. private bool IsBodyRelevant(FixturesComponent fixtures)
  182. {
  183. foreach (var fixture in fixtures.Fixtures.Values)
  184. {
  185. if (!fixture.Hard)
  186. continue;
  187. if ((fixture.CollisionMask & PathfindingCollisionLayer) != 0x0 ||
  188. (fixture.CollisionLayer & PathfindingCollisionMask) != 0x0)
  189. {
  190. return true;
  191. }
  192. }
  193. return false;
  194. }
  195. private void OnCollisionChange(ref CollisionChangeEvent ev)
  196. {
  197. var xform = Transform(ev.BodyUid);
  198. if (xform.GridUid == null)
  199. return;
  200. // This will also rebuild on door open / closes which I think is good?
  201. var aabb = _lookup.GetAABBNoContainer(ev.BodyUid, xform.Coordinates.Position, xform.LocalRotation);
  202. DirtyChunkArea(xform.GridUid.Value, aabb);
  203. }
  204. private void OnCollisionLayerChange(ref CollisionLayerChangeEvent ev)
  205. {
  206. var xform = Transform(ev.Body);
  207. if (xform.GridUid == null)
  208. return;
  209. var aabb = _lookup.GetAABBNoContainer(ev.Body, xform.Coordinates.Position, xform.LocalRotation);
  210. DirtyChunkArea(xform.GridUid.Value, aabb);
  211. }
  212. private void OnBodyTypeChange(ref PhysicsBodyTypeChangedEvent ev)
  213. {
  214. if (TryComp(ev.Entity, out TransformComponent? xform) &&
  215. xform.GridUid != null)
  216. {
  217. var aabb = _lookup.GetAABBNoContainer(ev.Entity, xform.Coordinates.Position, xform.LocalRotation);
  218. DirtyChunkArea(xform.GridUid.Value, aabb);
  219. }
  220. }
  221. private void OnMoveEvent(ref MoveEvent ev)
  222. {
  223. if (!_fixturesQuery.TryGetComponent(ev.Sender, out var fixtures) ||
  224. !IsBodyRelevant(fixtures) ||
  225. _gridQuery.HasComponent(ev.Sender))
  226. {
  227. return;
  228. }
  229. var gridUid = ev.Component.GridUid;
  230. var oldGridUid = ev.OldPosition.EntityId == ev.NewPosition.EntityId
  231. ? gridUid
  232. : ev.OldPosition.GetGridUid(EntityManager);
  233. if (oldGridUid != null && oldGridUid != gridUid)
  234. {
  235. var aabb = _lookup.GetAABBNoContainer(ev.Sender, ev.OldPosition.Position, ev.OldRotation);
  236. DirtyChunkArea(oldGridUid.Value, aabb);
  237. }
  238. if (gridUid != null)
  239. {
  240. var aabb = _lookup.GetAABBNoContainer(ev.Sender, ev.NewPosition.Position, ev.NewRotation);
  241. DirtyChunkArea(gridUid.Value, aabb);
  242. }
  243. }
  244. private void OnGridInit(GridInitializeEvent ev)
  245. {
  246. EnsureComp<GridPathfindingComponent>(ev.EntityUid);
  247. // Pathfinder refactor
  248. var mapGrid = Comp<MapGridComponent>(ev.EntityUid);
  249. for (var x = Math.Floor(mapGrid.LocalAABB.Left); x <= Math.Ceiling(mapGrid.LocalAABB.Right + ChunkSize); x += ChunkSize)
  250. {
  251. for (var y = Math.Floor(mapGrid.LocalAABB.Bottom); y <= Math.Ceiling(mapGrid.LocalAABB.Top + ChunkSize); y += ChunkSize)
  252. {
  253. DirtyChunk(ev.EntityUid, mapGrid.GridTileToLocal(new Vector2i((int) x, (int) y)));
  254. }
  255. }
  256. }
  257. private void OnGridRemoved(GridRemovalEvent ev)
  258. {
  259. RemComp<GridPathfindingComponent>(ev.EntityUid);
  260. }
  261. /// <summary>
  262. /// Queues the entire relevant chunk to be re-built in the next update.
  263. /// </summary>
  264. private void DirtyChunk(EntityUid gridUid, EntityCoordinates coordinates)
  265. {
  266. if (!TryComp<GridPathfindingComponent>(gridUid, out var comp))
  267. return;
  268. var currentTime = _timing.CurTime;
  269. if (comp.NextUpdate < currentTime && !MetaData(gridUid).EntityPaused)
  270. comp.NextUpdate = currentTime + UpdateCooldown;
  271. var chunks = comp.DirtyChunks;
  272. // TODO: Change these args around.
  273. chunks.Add(GetOrigin(coordinates, gridUid));
  274. }
  275. private void DirtyChunkArea(EntityUid gridUid, Box2 aabb)
  276. {
  277. if (!TryComp<GridPathfindingComponent>(gridUid, out var comp))
  278. return;
  279. var currentTime = _timing.CurTime;
  280. if (comp.NextUpdate < currentTime)
  281. comp.NextUpdate = currentTime + UpdateCooldown;
  282. var chunks = comp.DirtyChunks;
  283. // This assumes you never have bounds equal to or larger than 2 * ChunkSize.
  284. var corners = new Vector2[] { aabb.BottomLeft, aabb.TopRight, aabb.BottomRight, aabb.TopLeft };
  285. foreach (var corner in corners)
  286. {
  287. var sampledPoint = new Vector2i(
  288. (int) Math.Floor((corner.X) / ChunkSize),
  289. (int) Math.Floor((corner.Y) / ChunkSize));
  290. chunks.Add(sampledPoint);
  291. }
  292. }
  293. private GridPathfindingChunk GetChunk(Vector2i origin, EntityUid uid, GridPathfindingComponent? component = null)
  294. {
  295. if (!Resolve(uid, ref component))
  296. {
  297. throw new InvalidOperationException();
  298. }
  299. if (component.Chunks.TryGetValue(origin, out var chunk))
  300. return chunk;
  301. chunk = new GridPathfindingChunk()
  302. {
  303. Origin = origin,
  304. };
  305. component.Chunks[origin] = chunk;
  306. return chunk;
  307. }
  308. private bool TryGetChunk(Vector2i origin, GridPathfindingComponent component, [NotNullWhen(true)] out GridPathfindingChunk? chunk)
  309. {
  310. return component.Chunks.TryGetValue(origin, out chunk);
  311. }
  312. private byte GetIndex(int x, int y)
  313. {
  314. return (byte) (x * ChunkSize + y);
  315. }
  316. private Vector2i GetOrigin(Vector2 localPos)
  317. {
  318. return new Vector2i((int) Math.Floor(localPos.X / ChunkSize), (int) Math.Floor(localPos.Y / ChunkSize));
  319. }
  320. private Vector2i GetOrigin(EntityCoordinates coordinates, EntityUid gridUid)
  321. {
  322. var localPos = Vector2.Transform(coordinates.ToMapPos(EntityManager, _transform), _transform.GetInvWorldMatrix(gridUid));
  323. return new Vector2i((int) Math.Floor(localPos.X / ChunkSize), (int) Math.Floor(localPos.Y / ChunkSize));
  324. }
  325. private void BuildBreadcrumbs(GridPathfindingChunk chunk, Entity<MapGridComponent> grid)
  326. {
  327. var sw = new Stopwatch();
  328. sw.Start();
  329. var points = chunk.Points;
  330. var gridOrigin = chunk.Origin * ChunkSize;
  331. var tileEntities = new ValueList<EntityUid>();
  332. var chunkPolys = chunk.BufferPolygons;
  333. for (var i = 0; i < chunkPolys.Length; i++)
  334. {
  335. chunkPolys[i].Clear();
  336. }
  337. var tilePolys = new ValueList<Box2i>(SubStep);
  338. // Need to get the relevant polygons in each tile.
  339. // If we wanted to create a larger navmesh we could triangulate these points but in our case we're just going
  340. // to treat them as tile-based.
  341. for (var x = 0; x < ChunkSize; x++)
  342. {
  343. for (var y = 0; y < ChunkSize; y++)
  344. {
  345. // Tile
  346. var tilePos = new Vector2i(x, y) + gridOrigin;
  347. tilePolys.Clear();
  348. var tile = _maps.GetTileRef(grid.Owner, grid.Comp, tilePos);
  349. var flags = tile.Tile.IsEmpty ? PathfindingBreadcrumbFlag.Space : PathfindingBreadcrumbFlag.None;
  350. // var isBorder = x < 0 || y < 0 || x == ChunkSize - 1 || y == ChunkSize - 1;
  351. tileEntities.Clear();
  352. var available = _lookup.GetLocalEntitiesIntersecting(tile, flags: LookupFlags.Dynamic | LookupFlags.Static);
  353. foreach (var ent in available)
  354. {
  355. // Irrelevant for pathfinding
  356. if (!_fixturesQuery.TryGetComponent(ent, out var fixtures) ||
  357. !IsBodyRelevant(fixtures))
  358. {
  359. continue;
  360. }
  361. var xform = _xformQuery.GetComponent(ent);
  362. if (xform.ParentUid != grid.Owner ||
  363. _maps.LocalToTile(grid.Owner, grid.Comp, xform.Coordinates) != tilePos)
  364. {
  365. continue;
  366. }
  367. tileEntities.Add(ent);
  368. }
  369. for (var subX = 0; subX < SubStep; subX++)
  370. {
  371. for (var subY = 0; subY < SubStep; subY++)
  372. {
  373. var xOffset = x * SubStep + subX;
  374. var yOffset = y * SubStep + subY;
  375. // Subtile
  376. var localPos = new Vector2(StepOffset + gridOrigin.X + x + (float) subX / SubStep, StepOffset + gridOrigin.Y + y + (float) subY / SubStep);
  377. var collisionMask = 0x0;
  378. var collisionLayer = 0x0;
  379. var damage = 0f;
  380. foreach (var ent in tileEntities)
  381. {
  382. if (!_fixturesQuery.TryGetComponent(ent, out var fixtures))
  383. continue;
  384. var colliding = false;
  385. foreach (var fixture in fixtures.Fixtures.Values)
  386. {
  387. // Don't need to re-do it.
  388. if (!fixture.Hard ||
  389. (collisionMask & fixture.CollisionMask) == fixture.CollisionMask &&
  390. (collisionLayer & fixture.CollisionLayer) == fixture.CollisionLayer)
  391. {
  392. continue;
  393. }
  394. // Do an AABB check first as it's probably faster, then do an actual point check.
  395. var intersects = false;
  396. foreach (var proxy in fixture.Proxies)
  397. {
  398. if (!proxy.AABB.Contains(localPos))
  399. continue;
  400. intersects = true;
  401. }
  402. if (!intersects ||
  403. !_xformQuery.TryGetComponent(ent, out var xform))
  404. {
  405. continue;
  406. }
  407. if (!_fixtures.TestPoint(fixture.Shape, new Transform(xform.LocalPosition, xform.LocalRotation), localPos))
  408. {
  409. continue;
  410. }
  411. collisionLayer |= fixture.CollisionLayer;
  412. collisionMask |= fixture.CollisionMask;
  413. colliding = true;
  414. }
  415. // If entity doesn't intersect this node (e.g. thindows) then ignore it.
  416. if (!colliding)
  417. continue;
  418. if (_accessQuery.HasComponent(ent))
  419. {
  420. flags |= PathfindingBreadcrumbFlag.Access;
  421. }
  422. if (_doorQuery.HasComponent(ent))
  423. {
  424. flags |= PathfindingBreadcrumbFlag.Door;
  425. }
  426. if (_climbableQuery.HasComponent(ent))
  427. {
  428. flags |= PathfindingBreadcrumbFlag.Climb;
  429. }
  430. if (_destructibleQuery.TryGetComponent(ent, out var damageable))
  431. {
  432. damage += _destructible.DestroyedAt(ent, damageable).Float();
  433. }
  434. }
  435. /*This is causing too many issues and I'd rather just ignore it until pathfinder refactor
  436. to just get tiles at runtime.
  437. if ((flags & PathfindingBreadcrumbFlag.Space) != 0x0)
  438. {
  439. // DebugTools.Assert(tileEntities.Count == 0);
  440. }
  441. */
  442. var crumb = new PathfindingBreadcrumb()
  443. {
  444. Coordinates = new Vector2i(xOffset, yOffset),
  445. Data = new PathfindingData(flags, collisionLayer, collisionMask, damage),
  446. };
  447. points[xOffset, yOffset] = crumb;
  448. }
  449. }
  450. // Now we got tile data and we can get the polys
  451. var data = points[x * SubStep, y * SubStep].Data;
  452. var start = Vector2i.Zero;
  453. for (var i = 0; i < SubStep * SubStep; i++)
  454. {
  455. var ix = i / SubStep;
  456. var iy = i % SubStep;
  457. var nextX = (i + 1) / SubStep;
  458. var nextY = (i + 1) % SubStep;
  459. // End point
  460. if (iy == SubStep - 1 ||
  461. !points[x * SubStep + nextX, y * SubStep + nextY].Data.Equals(data))
  462. {
  463. tilePolys.Add(new Box2i(start, new Vector2i(ix, iy)));
  464. if (i < (SubStep * SubStep) - 1)
  465. {
  466. start = new Vector2i(nextX, nextY);
  467. data = points[x * SubStep + nextX, y * SubStep + nextY].Data;
  468. }
  469. }
  470. }
  471. // Now combine the lines
  472. var anyCombined = true;
  473. while (anyCombined)
  474. {
  475. anyCombined = false;
  476. for (var i = 0; i < tilePolys.Count; i++)
  477. {
  478. var poly = tilePolys[i];
  479. data = points[x * SubStep + poly.Left, y * SubStep + poly.Bottom].Data;
  480. for (var j = i + 1; j < tilePolys.Count; j++)
  481. {
  482. var nextPoly = tilePolys[j];
  483. var nextData = points[x * SubStep + nextPoly.Left, y * SubStep + nextPoly.Bottom].Data;
  484. // Oh no, Combine
  485. if (poly.Bottom == nextPoly.Bottom &&
  486. poly.Top == nextPoly.Top &&
  487. poly.Right + 1 == nextPoly.Left &&
  488. data.Equals(nextData))
  489. {
  490. tilePolys.RemoveAt(j);
  491. j--;
  492. poly = new Box2i(poly.Left, poly.Bottom, poly.Right + 1, poly.Top);
  493. anyCombined = true;
  494. }
  495. }
  496. tilePolys[i] = poly;
  497. }
  498. }
  499. // TODO: Can store a hash for each tile and check if the breadcrumbs match and avoid allocating these at all.
  500. var tilePoly = chunkPolys[x * ChunkSize + y];
  501. var polyOffset = gridOrigin + new Vector2(x, y);
  502. foreach (var poly in tilePolys)
  503. {
  504. var box = new Box2((Vector2) poly.BottomLeft / SubStep + polyOffset,
  505. (Vector2) (poly.TopRight + Vector2i.One) / SubStep + polyOffset);
  506. var polyData = points[x * SubStep + poly.Left, y * SubStep + poly.Bottom].Data;
  507. var neighbors = new HashSet<PathPoly>();
  508. tilePoly.Add(new PathPoly(grid, chunk.Origin, GetIndex(x, y), box, polyData, neighbors));
  509. }
  510. }
  511. }
  512. // Log.Debug($"Built breadcrumbs in {sw.Elapsed.TotalMilliseconds}ms");
  513. SendBreadcrumbs(chunk, grid);
  514. }
  515. /// <summary>
  516. /// Clears all of the polygons on a tile.
  517. /// </summary>
  518. private void ClearTilePolys(List<PathPoly> polys)
  519. {
  520. foreach (var poly in polys)
  521. {
  522. ClearPoly(poly);
  523. }
  524. polys.Clear();
  525. }
  526. /// <summary>
  527. /// Clears a polygon and invalidates its flags if anyone still has a reference to it.
  528. /// </summary>
  529. private void ClearPoly(PathPoly poly)
  530. {
  531. foreach (var neighbor in poly.Neighbors)
  532. {
  533. neighbor.Neighbors.Remove(poly);
  534. }
  535. // If any paths have a ref to it let them know that the class is no longer a valid node.
  536. poly.Data.Flags = PathfindingBreadcrumbFlag.Invalid;
  537. poly.Neighbors.Clear();
  538. }
  539. private void ClearOldPolys(GridPathfindingChunk chunk)
  540. {
  541. // Can't do this in BuildBreadcrumbs because it mutates neighbors
  542. // but also we need this entirely done before BuildNavmesh
  543. var chunkPolys = chunk.Polygons;
  544. var bufferPolygons = chunk.BufferPolygons;
  545. for (var x = 0; x < ChunkSize; x++)
  546. {
  547. for (var y = 0; y < ChunkSize; y++)
  548. {
  549. var index = x * ChunkSize + y;
  550. var polys = bufferPolygons[index];
  551. var existing = chunkPolys[index];
  552. var isEquivalent = true;
  553. if (polys.Count == existing.Count)
  554. {
  555. // May want to update damage or the likes if it's different but not invalidate the ref.
  556. for (var i = 0; i < existing.Count; i++)
  557. {
  558. var ePoly = existing[i];
  559. var poly = polys[i];
  560. if (!ePoly.IsEquivalent(poly))
  561. {
  562. isEquivalent = false;
  563. break;
  564. }
  565. ePoly.Data.Damage = poly.Data.Damage;
  566. }
  567. if (isEquivalent)
  568. continue;
  569. }
  570. ClearTilePolys(existing);
  571. existing.AddRange(polys);
  572. }
  573. }
  574. }
  575. private void BuildNavmesh(GridPathfindingChunk chunk, Entity<GridPathfindingComponent> pathfinding)
  576. {
  577. var sw = new Stopwatch();
  578. sw.Start();
  579. var chunkPolys = chunk.Polygons;
  580. var component = pathfinding.Comp;
  581. component.Chunks.TryGetValue(chunk.Origin + new Vector2i(-1, 0), out var leftChunk);
  582. component.Chunks.TryGetValue(chunk.Origin + new Vector2i(0, -1), out var bottomChunk);
  583. component.Chunks.TryGetValue(chunk.Origin + new Vector2i(1, 0), out var rightChunk);
  584. component.Chunks.TryGetValue(chunk.Origin + new Vector2i(0, 1), out var topChunk);
  585. // Now we can get the neighbors for our tile polys
  586. for (var x = 0; x < ChunkSize; x++)
  587. {
  588. for (var y = 0; y < ChunkSize; y++)
  589. {
  590. var index = GetIndex(x, y);
  591. var tile = chunkPolys[index];
  592. for (byte i = 0; i < tile.Count; i++)
  593. {
  594. var poly = tile[i];
  595. var enlarged = poly.Box.Enlarged(StepOffset);
  596. // Shouldn't need to wraparound as previous neighbors would've handled us.
  597. for (var j = (byte) (i + 1); j < tile.Count; j++)
  598. {
  599. var neighbor = tile[j];
  600. var enlargedNeighbor = neighbor.Box.Enlarged(StepOffset);
  601. var overlap = Box2.Area(enlarged.Intersect(enlargedNeighbor));
  602. // Need to ensure they intersect by at least 2 tiles.
  603. if (overlap <= 0.5f / SubStep)
  604. continue;
  605. AddNeighbors(poly, neighbor);
  606. }
  607. // TODO: Get neighbor tile polys
  608. for (var ix = -1; ix <= 1; ix++)
  609. {
  610. for (var iy = -1; iy <= 1; iy++)
  611. {
  612. if (ix != 0 && iy != 0)
  613. continue;
  614. var neighborX = x + ix;
  615. var neighborY = y + iy;
  616. var neighborIndex = GetIndex(neighborX, neighborY);
  617. List<PathPoly> neighborTile;
  618. if (neighborX < 0)
  619. {
  620. if (leftChunk == null)
  621. continue;
  622. neighborX = ChunkSize - 1;
  623. neighborIndex = GetIndex(neighborX, neighborY);
  624. neighborTile = leftChunk.Polygons[neighborIndex];
  625. }
  626. else if (neighborY < 0)
  627. {
  628. if (bottomChunk == null)
  629. continue;
  630. neighborY = ChunkSize - 1;
  631. neighborIndex = GetIndex(neighborX, neighborY);
  632. neighborTile = bottomChunk.Polygons[neighborIndex];
  633. }
  634. else if (neighborX >= ChunkSize)
  635. {
  636. if (rightChunk == null)
  637. continue;
  638. neighborX = 0;
  639. neighborIndex = GetIndex(neighborX, neighborY);
  640. neighborTile = rightChunk.Polygons[neighborIndex];
  641. }
  642. else if (neighborY >= ChunkSize)
  643. {
  644. if (topChunk == null)
  645. continue;
  646. neighborY = 0;
  647. neighborIndex = GetIndex(neighborX, neighborY);
  648. neighborTile = topChunk.Polygons[neighborIndex];
  649. }
  650. else
  651. {
  652. neighborTile = chunkPolys[neighborIndex];
  653. }
  654. for (byte j = 0; j < neighborTile.Count; j++)
  655. {
  656. var neighbor = neighborTile[j];
  657. var enlargedNeighbor = neighbor.Box.Enlarged(StepOffset);
  658. var overlap = Box2.Area(enlarged.Intersect(enlargedNeighbor));
  659. // Need to ensure they intersect by at least 2 tiles.
  660. if (overlap <= 0.5f / SubStep)
  661. continue;
  662. AddNeighbors(poly, neighbor);
  663. }
  664. }
  665. }
  666. }
  667. }
  668. }
  669. // Log.Debug($"Built navmesh in {sw.Elapsed.TotalMilliseconds}ms");
  670. SendPolys(chunk, pathfinding, chunkPolys);
  671. }
  672. private void AddNeighbors(PathPoly polyA, PathPoly polyB)
  673. {
  674. DebugTools.Assert((polyA.Data.Flags & PathfindingBreadcrumbFlag.Invalid) == 0x0);
  675. DebugTools.Assert((polyB.Data.Flags & PathfindingBreadcrumbFlag.Invalid) == 0x0);
  676. polyA.Neighbors.Add(polyB);
  677. polyB.Neighbors.Add(polyA);
  678. }
  679. }