Monster Fite! It's been just over two weeks since I last did anything with it and (according to my first tumblr post about it) around three months since I first wanted to make it. Time to write up about it then! Just like with NTGIM, this first post'll focus on the coding and technical side of things and then the second'll look more at the visual design.
From the start I had a clear idea of what I wanted to achieve with this project - I wanted to make a battle engine similar to the one used in Monster Rancher 2, a game for the Playstation that I played a ton as a kid. It's always been a favourite game of mine and it features a great battle system that I haven't seen used in anything else. MR2's battles weren't turn based but timed matches of 60 seconds with a raising "guts" value that was spent on using attacks. The attacks you could use depended on the distance between your monster and the other, adding a whole new dimension to battles in the sense that your monster's position controlled the flow of battle - you could move your monster to a range where the enemy had weaker or no attacks to be safer or risk going into a range where they had powerful moves for a chance of pulling off one of your own. Even now, years after first learning about this battle system, I still find that MR2 matches are tense and fast-paced to play. They're always a challenge! With this in mind I started planning how I'd go about emulating it myself and maybe improving certain elements - here's an early sketch of how I pictured a battle scene looking:
I wanted this game to be fully 3D which meant I needed to learn how to make and import animated models from Maya into Unity. I'll cover the process of building the models in the second post - right now I'll talk about getting them to work. I'd make all of the model's animations in a continuous stream in a single file, making notes of the frames where separate animations began and ended. I'd then save this animated model as a .mb (a Maya file that can be read by Unity) and import it in. In the model's rig settings I'd make sure the animation type was set to "Legacy" and generation set to "Store in Root (New)". Then I'd tick "Import Animations" and "Bake Animations" in the animation settings and mark in the times for the separate animations, or "Clips" as Unity calls them. I'd also change the individual clip's Wrap Mode depending on if I wanted it to be an animation that looped or played once. Here's what the list of clips for Sweetle looked like in the animation settings for their model:
LOADING IN A MONSTER
Even though Monster Fite sees two of the same monsters fighting in game they are not pre-loaded into the scene before a game starts. Their model is loaded in depending on the variables P1MonsterSelection and P2MonsterSelection inside of a script named FightPlayers. Because I didn't fully implement other monsters these variables are set to load in Sweetle by default. FightPlayers loads in the "selected" monster's model and also generates a custom collision box around it to prevent players walking through each other during a battle. These models then replace empty placeholder gameobjects that previously stood in for the player's (currently unselected) monsters. The innermost faces of the generated colliders have their positions recorded here for use in another script, FightCameraControl, as can be seen in this gif when the innermost edge's positions are marked with cubes:
THE BATTLE CAMERA
I wanted the camera in Monster Fite to always show both players during a battle while keeping them centered on-screen. Using the positions recorded in FightPlayers, FightCameraControl draws a line between the two in order to calculate the midpoint. The camera then uses the midpoint's x position as it's own as to always be focused on the center. In order to keep both players on screen, the camera also moves along another line in the z position between two points previously marked in the scene. The larger the distance between the two players the further back the camera moves along this line, as can be seen in this gif:
THE BATTLE MENU
Like the models, the menu displayed in game isn't pre-written before the game starts and is actually scripted to show the specific stats and attacks of the selected monster (even though, as previously mentioned, this monster is always Sweetle by default). This information is gathered from a script named MonsterInfo which stores each monster's specific battle data. This includes arrays of their stats, attacks names, physical/nonphysical base attack powers and hit chance/crit chance/cost/drain values for each attack. Here's a code snippet showing how Sweetle's info was recorded:
Depending on which monster the player chose, that monster's info would then populate arrays that would be used in later code for everything relevant to that player. The information in the code snippet would not be used directly, rather the copy of it assigned to the player who chose the monster would be. I wrote it like this with the aim to implement a way for a player to select new monsters without having to alter code elsewhere - rather I could refer to the re-wrote player info arrays that held the same name throughout. For example, the value used for displaying the selected monster's health and name on the battle menu is taken from these player specific arrays and not the sweetle specific ones.
Another script named FightMenu deals with the battle menu itself after the monster has been selected and a battle begins. When it comes to filling in the battle menu with the player's monster's specific attack list it goes through an array of text meshes and writes in the attack names in order from closest range to furthest.
To prevent "empty" attacks being selected from the menu (attacks that have no recorded name to fill a space with) the script checks to see if it has a cost and, if it doesn't, it recognises this as being a slot with no attack and prevents it from being selected, as can be seen in this gif:
Before implementing attacking I knew I wanted it to work something like this:
First I needed to write in a way that let the game know what attack ranges the monsters were in at any given time. This is done by splitting up a set "maximum distance" possible between both players into four segments - close range, mid range, long range and out of range. Close range is a distance equal to or smaller than 33.3% of the set max distance, mid range is 33.3%-66.6%, long range is 66.6%-100% and out of range is anything higher than the set value. This out of range segment allowed for players to try to avoid attacks all together by walking back far enough, just as you can in MR2. This was all calculated in a script I named FightDistance which included bool variables named after the four ranges that became true when the monsters were in that particular range. These ranges were then used in the FightMenu script to make attacks of that range selectable and usable (after a check was made to see if the player had enough stamina to use that specific attack).
If an attack begins then the player that is attacking is noted to be used in preventative measures against the same player trying to attack again during this time, the other player trying to attack, either player being able to move through button input and freezing the increasing stamina bars of both players. Another script named FightAttackAnims deals with everything involving both player's movements and animations after this point and, when relevant during the particular attack, it activates another script's processes, FightDamage, to calculate everything to do with the attack's damage.
I wrote FightAttackAnims in a modular way with the aim to reuse chunks across multiple animations for multiple monsters, sewing repeated and shared actions together in sequences rather than writing new segments for every single attack for each monster. First the script would record who was the attacking player and who was defending and various variables about each one like their model, starting positions, the active attack of the attacker, whether or not the defender was blocking, etc. The name of the active attack was used to select the code chunk that would play out. Here're the coroutines for two of Sweetle's attacks, Jump Uppercut (a close range attack) and Horn Charge (mid range):
I wanted these to be as readable as possible from a surface level to make visualising these animation sequences easier. Jump Uppercut starts by calling another coroutine named "R_JumpAndMoveToEnemy". This is a good example of a standard chunk of code I wanted to be reused across the board (the R stands for "reusable"). In this chunk, the attacking player is moved to a point just in front of the enemy while playing the appropriate animations that would be named the same for all monsters (jumpAnim, walkAnim, etc). The first coroutine (JumpUppercut) would wait until the second (R_JumpAndMoveToEnemy) was complete before returning to an idle state, waiting 0.4 seconds and then starting another coroutine named "PhysicalHit2" - the attack sequence that would match with Jump Uppercut. After this is finished, the attacking player waits 0.2 seconds while FightDamage updates the health change of the defending player while also triggering another coroutine that would deal with the appropriate reaction animation sequence for the defending player. The attacker then continues to idle for a further 0.5 seconds while this update is displayed on screen, allowing a small pause for players to register the outcome of the attack. Finally, the attacking player moves back to their starting position by means of R_JumpBackFromEnemy, another reusable chunk.
R_EnemyReaction is a coroutine called in every attack and it determines the animation played by the defending monster after FightDamage has calculated if the attack has missed, hit, crit hit or been blocked. FightDamage does this by first checking to see if the attack has even landed. A random number is picked between 1-100 and if it's smaller than the hit chance of the used attack then the attack is recorded as a success, otherwise it misses and no further calculations are made. If it does hit, another random number is picked between 1-100 to determine if the hit is a critical one or not - if the number is lower than the attack's crit chance then it's critical, otherwise it's a normal hit. The damage attack formula worked like this:
Attack Damage = (((Debuffed Attacker Strength / Enemy Defense) * Attack's Base Power) * Crit Value) * Random Bonus
The Debuffed Attacker Strength would depend on if the active attack was a physical or a non-physical one, even though this wasn't fully utilised in the version of Monster Fite released (though fully functionable). Physical attacks used the attacker's strength stat where as non-physical ones used their mind stat. If it was a physical attack then Debuffed Attacker Strength would be the attacker's strength stat minus five percent of the enemy's own strength stat. A non-physical attack would see this value being the attacker's mind stat minus five percent of the enemy's own mind stat. This meant that monsters with high strength stats would be more resistance to physical attacks and those with high mind more resistant to nonphysical ones. Debuffed Attacker Strength is then divided by the enemy's defense stat, with this new value being multiplied by the active attack's base power. If an attack is critical, Crit Value is a random number between 1.1 - 1.5, else it's 0. Random Bonus is a value between 1.1 - 1.5 also, to add variation to attacks.
After the attack damage has been calculated it's converted from a float to an int and is taken away from the defender's current health. Another calculation is then made if the attack is a draining one (an attack that also reduces the enemy's stamina as well as health) with the formula for this being:
Stamina Drain = Current Attack's Base Stamina Drain + Drain Variation
If a defending player is blocking when an attack starts then they continue to block throughout the attack. Blocking raises the defense stat of the blocker by 25% and drains stamina while active. This was a feature not included in MR2 but one that I felt would add an interesting defensive element of play.
Monster Fite has three main states - before a battle, during a battle and after a battle. Before a battle the player is presented with what was planned to be a monster selection screen but, due to the other monsters not being implemented fully, it ended up as more of an initial starting screen that displayed Sweetle's stats. Again these stats were not pre-set before a game started, rather they are taken from sweetle's arrays in MonsterInfo. If this had been finished then arrows would have appeared that allowed for the players to click through all the selectable monsters with their card image and stats changing to suit.
When a battle starts there's a 3 second countdown before players can move, then the 60 second countdown starts. A battle ends when this timer reaches zero or if either player's health reaches 0 (resulting in a KO). If either player is currently attacking when the countdown runs out then the game waits for the attack to finish completely before ending the match to prevent sudden cut offs of animation or damage calculation.
When a match ends the camera switches to one that circles around the arena (referred to as circle cam multiple times throughout the code) and the two monster's remaining health percentages are compared. If a player wins (higher health percentage) their winning animation plays and the other player's losing animation plays. A tie (both health values are the same percent) results in both players having their losing animations play. In the case of a KO'd player, their KO animation continues to play, as'd be expected. The game can then be started over again from the state before a battle.
And that just about does it for this post! I'm pretty sure I've covered all the major aspects of the code for Monster Fite but if there's anything else you'd like to know the workings behind then feel free to comment on patreon, ask on tumblr or tweet @pixelatedcrown. There are a few things I haven't gone into detail about since I figured they'd either be obvious or boring or just not that relevant to anyone wanting to know more about the workings of a battle system so lemme know if you think there's something I could've explained better or covered a bit more. Otherwise, thank you again for continuing to support me on Patreon!! I'll be posting the second part of the making of Monster Fite before the month is out!