Tutorial Part 7: Tunnels and ViewPort

Important note: These tutorials were written for SadConsole version 7.x. The version 8.x tutorials are here.

We’ve got a reliable means for storing map data, we’ve got rooms carved out of a dungeon consisting of walls – and now we’re going to connect the rooms to one another using a simple “tunnelling” algorithm. There are many, many ways of skinning this cat – but I’m relying upon the method used in the RogueSharp tutorials. It is straightforward and makes sense without a ton of ifs, ands, or buts.

Tunneling

The goal of tunnelling is to carve out hallways between rooms, one at a time. This algorithm connects the first to the second, second to the third, and fourth to the fifth room … and so on, until all rooms have been connected in a large chain. This produces a fairly complex map with three- and four-way intersections where tunnels meet one another. A nice feature is that the method randomly determines which type of L-shaped hallway to carve out, making for a more visually interesting map.

Now let’s modify our MapGenerator class to add some tunnelling. Insert some new code after the foreach (Rectangle room in Rooms) loop in the GenerateMap method:

            // carve out tunnels between all rooms
            // based on the Positions of their centers
            for (int r = 1; r < Rooms.Count; r++)
            {
                //for all remaining rooms get the center of the room and the previous room
                Point previousRoomCenter = Rooms[r - 1].Center;
                Point currentRoomCenter = Rooms[r].Center;

                // give a 50/50 chance of which 'L' shaped connecting hallway to tunnel out
                if (randNum.Next(1, 2) == 1)
                {
                    CreateHorizontalTunnel(previousRoomCenter.X, currentRoomCenter.X, previousRoomCenter.Y);
                    CreateVerticalTunnel(previousRoomCenter.Y, currentRoomCenter.Y, currentRoomCenter.X);
                }
                else
                {
                    CreateVerticalTunnel(previousRoomCenter.Y, currentRoomCenter.Y, previousRoomCenter.X);
                    CreateHorizontalTunnel(previousRoomCenter.X, currentRoomCenter.X, currentRoomCenter.Y);
                }
            }

This loop runs through the entire list of Rooms, storing the Center coordinates of the previous room and current room in the list. It then flips a coin to determine how the algorithm should carve out one of two L-shaped tunnels. The difference between each L-shape is like a Tetris block:

------------|           |   |
---------   |           |   |
         |  |           |   ----------
         |  |           |-------------
  L-Shape 1                 L-Shape 2

Both tunnels connect two rooms with slightly different geometry.

Let’s add the CreateHorizontalTunnel and CreateVerticalTunnel methods:

        // carve a tunnel out of the map parallel to the x-axis
        private void CreateHorizontalTunnel(int xStart, int xEnd, int yPosition)
        {
            for (int x = Math.Min(xStart, xEnd); x <= Math.Max(xStart, xEnd); x++)
            {
                CreateFloor(new Point(x, yPosition));
            }
        }

        // carve a tunnel using the y-axis
        private void CreateVerticalTunnel(int yStart, int yEnd, int xPosition)
        {
            for (int y = Math.Min(yStart, yEnd); y <= Math.Max(yStart, yEnd); y++)
            {
                CreateFloor(new Point(xPosition, y));
            }
        }

And that’s it! Give it a run, and play around with the number and size of rooms in your GameLoop to generate different map geometries. This is a very simple algorithm, and it produces serviceable maps. Sure, the brutalist architectural style belongs in a Soviet-era Naukograd, but we’re just getting started!

No More Static Cling?

A suggestion from /u/ProfessionalNihilist: if you are intending to also teach OOP properties then I would eliminate all static state in the game-loop and make it a class with non-static methods that is initialised and then ran by Main.

Agreed. Boy do I dislike seeing statics strewn all over the place in our GameLoop. By now you’re likely noticing that every time we add a variable to the GameLoop, we have to declare it as a static. The static keyword declares that the variable is available to all instances of the class. Since we’re only declaring one instance of the GameLoop class, this is okay – there won’t be any data corrupted by multiple instances accessing/changing _mapWidth for instance. GameLoop is effectively a singleton. But who cares? We’re only ever going to run a single GameLoop anyway.

Besides the data sharing issue, our program becomes less flexible with each static we add to it. Notice how adding just one static field to the class forces us into making every variable/method  it references static? Go ahead – try removing a single static keyword from the variable definitions at the top of the class. You’re going to see one or more compiler errors instantly. Aside from the extra (extraneous) code we’re adding with each static keyword, static definitions begin to smell a bit.

Unfortunately, you’re going to have to learn how to live with some static fields. That’s because SadConsole’s OnInitialize and OnUpdate events require linkage with static methods. All we can do is improve the flexibility of the GameLoop gradually by detaching as much gameplay data and methods as we can from it, and inserting them into their own classes.

When we’re forced to use static fields, we’re going to make our program as flexible as possible by:

SadConsole’s EntityManager

One nagging problem we have is that our map does not scroll as we move our @ player around. Thankfully, SadConsole already takes care of scrolling for us using a console’s ViewPort property and its EntityManager class. A ViewPort is like staring into a telescope – you get to see a portion of the world at a time, and the rest of the world is obscured by the edges of the lenses. SadConsole has an incredibly flexible ViewPort which will let us set its size, and move it around at our convenience.

The EntityManager keeps track of where Entities (e.g. any objects based on our Actor class) are in relation to the ViewPort, and shows (or hides) entities on the console when they’re beyond the console’s edges. Normally we’d have to write our own manager to deal with this, but SadConsole.Entities.EntityManager does all the heavy lifting for us.

Let’s create a new EntityManager (yes, a singleton static field!) in our GameLoop class. Don’t worry – eventually this will be moved into its own UI class – but for now we want something workable. Let’s also add startingConsole as a field in our class too, so it is visible to the entire class:

        public static SadConsole.Entities.EntityManager;
        private static Console startingConsole;

Update the Init method’s startingConsole instantiation:

            // Create a console using gameMap's tiles
            startingConsole = new Console(GameMap.Width, GameMap.Height, Global.FontDefault, new Rectangle(0, 0, Width, Height), GameMap.Tiles);

After creating the startingConsole we add:

            //Instantiate the EntityManager
            EntityManager = new SadConsole.Entities.EntityManager();

            // Add the EntityManager to the startingConsole
            startingConsole.Children.Add(EntityManager);

And then at the bottom of the Init method, we add:

            // add the player to the EntityManager's collection of Entities
            EntityManager.Entities.Add(player);

The ViewPort works by specifying a rectangle that defines the position, width and height of the viewer. So if we only want to see a tiny portion of the map, we can set the width and height to be 10×10. To move the ViewPort, we just adjust the position of the rectangle while keeping the width and height the same. So let’s add some code that moves and re-centers the ViewPort each time our @ player moves:

        // centers the viewport camera on an Actor
        public static void CenterOnActor(Actor actor)
        {
            startingConsole.CenterViewPortOnPoint(actor.Position);
        }

And now we call that method whenever we hit an arrow key on the keyboard. Update the CheckKeyboard method with this code:

            // Keyboard movement for Player character: Up arrow
            // Decrement player's Y coordinate by 1
            if (SadConsole.Global.KeyboardState.IsKeyPressed(Microsoft.Xna.Framework.Input.Keys.Up))
            {
                player.MoveBy(new Point(0, -1));
                CenterOnActor(player);
            }

            // Keyboard movement for Player character: Down arrow
            // Increment player's Y coordinate by 1
            if (SadConsole.Global.KeyboardState.IsKeyPressed(Microsoft.Xna.Framework.Input.Keys.Down))
            {
                player.MoveBy(new Point(0, 1));
                CenterOnActor(player);
            }

            // Keyboard movement for Player character: Left arrow
            // Decrement player's X coordinate by 1
            if (SadConsole.Global.KeyboardState.IsKeyPressed(Microsoft.Xna.Framework.Input.Keys.Left))
            {
                player.MoveBy(new Point(-1, 0));
                CenterOnActor(player);
            }

            // Keyboard movement for Player character: Right arrow
            // Increment player's X coordinate by 1
            if (SadConsole.Global.KeyboardState.IsKeyPressed(Microsoft.Xna.Framework.Input.Keys.Right))
            {
                player.MoveBy(new Point(1, 0));
                CenterOnActor(player);
            }

Run your code. Your little @ should be moving around the map, with the map scrolling as it moves.

By default, the ViewPort is constrained to the same dimensions as the console – but if you want to constrain it even further, just add this code to the Init method:

            // Constrain console's viewport to a smaller area
            startingConsole.ViewPort = new Rectangle(0, 0, Width-10, Height-10);

Hopefully this shows that the viewport can be treated like a virtual camera. There’s plenty of stuff we can do with this in the future. For the time being, remove the above line of code (or else you’ll be stuck with a small viewport).

There’s a bug that you might have noticed by now: our @ symbol flickers a bit as it moves.. leaving a ghost @ symbol behind it. That movement/draw sync issue has to do with SadConsole’s Draw and Update processing order. Remember when I mentioned that processing data in the main game loop is a bad idea? This is why. We’ll engineer our way out of it later, when we build a UIManager that creates/destroys/updates consoles. For now, just live with it.

That last bug feature of our program is a perfect lead-in to our next tutorial, where we’ll construct a UIManager that controls all of our consoles, and in the process we’ll move a ton of UI/console code out of the GameLoop.

Download the final source code for this tutorial here.

Published on November 8, 2018