Press "Enter" to skip to content

Lesson 14.1: Moving game data to external files

I’m back from taking time off for my birthday, so this lesson will be a simple one. We’ll add the ability to change the displayed tab to Inventory, Quests, or Recipes. Plus, we’ll fix a potential crash if the user clicks on the datagrid and tries to use the keyboard actions.

Step 1: Modify WPFUI\MainWindow.xaml

There was a problem with the last change.

If the user clicks on one of the rows in the Inventory, Quests, or Recipes datagrid, that row has “focus” – the program thinks that is what we want to work with. Then, if the user presses the “W” key (to move North), the program will think they want to enter data into the datagrid cell that has focus.

Since some of the bound properties are readonly (they don’t have setters), this causes the program to crash.

To fix this problem, we’ll update the XAML Bindings with “Mode=OneWay”. Then, the UI will know that is should only read values from the properties, and not try to change the property’s values from the UI.

The code for this change in MainWindow.xaml is shown in step 2, because we’re making some other changes to the same part of the code. Look at lines 175, 180, 182, 195, 198, and 211 of the new code, to see where we add the OneWay mode to the binding.

Step 2: Modify WPFUI\MainWindow.xaml.cs

I also had a request to show how to use keyboard input to change the focus of the Inventory, Quests, and Recipes tabs. I also added the ability to use a keypress to show the trade screen – if the player is at a location with a trader.

To set focus on a TabItem, we need a way to distinguish the three different TabItems in the TabControl and set the focus to (display) the desired TabItem.

The TabControl object holds a collection of its TabItems. We could reference them by using an index: Inventory is at index 0, Quests is at index 1, and Recipes is at index 2.

But, if we ever add a new tab, or change the existing tabs’ order, we would need to remember to change the code to use the new index values.

To prevent that future problem, we’ll give each TabItem a Name value and use that name to set the focus. This is done in MainWindow.xaml, on lines 169, 189, and 205.

We also needed to give a name to the TabControl, so we can reference it in the code-behind page. On line 167, add the new attribute: x:Name=”PlayerDataTabControl”.

In MainWindow.xaml.cs, on lines 94-96, I added three new keys to watch for “I” (for inventory), “Q” (for quests), and “R” (for recipes). When the user presses one of those keys, we call a new SetTabFocusTo() function, passing in the name of the TabItem we went to set the focus on.

Add the new SetTabFocusTo function to lines 108-121.

We loop through the PlayerDataTabControl’s Items property (its collection of TabItems) and checks if the TabItem’s Name matches the value of the passed-in parameter. If it matches, it sets that TabItem’s IsSelected property to “true”, making is the displayed TabItem, and stops looking at the other TabItems for one with a matching name.

Notice line 112 “if(item is TabItem tabItem)”. It might look a little strange. This assigns the current item to the “tabItem” variable, whose expected datatype is TabItem. If the item is a TabItem, the “if” statement will evaluate to “true”, and the program will run the code inside. The code is functionally the same as the code below, but combine the first two lines into one:

// Try to cast "item" to a TabItem.
// tabItem is null if it fails ("item" is not a TabItem object).
TabItem tabItem = item as TabItem;
if(tabItem != null)
{
    tabItem.IsSelected = true;
    return;
}

To display the trade screen, I added line 97, which adds a new value to _userInputActions that looks for a “T” keypress and calls the function that displays the trade screen – the same function we use for the “Trade” button.

Because the user can press “T” when they’re at a location without a trader, we need to add a “guard clause” around the code that displays the trade screen. This is the new “if” statement on lines 71-77.

MainWindow.xaml.cs (lines 163-226, the “Inventory, Quests, and Recipes” section)
<!-- Inventory, Quests, and Recipes -->
        <Grid Grid.Row="2" Grid.Column="0"
              Background="BurlyWood">

            <TabControl x:Name="PlayerDataTabControl">
                <TabItem Header="Inventory"
                         x:Name="InventoryTabItem">
                    <DataGrid ItemsSource="{Binding CurrentPlayer.GroupedInventory}"
                              AutoGenerateColumns="False"
                              HeadersVisibility="Column">
                        <DataGrid.Columns>
                            <DataGridTextColumn Header="Description"
                                                Binding="{Binding Item.Name, Mode=OneWay}"
                                                Width="*"/>
                            <DataGridTextColumn Header="Qty"
                                                IsReadOnly="True"
                                                Width="Auto"
                                                Binding="{Binding Quantity, Mode=OneWay}"/>
                            <DataGridTextColumn Header="Price"
                                                Binding="{Binding Item.Price, Mode=OneWay}"
                                                Width="Auto"/>
                        </DataGrid.Columns>
                    </DataGrid>
                </TabItem>

                <TabItem Header="Quests"
                         x:Name="QuestsTabItem">
                    <DataGrid ItemsSource="{Binding CurrentPlayer.Quests}"
                              AutoGenerateColumns="False"
                              HeadersVisibility="Column">
                        <DataGrid.Columns>
                            <DataGridTextColumn Header="Name"
                                                Binding="{Binding PlayerQuest.Name, Mode=OneWay}"
                                                Width="*"/>
                            <DataGridTextColumn Header="Done?"
                                                Binding="{Binding IsCompleted, Mode=OneWay}"
                                                Width="Auto"/>
                        </DataGrid.Columns>
                    </DataGrid>
                </TabItem>

                <TabItem Header="Recipes"
                         x:Name="RecipesTabItem">
                    <DataGrid ItemsSource="{Binding CurrentPlayer.Recipes}"
                              AutoGenerateColumns="False"
                              HeadersVisibility="Column">
                        <DataGrid.Columns>
                            <DataGridTextColumn Header="Name"
                                                Binding="{Binding Name, Mode=OneWay}"
                                                Width="*"/>
                            <DataGridTemplateColumn MinWidth="75">
                                <DataGridTemplateColumn.CellTemplate>
                                    <DataTemplate>
                                        <Button Click="OnClick_Craft"
                                                Width="55"
                                                Content="Craft"/>
                                    </DataTemplate>
                                </DataGridTemplateColumn.CellTemplate>
                            </DataGridTemplateColumn>
                        </DataGrid.Columns>
                    </DataGrid>
                </TabItem>
            </TabControl>
        </Grid>
MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using Engine.EventArgs;
using Engine.Models;
using Engine.ViewModels;

namespace WPFUI
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private readonly GameSession _gameSession = new GameSession();
        private readonly Dictionary<Key, Action> _userInputActions =
            new Dictionary<Key, Action>();

        public MainWindow()
        {
            InitializeComponent();

            InitializeUserInputActions();

            _gameSession.OnMessageRaised += OnGameMessageRaised;

            DataContext = _gameSession;
        }

        private void OnClick_MoveNorth(object sender, RoutedEventArgs e)
        {
            _gameSession.MoveNorth();
        }

        private void OnClick_MoveWest(object sender, RoutedEventArgs e)
        {
            _gameSession.MoveWest();
        }

        private void OnClick_MoveEast(object sender, RoutedEventArgs e)
        {
            _gameSession.MoveEast();
        }

        private void OnClick_MoveSouth(object sender, RoutedEventArgs e)
        {
            _gameSession.MoveSouth();
        }

        private void OnClick_AttackMonster(object sender, RoutedEventArgs e)
        {
            _gameSession.AttackCurrentMonster();
        }

        private void OnClick_UseCurrentConsumable(object sender, RoutedEventArgs e)
        {
            _gameSession.UseCurrentConsumable();
        }

        private void OnGameMessageRaised(object sender, GameMessageEventArgs e)
        {
            GameMessages.Document.Blocks.Add(new Paragraph(new Run(e.Message)));
            GameMessages.ScrollToEnd();
        }

        private void OnClick_DisplayTradeScreen(object sender, RoutedEventArgs e)
        {
            if(_gameSession.CurrentTrader != null)
            {
                TradeScreen tradeScreen = new TradeScreen();
                tradeScreen.Owner = this;
                tradeScreen.DataContext = _gameSession;
                tradeScreen.ShowDialog();
            }
        }

        private void OnClick_Craft(object sender, RoutedEventArgs e)
        {
            Recipe recipe = ((FrameworkElement)sender).DataContext as Recipe;
            _gameSession.CraftItemUsing(recipe);
        }

        private void InitializeUserInputActions()
        {
            _userInputActions.Add(Key.W, () => _gameSession.MoveNorth());
            _userInputActions.Add(Key.A, () => _gameSession.MoveWest());
            _userInputActions.Add(Key.S, () => _gameSession.MoveSouth());
            _userInputActions.Add(Key.D, () => _gameSession.MoveEast());
            _userInputActions.Add(Key.Z, () => _gameSession.AttackCurrentMonster());
            _userInputActions.Add(Key.C, () => _gameSession.UseCurrentConsumable());
            _userInputActions.Add(Key.I, () => SetTabFocusTo("InventoryTabItem"));
            _userInputActions.Add(Key.Q, () => SetTabFocusTo("QuestsTabItem"));
            _userInputActions.Add(Key.R, () => SetTabFocusTo("RecipesTabItem"));
            _userInputActions.Add(Key.T, () => OnClick_DisplayTradeScreen(this, new RoutedEventArgs()));
        }

        private void MainWindow_OnKeyDown(object sender, KeyEventArgs e)
        {
            if(_userInputActions.ContainsKey(e.Key))
            {
                _userInputActions[e.Key].Invoke();
            }
        }

        private void SetTabFocusTo(string tabName)
        {
            foreach(object item in PlayerDataTabControl.Items)
            {
                if (item is TabItem tabItem)
                {
                    if (tabItem.Name == tabName)
                    {
                        tabItem.IsSelected = true;
                        return;
                    }
                }
            }
        }
    }
}

Step 3: Test the game.

Be sure to click on the different datagrids (especially the text values in the rows) and press some keys.

NOTE: The next thing I’ll probably work on is moving the data in the factories to XML files, so you can modify those files to add more locations, monster, items, etc. to the game – without needing to recompile the program. But, if there are features you want to see done first, please leave a comment and let me know.

Leave a Reply

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