Tactics RPG Unit Factory

It’s well past time to add some enemies to the board, but by now we’ve made all sorts of modifications to the “Hero” prefab which haven’t been propogated to the “Monster” prefab. We could spend time manually setting up each new character to have the same kind of structure, but I really don’t want to. Instead, we will use this lesson to lay the groundwork on a more flexible system. We will be making a factory to create and configure new units for us, so that introducing new and unique characters in the future will be a much simpler process.

Hero Prefab

Before you do anything, drag the Hero prefab into the scene so you can see its full hierarchy. Open up the “Ability Catalog” and drag each ability into the project pane to create a prefab out of it – we will load and use them again later. I put them in a Resources/Abilities/ folder with subfolders for the categories. I put the “Attack” ability in a “Common” category subfolder. Refer to the project repo so that your setup will match mine. Note that the hierarchy and naming matters when using Resources.Load with file paths.

With that completed, let’s actually strip the “Hero” prefab back to the way the “Monster” prefab is currently setup – no scripts and no children GameObjects except the “Jumper” which holds the spheres we use to represent a character. By keeping our character models reduced to a very basic form it will be much easier to add new characters.

Finally, we need to move both prefabs from the folder they currently sit in (“Prefabs”) into Resources/Units. This will allow us to load character models as we need them, rather than require us to maintain a reference to them. This also means you can remove the reference to the “Hero” prefab from the BattleController script.

Recipes

The next step is finding a way to make both the “Hero” and “Monster” be the way that the “Hero” had been before I made you revert all of the changes. The system needs to be flexible so that the same system can create the same setup on any model, with any compatible kinds of movement components, jobs, and any combinations of sets of abilities, etc.

To manage this idea, I decided that I would pass a “recipe” for a unit to the “factory” which contained all the specifics of the setup I wanted. Go ahead and add a new script named UnitRecipe to the Scripts/Factory directory.

using UnityEngine;
using System.Collections;

public class UnitRecipe : ScriptableObject 
{
	public string model;
	public string job;
	public string attack;
	public string abilityCatalog;
	public Locomotions locomotion;
	public Alliances alliance;
}

The first several fields are simple string data types. These strings are the names of a resource which the factory will be able to load at run time. Note that this means I also need to move the prefabs into a “Resources” folder in order for the whole process to work.

You might wonder why I didn’t simply add GameObject references directly to the prefab instead of having to copy their name. The reason I chose not to do this is because several of these types of objects, such as jobs, are dynamically created based on other files like spreadsheets. Sometimes I can update them in place, but if I ever wanted to make major changes I might decide to delete the prefabs and let the system rebuild them from scratch. At that point, any other project resource which was referencing the original job will be disconnected, even though I would end up creating another object in the same place with the same name.

Another possibility is that either the model, or job, etc would also become a more complex subsystem. I could for example create another “recipe” style system for one of the features of the unit and then I could modify the Factory to load the recipe instead of the GameObject I currently have in place. The abilityCatalog field is an example of such a subsystem. It is a string name which will load another recipe called AbilityCatalogRecipe.

Go ahead and create another script in the same folder for the AbilityCatalogRecipe.

using UnityEngine;
using System.Collections;

public class AbilityCatalogRecipe : ScriptableObject 
{
	[System.Serializable]
	public class Category
	{
		public string name;
		public string[] entries;
	}
	public Category[] categories;
}

This recipe is similarly setup with simple strings. I have a category name which relates to the categories of abilities like “White Magic” we used previously. Then you will see an array of string entries for the actual abilities like “Cure”. The idea behind this recipe is that you can easily make a few reusable catalogs for enemies. Perhaps certain mage enemies will share certain spells, etc. Instead of recreating this structure on all of the enemies that use it, you can simply point to the shared recipe.

The “Unit Recipe” also had fields for “Locomotions” and “Alliances”, both of which I haven’t defined yet. These are both enums which help the factory know what kind of component to add, and/or how to configure a component.

using UnityEngine;
using System.Collections;

public enum Locomotions
{
	Walk,
	Fly,
	Teleport
}

The Locomotions enum will be used to determine what kind of Movement component to add to a unit. I could have skipped the enum and simply passed the name of the component’s class directly, but this way I can refactor the names of the classes without breaking anything.

using UnityEngine;
using System.Collections;

public enum Alliances
{
	None = 0,
	Neutral = 1 << 0,
	Hero = 1 << 1,
	Enemy = 1 << 2
}

The Alliances enum decides what “side” of a battle you are on – primarily either the player’s side (the Hero) or the computer’s side (the Enemy). Each unit created will only have a single type of Alliance, but I marked them as a bit mask for flexibility in the future. For example, an enemy AI might normally target the units of the Hero alliance, but others might also want to attack Neutral units. Or if they were under some sort of status effect they might attack every kind of unit including other enemies. Bit-masks allow me to handle these combinations with a single field.

The Alliances enum type will be applied to another component of a similar name. Add a new script named Alliance to the Scripts/View Model Component/Actor directory.

using UnityEngine;
using System.Collections;

public class Alliance : MonoBehaviour
{
	public Alliances type;
}

The component is super simple for now, but I plan to extend its functionality in the future. Perhaps it will hold additional fields for who it consideres a friend and foe (which will be great targets for status effects to modify). I wont want to change the intial “type” field because the other units on the field who are not under a status effect still need a way to determine the “base” alliance type of a unit.

Asset Creator

Scriptable objects need to be created by some other script. We have done this before to create ConversationData and now we will want to add a few more:

[MenuItem("Assets/Create/Unit Recipe")]
public static void CreateUnitRecipe ()
{
	ScriptableObjectUtility.CreateAsset<UnitRecipe> ();
}

[MenuItem("Assets/Create/Ability Catalog Recipe")]
public static void CreateAbilityCatalogRecipe ()
{
	ScriptableObjectUtility.CreateAsset<AbilityCatalogRecipe> ();
}

Once you have created your first recipe you can always duplicate it and modify the copy for the changes needed, or start from scratch as you like. I have created several unit recipes and ability catalog recipes which you can feel free to copy from the project repository.

Unit Factory

Now that we have our recipes in place, its time to add the script which accepts them and returns a unit. Create a new script named UnitFactory and add it to a new Scripts/Factory folder. This script is a bit on the long side, so I will break it down to just a little bit at a time.

using UnityEngine;
using System.IO;
using System.Collections;

public static class UnitFactory
{
	//... Add next code samples here
}

The class itself is a static class. I don’t need any instance of a factory, because the factory doesn’t need to hold any instance data. Everything it will need to do its job (the recipe) will be passed along as a parameter anyway.

public static GameObject Create (string name, int level)
{
	UnitRecipe recipe = Resources.Load<UnitRecipe>("Unit Recipes/" + name);
	if (recipe == null)
	{
		Debug.LogError("No Unit Recipe for name: " + name);
		return null;
	}
	return Create(recipe, level);
}

The interface for the class is pretty simple, I have overloaded a method called Create which takes either a name of a recipe, or a recipe itself, along with the level we want the unit to be created at.

The version of Create which takes the name of a recipe attempts to load the recipe for you and then passes it along to the other method. If it cant find the recipe it prints an error message to the console and returns a null object.

public static GameObject Create (UnitRecipe recipe, int level)
{
	GameObject obj = InstantiatePrefab("Units/" + recipe.model);
	obj.name = recipe.name;
	obj.AddComponent<Unit>();
	AddStats(obj);
	AddLocomotion(obj, recipe.locomotion);
	obj.AddComponent<Status>();
	obj.AddComponent<Equipment>();
	AddJob(obj, recipe.job);
	AddRank(obj, level);
	obj.AddComponent<Health>();
	obj.AddComponent<Mana>();
	AddAttack(obj, recipe.attack);
	AddAbilityCatalog(obj, recipe.abilityCatalog);
	AddAlliance(obj, recipe.alliance);
	return obj;
}

The version of Create which takes the recipe instance does the real work of creating a unit. It is worth pointing out that creating an object like this can have certain benefits over having all the components pre-existing on a prefab:

  1. Should you decide to change the setup of a unit you only have to modify the factory class rather than ALL of the instances of your prefabs.
  2. Some components are dependent on other components and will need to initialize themselves based on the data in the other script. By manually adding them in the correct order, you are also controlling the initialization order. In contrast, components which are already on a GameObject (like in a prefab) will initialize in a random order and are subject to race conditions. Sometimes a setup may appear to work fine, and then it will stop working leaving you to wonder what you could have done to break it. Note that another solution is via Edit->Project Settings->Script Execution Order.
  3. Sometimes prefabs break in a variety of ways such as if they get corrupted, lose references to scripts due to renaming, etc. If you don’t remember how the object was configured it might be a pain to recreate. Scripts are much more reliable and are also better for version control.
static GameObject InstantiatePrefab (string name)
{
	GameObject prefab = Resources.Load<GameObject>(name);
	if (prefab == null)
	{
		Debug.LogError("No Prefab for name: " + name);
		return new GameObject(name);
	}
	GameObject instance = GameObject.Instantiate(prefab);
	return instance;
}

Several of the methods will make use of this InstantiatePrefab method. It attempts to load a GameObject from Resources according to the name you provide. If it can’t find a prefab by that name it will print an error in the console and tell you the name of the object you had asked it to load. These little messages are really helpful to indicate when you have a typo, or have missed an asset in one form or another.

In the event that a prefab is found, the method goes ahead and instantiates a clone and then returns the new instance to you.

static void AddStats (GameObject obj)
{
	Stats s = obj.AddComponent<Stats>();
	s.SetValue(StatTypes.LVL, 1, false);
}

static void AddJob (GameObject obj, string name)
{
	GameObject instance = InstantiatePrefab("Jobs/" + name);
	instance.transform.SetParent(obj.transform);
	Job job = instance.GetComponent<Job>();
	job.Employ();
	job.LoadDefaultStats();
}

static void AddLocomotion (GameObject obj, Locomotions type)
{
	switch (type)
	{
	case Locomotions.Walk:
		obj.AddComponent<WalkMovement>();
		break;
	case Locomotions.Fly:
		obj.AddComponent<FlyMovement>();
		break;
	case Locomotions.Teleport:
		obj.AddComponent<TeleportMovement>();
		break;
	}
}

static void AddAlliance (GameObject obj, Alliances type)
{
	Alliance alliance = obj.AddComponent<Alliance>();
	alliance.type = type;
}

static void AddRank (GameObject obj, int level)
{
	Rank rank = obj.AddComponent<Rank>();
	rank.Init(level);
}

static void AddAttack (GameObject obj, string name)
{
	GameObject instance = InstantiatePrefab("Abilities/" + name);
	instance.transform.SetParent(obj.transform);
}

A lot of the statements in the Create method are little one-off methods to do a simple task. For example, the Unit component didn’t require any configuration at the moment so I simply used the AddComponent method on the object we had created. However, the Job component needs to be added and configured, so I created a little method to handle both in one step. This way I can easily view the overall construction process of the unit and not worry about the little details. It is also easy to reorder the “steps” of construction if necessary.

static void AddAbilityCatalog (GameObject obj, string name)
{
	GameObject main = new GameObject("Ability Catalog");
	main.transform.SetParent(obj.transform);
	main.AddComponent<AbilityCatalog>();

	AbilityCatalogRecipe recipe = Resources.Load<AbilityCatalogRecipe>("Ability Catalog Recipes/" + name);
	if (recipe == null)
	{
		Debug.LogError("No Ability Catalog Recipe Found: " + name);
		return;
	}

	for (int i = 0; i < recipe.categories.Length; ++i)
	{
		GameObject category = new GameObject( recipe.categories[i].name );
		category.transform.SetParent(main.transform);

		for (int j = 0; j < recipe.categories[i].entries.Length; ++j)
		{
			string abilityName = string.Format("Abilities/{0}/{1}", recipe.categories[i].name, recipe.categories[i].entries[j]);
			GameObject ability = InstantiatePrefab(abilityName);
			ability.name = recipe.categories[i].entries[j];
			ability.transform.SetParent(category.transform);
		}
	}
}

The AddAbilityCatalog is another construction step method like the several preceeding ones, but it is a bit more complex. It dynamically creates an object hierarchy to match the one we had setup on the “Hero” prefab for it to use the “Ability Catalog” where we begin with a base GameObject, then children GameObjects (one per category), then grandchildren GameObjects (one per ability).

We use the name of an ability catalog to load the instance of the recipe from a Resources.Load call, and then loop through the content of that recipe using a nested loop. The outer loop iterates over the categories of the recipe, and the inner loop iterates over the abilities within each category.

Init Battle State

Before we had the “Unit Factory” we simply spawned and configured unit prefabs in the SpawnTestUnits method of the InitBattleState script. I’m not quite ready to remove the temp code completely, but we can clean it up a bit and start using our new factory!

// Add this at the top of the script
using System.Collections.Generic;

// Replace the old method with this version
void SpawnTestUnits ()
{
	string[] recipes = new string[]
	{
		"Alaois",
		"Hania",
		"Kamau",
		"Enemy Rogue",
		"Enemy Warrior",
		"Enemy Wizard"
	};

	List<Tile> locations = new List<Tile>(board.tiles.Values);
	for (int i = 0; i < recipes.Length; ++i)
	{
		int level = UnityEngine.Random.Range(9, 12);
		GameObject instance = UnitFactory.Create(recipes[i], level);

		int random = UnityEngine.Random.Range(0, locations.Count);
		Tile randomTile = locations[ random ];
		locations.RemoveAt(random);

		Unit unit = instance.GetComponent<Unit>();
		unit.Place( randomTile );
		unit.dir = (Directions)UnityEngine.Random.Range(0, 4);
		unit.Match();

		units.Add(unit);
	}

	SelectTile(units[0].tile.pos);
}

Note that the list of unit recipes are all resources which I created and added to the project. You can feel free to create and use your own, or of course, grab a copy of the ones I made in the repository.

As a random side note, I googled lists of names to come up with some good names for the three basic hero jobs I had created. I found the following:

  • Alaois – means “famous warrior” so I used it for my warrior hero.
  • Hania – means “spirit warrior” so I used it for my mage.
  • Kamau – means “quiet warrior” so I used it for my rogue.

I didn’t bother to name any of the monsters, but I am sure I would come up with something really clever like, “Goblin” if I actually had some kind of matching art to accompany it!

There is still a good bit of temporary code here that wouldn’t exist in a fully implemented version. For example, defining what enemies will spawn, how many there will be, and where they appear, could all be saved as part of the level data. The list of heroes would also already exist in a party data object somewhere else, because they have information which needs to persist between multiple battles.

Next I grabbed a list with a copy of all the tiles on the board. Then I randomly grab one of the tiles to use as a spawn location for one of the units. I remove the tile I chose from the list so that I wont accidentally select it a second time and try to spawn two units at the same location.

I give each of the units a random rotation, just because. Then I add the spawned unit to the BattleController’s list of units as we did previously, and finally select whatever tile we had spawned the first unit onto.

Demo

At this point you should be able to press play and see both our “Hero” and “Monster” characters populate the board. The game hasn’t really changed at all, but I still think its more fun to have clear enemies and allies (even if you are still controlling them both)!

Summary

In this lesson we paved the way for us to have more than one type of unit in our game by stripping the initial unit to its base form and then rebuilding it with a factory. It should be easy to add many new characters (including upgrading to actual character models with animations, etc) as well as new abilities, jobs, and well, pretty much everything else.

Don’t forget that the project repository is available online here. If you ever have any trouble getting something to compile, or need an asset, feel free to use this resource.

10 thoughts on “Tactics RPG Unit Factory

  1. I just wanted to ask unrelated to this particular tutorial.

    After reading the introductory article it seems everything here is already done correct? As in, your tutorials explain how to get to where you currently are in that video, right?

    1. The stuff in the video was created with an earlier prototype. It has a lot of code and implementations which don’t exist in the tutorials, but I think the later prototype (from the tutorials) is more flexible. By this point, yes I think the series can do everything the video could do and more. The only thing I haven’t added is the little dots above the heads of the units to help indicate facing direction.

      1. Cool. Are you going to keep doing this series or are you going to stop soon? Even though I’m not really using these tutorials I do enjoy reading them. I find it very interesting how you explain the way to implement things, and I keep wondering how are you going to tackle what comes next.

        If you stop it would be like cancelling one of my favorite series!

        1. I’m really glad to hear you’re enjoying it so much! There is plenty I could continue adding to this project. I had planned to take the series a little bit further – I am hoping to get AI in at a minimum. After that I had considered starting a new project, but it might partly depend on the fans who speak the loudest. My visitors/views have gone down a bit recently but its hard to tell why. I don’t know if people are getting bored of the same thing, or if school is keeping people to busy, etc. I wondered if a new project might help pick the numbers up again. Either way I am open to suggestions!

  2. Keep up the good work, sir. I think you should continue this until the end before starting a new project – it’s just like what you said, at least have the player and AI interactions (with each other – along with some stat levelling) going (should you decide to move on to another project)

  3. I need some help. I’ve stepped through the debugger without success. My stack trace is below:

    NullReferenceException: Object reference not set to an instance of an object
    InitBattleState.SpawnTestUnits () (at Assets/Scripts/Controller/Battle States/InitBattleState.cs:60)
    InitBattleState+c__Iterator5.MoveNext () (at Assets/Scripts/Controller/Battle States/InitBattleState.cs:18)
    UnityEngine.SetupCoroutine.InvokeMoveNext (IEnumerator enumerator, IntPtr returnValueAddress) (at C:/buildslave/unity/build/Runtime/Export/Coroutines.cs:17)
    UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
    InitBattleState:Enter() (at Assets/Scripts/Controller/Battle States/InitBattleState.cs:10)
    StateMachine:Transition(State) (at Assets/Scripts/Common/State Machine/StateMachine.cs:40)
    StateMachine:set_CurrentState(State) (at Assets/Scripts/Common/State Machine/StateMachine.cs:9)
    StateMachine:ChangeState() (at Assets/Scripts/Common/State Machine/StateMachine.cs:24)
    BattleController:Start() (at Assets/Scripts/Controller/BattleController.cs:25)

    I downloaded the repository just to be sure that I have all the necessary code, and I’m still stumped. Any ideas on where I should start?

    1. I checked out the project at the Unit Factory commit, but my “InitBattleState” class is only 56 lines long and your error occurs on line 60. Are you at a different spot in the project or just customizing things on your own? I may need to see what your code looks like to give you better advice, but it is pretty likely that your problem will end up being related to a project asset being missed, using a wrong or misspelled name, having an asset located in the wrong folder, not putting the right components on an asset, etc.

      1. Maybe that’s my problem then. I downloaded the whole repository because I don’t know how to do source control with Unity. Looks like I need to learn. I’ll let you know if I need more help, thanks again!

  4. So you mentioned party data object. Would you just make a scriptable object that holds all the data for the party and just have it update after each battle? Guess I’m just a bit confused on how you planed to serialize this data at the end of the day. I was going to try to use json to generate and save characters… but all of this looks way better.

    I was doing things way differently then you were in the project I was making… I was trying to find a project that uses the component design pattern, I really lucked out finding your project. Thanks for all of the useful things I’m going to have to tinker with and learn with.

    1. I am glad you are enjoying the project so much! I wrote this project a long time ago, so it’s a little hard to remember what exactly I was imagining, but a scriptable object would work fine and would fit well with the same pattern I had already been using. Other methods like json are also appropriate and there are pros and cons on any approach, so it really is up to you.

Leave a Reply

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