/// <summary> /// Clears out a cluster and regenerates all the nodes inside and any links to outside /// cluster GraphNode objects. /// </summary> /// <param name="pos">The position inside a cluster which should be regenerated.</param> public void RegenerateCluster(Vector2 pos) { // To start, find if and what cluster was invalidated. Cluster cluster = GetClusterAtPosition(pos); // Remove all the GraphNode objects from the Cluster and remove any links between GraphNodes // and adjacent Cluster objects. ClearCluster(cluster); Level.Tile nextStartPoint = cluster.pTopLeft; // With the Cluster cleared out, it is now required that entrances/exits be regenerated. nextStartPoint = WalkWall(cluster, nextStartPoint, null, Level.Tile.AdjacentTileDir.RIGHT, Level.Tile.AdjacentTileDir.UP, Cluster.AdjacentClusterDirections.Up); nextStartPoint = WalkWall(cluster, nextStartPoint, null, Level.Tile.AdjacentTileDir.DOWN, Level.Tile.AdjacentTileDir.RIGHT, Cluster.AdjacentClusterDirections.Right); nextStartPoint = WalkWall(cluster, nextStartPoint, null, Level.Tile.AdjacentTileDir.LEFT, Level.Tile.AdjacentTileDir.DOWN, Cluster.AdjacentClusterDirections.Down); nextStartPoint = WalkWall(cluster, nextStartPoint, null, Level.Tile.AdjacentTileDir.UP, Level.Tile.AdjacentTileDir.LEFT, Cluster.AdjacentClusterDirections.Left); // Link all the GraphNode within this cluster. LinkClusterGraphNodes(cluster); // Neighbouring Clusters may have had GraphNode added to them, and so they may require // new linkages to be set up. for (Int32 i = 0; i < cluster.pNeighbouringClusters.Length; i++) { LinkClusterGraphNodes(cluster.pNeighbouringClusters[i]); } }
//Builds a Level XML out of the layout of the level set up inside levelCanvas //and the given Level properties. public static void BuildLevelPrimitiveAndSaveLevel(GameObject levelCanvas, string levelName, int levelWidth, int levelHeight, float levelGravity, string levelBorder) { Level levelToSave = new Level(); levelToSave.levelName = levelName; levelToSave.levelWidth = levelWidth; levelToSave.levelHeight = levelHeight; levelToSave.levelGravity = levelGravity; levelToSave.levelBorder = levelBorder; levelToSave.tiles = new List <Level.Tile>(); foreach (Transform child in levelCanvas.transform) { if (child.tag == "Tiles") { Level.Tile tile = new Level.Tile(); //Truncates the ending (clone) from tiles tile.prefab = child.name.Split('(')[0]; tile.posX = child.transform.localPosition.x; //make function to turn into absolute position tile.posY = child.transform.localPosition.y; tile.rot = child.transform.rotation.eulerAngles.z; tile.scaleX = child.localScale.x; //make function to find absolute scale tile.scaleY = child.localScale.y; levelToSave.tiles.Add(tile); } } Debug.Log("Level contains " + levelToSave.tiles.Count + " tiles"); SaveLevel(levelToSave); }
/// <summary> /// Check if this GraphNode can be passed; eg. is it solid of empty? /// </summary> /// <param name="startingNode">The node we are travelling from.</param> /// <returns>True if if the node can be travelled to.</returns> public override Boolean IsPassable(GraphNode startingNode) { // If this GraphNode is not storing a tile, or that Tile is not empty, thats // an instant fail. if (!IsEmpty()) { return(false); } // We should not have mismatch GraphNode objects, so pData should be Level.Tile in this case. Level.Tile tile = startingNode.pData as Level.Tile; // Loop through all adjacent tiles to figure out which direction we are travelling in. // We need that data in order to determine if this is a legal diagonal move. for (Int32 i = 0; i < tile.mAdjecentTiles.Length; i++) { if (mTile == tile.mAdjecentTiles[i]) { if (Level.IsAttemptingInvalidDiagonalMove((Level.Tile.AdjacentTileDir)i, tile)) { return(false); } else { // No need to continue searching. break; } } } return(true); }
/// <summary> /// Update where this Planner is travelling from. The position is used to look up a tile at that /// position in the world. /// </summary> /// <param name="source">The position in the world that the planner will start at.</param> public void SetSource(Vector2 source) { // Grab the tile at the source position. mGetTileAtPositionMsg.mPosition_In = source; World.WorldManager.pInstance.pCurrentLevel.OnMessage(mGetTileAtPositionMsg); // We only care if they moved enough to make it onto a new tile. if (mSourceTile != mGetTileAtPositionMsg.mTile_Out) { // Update the source incase the GO has moved since the last update. mSource = source; // Update the source tile with the tile the GO has moved to. mSourceTile = mGetTileAtPositionMsg.mTile_Out; // Let the algorithm know that it needs to recalculate. // TODO: With only moving one tile at a time, could this be optimized to // check if this tile is already in the path, or append it to the start // of the path? mPathInvalidated = true; // Since the source has changed, this path can no longer be considered solved. mSolved = false; } }
/// <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> /// These nodes get used over and over again, so we need to make sure they /// get completely reset between uses. /// </summary> public void Reset() { mTile = null; mPrev = null; mCostFromStart = 0; mCostToEnd = 0; mReached = false; mPathSolved = false; }
/// <summary> /// Clear out the destination that the planner is trying to reach. This doubles as a way /// to stop the planner from trying to advance. /// </summary> public void ClearDestination() { mDestinationTile = null; mSolved = false; // Release the Nodes. We don't need them anymore. If a new destination // is set, we will need to start from scratch anyway. ClearNodeLists(); }
/// <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> /// 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> /// Update the location that the Planner is attempting to reach. The position will be used /// to look up the tile at that position in the world. /// </summary> /// <param name="destination"></param> public void SetDestination(Vector2 destination) { mGetTileAtPositionMsg.mPosition_In = destination; World.WorldManager.pInstance.pCurrentLevel.OnMessage(mGetTileAtPositionMsg); if (mDestinationTile != mGetTileAtPositionMsg.mTile_Out) { mDestination = destination; mPathInvalidated = true; mSolved = false; // We will need to do a couple checks to see if we have found the destination tile, so cache that // now. mDestinationTile = mGetTileAtPositionMsg.mTile_Out; } }
/// <summary> /// Once the Flood Fill has been started with a call to FloodFill, calling this function /// will do the actual flooding. /// </summary> public void ProcessFill() { // Go through all the tiles that we determined as value tiles to change, and change them. if (mTilesToChangeFinal.Count > 0) { Level.Tile tile = mTilesToChangeFinal[0]; // Create a new game object and place it at the location of the tile. GameObject newFloor = GameObjectFactory.pInstance.GetTemplate(mGameObjectTemplateName); newFloor.pPosition = tile.mCollisionRect.pCenterPoint; GameObjectManager.pInstance.Add(newFloor); mTilesToChangeFinal.RemoveAt(0); } }
/// <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> /// 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); }
private Color GetTileColor(Level.Tile tile) { switch (tile) { case Level.Tile.Path: return(new Color(0.827f, 0.827f, 0.827f)); // Light grey case Level.Tile.Buildable: return(Color.grey); case Level.Tile.Spawner: return(Color.red); case Level.Tile.Goal: return(Color.green); case Level.Tile.Empty: return(Color.black); default: return(Color.magenta); } }
/// <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> /// Does a search for a valid location to try to patrol to. /// </summary> private void FindNextRepairPoint() { // How far do we want to search for damaged walls? // This is in "tiles" not pixels. const int range = 16; const int range_half = range / 2; // Start by finding a center point for the search. For that we find a random // spot in the safe house. List <GameObject> safeHouses = GameObjectManager.pInstance.GetGameObjectsOfClassification(GameObjectDefinition.Classifications.SAFE_HOUSE); Int32 index = RandomManager.pInstance.pRand.Next(safeHouses.Count); // We are going to search for damaged walls in a square around that safehouse position. // We also need to convert it to world space. Vector2 startPos = safeHouses[index].pPosition - new Vector2(range_half * 8.0f, range_half * 8.0f); mDebugOriginPos = safeHouses[index].pPosition; mGetTileAtPositionMsg.mPosition_In = startPos; WorldManager.pInstance.pCurrentLevel.OnMessage(mGetTileAtPositionMsg, pParentGOH); // The top left most tile in our search. Level.Tile tile = mGetTileAtPositionMsg.mTile_Out; mDamagedTiles.Clear(); // Loop through the tiles surrounding the randomly chosen center point. for (Int32 y = 0; y < range && tile != null; y++) { // The tile a the start of this row. Level.Tile startTile = tile; for (Int32 x = 0; x < range && tile != null; x++) { // Is this a wall? if (tile.mType == Level.Tile.TileTypes.Solid) { // The level only stores collision info, but we need the actual wall object so that // we can check its health levels. mFoundTileObjects.Clear(); GameObjectManager.pInstance.GetGameObjectsInRange(tile.mCollisionRect.pCenterPoint, 1.0f, ref mFoundTileObjects, mTileClassifications); System.Diagnostics.Debug.Assert(mFoundTileObjects.Count == 1, "Unexpected number of objects at tile position: " + mFoundTileObjects.Count); // This should never happen. if (null != mFoundTileObjects[0]) { mFoundTileObjects[0].OnMessage(mGetHealthMsg, pParentGOH); // Is this wall damaged? if (mGetHealthMsg.mCurrentHealth_Out < mGetHealthMsg.mMaxHealth_Out) { // Add it to a list so that a random damaged wall can be chosen. mDamagedTiles.Add(tile); } } } // Move to the next tile. tile = tile.mAdjecentTiles[(Int32)Level.Tile.AdjacentTileDir.RIGHT]; } // Move down to the start of the next row. tile = startTile.mAdjecentTiles[(Int32)Level.Tile.AdjacentTileDir.DOWN]; } bool tileFound = false; // Keep goind until we find a valid tile, or all posibilites have been exhusted. while (mDamagedTiles.Count > 0 && !tileFound) { // Pick a random tile to try. Int32 damagedTileIndex = RandomManager.pInstance.pRand.Next(mDamagedTiles.Count); Level.Tile chosenTile = mDamagedTiles[damagedTileIndex]; // Remove it from the list so that it can't be considered again if it is not // chosen now. mDamagedTiles.RemoveAt(damagedTileIndex); // Loop through all surrounding tiles to see if any of them are empty. // A tile cannot be repaired if the character cannot stand next to it. for (UInt32 tileIndex = (UInt32)Level.Tile.AdjacentTileDir.START_HORZ; (tileIndex < (UInt32)Level.Tile.AdjacentTileDir.NUM_DIRECTIONS); tileIndex += 1) { // Is the tile next to the damaged one empty, giving us a place to stand? if (null != chosenTile.mAdjecentTiles[tileIndex] && chosenTile.mAdjecentTiles[tileIndex].mType == Level.Tile.TileTypes.Empty) { Level.Tile nearby = chosenTile.mAdjecentTiles[tileIndex]; // On the offset chance that the Follow Behaviour already has a target set. mSetTargetObjectMsg.mTarget_In = null; pParentGOH.OnMessage(mSetTargetObjectMsg); mSetDestinationMsg.mDestination_In = nearby.mCollisionRect.pCenterPoint; pParentGOH.OnMessage(mSetDestinationMsg); mSetSourceMsg.mSource_In = pParentGOH.pCollisionRect.pCenterPoint; pParentGOH.OnMessage(mSetSourceMsg); // Store for debug drawing. mDebugStart = pParentGOH.pPosition; mDebugEnd = nearby.mCollisionRect.pCenterPoint; mFoundTileObjects.Clear(); GameObjectManager.pInstance.GetGameObjectsInRange(chosenTile.mCollisionRect.pCenterPoint, 1.0f, ref mFoundTileObjects, mTileClassifications); System.Diagnostics.Debug.Assert(mFoundTileObjects.Count == 1, "Unexpected number of objects at tile position: " + mFoundTileObjects.Count); // This should never happen. if (null != mFoundTileObjects[0]) { mSetTileToRepairMsg.mTile_In = mFoundTileObjects[0]; pParentGOH.OnMessage(mSetTileToRepairMsg); } tileFound = true; } } } mNoValidTargets = !tileFound; }
/// <summary> /// Put the Node back into a default state. /// </summary> public override void Reset() { mTile = null; base.Reset(); }
/// <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; }
/// <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)); }
public override void OnInspectorGUI() { serializedObject.Update(); EditorGUILayout.PropertyField(m_PreviewSpriteProperty); EditorGUILayout.PropertyField(m_PathTileProperty); EditorGUILayout.PropertyField(m_BuildableTileProperty); EditorGUILayout.PropertyField(m_SpawnerTileProperty); EditorGUILayout.PropertyField(m_GoalTileProperty); EditorGUILayout.PropertyField(m_HeightProperty); EditorGUILayout.PropertyField(m_WidthProperty); if (m_HeightProperty.intValue != m_TargetLevel.m_Map.GetLength(0) || m_WidthProperty.intValue != m_TargetLevel.m_Map.GetLength(1)) { // Height or width changed, so provide a button to update the map if (GUILayout.Button("Update Map Dimensions")) { Level.Tile[,] newMap = new Level.Tile[m_HeightProperty.intValue, m_WidthProperty.intValue]; // Copy what can be copied from old map, and either discard excess or fill new space with empty tiles for (int row = 0; row < newMap.GetLength(0); row++) { for (int col = 0; col < newMap.GetLength(1); col++) { if (row < m_TargetLevel.m_Map.GetLength(0) && col < m_TargetLevel.m_Map.GetLength(1)) { // This square is within the old map bounds, so just copy the old value newMap[row, col] = m_TargetLevel.m_Map[row, col]; } else { // This square is outside the old map bounds, so fill with empty newMap[row, col] = Level.Tile.Empty; } } } m_TargetLevel.m_Map = newMap; } } EditorGUILayout.Space(); EditorGUILayout.LabelField("Map (" + m_TargetLevel.m_Map.GetLength(0) + "x" + m_TargetLevel.m_Map.GetLength(1) + ")", EditorStyles.boldLabel); m_WaypointListScrollPosition = EditorGUILayout.BeginScrollView(m_WaypointListScrollPosition); m_WaypointsList.DoLayoutList(); EditorGUILayout.EndScrollView(); EditorGUILayout.LabelField("Place"); m_SelectedTileToPlace = GUILayout.SelectionGrid(m_SelectedTileToPlace, m_TileNames, m_TileNames.Length); EditorGUILayout.Space(); EditorGUILayout.LabelField("Mouse Position", "Col(X): " + m_MouseCol + ", Row(Y): " + m_MouseRow); m_MapScrollPosition = EditorGUILayout.BeginScrollView(m_MapScrollPosition); GUILayoutUtility.GetRect( m_PreviewBuffer * 2 + m_TileSize * m_TargetLevel.m_Map.GetLength(1) + m_TargetLevel.m_Map.GetLength(1), m_PreviewBuffer * 2 + m_TileSize * m_TargetLevel.m_Map.GetLength(0) + m_TargetLevel.m_Map.GetLength(0) ); for (int row = 0; row < m_TargetLevel.m_Map.GetLength(0); row++) { for (int col = 0; col < m_TargetLevel.m_Map.GetLength(1); col++) { Rect rect = new Rect( m_PreviewBuffer + m_TileSize * col + col, m_PreviewBuffer + m_TileSize * row + row, m_TileSize, m_TileSize ); if (Event.current.isMouse && rect.Contains(Event.current.mousePosition)) { if (Event.current.button == 0) { // The left mouse button was clicked, so change this tile to the currently selected tile Level.Tile newTile = (Level.Tile)m_SelectedTileToPlace; if (newTile == Level.Tile.Spawner || newTile == Level.Tile.Goal) { // Attempting to place a spawner or goal, so remove any pre-existing ones for (int r = 0; r < m_TargetLevel.m_Map.GetLength(0); r++) { for (int c = 0; c < m_TargetLevel.m_Map.GetLength(1); c++) { if (m_TargetLevel.m_Map[r, c] == newTile) { // Found a pre-existing spawner or goal, so remove it m_TargetLevel.m_Map[r, c] = Level.Tile.Empty; } } } if (newTile == Level.Tile.Spawner) { // Move first waypoint to new spawner location if (!m_SpawnerPlaced) { if (m_WaypointsProperty.arraySize < 2) { // There's less than 2 waypoints, so add a new one for the spawner m_WaypointsProperty.InsertArrayElementAtIndex(0); } m_SpawnerPlaced = true; } SerializedProperty spawnerWaypoint = m_WaypointsProperty.GetArrayElementAtIndex(0); spawnerWaypoint.vector2IntValue = new Vector2Int(col, row); } else if (newTile == Level.Tile.Goal) { // Move last waypoint to new goal location if (!m_GoalPlaced) { if (m_WaypointsProperty.arraySize < 2) { // There's less than 2 waypoints, so add a new one for the goal m_WaypointsProperty.InsertArrayElementAtIndex(m_WaypointsProperty.arraySize - 1); } m_GoalPlaced = true; } SerializedProperty goalWaypoint = m_WaypointsProperty.GetArrayElementAtIndex(m_WaypointsProperty.arraySize - 1); goalWaypoint.vector2IntValue = new Vector2Int(col, row); } } m_TargetLevel.m_Map[row, col] = newTile; // Also need to update serialized property for serializablemap, otherwise all changes will be lost on restart for some reason... m_SerializableMapProperty.GetArrayElementAtIndex(row * m_TargetLevel.m_Map.GetLength(1) + col).enumValueIndex = (int)newTile; } else if (Event.current.button == 1) { // The right mouse button was clicked, so replace this tile with empty m_TargetLevel.m_Map[row, col] = Level.Tile.Empty; } } if (rect.Contains(Event.current.mousePosition)) { m_MouseRow = row; m_MouseCol = col; } EditorGUI.DrawRect(rect, GetTileColor(m_TargetLevel.m_Map[row, col]) ); } } Handles.BeginGUI(); Handles.color = Color.yellow; List <Vector3> path = new List <Vector3>(); foreach (Vector2Int point in m_TargetLevel.m_Waypoints) { path.Add(new Vector3( m_PreviewBuffer + m_TileSize * point.x + point.x + m_TileSize / 2, m_PreviewBuffer + m_TileSize * point.y + point.y + m_TileSize / 2 ) ); EditorGUI.DrawRect(new Rect( m_PreviewBuffer + m_TileSize * point.x + point.x + m_TileSize / 2 - m_TileSize / 10, m_PreviewBuffer + m_TileSize * point.y + point.y + m_TileSize / 2 - m_TileSize / 10, m_TileSize / 5, m_TileSize / 5), Color.yellow ); } Handles.DrawPolyLine(path.ToArray()); Handles.EndGUI(); EditorGUILayout.EndScrollView(); serializedObject.ApplyModifiedProperties(); Repaint(); }
/// <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); }