Lesson 06.2: Using Quests in the Game

In this lesson, we will add quests to locations, give quests to the player (when they move to a location with a quest), and display the player’s quests on the screen.

The changes will be similar to what we did to add an inventory to the Player class, and display it on a tab in the UI.

 

 

Steps

Step 1: Open Engine\Models\Location.cs

We’ll add a new List<Quest> property, to hold quests available at the Location.

I’m making it a list property, so we can have multiple quests available at a location. In the future, we may create quest “chains”, where the player needs to complete one quest, before they can receive the next quest in the “chain”.

 

Location.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Engine.Models
{
    public class Location
    {
        public int XCoordinate { get; set; }
        public int YCoordinate { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public string ImageName { get; set; }
        public List<Quest> QuestsAvailableHere { get; set; } = new List<Quest>();
    }
}

 

Step 2: Open Engine\WorldFactory.cs

After we create the Herbalist’s Hut location (position 0, 1), we will add the “Clear the Herb Garden” quest to its new QuestsAvailableHere property.

newWorld.LocationAt(0, 1) returns a Location object, which has its QuestsAvailableHere property. We can directly add the quest to this location by calling the code in this order.

 

WorldFactory.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Engine.Models;

namespace Engine.Factories
{
    internal static class WorldFactory
    {
        internal static World CreateWorld()
        {
            World newWorld = new World();

            newWorld.AddLocation(-2, -1, "Farmer's Field", 
                "There are rows of corn growing here, with giant rats hiding between them.", 
                "/Engine;component/Images/Locations/FarmFields.png");

            newWorld.AddLocation(-1, -1, "Farmer's House",
                "This is the house of your neighbor, Farmer Ted.",
                "/Engine;component/Images/Locations/Farmhouse.png");

            newWorld.AddLocation(0, -1, "Home", 
                "This is your home", 
                "/Engine;component/Images/Locations/Home.png");

            newWorld.AddLocation(-1, 0, "Trading Shop",
                "The shop of Susan, the trader.",
                "/Engine;component/Images/Locations/Trader.png");

            newWorld.AddLocation(0, 0, "Town square",
                "You see a fountain here.",
                "/Engine;component/Images/Locations/TownSquare.png");

            newWorld.AddLocation(1, 0, "Town Gate",
                "There is a gate here, protecting the town from giant spiders.",
                "/Engine;component/Images/Locations/TownGate.png");

            newWorld.AddLocation(2, 0, "Spider Forest",
                "The trees in this forest are covered with spider webs.",
                "/Engine;component/Images/Locations/SpiderForest.png");

            newWorld.AddLocation(0, 1, "Herbalist's hut",
                "You see a small hut, with plants drying from the roof.",
                "/Engine;component/Images/Locations/HerbalistsHut.png");

            newWorld.LocationAt(0, 1).QuestsAvailableHere.Add(QuestFactory.GetQuestByID(1));

            newWorld.AddLocation(0, 2, "Herbalist's garden",
                "There are many plants here, with snakes hiding behind them.",
                "/Engine;component/Images/Locations/HerbalistsGarden.png");

            return newWorld;
        }
    }
}

 

Step 3: Create a new class Engine\Models\QuestStatus.cs

This is like the ItemQuantity class – except, instead of tracking the quantity of an item, we will track the completion status of a quest.

When the player receives a quest, we will create a new QuestStatus object, and set the IsCompleted status to “false”. Later, when the player completes the quest, we will set IsCompleted to “true”.

 

QuestStatus.cs

namespace Engine.Models
{
    public class QuestStatus
    {
        public Quest PlayerQuest { get; set; }
        public bool IsCompleted { get; set; }

        public QuestStatus(Quest quest)
        {
            PlayerQuest = quest;
            IsCompleted = false;
        }
    }
}

Step 4: Open Engine\Models\Player.cs

Like the Player Inventory property, we will create a new Quests property. This is an ObservableCollection<QuestStatus>, so it can automatically update the UI when the Player receives a new quest, or completes one of their existing quests.

 

Player.cs

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Engine.Models
{
    public class Player : BaseNotificationClass
    {
        private string _name;
        private string _characterClass;
        private int _hitPoints;
        private int _experiencePoints;
        private int _level;
        private int _gold;

        public string Name
        {
            get { return _name; }
            set
            {
                _name = value; 
                OnPropertyChanged(nameof(Name));
            }
        }

        public string CharacterClass
        {
            get { return _characterClass; }
            set
            {
                _characterClass = value; 
                OnPropertyChanged(nameof(CharacterClass));
            }
        }

        public int HitPoints
        {
            get { return _hitPoints; }
            set
            {
                _hitPoints = value; 
                OnPropertyChanged(nameof(HitPoints));
            }
        }

        public int ExperiencePoints
        {
            get { return _experiencePoints; }
            set
            {
                _experiencePoints = value; 
                OnPropertyChanged(nameof(ExperiencePoints));
            }
        }

        public int Level
        {
            get { return _level; }
            set
            {
                _level = value; 
                OnPropertyChanged(nameof(Level));
            }
        }

        public int Gold
        {
            get { return _gold; }
            set
            {
                _gold = value;
                OnPropertyChanged(nameof(Gold));
            }
        }

        public ObservableCollection<GameItem> Inventory { get; set; }

        public ObservableCollection<QuestStatus> Quests { get; set; }

        public Player()
        {
            Inventory = new ObservableCollection<GameItem>();
            Quests = new ObservableCollection<QuestStatus>();
        }
    }
}

 

Step 5: Open Engine\ViewModels\GameSession.cs

This class handles player movement. So, we will modify it to check if there is a new quest available, when the player moves to a new location.

We need to add “using System.Linq;”, so we can use LINQ to search through the CurrentPlayer’s Quests list – to ensure we don’t give the player a quest they already have (in the GivePlayerQuestsAtLocation function).

We also need to update the “set” for the CurrentLocation property. Now, when the player moves to a new location, and CurrentLocation is updated, we will call GivePlayerQuestsAtLocation the new function at the end of the GaneSession.cs.

 

GameSession.cs

using System.Linq;
using Engine.Factories;
using Engine.Models;

namespace Engine.ViewModels
{
    public class GameSession : BaseNotificationClass
    {
        private Location _currentLocation;

        public World CurrentWorld { get; set; }
        public Player CurrentPlayer { get; set; }

        public Location CurrentLocation
        {
            get { return _currentLocation; }
            set
            {
                _currentLocation = value;

                OnPropertyChanged(nameof(CurrentLocation));
                OnPropertyChanged(nameof(HasLocationToNorth));
                OnPropertyChanged(nameof(HasLocationToEast));
                OnPropertyChanged(nameof(HasLocationToWest));
                OnPropertyChanged(nameof(HasLocationToSouth));

                GivePlayerQuestsAtLocation();
            }
        }

        public bool HasLocationToNorth
        {
            get
            {
                return CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate + 1) != null;
            }
        }

        public bool HasLocationToEast
        {
            get
            {
                return CurrentWorld.LocationAt(CurrentLocation.XCoordinate + 1, CurrentLocation.YCoordinate) != null;
            }
        }

        public bool HasLocationToSouth
        {
            get
            {
                return CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate - 1) != null;
            }
        }

        public bool HasLocationToWest
        {
            get
            {
                return CurrentWorld.LocationAt(CurrentLocation.XCoordinate - 1, CurrentLocation.YCoordinate) != null;
            }
        }

        public GameSession()
        {
            CurrentPlayer = new Player
                            {
                                Name = "Scott",
                                CharacterClass = "Fighter",
                                HitPoints = 10,
                                Gold = 1000000,
                                ExperiencePoints = 0,
                                Level = 1
                            };

            CurrentWorld = WorldFactory.CreateWorld();

            CurrentLocation = CurrentWorld.LocationAt(0, 0);
        }

        public void MoveNorth()
        {
            if(HasLocationToNorth)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate + 1);
            }
        }

        public void MoveEast()
        {
            if(HasLocationToEast)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate + 1, CurrentLocation.YCoordinate);
            }
        }

        public void MoveSouth()
        {
            if(HasLocationToSouth)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate - 1);
            }
        }

        public void MoveWest()
        {
            if(HasLocationToWest)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate - 1, CurrentLocation.YCoordinate);
            }
        }

        private void GivePlayerQuestsAtLocation()
        {
            foreach(Quest quest in CurrentLocation.QuestsAvailableHere)
            {
                if(!CurrentPlayer.Quests.Any(q => q.PlayerQuest.ID == quest.ID))
                {
                    CurrentPlayer.Quests.Add(new QuestStatus(quest));
                }
            }
        }
    }
}

 

Step 6: Open QPFUI\MainWindow.xaml

Finally, we will add another TabItem, to display the player’s quest, and its completion status.

This is like the Inventory tab. We bind it to the Player’s Quests property and configure the columns to display.

Notice that the first column is binding to PlayerQuest.Name. This is because we are binding to a list of QuestStatus objects. The QuestStatus.PlayerQuest property is a Quest object, which has a Name property. So, we are displaying a property of a property.

The new code is the TabItem for the Quests, at lines 121 to 134.

 

MainWindow.xaml

<Window x:Class="WPFUI.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPFUI"
        mc:Ignorable="d"
        FontSize="11pt"
        Title="Scott's Awesome Game" Height="768" Width="1024">

    <Window.Resources>
        <BooleanToVisibilityConverter x:Key="BooleanToVisibility" />
    </Window.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="225"/>
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="250"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Content="Menu" Background="AliceBlue"/>

        <Grid Grid.Row="1" Grid.Column="0" Background="Aquamarine">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            
            <Label Grid.Row="0" Grid.Column="0" Content="Name:"/>
            <Label Grid.Row="0" Grid.Column="1" Content="{Binding CurrentPlayer.Name}"/>
            <Label Grid.Row="1" Grid.Column="0" Content="Class:"/>
            <Label Grid.Row="1" Grid.Column="1" Content="{Binding CurrentPlayer.CharacterClass}"/>
            <Label Grid.Row="2" Grid.Column="0" Content="Hit points:"/>
            <Label Grid.Row="2" Grid.Column="1" Content="{Binding CurrentPlayer.HitPoints}"/>
            <Label Grid.Row="3" Grid.Column="0" Content="Gold:"/>
            <Label Grid.Row="3" Grid.Column="1" Content="{Binding CurrentPlayer.Gold}"/>
            <Label Grid.Row="4" Grid.Column="0" Content="XP:"/>
            <Label Grid.Row="4" Grid.Column="1" Content="{Binding CurrentPlayer.ExperiencePoints}"/>
            <Label Grid.Row="5" Grid.Column="0" Content="Level:"/>
            <Label Grid.Row="5" Grid.Column="1" Content="{Binding CurrentPlayer.Level}"/>
        </Grid>

        <Grid Grid.Row="1" Grid.Column="1"
              Background="Beige">
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="3*"/>
                <ColumnDefinition Width="2*"/>
            </Grid.ColumnDefinitions>
            
            <Border Grid.Row="0" Grid.Column="1"
                    BorderBrush="Gainsboro"
                    BorderThickness="1">
                
                <Grid Margin="3">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="*"/>
                        <RowDefinition Height="Auto"/>
                    </Grid.RowDefinitions>
                    
                    <TextBlock Grid.Row="0"
                               HorizontalAlignment="Center"
                               Text="{Binding CurrentLocation.Name}"/>
                    
                    <Image Grid.Row="1"
                           HorizontalAlignment="Center"
                           VerticalAlignment="Center"
                           Height="125"
                           Width="125"
                           Source="{Binding CurrentLocation.ImageName}"/>
                    
                    <TextBlock Grid.Row="2"
                               HorizontalAlignment="Center"
                               Text="{Binding CurrentLocation.Description}"
                               TextWrapping="Wrap"/>
                </Grid>
                
            </Border>
        </Grid>

        <Grid Grid.Row="2" Grid.Column="0"
              Background="BurlyWood">
        
            <TabControl>
                <TabItem Header="Inventory">
                    <DataGrid ItemsSource="{Binding CurrentPlayer.Inventory}"
                              AutoGenerateColumns="False"
                              HeadersVisibility="Column">
                        <DataGrid.Columns>
                            <DataGridTextColumn Header="Description"
                                                Binding="{Binding Name}"
                                                Width="*"/>
                            <DataGridTextColumn Header="Price"
                                                Binding="{Binding Price}"
                                                Width="Auto"/>
                        </DataGrid.Columns>
                    </DataGrid>
                </TabItem>

                <TabItem Header="Quests">
                    <DataGrid ItemsSource="{Binding CurrentPlayer.Quests}"
                              AutoGenerateColumns="False"
                              HeadersVisibility="Column">
                        <DataGrid.Columns>
                            <DataGridTextColumn Header="Name"
                                                Binding="{Binding PlayerQuest.Name}"
                                                Width="*"/>
                            <DataGridTextColumn Header="Done?"
                                                Binding="{Binding IsCompleted}"
                                                Width="Auto"/>
                        </DataGrid.Columns>
                    </DataGrid>
                </TabItem>
            </TabControl>
        </Grid>

        <Grid Grid.Row="2" Grid.Column="1"
              Background="Lavender">

            <Grid.RowDefinitions>
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>

            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="255" />
            </Grid.ColumnDefinitions>
            
            <Grid Grid.Row="0" Grid.Column="1">

                <Grid.RowDefinitions>
                    <RowDefinition Height="*" />
                    <RowDefinition Height="*" />
                    <RowDefinition Height="*" />
                </Grid.RowDefinitions>

                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>

                <Button Grid.Row="0" Grid.Column="1" 
                        Height="25" Width="65" Margin="10" 
                        Click="OnClick_MoveNorth"
                        Visibility="{Binding HasLocationToNorth, Converter={StaticResource BooleanToVisibility}}"
                        Content="North"/>
                <Button Grid.Row="1" Grid.Column="0" 
                        Height="25" Width="65" Margin="10" 
                        Click="OnClick_MoveWest"
                        Visibility="{Binding HasLocationToWest, Converter={StaticResource BooleanToVisibility}}"
                        Content="West"/>
                <Button Grid.Row="1" Grid.Column="2" 
                        Height="25" Width="65" Margin="10" 
                        Click="OnClick_MoveEast"
                        Visibility="{Binding HasLocationToEast, Converter={StaticResource BooleanToVisibility}}"
                        Content="East"/>
                <Button Grid.Row="2" Grid.Column="1" 
                        Height="25" Width="65" Margin="10" 
                        Click="OnClick_MoveSouth"
                        Visibility="{Binding HasLocationToSouth, Converter={StaticResource BooleanToVisibility}}"
                        Content="South"/>

            </Grid>

        </Grid>

    </Grid>
</Window>

 

Return to main page

13 thoughts on “Lesson 06.2: Using Quests in the Game

  1. Scott,

    I am interested in following along with this project. Is there a lesson where I can download the source code and start at a mid-point? Or, is it better to just start from the beginning. The reason I ask is I don’t have a lot of time to get started. I would like to be caught up so I can do each lesson as you release them.

    Thanks,

    Zach

        1. I am all caught up! Your tutorials are very clear and straight forward. I am going to make a copy of the solution to tinker with and keep one for when you come out with new lessons.

          Thanks again!

  2. Hi Scott,

    I m using visual studio 2013
    My i know how to convert this

    public List QuestsAvailableHere { get; set; } = new List(); (C#7.0)

    to 2013 version?

    Thanks

    1. Hi Marcus,

      With earlier versions of C#, you will need to set the property’s initial value inside the constructor, instead of on the same line where you declare it.

      So, you would declare the property with:
      public List QuestsAvailableHere { get; set; }

      Then, in the constructor, add this line:
      QuestsAvailableHere = new List();

      Please tell me if that does not work, or if you have any questions.

      1. HI Scott ,
        Sorry for late reply.The sample is working.

        My Code
        public List QuestsAvailableHere { get; set; }
        public Location()
        {
        QuestsAvailableHere = new List();

        }

        Thanks a lot.

        Regards
        Marcus

  3. Hi Scott, first post for me on your site. I wanted to say how much I have been enjoying your tutorial and how I am looking forward to taking what is made here further myself. A quick question could you explain line 116 of the GameSession.cs a little more

    if(!CurrentPlayer.Quests.Any(q => q.PlayerQuest.ID == quest.ID))

    I understand most of it my only question is the q part of it (q => q.PlayerQuest.ID) what is the q stand for I and why do we need it? Sorry this is probably a really stupid question.

    1. Hi Benjamin,

      The “q” is used as a variable to hold each object in the collection.

      In the “Any” method, we want to see if any of the Quest objects in CurrentPlayer.Quests have an ID that matches the ID of the Quest object in the “quest” variable. To do that comparison, we’re going to put each Quest object (from “CurrentPlayer.Quests”) into the “q” variable, and perform the comparison “q.PlayerQuest.ID == quest.ID”.

      If we didn’t use LINQ, we could write the code like this (I didn’t build this code, so there could be an error). Notice the “q” variable in the “foreach” line. This is basically the same “q” variable in the LINQ statement:

      bool playerHasQuest = false;
      foreach(Quest q in CurrentPlayer.Quests)
      {
      if(q.PlayerQuest.ID == quest.ID)
      {
      playerHasQuest = true;
      }
      }

      Let me know if that isn’t clear, or if you have any other questions.

  4. In line 116 of the GameSession.cs a little more

    if(!CurrentPlayer.Quests.Any(q => q.PlayerQuest.ID == quest.ID))

    What does => mean. I always thought it was “a greater than or equals” symbol. Does it mean something different in this code?

    1. The => operator is used for lambda expressions (https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-operator).

      The Any() function is looking for an equation that will evaluate to “true” or “false”. It’s going to run against all the objects in the CurrentPlayer.Quests collection. For each object in the collection, it will assign it to the “q” variable (on the left side of the lambda operator), use that “q” variable in the equation on the right of the lambda operator “q.PlayerQuest.ID == quest.ID”, and determine if that object returns a true or false for that equation.

      That line of code is about the same as this (assuming I don’t make a mistake):


      bool playerHasQuestWithTheSameID = false;

      foreach(Quest q in CurrentPlayer.Quests)
      {
      if(q.PlayerQuest.ID == quest.ID)
      {
      playerHasQuestWithTheSameID = true;
      }
      }

      if(playerHasQuestWithTheSameID)
      {
      ... do the code inside the original "if"
      }

      The => is also used for “expression-bodied members“, which you might use in place of a very small function.

Leave a Reply

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