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<Location>”. 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"/>

 

 

Click here to view the final source code for all files

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>

 

Return to main page

18 thoughts on “Lesson 04.2: Creating the World

    1. That might affect the other parts of the screen we will add (the monster image, and the game messages). You can probably balance that by modifying the amount of space the rows and columns have, for the game area grid. Or, you could use a graphics program that lets you resize images, like Paint.NET – the free graphics editing program I use.

      1. I figured it would mess something up… just didnt want to have to go through all the images I found for it and resize them all, but thanks for the reply.

        When is 4.3 coming out?

  1. I have a problem with Visual studio saying “An exception of type ‘System.NullReferenceException’ occurred in Engine.dll but was not handled in user code”.

    How do I fix this error?

  2. Great tutorials! This one was my favorite so far. What about porting the game to the Monogame framework instead of Unity, in the future?

    1. Thank you! I probably won’t be creating any new versions after this WPF series. I’m trying to start a consulting business, and that is taking up a huge amount of my time.

  3. Hello please help me. When I run the program, nothing shows up in the box for the location information. I have no errors showing up in the error list and I carefully compared my code to yours, even changing slight differences, but I still can’t figure out what the problem is. Thank you!

      1. I was having the same problem and I did end up finding out what happen it is under the GameSession where the CurrentLocation = CurrentWorld.LocationAt(0, -1); you need to test it before you change it to 0,0 at which case it will not show up anything at all. That was what the problem was with me so at 15:46 that was when I saw what I had done wrong just that one number 0 instead of the -1.

  4. Hi, Scott.

    At first, I am really a fan of this tutorial.

    As a beginner, I am reading every lesson letter by letter to absorb as much as possible.
    During that, I found your expression of “the next variable” in step 7:
    “the “foreach” loop will get the next variable in the _locations list, assign it to “loc”, and go through the code inside the loop again.”

    For my understanding, it should be “the next element” under that context.
    Is there any special reason that you choose the variable instead?

    1. Hi Charles,

      There isn’t any specific reason I used “variable” instead of “element” or “object” (which would probably be more correct). I changed the text to say “the next object in the _locations list, assign it to the “loc” variable, and go through the code…” – to be a bit more precise.

  5. Hello! I’m really enjoying these lessons so far but I’ve hit a bit of a road block.

    When I try to run the program I’m getting an Exception Unhandled error where it says:
    “System.Reflection.TargetInvocationException: ‘Exception has been thrown by the target of an invocation.’

    Inner Exception
    NullReferenceException: Object reference not set to an instance of an object.”

    I’ve gone through everything a couple of times and can’t seem to find any reason for this. If anyone has a chance to take a look I’d greatly appreciate it!

    Here’s a dropbox link https://www.dropbox.com/sh/db5tn27x95u62by/AABRD73eLdRva5jREUBT4B1Na?dl=0

    1. Hi Stephen,

      I updated Step 4 of the lesson to show where the two lines need to be added in the GameSession constructor.

      The CurrentWorld property needs to populated before you can call CurrentWorld.LocationAt(0, -1), since that line is looking at the object in the CurrentWorld property.

      Let me know if that wasn’t clear, or if you encounter anything else. This got a little messed up when my source code formatting plug-in broke a few weeks ago, and I had to manually clean up most of the lessons.

Leave a Reply

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