In version 1.0 we explored the beginning architecture for a Spawning Manager; however, it was not perfect, and at the end we talked about some problems with its design. We are going to try and address some of those concerns with a version 2.0 of the architecture.
Below are any new terms that will need to be defined for this new version.
|Weight||A means of controlling the LIKELIHOOD of a random choice.|
|Conditions||There are a few different types of [Conditions] used by the system:
|Spawning Queue||Once the conditions have been met, a [Wave] will push all of it’s assigned [Units] onto a stack, which the system attempts to spawn. For each [Unit] it attempts to find a [Spawn Location] that satisfies that unit’s [Spawn Rules].|
|Spawn Locations||A Point or Volume in the level that specifies where [Waves] are allowed to spawn their [Units]. These are placed by designers.|
|Spawn Rules||These are rules that govern HOW [Units] choose their [Spawn Locations]. These are generally weighted values, and not binary flags, though binary flag rules are possible. As an example, a [Unit] might have a “Proximity To Player” weight, but also have a “Off Camera” flag. For more information, see below.|
|Scene||The physical space that the spawning will take place. The Scene contains data about the [Encounters] that take place within it, and it contains data about valid [Spawn Locations]|
|Scenario||A [Scenario] is a container for a set of [Encounters]. It has an assigned Point [Value]. The system distributes these points between the [Encounters] in a [Scene]. Each [Encounter] begins as soon as the player satisfies its set of [Start Conditions], and an [Encounter] ends when the player satisfies the [End Conditions].|
The architecture is about to get a bit more complicated, so let’s start with a review of the previous version to remind ourselves of what we’ve already built.
Review of Previous Version
The Spawn Manager is built upon pre-designed Templates and Unit Point Values, which designers have set up at tool time. At Game time, the Spawn Manager is given a number of points that it wants to “spend”. These points are divided up between all of its assigned Encounters, each Encounter is assigned a Template, and then each Encounter “purchases” units from it’s Template until it runs out of points.
Once it has purchased all of its Units, it bundles them into Waves and determines the activation conditions for each of those waves, based upon some global information.
I will start by covering the new additions to the system, and then I will move onto updates to the old parts of the system. Topics will proceed in the following order:
- The Scene Side of our System
- The Story Beat System
- Updates to Version 1.0
- Problems That STILL Exist…
- Visualization of a Problem
I’m going to dive right into the major update to the architecture with this revision, which is adding data and information into the scene to aid the system.
After that, I will move onto the second major problem with the previous design, which was a lack of authorship control. This will be a big topic. I won’t be giving up as much control as you’d probably like, at least at first, but we’ll take some initial steps to enhance the user’s control over the system.
That will cover the major new updates to the architecture. Beyond that I’ll briefly walk through all the previous parts of the system and talk about what they look like now in version 2.0.
Next we’ll talk about the problems that STILL exist with this design, and talk a bit about how version 3 is going to address those concerns. Lastly, once we are done, I will give another little interactive example, to help highlight some of the issues that still exist.
The Scene Side of Spawning
We have been neglecting a pretty obvious and important aspect of this system, which was how we actually determine WHERE things are going to spawn.
For this we must add some information into the scene itself. There are a LOT of ways you can do this. You can add volumes to the scene, you can add additional meta-data to the navmesh (if you have one), or you can manually place points… I am going to choose one for the sake of continuing with this system (volumes), but just understand that the EXACT choice that you make for your game would be heavily dependant on the TYPE of game you are making.
What is universal, regardless of you choice, is that we must add authorship control back into the system in some way. Ok, well, this should be pretty straight forward, yeah? Let’s create an object we can place in the scene, and to that we can attach some information, like a number of points to spend, and then we can tell it what kind of template it should use, maybe from a dropdown? And th… STOP RIGHT THERE CRIMINAL SCUM!
We JUST talked about this in version 1.0. You are attaching balancing data into the scene. This is antithetical to our goal!
As the person balancing or tuning, we are trying to avoid opening up EVERY Scene in the game. You must fight this desire, as it can be very easy to acquiesce to the easy solution. So, the question then becomes what authorship goes into the scene, and what stays outside the scene? Let’s try an analogy.
Picture two people on opposite sides of a wall. They can can pass notes, but they cannot SEE what the other person sees. In this case, on one side of the wall is our Level Designer, who can see the scene. On the other side of the wall is us, the System Designer, who can see the Unit and Template Tables. What notes do we need to pass between the two of us such that we never need to look at the other side of the wall, and how can we do our jobs while passing as few notes as possible?
To do that, we have our ask ourselves the following:
- What is the goal of the users on either side of the wall.
- What is the minimum amount of information that they NEED to accomplish their goals.
On our side, our goal is to be able to balance and tune the game without opening any scenes. To understand the goals of the other side, we can revisit an old article I once wrote about making good combat encounters, which you can read about here. To summarize, you are trying to tell a good story through the combat, and you are trying to always keep in mind the pacing of the entire experience.
Without knowing where an encounter exists within the context of the scene, there’s no way that the spawning manager could know enough about it, so we clearly need to give the level designer SOME authorship controls, so they can have agency over the pacing. Having said that, we don’t really want the scene to have to care about the specific units that they are spawning, because that kind of specificity should be easily adjustable. If we decide, for example, that the Cyclops should show up in level 3, instead of level 4, that should be easy to change. Additionally, and more importantly, the Level Designer has no context for “the amount of points needed to achieve the experience they want.”
To fully unpack the final answer to these questions would take up most of this article, but I wanted to give you a taste of the type of thought process that you should go through when facing a problem like this. In the end, we are going to be adding the following features to the system:
- A Scenario Asset
- Encounter Assets
- Spawn Volumes Assets
- Story Beat Data
This is a centralized object that holds references to all encounters in a scene, and it currently is where we define the number of points for ALL of the encounters in this scene. This level of authorship will likely not stay here in a future version, but for now its job is to equally assign points to its linked encounters. If a Scenario Asset is set to 200 points, then, for now, each linked Encounter has 200 points. We will modify this in later versions.
Note, there’s nothing that requires this to be physically present in the scene. If we were building this game in Unreal, for example, there’s no reason for this to be an actor that you put into the scene (unlike the Encounters, which we’ll talk about next). This could be a Data Asset that is associated with this scene, or the data could be directly added to the Game State. The specifics of which are beyond the scope of this document, but worth pointing out.
The level designer will be placing Spawn Volumes in the world, which determines the valid areas where units can be spawned into the world. We don’t want units appearing on top of boxes, or under the world, and only the level designer has the proper context.
This begs the very important question: how does it decide WHICH spawn volume to pick, and where WITHIN that volume does it place our unit? There are three aspects to this:
- An Association between Encounter Assets and Volumes (see below)
- Rules for Choosing the Volume
- Rules for Positioning within the Volume
For this we are going to be designing “Spawning Rules”, which are a probability-weight driven method of choosing the BEST option between many options. You rarely, if ever, want your spawning rules to be binary (as in YES/NO flags). The last thing you want is for your system to stall because it cannot find a location to spawn something. There are exceptions to this, of course, such as a flag to force only spawning a unit out of Line of Sight, but for all other things use WEIGHTING over FLAGS.
We start by adding Archetype Spawning Rules to the Global Data. Remember: always start with generic rules, first! These rules determine where Units like to spawn WITHIN their Volume, and it does this by weighting their choice by how much they prefer to spawn near other archetypes. These will be detailed further below.
Note: This does NOT determine what volume they spawn in, because THAT is determined by their WAVE. Why might you ask? Well, if you remember from Version 1, a Wave can be a composite of more than one type of unit. If you tried to blend between their weights, you might have success between archetypes that share similar, but slightly different, desires….. Buuuuutt you’d likely have a strong conflict between a Melee archetype and a Ranged archetype. Their blend would produce chaotic output.
You could ALSO consider spawning every Unit or Archetype into its own Volume, regardless of the wave that it has been composed into, but that is ALSO not want you want 100% of the time. In fact, you often want there to be cohesion between your spawns (a big tanky shield guy, with shooters behind him). The solution is actually quite simple.
What we REALLY need are Global Wave Spawning Rules, which govern how waves choose the volumes they are going to dump their units into. Already, you are thinking, this doesn’t provide NEARLY the authorship control that you’d want in a system, and…. you are correct. But hold on, we are going to get there. Similar to the Archetype Spawning Rules, the specific rules we are going to add are detailed in the section where I go over the changes to features from Version 1. For now, let’s move on to the Encounter.
The Encounter Asset is an object that we place in the Scene.
Note: It’s physical location IS important, as we will be using it as a reference point.
As we discussed above, we (the system designer) have no context for the physical layout of the space, nor should we; only the person placing the Encounter in the level knows “where do I want things to spawn.” The Encounter Asset has an association with Spawn Volumes, whether it is by pure proximity, or by linking them directly.
The last piece of information attached to the Encounter is a very important piece of data, which we are calling the Story Beat. That is a much bigger feature, so we will break that up a bit more in the next section.
Story Beat System
When setting up an encounter, you have context for the general pacing of where this fight takes place in the Scene, but you don’t necessarily need the specifics of what type of units are actually going to show up, nor should you be required to worry about the specifics of when things would spawn. (Narratively, you do… but just hold onto that thought).
To return to our analogy of the passing notes, you want to be passed a note that resembles a restaurant menu filled with options that can satisfy your needs; but, to continue the analogy, you needn’t know how the chef is going to prepare your meals. In other words, when setting up an encounter, you will be given a list of predefined options that will satisfy the pacing needs of your encounter, but frees you from needing to know the specifics of what will spawn.
Story Beats are set up by the system designer, and they define a few things:
- What Spawns
- How it Spawns
- When it Spawns
Each Story Beat defines the templates that it is allowed to spawn. When an encounter activates, it sends a message to the Story Beat, which choses, at random, one of its assigned Templates.
How and When It Spawns
In our previous version, we talked a lot about how Waves get constructed. It was one of the more complicated aspects of version 1, but it’s about to get even more complicated. In the previous version there was a 4 step process to the construction:
- Purchase Units from our Template
- Break those Units up into Homogenous Waves
- Combine Homogenous Waves Together
- Assign Trigger Conditions
Take the step where we create waves, for example. Currently, this is a giant monolithic black box of a system, where on one side of the system you input an array of tuples that define the type and number of each unit you want to spawn, and out the other side it outputs a 2 dimensional array of waves, where each wave has a certain number of units.
But, if you think about it, our Wave Creation algorithm doesn’t really KNOW about units and waves. It just takes in an array, and spits out more arrays. If you remember, it uses recursive logic to split up Units into smaller groups based upon two parameters that we defined in the Global Settings (Division Count, and % of Total). As long as it receives SOME kind of array that it understands, it should work just the same.
So what if we broke this up even more? What if we had SEVERAL black boxes, where each one of them doesn’t really care what the other black box did or is doing. As long as they receive the data they are looking for, they are happy.
Let’s picture the system like this, instead.
Ok, but what does this mean, and why am I asking you to think of Wave Creation in this way? First, what do I mean by “Pre Process” and what is an example of a “Post Process”. During the preprocessing step we can attach additional data to our units before we start constructing waves. Some examples include:
- Limiting the total number of units we are about to spawn in some way.
- Attaching meta-data to units, or buffing their stats.
These “rules” or “processes” we are applying can be thought of as generic “components”. We can create any number of “Components” that modify our data, and, as long as the information being returned is understandable by the next component, they can be chained together! This is hard to picture, I realize, but just hold on. The power here is great. If we take this approach, we can make the entire process completely modular!
Note: We are not limited to applying this to “Step 2: Create Waves.” We can also apply this to Steps 1, 3 and 4!
You might be asking at this point, why are we creating this level of modularity? Well, because we want the power and flexibility to define enough “Story Beats” to satisfy the many and varied needs of the Level Designer, while keeping it as a black box that doesn’t require knowledge about the tuning and specifics. Story Beats tend to be incredibly varied, so our spawning system needs to be VERY flexible, or you will end up doing a lot of hard-coded, one-off solutions.
Wave Creation Components
OK, we are ready to get into the nitty-gritty of this new system. Wave Creation Components come in two types:
- Processors — Mutates the data, but always returns the same type of data
- If the Input was an Array, it must return an Array.
- They are not necessarily Commutative! (A then B is not necessarily the same result as B then A).
- Later Processors can end up overriding early processors, so be careful!
- Creation Rules — Responsible for the creation of new types of data based upon Rules.
- Can be defined as either AND or OR
Our original 4 Steps (Purchase, Create, Combine, and Assign Triggers) has now become 8 Steps, and each step can use any number of the following components:
1 – Pre Purchase Step (Processing)
- Archetype Value Scalar
- Archetype Weighting Scalar
2 – Purchase Step (Rules)
- [We don’t have any yet, but in the future we might]
3 – Post Purchase Step (Processing)
- Create Leader Unit
- Apply Quantity Scalar
- Apply Quantity Square
- Apply Quantity Square Root
- Set Max Quantity
- Normalize and Apply Scalar Quantity
- Apply Archetype Histogram
4 – Wave Creation Step (Rules)
- Division by Count
- Division by Percentage of Quantity
- Division by Percentage of Health
5 – Pre Composition Step (Processing)
- Sort by Quantity
6 – Wave Composition Step (Rules)
- Single Stack Composition
- Right Aligned Matrix Composition
- Left Aligned Matrix Composition
7 – Post Composition Step (Processing)
- Sort by Leader
- Sort by Archetype
- Sort by Wave Size
8 – Wave Trigger Condition Assignment Step (Processing)
- Health Trigger %
List of Story Beats
With these components, we are ready to create a few story beats. How many should we create for our game? If, by the end, you have 100 different Story Beats, then you are fighting the design of the system. We are going to start by creating 6, and that is actually enough to get quite a bit of variation in our spawning.
- Downbeat – a small encounter, never meant to be a threat.
- Surprise – a small encounter, but can happen when the player is low on resources.
- Standard – a standard encounter
- Pincer – a variation on the standard encounter, where we change spawning rules
- Swarm – an emphasis on the pest archetype
- Challenge – an emphasis on the heavy, or boss archetype
I won’t take the time to show you the full setup for each of these, but let’s take a single one of them and show how you would use our new architecture to achieve that story beat. Moreover, you needn’t keep the processors I have here. That’s the whole point. With this architecture, your only limit on the type of story beats you create is your ability to combine your processors in interesting ways.
Swarm Story Beat
- Template List: [Pest, Melee], [Pest, Ranged], [Pest]
- Pre Creation Step (ORDER MATTERS)
- Apply Square (Squares the quantity of each unit)
- Normalize and Apply Scalar Quantity (35)
- Wave Creation
- Division Count: 20
- Pre Composition Step
- Sort by Quantity
- Wave Composition
- Left Aligned Matrix
- Wave Trigger
- Time (15 seconds)
We have basically created a scenario where, regardless of how many points got spent, it is GOING to spawn AT MOST 35 pests. The other enemies spawned, will depend on the type of enemy (again, point value is still aiding us here), but we’ve done something interesting. We first squared the quantity of each unit, and then normalized it. This has the effect of putting an EVEN GREATER emphasis on what was already the higher quantity unit.
The person setting up the Story Beats wields a phenomenal amount of power, and we are definitely trusting them, but this is the person with the most context for balance, so we should be ok with that!
Updates to Version 1.0
That summarizes all the major new features in Version 2. Let’s look back at the parts of Version 1 and what’s changed about those.
You might be wondering how this applies, now that we have the Story Beat system, but you are not required to define every part of the Story Beat. Anything that you leave out will simply use the Global Defaults!
Remember, and I’m going to repeat this ad-infinitum: Specific Overrides Generic. But for that to hold true, there must be generic settings!
There are also some new rules that we need to add for spawning, which we talked about above. Rules for how the Archetypes like to spawn within their chosen volume, based upon their preferences, and rules for how the waves choose the volumes they are going to spawn their units into.
Per Archetype Spawning Rules
- Proximity to [Archetype]
- Proximity to Player
- Spawn on Camera
For each archetype that we defined in version 1, we specify some rules for how it PREFERS to spawn. Remember, these are WEIGHTS not FLAGS. We give each Archetype a general preference how they like to spawn, with regards to the other archetypes. This will not PERFECTLY solve our spawning needs, but it’s a very strong default. We will discuss the problems this creates when we get to version 3.0.
As an example, your Melee archetype probably prefers to spawn in close proximity to the player, and also in close proximity to themselves, to heavies, and bosses. The Ranged archetype, on the other hand, prefer to spawn as FAR from the player, and also far from melee, heavy, and bosses.
We also define some very similar, but slightly different, global rules for Waves
Wave Spawning Rules
- Spawn on Camera
- Proximity to Player
- Proximity to Cast
Unit and Template Tables
Spending Points / Wave Construction
Fairly majorly, see above!
Problems with this design:
Phew! We’ve certainly added a lot of complexity to our wave creation, but it’s given us a TON of flexibility, and we’ve definitely added a plethora of dials that we can now tune for greater authorship control… but is it enough?
We still have a few major problems with this system. Here are some issues:
- Narrative Filtering: If story beats are made out of giant lists of templates… how do you make sure that the Cyclops doesn’t appear until the 5th level? Or how do you make sure that the Twig Blights ONLY appear in the 6th level, as that takes place in the giant world tree?
- Progression Filtering: How do we make sure that “harder enemies” don’t appear early in the game, especially if we were making a game like Path of Exile, where the player’s power curve grows over a VERY long time.
Beyond those two listen concerns above, we also have a huge elephant in the room that we are going to talk about first, and that is randomization! We are far too reliant, currently, on TRUE randomization. As in, the way it is purchasing units right now is purely random. It rolls and dice, and picks a unit, and that is…. Not what we want. In fact, that’s almost NEVER what you want, in ANY procedural system.
So, in version 3 we are going to finally tackle our issues with randomization, which will in turn add a bit more authorship control back into the system.
Lastly, I’d like to close with a little toy that will showcase the issues that arise from our reliance on randomization.
Extended Example Case
Below you will see the results of running the “Purchase units at random” process over the course of 100’s of simulations. These simulations are based upon the following parameters
We have the following units in a template, where the encounter is spending 200 points.
- Unit A – Value 5
- Unit B – Value 15
- Unit C – Value 50
Click on the lines “View Unit X” to see simulations with the different units. Notice how it is quite likely to have an encounter with only a single A Unit. Is that REALLY ever something we want? Click the “Unit B” several times, and notice the wide swing in results. Remember our goal! Are we actually generating, on average, “fun” combat encounters? In Version 3, we’ll talk about how we can deal with the problems of true randomization.
View Unit A!
View Unit B!
View Unit C!