The last article recreated our game loop using RLNET. We’re now going to talk a bit more about some of the design choices of RLNET so we can understand how they’ll affect our design moving forward.
First if you ran the last demo (do so now if you didn’t) you likely noticed there were two windows. Our console containing our player and a second console in the background. As our project is set as a Console Application, this console was created even though we don’t want it and don’t use it. We could get rid of the window by hiding it, but a better option in our case is to never create it. Since we’re not a console app anymore, we change our project to work as a Windows application.
Right click the project in Visual Studio’s Solution Explorer and choose Properties. Under the Application tab change the Output type to Windows Application. Save and run and you’ll see no more unwanted console window.
You’ll also notice the window looks quite a bit different now. Our Console based program had a window that was roughly in the 4×3 ratio common to Console apps. The RLNET window though is very elongated.
This is exaggerated by the use of the 2.0 scaling factor, but most comes from our change to square tiles. Most console fonts have characters that taller than they are wide. Moving to characters that are the same height as their width means that the same size window will now look shorter. For now we’ll leave it, but will come back to the window size later.
Moving to RLNET also made another change to our code that isn’t currently obvious. In our Console verion the program waited for keyboard input and then processed that input. It still appears that’s the case, but a quick test will show this is not how RLNET works. Let’s add a simple counter after our other variables.
[code language=”csharp”]
private static int tick = 0;
[/code]
We’ll add a line to print this counter to our RootConsole_Render
method.
[code language=”csharp”]
_rootConsole.Print(1, 1, $"Current tick: {tick}", RLColor.White);
[/code]
Note that I’m using a recent feature of C# known as interpolated strings. If you’re not familiar with this syntax
$"Current tick: {tick}"
is equivalent tostring.Format("Current tick: {0}", tick)
.
If you run the app you’ll see the tick counter increases even when we don’t press a key. That’s because the keyboard check _rootConsole.Keyboard.GetKeyPress()
doesn’t wait until a key is pressed. If checks for a key press, but if no keys was pressed it continues and returns a null.
Our game loop therefore now runs in realtime. This is a change from our original game loop using the Console as the Console.ReadKey(true)
command does wait until a key is pressed before continuing. It doesn’t matter here since our game does nothing at the moment, but will become more important when we add other objects to the game, especially those hostile to the player. It would be quite a surprise if the player expects a monster to move only as they do, only for it to charge across the map and attack without the player doing a thing.
That doesn’t mean that this real time aspect is a bad thing. In fact we can make a real-time loop like this operate as a turn based loop. It adds a little work on us as a programmer, but gives us flexibility for the future we can use for animations or other things we might want to occur even when there’s no action by the user.
The simplest way to move toward real time would be to loop until a key is pressed. This way the actions of the game only continue after a player action. The code would look like:
[code language=”csharp”]
RLKeyPress key;
do
{
key = _rootConsole.Keyboard.GetKeyPress();
} while (key == null);
[/code]
This won’t work. If you make this change and run the game you’ll find your game won’t respond to any keypress. What we will do instead is a more flexible approach that divides our update handler into real time and turn based components. Let’s add a second counter to track this. After the tick
counter we added, add another counter.
[code language=”csharp”]
private static int turn = 0;
[/code]
and change the print statement we added earlier to also show this new counter.
[code language=”csharp”]
_rootConsole.Print(1, 1, $"Current tick: {tick} Current turn: {turn}", RLColor.White);
[/code]
Next we’ll change our Update
handler to deal with the mix of events that occur every time we go through the loop (ticks) and events that we’ll process only after the player makes an action (turns).
[code language=”csharp”]
private static void RootConsole_Update(object sender, UpdateEventArgs e)
{
bool userAction = false;
// Handle keyboard input
RLKeyPress key = _rootConsole.Keyboard.GetKeyPress();
if (key != null)
{
userAction = true;
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;
}
}
// Turn based events only if userAction is true
if (userAction)
{
// 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;
turn++;
}
// Real time actions
tick += 1;
}
[/code]
Now running the app you’ll see the tick counter increases in real time. Our turn counter only increases when we move our character. As I originally noted, my design for this game will be turn based, that’s a purely a game design choice. Having a creature charging at the player doing nothing can be normal and expected in many games.
One last change to make today. We chose the i,j,k, and m keys out of tradition, but I think most players would be expecting the arrow keys to move the player. So let’s make a change that uses those keys for movement instead, and we’ll also let the escape key end our game.
The i, j, k, and m key arrangement dates back to the popular Unix vi editor. It therefore became rather common in software written for Unix to use these keys for cursor movement. This included Rogue and Nethack.
Change the section managing keyboard input to:
[code language=”csharp”]
if (key != null)
{
userAction = true;
switch (key.Key)
{
case RLKey.Up:
playerY -= 1;
break;
case RLKey.Left:
playerX -= 1;
break;
case RLKey.Down:
playerY += 1;
break;
case RLKey.Right:
playerX += 1;
break;
case RLKey.Q:
case RLKey.Escape:
_rootConsole.Close();
break;
}
}
[/code]
That concludes this tutorial. You can download a zip file with the code to this point. Next we’ll start giving our player something to do other than wander in empty space.