Press "Enter" to skip to content

Lesson 04.2: Creating the World

In this lesson, we will create a World class, to manage the Location objects that make up our game world.

Step 1: Create a new World class, in the Engine project, inside the Models folder.

Make the class public, because we will need to use it in other projects (the UI project).

World.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Engine.Models
{
    public class World
    {
    }
}

Step 2: In the GameSession class, add this new property, to hold a World object:

GameSession.cs
public World CurrentWorld { get; set; }

We could instantiate a new World object, inside the GameSession constructor, and save it in the CurrentWorld property. However, we are going to need to add a lot of code in the World class constructor. That is where we will create all the Location objects. We are also going to (in future lessons) have the option to populate the World Locations from a database or file. So, we will create a “factory” class, to instantiate and populate a World object.

Step 3: In the Engine project, create a new folder named “Factories”. C# does not require the name to be Factories, but that is what we will use for this project.

Add a new class in the Factories folder, named WorldFactory. Set its scope to “internal”, instead of “public”. By default, classes are internal. But, it’s a good idea to add this, so it is obvious to anyone reading the code.

An “internal” class can only be used by other classes inside the same project. The only class that should ever be using the WorldFactory class is the GameSession class – which is also inside the Engine project.

It’s usually a good idea to make the scope as restrictive as possible. If everything is public, then your classes/properties/methods can be accessed by any other class in the solution. In that situation, there are more places where a value could be changed, making it more difficult to track down problems. By limiting scope, you limit the possible sources of problems.

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 class WorldFactory
    {
    }
}

Step 4: In the WorldFactory class, create a method (function) to instantiate a new World object, and return it to the code that called the function.

The scope of the CreateWorld function is “internal”, so it can only be called by other classes inside the Engine project. After the “internal” is “World” – the datatype of the object that this method will return to its caller.

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 class WorldFactory
    {
        internal World CreateWorld()
        {
            return new World();
        }
    }
}

Now, inside the GameSession class constructor, you can create a WorldFactory object, and save it to a variable named “factory”. Then, populate the CurrentWorld property by calling the CreateWorld function on the “factory” object.

GameSession.cs constructor
        public GameSession()
        {
            CurrentPlayer = new Player();
            CurrentPlayer.Name = "Scott";
            CurrentPlayer.CharacterClass = "Fighter";
            CurrentPlayer.HitPoints = 10;
            CurrentPlayer.Gold = 1000000;
            CurrentPlayer.ExperiencePoints = 0;
            CurrentPlayer.Level = 1;
            WorldFactory factory = new WorldFactory();
            CurrentWorld = factory.CreateWorld();
            CurrentLocation = CurrentWorld.LocationAt(0, -1);
        }

Step 5: Now, we will populate the World with the game’s Locations.

Inside the World class, we declare a “_locations” variable on line 12. Its datatype is “List“. That means it can hold multiple Location objects in it. Lists are part of C# “Generics”, which is why we need to have the “using System.Collections.Generic;” on line 2.

We could populate the _locations list inside a constructor. However, we’ll use a function to add Locations. This is the “AddLocation” function.

We only want this function called by the WorldFactory, which is inside the Engine project. So, we can declare the function’s scope as “internal”.

To create a Location, we need to have a X coordinate, Y coordinate, name, description, and image file name. So, we will make those parameters of the function. When the WorldFactory class calls AddLocation, it will pass values for these parameters. Then, the parameters will be used to create a new Location object (lines 16 to 20). Finally, the Location object will be added to the list of Locations for the World (line 22).

Notice that the parameters have the same names as the properties, except that the first letters are lower-case for the parameters. This is not a requirement of C#. However, it is a very common standard.

C# is case-sensitive. So, a variable named “name” is different from one named “Name”. By using the same word for the parameters, and the properties, it is obvious which parameters go to which properties. However, by using different upper/lower casing, it distinguishes the properties and the parameters as different things.

World.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Engine.Models
{
    public class World
    {
        private List<Location> _locations = new List<Location>();
        internal void AddLocation(int xCoordinate, int yCoordinate, string name, string description, string imageName)
        {
            Location loc = new Location();
            loc.XCoordinate = xCoordinate;
            loc.YCoordinate = yCoordinate;
            loc.Name = name;
            loc.Description = description;
            loc.ImageName = imageName;
            _locations.Add(loc);
        }
   }
}

Step 6: We can go back to the WorldFactory class and change the CreateWorld function to the code below. This will populate the game’s locations into the World object, by calling the AddLocation function, with the parameter values for each location.

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 class WorldFactory
    {
        internal 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.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 7: Next, we need a way to retrieve Locations from the World object.

In the World class, we will add a new “LocationAt” function where we can pass in X and Y coordinates, and get the Location object at those coordinates – if one exists.

One way to look at the objects in a List is to use a “foreach” loop.

On line 27 (of the code below), we start a “foreach” loop. It will get the first Location in the list (if there are any) and put it into the “loc” variable, whose datatype is Location. Then, inside the loop, we can use that “loc” variable.

For this function, we will check if the XCoordinate and YCoordinate of the “loc” variable matches the values passed into LocationAt. If the coordinates match, we will return the “loc” object to the calling code (which also means it will stop running the code inside the function). If not, the “foreach” loop will get the next object in the _locations list, assign it to the “loc” variable, and go through the code inside the loop again.

If there is not a Location with matching coordinates, the “foreach” loop will stop, and the rest of the function will run. In this function, the only other code in the function is “return null;”. So, if there isn’t a matching Location, the LocationAt function will return “null”, which is the C# way to say “nothing”.

NOTES: The double equal signs “==” are used to check if two values are equal. The single equal sign “=” is used when you want to assign a value to a variable or property. Also, the “&&” is how you write an “if” statement where all conditions must be true (the X coordinate must match, and the Y coordinate must match).

World.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Engine.Models
{
    public class World
    {
        private List<Location> _locations = new List<Location>();
        internal void AddLocation(int xCoordinate, int yCoordinate, string name, string description, string imageName)
        {
            Location loc = new Location();
            loc.XCoordinate = xCoordinate;
            loc.YCoordinate = yCoordinate;
            loc.Name = name;
            loc.Description = description;
            loc.ImageName = imageName;
            _locations.Add(loc);
        }
        public Location LocationAt(int xCoordinate, int yCoordinate)
        {
            foreach(Location loc in _locations)
            {
                if(loc.XCoordinate == xCoordinate && loc.YCoordinate == yCoordinate)
                {
                    return loc;
                }
            }
            return null;
        }
    }
}

Step 8: Now we can go back to the GameSession class and set the CurrentLocation property by using the LocationAt function.

We can delete the old code that set the CurrentLocation property, and replace it with a call to CurrentWorld.LocationAt(0, 0).

GameSession.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Engine.Models;
using Engine.Factories;
namespace Engine.ViewModels
{
    public class GameSession
    {
        public World CurrentWorld { get; set; }
        public Player CurrentPlayer { get; set; }
        public Location CurrentLocation { get; set; }
        public GameSession()
        {
            CurrentPlayer = new Player();
            CurrentPlayer.Name = "Scott";
            CurrentPlayer.CharacterClass = "Fighter";
            CurrentPlayer.HitPoints = 10;
            CurrentPlayer.Gold = 1000000;
            CurrentPlayer.ExperiencePoints = 0;
            CurrentPlayer.Level = 1;
            WorldFactory factory = new WorldFactory();
            CurrentWorld = factory.CreateWorld();
            CurrentLocation = CurrentWorld.LocationAt(0, 0);
        }
    }
}

FINAL STEP: While testing if the game displays the Location information, I noticed that long location descriptions are cut off on the screen. To fix this, open MainWindow.xaml and find the TextBlock that displays the CurrentLocation.Description. Change it to include the TextWrapping attribute shown below:

<TextBlock Grid.Row="2"
           HorizontalAlignment="Center"
           Text="{Binding CurrentLocation.Description}"
           TextWrapping="Wrap"/>
World.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Engine.Models
{
    public class World
    {
        private List<Location> _locations = new List<Location>();
        internal void AddLocation(int xCoordinate, int yCoordinate, string name, string description, string imageName)
        {
            Location loc = new Location();
            loc.XCoordinate = xCoordinate;
            loc.YCoordinate = yCoordinate;
            loc.Name = name;
            loc.Description = description;
            loc.ImageName = imageName;
            _locations.Add(loc);
        }
        public Location LocationAt(int xCoordinate, int yCoordinate)
        {
            foreach(Location loc in _locations)
            {
                if(loc.XCoordinate == xCoordinate && loc.YCoordinate == yCoordinate)
                {
                    return loc;
                }
            }
            return null;
        }
    }
}
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 class WorldFactory
    {
        internal 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.AddLocation(0, 2, "Herbalist's garden",
                "There are many plants here, with snakes hiding behind them.",
                "/Engine;component/Images/Locations/HerbalistsGarden.png");
            return newWorld;
        }
    }
}
GameSession.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Engine.Models;
using Engine.Factories;
namespace Engine.ViewModels
{
    public class GameSession
    {
        public World CurrentWorld { get; set; }
        public Player CurrentPlayer { get; set; }
        public Location CurrentLocation { get; set; }
        public GameSession()
        {
            CurrentPlayer = new Player();
            CurrentPlayer.Name = "Scott";
            CurrentPlayer.CharacterClass = "Fighter";
            CurrentPlayer.HitPoints = 10;
            CurrentPlayer.Gold = 1000000;
            CurrentPlayer.ExperiencePoints = 0;
            CurrentPlayer.Level = 1;
            WorldFactory factory = new WorldFactory();
            CurrentWorld = factory.CreateWorld();
            CurrentLocation = CurrentWorld.LocationAt(0, 0);
        }
    }
}
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">
    <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}"/>
            <Button Grid.Row="6" Grid.Column="1" Content="Add XP" Click="ButtonBase_OnClick"></Button>
        </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>
        <Label Grid.Row="2" Grid.Column="0" Content="Inventory/Quests" Background="BurlyWood"/>
        <Label Grid.Row="2" Grid.Column="1" Content="Combat/Movement Controls" Background="Lavender"/>
    </Grid>
</Window>

NEXT LESSON: Lesson 04.3: Moving in the game world

PREVIOUS LESSON: Lesson 04.1: Creating the Location class

6 Comments

  1. Old Jobobo
    Old Jobobo 2022-12-03

    When I get it all done to the point of the home location and do the initial test run everything works except I no longer see the location info in the upper right corner.
    I’ve gone over the code and everything is as you have it but the location view does not populate.

    • Old Jobobo
      Old Jobobo 2022-12-03

      Why is this always returning null? the debug line inside the if loop fires off so I no it is resolving but the method always ends up null.

      public Location LocationAt(int xCoordinate, int yCoordinate)
      {
      Debug.WriteLine(“locAt: ” + xCoordinate + “, ” + yCoordinate);

      bool found = false;

      foreach(Location loc in _locations)
      {

      if (loc.XCoordinate == xCoordinate && loc.YCoordinate == yCoordinate)
      {
      Debug.WriteLine(loc.Name + ” inside ifloop”);
      found = true;
      return loc;

      }

      }
      return null;
      }

      • SOSCSRPG
        SOSCSRPG 2022-12-04

        That would probably indicate there is not a matching location in the _locations list. Can you set a breakpoint in the code (see this, if you aren’t familiar with breakpoints: https://scottlilly.com/how-to-use-the-visual-studio-2013-debugger/) and check the contents of _locations, along with the values of the xCoordinate and yCoordinate?

        If that does not help you solve it, can you upload your solution (including the directories under it, and all the files in those directories) to GitHub, Dropbox, or some other file-sharing location so I can look at it?

        If you haven’t used GitHub before, here is some information (and a video) on how to upload your solution to GitHub and share it with me. https://codingwithscott.com/how-to-connect-visual-studio-community-edition-2022-to-github/

        • Old Jobobo
          Old Jobobo 2022-12-05

          ok I added you to the git of my tutorial project.

          • SOSCSRPG
            SOSCSRPG 2022-12-05

            Thanks. I found why the location was not being found.

            In the CreateWorld() function in WorldFactory.cs, we start this lesson by having “return new World();”. But, in the version of WorldFactory at the end, we have a “newWorld” variable that stores an instantiated World object. Next, we populate location in the newWorld variable. Finally, we return the populated newWorld variable from the CreateWorld function.

            However, your CreateWorld function has “return new World();”, instead of “return newWorld;” as the final line. So, it instantiated another World object and returned that new object – which did not have any locations populated. So, it should work if you change the last line of CreateWorld to “return newWorld;”. That will return the World object with the populated locations.

            Let me know if that was not clear, or if you have any other questions.

  2. Old Jobobo
    Old Jobobo 2022-12-06

    yup that would do it, and it works now, thanks so much for helping me and for making this tutorial. It is really helping me get the hang of WPF.

Leave a Reply

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