I’d love to move right to making our attack action now… but I need something to attack first. I guess we’ll need to spawn a monster!
Overview
We will be using this lesson to properly instantiate both the hero and monster of our encounter. We will do this based on the game and encounter data. This means we should be able to control what appears, and where it appears. If we want an encounter with multiple baddies, I want to be able to handle that easily too.
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.
Encounter
Let’s begin by fleshing out the Encounter asset a bit more. I want this asset to define a list of places where the hero party members will spawn, and I also want it to define a list of “something” that defines both a monster and position for the monster. I’ll need to define that “something” before I can make a list of it, so go ahead and add this snippet to the script:
using System; using System.Collections.Generic; [Serializable] public struct MonsterSpawn { public string assetName; public Point position; }
There we go, now I’ve got a string representing the name of an addressable asset from which I can create a new monster entity. This will be a “recipe” object much like the one we used to create the solo hero. As part of the same struct, I also define a position for where to spawn this monster.
Add the following to the IEncounter interface:
List<MonsterSpawn> MonsterSpawns { get; } List<Point> HeroPositions { get; }
This will allow us to determine how to spawn and place our hero and monsters for each combat. Note that I didn’t need to define the asset for the heroes because they should already exist outside of combat. I only need to know where to put them when battle begins.
Add the following to the Encounter class:
public List<MonsterSpawn> MonsterSpawns { get { return monsterSpawns; } } [SerializeField] List<MonsterSpawn> monsterSpawns; public List<Point> HeroPositions { get { return heroPositions; } } [SerializeField] List<Point> heroPositions;
I’ve implemented our new interface properties by wrapping some serialized fields of the same name and type. Pretty simple. Keep in mind that we can always create more complex implementations of this interface, such as making a version that randomly places both monster types and positions based on various criteria. As long as it can implement the IEncounter interface, then we are basically unlimited in options.
Encounter Prefab
Select the “Encounter_01” prefab asset from the Project pane. In the inspector we can implement the new fields. Add 1 entry to both new lists. For the monster spawn, we will specify an asset name of “Rat”, and a Position of (3,0). For the Hero Position we will use (-4,0).
Encounter Scene
Open the Encounter scene. Delete the instance of “Warrior” and “Rat” that we had added. Select the “Demo” object at the root of the Hierarchy and remove the “Demo” component script. You may also delete the Demo script from the project at this time.
By the time we finish this lesson, playing the game should not look any different that it had in the previous lesson, though we will know that things are appearing dynamically rather than by being statically placed.
Encounter System
Open the EncounterSystem script. We will allow this system to do more than simply set or get the “name” of an encounter. I want it to also handle the “Setup” of an encounter, such as applying whatever we’ve configured the asset with when an encounter begins.
We will need a couple of new using statements for some of the features we will add:
using UnityEngine; using Cysharp.Threading.Tasks;
Now you can add the following to the IEncounterSystem interface:
UniTask Setup(IEncounter encounter);
Add a couple of private fields to the EncounterSystem class:
string heroPath = "Assets/Prefabs/Combatants/Heroes/{0}.prefab"; string monsterPath = "Assets/Prefabs/Combatants/Monsters/{0}.prefab";
These strings represent a formatted path to one of our view prefabs for a combatant. The bracketed part ‘{0}’ will be replaced by the name of the asset we want to load.
Finally, add the following methods to the same class:
public async UniTask Setup(IEncounter encounter) { foreach (var spawn in encounter.MonsterSpawns) { var monster = await IEntityRecipeSystem.Resolve().Create(spawn.assetName); monster.Position = spawn.position; await CreateView(monster, monsterPath); } var hero = ISoloHeroSystem.Resolve().Hero; hero.Position = encounter.HeroPositions[0]; await CreateView(hero, heroPath); } async UniTask CreateView(Entity entity, string path) { var viewProvider = IEntityViewProvider.Resolve(); var assetManager = IAssetManager<GameObject>.Resolve(); var key = string.Format(path, entity.CombatantAsset); var view = await assetManager.InstantiateAsync(key); view.transform.position = entity.Position; viewProvider.SetView(view, entity, ViewZone.Combatant); }
You might notice that I made the Encounter asset hold a list of hero positions even though we only have a single hero and would therefore only need a single position. I did this because I am looking forward to the future, where I expect to eventually modify our combat to have a whole hero “party” and will want to place multiple heroes.
You might also notice that we only spawn a single monster as well, but both the code and asset already support more than one monster in case you want to try adding multiple on your own. You would simply need to specify additional monster spawns on the encounter asset.
When creating both the hero and monster entities we call the CreateView method for it, which will load and instantiate the appropriate prefab asset and assign the instance to the view provider so that we can obtain references whenever we need to.
Encounter Flow
We used the EncounterFlow to begin a new encounter based on the asset, so that is also where we will perform the “Setup”. Open the script and update the Enter method to look like this:
async UniTask<IEncounter> Enter() { await SceneManager.LoadSceneAsync("Encounter"); var asset = await IEncounterAssetSystem.Resolve().Load(); await IEncounterSystem.Resolve().Setup(asset); return asset; }
Combatant Asset System
Each Entity should hold data that determines what “view” is loaded to represent it. Create a new C# script at Assets -> Scripts -> SoloAdventure -> Encounter named CombatantAssetSystem and add the following:
public partial class Data { public CoreDictionary<Entity, string> combatantAsset = new CoreDictionary<Entity, string>(); } public interface ICombatantAssetSystem : IDependency<ICombatantAssetSystem>, IEntityTableSystem<string> { } public class CombatantAssetSystem : EntityTableSystem<string>, ICombatantAssetSystem { public override CoreDictionary<Entity, string> Table => IDataSystem.Resolve().Data.combatantAsset; } public partial struct Entity { public string CombatantAsset { get { return ICombatantAssetSystem.Resolve().Get(this); } set { ICombatantAssetSystem.Resolve().Set(this, value); } } }
Here we define a new piece of game data that maps from an Entity to a string where the value represents the name of a prefab which should be loaded as the combat “view” of the entity.
Open the SoloAdventureInjector script and add the following to its Inject method:
ICombatantAssetSystem.Register(new CombatantAssetSystem());
Combatant Asset Provider
Next I want to add a new attribute provider which specifies the combatant asset value for an entity. Create a new C# script at Assets -> Scripts -> AttributeProvider named CombatantAssetProvider and add the following:
using UnityEngine; public class CombatantAssetProvider : MonoBehaviour, IAttributeProvider { [SerializeField] string value; public void Setup(Entity entity) { entity.CombatantAsset = value; } }
Entity Recipe Assets
Now we need to create a new entity recipe prefab asset for our “Rat” monster. Create an Empty GameObject named “Rat” and create a prefab from it. It should be saved to the same folder as our Hero – Assets/Objects/EntityRecipe/.
Once you’ve got the prefab, delete the instance from the scene and select the project asset. Check the box to make the asset Addressable.
Add an EncounterActionsProvider component with the following actions: “Bite” and “Stride”.
Add an CombatantAssetProvider component with the following value: “Rat”.
Now select the existing “Hero” prefab asset in the same folder. Add our new CombatantAssetProvider with a value of “Warrior”.
Finally, find the combatant view assets and enable Addressables for both:
- Assets/Prefabs/Combatants/Heroes/Warrior
- Assets/Prefabs/Combatants/Monsters/Rat
Demo
Run the game like normal. When you reach the encounter scene, you should still see both the hero and rat, just like you used to, but this time they were loaded dynamically. To demonstrate this, feel free to edit the the “Encounter_01” asset by adding an additional spawned monster, or by changing the spawn positions.
Also note that you can still move the hero around using the stride action, but now, the selection indicator starts out at the correct place.
Bug fixes
While playing around, I encountered several more bugs – bugs which I didn’t expect, and so decided to fix now. The first thing I noticed was some issues on the slider. It was modifying the slider value as I used arrow keys for navigation, and also the full and empty states looked wrong. For example, at a value of 1, I didn’t want to see any red, and at a value of 0, I didn’t want to see any green. Feel free to examine the project at the end of this lesson to see how.
I also noticed that on the main screen, choosing “Continue” could cause a crash when loading an encounter. There were actually several reasons for this. First, it turns out that my Entity struct wasn’t persisting correctly. Despite being marked as serializable, the `readonly` id wasn’t playing nicely with Unity’s Json serializer. Second, the encounter actions had only been saved as an in-memory table. I fixed it by adding the table to the Data class. Third, it turns out that the serializer also struggled with the encounter actions generic list. It can actually work with lists, but the core dictionary already saves a list for its values, resulting in a list of lists which the serializer can’t handle. I was able to work around this issue by wrapping the inner list in a serializable struct named EncounterActions.
All of the above fixes and relevant refactoring are included in the project zip at the end of this lesson.
Summary
In this lesson, we added everything we needed to allow our encounters to load more dynamically. In particular by providing control over what would spawn, and where it could spawn. Until now the rat had only existed as a view, but now there is an “Entity” that the view represents. Once we add a few more game mechanics we will be able to have a proper battle!
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!