Lesson 2.9: Creating The Game World

The location was a single spot in a larger game world. We want to represent the game world as a grid of places that the player can move to. We will manage that by creating a new World model class that encapsulates the structure of the world and exposes some methods for retrieving location data. This allows us to hide the implementation details of the game world structure so that we can adapt it as needed throughout the lifetime of the game.

World Model Class

In the SimpleRPG.Game.Engine project and Models folder, create the World class.

using System;
using System.Collections.Generic;
using System.Linq;

namespace SimpleRPG.Game.Engine.Models
{
    public class World
    {
        private readonly IList<Location> locations;

        public World(IEnumerable<Location> locs)
        {
            this.locations = locs is null ? new List<Location>() : locs.ToList();
        }

        public Location LocationAt(int xCoordinate, int yCoordinate)
        {
            var loc = locations.FirstOrDefault(p => p.XCoordinate == xCoordinate && p.YCoordinate == yCoordinate);
            return loc ?? throw new ArgumentOutOfRangeException("Coordinates", "Provided coordinates could not be found in game world.");
        }

        public Location GetHomeLocation()
        {
            return this.LocationAt(0, -1);
        }
    }
}

The World class has a constructor that takes an enumeration of Locations that represent the data of the game world. This constructor allows us to set the data for the world, so that we can set it differently between production and test environments. This helps with our testability.

Ternary operator

This line of code in the constructor has some interesting C# code examples:

this.locations = locs is null ? new List<Location>() : locs.ToList();

The ?: ternary operator is short hand for an if-then-else block with simple statements. It means that: condition ? consequent : alternative. If the condition is true, then return the consequent. If it is false, return the alternative. This code could be written out as the following, but it makes code cumbersome to read at times:

if (locs is null)
{
    this.locations = new List<Location>();
}
else
{
    this.locations = locs.ToList();
}

is null Operator

Also, the is null operator is the equivalent of (locs == null) check. For new developers, this makes the code more understandable. Note: in C# 9, there will also be an is not null operator.

Linq Extensions

The LocationAt method takes x and y coordinates and tries to find if that location exists in our grid. It calls the locations.FirstOrDefault method. This method is provided by the Linq extensions to the IEnumerable interface, and returns the first element that meets the specified criteria… If no element is found, it returns the default value for this type, which is null.

The search criteria for FirstOrDefault method is defined by a user provided function. That function is defined as: Func<TSource,bool> predicate, which represents a function that takes one parameter of type TSource (which is the type of the IEnumerable) and returns a bool => true if the condition is true; false if the condition is false.

Lambda Expressions

We then use a lambda expression to provide that function: p is the parameter; the condition check is p.XCoordinate == xCoordinate && p.YCoordinate == yCoordinate. This check is called for every element of the list, until the first element that satisfies that condition is true. When that element is found it is returned. That one line of code is equivalent to the following code block:

    foreach (var p in locations)
    {
        if (p.XCoordinate == xCoordinate && p.YCoordinate == yCoordinate)
        {
            return p;
        }
    }

    return default;

Null-Coalescing Operator

The LocationAt method’s last line uses the null-coalescing operator (??). The ?? operator doesn’t evaluate its right-hand operand if the left-hand operand evaluates to non-null. So in our code above, we return the loc variable if it is not null. If it is null, we throw an ArgumentOutOfRangeException. This is also a convenient way of writing the following code:

if (loc == null)
{
    throw new ArgumentOutOfRangeException("Coordinates", "Provided coordinates could not be found in game world.");
}

return loc;

The code in the World class wasn’t a lot of code, but it was densely packed with many new(ish) C# language constructs. We should get familiar with them because much of the new code in .NET Core and open source projects use these constructs. We need to be comfortable writing and reviewing code that uses them.

The World Factory

To create the game world with all of its locations, we will use the Factory Method design pattern to create the World object and populate it with all of the locations. The Factory Method design pattern is used, when we need to create the object without exposing the object creation logic to the client. To achieve this, we will create a Factory class which will create and return the instance of the populated World class.

We want to hide the world creation logic because in the future we may want to load the world location data from a file or retrieve it from a web service. Keeping creation in this factory class makes it easy to change it later, without having that logic spread throughout the game.

In the SimpleRPG.Game.Engine project, create a Factories folder and then a WorldFactory class within it. We are putting into this folder because we will be creating more factories in upcoming lessons.

using SimpleRPG.Game.Engine.Models;
using System.Collections.Generic;

namespace SimpleRPG.Game.Engine.Factories
{
    internal static class WorldFactory
    {
        internal static World CreateWorld()
        {
            var locations = new List<Location>
            {
                new Location
                {
                    XCoordinate = -2,
                    YCoordinate = -1,
                    Name = "Farmer's Field",
                    Description = "There are rows of corn growing here, with giant rats hiding between them.",
                    ImageName = "/images/locations/FarmFields.png"
                },
                new Location
                {
                    XCoordinate = -1,
                    YCoordinate = -1,
                    Name = "Farmer's House",
                    Description = "This is the house of your neighbor, Farmer Ted.",
                    ImageName = "/images/locations/Farmhouse.png"
                },
                new Location
                {
                    XCoordinate = 0,
                    YCoordinate = -1,
                    Name = "Home",
                    Description = "This is your home.",
                    ImageName = "/images/locations/Home.png"
                },
                new Location
                {
                    XCoordinate = -1,
                    YCoordinate = 0,
                    Name = "Trading Shop",
                    Description = "The shop of Susan, the trader.",
                    ImageName = "/images/locations/Trader.png"
                },
                new Location
                {
                    XCoordinate = 0,
                    YCoordinate = 0,
                    Name = "Town Square",
                    Description = "You see a fountain here.",
                    ImageName = "/images/locations/TownSquare.png"
                },
                new Location
                {
                    XCoordinate = 1,
                    YCoordinate = 0,
                    Name = "Town Gate",
                    Description = "There is a gate here, protecting the town from giant spiders.",
                    ImageName = "/images/locations/TownGate.png"
                },
                new Location
                {
                    XCoordinate = 2,
                    YCoordinate = 0,
                    Name = "Spider Forest",
                    Description = "The trees in this forest are covered with spider webs.",
                    ImageName = "/images/locations/SpiderForest.png"
                },
                new Location
                {
                    XCoordinate = 0,
                    YCoordinate = 1,
                    Name = "Herbalist's Hut",
                    Description = "You see a small hut, with plants drying from the roof.",
                    ImageName = "/images/locations/HerbalistsHut.png"
                },
                new Location
                {
                    XCoordinate = 0,
                    YCoordinate = 2,
                    Name = "Herbalist's Garden",
                    Description = "There are many plants here, with snakes hiding behind them.",
                    ImageName = "/images/locations/HerbalistsGarden.png"
                },
            };

            return new World(locations);
        }
    }
}

First, we create an in-memory list of hard-coded game locations and their interlocked positions. These locations represent the map we saw in the last lesson:

Screenshot of game world
Fig 1 – Game location map

Then, we create an instance of the World class with the locations list.

To complete the Factory pattern, we have the class and method defined as static because they don’t use any local data members or state, and it makes it easier to call the factory method.

Now, let’s update the GameSession class to use the WorldFactory to create and retrieve the World object.

using SimpleRPG.Game.Engine.Factories;
using SimpleRPG.Game.Engine.Models;

namespace SimpleRPG.Game.Engine.ViewModels
{
    public class GameSession : IGameSession
    {
        public World CurrentWorld { get; private set; }

        public Player CurrentPlayer { get; private set; }

        public Location CurrentLocation { get; private set; }

        public GameSession()
        {
            this.CurrentPlayer = new Player
            {
                Name = "DarthPedro",
                CharacterClass = "Fighter",
                HitPoints = 10,
                Gold = 1000,
                ExperiencePoints = 0,
                Level = 1
            };

            this.CurrentWorld = WorldFactory.CreateWorld();
            this.CurrentLocation = this.CurrentWorld.GetHomeLocation();
        }

        public void AddXP()
        {
            this.CurrentPlayer.ExperiencePoints += 10;
        }
    }
}

After we create the game world, we call the GetHomeLocation and save it as the CurrentLocation in the GameSession.

That’s it, we’ve introduced the concept of the game world into our engine and used it to set the current location. If we build and run the game, everything should run fine, but there are no visible change yet.

In this lesson, we covered a lot of different C# coding features and introduced the Factory Method design pattern. Let’s make sure that we’re familiar with those concepts because those constructs and coding style will be used through the lessons.

Next lesson, we will take a look at moving between locations in the game world.

One thought on “Lesson 2.9: Creating The Game World

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s