/// <summary> /// Helper function for creating a Node for an entrace but also checking that it hasn't already been /// created, and if it has, just using that one instead to avoid multiple Nodes on top of the same /// tile. /// </summary> /// <param name="cluster">The custer which this node lives in.</param> /// <param name="tile">The tile which this node wraps.</param> /// <param name="node"> /// If a node already exists over <paramref name="tile"/> this will be that GraphNode. If not it will /// be a newly created GraphNode. /// </param> /// <returns>True if a new GraphNode was created.</returns> private Boolean CreateEntraceNode(Cluster cluster, Level.Tile tile, out GraphNode node) { Boolean created = false; // First see if this Tile is already managed by this cluster. If it is, it will return us // the node which contains it. node = cluster.GetNodeContaining(tile); // If the node isn't already being managed, we need to create a new one. if (null == node) { node = mNodeFactory.GetNode(); node.pData = tile; created = true; // New nodes need to be registers with the Graph. AddNode(node); } return created; }
/// <summary> /// Helper function to do the work needed to create a single new extrance/exit based on the current tile being walked, /// and the Tile at the start of the sequence. /// </summary> /// <param name="cluster">The cluster we are evaluating. Hitting a Tile outside this cluster will end that wall.</param> /// <param name="tile">The current tile to evaluate.</param> /// <param name="sequeceStart">The tile which started this sequence. Use null to start.</param> /// <param name="dirWalk">The direction to walk along the wall.</param> /// <param name="dirCheck">The direction of the neighbouring Cluster to check. It isn't enough to check outselves; the neighbour may be blocking travel.</param> /// <param name="dirNeighbourCluster">Same as dirCheck but using the enum that Clusters understand.</param> /// <param name="removeSelf">True if the tile that triggered this call should not be included in the sequence. Useful if this sequence ended because you hit a wall.</param> private void CreateEntrance(Cluster cluster, Level.Tile tile, ref Level.Tile sequenceStart, Level.Tile.AdjacentTileDir dirWalk, Level.Tile.AdjacentTileDir dirCheck, Cluster.AdjacentClusterDirections dirNeighbourCluster, Boolean removeSelf) { // Find the center point between the tile at the start of the sequence of enpty tiles // and the current tile. Vector2 sequenceVector = tile.mCollisionRect.pCenterPoint - sequenceStart.mCollisionRect.pCenterPoint; // If we enter this block by hitting a wall, we need to remove that a Tile length from our // calculations since that wall is not part of the entrance/exit. if (removeSelf) { sequenceVector -= Vector2.Normalize(sequenceVector) * new Vector2(cluster.pTileDimensions.X, cluster.pTileDimensions.Y); } // If the sequence is long enough, instead of putting a GraphNode in the center, create 2 GraphNode objects, // and place them at opposite ends of the Sequence. This is recommended by the original HPA* white paper. if (sequenceVector.LengthSquared() >= (mClusterSize * mGetMapInfoMsg.mInfo_Out.mMapWidth * 0.5f)) { // Add the length of the Sequence to the starting point to get our ending position. Vector2 end = (sequenceVector) + sequenceStart.mCollisionRect.pCenterPoint; // We need to find the tile at that position because our GraphNode depends on that data. mGetTileAtPositionMsg.mPosition_In = end; WorldManager.pInstance.pCurrentLevel.OnMessage(mGetTileAtPositionMsg); System.Diagnostics.Debug.Assert(null != mGetTileAtPositionMsg.mTile_Out, "Unable to find tile."); CreateEntranceNodes( cluster, cluster.pNeighbouringClusters[(Int32)dirNeighbourCluster], mGetTileAtPositionMsg.mTile_Out, mGetTileAtPositionMsg.mTile_Out.mAdjecentTiles[(Int32)dirCheck]); CreateEntranceNodes( cluster, cluster.pNeighbouringClusters[(Int32)dirNeighbourCluster], sequenceStart, sequenceStart.mAdjecentTiles[(Int32)dirCheck]); } else { // Add half the length in order to put us in the middle of the sequence. Vector2 middle = (sequenceVector * 0.5f) + sequenceStart.mCollisionRect.pCenterPoint; // We need to find the tile at that position because our GraphNode depends on that data. mGetTileAtPositionMsg.mPosition_In = middle; WorldManager.pInstance.pCurrentLevel.OnMessage(mGetTileAtPositionMsg); System.Diagnostics.Debug.Assert(null != mGetTileAtPositionMsg.mTile_Out, "Unable to find tile."); CreateEntranceNodes( cluster, cluster.pNeighbouringClusters[(Int32)dirNeighbourCluster], mGetTileAtPositionMsg.mTile_Out, mGetTileAtPositionMsg.mTile_Out.mAdjecentTiles[(Int32)dirCheck]); } // Start a new sequence. sequenceStart = null; }
/// <summary> /// Helper function for creating 2 linked GraphNodes in neighnbouring clusters. /// </summary> /// <param name="localCluster">The cluster currently being evaluated.</param> /// <param name="otherCluster">The neighbouring cluster of <paramref name="localCluster"/>.</param> /// <param name="localTile">The Tile on which the new entrance sits on top of.</param> /// <param name="otherTile">The Tile on wihich the other new entrace sits on top of.</param> private void CreateEntranceNodes(Cluster localCluster, Cluster otherCluster, Level.Tile localTile, Level.Tile otherTile) { GraphNode localNode; GraphNode otherNode; // Create the new nodes with the appropriate Tile data. Boolean localCreated = CreateEntraceNode(localCluster, localTile, out localNode); Boolean otherCreated = CreateEntraceNode(otherCluster, otherTile, out otherNode); // Link the two nodes together creating an Intra-Connection. localNode.AddNeighbour(otherNode); otherNode.AddNeighbour(localNode); // Add the nodes the appropriate Cluster objects. if (localCreated) { localCluster.AddNode(localNode); } if (otherCreated) { otherCluster.AddNode(otherNode); } }
/// <summary> /// Constrctor. /// </summary> /// <param name="solid">The direction of the solid tile relative to the tile that would be a doorway.</param> /// <param name="empty">The direction of the emoty tile relative to teh tile that would be a doorway.</param> public DoorwayConfiguration(Level.Tile.AdjacentTileDir solid, Level.Tile.AdjacentTileDir empty) { mSolid = solid; mEmpty = empty; }
/// <summary> /// Recursive algorithm for finding all entraces/exits in a cluster, and creating GraphNodes at those points /// in neighbouring Clusters and linking them together. Has logic to only create on entrance/exit per concurent /// set of empty tiles. /// </summary> /// <param name="cluster">The cluster we are evaluating. Hitting a Tile outside this cluster will end that wall.</param> /// <param name="tile">The current tile to evaluate.</param> /// <param name="sequeceStart">The tile which started this sequence. Use null to start.</param> /// <param name="dirWalk">The direction to walk along the wall.</param> /// <param name="dirCheck">The direction of the neighbouring Cluster to check. It isn't enough to check outselves; the neighbour may be blocking travel.</param> /// <param name="dirNeighbourCluster">Same as dirCheck but using the enum that Clusters understand.</param> /// <returns>The last tile visited on this wall. Useful for walking an entire perimeter.</returns> private Level.Tile WalkWall(Cluster cluster, Level.Tile tile, Level.Tile sequeceStart, Level.Tile.AdjacentTileDir dirWalk, Level.Tile.AdjacentTileDir dirCheck, Cluster.AdjacentClusterDirections dirNeighbourCluster) { // Get the Tile in the neighbouring Cluster. It being solid creates a wall just the same as // if the tile in this Cluster is solid. Level.Tile adj = tile.mAdjecentTiles[(Int32)dirCheck]; Boolean entraceMade = false; if (null != adj) { // If we don't yet have a sequence start point, and this tile is an entrace, // it becomes the new sequence start. if (null == sequeceStart && tile.mType == Level.Tile.TileTypes.Empty && adj.mType == Level.Tile.TileTypes.Empty) { sequeceStart = tile; } // The sequence has started already and we just hit a wall. Time to create an entrance in the // center of this sequence. else if (null != sequeceStart && (tile.mType != Level.Tile.TileTypes.Empty || adj.mType != Level.Tile.TileTypes.Empty)) { CreateEntrance(cluster, tile, ref sequeceStart, dirWalk, dirCheck, dirNeighbourCluster, true); entraceMade = true; } } // Walk to the next Tile. adj = tile.mAdjecentTiles[(Int32)dirWalk]; // Are we still in the Cluster/Level? if (null != adj && cluster.IsInBounds(adj)) { // Recursivly visit the next Tile. return WalkWall(cluster, adj, sequeceStart, dirWalk, dirCheck, dirNeighbourCluster); } else { // We have left either the map or the Cluster. Either way that is considered an end to // the current sequence, should one be in progress. if (null != sequeceStart) { System.Diagnostics.Debug.Assert(!entraceMade, "Entrance made twice."); CreateEntrance(cluster, tile, ref sequeceStart, dirWalk, dirCheck, dirNeighbourCluster, false); } return tile; } }
/// <summary> /// Does a kind of flood fill of game objects inside a structure in a way that the user /// would expect, meaning 1 tile opening are treating like doors, which can connect rooms /// or be an exit to the outside world. /// </summary> /// <param name="firstTile">The Tile to start the fill at.</param> /// <param name="maxFillCount"> /// The maximum number of tiles to search before considering /// the SafeHouse filled. Any rooms to completely changed when this cap is hit will not /// be filled. /// </param> /// <param name="gameObjectTemplateName">The name of the GameObject which will be flood filled.</param> /// <returns>True if at least one room was filled.</returns> public Boolean FloodFill(Level.Tile firstTile, UInt32 maxFillCount, String gameObjectTemplateName) { // Tracks whether or not any rooms were filled. Boolean success = false; // Store for use later. mGameObjectTemplateName = gameObjectTemplateName; // A new fill means any currently running should stop. mTilesToChangeFinal.Clear(); // This algorithm works by breaking the world into rooms. A room is an area surrounded // by wall, with the one cavet that spaces 1 pixel wide are allowed when moving from // one room to another, but are considered a wall when exiting the entire structure. // Filling is done on a per room basis. If we reach maxFillCount before a room is filled, // none of the room gets filled in. However, any rooms previously filled in stay, and // rooms still in the queue can potentially be filled if they are smaller than the one // that failed. mRoomStarters.Enqueue(firstTile); // Keep track of how many tiles have been changed overall in order to make sure // we don't go over maxFillCount over all. Int32 curFillCount = 0; // Go room by room trying to cover it in the desired object. while (mRoomStarters.Count > 0 && (curFillCount <= maxFillCount)) { // The starting tile is the tile in the roomStarters Queue. Level.Tile startTile = mRoomStarters.Dequeue(); // Clear out any data that might be sitting around from the previous room. mTilesToCheck.Clear(); mTilesToChange.Clear(); mRoomStartersToAdd.Clear(); // There is a chance we were given a Tile that is null or not empty. In those cases // we just want to skip over them. It was Dequeued from roomStarters so it will be // forgotten. if (null != startTile && startTile.mType == Level.Tile.TileTypes.Empty) { // Start the tile travesal with the roomStarter. mTilesToCheck.Enqueue(startTile); mProcessedTiles.Add(startTile); } // Breath first search through the tile map stopping at walls and doors, or at a point // where we have visiting more tiles than allowed by maxFillCount. while ((mTilesToCheck.Count) > 0 && (mTilesToChange.Count + curFillCount <= maxFillCount)) { // Grab the next Tile, or in the case of the first interation it will be a roomStarter. Level.Tile currentTile = mTilesToCheck.Dequeue(); // Safety check in case we got sent a bad starting tile. if (null == currentTile || currentTile.mType != Level.Tile.TileTypes.Empty) { continue; } // Should this room be completed, this tile should be changed. mTilesToChange.Add(currentTile); // Loop through all the surrounding tiles adding the appropriate ones to the // tiles Queue. for (UInt32 tileIndex = (UInt32)Level.Tile.AdjacentTileDir.START_HORZ; (tileIndex < (UInt32)Level.Tile.AdjacentTileDir.NUM_DIRECTIONS); tileIndex += 1) { Level.Tile nextTile = currentTile.mAdjecentTiles[tileIndex]; if (null != nextTile && nextTile.mType == Level.Tile.TileTypes.Empty && // Safety check false == mProcessedTiles.Contains(nextTile) && // Don't check the same tile more than once. !Level.IsAttemptingInvalidDiagonalMove((Level.Tile.AdjacentTileDir)tileIndex, currentTile)) // Don't clip through diagonal walls. { if (IsDoorway(nextTile)) { // If this tile is actually a doorway, it becomes the start of a new room. mRoomStartersToAdd.Add(nextTile); } else { // This is just a regular tile, so it should be changed, should this room be completed. mTilesToCheck.Enqueue(nextTile); } // This tile has been processed so it should not be checked again. mProcessedTiles.Add(nextTile); } } } // The breath first search has completed. If it exusted all tiles, then the room is considered // completed, and we can start actually changing tiles and adding the connected rooms. if (mTilesToCheck.Count <= 0) { // Once any room gets filled it is considered a success. success = true; // This is a legit change now, so count it towards to total fill count. curFillCount += mTilesToChange.Count; mTilesToChangeFinal.AddRange(mTilesToChange); // Any new rooms that were queued up can now be safely added as the room that // linked to them was completed. for (Int32 i = 0; i < mRoomStartersToAdd.Count; i++) { mRoomStarters.Enqueue(mRoomStartersToAdd[i]); } } } // Don't want to be hanging onto this data. mProcessedTiles.Clear(); mRoomStarters.Clear(); mTilesToCheck.Clear(); mTilesToChange.Clear(); mRoomStartersToAdd.Clear(); return success; }
/// <summary> /// A somewhat complex check to determine if a tile is what would be considered a doorway. /// A doorway is an empty tile, surrounded by 2 or more empty tiles on opposite sides. However, /// it is not quite as simple as that, since some combination block what would be the room after /// the door way, so we also need to verify that for a given combination of walls, there is a /// another empty tile at a particular location. /// </summary> /// <param name="tile"></param> /// <returns></returns> private Boolean IsDoorway(Level.Tile tile) { // Build this mapping of walls and empty spaces required to make up a doorway. /// <todo>Do this once.</todo> List<DoorwayConfiguration> [] relaventSiblings = BuildDoorwayConfigurationTable(); // We need to loop through every surround tile and see if they are walls. If they ar walls, // then we start checking the relaventSiblings for the required corrisponding walls and empty tiles // which mean this tile is a doorway. for (UInt32 tileIndex = (UInt32)Level.Tile.AdjacentTileDir.START_HORZ; (tileIndex < (UInt32)Level.Tile.AdjacentTileDir.NUM_DIRECTIONS); tileIndex++) { // Grab the next tile surrounding the one we are checking. Level.Tile siblingTile = tile.mAdjecentTiles[tileIndex]; // None of this matters if the adjacent tile is empty. if (null != siblingTile && siblingTile.mType != Level.Tile.TileTypes.Empty) { // Every surrounding tile has a list of corrisponding tiles that when in the proper // configuration mean that the center tile is a doorway. Loop through those conigurations. for (Int32 i = 0; i < relaventSiblings[tileIndex].Count; i++) { // When siblingTile is Solid, another tile on the opposite side also needs to be solid. Level.Tile adjTileSolid = tile.mAdjecentTiles[(Int32)relaventSiblings[tileIndex][i].mSolid]; // Is a relavent sibling solid? if (null != adjTileSolid && adjTileSolid.mType != Level.Tile.TileTypes.Empty) { // But it isn't enough to have solid tiles on opposite sides of the center tile. // There also needs to be an empty tile in the right direction or else this is // just a dead end and should be considered part of the current room, not the // start of a new one. Level.Tile adjEmpty = tile.mAdjecentTiles[(Int32)relaventSiblings[tileIndex][i].mEmpty]; // Is the relavent sibling Empty? if (null != adjEmpty && adjEmpty.mType == Level.Tile.TileTypes.Empty) { // Once this is a doorway in one case, nothing can change that; it can only // have additional paths, but that will be handled when this Tile becomes // the starting point of the next room. return true; } } } } } return false; }
/// <summary> /// Is a particular Tile already part of this Cluster, meaning that a GraphNode in this /// Cluster is wrapping that tile. If it is, then that GraphNode gets returned so that it /// can be reused. /// </summary> /// <param name="tile">The tile to check for.</param> /// <returns>The GraphNode in this Cluster which wraps the given Tile.</returns> public GraphNode GetNodeContaining(Level.Tile tile) { // Just loop through every GraphNode looking for one that is storing the Tile. for (Int32 i = 0; i < pNodes.Count; i++) { if (pNodes[i].pData as Level.Tile == tile) { if (!(pNodes[i] as NavMeshTileGraphNode).pIsTemporary) { return pNodes[i]; } } else if (pNodes[i].pPosition == tile.mCollisionRect.pCenterPoint) { System.Diagnostics.Debug.Assert(false, "Tile at same position but claiming to be different."); } } return null; }
/// <summary> /// Checks if a Tile is inside of this cluster (at all). /// </summary> /// <param name="tile">The Tile to check for.</param> /// <returns>True if tile is inside this cluster.</returns> public Boolean IsInBounds(Level.Tile tile) { return (mBounds.Intersects(tile.mCollisionRect)); }
/// <summary> /// When choosing a path we want to avoid clipping the edges of solid tiles. For example if you are /// going LEFT_DOWN, there should be no solid tile LEFT or DOWN or else the character would clip /// into them. /// It also avoids the problem where the path slips between kitty-cornered tiles. /// </summary> /// <param name="dir">The direction we want to move.</param> /// <param name="rootTile">The tile we are moving from.</param> /// <returns>True if this is an invalid move.</returns> static public Boolean IsAttemptingInvalidDiagonalMove(Level.Tile.AdjacentTileDir dir, Level.Tile rootTile) { switch ((Int32)dir) { // The path wants to move down and to the left... case (Int32)Level.Tile.AdjacentTileDir.LEFT_DOWN: { // But it should only do so if their are no solid tiles to the left and // no solid tiles below. If there are, it needs to find another way round. if (IsTileInDirectionSolid(Level.Tile.AdjacentTileDir.LEFT, rootTile) || IsTileInDirectionSolid(Level.Tile.AdjacentTileDir.DOWN, rootTile)) { return true; } break; } case (Int32)Level.Tile.AdjacentTileDir.LEFT_UP: { if (IsTileInDirectionSolid(Level.Tile.AdjacentTileDir.LEFT, rootTile) || IsTileInDirectionSolid(Level.Tile.AdjacentTileDir.UP, rootTile)) { return true; } break; } case (Int32)Level.Tile.AdjacentTileDir.RIGHT_DOWN: { if (IsTileInDirectionSolid(Level.Tile.AdjacentTileDir.RIGHT, rootTile) || IsTileInDirectionSolid(Level.Tile.AdjacentTileDir.DOWN, rootTile)) { return true; } break; } case (Int32)Level.Tile.AdjacentTileDir.RIGHT_UP: { if (IsTileInDirectionSolid(Level.Tile.AdjacentTileDir.RIGHT, rootTile) || IsTileInDirectionSolid(Level.Tile.AdjacentTileDir.UP, rootTile)) { return true; } break; } }; return false; }
/// <summary> /// Checks in an adjecent tile is solid. Safely avoids cases where their is no adjecent tile. /// </summary> /// <param name="dir">The direction to check in.</param> /// <param name="rootTile">The tile to move from.</param> /// <returns>True if the adjecent tile exists and is solid.</returns> static private Boolean IsTileInDirectionSolid(Level.Tile.AdjacentTileDir dir, Level.Tile rootTile) { return (rootTile.mAdjecentTiles[(Int32)dir] != null && rootTile.mAdjecentTiles[(Int32)dir].mType != Level.Tile.TileTypes.Empty); }
/// <summary> /// Constructor. /// </summary> /// <param name="tile">The Tile at this Node's location in the world.</param> public TileGraphNode(Level.Tile tile) : base() { mTile = tile; }