At the end of the previous part we’d implemented a basic game loop using the Console for input and display. It works pretty well now, but the C# interface to the console is lacking at best. Pretty soon we’ll run into those limitations so we’re going to move to something else before going any further. We’re going to stick with a text based game, we can do that while leaving ourselves flexibility going forward that will make a graphical also possible.
C# libraries related to Roguelikes are a bit scarce compared to some other languages, but there are a several that will work for our needs. In place of using the Windows console I’m going to use RLNET. RLNET provides an API for creating tile based games under .NET that will handle our console output. I also handles our keyboard and mouse input.
Using an external library does add a dependency to our project. That’s a risk worth taking for a learning process like this as this library will save time we can devote to the other aspects of a game. Given that RLNET is open source, meaning we have access to the source code in case of bugs or needed changes, it feels like a safe risk.
Let’s walk through building the same functionality that we had at the end of the last article using RLNET. Begin in Visual Studio by selecting File
-> New Project...
from the menu. Under Templates choose Visual C#
and expand Windows Desktop
and choose Classic Desktop
-> Empty Project
and give it the name RogueLearning.
Since we have an empty project, we need to add in the Program.cs class that we’d have created for us using the Console Application. Right click on the project and select Add
-> Class
. Name the class Program.cs and change it to match the following code. This will create a main method that will be called when our application begins.
[code language=”csharp”]
namespace RogueLearnng
{
class Program
{
static void Main(string[] args)
{
}
}
}
[/code]
We next will add RLNET to our project using the NuGet Package Manager. Right click on the project and select Manage Nuget Packages...
. Choose the Browse tab and search for RLNET and click Install. This will add RLNET and its dependency OpenTK to your project. At this time the current version of RLNET is 1.0.6.
While we’re keeping our game text based for simplicity, RLNET actually is tile based. It uses a special image called a Font File to define these tiles. The Font File is a rectangular image containing all the tiles in this one image. These tiles must all be the same size, but there is no requirement that they contain text. They could as easily be made of images that we’d use instead. The format is that used by the popular C language Roguelike library libtcod and you can read more about these files on the libtcod documentation site.
The RLNET download comes with a simple text font file ascii_8x8.png
, but the NuGet package does not install it so I’m providing a copy here. Download the file and drag it onto your project in Solution Explorer to add it to your program. In the properties for this file, change the Copy to Output Directory option to Copy if newer. Skip this step, and you’ll get an error when you try to run the app that the bitmap cannot be found.
Viewing the file you can see that it is a 128 x 128 pixel image that contains 256 8 x 8 pixels tiles arranged in 16 rows and 16 columns. These tiles correspond pretty closely to the original IBM CGA character set and is popular for text based games like this.
With the library loaded and a font file added to our project, we can being implementing our game loop. First at the top of the Program.cs file we’ll add a reference to the RLNET namespace so we don’t need to prefix it to every method.
[code language=”csharp”]
using RLNET;
[/code]
As in the previous entry, we’ll begin by adding the global variables that will contain our game state to the top of the class before the Main
method definition.
[code language=”csharp”]
private static RLRootConsole _rootConsole;
private const int screenWidth = 80;
private const int screenHeight = 24;
private static int playerX;
private static int playerY;
[/code]
Two changes here. We’ve removed the quit flag that’s no longer needed with RLNET. We’ve added a RLRootConsole
object that we’ll use to set and render our screen. Now let’s look at the new Main
method.
[code language=”csharp”]
static void Main(string[] args)
{
_rootConsole = new RLRootConsole("ascii_8x8.png", screenWidth, screenHeight, 8, 8, 2f, "Rogue Learning");
_rootConsole.OnLoad += RootConsole_OnLoad;
_rootConsole.Render += RootConsole_Render;
_rootConsole.Update += RootConsole_Update;
_rootConsole.Run();
}
[/code]
The first line creates our console object that we’ll be using for display and to read player input. There seven parameters here, and each is important so let’s go through them in detail.
The first parameter “ascii_8x8.png” chooses the font file described earlier. The next two parameters, screenWidth and screenHeight, define the width and height of the console for our game. We’re using the constants declared a moment ago.
The next two parameters relate back to the font file and tell RLNET the size of each tile in pixels. For the ascii_8x8.png file each tile is 8 pixels wide by 8 pixels tall. We next provide a scale factor. This is a float value that zooms the screen by that factor. We’re using 2 here. This will make the display appear more pixelated, but shows up a bit better using these small tiles for screenshots. The last parameter lets us specify the name to appear on the window.
The next three lines set the handlers for the three events that we’ll deal with as our game loop. The first OnLoad event runs when we begin the game loop. The Render handler takes care of the display of the game state to the player. The Update handler allows us to read user input and update the state of the game.
Next we’ll implement these handlers starting with OnLoad
:
[code language=”csharp”]
private static void RootConsole_OnLoad(object sender, EventArgs e)
{
playerX = screenWidth / 2;
playerY = screenHeight / 2;
}
[/code]
This performs the step of setting the player’s initial position as we did in the InitializeGame
method of the last article. Note that since we defined the size of the console on creation, we do not need to do that here.
Our Render
handler also looks familiar:
[code language=”csharp”]
private static void RootConsole_Render(object sender, UpdateEventArgs e)
{
_rootConsole.Clear();
_rootConsole.SetChar(playerX, playerY, ‘@’);
_rootConsole.Draw();
}
[/code]
We first clear the console so we have an empty display to begin. We then set the @
character at the player’s current position. Last we draw the current console to the window.
What we’re doing when setting the @
character is a little more complicated than it sounds. We aren’t displaying text directly as we were in the Console based version. We’re telling RLNET to display a tile at the player’s position. That tile is the integer value of the @
character, which is 64. Tile 64 of the loaded font file is a graphic that looks like the @
character. If you didn’t follow that, don’t worry about it for now. With our current font file the distinction isn’t important as characters and tiles match. We’ll go in more depth when needed.
Last we have the Update
handler.
[code language=”csharp”]
private static void RootConsole_Update(object sender, UpdateEventArgs e)
{
// Handle keyboard input
RLKeyPress key = _rootConsole.Keyboard.GetKeyPress();
if(key != null)
{
switch(key.Key)
{
case RLKey.I:
playerY -= 1;
break;
case RLKey.J:
playerX -= 1;
break;
case RLKey.M:
playerY += 1;
break;
case RLKey.K:
playerX += 1;
break;
case RLKey.Q:
_rootConsole.Close();
break;
}
}
// Ensure player stays on the screen
if (playerX < 0)
playerX = 0;
if (playerX > screenWidth – 1)
playerX = screenWidth – 1;
if (playerY < 0)
playerY = 0;
if (playerY > screenHeight – 1)
playerY = screenHeight – 1;
}
[/code]
Again the code should look familiar. We now use the RLNET library to get our keyboard press, but otherwise we check for the i
,j
,k
, or m
keys as before and update the position as we did before. We also handle the quit method here by using the Close
method on the console to end the game. After dealing with player input, we ensure the player isn’t trying to wander off the screen and correct if needed.
At this point we have recreated the previous demo using RLNET. This tutorial has already run a bit long, so we’ll stop there for this article and you can download the code to this point. Next time we’ll delve a bit more into the Update
handler and discuss some subtle changes the move to RLNET brought.