/// <summary> /// Spawn things along an edge /// </summary> /// <param name="d"></param> /// <param name="edge"></param> /// <param name="thing"></param> private static void spawnThingOnEdge(Dungeoneer d, Edge edge, GameObject thing, float thingLength) { int thingsToSpawn = Mathf.CeilToInt(edge.length / thingLength); float lerpDistance = 1 / (float)thingsToSpawn; float lerp = 0; Vector3 p = edge.p.toVector3AtHeight(0); Vector3 q = edge.q.toVector3AtHeight(0); Vector3 perpV = Vector3.zero; for (int i = 0; i < thingsToSpawn; ++i) { lerp += lerpDistance; Vector3 where = Vector3.Lerp(p, q, lerp); GameObject gro = UnityEngine.Object.Instantiate(thing, where, thing.transform.rotation); // The last segment is placed too close to "q," so it ends up // not getting rotated. Face the center (0,0,0) instead. if (Vector3.Distance(q, where) > 1e-1) { perpV = Vector3.Cross(q - where, Vector3.up).normalized; } gro.transform.LookAt(where + perpV); gro.transform.Rotate(0, 90, 0); // It still might not get rotated if the wall segment is 1 tile long if (thingsToSpawn == 1) { gro.transform.LookAt(Vector3.zero); } gro.transform.parent = d.thingOrganizer.transform; } }
/// <summary> /// Stand up trigger zones on the bounding box of the world for /// enemy movement AI to detect using raycasts. Also stands up /// walls to prevent the player from falling off of the world. /// </summary> private static void constructEnemyAvoidanceBoundingBox(Dungeoneer d) { GameObject trigger = GameObject.Find("EnemyAvoidanceTrigger"); GameObject floor = GameObject.FindGameObjectWithTag("Ground"); Bounds floorBounds = floor.GetComponent <Renderer>().bounds; Vector3 topLeft = new Vector3(0, 0, floorBounds.extents.z * 2); Vector3 topRight = new Vector3(floorBounds.extents.x * 2, 0, floorBounds.extents.z * 2); Vector3 bottomLeft = new Vector3(0, 0, 0); // Update the dungeoneer object with info about the min/max reachable points. These points are implied by the bounds of the floor. d.topRightWallCorner = floorBounds.max; d.bottomLeftWallCorner = floorBounds.min; float xlen = Vector3.Distance(topLeft, topRight); float zlen = Vector3.Distance(topLeft, bottomLeft); Vector3 west = new Vector3(floorBounds.min.x, d.currentDungeonType.wallHeight / 2, d.currentDungeonType.height / 2); Vector3 north = new Vector3(d.currentDungeonType.width / 2, d.currentDungeonType.wallHeight / 2, floorBounds.max.z); Vector3 east = new Vector3(floorBounds.max.x, d.currentDungeonType.wallHeight / 2, d.currentDungeonType.height / 2); Vector3 south = new Vector3(d.currentDungeonType.width / 2, d.currentDungeonType.wallHeight / 2, floorBounds.min.z); setAvoidanceBoxOnSide(trigger, west, 1, d.currentDungeonType.wallHeight, zlen, d.currentDungeonType.wall.prefab); setAvoidanceBoxOnSide(trigger, north, xlen, d.currentDungeonType.wallHeight, 1, d.currentDungeonType.wall.prefab); setAvoidanceBoxOnSide(trigger, east, 1, d.currentDungeonType.wallHeight, zlen, d.currentDungeonType.wall.prefab); setAvoidanceBoxOnSide(trigger, south, xlen, d.currentDungeonType.wallHeight, 1, d.currentDungeonType.wall.prefab); // The original one isn't used, but it stays in the middle of the map, so destroy it to prevent weirdness on the AI UnityEngine.Object.Destroy(trigger.gameObject); }
/// <summary> /// Make walls along an arbitrary edge /// </summary> /// <param name="wall"></param> private static void constructWallOnEdge(Dungeoneer d, Edge wall) { // Since the prefab is stretched equally along x and y, it must be placed at the center for both x and y Vector3 p = wall.p.toVector3AtHeight(d.currentDungeonType.wallHeight / 2); Vector3 q = wall.q.toVector3AtHeight(d.currentDungeonType.wallHeight / 2); Vector3 wallCenter = (p + q) / 2; GameObject wallObject = UnityEngine.Object.Instantiate(d.currentDungeonType.wall.prefab, wallCenter, d.currentDungeonType.wall.prefab.transform.rotation); wallObject.transform.parent = d.wallOrganizer.transform; wall.wallObjects.Add(wallObject); // Stretch wall wallObject.transform.localScale = new Vector3( wall.length, // length of the original edge wallObject.transform.localScale.y * d.currentDungeonType.wallHeight, // desired wall height wallObject.transform.localScale.z // original prefab thickness ); // Rotate the wall so that it's placed along the original edge Vector3 lookatme = Vector3.Cross(q - wallCenter, Vector3.up).normalized; wallObject.transform.LookAt(wallCenter + lookatme); // Attach info to game object for later use wallObject.GetComponent <WallInfo>().associatedEdge = wall; wall.isWall = true; /* * Vector3 direction = (q-p).normalized; #if UNITY_EDITOR * Debug.DrawRay(p, direction * wall.length, Color.white, 512); #endif */ }
/// <summary> /// Make walls only on Voronoi rooms /// </summary> /// <param name="d"></param> private static void constructWallsOnRooms(Dungeoneer d) { foreach (VoronoiRoom r in d.roomVoronoi) { constructWallsOnPolygon(d, r.exterior); } }
/// <summary> /// Make walls on all edges of a polygon /// </summary> /// <param name="d"></param> /// <param name="p"></param> private static void constructWallsOnPolygon(Dungeoneer d, Polygon p) { foreach (Edge e in p.sides) { constructWallOnEdge(d, e); } }
public static void makeEscapePoint(Dungeoneer d) { Debug.Log("Placing Escape portal"); int escapeSite = makePortal(d, d.currentDungeonType.returnPrefab); d.validCells[escapeSite].spawnedObject.name = "Escape Point"; }
public static void makeDelvePoint(Dungeoneer d) { Debug.Log("Placing Delve portal"); int delveSite = makePortal(d, d.currentDungeonType.delvePrefab); d.validCells[delveSite].spawnedObject.name = "Delve Point"; }
/// <summary> /// Generate the selected prefab along the specified graph's edges /// </summary> /// <param name="d">reference to Dungeoneer</param> /// <param name="edges">list of edges to spawn things on</param> /// <param name="thing">thing to spawn</param> public static void generateOnEdges(Dungeoneer d, List <Edge> edges, GameObject thing) { float thingLength = thing.GetComponentInChildren <Renderer>().bounds.size.x; foreach (Edge e in edges) { spawnThingOnEdge(d, e, thing, thingLength); } }
/// <summary> /// Calls all functions to create walls in a dungeon /// </summary> /// <param name="d"></param> public static void setWalls(Dungeoneer d) { constructWallsOnVoronoi(d); constructWallsOnRooms(d); List <GameObject> wallsToKeep = destroyWallsForCorridors(d); destroyLagWalls(d, wallsToKeep); constructEnemyAvoidanceBoundingBox(d); }
/// <summary> /// Generate walls all over the Voronoi, except for room interiors. Used /// to do subtractive corridor generation. /// </summary> /// <param name="d"></param> private static void constructWallsOnVoronoi(Dungeoneer d) { List <Cell> roomCells = d.roomVoronoi.SelectMany(r => r.cells).ToList(); foreach (Cell c in d.vd.cells) { if (!roomCells.Contains(c)) { constructWallsOnPolygon(d, c); } } }
/// <summary> /// Main function to generate walls and update pathfinding graphs. /// The order of functions is important! Some variables are set in /// certain functions, and other conditions check for the existence /// of walls under the expectation that they are (or are not) there. /// </summary> /// <param name="d">dungeoneer object</param> /// <returns>list of cells that may be used for A* pathfinding</returns> public static List <Cell> setWalls(Dungeoneer d) { // Create walls everywhere, except for the rooms. This also // excludes the exteriors of the rooms. constructWallsOnVoronoi(d); // Create walls on the exteriors of the rooms. constructWallsOnRooms(d); // Eliminate walls where corridors should exist. List <GameObject> wallsToKeep = destroyWallsForCorridors(d); // Bounding box needs to be set up before reachableCells. // This function sets the min/max vectors on the dungeoneer // object that reachableCells needs to check. constructEnemyAvoidanceBoundingBox(d); // Enumerate reachable cells for A* pathfinding. // Note: Close-range pathfinding does not take reachable cells into // account. // A cell is only "A*-reachable" under the following conditions: // 1. At least one of its sides is not a wall // 2. None of its vertices exist outside the bounds of the walls // The bounds are tracked as 3D vertices, so the 2D cell vertex's y // must be compared to the 3D corner's z. List <Cell> reachableCells = d.vd.cells.Where(c => { c.reachable = ( !c.sides.All(s => s.isWall) && !c.vertices.Any(v => v.x <d.bottomLeftWallCorner.x || v.x> d.topRightWallCorner.x || v.y <d.bottomLeftWallCorner.z || v.y> d.topRightWallCorner.z) ); return(c.reachable); }).ToList(); // Extraneous walls must be destroyed *after* reachableCells are // determined. Otherwise, the cleared area is considered reachable. destroyLagWalls(d, wallsToKeep); return(reachableCells); }
/// <summary> /// Places floor tiles under a polygon. /// Should only be used with convex polygons! /// </summary> /// <param name="d"></param> /// <param name="p"></param> public static void constructFloorUnderPolygon(Dungeoneer d, Polygon p) { Vector2 center = new Vector2(d.currentDungeonType.width / 2, d.currentDungeonType.height / 2); float actualWidth = d.currentDungeonType.width * 1.1f; float actualHeight = d.currentDungeonType.width * 1.1f; // fudge factor on ground cube y to make it line up more nicely GameObject flo = stretchCube(d.currentDungeonType.ground.prefab, actualWidth, actualHeight, -0.2f, center); flo.transform.parent = d.organizer.transform; flo.name = "Ground"; // add optional ceiling if (d.currentDungeonType.ceiling != null) { GameObject ceiling = stretchCube(d.currentDungeonType.ceiling.prefab, actualWidth, actualHeight, d.currentDungeonType.wallHeight, center); ceiling.transform.parent = d.organizer.transform; ceiling.name = "Ceiling"; } }
public static void setFloor(Dungeoneer d) { Debug.Log("Spawning floor"); IEnumerable <Vertex> vs = d.roomVoronoi.SelectMany(r => r.vertices); float mix = vs.Min(v => v.x); float max = vs.Max(v => v.x); float miy = vs.Min(v => v.y); float may = vs.Max(v => v.y); Polygon floorever = new Polygon( new List <Vertex> { new Vertex(mix, miy), new Vertex(mix, may), new Vertex(max, may), new Vertex(max, miy) } ); // already a bounding box because we just made a bounding box constructFloorUnderPolygon(d, floorever); }
public static int makePortal(Dungeoneer d, GameObject portal) { int portalSite = d.mt.Next(d.validCells.Count - 1); Cell chosenCell = d.validCells[portalSite]; // Remove any currently spawned objects here if (chosenCell.spawnedObject != null) { UnityEngine.Object.Destroy(chosenCell.spawnedObject); } Vector3 where = new Vector3(chosenCell.site.x, 0, chosenCell.site.y); chosenCell.spawnedObject = UnityEngine.Object.Instantiate(portal, where, portal.transform.rotation); // Randomly rotate about the y-axis float rotation = d.mt.Next(360); chosenCell.spawnedObject.transform.Rotate(0, rotation, 0); chosenCell.hasPortal = true; Bounds bounds = chosenCell.spawnedObject.GetComponentInChildren <Renderer>().bounds; // Try to respawn it if it's clipping // For some reason, Physics.Check[Primitive] is always true... Collider[] wtf = Physics.OverlapBox(where, bounds.extents, chosenCell.spawnedObject.transform.rotation, layersToAvoid); foreach (Collider col in wtf) { if (col.gameObject.layer == LayerMask.NameToLayer("Wall")) { Debug.Log($"Need to retry portal: object was {col.gameObject} on layer {LayerMask.LayerToName(col.gameObject.layer)}"); return(makePortal(d, portal)); } ObjectInfo oi = col.gameObject.GetComponent <ObjectInfo>(); if (oi != null) { oi.toBeDestroyed = true; } } return(portalSite); }
/// <summary> /// Remove walls that don't need to exist for performance reasons /// </summary> /// <param name="d">dungeoneer object</param> /// <param name="wallsToKeep">list of walls to avoid destroying</param> private static void destroyLagWalls(Dungeoneer d, List <GameObject> wallsToKeep) { foreach (VoronoiRoom r in d.roomVoronoi) { foreach (Edge e in r.exterior.sides) { wallsToKeep.AddRange(e.wallObjects); } } foreach (GameObject w in GameObject.FindGameObjectsWithTag("Wall")) { if (w != null && !wallsToKeep.Contains(w)) { WallInfo wi = w.GetComponent <WallInfo>(); if (wi != null) { wi.associatedEdge.isWall = false; } w.name = $"Destroyed {w.name}"; UnityEngine.Object.Destroy(w); } } }
/// <summary> /// Destroys wall objects where corridors should exist /// </summary> /// <param name="d"></param> /// <returns>list of walls that are valid</returns> private static List <GameObject> destroyWallsForCorridors(Dungeoneer d) { LayerMask wallMask = 1 << LayerMask.NameToLayer("Wall"); List <GameObject> wallsToKeep = new List <GameObject>(); // Need to make sure we can fit through these ones List <Edge> shortDestroyedWalls = new List <Edge>(); foreach (Edge e in d.spanningTree) { float len = e.length; Vector3 p = e.p.toVector3AtHeight(1); Vector3 q = e.q.toVector3AtHeight(1); Vector3 direction = (q - p).normalized; // uncomment to debug raycasts /* #if UNITY_EDITOR * Debug.DrawRay(p, direction * len, Color.blue, 512); #endif */ // Cast a ray to destroy initial set of walls. SphereCast does // not always catch all walls close to the sphere center List <RaycastHit> hits = new List <RaycastHit>(Physics.CapsuleCastAll(p, p + Vector3.up * d.currentDungeonType.wallHeight, d.currentDungeonType.hallWidth, direction, len, wallMask)); foreach (RaycastHit hit in hits) { Edge er = hit.collider.gameObject.GetComponent <WallInfo>().associatedEdge; er.isWall = false; if (er.length < FATTEST_CONTROLLER * 2) { shortDestroyedWalls.Add(er); } hit.collider.gameObject.name = $"Destroyed {hit.collider.gameObject.name}"; UnityEngine.Object.Destroy(hit.collider.gameObject); } // Cast a wider sphere to determine what walls to keep during cleanup foreach (RaycastHit hit in Physics.SphereCastAll(p, d.currentDungeonType.hallWidth * 4, direction, len, wallMask)) { wallsToKeep.Add(hit.collider.gameObject); } } // Sometimes, only a really short wall is removed, so the player still can't fit. Probably a Unity spherecast bug foreach (Edge w in shortDestroyedWalls) { int pwalls = w.p.incidentEdges.Count(e => e.isWall); int qwalls = w.q.incidentEdges.Count(e => e.isWall); float minp = (pwalls > 0)? w.p.incidentEdges.Where(e => e.isWall).Min(e => e.length) : 0; float minq = (qwalls > 0)? w.q.incidentEdges.Where(e => e.isWall).Min(e => e.length) : 0; Vertex v; // knock out the least-connected side if (pwalls < qwalls) { v = w.p; } else if (pwalls == qwalls) { if (pwalls == 0) // don't do anything if neither have a wall to remove... { continue; } v = (minp > minq)? w.q : w.p; } else { v = w.q; } // remove the shortest edge // float equality is dumb, so this is ugly Edge shortestEdge = null; float s = Mathf.Infinity; foreach (Edge hurr in v.incidentEdges) { if (hurr.isWall && hurr.length < s) { shortestEdge = hurr; s = hurr.length; } } if (shortestEdge == null) // why does this happen?! { continue; } shortestEdge.isWall = false; foreach (GameObject wo in shortestEdge.wallObjects) { wallsToKeep.Remove(wo); UnityEngine.Object.Destroy(wo); } } return(wallsToKeep); }