Click here to Skip to main content
15,885,128 members
Articles / Game Development

Procedural Content Generation Part I: Generic Asset Factory

Rate me:
Please Sign up or sign in to vote.
5.00/5 (7 votes)
8 Sep 2017MIT10 min read 9.4K   11  
An extensible procedural generation framework

Introduction

Procedural generation is a technique in gaming to create dynamic content through algorithms and mathematics. There are many approaches to procedural generation, I will take you through a few techniques I have developed throughout the course of this article series. In the first chapter, we will create a generic factory pattern for loading and managing in game assets. We will compose these assets into scenes and then examine different approaches to procedurally create them at runtime in subsequent chapters.

Background

I have been interested in procedural content generation for many years. I have experimented with several different concepts and approaches with varying degrees of success. I do this as a hobby and I want to share some of my experience with the community. A lot of my work is based on techniques by Ken Perlin. Ken Perlin developed the Perlin Noise algorithm in 1982 for the movie Tron. He later won an academy award for his work. I also like games such as Minecraft and enjoy the procedurally generated voxel environment. My goal is to combine different approaches together to create a flexible toolbox for procedural generation in C#.

Overview

We are going to start by creating a generic asset management system that can be used to create and store 2D assets. The first step is to create a core of interfaces that will be the base of our generic type system. Once we have the interfaces setup, we can go about creating a specific implementation to produce content of our choice.

Image 1

I am going to start by creating a 2D tiling system that can create terrain dynamically. To render a decent looking map, we may need to load and track many assets. This system will allow us to manage the lifecycle of the procedurally generated assets. We will cover Agents and the IAgent interface in a later chapter. To keep it simple, we are just going to render this work to the console for now. It will be easy to extend this design to other platforms like GDI+, XNA, MonoGame, or other gaming platforms!

Scenes

The first step is to create a scene interface. A scene is a collection of game assets arranged spatially in either 2D or 3D space. We will start with 2D and then explore ways to extend this to 3D in subsequent chapters. The IScene interface will be responsible for storing and managing the lifecycle of in game assets.

C#
// The Scene interface.
public interface IScene
{
    // Registers an Asset of type T with the Abstract Factory.
    IScene RegisterAsset<T>()
        where T : class, IAsset;
 
    // Registers Loaders of type T2 for type T1. Implements the Abstract Factory for type T1.
    IScene RegisterLoader<T1, T2>()
        where T1 : class, IAsset
        where T2 : class, ILoader;

     // Gets a list of IAssets of type T.
     List<IAsset> GetAssets<T>() where T : class, IAsset;
 
     // Queries a list of IAssets of type T within a boundary.
     List<IAsset> QueryAssets<T>(RectangleF bounds) where T : class, IAsset;

     // Loads an Asset of type T at location (X, Y)
     T LoadAsset<T>(IAsset parent, int x, int y) where T : class, IAsset;

     // Unloads an Asset and calls Dispose.
     void UnloadAsset(IAsset asset);
}

This interface creates a simple generic factory pattern to manage multiple types based on IAsset. It also allows you to query assets stored within the scene and load and unload assets as needed. We'll cover a concrete implementation in another section.

Assets

Assets are the building blocks of our game environment. Assets are basically containers for storing data to represent different aspects our game world. These could be tiles for a map, items, npcs, or even more advanced concepts like audio zones and loading areas. I'm not going to cover all of these ideas in this series. As we progress, you will hopefully recognize the flexibility of this system to manage many different types of content. We will implement the IAsset interface as follows:

C#
// The IAsset interface, requires implementation of IDisposable.
public interface IAsset : IDisposable
{
    // Referene to the containing Scene object.
    IScene Scene { get; }
  
    // A Referene a Parent Asset if one exists.
    IAsset Parent { get; }

    // A boolean indicator for disposed status.
    bool Disposed { get; }

    // A rectangle that represents the Assets location in the Scene.
    RectangleF Bounds { get; }

    // An Event for the QuadTree Implementation used in this example.
    event EventHandler BoundsChanged;

    // An Update function updating assets in a game loop.
    void Update(TimeSpan elapsed);
}

As you can see, it's a fairly simple implementation that sets up a relationship between the asset and a scene. You'll also notice we have a reference to a parent object. This allows for more advanced compositing of assets into simple structures. The asset implements IDisposable and has an indicator for disposed status - this works with the IDisposable interface. The location of the asset is retrieved through the Bounds property, the implementation of this QuadTree uses floats but we are just going to convert these to integers later for our tiling system. An update function is also provided for updates in a game loop.

Loaders

Loaders represent the implementation of the factory pattern for each type of IAsset registered with the scene. We'll create an interface so that we can provide a custom implementation for each type registered. This allows for a lot of flexibility in procedural generation techniques as you'll see later. We're going to implement a procedural loader for our example, but loaders can easily be implemented to load for other mediums, like the filesystem, or a database.

C#
public interface ILoader
{
    IAsset LoadAsset(IScene scene, IAsset parent, int x, int y);
}

The ILoader interface in this case only has one method, LoadAsset. The LoadAsset function takes the containing scene, a parent asset(if any), an X and Y position in our 2D coordinate system and an optional parameter. We could implement LoadAsset overloads that would take a file name or database ID.

Agents

Agents will be addressed in a later part of this series. I will mention them briefly here so you can have a better idea of the overall design. Agents will allow us to do customized procedural generation tasks that don't necessarily use volumetric filling algorithms like Perlin Noise. Agents work more like a finite state automata and can modify assets with a simple set of rules.

Creating a Scene

Now that the core interfaces are setup, we can create an implementation. I created a new class called Scene that inherits IScene. I chose to use a QuadTree to store assets for this project but it is possible to use other data structures like an array, octree, or whatever suits your needs.

C#
public class Scene : IScene
{
    ...
}

Next, we'll need to define each function specified by the interface. We'll start with registering asset types and then move on to loaders.

Registering Assets

First, create a List<Type> to store asset types registered with the Scene, we'll also need a Dictionary with the asset types as keys to store a QuadTree for each asset type registered. Then to register asset types, we will create a generic method that takes a type T which must be a class that inherits from IAsset. We'll use the type defined by T to create a new entry in a list of asset types managed by the scene. I return the this reference to create a fluent interface, you'll see how this works when we call these functions later.

C#
// List of types registered with Scene.
private List<Type> m_assetTypes = new List<Type>();

// List of QuadTrees to store assets of different types.
private Dictionary<Type, QuadTree<IAsset>> m_assets = new Dictionary<Type, QuadTree<IAsset>>();

...

// Register Asset.
public IScene RegisterAsset<T>() where T : class, IAsset
{
    // Convert Generic T to Type.
    Type assetType = typeof(T);

    // Throw an exception if this type is already registered.
    if (m_assetTypes.Contains(assetType))
    {
       throw new Exception(assetType.Name + " already registered.");
    }

    // Register Asset Type with the Scene.
    m_assetTypes.Add(assetType);

    // Create a new QuadTree to store loaded assets of type T.
    m_assets.Add(assetType, new QuadTree<IAsset>(new Size(100, 100), 1));

    // Return this for fluent interface.
    return this;
}

Registering Loaders

When we register a loader, we're going to instantiate a single instance and store in a dictionary indexed by the asset type. We'll later use this object to create instances of our assets. To create an instance of the loader, we'll use reflection, we can call Activator.CreateInstance<T2> to create a new instance.

C#
...

// Dictionary of Registered Loaders.
private Dictionary<Type, ILoader> m_assetLoaders = new Dictionary<Type, ILoader>();

...

// Register ILoader of type T2 where T1 is an IAsset
public IScene RegisterLoader<T1, T2>() where T1 : class, IAsset where T2 : class, ILoader
{
    // Convert generics to types.
    Type assetType = typeof(T1);
    Type loaderType = typeof(T2);

    // Throw an Exception if T1 has not been registered.
    if (!m_assetTypes.Contains(assetType))
    {
         throw new Exception("Unable to register loader without registered asset.");
    }

    // Ensure a single instance of the Loader is created.
    if (!m_assetLoaders.ContainsKey(assetType))
    {
        // Use Reflection to create an instance of loader T2.
        m_assetLoaders.Add(assetType, Activator.CreateInstance<T2>());
    }

    // Return this for fluent interface.
    return this;
}

Assets & Loaders

To load an asset, we need to create an asset and a loader class that inherit from IAsset and ILoader respectively. In this example, we're just going to create a two generic assets each with a loader to demonstrate how the abstract factory pattern will work. When we get to the second chapter, we will see some more advanced uses of this pattern when we load chunks and tiles.

Asset Types

I'm just going to create two new asset types. Asset1 and Asset2, both of which will inherit from IAsset. Both types will have the same implementation but I am also going to add a Name property to each which will return "Asset1" and "Asset2" for our demo.

C#
// Asset1 concrete implementation of IAsset.

public class Asset1 : IAsset
{
    public IScene Scene { get; private set; }
    public IAsset Parent { get; private set; }
    public RectangleF Bounds { get; private set; }
    public bool Disposed { get; private set; }
    public event EventHandler BoundsChanged; 

    public string Name
    {
        get
        {
            return "Asset1";
        }
    }

    public Asset1(IScene scene, IAsset parent, int x, int y)
    {
        Scene = scene;
        Parent = parent;
        Bounds = new RectangleF(x, y, 1, 1);
    }

    public void Update(TimeSpan elapsed)
    {
        // Perform any asset specific logic.
    }

    public void Dispose() 
    {
        if(!Disposed)
        {
            // Perform any Cleanup here.

            Disposed = true;
        }
    }
}

Next, the Asset2 class will have a slightly different implementation, just copy the Asset1 class and change Asset1 to Asset2. Typically, your asset types will have different properties than just a name, but for this demo, we're just going to keep it simple.

Loader Types

Creating a loader is pretty simple, in this example, we're just going to instantiate our new Asset types. We'll need a different loader for each Asset type. This will allow us to use custom loading logic for each type. This could mean loading assets from a file, database, or by using a procedural approach as you'll see later.

C#
// Asset1 Loader class.
public class Asset1Loader : ILoader
{
    public IAsset LoadAsset(IScene scene, IAsset parent, int x, int y)
    {
        // Create a new asset using a constructor
        Asset1 asset = new Asset1(scene, parent, x, y);

        // Perform additional loading logic here.

        // Return the loaded asset.
        return asset;
    }
}

// Asset2 Loader class.
public class Asset2Loader : ILoader 
{ 
    public IAsset LoadAsset(IScene scene, IAsset parent, int x, int y)
    {
        // Create a new asset using a constructor
        Asset2 asset = new Asset2(scene, parent, x, y);

        // Perform additional loading logic here.

        // Return the loaded asset.
        return asset;
    } 
}

In our example loaders, we just have a few lines of code, here we are just instantiating a new instance using the constructor. We would perform additional loading logic here where necessary.

Registering & Loading

Now that we have a basic scene and some example assets with loaders, we're ready to put them together so we can move on the loading assets into the scene. We're going to create a console application that loads and queries the new asset types. Start by creating a simple main function that registers our new assets as follows.

Registering

C#
static void Main(string[] args) 
{
    // Create a new Scene instance.
    Scene scene = new Scene();
    
    // Use fluent interface to Register Asset1 and it's Loader.
    Scene.RegisterAsset<Asset1>()
         .RegisterLoader<Asset1, Asset1Loader>();

    // Use fluent interface to Register Asset2 and it's Loader.
    Scene.RegisterAsset<Asset2>()
         .RegisterLoader<Asset2, Asset2Loader();

    ...
}

Here, we are using the fluent interface defined by the register functions created in the Scene. We pass the types for the assets and loaders as generic parameters to our registration functions.

Loading

Next, we need to add a function that will call our new loaders and store the assets created in the scene. We already have a function defined in our IScene interface called LoadAsset<T>(). We will create the LoadAsset function in the Scene class as follows:

C#
public Scene : IScene 
{
    ...

    // Loads an Asset of type T at location (X, Y) 
    public T LoadAsset<T>(IAsset parent, int x, int y) where T : class, IAsset
    {
        IAsset asset = null;
        Type assetType = typeof(T);

        // Make sure the asset type has been registered with the scene.
        if(!m_assetTypes.Contains(assetType))
        {
            throw new Exception(assetType.Name + " has not been registered.");
        }

        // Make sure a loader has been registered for the asset type.
        if(!m_assetLoaders.ContainsKey(assetType))
        {
            throw new Exception("No loader registered for " + assetType.Name + ".");
        }

        // Call LoadAsset with registered asset loader.
        asset = m_assetLoaders[assetType].LoadAsset(this, parent, x, y);

        // Store the new asset in our scene.
        m_assets[assetType].Insert(asset);

        return asset;
    }    

    ...
}

Unloading

We can also add an unload function that will allow us to remove assets from the scene. I have defined it as follows in the Scene class.

C#
public Scene : IScene
{
    ...

    // Unloads an asset and calls it's dispose method.
    public void UnloadAsset(IAsset asset)
    {
        // Check if asset is contained in the Scene.
        if(m_assets.Contains(asset))
        {
            // Remove the asset from our QuadTree.
            m_assets.Remove(asset);

            // Call the Dispose function defined for the Asset.
            asset.Dispose();
        }
    }

    ...
}

Our scene can now create instances of generic assets and store them in it's internal storage, in this case it's our QuadTree. It's pretty simple to use our new functions. We can update our main function to load and unload an asset like below:

C#
static void Main(string[] args)
{   
    // Create a new Scene instance. 
    Scene scene = new Scene();

    // Use fluent interface to Register Asset1 and it's Loader.   
    scene.RegisterAsset<Asset1>()
         .RegisterLoader<Asset1, Asset1Loader>();   

    // Use fluent interface to Register Asset2 and it's Loader.   
    scene.RegisterAsset<Asset2>()
         .RegisterLoader<Asset2, Asset2Loader();   

    // Create a new asset at (0, 0) with no parent.    
​​​​​​    Asset1 asset1 = scene.LoadAsset<Asset1>(null, 0, 0);​

    // Create a new asset at (1, 1) with asset1 as a parent.
    Asset2 asset2 = scene.LoadAsset<Asset2>(asset1, 1, 1);

    // Perform additional steps with assets
    Console.Print("{0} ({1}, {2})", asset1.Name, asset1.Bounds.X, asset1.Bounds.Y);
    Console.Print("{0} ({1}, {2})", asset2.Name, asset2.Bounds.X, asset2.Bounds.Y);

    // Unload asset2.
    scene.UnloadAsset(asset2);

    ... 
}

Querying Assets

We have a way to store assets but in the demo, we're just using the assets returned by the LoadAsset function directly. This approach isn't very useful for managing many assets at once. In our Scene implementation, we setup a dictionary of QuadTrees for each type registered. You will also recall that we stored the asset loaded in LoadAsset in the QuadTree dictionary.

Quad Trees

Quote: https://en.wikipedia.org/wiki/Quadtree
Image 2

A quadtree is a tree data structure in which each internal node has exactly four children. Quadtrees are most often used to partition a two-dimensional space by recursively subdividing it into four quadrants or regions. The regions may be square or rectangular, or may have arbitrary shapes. This data structure was named a quadtree by Raphael Finkel and J.L. Bentley in 1974. A similar partitioning is also known as a Q-tree. All forms of quadtrees share some common features:

  • They decompose space into adaptable cells
  • Each cell (or bucket) has a maximum capacity. When maximum capacity is reached, the bucket splits
  • The tree directory follows the spatial decomposition of the quadtree.

I'm not going to go into details how QuadTrees work but as you can see from the quote above, they store objects in rectangular cells, which makes it perfect for storing 2D data. I'm using a slightly modified QuadTree from here. It is possible to use other data structures and could be extended to 3D using an octree. The IScene interface has two methods defined for querying assets. GetAssets<T> returns a list of each asset loaded into the scene of type T without regard for location. While QueryAssets<T> takes a rectangle parameter and returns a list of assets contained within.

Query Assets

The QueryAssets requires a generic parameter of type T and takes a RectangleF boundary to query in our QuadTree. The QuadTree has a QuadTree.Query method, we just need to verify that the asset type is registered with the factory. QuadTree.Query returns a List containing all assets contained within the boundary.

C#
public List<IAsset> QueryAssets<T>(RectangleF bounds) where T : class, IAsset
{
    List<IAsset> assets = new List<IAsset>();

    // Verify Asset type is registered.
    if (!m_assets.ContainsKey(typeof(T)))
    {
        throw new Exception("Asset Type not registered.");
    }
    
    // Query assets of type T within bounds.
    assets = m_assets[typeof(T)].Query(bounds);

    return assets;
}

Get Assets

The GetAssets function takes a generic parameter of type T, and returns a list of assets. The QuadTree I'm using has a way to query the data in the nodes directly without using a rectangle. I just loop through each node and add them to the list returned by the function.

C#
public List<IAsset> GetAssets<T>() where T : class, IAsset 
{
    List<IAsset> assets = new List<IAsset>();
    
     // Loop through each QuadTree node.
    foreach(QuadTree<IAsset>.QuadNode node in m_assets[typeof(T)].GetAllNodes()) 
    {
        // Add the objects contained in the node to the list.
        assets.AddRange(node.Objects);
    }

    return assets;
}

Finishing Up

In the Main function, we can query the assets based on a rectangle instead of using the references returned by LoadAsset directly. We can load several assets in different parts of the QuadTree and then query a subset of them. In this example, we're just printing the returned assets and then unloading them.

C#
static void Main(string[] args) 
{   
    // Create a new Scene instance.   
    Scene scene = new Scene();   

    // Use fluent interface to Register Asset1 and it's Loader.   
    scene.RegisterAsset<Asset1>()   
         .RegisterLoader<Asset1, Asset1Loader>();   

    // Load a few assets into the scene. 
    scene.LoadAsset<Asset1>(null, 0, 0);   
    scene.LoadAsset<Asset1>(null, 5, 5);
    scene.LoadAsset<Asset1>(null, 10, 10);
    scene.LoadAsset<Asset1>(null, 15, 15);

    // Query a rectangle of 10 by 10
    var assets = scene.QueryAssets<Asset1>(new RectangleF(0, 0, 10, 10));

    // Print the assets returned and then unload them.
    foreach(Asset1 asset in assets)
    {
        Console.Print("{0} ({1}, {2})", asset.Name, asset.Bounds.X, asset.Bounds.Y); 
        
        // Unload the asset.
        scene.UnloadAsset(asset);  
    }    

    ...
}

We will see the following output when running the program:

Asset1 (0, 0)
Asset1 (50, 50)
Asset1 (100, 100)

A visual representation of the data loaded and selected looks like the following image:

Image 3

That's it! In the next tutorial, we will look into loading larger amounts of assets and organizing them into manageable units. The final result will be a 2D tiling system that we will be able to create procedurally generated maps with. Stay tuned for: Procedural Terrain Generation Part II: Loading Chunks and Tiles

History

  • 23rd August, 2016 - Article posted

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Software Developer GalacticSoft
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --