Tutorial Part 7: Tunnels and ViewPort (SadConsole v8)

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:

A Scrolling ViewPort

One nagging problem we have is that our map does not scroll as we move our @ player around. What do I mean by scrolling? 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.

Thankfully, SadConsole already takes care of scrolling for us using a brand new Components addon in v8. We anticipated this change in earlier tutorials by using a ScrollingConsole instead of a plain old Console. Now, it’s just a matter of hooking up a new Component to ScrollingConsole.

Let’s start by adding startingConsole as a field in our class too, so it is visible to the entire class:

private static ScrollingConsole startingConsole;

Update the Init method’s startingConsole instantiation:

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

Now we can add the critical code frag that adds the EntityViewSyncComponent component to the player:

// add the EntityViewSyncComponent to the player
player.Components.Add(new EntityViewSyncComponent());

And don’t forget that the player needs to be on the startingConsole’s render list of course:

// Add the player to the startingConsole's render list
startingConsole.Children.Add(player);

This might seem counterintuitive at first, especially if you followed a previous version of these tutorials (for SadConsole v7 and earlier) that used a very OOP-heavy EntityManager. Think of a Component as an optional plugin that enables additional features for an object. In this case, the EntityViewSyncComponent allows an Entity to sync its visibility and position offset with the parent console. The parent console can then determine whether the entity is inside or outside of its viewport. In earlier versions of SadConsole, we had to compute these offsets and update the console ourselves. With version 8, we’re just turning on the Component and letting it do its magic.

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 February 27, 2019