Building a Random Quest Generator

TL;DR

I wanted a low maintenance way to generate random quests for my game. So I created an awesome Factory class that did not require maintenance or upkeep as I added new quests. Keep reading to find out how I did it…

The Problem
A Solution
The Code


The Problem

I’m building an RPG-themed idle/incremental game, where the player takes on quests to earn money and other rewards, which leads to more quests and so on. Naturally, each quest requires the player to achieve a specific goal, and I’ve developed multiple goals to provide variety. For example, one goal requires achieving 5,000 wingits (the game’s currency), and another one has the player finishing 50 combat encounters before moving on.

To achieve this I created an abstract Objective class that each concrete goal derived from e.g., IncomeObjective, EncountersCompletedObjective, and so on. Next, I needed a Factory that created random goals during runtime. However, most Factory implementations use a switch statement, meaning I would end up with the following code.

public static class ObjectiveFactory
{
  private int _numberOfObjectiveClasses = 3;

  public static Objective CreateGoal()
  {
    Random rand = new Random();
    rand.Next(_numberOfObjectiveClasses);
    switch (rand)
    {
      case 0: return new IncomeObjective();
      case 1: return new CompletedTiersObjective();
      default: return new HaveMoneyObjective();
    }
  }
}

This works, however, it requires a lot of maintenance. Every time I added more goals I would have to update the factory. I don’t like babysitting my factories so I went on a quest to find a better way.

A Solution

Giving my quest some thought, I broke it down into the following three steps.

  1. Get a list of classes derived from Objective
  2. Create a random number using the count of the list
  3. Return the concrete Objective object

Broken down like this, I assumed I would need Reflection for step 1, step 2 was trivial, and step 3 required further research. After some research I figured out I didn’t need Reflection or anything fancy after all, instead I needed a mostly basic calls from common namespaces and one call that I wasn’t familiar with previously.

If I queried the Assembly containing Objective it would return a list of derived classes, this solved step 1. Creating a random number covered step 2. And step 3 required feeding the random concrete class to the an Activator (new to me) which would create a new instance. (Jump directly to the code.)

Now this type of Factory has a specific use-case, one where there are lots of derived classes from a base class, the creation logic is similar (i.e., all classes take the same input params), and most importantly, a random class can be returned. This scenario exists in lots of video games, so awesome. There are any number of games that have random customers, monsters, rewards, and so on. If I assume all monsters derive from a BaseMonster class then this code can easily work to create random monsters. With a little refactoring I can even make it support classes containing a differing number of parameters for the constructor. I’ll save that for the future.

The Code

I added the RandomQuestFactory scene to my Sandbox 2020 project (it uses Unity 2020.1.5f1 as of 5 Dec 2020) on GitHub. For the first code in my example, I created the following base Objective class.

public abstract class Objective
{
    #region Ctors
    public Objective() { }
    #endregion Ctors

    #region Properties
    public abstract string Description { get; }
    public virtual float TargetValue { get; protected set; }
    public bool IsComplete { get; protected set; }
    #endregion Properties

    #region Methods
    public virtual void CompleteGoal() => IsComplete = true;
    #endregion Methods
}

Then I created a few concrete implementations.

public class IncomeObjective : Objective
{
    #region Members
    private float _baseTarget = 10f;
    #endregion Members

    #region Ctor
    public IncomeObjective(float targetModifier) => TargetValue = targetModifier * _baseTarget;
    #endregion Ctor

    #region Properties
    public override string Description => $"Reach {TargetValue} income per second";
    #endregion Properties
}

public class GooberObjective : Objective
{
    #region Members
    private float _baseTarget = 25f;
    #endregion Members

    #region Ctor
    public GooberObjective(float targetModifier) => TargetValue = targetModifier * _baseTarget;
    #endregion Ctor

    #region Properties
    public override string Description => $"Reach {TargetValue} goobers";
    #endregion Properties
}

public class WidgetObjective : Objective
{
    #region Members
    private float _baseTarget = 50f;
    #endregion Members

    #region Ctor
    public WidgetObjective(float targetModifier) => TargetValue = targetModifier * _baseTarget;
    #endregion Ctor

    #region Properties
    public override string Description => $"Build {TargetValue} widgets";
    #endregion Properties
}

Which leads me to the Factory class.

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

[System.Serializable]
public static class ObjectiveFactory
{
    #region Members
    private static Random _rand = new Random();
    private static IEnumerable<Type> _goals;
    #endregion Member

    #region Ctor
    static ObjectiveFactory()
    {
        _goals = typeof(Objective).Assembly.GetTypes().Where(x => x.IsSubclassOf(typeof(Objective)) && !x.IsAbstract);
    }
    #endregion Ctor

    #region Methods
    public static Objective CreateGoal(float targetModifier)
    {
        var ans = _goals.ElementAt(_rand.Next(_goals.Count()));
        return (Objective)Activator.CreateInstance(ans, targetModifier);
    }
    #endregion Methods
}

Lines 16, 23, and 24 cover steps 1, 2, and 3 and thusly make the magic happen. So let’s decipher the script and see what’s going on.

On line 16 the typeof(Objective).Assembly, gets the assembly that all your classes of type Objective are in. By default in Unity everything is in the same Assembly. There are advanced ways to split up your classes into separate assemblies, however, that’s beyond the scope of this article.

The next part, GetTypes(), takes the assembly and returns an array of Type[]. This includes the abstract base class, and all private classes, you could use GetExportedTypes() to only get the public classes.

The last part, Where(x => x.IsSubclassOf(typeof(Objective)) && !x.IsAbstract), is a bit of LINQ to exclude the base class and any other abstract types that you might have derived from the base class.

Now _goals contains a list of concrete types derived from Objective. Awesome. The game now knows exactly how many goals it can choose from, and line 23 gets a random number based on the count of _goals. That covers steps 1 and 2. Now onto step 3 on line 24.

If you look up Activator on the .NET docs, you find it defined as a way “to create types of objects.” It has a static method public static object CreateInstance(Type type, params object[] args) that becomes extremely useful to my factory class. On line 24, I pass in a specific type and then pass in the float needed in the constructors. If classes required more inputs for the constructors I could have passed those in as well. The last thing to do was to cast it to Objective and then return it.

Now that I created all that I needed another script to test it in Unity.

using System.Collections.Generic;
using UnityEngine;

public class GoalSpawner : MonoBehaviour
{
    #region MonoBehavioiurs
    void Start()
    {
        List<Objective> goals = new List<Objective>(5);
        for (int i = 0; i < 5; i++)
        {
            goals.Add(ObjectiveFactory.CreateGoal(i + 1));
            Debug.Log($"Goal {i + 1} - {goals[i].Description}");
        }
    }
    #endregion MonoBehavioiurs
}

I created an empty GameObject, added my new GoalSpawner script to it and hit run. As expected I got five randomly generated goals written out on the Console.

I hope you find this Factory implementation useful. Please drop me a comment letting me know what you think, or ways you would change or improve on this functionality.

Life’s an adventure, enjoy your quest!


Posted

in

, , ,

by

Comments

Send a Missive

This site uses Akismet to reduce spam. Learn how your comment data is processed.