“Blacksmithing might be an ancient profession, but you are its cutting edge…” – Alloysmith, Pathfinder
Overview
In the previous lesson we started a deeper dive into character generation by partially implementing Ancestries. In this lesson, we will take the next step with character Backgrounds. If you are not already familiar with this game mechanic, you may read about it here.
We will take a pretty similar approach to the way that Ancestries were implemented. Start from a json document that holds all the information about Backgrounds, create a parser to turn that document into project assets (prefabs), and use Addressables to load the assets at runtime when we need them. For simplicity sake, I will continue to have the hero party be auto-generated, but we can allow the “chosen” background to be picked based on the “suggested” backgrounds of the Ancestry to tie things together.
Getting Started
Feel free to continue from where we left off, or download and use this project here. It has everything we did in the previous lesson ready to go.
You will also need to download and import this package, which contains the json file representing the various Backgrounds. Let’s examine what we’ve got:
{ "datas": [ { "title": "Academy Dropout", "abilities": [ "Intelligence", "Charisma" ], "skills": [ "Arcana", "Academia Lore" ], "feats": [ "Dubious Knowledge" ], "rarity": "Common", "summary": "You were enrolled at a prestigious magical academy, but you've since dropped out. Maybe there was a momentous incident, maybe you had to return to …" }, ... Other Backgrounds listed here ... ] }
Just like with Ancestries, I have structured the data to be more compatible with Unity’s Json utility. That means the root is an object, and within that object I have a key to the array of content that I actually care about. In addition to general information about the Background itself (such as the title, rarity, and summary) the background provides special “training” to a character in the form of ability boosts and skills (also feats but I haven’t implemented those yet).
Background
Let’s start by creating the Background – this will be a MonoBehaviour based script that can be attached to a GameObject Prefab just like we did with the Ancestry. Create a new C# script at Scripts -> Component named Background and add the following:
using UnityEngine; using System.Collections.Generic; public interface IBackground { string Title { get; } Rarity Rarity { get; } string Summary { get; } List<IAttributeProvider> AttributeProviders { get; } } public class Background : MonoBehaviour, IBackground { public string Title { get { return _title; } set { _title = value; } } [SerializeField] string _title; public Rarity Rarity { get { return _rarity; } set { _rarity = value; } } [SerializeField] Rarity _rarity; public string Summary { get { return _summary; } set { _summary = value; } } [SerializeField] string _summary; public List<IAttributeProvider> AttributeProviders { get { return new List<IAttributeProvider>(gameObject.GetComponents<IAttributeProvider>()); } } }
Once again we start with an interface so that the consumers of the Background are not tied to any concrete idea of what a Background “is”. In other words, I don’t want to let other code know that it is a MonoBehaviour, or I might be tempted to get sloppy with my architecture. For example, if I took direct advantage of the component relationship, then I am locked in to that design, and couldn’t swap from prefabs to scriptable objects, or to native objects decoded straight from a REST response etc. I probably won’t change my design, but its still nice to know that I could.
Background System
Next we will create a system that assigns “Background” information to an Entity. So create new script at Scripts -> Component named BackgroundSystem and add the following:
public partial class Data { public CoreDictionary<Entity, string> background = new CoreDictionary<Entity, string>(); } public interface IBackgroundSystem : IDependency<IBackgroundSystem>, IEntityTableSystem<string> { } public class BackgroundSystem : EntityTableSystem<string>, IBackgroundSystem { public override CoreDictionary<Entity, string> Table => IDataSystem.Resolve().Data.background; } public partial struct Entity { public string Background { get { return IBackgroundSystem.Resolve().Get(this); } set { IBackgroundSystem.Resolve().Set(this, value); } } }
Once again, this is just a simple Entity Table System that maps from an Entity to a String, where the String value is merely the Title of a Background Asset.
To use this system, inject it in the ComponentInjector:
IBackgroundSystem.Register(new BackgroundSystem());
Background Asset System
Next, we need the system that knows how to load a Background Asset based on an assigned Background title. Create a new script at Scripts -> AssetManager named BackgroundAssetSystem and copy the following:
using UnityEngine; using Cysharp.Threading.Tasks; public interface IBackgroundAssetSystem : IDependency<IBackgroundAssetSystem> { UniTask<IBackground> Load(string name); } public class BackgroundAssetSystem : IBackgroundAssetSystem { public async UniTask<IBackground> Load(string name) { var assetManager = IAssetManager<GameObject>.Resolve(); var key = string.Format("Assets/AutoGeneration/Backgrounds/{0}.prefab", name); var prefab = await assetManager.LoadAssetAsync(key); return prefab.GetComponent<IBackground>(); } }
To use this system, remember to inject it in the AssetManagerInjector:
IBackgroundAssetSystem.Register(new BackgroundAssetSystem());
Skills Proficiency Provider
We already have an attribute provider for skills, but we need to make a quick change to make it easier to configure from another script. Go ahead and open the SkillsProficiencyProvider script and change its valuePairs field to be public.
// Change this: [SerializeField] List<ValuePair> valuePairs; // To this: public List<ValuePair> valuePairs;
Background Parser
Now we are ready to create the parser that will read our json document and create background assets from it. Create a new C# script at Scripts -> PreProduction named BackgroundParser and add the following:
using System.Collections.Generic; using UnityEngine; using UnityEditor; using System; public static class BackgroundParser { [System.Serializable] public class BackgroundList { public List<BackgroundData> datas; } [System.Serializable] public class BackgroundData { public string title; public List<string> abilities; public List<string> skills; public List<string> feats; public string rarity; public string summary; } [MenuItem("Pre Production/Generate/Backgrounds")] public static void GenerateAll() { if (!AssetDatabase.IsValidFolder("Assets/AutoGeneration")) AssetDatabase.CreateFolder("Assets", "AutoGeneration"); if (!AssetDatabase.IsValidFolder("Assets/AutoGeneration/Backgrounds")) AssetDatabase.CreateFolder("Assets/AutoGeneration", "Backgrounds"); string filePath = "Assets/Docs/Backgrounds.json"; TextAsset asset = AssetDatabase.LoadAssetAtPath<TextAsset>(filePath); var result = JsonUtility.FromJson<BackgroundList>(asset.text); foreach (var data in result.datas) { GenerateAsset(data); } } static void GenerateAsset(BackgroundData data) { var asset = new GameObject(data.title); AddBackground(asset, data); AddBoosts(asset, data); AddSkills(asset, data); CreatePrefab(asset, data); GameObject.DestroyImmediate(asset); } static void AddBackground(GameObject asset, BackgroundData data) { var bg = asset.AddComponent<Background>(); bg.Title = data.title; bg.Rarity = (Rarity)Enum.Parse(typeof(Rarity), data.rarity); bg.Summary = data.summary; } static void AddBoosts(GameObject asset, BackgroundData data) { var boosts = new List<AbilityBoost>(); foreach (var ability in data.abilities) { if (string.IsNullOrEmpty(ability)) continue; AbilityBoost boost; if (Enum.TryParse<AbilityBoost>(ability, out boost)) { boosts.Add(boost); } else { Debug.Log("Unhandled boost: " + boost); } } var boostProvider = asset.AddComponent<AbilityBoostProvider>(); boostProvider.boosts = boosts; } static void AddSkills(GameObject asset, BackgroundData data) { var valuePairs = new List<SkillsProficiencyProvider.ValuePair>(); foreach (var name in data.skills) { if (string.IsNullOrEmpty(name)) continue; Skill skill; if (Enum.TryParse<Skill>(name, out skill)) { var pair = new SkillsProficiencyProvider.ValuePair(); pair.skill = skill; pair.proficiency = Proficiency.Trained; valuePairs.Add(pair); } else { Debug.LogWarning("Unhandled skill: " + name); } } var provider = asset.AddComponent<SkillsProficiencyProvider>(); provider.valuePairs = valuePairs; } static void CreatePrefab(GameObject asset, BackgroundData data) { string path = string.Format("Assets/AutoGeneration/Backgrounds/{0}.prefab", data.title); PrefabUtility.SaveAsPrefabAsset(asset, path); } }
If you compare this with the Ancestry parser, you should notice that the two follow the same pattern. I created local models for representing the raw json data, then used those models to easily configure attribute providers with the data they would need to configure an Entity. The attribute providers are attached to dynamically created GameObjects, which are then saved as project assets, and then we can delete the original instance from the scene.
Generate Backgrounds
Head over to Unity and notice that another option has been added to the menu. Select “Pre Production -> Generate -> Backgrounds”. You may see a spinner for a bit, because we are making 198 new assets this time around. Given such a large number, hopefully you appreciate not having had to create these by hand!
Take a look at the newly generated assets to verify that they were created correctly. Then take a look in the console at the various warnings. Most of the “unhandled” warnings are some kind of “lore”. For example, the first Background “Academy Dropout” has “Academia Lore”. The parser currently fails to convert this into a “Skill” because the Skill enum would only recognize “Lore” and not its various categories. When we get around to implementing “Lore” game mechanics we will need to be able to track the “Academia” part of the “Lore”. For now, since I am not actually using this game mechanic yet, we can just skip it.
There are also some “skills” that were provided as a sentence such as: “Lore related to one terrain inhabited by animals you like (such as Plains Lore or Swamp Lore).” Or skills that were optional: “Athletics or Performance”. Once again, these are not yet handled. We can always improve both the data and json parser over time as needed.
Select all of the backgrounds then click the “Addressable” check box so that we can load them in game.
Random Background Provider
Now that we have a bunch of new Background assets, we need to consider how to apply them. The Ancestry data provides a list of suggested backgrounds, so let’s start there. Create a new script at Scripts -> AttributeProvider named RandomBackgroundProvider and add the following:
using System.Collections.Generic; using UnityEngine; public class RandomBackgroundProvider : MonoBehaviour, IAttributeProvider { public List<string> names; public void Setup(Entity entity) { // TODO: If names is empty, pick from a common list if (names == null || names.Count == 0) { Debug.LogError("No background assigned"); return; } var rnd = IRandomNumberGenerator.Resolve().Range(0, names.Count); entity.Background = names[rnd]; } }
Ancestry Parser
We will want to attach the RandomBackgroundProvider to our Ancestry assets, so go ahead and open the AncestryParser script. Inside of the GenerateAsset method just after adding the Ancestry, insert the following lines:
var backgrounds = CommaSeparatedStrings(data.backgrounds); asset.AddComponent<RandomBackgroundProvider>().names = backgrounds;
Re-Generate Ancestries
Head back over to Unity and select the menu option “Pre Production -> Generate -> Ancestries”. I believe that Unity is overwriting the original assets here, and so in theory any manually made changes would be lost. I was expecting that to include the “Addressable” setting for the assets, and I think I have had to re-enable “Addressables” before, but recently I haven’t needed to. To be safe, verify that it is still enabled before moving on.
Create Hero Party Flow
Now we can finally assign backgrounds to our newly created hero party. We will handle this step of the code in the CreateHeroPartyFlow so go ahead and open that script. Add the following method:
async UniTask LoadBackground(Entity entity) { UnityEngine.Debug.Log(string.Format("Loading Background: {0}", entity.Background)); var backgroundAsset = await IBackgroundAssetSystem.Resolve().Load(entity.Background); foreach (var provider in backgroundAsset.AttributeProviders) { provider.Setup(entity); } }
We will invoke the new method from within the Play method, just after the await LoadAncestry:
await LoadBackground(entity);
Hero Recipe
One last step. Since we are now assigning skills from assets such as the Background, we don’t need the default ones on our Hero prefab. Open the asset at Objects -> EntityRecipe -> Hero and delete the Skills Proficiency Provider that we had been using. Save the project.
Demo
With that, we’re done. Go ahead and play the game from the beginning (the “LoadingScreen”) and see what you get. On a sample play I received:
- Dwarf, Warrior
- Elf, Scholar
- Gnome, Tinker
- Halfling, Entertainer
Summary
Hopefully this lesson was simple – more of a refresher of what we’ve already done in the past. We added a new document representing backgrounds, parsed the data, and added systems to assign Backgrounds and load their assets. We modified and added attribute providers as necessary so that recommended backgrounds on Ancestries could now be loaded on our Hero.
If you got stuck along the way, feel free to download the finished project for this lesson here.
If you find value in my blog, you can support its continued development by becoming my patron. Visit my Patreon page here. Thanks!