Tactics RPG Jobs

In this lesson we will show how to define a variety of jobs and store their data as a project asset. Although we have done this before (with conversation assets), this time we will be creating prefabs programmatically. By choosing prefabs over scriptable objects, we have the ability to take advantage of components such as the features which we introduced in the previous lesson.


Although I have looked at several games, I’ve been spending the most time looking at Final Fantasy Tactics Advance (FFTA). You can find plenty of FAQ’s and guides online which provide a great way to understand the overall scope of the game. For example, there are around 100 different jobs specified by FFTA – each providing a variation on gameplay:

  1. Stats (some fixed like movement range, others as growth on level up)
  2. Items (what categories can be equipped)
  3. Abilities (what can be actively used while operating as that job, what can be learned and used even outside the job)
  4. Job Tree (learn enough of one job, and there may be a secondary job which opens up to you)

There is a lot of room for complexity here, but a lot of it is really dependent on your own design. Initially, our job system will be limited to determining the starting stats and growth rates of characters, but it shouldn’t be hard to add Job features to control the categories of equippable items and usable skills in much the same way as we added features to items.


I still like the idea of being able to change jobs and so I see a great reason to define a lot of different job types. We will begin by creating spreadsheets (.csv) which contain data from which to programmatically create our project assets. Of course it’s up to you to determine how you want to organize your data. Do whatever feels the best to you in order for the data to be easy to view and balance.

Here I have created a simple example with three very generic job-types. I used values somewhere within the ranges you might see from FFTA but made it my own custom list. Ideally you will do the same and flesh out many, many more jobs, rather than cheating by directly copying data from Final Fantasy, tempting though it may be. I am starting with two different spreadsheets. The first I call JobStartingStats.csv which I have placed in the Settings folder.


Note that in this case, MOV and JMP are not merely starting stats. They will actually be implemented as StatModifierFeature components so that changing to or from a job will allow the stats to fluctuate up and down.

Next I created a spreadsheet called JobGrowthStats.csv which I also placed in the Settings folder.


Here I have used a floating point number for each modified stat. However, it is a special convention I saw while referencing the FAQ’s. The whole number portion of the number is a fixed amount of growth in that stat with every level-up. The fractional portion of the number is a percent chance that an additional bonus point will be awarded.

For example, using the two spreadsheets above you can deduce that a character which begins the game as a Warrior will start with 43 hit points. Upon gaining a level this character’s maximum hit points will grow by a minimum of 8 but there is a 40% chance it could grow by 9.


Now let’s implement the component which holds the data from our spreadsheets, and which listens to level-ups to actually apply the stat growth, etc. Create a new script called Job in the Scripts/View Model Component/Actor folder.

using UnityEngine;
using System.Collections;

public class Job : MonoBehaviour
	#region Fields / Properties
	public static readonly StatTypes[] statOrder = new StatTypes[]

	public int[] baseStats = new int[ statOrder.Length ];
	public float[] growStats = new float[ statOrder.Length ];
	Stats stats;

	#region MonoBehaviour
	void OnDestroy ()
		this.RemoveObserver(OnLvlChangeNotification, Stats.DidChangeNotification(StatTypes.LVL));

	#region Public
	public void Employ ()
		stats = gameObject.GetComponentInParent<Stats>();
		this.AddObserver(OnLvlChangeNotification, Stats.DidChangeNotification(StatTypes.LVL), stats);

		Feature[] features = GetComponentsInChildren<Feature>();
		for (int i = 0; i < features.Length; ++i)

	public void UnEmploy ()
		Feature[] features = GetComponentsInChildren<Feature>();
		for (int i = 0; i < features.Length; ++i)

		this.RemoveObserver(OnLvlChangeNotification, Stats.DidChangeNotification(StatTypes.LVL), stats);
		stats = null;

	public void LoadDefaultStats ()
		for (int i = 0; i < statOrder.Length; ++i)
			StatTypes type = statOrder[i];
			stats.SetValue(type, baseStats[i], false);

		stats.SetValue(StatTypes.HP, stats[StatTypes.MHP], false);
		stats.SetValue(StatTypes.MP, stats[StatTypes.MMP], false);

	#region Event Handlers
	protected virtual void OnLvlChangeNotification (object sender, object args)
		int oldValue = (int)args;
		int newValue = stats[StatTypes.LVL];

		for (int i = oldValue; i < newValue; ++i)

	#region Private
	void LevelUp ()
		for (int i = 0; i < statOrder.Length; ++i)
			StatTypes type = statOrder[i];
			int whole = Mathf.FloorToInt(growStats[i]);
			float fraction = growStats[i] - whole;

			int value = stats[type];
			value += whole;
			if (UnityEngine.Random.value > (1f - fraction))

			stats.SetValue(type, value, false);

		stats.SetValue(StatTypes.HP, stats[StatTypes.MHP], false);
		stats.SetValue(StatTypes.MP, stats[StatTypes.MMP], false);

First, I declared an array of StatTypes called statOrder – this will serve as a convenience array to help me parse data from the spreadsheets we created earlier. It is static because it wont change from job to job and this way they can all share.

Next I defined two instance arrays, one for holding the starting stat values, and one for holding the grow stat values. I was able to init them with a length equal to the length of the statOrder array from earlier. I might have decided to implement these as a Dictionary, but because Unity doesn’t serialize Dictionaries I decided to keep it as an Array.

There are three public methods. First is Employ which should be called after instantiating a job and attaching it to an actor’s hierarchy. In this method, we get a reference to the actor’s Stats component so that we can listen to targeted level up notifications as well as apply growth to the other stats in response. In addition, this method will allow any job-based feature to become active.

If you want to switch jobs, you should first UnEmploy any currently active Job. This gives the script a chance to deactivate its features and unregister from level up notifications etc.

When creating a unit for the first time, call LoadDefaultstats so that its stats will be initiated to playable values.

Job Parser

Now it’s time to create a script which can parse our spreadsheets and create project assets from them. Create a new script named JobParser in the Editor folder.

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

public static class JobParser 
	[MenuItem("Pre Production/Parse Jobs")]
	public static void Parse()
		CreateDirectories ();
		ParseStartingStats ();
		ParseGrowthStats ();

	static void CreateDirectories ()
		if (!AssetDatabase.IsValidFolder("Assets/Resources/Jobs"))
			AssetDatabase.CreateFolder("Assets/Resources", "Jobs");

	static void ParseStartingStats ()
		string readPath = string.Format("{0}/Settings/JobStartingStats.csv", Application.dataPath);
		string[] readText = File.ReadAllLines(readPath);
		for (int i = 1; i < readText.Length; ++i)

	static void PartsStartingStats (string line)
		string[] elements = line.Split(',');
		GameObject obj = GetOrCreate(elements[0]);
		Job job = obj.GetComponent<Job>();
		for (int i = 1; i < Job.statOrder.Length + 1; ++i)
			job.baseStats[i-1] = Convert.ToInt32(elements[i]);

		StatModifierFeature move = GetFeature (obj, StatTypes.MOV);
		move.amount = Convert.ToInt32(elements[8]);

		StatModifierFeature jump = GetFeature (obj, StatTypes.JMP);
		jump.amount = Convert.ToInt32(elements[9]);

	static void ParseGrowthStats ()
		string readPath = string.Format("{0}/Settings/JobGrowthStats.csv", Application.dataPath);
		string[] readText = File.ReadAllLines(readPath);
		for (int i = 1; i < readText.Length; ++i)

	static void ParseGrowthStats (string line)
		string[] elements = line.Split(',');
		GameObject obj = GetOrCreate(elements[0]);
		Job job = obj.GetComponent<Job>();
		for (int i = 1; i < elements.Length; ++i)
			job.growStats[i-1] = Convert.ToSingle(elements[i]);

	static StatModifierFeature GetFeature (GameObject obj, StatTypes type)
		StatModifierFeature[] smf = obj.GetComponents<StatModifierFeature>();
		for (int i = 0; i < smf.Length; ++i)
			if (smf[i].type == type)
				return smf[i];

		StatModifierFeature feature = obj.AddComponent<StatModifierFeature>();
		feature.type = type;
		return feature;

	static GameObject GetOrCreate (string jobName)
		string fullPath = string.Format("Assets/Resources/Jobs/{0}.prefab", jobName);
		GameObject obj = AssetDatabase.LoadAssetAtPath<GameObject>(fullPath);
		if (obj == null)
			obj = Create(fullPath);
		return obj;

	static GameObject Create (string fullPath)
		GameObject instance = new GameObject ("temp");
		GameObject prefab = PrefabUtility.CreatePrefab( fullPath, instance );
		return prefab;

Because this is a pre-production script, I didn’t put a lot of effort into it. There are hard coded strings, repeated bits of code, etc that could all be cleaned up, but this is not at all re-usable, and doesn’t need to be performant, so I felt no need to waste time on it. As long as it works, I am happy.

In order to make this script work its magic, we added a MenuItem tag. As the name implies, this adds a new entry into Unity’s menu bar. You should see a new entry called “Pre Production” and under that an option called “Parse Jobs”. Select that and our Job assets will be created in the project.

You can easily delete and recreate these assets at any time. Because of this, you might choose to ignore these assets in your source control repository, not that it hurts to keep them. All you truly need to version is the spreadsheet and parser, not the result of using them together.

It is possible to “listen” for changes to your spreadsheets and have the assets re-created automatically. See my post on Bestiary Management and Scriptable Objects for an example of this.

Init Battle State

Now that movement range and jump height stats are able to be driven by a job, let’s change our SpawnTestUnits code to create one of each of the three sample job types. The code to create and configure our units is getting a bit long, and is an indication that we will probably need some sort of factory class soon.

void SpawnTestUnits ()
	string[] jobs = new string[]{"Rogue", "Warrior", "Wizard"};
	for (int i = 0; i < jobs.Length; ++i)
		GameObject instance = Instantiate(owner.heroPrefab) as GameObject;

		Stats s = instance.AddComponent<Stats>();
		s[StatTypes.LVL] = 1;

		GameObject jobPrefab = Resources.Load<GameObject>( "Jobs/" + jobs[i] );
		GameObject jobInstance = Instantiate(jobPrefab) as GameObject;

		Job job = jobInstance.GetComponent<Job>();

		Point p = new Point((int)levelData.tiles[i].x, (int)levelData.tiles[i].z);

		Unit unit = instance.GetComponent<Unit>();



//		Rank rank = instance.AddComponent<Rank>();
//		rank.Init (10);


We will also need to convert our Movement component into a wrapper much like the Rank component was. For this, add a field to store a reference to the Stats component, and then turn range and jumpHeight into properties as follows:

public int range { get { return stats[StatTypes.MOV]; }}
public int jumpHeight { get { return stats[StatTypes.JMP]; }}
protected Stats stats;

Have the component get its reference to the Stats component in the Start method:

protected virtual void Start ()
	stats = GetComponent<Stats>();


Open the main Battle scene and play it. There should be three units as there were before, but we removed the variation on movement types – everyone walks for now. See that the range of the units is different depending on the job they began with.

Switch the inspector to Debug mode so that you can see the private stat data in the Stats component. You should see the values have been set according to the starting stats which we had specified for the job.

Stop the scene and go back to the SpawnTestUnits method of the InitBattleState then uncomment the two lines where we add the Rank component and init the starting level to 10. Play the scene a few times and look at the stats of each hero. You should see slight differences in the stats thanks to the random bonus portion of the Job.


In this lesson we discussed the various purposes of a Jobs system and looked at references from the Final Fantasy series. Then we began implementing our game via spreadsheets so that it would be easy to see and balance the data. We created an editor script which could then parse our spreadsheets and create prefabs as project assets. Finally, we tied these systems back into the main game so that our demo units have stats (including movement stats) which are driven by their job.

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.

49 thoughts on “Tactics RPG Jobs

  1. A quick question for you: Why, in the updated Movement class, did you put the line where you connect the stats component in the Start method, instead of the awake method, where you have the other connectors (unit and jumper)?

    I tried it with the connector in the Start method, and the stat line was never run, causing an error when range or jumpHeight tried to reference stats- the entire Start method wasn’t run, which is strange to me. Thinking I had mistyped something, I copied the movement class from your repository, and experienced the same error. Putting the connector line in the Awake function fixed the problem.

    1. I figured it out- I had left a blank Start method in WalkMovement that was overriding the Movement.Start method.

      I am still curious about the reasoning for placing it in start vs awake, however ๐Ÿ™‚ Is it to make sure the Stats are loaded before connecting it?

      1. Good question Jordan, I don’t remember any particular reason why I put it in Start instead of Awake. Perhaps just an oversight. Sometimes there are reasons – like if I was manually adding components instead of including them with the prefab, but in this case both Awake and Start would work fine.

  2. Alright, I got prefab generation based on .asset files to work. Basically each prefab has an ‘item’ component attached, but one of the variables in item is ‘sprite’.

    I don’t think there is a way to assign a sprite in the CSV file that generates the .asset, unless I reference the image name/path directly. This seems like bad practice.

    Is there a good method for data-driving images onto a prefab?

    1. I don’t think I would reference a whole path in the csv, but referencing the name of a sprite doesn’t seem bad to me. You can also try naming conventions so that the names of prefabs have similarly named assets which can be loaded, though in many cases the simple rules end up with too many exceptions and you might as well do it all.

      1. Do you think it’s best for me to fill the item data at runtime or to do it in editor similar to the .asset file generation?

        So for example, I have a bunch of prefabs with an ‘Item’ class attached but that class doesn’t have values assigned for its variables yet. Should I assign the variables at runtime only, or would you do it automagically through Editor scripts?

        1. It depends. If you reuse the same prefab with different item data sets then I would do it at runtime. If you always pair exactly the same prefab with exactly the same data, then I would do it at Editor time.

      2. I’ve been looking for an example resource but I’m having trouble finding one. Do you know of any good resources where the name of a file is referenced in an external data file and then that file is linked to the prefab?

  3. Okay i love your tutorial so far, but now i get an error saying “ArgumentException: The thing you want to instantiate is null.” and it brings me to the line GameObject jobInstance = Instantiate(jobPrefab) as GameObject; from SpawnUnits, but i don’t understand what could be wrong here :

    1. Glad you are enjoying it. Did you remember to run the “Pre Production/Parse Jobs” menu command? You won’t be able to instantiate a job prefab if you didn’t create it first.

      1. I wish I could edit my posts here, but the problem was I had misspelled “Rogue” in the stat.csv as “Rouge”. When that was fixed it could find everything fine.

  4. Hey Men

    ive got a confusing error.

    Assets/Scripts/Exceptions/ValueChangeException.cs(39,25): error CS1501: No overload for method `Modify’ takes `1′ arguments

    and also all my skripts wont loading. (Skript can not be loaded) everywhere

    do you know what i could do to solve this mistake ?

    could it be: when i have everywehre ( using system; and using System.Collections.Generic;

    that this send me the errormessage

  5. Hi Jon!
    I just completed this part, and I couldn’t figure out where I could see the stats growth after uncommenting the lines for Rank in the Demo.
    I ended up just doing a debug.log so I could see it. Was I supposed to see those stats somewhere else?

    1. Setting up a simple Debug Log is a perfect way to test for now. The stat growth wouldn’t mean much until you had some other sort of data that persisted outside of a battle, and I only built the battle portion of this project. I felt that the non-battle portions were less complicated, and would also be more varied, so I hoped that my readers would be able to implement it based on their own design.

  6. First of all, I am loving these tutorials. I appreciate how much effort you put into automating processes like putting jobs together. Seems really smart for balancing purposes to quickly change a value in a .csv and then pushing a button in unity to update all the objects.

    I am using this idea and putting it towards creating a skill tree type system but I am running into an issue where the created prefab is being cleared (all values set to NULL) when I push play in Unity. Any idea why this may be happening?

    Thanks again!

    1. I’m glad you’re enjoying them! Regarding your issue, do you experience a similar problem when using the Job prefabs? Do the skill prefabs hold values before you push play? Do they also regain their values after you exit play mode? If so, it might be that something in your code is clearing the values.

      1. Yeah, the skill prefabs are holding their values before pushing play and after pushing play. Upon exiting play the list that the skills are contained in is set to Null along with all the skills in my resource folder. Upon reparsing the values are restored.

        I’m going to make some tweeks to the skill tree and skill classes so that the data is private and see if that works. I’ll let you know how it goes. Thanks.

      2. After playing around with the classes I am still running into the issue where the assest values are set to NULL after exiting play as well as when reopening the project.

        I find this strange because I am not encountering this issue with the job assets created from this tutorial.

        Here is my set up; I have a Skill class which holds several variables. The variables are private and are set through the parsing command. After the Skill is made it is added to the skill tree class. The skill tree class is simply a private list.

        I really wonder what could be causing the values to be set to NULL when exiting play and reopening the project. If you have any idea hit me up. Thanks again for sharing your knowledge!

        1. Perhaps if you could share your project – or at least a simplified version with the Skill class, Skill Tree class, parser and sample data to parse I could recreate what you are seeing and might be able to help you out.

          1. You could zip your project and share with a link via something like drive.google.com or if you want to just copy and paste the relevant code, perhaps you could start a new thread on my forum since it is a bit off topic for this post.

          2. I created a post on the forum and added a zip with the scripts in question. Let me know what you think!

  7. I started your tutorial here and I like what I see so far. I noticed that you mentioned a job tree kind of system. I got excited and tried to skip ahead to find it, but it seems that you did not actually get into it.
    Any chance you will do that at some point? Or give some direction on how to make it with this one? For example, add another experience stat like a job point stat? Keep job levels like fftactics did?

    1. I didn’t actually implement the Job Tree in this project, and only hinted about the possibility. You seem to have picked up on the idea just like I’d hoped. I had been imagining something much like what you have suggested – creating new job related stats such as a string for a job name to use for evolution and an integer for the level required to upgrade. Beyond that you would just need to create a system to either observe the level up notifications or to provide a menu that could control the activation etc.

      1. I see, thank you for confirming my thoughts! I ended up making some extra logic to the onlevelup/changed parts, making a new separate joblevellistener script. Attached to a extra GO in my scene in battles to record. Still debating putting a stat on the unit for it or not, as that would be a lot of extra code. Right now I save it to an external file and read from that in a unit selection stage to change the jobs based on level.

        1. I finally got my system working all the way! I took a closer look at what you had on scriptable objects.
          I started by making a scrIptable object with the name, and a list of required jobs, then a list of required levels.
          I can add any new job, then simply add base requirements of other job levels to unlock it.
          I read all the jobs in the resource folder, then check them for the job subreqs. This way I only show currently useable jobs, or soon to be attained jobs for characters in the job change view. Made a simple overlay that when I finish will display the requirements if not met, or be disabled if the job can be selected already.

          Now I am looking to improve on the abilities system. I hope to make different skills unlock at different levels in the class. Any suggestions on how to do this? I was thinking I could just read the job level, and have a case statement in my character factory. Check the current job level, find the case for that level range, then add a number to the ability file to load maybe. Lvl 5 mage only knows missile attacks. Lvl 10 knows aoe elemental magic as well. Have to ability SO for the unit. What do you think?

          Also am looking at using the same job level to check for a mastered class, and if a class is mastered, be able to equip a second list of abilities. ( No idea what I will do here yet.)

          1. Awesome, congrats! Regarding other tips, I’d suggest opening a new thread in my forums. It’s too much for a nested comment to handle well.

  8. Any reason this wouldn’t work for the LevelUp function?

    void LevelUp ()
    for (int i = 0; i < statOrder.Length; ++i)
    StatTypes type = statOrder[i];
    int growth = Mathf.FloorToInt(growStats[i] + UnityEngine.Random.value);
    int value = stats[type];
    value += growth;
    stats.SetValue(type, value, false);
    stats.SetValue(StatTypes.HP, stats[StatTypes.MHP], false);
    stats.SetValue(StatTypes.MP, stats[StatTypes.MMP], false);

    Basically instead of juggling variables and doing an if statement I just added the number to the stats and truncated it.

    Also I'm still having trouble getting errors at the top of the website. I thought this one was fine, but it's just the home page that isn't having errors.

    Warning: count(): Parameter must be an array or an object that implements Countable in ….. on line 284

    1. It seems like there was a purpose to the fractional part of the stat, but to be perfectly honest, I don’t remember why that code is written that way. I know that I was closely following a mechanics guide and that they did things a little weird due to platform limitations etc. Mostly I did this so I didn’t have to put any effort into designing my own systems and could focus merely on getting something working. I would encourage you to feel free to redesign any or all of the systems as you see fit!

      I’m not sure why you’re getting errors on the website, but I will try to look into it a bit.

  9. Hi, How many elements should I be seeing for the stats (script) and should they be named yet in the debug window? (I think no for the second question, but just need to double check. Across all of the hero(clone)s, I see Element 10 and Element 11 at 0, with Element 12, 13, and 14 representing (presumably): speed, mov, and jump. All the other stats also seem accounted for from Element 0 – 9. Everything other than element 10 and 11 are non-zero values that seem to make sense and match the csv at level 1 at least.

    Should I be seeing those two elements at a value of 0? they seem extra to me, but nothing else is broken. I’m afraid I left some code snippet in or something that I wasn’t supposed to, but I don’t want to start a grand search just yet since everything else is fine, and I plan to move to the next lesson for now.

    1. There are 15 total elements, indexed from 0 to 14. They are not named when viewed through the Debug inspector other than being listed as “Element 0” etc. The elements at 10 and 11 currently hold the value of 50 for each of the heroes. They are configured on the Jobs prefabs via “Stat Modifier Feature” components – one for each with the Type specified as EVD and RES.

      1. Thanks for the quick response! I’m afraid I must still not be understanding something, as I don’t know where the value 50 gets added to this. From your guidance, and a little backtracking, I now get that those elements came from the StatTypes enumeration, but im still unsure where the value of 50 gets defined or set. it looks like I could manually change the stat in the inspector for an instance on the StatModifierFeature component, but I’m trying to figure out where in the code the value for those two stats of the generated prefab is produced. (The two attributes are not in the CSV’s either from what I am seeing.) I also don’t think I see EVD or RES mentioned in this lesson, so I’m unsure where to look right now.

        1. No, I think you are on top of things. It has been a long time since I made this, so I had to look a little deeper. If I delete the job prefab objects and recreate them in the repository project then they will end up with 0 for those two stats. I probably created the jobs, then added the stats manually (which in the long run is not an ideal way to do it). Good catch!

          1. Ah thanks again for the response and explanation! Everything has been really helpful for me on getting a grasp of how to create my own projects.

  10. So apprantly you cant add components to prefabs anymore… YOu have to do load prefab contents or something. Nobody ran into this problem?

    1. You can still add components to prefabs, they just modified how you view them in the interface. After selecting a prefab click the “Open Prefab” button in the inspector to view its first level contents like normal.

          1. I saw a forum it was like saveassettprefab and loadasassetprefab. I don’t know I couldn’t figure it out. What I have been trying to do is just create the jobs as scriptable objects but I am having trouble getting it to work. Your coding skills are WAY more advanced than mine! I am having trouble learning. Is the whole liquid fire you? I have tried to back up and get better. I am thinking about the CCG tutorial. Any suggestions as to where to start on your website? I read through all of the beginner stuff, I understand most of it.

          2. Yep, pretty much everything on this site is mine (I did borrow simple scripts in some places and give credit then). As far as where to start, that’s a good question. Generally I would have suggested the Tactics RPG because the architectural choices I made (which rely heavily on Unity) are very simple for beginners to understand. It is pretty old by now though and it’s not surprising that some of the content is now obsolete. The Unofficial Pokemon Board Game project was also pretty simple as I made it with my son to help him learn. I can’t comment on whether it will have the same issues.

            Each of my tutorial projects use different IDE’s, languages, and even different architectural styles, so you may want to favor whichever topic is most important or interesting to you. Unless you truly grasp all of the material you may find it difficult to apply concepts between the projects because of those differences.

          3. Hello. While going through this excellent tutorial I’ve run into this same issue. I made a few changes to the JobParser script to work around that. I can share my version of the script with you if you like.

          4. For some reason I was unable to reply to the poster below, but would love to see your script @matteo how you changed the job parser.

            Perhaps if you have it in google drive or such, but I would like to request you give a link to the methods needed perhaps first. Would be more beneficial to try and learn my way to a useable example and then compare with what you came up with after.

Leave a Reply

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