Client LuaCsForBarotrauma
PathFinder.cs
2 using Microsoft.Xna.Framework;
3 using System;
4 using System.Collections.Generic;
5 using System.Linq;
6 
7 namespace Barotrauma
8 {
9  class PathNode
10  {
11  public int state;
12 
13  public PathNode Parent;
14 
15  public float F, G, H;
16 
17  public readonly List<PathNode> connections = new List<PathNode>();
18  public List<float> distances;
19 
20  public Vector2 TempPosition;
21  public float TempDistance;
22 
23  public readonly WayPoint Waypoint;
24  public readonly Vector2 Position;
25  public readonly int WayPointID;
26 
27  public override string ToString()
28  {
29  return $"PathNode {WayPointID}";
30  }
31 
32  public PathNode(WayPoint wayPoint)
33  {
34  Waypoint = wayPoint;
35  Position = wayPoint.SimPosition;
37  }
38 
39  public static List<PathNode> GenerateNodes(List<WayPoint> wayPoints, bool removeOrphans)
40  {
41  var nodes = new Dictionary<int, PathNode>();
42  foreach (WayPoint wayPoint in wayPoints)
43  {
44  if (wayPoint == null) { continue; }
45  if (nodes.ContainsKey(wayPoint.ID))
46  {
47 #if DEBUG
48  DebugConsole.ThrowError("Error in PathFinder.GenerateNodes (duplicate ID \"" + wayPoint.ID + "\")");
49 #endif
50  continue;
51  }
52  nodes.Add(wayPoint.ID, new PathNode(wayPoint));
53  }
54 
55  foreach (KeyValuePair<int, PathNode> node in nodes)
56  {
57  foreach (MapEntity linked in node.Value.Waypoint.linkedTo)
58  {
59  nodes.TryGetValue(linked.ID, out PathNode connectedNode);
60  if (connectedNode == null) { continue; }
61  if (!node.Value.connections.Contains(connectedNode)) { node.Value.connections.Add(connectedNode); }
62  if (!connectedNode.connections.Contains(node.Value)) { connectedNode.connections.Add(node.Value); }
63  }
64  }
65 
66  var nodeList = nodes.Values.ToList();
67  if (removeOrphans)
68  {
69  nodeList.RemoveAll(n => n.connections.Count == 0);
70  }
71  foreach (PathNode node in nodeList)
72  {
73  node.distances = new List<float>();
74  for (int i = 0; i < node.connections.Count; i++)
75  {
76  node.distances.Add(Vector2.Distance(node.Position, node.connections[i].Position));
77  }
78  }
79 
80  return nodeList;
81  }
82 
83  private bool? blocked;
84  public bool IsBlocked()
85  {
86  if (blocked.HasValue) { return blocked.Value; }
87  blocked = false;
88  if (Waypoint.Submarine != null) { return blocked.Value; }
89  if (Waypoint.Tunnel?.Type != Level.TunnelType.Cave) { return blocked.Value; }
90  foreach (var w in Level.Loaded.ExtraWalls)
91  {
92  if (!w.IsPointInside(Waypoint.Position)) { continue; }
93  if (w is DestructibleLevelWall d)
94  {
95  blocked = !d.Destroyed;
96  }
97  if (blocked.Value) { break; }
98  }
99  return blocked.Value;
100  }
101 
102  public void ResetBlocked()
103  {
104  blocked = null;
105  }
106  }
107 
109  {
110  public delegate float? GetNodePenaltyHandler(PathNode node, PathNode prevNode);
112  public delegate float? GetSingleNodePenaltyHandler(PathNode node);
114 
115  private readonly List<PathNode> nodes;
116  private readonly bool isCharacter;
117 
118  public bool InsideSubmarine { get; set; }
119  public bool ApplyPenaltyToOutsideNodes { get; set; }
120 
121  public PathFinder(List<WayPoint> wayPoints, bool isCharacter)
122  {
123  var filtered = isCharacter ? wayPoints : wayPoints.FindAll(w => w.Submarine == null);
124  nodes = PathNode.GenerateNodes(filtered, removeOrphans: true);
125  foreach (WayPoint wp in wayPoints)
126  {
127  wp.OnLinksChanged += WaypointLinksChanged;
128  }
129  sortedNodes = new List<PathNode>(nodes.Count);
130  this.isCharacter = isCharacter;
131  }
132 
133  void WaypointLinksChanged(WayPoint wp)
134  {
135  if (Submarine.Unloading) { return; }
136 
137  var node = nodes.Find(n => n.Waypoint == wp);
138  if (node == null) { return; }
139 
140  for (int i = node.connections.Count - 1; i >= 0; i--)
141  {
142  //remove connection if the waypoint isn't connected anymore
143  if (wp.linkedTo.FirstOrDefault(l => l == node.connections[i].Waypoint) == null)
144  {
145  node.connections.RemoveAt(i);
146  node.distances.RemoveAt(i);
147  }
148  }
149 
150  for (int i = 0; i < wp.linkedTo.Count; i++)
151  {
152  if (!(wp.linkedTo[i] is WayPoint connected)) { continue; }
153 
154  //already connected, continue
155  if (node.connections.Any(n => n.Waypoint == connected)) { continue; }
156 
157  var matchingNode = nodes.Find(n => n.Waypoint == connected);
158  if (matchingNode == null)
159  {
160 #if DEBUG
161  DebugConsole.ThrowError("Waypoint connections were changed, no matching path node found in PathFinder");
162 #endif
163  return;
164  }
165 
166  node.connections.Add(matchingNode);
167  node.distances.Add(Vector2.Distance(node.Position, matchingNode.Position));
168  }
169  }
170 
171  private readonly List<PathNode> sortedNodes;
172 
173  public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, float minGapSize = 0, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisibility = true)
174  {
175  foreach (PathNode node in nodes)
176  {
177  node.ResetBlocked();
178  }
179 
180  // First calculate the temp positions for all nodes.
181  foreach (PathNode node in nodes)
182  {
183  node.TempPosition = node.Position;
184  var wpSub = node.Waypoint.Submarine;
185  if (hostSub != null && wpSub == null)
186  {
187  // inside and targeting outside
188  node.TempPosition -= hostSub.SimPosition;
189  }
190  else if (wpSub != null && hostSub != null && wpSub != hostSub)
191  {
192  // different subs
193  node.TempPosition -= hostSub.SimPosition - wpSub.SimPosition;
194  }
195  else if (hostSub == null && wpSub != null)
196  {
197  // Outside and targeting inside
198  node.TempPosition += wpSub.SimPosition;
199  }
200  }
201 
202  //sort nodes roughly according to distance
203  sortedNodes.Clear();
204  PathNode startNode = null;
205  foreach (PathNode node in nodes)
206  {
207  float xDiff = Math.Abs(start.X - node.TempPosition.X);
208  float yDiff = Math.Abs(start.Y - node.TempPosition.Y);
209  if (InsideSubmarine && !(node.Waypoint.Submarine?.Info?.IsRuin ?? false))
210  {
211  //higher cost for vertical movement when inside the sub
212  if (yDiff > 1.0f && node.Waypoint.Ladders == null && node.Waypoint.Stairs == null)
213  {
214  yDiff += 10.0f;
215  }
216  node.TempDistance = xDiff + yDiff * 10.0f;
217  }
218  else
219  {
220  node.TempDistance = xDiff + yDiff;
221  }
222 
223  //much higher cost to waypoints that are outside
224  if (node.Waypoint.CurrentHull == null && ApplyPenaltyToOutsideNodes) { node.TempDistance *= 10.0f; }
225 
226  //optimization: node extremely far, don't try to use it as a start node
227  if (node.TempDistance > (InsideSubmarine ? 100.0f : 800.0f))
228  {
229  continue;
230  }
231  //optimization: node close enough. If it's valid, choose it as the start node and skip the more exhaustive search for the closest one
232  if (node.TempDistance < FarseerPhysics.ConvertUnits.ToSimUnits(AIObjectiveGetItem.DefaultReach))
233  {
234  if (IsValidStartNode(node))
235  {
236  startNode = node;
237  break;
238  }
239  }
240  //prefer nodes that are closer to the end position
241  node.TempDistance += (Math.Abs(end.X - node.TempPosition.X) + Math.Abs(end.Y - node.TempPosition.Y)) / 100.0f;
242 
243  int i = 0;
244  while (i < sortedNodes.Count && sortedNodes[i].TempDistance < node.TempDistance)
245  {
246  i++;
247  }
248  sortedNodes.Insert(i, node);
249  }
250 
251  //find the most suitable start node, starting from the ones that are the closest
252  if (startNode == null)
253  {
254  foreach (PathNode node in sortedNodes)
255  {
256  if (IsValidStartNode(node))
257  {
258  startNode = node;
259  break;
260  }
261  }
262  }
263 
264  if (startNode == null)
265  {
266 #if DEBUG
267  DebugConsole.NewMessage("Pathfinding error, couldn't find a start node. "+ errorMsgStr, Color.DarkRed);
268 #endif
269  return new SteeringPath(true);
270  }
271 
272  //sort nodes again, now based on distance from the end position
273  sortedNodes.Clear();
274  PathNode endNode = null;
275  foreach (PathNode node in nodes)
276  {
277  node.TempDistance = Vector2.DistanceSquared(end, node.TempPosition);
278  if (InsideSubmarine)
279  {
281  {
282  //much higher cost to waypoints that are outside
283  if (node.Waypoint.CurrentHull == null) { node.TempDistance *= 10.0f; }
284  }
285  //avoid stopping at a doorway
286  if (node.Waypoint.ConnectedDoor != null) { node.TempDistance *= 10.0f; }
287  }
288  //optimization: node extremely far (> 100m / 800 m) from the end position, don't try to use it as an end node
289  if (node.TempDistance > (InsideSubmarine ? 100.0f * 100.0f : 800.0f * 800.0f))
290  {
291  continue;
292  }
293  //optimization: node extremely close (< 1 m). If it's valid, choose it as the end node and skip the more exhaustive search for the closest one
294  if (node.TempDistance < 1.0f)
295  {
296  if (IsValidEndNode(node))
297  {
298  endNode = node;
299  break;
300  }
301  }
302  int i = 0;
303  while (i < sortedNodes.Count && sortedNodes[i].TempDistance < node.TempDistance)
304  {
305  i++;
306  }
307  sortedNodes.Insert(i, node);
308  }
309  if (endNode == null)
310  {
311  //find the most suitable end node, starting from the ones closest to the end position
312  foreach (PathNode node in sortedNodes)
313  {
314  if (IsValidEndNode(node))
315  {
316  endNode = node;
317  break;
318  }
319  }
320  }
321  if (endNode == null)
322  {
323 #if DEBUG
324  DebugConsole.NewMessage("Pathfinding error, couldn't find an end node. " + errorMsgStr, Color.DarkRed);
325 #endif
326  return new SteeringPath(true);
327  }
328  return FindPath(startNode, endNode, nodeFilter, errorMsgStr, minGapSize);
329 
330  bool IsValidStartNode(PathNode node) => IsValidNode(node, (isCharacter, start), startNodeFilter);
331 
332  bool IsValidEndNode(PathNode node) => IsValidNode(node, (isCharacter && checkVisibility, end), endNodeFilter);
333 
334  bool IsValidNode(PathNode node, (bool check, Vector2 start) visibilityCheck, Func<PathNode, bool> extraFilter)
335  {
336  if (nodeFilter != null && !nodeFilter(node)) { return false; }
337  if (extraFilter != null && !extraFilter(node)) { return false; }
338  if (GetSingleNodePenalty != null && GetSingleNodePenalty(node) == null) { return false; }
339  if (node.Waypoint.ConnectedGap != null)
340  {
341  if (!CanFitThroughGap(node.Waypoint.ConnectedGap, minGapSize)) { return false; }
342  }
343  if (visibilityCheck.check)
344  {
345  var body = Submarine.PickBody(visibilityCheck.start, node.TempPosition,
346  collisionCategory: Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs);
347  if (body != null)
348  {
349  if (body.UserData is Submarine) { return false; }
350  if (body.UserData is Structure s && !s.IsPlatform) { return false; }
351  if (body.UserData is Voronoi2.VoronoiCell) { return false; }
352  if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { return false; }
353  }
354  }
355  return true;
356  }
357  }
358 
359  private SteeringPath FindPath(PathNode start, PathNode end, Func<PathNode, bool> filter = null, string errorMsgStr = "", float minGapSize = 0)
360  {
361  if (start == end)
362  {
363  var path1 = new SteeringPath();
364  path1.AddNode(start.Waypoint);
365  return path1;
366  }
367 
368  foreach (PathNode node in nodes)
369  {
370  node.Parent = null;
371  node.state = 0;
372  node.F = 0.0f;
373  node.G = 0.0f;
374  node.H = 0.0f;
375  }
376 
377  start.state = 1;
378  while (true)
379  {
380  PathNode currNode = null;
381  float dist = float.MaxValue;
382  foreach (PathNode node in nodes)
383  {
384  if (node.state != 1 || node.F > dist) { continue; }
385  if (filter != null && !filter(node)) { continue; }
386  if (node.Waypoint.ConnectedGap != null)
387  {
388  if (!CanFitThroughGap(node.Waypoint.ConnectedGap, minGapSize)) { continue; }
389  }
390  dist = node.F;
391  currNode = node;
392  }
393 
394  if (currNode == null || currNode == end) { break; }
395 
396  currNode.state = 2;
397 
398  for (int i = 0; i < currNode.connections.Count; i++)
399  {
400  PathNode nextNode = currNode.connections[i];
401 
402  //a node that hasn't been searched yet
403  if (nextNode.state == 0)
404  {
405  nextNode.H = Vector2.Distance(nextNode.Position, end.Position);
406 
407  float penalty = 0.0f;
408  if (GetNodePenalty != null)
409  {
410  float? nodePenalty = GetNodePenalty(currNode, nextNode);
411  if (nodePenalty == null)
412  {
413  nextNode.state = -1;
414  continue;
415  }
416  penalty = nodePenalty.Value;
417  }
418 
419  nextNode.G = currNode.G + currNode.distances[i] + penalty;
420  nextNode.F = nextNode.G + nextNode.H;
421  nextNode.Parent = currNode;
422  nextNode.state = 1;
423  }
424  //node that has been searched
425  else if (nextNode.state == 1 || nextNode.state == -1)
426  {
427  float tempG = currNode.G + currNode.distances[i];
428 
429  if (GetNodePenalty != null)
430  {
431  float? nodePenalty = GetNodePenalty(currNode, nextNode);
432  if (nodePenalty == null) { continue; }
433  tempG += nodePenalty.Value;
434  }
435 
436  //only use if this new route is better than the
437  //route the node was a part of
438  if (tempG < nextNode.G)
439  {
440  nextNode.G = tempG;
441  nextNode.F = nextNode.G + nextNode.H;
442  nextNode.Parent = currNode;
443  nextNode.state = 1;
444  }
445  }
446  }
447  }
448 
449  if (end.state == 0 || end.Parent == null)
450  {
451 #if DEBUG
452  if (errorMsgStr != null)
453  {
454  DebugConsole.NewMessage("Path not found. " + errorMsgStr, Color.Yellow);
455  }
456 #endif
457  return new SteeringPath(true);
458  }
459 
460  SteeringPath path = new SteeringPath();
461  List<WayPoint> finalPath = new List<WayPoint>();
462 
463  PathNode pathNode = end;
464  while (pathNode != start && pathNode != null)
465  {
466  finalPath.Add(pathNode.Waypoint);
467 
468  //(there was one bug report that seems to have been caused by this loop never terminating:
469  //couldn't reproduce or figure out what caused it, but here's a workaround that prevents the game from crashing in case it happens again)
470 
471  //should be fixed now, was most likely caused by the parent fields of the nodes not being cleared before starting the pathfinding
472  if (finalPath.Count > nodes.Count)
473  {
474 #if DEBUG
475  DebugConsole.ThrowError("Pathfinding error: constructing final path failed");
476 #endif
477  return new SteeringPath(true);
478  }
479 
480  path.Cost += pathNode.F;
481  pathNode = pathNode.Parent;
482  }
483 
484  finalPath.Add(start.Waypoint);
485  for (int i = finalPath.Count - 1; i >= 0; i--)
486  {
487  path.AddNode(finalPath[i]);
488  }
489  System.Diagnostics.Debug.Assert(finalPath.Count == path.Nodes.Count);
490 
491  return path;
492  }
493 
494  private bool CanFitThroughGap(Gap gap, float minWidth) => gap.IsHorizontal ? gap.RectHeight > minWidth : gap.RectWidth > minWidth;
495  }
496 }
Submarine Submarine
Definition: Entity.cs:53
readonly ushort ID
Unique, but non-persistent identifier. Stays the same if the entities are created in the exactly same...
Definition: Entity.cs:43
delegate? float GetSingleNodePenaltyHandler(PathNode node)
GetNodePenaltyHandler GetNodePenalty
Definition: PathFinder.cs:111
PathFinder(List< WayPoint > wayPoints, bool isCharacter)
Definition: PathFinder.cs:121
GetSingleNodePenaltyHandler GetSingleNodePenalty
Definition: PathFinder.cs:113
SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub=null, string errorMsgStr=null, float minGapSize=0, Func< PathNode, bool > startNodeFilter=null, Func< PathNode, bool > endNodeFilter=null, Func< PathNode, bool > nodeFilter=null, bool checkVisibility=true)
Definition: PathFinder.cs:173
delegate? float GetNodePenaltyHandler(PathNode node, PathNode prevNode)
readonly WayPoint Waypoint
Definition: PathFinder.cs:23
Vector2 TempPosition
Definition: PathFinder.cs:20
override string ToString()
Definition: PathFinder.cs:27
readonly Vector2 Position
Definition: PathFinder.cs:24
static List< PathNode > GenerateNodes(List< WayPoint > wayPoints, bool removeOrphans)
Definition: PathFinder.cs:39
readonly List< PathNode > connections
Definition: PathFinder.cs:17
readonly int WayPointID
Definition: PathFinder.cs:25
PathNode(WayPoint wayPoint)
Definition: PathFinder.cs:32
List< float > distances
Definition: PathFinder.cs:18
static Body PickBody(Vector2 rayStart, Vector2 rayEnd, IEnumerable< Body > ignoredBodies=null, Category? collisionCategory=null, bool ignoreSensors=true, Predicate< Fixture > customPredicate=null, bool allowInsideFixture=false)