Last week I entered PROCJAM 17, where the goal is to “make something that makes something”. It’s a very relaxed week where a lot of people develop games or systems capable of generating content procedurally.
For those interested, you can try out my entry here!
For this jam I wanted to return to a mechanic that I’ve explored a lot, but put a procedural spin on it.
- Completed procedural monster generation: All monsters shown in the game are generated and animated entirely with code.
- Completed monster merging mechanic: You can select two monsters from the menu and combine them to create a hybrid.
- Completed battle system: A turn based battle system where the enemies are generated randomly, with a chance of fighting multiple enemies at the same time. The game’s difficulty also increases with your winstreak, generating tougher monsters.
Procedural Monster Generation
- Select a species from data
- Generate a mesh using the species parameters
- A mesh is generated per body part
- Generate a skeleton using the species parameters
- Apply a bone weighting algorithm per body part
Select a Species from Data
The base data that the generator selects from is a species pool. The game has 9 unique “species” including creatures like “Dog”, “Dragon” and “Spider” among others.
A species is a collection of ranges of values for properties like limb length, thickness, curves to determine limb shape, as well as how many of each limb to generate.
The generator takes a species, and from this it can build up a list of body parts. With this the mesh and skeleton can now be generated.
Generate a Mesh using the Species parameters
With a species selected, body parts are generated using the species constraints. The body part data gives some key information for the mesh generation.
To generate a body part, I essentially sample an elliptic cylinder that has had its radii altered by AnimationCurves. I sample based on a NumSections and LOD property. The NumSections property tells me how many vertices rings I will get from the shape, while LOD tells me how many vertices there are per ring, joining the vertices up into triangles is then just a matter of mathematics.
This video shows how the AnimationCurves are used to alter the shape that’s formed for each body part.
To generate the skeleton, I just select the origin and end point of a body part, the challenge wasn’t so much generating the skeleton as it was applying the skeleton to the mesh.
Apply a Bone weighting algorithm per Body Part
Bone weighting was tricky at first, there are some excellent gamedev articles on bone weighting algorithms. In the end, I combined the naive approach of distance weighting with the fact that I already know which vertices belong to which body part, so I can perform the bone weighting per part instead of across the whole mesh like some of the algorithms I found.
The distance algorithm typically goes like this:
- For each vertex:
- Compare the distance between the vertex and each bone
- Order the bones by distance
- Select the 2-3 closest bones, these are the candidate bones
- Assign their weights using a fraction determined using their distance in relation to the other candidate bones
The problem with this normally, is that you end up applying weights for vertices to bones that aren’t directly connected to them (e.g. Part of an arm gets weighted to a bone on the torso.) Since my mesh and skeleton is generated per body part, I automatically know which bones are relevant to which vertices, and can immediately ignore the bulk of the bones for any given vertex.
Here are two demonstrations, one shows the naive bone weighting, while the other shows the same approach but with the limited bone assumption applied:
Monster Merging Mechanic
Having produced a monster generator that is completely data-driven, creating the merging mechanic was just a matter of mixing the data.
Applying this mechanic to the generation process also allows far more interesting looking creatures to be produced, because the mixing stage will also mix values like limb orientation, creating bone structures that weren’t possible from the base species list alone.
Here is a demonstration of the merging in action: On the left are the two inputs, and on the right is the resulting merged creature.
Having spent the first 7 days on the procedural portion of the game, I was left with the weekend to put together a turn-based battle system. Much like the procedural monsters, this was something I haven’t attempted before, but I did have a few ideas about how to achieve certain aspects.
Ultimately the battle system had the following:
- Speed-based turns
- Attack/defense-based damage calculation
- Animated attacks with events at key moments
- Dynamic difficulty scaling
- Multiple opponents
To start with, I created a BattleCharacter object and a BattleContext object. The BattleCharacter object houses data about a character relevant to the fight, while the BattleContext object contains all the BattleCharacters, keeps track of who belongs to what team and who is actively in the battlefield. If any of my other systems want information about a BattleCharacter, they get it by querying the BattleContext.
This was the first aspect I thought about. The main inspiration for this whole project was Jade Cocoon, and in that game it was possible for characters to take a second turn under some circumstance.
Another case study I used for this was Final Fantasy X, in which speed played a big role in determining the order of play, going so far as to offer a timeline of future turns on the side, the best way to see the impact of speed is to target somebody with ‘Haste’ and observe how their name suddenly appears on the timeline a lot more.
To achieve this, I created the TurnTicker object. Imagine that the characters are in some sort of race, the track length is equal to the sum of their speed values, the following rules apply:
- In one tick, a character runs a distance equal to its speed.
- When a character reaches or crosses the finish line, it is their turn.
- When a character passes the finish line, move them backward by a distance equal to the track length, resume the race.
With this ruleset, characters trigger their turn when they reach the finish line, faster characters reach the finish line first, characters that are exceptionally faster than their opponent will often get two or more consecutive turns before their opponent can get an attack in.
I described this as a race, and talked about moving characters some distance over time. If you haven’t noticed yet, this means I can determine who goes first by solving a set of linear equations. We have:
speed*numTicks >= speedSum
Meaning that numTicks is calculated like so:
numTicks >= ceil(speedSum/speed)
I round up so that I only get the tick count for ‘crossing the line’ since that is when we guarantee that it is their turn.
Some more rules:
- If two characters cross the finish line at the same time, the faster character goes first.
- If two characters cross the finish line at the same time, the slower character is added to a queue.
- When querying the TurnTicker for the next character’s turn, the TurnTicker will check and exhaust the queue before calculating any further turns.
Doing the turns in this way also means that I can recreate the FFX-style timeline and show the future lineup of turns at any given time, this was something I didn’t get to show in-game on release but it’s trivial to add at this stage.
Attack/Defense-based damage calculation
I didn’t do anything particularly innovative here, as before I observed how other turn-based battle games such as Pokemon, Jade Cocoon and Final Fantasy do their damage.
In its primitive jam state, my game contains stats for Attack, Defense and Speed. However the goal would have been to include elemental damage and a difference between physical and ‘special’ attacks, as well as modifiers and status effects.
These stats are contained in a BattleStats object, which belongs to the BattleCharacter in a couple of forms: base stats, and buff stats. Base stats are the character’s actual stats in and out of combat, while the buff stats are temporary figures applied through attacking or powers (which I also wasn’t able to implement in time).
I created an AttackList for the game, which contained a list of AttackData objects. AttackData contained stats to be added to the attacker’s damage stats, as well as stats to be “inflicted” upon the enemy (added to their buff stats).
If I come back to implementing those, I will include the details here.
Animated attacks with events at key moments
While this system was a result of my lackluster animation scripts, it has some key functionality that make it very useful when I come to redo those.
I returned to the AttackData object to add an AttackSequence object, this is a data-driven object that provides AnimationCurves for the character’s height in the arena, as well as an interpolation value that moves the character between their ‘home spot’ and their target.
The AttackSequence object also contains a map of timestamps to strings. When one of these timestamps is reached, an event is fired off with the corresponding string as a parameter. This allows me to display damage values on-screen at exactly the moment of impact.
When I come to implement an improved animation system for the characters themselves, this will also allow me to message the characters to tell them which animation they should play at important timestamps (e.g. jump, get hit, dodge, etc)
Dynamic difficulty scaling
This was very rudimentary, but it was a good basis for keeping the game interesting when I didn’t have time to add things like an overworld or story to provide more wild encounters.
The game has a difficulty setting, it starts at 0, increases by 1 with every win and falls by 1 with every loss. The difficulty value affects 3 things:
- The likelihood of encountering more than 1 enemy in the same fight.
- The base stats of an enemy.
- How many times the enemy gets merged before being added to the fight.
I said before that merging is a mixture of data values, this includes the BattleStats data. The player is encouraged to merge their captures because it maximizes their combat stats, they are also encouraged to do this because the difficulty scale increasing the enemy merge count means that the enemies get strong quite quickly.
The player is given an advantage at the start by being given a ‘starter minion’ that has been merged 3 times, making it significantly stronger than the first encounter. If a player somehow loses their first encounter (and therefore loses their only minion), they are given another, much stronger minion that has been merged 6 times.
As I said, this was very rudimentary. There are obvious balance issues to be considered, such as the player losing on purpose to deliberately get a much stronger monster. It is all stuff I would like to return to now that I’m not feeling so constrained by the jam.
The TurnTicker determines whose turn it is, it does this regardless of which side a character is on. Thanks to that, no special behaviour was required for adding extra characters into the mix, I can probably allow for the player to bring multiple characters in as well, I just didn’t sort out a good UI for that in time (some might argue that I didn’t sort out a good UI at all!)
- Add more species
- Balance the stats
- Implement elemental damage and status effects
- Rework monster animations – provide a system that creates sensible animations based on type of body part and context (which other body parts are on the same creature), encapsulate all of this in an animation class for the creature that switches between idle, attack, dodge, death, etc, poses when appropriate.
- Improve UI, merging and main navigation preferably done with mouse instead.
- Music and SFX
Thanks for reading!