Posted September 24, 2023 by BenjiSt
#devlog #forsakencolonylastsurvivors #progress #survivorslike #update #enemies #leveldesign
Welcome to another weekly devlog update of Forsaken Colony: Last Survivors! After playing with shader graphs and level blocking last week we now turn to some more exciting aspects of the game: enemies and stage progression! However, I have to dampen your enthusiasm a little because this devlog is going to focus mostly on scripting and mathematical implementations of these elements; This is to get a better understanding how all these systems work in symbiosis in the background of my game.
For the sake of making stats of different enemies and the player more comparable I decided to introduce a Stats-class which has derived classes for all types of objects with stats. There is a class PlayerStats for stats of the player and enemies, AttackStats and ProjectileAttackStats for attacks of the player (as ProjectileAttack itself is a derived class from AttackBaseClass it has some additional stats that can only be found in this type of attack). The Stats-class itself is derived from ScriptableObject which allows easy creation of Stats-assets for all characters in the game.
For the damage calculations of enemies and the player I added a static method to the PlayerStats class which takes two PlayerStats class instances as arguments (receiverStats and dealerStats) and calculates the damage dealt based on various stats; Stats effecting the calculation are physicalDamage, armorPenetrationFlat, armorPenetrationPercentage (and the same for magicDamage) of the dealerStats and armor/magicResistance of the receiverStats. The code snippet showing the static method can be seen in Figure 1, the corresponding function graph in Figure 2. There is another function which does essentially the same but takes an additional AttackStats argument and uses parameters from this class as well.
Figure 1: Code for DamageCalculation function
To generalise the way upgrades work and how they are applied to classes derived from the Stats class I decided to introduce a StatsIncrease class. This class stores values for absolute and relative increases of every stat in the Stats class. For instance, for physicalDamage in the PlayerStats class there are values flatPhysicalDamage and percPhysicalDamage in the PlayerStatsIncrease class; To apply these value changes to the individual Stats class instance I decided to override the plus operator for adding a StatsIncrease-object to a Stats-object. The formula how these values are generally applied is:
newStat = (oldStat + flatStatIncrease) * (1 + percStatIncrease)
(Note: some stats are integer values, for those only a flatStatIncrease exists)
Furthermore, I implemented the functionality to add two StatsIncrease objects. Therefore, the PlayerBaseClass only has to store one StatsIncrease object that is changed every time an upgrade is applied.
Upon levelling up there are always four different upgrade options to choose from (Figure 3). Currently, the upgrades come in three different rarities and are chosen randomly when reaching a new level. Every level the amount of experience required for the next level up increases. I first used a polynomial function to determine but the progression in the early stage of the game was very slow (amount of experience required increased too fast) and once you passed a certain time in the stage you got a new level up every other second. Therefore, I decided it to change it to an exponential function with 10% growth every level:
xpNextLevel = xpFirstLevel * (1.1 ^ currLevel)
There are currently two enemy types in the game which are ranged and melee enemies. Both of these use the move functionality from the tutorial and just directly move into the direction of the player. All enemies deal damage on contact (there is a small amount of invincibility time after the player is hit by an enemy to avoid being killed in a fraction of a second); Ranged enemies stop when the player is within 60% percent of their attack range and start a casting animation, which fires a projectile into the direction of the player. The first stage of the game currently features three different melee enemies (goblin, golem and bronze golem) and two different ranged enemies (green lizard and white lizard) which can be seen in Figure 4. All of them have different stats with enemies showing up later in the stage generally being stronger and having better stats. Figure 5 shows different enemies in a later part of the stage.
The general idea for spawning of enemies is that they should spawn increasingly fast throughout the stage and that there are different enemy types during different parts of the stage. Therefore, I implemented a serializable struct which allows the change of spawning parameters in the editor without having to change anything in a script.
The formula for calculating the current spawnrate consists of two parts: the first part features the exponential function which doubles the spawnrate after the specified time; The second part decreases the spawnrate depending on the number of enemies of the specified type currently alive. If this number equals the number of max enemies alive the spawnrate is decreased to 60% of what it would be; The complete formula is:
spawnrate = startingSpawnrate * (2^((timeSinceLevelLoad - startSpawnTime) / doubleSpawnrateTime) * (1 - 100^(currEnemiesAlive / maxEnemiesAlive) - 1.2)
The corresponding graphs for the two parts of the formula can be seen in Figure 7.
Main points of criticism this week were:
Planned improvements on existing features until the next update are: