//Check if the level block meets the given requirements. public bool MeetsRequirements(LevelBlockRequirements requirements) { return ( //If the requirements dictate that a wall or exit should be at a certain position, check if the walls and exits match. (requirements.LeftSideType != SideType.Wall | HasLeftWall && requirements.LeftSideType != SideType.Exit | HasLeftExit && requirements.RightSideType != SideType.Wall | HasRightWall && requirements.RightSideType != SideType.Exit | HasRightExit && requirements.TopSideType != SideType.Wall | HasTopWall && requirements.TopSideType != SideType.Exit | HasTopExit && requirements.BottomSideType != SideType.Wall | HasBottomWall && requirements.BottomSideType != SideType.Exit | HasBottomExit) && //Check if the exits match completely if required. ((!ExitsMustMatch) || (requirements.LeftSideType == SideType.Exit | !HasLeftExit && requirements.RightSideType == SideType.Exit | !HasRightExit && requirements.TopSideType == SideType.Exit | !HasTopExit && requirements.BottomSideType == SideType.Exit | !HasBottomExit)) && //And check if the group matches (Group == requirements.Group) && //And check if the theme matches (if this group has a theme) (Theme == "" || requirements.Theme.Contains(Theme))); }
//Generate the level public void Generate(World world, Vector2 position, List<RoomExit> roomExits = null, List<string> guaranteedSpecialBlocks = null, string theme = "", int enemyNum = 0, List<Type> possibleEnemyTypes = null) { //Convert null arguments into default values roomExits = roomExits ?? new List<RoomExit>(); guaranteedSpecialBlocks = guaranteedSpecialBlocks ?? new List<string>(); possibleEnemyTypes = possibleEnemyTypes ?? new List<Type>(); //Create vars World = world; //Create the basic grid for the level. This will contain the main path through the level. int hBlocks = Width / BlockWidth, vBlocks = Height / BlockHeight; LevelBlockRequirements[,] basicGrid = new LevelBlockRequirements[hBlocks, vBlocks]; int[,] enemyNumber = new int[hBlocks, vBlocks]; //List for positions with enemies FairRandomCollection<Point> pointsThatCanHaveEnemies = new FairRandomCollection<Point>(); //By default, all sides are walls, unless this is the bossroom, in which case all sides (except the walls) are open. for (int i = 0; i < hBlocks; i++) { for (int j = 0; j < vBlocks; j++) { pointsThatCanHaveEnemies.Add(new Point(i, j)); if (theme == "Boss") { basicGrid[i, j] = new LevelBlockRequirements(SideType.Exit); if (i == 0) basicGrid[i, j].LeftSideType = SideType.Wall; if (j == 0) basicGrid[i, j].TopSideType = SideType.Wall; if (i == hBlocks - 1) basicGrid[i, j].RightSideType = SideType.Wall; if (j == vBlocks - 1) basicGrid[i, j].BottomSideType = SideType.Wall; } else basicGrid[i, j] = new LevelBlockRequirements(SideType.Wall); } } //Create exits where they should be foreach (RoomExit exit in roomExits) { if (exit.Direction == Direction.Left) basicGrid[exit.Position.X, exit.Position.Y].LeftSideType = SideType.Exit; if (exit.Direction == Direction.Right) basicGrid[exit.Position.X, exit.Position.Y].RightSideType = SideType.Exit; if (exit.Direction == Direction.Up) basicGrid[exit.Position.X, exit.Position.Y].TopSideType = SideType.Exit; if (exit.Direction == Direction.Down) basicGrid[exit.Position.X, exit.Position.Y].BottomSideType = SideType.Exit; //Level blocks with an exit can't have enemies pointsThatCanHaveEnemies.Remove(new Point(exit.Position.X, exit.Position.Y)); } //Connect the exits ConnectPoints(basicGrid, roomExits.Select(exitPoint => exitPoint.Position).ToList()); //Connect each now unconnected point to a connected point ConnectUnconnectedPoints(basicGrid); //Prune unneeded walls PruneWalls(basicGrid); //Create the actual level grid LevelBlock[,] levelGrid = new LevelBlock[hBlocks, vBlocks]; //Get any border blocks from the guaranteed blocks. List<string> borderBlocks = guaranteedSpecialBlocks.Where(specialBlock => specialBlock.Contains("Border")).ToList(); if (borderBlocks.Count != 0) { foreach (string borderBlock in borderBlocks) { guaranteedSpecialBlocks.Remove(borderBlock); //Check which direction should be used Direction dir = Direction.Right; if (borderBlock.Contains("Left")) dir = Direction.Left; if (borderBlock.Contains("Right")) dir = Direction.Right; if (borderBlock.Contains("Up")) dir = Direction.Up; if (borderBlock.Contains("Down")) dir = Direction.Down; //Find the exit. RoomExit exit = roomExits.Find(ex => ex.Direction == dir); //And make sure the block is placed there. basicGrid[exit.Position.X, exit.Position.Y].Group = borderBlock; } } //And get the boss portal and place it on the bottom if (guaranteedSpecialBlocks.Contains("BossPortal")) { guaranteedSpecialBlocks.Remove("BossPortal"); basicGrid[World.Random.Next(1, hBlocks - 1), vBlocks - 1].Group = "BossPortal"; } //Handle any other guaranteed blocks foreach (string guaranteedBlock in guaranteedSpecialBlocks) { int i = 0, j = 0; do { i = World.Random.Next(hBlocks); j = World.Random.Next(vBlocks); } while (basicGrid[i, j].Group != ""); basicGrid[i, j].Group = guaranteedBlock; } //Choose the level blocks for (int i = 0; i < hBlocks; i++) { for (int j = 0; j < vBlocks; j++) { if (levelGrid[i, j] == null) { //Add the level theme basicGrid[i, j].Theme += theme; //Get a block that would fit at this position. LevelBlock foundBlock = GetPossibleLevelBlock(basicGrid[i, j]); if (foundBlock != null) levelGrid[i, j] = foundBlock; else throw new Exception("There's no block that would fit here! Please create more blocks (of group '" + basicGrid[i, j].Group + "') and add them to LevelBlocks.txt."); } } } //Add enemies for (int i = 0; i < enemyNum; i++) { Point placeEnemyAt = pointsThatCanHaveEnemies.Get(); enemyNumber[placeEnemyAt.X, placeEnemyAt.Y]++; } //And place them for (int i = 0; i < hBlocks; i++) { for (int j = 0; j < vBlocks; j++) { levelGrid[i, j].Place(World, specialTiles, (int) position.X + i * BlockWidth * World.TileWidth, (int) position.Y + j * BlockHeight * World.TileHeight, basicGrid[i, j].LeftSideType == SideType.Wall, basicGrid[i, j].RightSideType == SideType.Wall, basicGrid[i, j].TopSideType == SideType.Wall, basicGrid[i, j].BottomSideType == SideType.Wall, j == vBlocks - 1, enemyNumber[i, j], possibleEnemyTypes); } } }
//Remove walls where they are too thick for no reason. //For example, if two generation blocks on top of each other would declare a wall on the side of the other block, this'd remove //one of the two walls. void PruneWalls(LevelBlockRequirements[,] basicGrid) { int hBlocks = Width / BlockWidth, vBlocks = Height / BlockHeight; for (int i = 0; i < hBlocks; i++) { for (int j = 0; j < vBlocks; j++) { LevelBlockRequirements block = basicGrid[i, j]; //We only have to look to the right and bottom, as other blocks check for the left and top (their right and bottom) automatically. //Check to the right if (i < hBlocks - 1 && block.RightSideType == SideType.Wall) { LevelBlockRequirements rightBlock = basicGrid[i + 1, j]; if (rightBlock.LeftSideType == SideType.Wall) { switch (random.Next(2)) { case 0: block.RightSideType = SideType.Any; break; case 1: rightBlock.LeftSideType = SideType.Any; break; } } } //Check to the bottom if (j < vBlocks - 1 && block.BottomSideType == SideType.Wall) { LevelBlockRequirements bottomBlock = basicGrid[i, j + 1]; if (bottomBlock.BottomSideType == SideType.Wall) { switch (random.Next(2)) { case 0: block.BottomSideType = SideType.Any; break; case 1: bottomBlock.TopSideType = SideType.Any; break; } } } } } }
//Get a block that meets certain requirements. LevelBlock GetPossibleLevelBlock(LevelBlockRequirements requirements) { WeightedRandomCollection<LevelBlock> possibleLevelBlocks = new WeightedRandomCollection<LevelBlock>(); //Check all level blocks and see which ones would meet the requirements. foreach (LevelBlock levelBlock in levelBlocks) { if (levelBlock.MeetsRequirements(requirements)) possibleLevelBlocks.Add(levelBlock, levelBlock.AppearancePreference); } //If we've not found anything, return null. if (possibleLevelBlocks.IsWriteOnly) return null; //Then return one. return possibleLevelBlocks.Get(); }
//Connect all completely unconnected points to a connected point, making sure all cells are reachable. void ConnectUnconnectedPoints(LevelBlockRequirements[,] basicGrid) { int hBlocks = Width / BlockWidth, vBlocks = Height / BlockHeight; //Store which blocks weren't connected. bool[,] isConnected = new bool[hBlocks, vBlocks]; List<Point> unconnectedPoints = new List<Point>(); for (int i = 0; i < hBlocks; i++) { for (int j = 0; j < vBlocks; j++) { isConnected[i, j] = ! basicGrid[i, j].IsOnlyWalls(); if (! isConnected[i, j]) unconnectedPoints.Add(new Point(i, j)); } } bool connectedAnythingThisRound; while (unconnectedPoints.Count > 0) { connectedAnythingThisRound = false; List<Point> removedUnconnectedPoints = new List<Point>(); //A list to store points we remove as changing a list during foreach is impossible unconnectedPoints.Shuffle(); //Shuffle the points so that they connect in a more random way. foreach (Point point in unconnectedPoints) { int i = point.X, j = point.Y; //Temporary store the x and y of this point. List<Point> possiblePoints = new List<Point>(); //Points to connect to. if (i > 0 && isConnected[i - 1, j]) possiblePoints.Add(new Point(i - 1, j)); if (i < hBlocks - 1 && isConnected[i + 1, j]) possiblePoints.Add(new Point(i + 1, j)); if (j > 0 && isConnected[i, j - 1]) possiblePoints.Add(new Point(i, j - 1)); if (j < vBlocks - 1 && isConnected[i, j + 1]) possiblePoints.Add(new Point(i, j + 1)); if (possiblePoints.Count != 0) { //Connect this point to a random other one. ConnectPoints(basicGrid, new List<Point>() { point, possiblePoints.GetRandomItem() }); isConnected[i, j] = true; //Mark this point as connected. connectedAnythingThisRound = true; removedUnconnectedPoints.Add(point); } } foreach (Point point in removedUnconnectedPoints) unconnectedPoints.Remove(point); //Give up if we couldn't connect anything at all. if (! connectedAnythingThisRound) break; } }
//Connect any number of points on the level grid. void ConnectPoints(LevelBlockRequirements[,] basicGrid, List<Point> points) { if (points.Count == 0) return; //Choose a random point and connect that to each other point. //This automatically guarantees that each point is connected to each other point, as //you can always travel to the first point and then to the target point. Point point = points.GetRandomItem(); for (int i = 0; i < points.Count; i++) { Point otherPoint = points[i]; if (point == otherPoint) continue; //Check how far we have to travel Point distanceDifference = new Point(otherPoint.X - point.X, otherPoint.Y - point.Y); //And store the directions we have to take into a RandomCollection of directions RandomCollection<Direction> directionsToTravel = new RandomCollection<Direction>(); if (distanceDifference.X < 0) directionsToTravel.Add(Direction.Left, -distanceDifference.X); if (distanceDifference.X > 0) directionsToTravel.Add(Direction.Right, distanceDifference.X); if (distanceDifference.Y < 0) directionsToTravel.Add(Direction.Up, -distanceDifference.Y); if (distanceDifference.Y > 0) directionsToTravel.Add(Direction.Down, distanceDifference.Y); //Then actually travel from the first point to the second one. Point position = point; while (directionsToTravel.Count != 0) { switch (directionsToTravel.Take()) { case Direction.Left: basicGrid[position.X, position.Y].LeftSideType = SideType.Exit; position.X -= 1; basicGrid[position.X, position.Y].RightSideType = SideType.Exit; break; case Direction.Right: basicGrid[position.X, position.Y].RightSideType = SideType.Exit; position.X += 1; basicGrid[position.X, position.Y].LeftSideType = SideType.Exit; break; case Direction.Up: basicGrid[position.X, position.Y].TopSideType = SideType.Exit; position.Y -= 1; basicGrid[position.X, position.Y].BottomSideType = SideType.Exit; break; case Direction.Down: basicGrid[position.X, position.Y].BottomSideType = SideType.Exit; position.Y += 1; basicGrid[position.X, position.Y].TopSideType = SideType.Exit; break; } } } }