Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs
1 using FarseerPhysics;
2 using FarseerPhysics.Collision.Shapes;
3 using FarseerPhysics.Common;
4 using FarseerPhysics.Dynamics;
5 using Microsoft.Xna.Framework;
6 using System;
7 using System.Collections.Generic;
8 using System.Diagnostics;
9 using System.Linq;
10 using Voronoi2;
11 
12 namespace Barotrauma
13 {
14  static partial class CaveGenerator
15  {
16  public static List<VoronoiCell> GraphEdgesToCells(List<GraphEdge> graphEdges, Rectangle borders, float gridCellSize, out List<VoronoiCell>[,] cellGrid)
17  {
18  List<VoronoiCell> cells = new List<VoronoiCell>();
19 
20  cellGrid = new List<VoronoiCell>[(int)Math.Ceiling(borders.Width / gridCellSize), (int)Math.Ceiling(borders.Height / gridCellSize)];
21  for (int x = 0; x < borders.Width / gridCellSize; x++)
22  {
23  for (int y = 0; y < borders.Height / gridCellSize; y++)
24  {
25  cellGrid[x, y] = new List<VoronoiCell>();
26  }
27  }
28 
29  foreach (GraphEdge ge in graphEdges)
30  {
31  if (Vector2.DistanceSquared(ge.Point1, ge.Point2) < 0.001f) { continue; }
32 
33  for (int i = 0; i < 2; i++)
34  {
35  Site site = (i == 0) ? ge.Site1 : ge.Site2;
36 
37  int x = (int)(Math.Floor((site.Coord.X - borders.X) / gridCellSize));
38  int y = (int)(Math.Floor((site.Coord.Y - borders.Y) / gridCellSize));
39 
40  x = MathHelper.Clamp(x, 0, cellGrid.GetLength(0) - 1);
41  y = MathHelper.Clamp(y, 0, cellGrid.GetLength(1) - 1);
42 
43  VoronoiCell cell = cellGrid[x, y].Find(c => c.Site == site);
44 
45  if (cell == null)
46  {
47  cell = new VoronoiCell(site);
48  cellGrid[x, y].Add(cell);
49  cells.Add(cell);
50  }
51 
52  if (ge.Cell1 == null)
53  {
54  ge.Cell1 = cell;
55  }
56  else
57  {
58  ge.Cell2 = cell;
59  }
60  cell.Edges.Add(ge);
61  }
62  }
63 
64  //add edges to the borders of the graph
65  foreach (var cell in cells)
66  {
67  Vector2? point1 = null, point2 = null;
68  foreach (GraphEdge ge in cell.Edges)
69  {
70  if (MathUtils.NearlyEqual(ge.Point1.X, borders.X) || MathUtils.NearlyEqual(ge.Point1.X, borders.Right) ||
71  MathUtils.NearlyEqual(ge.Point1.Y, borders.Y) || MathUtils.NearlyEqual(ge.Point1.Y, borders.Bottom))
72  {
73  if (point1 == null)
74  {
75  point1 = ge.Point1;
76  }
77  else if (point2 == null)
78  {
79  if (MathUtils.NearlyEqual(point1.Value, ge.Point1)) { continue; }
80  point2 = ge.Point1;
81  }
82  }
83  if (MathUtils.NearlyEqual(ge.Point2.X, borders.X) || MathUtils.NearlyEqual(ge.Point2.X, borders.Right) ||
84  MathUtils.NearlyEqual(ge.Point2.Y, borders.Y) || MathUtils.NearlyEqual(ge.Point2.Y, borders.Bottom))
85  {
86  if (point1 == null)
87  {
88  point1 = ge.Point2;
89  }
90  else
91  {
92  if (MathUtils.NearlyEqual(point1.Value, ge.Point2)) { continue; }
93  point2 = ge.Point2;
94  }
95  }
96  if (point1.HasValue && point2.HasValue)
97  {
98  Debug.Assert(point1 != point2);
99  bool point1OnSide = MathUtils.NearlyEqual(point1.Value.X, borders.X) || MathUtils.NearlyEqual(point1.Value.X, borders.Right);
100  bool point2OnSide = MathUtils.NearlyEqual(point2.Value.X, borders.X) || MathUtils.NearlyEqual(point2.Value.X, borders.Right);
101  //one point is one the side, another on top/bottom
102  // -> the cell is in the corner of the level, we need 2 edges
103  if (point1OnSide != point2OnSide)
104  {
105  Vector2 cornerPos = new Vector2(
106  point1.Value.X < borders.Center.X ? borders.X : borders.Right,
107  point1.Value.Y < borders.Center.Y ? borders.Y : borders.Bottom);
108  cell.Edges.Add(
109  new GraphEdge(point1.Value, cornerPos)
110  {
111  Cell1 = cell,
112  IsSolid = true,
113  Site1 = cell.Site,
114  OutsideLevel = true
115  });
116  cell.Edges.Add(
117  new GraphEdge(point2.Value, cornerPos)
118  {
119  Cell1 = cell,
120  IsSolid = true,
121  Site1 = cell.Site,
122  OutsideLevel = true
123  });
124  }
125  else
126  {
127  cell.Edges.Add(
128  new GraphEdge(point1.Value, point2.Value)
129  {
130  Cell1 = cell,
131  IsSolid = true,
132  Site1 = cell.Site,
133  OutsideLevel = true
134  });
135  }
136  break;
137  }
138  }
139  }
140 
141  return cells;
142  }
143 
144  public static void GeneratePath(Level.Tunnel tunnel, Level level)
145  {
146  var targetCells = new List<VoronoiCell>();
147  for (int i = 0; i < tunnel.Nodes.Count; i++)
148  {
149  var closestCell = level.GetClosestCell(tunnel.Nodes[i].ToVector2());
150  if (closestCell != null && !targetCells.Contains(closestCell))
151  {
152  targetCells.Add(closestCell);
153  }
154  }
155  tunnel.Cells.AddRange(GeneratePath(targetCells, level.GetAllCells()));
156  }
157 
158  public static List<VoronoiCell> GeneratePath(List<VoronoiCell> targetCells, List<VoronoiCell> cells)
159  {
160  List<VoronoiCell> pathCells = new List<VoronoiCell>();
161 
162  if (targetCells.Count == 0) { return pathCells; }
163 
164  VoronoiCell currentCell = targetCells[0];
165  currentCell.CellType = CellType.Path;
166  pathCells.Add(currentCell);
167 
168  int currentTargetIndex = 0;
169 
170  int iterationsLeft = cells.Count / 2;
171 
172  do
173  {
174  int edgeIndex = 0;
175 
176  double smallestDist = double.PositiveInfinity;
177  for (int i = 0; i < currentCell.Edges.Count; i++)
178  {
179  var adjacentCell = currentCell.Edges[i].AdjacentCell(currentCell);
180  if (adjacentCell == null) { continue; }
181  double dist = MathUtils.Distance(adjacentCell.Site.Coord.X, adjacentCell.Site.Coord.Y, targetCells[currentTargetIndex].Site.Coord.X, targetCells[currentTargetIndex].Site.Coord.Y);
182  dist += MathUtils.Distance(adjacentCell.Site.Coord.X, adjacentCell.Site.Coord.Y, currentCell.Site.Coord.X, currentCell.Site.Coord.Y) * 0.5f;
183 
184  //disfavor short edges to prevent generating a very small passage
185  if (Vector2.DistanceSquared(currentCell.Edges[i].Point1, currentCell.Edges[i].Point2) < 150.0f * 150.0f)
186  {
187  //divide by the number of times the current cell has been used
188  // prevents the path from getting "stuck" (jumping back and forth between adjacent cells)
189  // if there's no other way to the destination than going through a short edge
190  dist *= 10.0f / Math.Max(pathCells.Count(c => c == currentCell), 1.0f);
191  }
192  if (dist < smallestDist)
193  {
194  edgeIndex = i;
195  smallestDist = dist;
196  }
197  }
198 
199  currentCell = currentCell.Edges[edgeIndex].AdjacentCell(currentCell);
200  currentCell.CellType = CellType.Path;
201  pathCells.Add(currentCell);
202 
203  iterationsLeft--;
204 
205  if (currentCell == targetCells[currentTargetIndex])
206  {
207  currentTargetIndex++;
208  if (currentTargetIndex >= targetCells.Count) { break; }
209  }
210 
211  } while (currentCell != targetCells[targetCells.Count - 1] && iterationsLeft > 0);
212 
213  return pathCells;
214  }
215 
220  public static void RoundCell(VoronoiCell cell, float minEdgeLength = 500.0f, float roundingAmount = 0.5f, float irregularity = 0.1f)
221  {
222  List<GraphEdge> tempEdges = new List<GraphEdge>();
223  foreach (GraphEdge edge in cell.Edges)
224  {
225  if (!edge.IsSolid || edge.OutsideLevel)
226  {
227  tempEdges.Add(edge);
228  continue;
229  }
230 
231  Vector2 edgeDiff = edge.Point2 - edge.Point1;
232  Vector2 edgeDir = Vector2.Normalize(edgeDiff);
233 
234  //If the edge is next to an empty cell and there's another solid cell at the other side of the empty one,
235  //don't touch this edge. Otherwise we may end up closing off small passages between cells.
236  var adjacentEmptyCell = edge.AdjacentCell(cell);
237  if (adjacentEmptyCell?.CellType == CellType.Solid) { adjacentEmptyCell = null; }
238  if (adjacentEmptyCell != null)
239  {
240  GraphEdge adjacentEdge = null;
241  //find the edge at the opposite side of the adjacent cell
242  foreach (GraphEdge otherEdge in adjacentEmptyCell.Edges)
243  {
244  if (otherEdge == edge || otherEdge.AdjacentCell(adjacentEmptyCell)?.CellType != CellType.Solid) { continue; }
245  Vector2 otherEdgeDir = Vector2.Normalize(otherEdge.Point2 - otherEdge.Point1);
246  //dot product is > 0.7 if the edges are roughly parallel
247  if (Math.Abs(Vector2.Dot(otherEdgeDir, edgeDir)) > 0.7f)
248  {
249  adjacentEdge = otherEdge;
250  break;
251  }
252  }
253  if (adjacentEdge != null)
254  {
255  tempEdges.Add(edge);
256  continue;
257  }
258  }
259  List<Vector2> edgePoints = new List<Vector2>();
260  Vector2 edgeNormal = edge.GetNormal(cell);
261 
262  float edgeLength = Vector2.Distance(edge.Point1, edge.Point2);
263  int pointCount = (int)Math.Max(Math.Ceiling(edgeLength / minEdgeLength), 1);
264  for (int i = 0; i <= pointCount; i++)
265  {
266  if (i == 0)
267  {
268  edgePoints.Add(edge.Point1);
269  }
270  else if (i == pointCount)
271  {
272  edgePoints.Add(edge.Point2);
273  }
274  else
275  {
276 
277  float centerF = 0.5f - Math.Abs(0.5f - (i / (float)pointCount));
278  float randomVariance = Rand.Range(0, irregularity, Rand.RandSync.ServerAndClient);
279  Vector2 extrudedPoint =
280  edge.Point1 +
281  edgeDiff * (i / (float)pointCount) +
282  edgeNormal * edgeLength * (roundingAmount + randomVariance) * centerF;
283 
284  var nearbyCells = Level.Loaded.GetCells(extrudedPoint, searchDepth: 2);
285  bool isInside = false;
286  foreach (var nearbyCell in nearbyCells)
287  {
288  if (nearbyCell == cell || nearbyCell.CellType != CellType.Solid) { continue; }
289  //check if extruding the edge causes it to go inside another one
290  if (nearbyCell.IsPointInside(extrudedPoint))
291  {
292  isInside = true;
293  break;
294  }
295  //check if another edge will be inside this cell after the extrusion
296  Vector2 triangleCenter = (edge.Point1 + edge.Point2 + extrudedPoint) / 3;
297  foreach (GraphEdge nearbyEdge in nearbyCell.Edges)
298  {
299  if (!MathUtils.LineSegmentsIntersect(nearbyEdge.Point1, triangleCenter, edge.Point1, extrudedPoint) &&
300  !MathUtils.LineSegmentsIntersect(nearbyEdge.Point1, triangleCenter, edge.Point2, extrudedPoint) &&
301  !MathUtils.LineSegmentsIntersect(nearbyEdge.Point1, triangleCenter, edge.Point1, edge.Point2))
302  {
303  isInside = true;
304  break;
305  }
306  }
307  if (isInside) { break; }
308  }
309 
310  if (!isInside)
311  {
312  edgePoints.Add(extrudedPoint);
313  }
314  }
315  }
316 
317  for (int i = 0; i < edgePoints.Count - 1; i++)
318  {
319  tempEdges.Add(new GraphEdge(edgePoints[i], edgePoints[i + 1])
320  {
321  Cell1 = edge.Cell1,
322  Cell2 = edge.Cell2,
323  IsSolid = edge.IsSolid,
324  Site1 = edge.Site1,
325  Site2 = edge.Site2,
326  OutsideLevel = edge.OutsideLevel,
327  NextToCave = edge.NextToCave,
328  NextToMainPath = edge.NextToMainPath,
329  NextToSidePath = edge.NextToSidePath
330  });
331  }
332  }
333 
334  cell.Edges = tempEdges;
335  }
336 
337  public static Body GeneratePolygons(List<VoronoiCell> cells, Level level, out List<Vector2[]> renderTriangles)
338  {
339  renderTriangles = new List<Vector2[]>();
340 
341  List<Vector2> tempVertices = new List<Vector2>();
342  List<Vector2> bodyPoints = new List<Vector2>();
343 
344  Body cellBody = new Body()
345  {
346  SleepingAllowed = false,
347  BodyType = BodyType.Static,
348  CollisionCategories = Physics.CollisionLevel
349  };
350  GameMain.World.Add(cellBody, findNewContacts: false);
351 
352  for (int n = cells.Count - 1; n >= 0; n-- )
353  {
354  VoronoiCell cell = cells[n];
355 
356  bodyPoints.Clear();
357  tempVertices.Clear();
358  foreach (GraphEdge ge in cell.Edges)
359  {
360  if (Vector2.DistanceSquared(ge.Point1, ge.Point2) < 0.01f) continue;
361  if (!tempVertices.Any(v => Vector2.DistanceSquared(ge.Point1, v) < 1.0f))
362  {
363  tempVertices.Add(ge.Point1);
364  bodyPoints.Add(ge.Point1);
365  }
366  if (!tempVertices.Any(v => Vector2.DistanceSquared(ge.Point2, v) < 1.0f))
367  {
368  tempVertices.Add(ge.Point2);
369  bodyPoints.Add(ge.Point2);
370  }
371  }
372 
373  if (tempVertices.Count < 3 || bodyPoints.Count < 2)
374  {
375  cells.RemoveAt(n);
376  continue;
377  }
378 
379  Vector2 minVert = tempVertices[0];
380  Vector2 maxVert = tempVertices[0];
381  foreach (var vert in tempVertices)
382  {
383  minVert = new Vector2(
384  Math.Min(minVert.X, vert.X),
385  Math.Min(minVert.Y, vert.Y));
386  maxVert = new Vector2(
387  Math.Max(maxVert.X, vert.X),
388  Math.Max(maxVert.Y, vert.Y));
389  }
390  Vector2 center = (minVert + maxVert) / 2;
391  renderTriangles.AddRange(MathUtils.TriangulateConvexHull(tempVertices, center));
392 
393  if (bodyPoints.Count < 2) { continue; }
394 
395  if (bodyPoints.Count < 3)
396  {
397  foreach (Vector2 vertex in tempVertices)
398  {
399  if (bodyPoints.Contains(vertex)) { continue; }
400  bodyPoints.Add(vertex);
401  break;
402  }
403  }
404 
405  for (int i = 0; i < bodyPoints.Count; i++)
406  {
407  cell.BodyVertices.Add(bodyPoints[i]);
408  bodyPoints[i] = ConvertUnits.ToSimUnits(bodyPoints[i]);
409  }
410 
411  if (cell.CellType == CellType.Empty) { continue; }
412 
413  cellBody.UserData = cell;
414  var triangles = MathUtils.TriangulateConvexHull(bodyPoints, ConvertUnits.ToSimUnits(center));
415 
416  for (int i = 0; i < triangles.Count; i++)
417  {
418  //don't create a triangle if the area of the triangle is too small
419  //(apparently Farseer doesn't like polygons with a very small area, see Shape.ComputeProperties)
420  Vector2 a = triangles[i][0];
421  Vector2 b = triangles[i][1];
422  Vector2 c = triangles[i][2];
423  float area = Math.Abs(a.X * (b.Y - c.Y) + b.X * (c.Y - a.Y) + c.X * (a.Y - b.Y)) / 2.0f;
424  if (area < 1.0f) { continue; }
425 
426  Vertices bodyVertices = new Vertices(triangles[i]);
427  PolygonShape polygon = new PolygonShape(bodyVertices, 5.0f);
428  Fixture fixture = new Fixture(polygon,
429  Physics.CollisionLevel,
430  Physics.CollisionAll)
431  {
432  UserData = cell
433  };
434  cellBody.Add(fixture, resetMassData: false);
435 
436  if (fixture.Shape.MassData.Area < FarseerPhysics.Settings.Epsilon)
437  {
438  DebugConsole.ThrowError("Invalid triangle created by CaveGenerator (" + triangles[i][0] + ", " + triangles[i][1] + ", " + triangles[i][2] + ")");
439  GameAnalyticsManager.AddErrorEventOnce(
440  "CaveGenerator.GeneratePolygons:InvalidTriangle",
441  GameAnalyticsManager.ErrorSeverity.Warning,
442  "Invalid triangle created by CaveGenerator (" + triangles[i][0] + ", " + triangles[i][1] + ", " + triangles[i][2] + "). Seed: " + level.Seed);
443  }
444  }
445  cell.Body = cellBody;
446  }
447  cellBody.ResetMassData();
448 
449  return cellBody;
450  }
451 
452  public static List<Vector2> CreateRandomChunk(float radius, int vertexCount, float radiusVariance)
453  {
454  return CreateRandomChunk(radius * 2, radius * 2, vertexCount, radiusVariance);
455  }
456 
457  public static List<Vector2> CreateRandomChunk(float width, float height, int vertexCount, float radiusVariance)
458  {
459  Debug.Assert(radiusVariance < Math.Min(width, height));
460  Debug.Assert(vertexCount >= 3);
461 
462  List<Vector2> verts = new List<Vector2>();
463  float angleStep = MathHelper.TwoPi / vertexCount;
464  float angle = 0.0f;
465  for (int i = 0; i < vertexCount; i++)
466  {
467  Vector2 dir = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle));
468  verts.Add(new Vector2(dir.X * width / 2, dir.Y * height / 2) + dir * Rand.Range(-radiusVariance, radiusVariance, Rand.RandSync.ServerAndClient));
469  angle += angleStep;
470  }
471  return verts;
472  }
473 
474  }
475 }
VoronoiCell AdjacentCell(VoronoiCell cell)
Vector2 GetNormal(VoronoiCell cell)
Returns the normal of the edge that points outwards from the specified cell
DoubleVector2 Coord
List< GraphEdge > Edges
List< Vector2 > BodyVertices