Lesson 13.1: Add keyboard input for actions, using delegates

The last feature took several lessons to complete. So, in this lesson, we’ll do a small feature we can add quickly – using keyboard input for player actions.

There are several ways to detect key presses and have them cause an action. However, many of them are more complex than we really need right now. I’ll use a different technique – partly because I also want introduce dictionaries, actions, and delegates.

 

 

 

Lesson Steps

Step 1: Modify WPFUI\MainWindow.xaml

When the user presses a key, it fires the KeyDown event. This is like the Click event that fires when the user clicks on a button.

In the opening Window section, add line 11 (below) to tell the program the function to run on the KeyDown event.

 

MainWindow.xaml (lines 1-11)

 

 

Step 2: Modify WPFUI\MainWindow.xaml.cs

The first thing to do is add some new “using” statements (lines 1, 2, and 5). These are for the new datatypes we’re using in this lesson.

 

Next, create a new private variable to store the key that was pressed and the action to perform for that key (lines 18-19).

This line uses three new things: Dictionary, Key, and Action.

A dictionary is like a collection that holds two values: the key and the value. Think of it like a real-life dictionary. It has a word (the key) and the word’s definition (the value). In a dictionary, you cannot have two values with the same keys – each key must be unique.

 

In the sample code below, I created a dictionary that has an integer for the key and a string for the value. I added three entries – the numbers from 1 to 3, with their text names.

The “result” variable in this code would hold the value “two”. “_integerNames[2]” says to find the entry where the key is 2 and return it’s value – in this case, the string “two”.

 

In the Dictionary we’re creating, the key (dictionary index) will be the key (keypress from the keyboard) the user pressed.

We’ll use W, A, S, and D for movement (North, West, South, and East). “Z” is attack the current monster, and “C” is to consume the current consumable item.

Instead of using a string for the dictionary entry’s key (for example, “W”), we’ll use a Key, which is an enum. This lets us see if the user also held down shift, control, or alt while pressing a letter or number.

 

For the dictionary entry’s value, we are going to store an Action. An Action is a pointer to a function to run. This is called a “delegate”. I’ll talk about that more in a minute.

 

In the constructor (line 25) call the new InitializeUserInputActions() function.

 

Add the InitializeUserInputActions() function on lines 82-90. This is where we populate the dictionary with the keys and the functions to run when the key is pressed.

 

To tell the program what function we want as the dictionary entry’s value (the delegate I mentioned), we use code like: “() => _gameSession.MoveNorth()”. That’s a pointer to the function we want to run when the uses presses the “w” key.

The first part “()” defines the parameters we want to pass into the delegate. Because the functions we’re using don’t require any parameters, we’ll use the empty parentheses.

 

Let’s say we wanted to pass two parameters into our delegate functions: an integer and a string. Then, we would have needed to define the dictionary variable like this:

In the dictionary definition, the “Action<int, string>” says the Action will accept two parameters: an integer and a string.

 

The delegate code would look like this:

 

The “(x, y)” is for the two parameters we’re passing into the Action. Of course, we’d only do this if we needed to use the parameters. So, we’d probably only use it if MoveNorth required an integer and string parameter. Then, the code would look like:

 

On lines 92-98 is the function to handle the keypress (the one from line 11 of MainWindow.xaml). It looks at the eventargs parameter “e”, gets the Key that was pressed, and checks to see if the dictionary has an entry whose key is the key that was pressed.

If it does, if gets the dictionary entry’s value “_userInputActions[e.Key]” and runs the function by calling Invoke().

If we were passing parameter values into the delegate, we would need to call “Invoke(x, y)”.

 

MainWindow.xaml.cs

 

 

Step 3: Modify Engine\ViewModels\GameSession.cs

Before making these changes, the only way the user could call AttackCurrentMonster was by clicking on the “Use” button for the weapon combobox – which was hidden if the location did not have a monster.

However, after these changes, the user can press the space bar at any time – even if they are at a location without a monster. So, we need to add some “guard” code to AttackCurrentMonster and UseCurrentConsumable, to only run the code if there is a value in CurrentMonster or CurrentPlayer.CurrentConsumable.

We already have this for the movement functions. They each check to see if HasLocationTo<direction> is true, before trying to move to a new location.

 

Add lines 250-253 to the beginning of AttackCurrentMonster. If CurrentMonster is null, we return from the function immediately. This technique is sometimes called “early exit” – under a certain condition, we exit the function early.

 

Change UseCurrentConsumable to the code below. We could do the same “early exit” as AttackCurrentMonster. But, because there is only one line in the function, I just surrounded it with an “if” statement.

 

GameSession.cs (lines 248-280)

 

 

Step 4: Test the program

 

Return to main page

2 thoughts on “Lesson 13.1: Add keyboard input for actions, using delegates

  1. When the user tries to Move after clicking to Craft an Item, the program gives off an error. Also, how would I go about trying to link a key press to open a certain tab (Like, I would open up Inventory, J wold open up Quest, and K would open up Recipes)

    1. I’m back from a bit of a vacation, and will look at this. It’s probably because the “Craft” button (or datagrid, or datagrid row) has focus and the program is treating the keypress as an attempt to change the value of a property behind it (like entering a value into a textbox). Adding “Mode=OneWay” to the databindings in the datagrids should fix it.

      I’ll also add some changes to handle opening the tabs on different keypresses. We’ll need to give the tabitems names, and set focus to them. Here’s a StackOverflow link, if you want to see more about doing that: https://stackoverflow.com/questions/1227132/how-can-i-make-a-specific-tabitem-gain-focus-on-a-tabcontrol-without-click-event

Leave a Reply

Your email address will not be published. Required fields are marked *