

About the game
Into the Soundscape is a Rythm Roguelite game where you step into Clio's shoes, armed only with few weapons, to adventure deeper and deeper in the wood in order to find the cure for your brother's deaseas. Fail and repeat, while syncronize your attacks with the pulse of nature, explore and find different items to create your personal build.
The game feature procedural room generations, fast-paced combat, incentives to attack on beat, permanent death and great replayablitity
Specs
-
Engine: Unreal Engine 5
-
Programming language: C++
-
Year: 2024
-
Development time: 6 months
-
Team: 15 members (4 designers, 3 concept artist, 4 artist 3D, 4 programmers)
-
Type: Accademy project
-
Role: Programmer
![]() | ![]() | ![]() | ![]() |
---|---|---|---|
![]() | ![]() | ![]() | ![]() |
![]() |
Game Structure
The game is a Rhythm Rogue-lite game.
You start the game in a hub room, where you can unlock permanent upgrades in a dedicated shop. After you proceed through the door, you start playing in a randomly generated rooms of different types: a classic combat room, an elite combat room where the goal is to survive a certain time period in a hard dangerous situation, a shop room, a healing room, a free power room, a deck changing room where you can change the next possible rooms to face, and obviously a final boss room.
A deck defines which room type you can deal with in a run, and after completing a room, when you decide to step forward, you can choose the next room type. There are 2 worlds that represent different forest states, with the second being harder and characterized by a more aggressive mood. At the end of each world, there is a boss fight, but only for the second time is there a real boss.
During the game an enthralling music incentivize to attack on beat, this cause more dame and range of the two weapons available: a sword for melee combat and a pistol for range combat. Other than that a lot of power up can be found by completing rooms, in the shop or in the dedicated room, that help you create your unique run and apply interesting effect by playing on beat.
If you die, you lose all the progress except for the permanent bonus and currency, but after completing a world, you restart from the next one.
What I have done
In this project, I can ensure I put my hands over a lot of code unrelated from my tasks to help and fix bug like: player attacks, subsystem for sounds to play and volume changing settings, saving system, interactables, beat actors.
To stay on what I have done on my own, I implement:
-
enemies and boss (AI)
-
base structure of power ups with the majority of them and the UI related part
-
base structure of permanent bonus (upgrades) with a few of them
-
items for buff stats
-
status
-
base set up of music and quartz to make world and entities act on time and beat
-
weapons
-
shops for power ups and items
-
stats
Quartz and Music Sync


The idea we came up with is to have an array of beat actors, actors that have to execute some actions on the beat, and to cycle through this array and call the respective beat function on the event fire on the beat of the clock. So, each actor that needs to execute some logic at the beat basically needs to add itself to this array at begin play. For this reason, we created a beat actor class with this simple logic, and every time we needed an actor to have functionality on beat, we simply derived from this class.

With the development of the project, the core idea remains, but several adjustments were made to consider layers in the music. This was made by exploring Metasound in order to have a base music layer, where it can be added a combat layer of music and a danger layer of music.
After some initial research on how to manage music in our game, we decided to use Quartz since it offers the possibility to subscribe to music events and use a separate thread to manage music, and it's ideal for our purpose. I first came out with a blueprint implementation following the documentation, and after some time I managed to convert the code into C++.
I set up the clock on the GameMode begin play with a fixed beat per minutes (100) and subscribed to a general beat event on the beat of the current play quantized song.
Weapons

For the hitting logic, I created an enemies array that is filled with overlapping enemies from one of these hit boxes. I have to check to add only new enemies and not repeat already added. In this way, after attacking and adding enemies, I apply damage to them and then reset the array at the end of the attack.

Another important logic for our game is the idea of attack on or out beat: if an attack is made out beat, it has less damage and range. In the case of the sword, an attack out of beat causes it to have half damage and not activate the extra hit box range.
The RangeWeapon logic is to spawn the projectiles. It has a public method called by the player that passes as parameters all the information, and then internally in the class it calls a method to effectively spawn the projectile. This is because of the combo systems and the different types of attacks based on an index we called attackcounter. This value can go from 1 to 3, and when using a range attack, this will identify the number of projectiles to spawn. Other than this, we also have each attack differing from the other: with 1 bullet, this goes forward from the player, with 2 they both go forward from the player, but the second spawns a little after the first one, and with 3 the shoot is composed by a folding fan of bullets in a forward direction with a small angle between them.
For the projectiles, I made a base class calls ProjectileBase. This has only few simple logics: check the overlap with a target to apply damage and destroy the projectile, check the range of the projectile in order to destroy it if the distance between spawn position and the actor exceed the range value and delay destruction of the projectile using a timer of few decimals to visualize better the projectile hit.
A subclass was made for enemy and player projectiles. ProjectilePlayer adds the check for enemy targets, the extra range of the projectile (similar to melee but in this case influences the distance the projectile can move) and the check for the projectile being on beat or out beat. This last part influences both damage and range, causing it to be half if out beat. ProjectileEnemy adds the same logic expect for beat check, and checks for player as target. There were made other 3 enemies projectiles subclasses: one to apply status effects, one to create an explosion at contact and one with a specific logic for a particular enemy type, the mortar. (for this see enemy section)
For the weapons, I created a base class named BaseWeapon with generic components and variables like scenecomponent, meshcomponent and niagaracomponent, basedamage, damage and extrarange. From that, I derived the two types of weapons we needed: MeleeWeapon and RangeWeapon. There is also an upper base class called NoteDamageCauser since, in the beginning, the weapons can apply a note when hitting and damaging an enemy. This was a removed feature during the course of the development.
The MeleeWeapon has only one particularity: the extra range and hit box of the sword. Except for visual effects, this weapon has a standard hit box as big as the blade of the mesh and an extra hit box immediately above the first one that is bigger as a percentage of the standard one. In this way, not only is the base range of hits of the sword is bigger, but this can be increase through power ups.
Statistics
The stats for this game are very simple, there are 6: maxhealth, attack, crit change, crit damage, speed, and weapon range. They are implemented simply as a public variable of type TMap with as key the enum value of the statistic and value the float corresponding value. In this way, it can be easily changed by other logic updating the value of each stat.
Items Stat Buff

These items are part of a general set of items that are the items found as reward for clearing a room. Furthermore, they also derived from base classes with logic for being buy in the shop.
The logic of these actors is very simple: they are derived from the base interacting class, so they have an overlapping sphere to check the player to pick up this buff. When the item is picked up, it shows an effect and plays a sound, while it calls a player method to increase a statistic of a certain value and processes some stat cases to update UI or other related characteristics.
Upgrade
The upgrades are a permanent buff that can be unlocked using music sheet currency, this is also permanent and kept between runs. For this scope, I implemented an UpgradeManager class (a simple UObject) that contains an array of upgrades. A reference to this object is in the GameInstance to be unmodified even after changing map. The single upgrade was implemented with a base class UpgradeBase containing a struct with all information for the upgrade to be saved (like a bool for being activated on start or some enum for the type and the world level) and a virtual method to call the upgrade activation when necessary. Most of the upgrades need to be activated at the start of the game, for this reason, there is a method on the UpgradeManager to loop through all the upgrades and call activation if needed.
I implemented 3 upgrades related to shops and enemies:
-
StartingRoomPowerUpShop: this activates when entering the starting room and spawns the power up shop
-
StartingRoomBuffItem: this activates when entering the starting room and spawns the items shop
-
EnemyDropHealItem: this activates when a enemy was killed and with a certain percentage spawn a heal vial item
Power Ups



To clarify a bit more, the PowerUpManager was derived from ActorComponent, so it's attached to the player. On the other side, the PowerUpBase class derived from other classes for shop and interaction, but basically an actor, and the PowerUpEffectBase derived from UObject. In this way, every power-up needs to only implement (code speaking) the subclass for the effect. In the blueprint, however, it's necessary to create a BP for the power-up (to have the correct information and visualization) and the corresponding effect to link to the power-up.
The PowerUpManager has only a few methods: one to add a new power-up pass as a parameter, one to try to use an effect of some power up, and one to clear all current active effects (this last is due to saving reasons). The first method is called directly from the power up: after interacting with it, passing itself as a parameter, the manager adds a new entry in the map with the correct information about the power up and creates the corresponding effect.

The second method makes a few checks to be sure the power-up is present in the map and the effect is valid, returning this last value.
The clear method instead loops through the map to call the reset on each effect.
The PowerUpBase class is not interesting since it has only a few simple methods for interaction, in which spawn and despawn the corresponding UI to show all power-up info, adding a power up after interacting and other few utilities methods.
The PowerUpEffectBase class has a main method called Effect and a lot of overloading of this method to match all the cases needed for the power-up effect information to be processed from it. In addition, it has a method to reset the effect (mention early) and activate the effect at the start (for the effects that need to be called each time the player spawns).

So the main idea is that through the codebase, when you need to call a power-up effect, you just need to get a player reference and call the TryUseEffect of the PowerUpManager of that specific power-up type, and if that is valid, then call the effect with the parameter needed.
Here is an example of the power-up call for the spawn of an explosion as an enemy dies:

To conclude, beyond all this logic, I implemented the majority of the power up effects of this game, since a lot of that was related to enemies or weapons.
The power-ups are bonuses that can be found after clearing a room, by buying them in a shop or in the specific power-up room. They give a variety types of bonuses: from more attacks, applying status, healing, generating more bullets, spawning thunder, generating areas of damage or status, adding a shield and a lot more. In this case, I implemented a PowerUpManager class that keeps track of which current power ups the player has. This was made with a map that contains as a key the value of an enum of power-up type (one for each power-up) and as a value a struct that contains the power up information (name, description, icon and possible extrainfo) and the power up effect, implement with a specific class.
Shops

The PowerUpSpawner class is similar, but it has 3 different pools and a pool with all the power-ups. So in this case, there is an extra struct to associate a spawner with the preferred number of pools, and extra support variables like a map to link a pool type with the respective pool values.
The method is overridden similarly: first check for get the player, then create an array of power-ups for the first spawner by adding all the power-ups selected from the linked pools, search the first power-up randomly that the player has not equipped, and spawn it at the first spawn point. The process is then repeated for the other two remaining spawn points, and finally, each of the elements is checked to be free or to be bought by calling the logic in WorldShoppable.

The shops I implemented are 2 and are both 3D: one for power-ups and one for item boosts. The logic is used for the shops in the dedicated room, the shops in the starting room, and also for the power-up spawn of the reward for completing a room or for the dedicated room.
I started by creating a ShopSpawnerBase class (derived from Actor) that has 3 SceneComponents representing the spawn points of the shop. This class has a virtual method to spawn the elements to be sold, called at begin play. It also has a bool to decide if it is used as a shop or a free pickup and a value to decide which currency is needed (2 available: normal coins or music sheets; the second is permanent).
So I implemented 2 derived classes: ShopItemSpawner and PowerUpSpawner. The last one is used both for shop and for free pick-up. The logic is similar: there is one or more pools of objects in which each spawner can choose to pick an element, and then this is spawned at the current spawn point selected. This process is repeated for each spawn point.
The ShopItemSpawner class has only a single pool with all the items, so the method to spawn is override: copy the pool array of items, select the first one randomly, spawn at the first spawn point, and then remove from the array. If the element is part of a shop, then call the logic in a class called WorldShoppable (part of the hierarchy of both items and power-ups) to set up visual 3D information like the price and the info text, and set up the interaction callback to check for player currency before adding it to the player. The process is then repeated for the two remaining spawn points.
Status

After this operation, then simply call the status activation effect method ApplyEffect, overridden by each subclass with the corresponding attribute modification.
There are 3 status in the game:
-
poison: inflict damage over a small period of time
-
weakness: reduce attack damage for a small period of time
-
slowdown: reduce speed for a small period of time
All these statuses can be applied both to the player and enemies.
The implementation consists of a base class, StatusEffect, that is derived from ActorComponent, and a subclass for each status. The idea is that every time you need to apply status to some actor, you simply add the corresponding component that automatically calls the activation effect at begin play.
To manage status for both player and enemies, I unfortunately needed to implement a component called AttributesManager. This is responsible for updating the variables for damage, speed and health based on the actor's status. So for the player, I modified the stats and the CharacterMovement speed, while for the enemy, I modified the corresponding variables for health and damage, as well as the same the CharacterMovement speed. This logic was not very optimal, and one possible improvement is to create the same component to manage both player and enemy attributes and to directly interact with it for the status effects.
The StatusEffect base class contains the variables for status information: the enum type, the effecvalue to apply, the duration and other auxiliary. It has two virtual methods, ApplyEffect and RestoreEffect to activate the effect and reset it, and a general SetUpStatus method to activate the status at begin play. This method, after checking for the AttributeManager, gets all the status components on that actor, checks if there is someone of the same type, then gets the remaining time, removes the old and starts the effect of the new one with the sum of the remaining time and the fresh complete time. On the other hand, if no status of the same type is present, then simply add the effect of the new one. So the status effects can concatenate with other effects, but the same effect type is not stuck in value but only in time duration.
Enemies / AI

The base class is Enemy derived from character to have accessed to multiple features already available. I'm not considering all effect/sound/animation/UI parts in this section description. So the logic present in this base class is for health, taking damage, dying, stopping, and all the references needed, like the player, the AttributeManager component and the room.
Then the first derived class is EnemyPrimite, which adds a series of variables from the state of the enemy, to the value for damage and drop, to the references like the class of entity to drop, the data table with all parameters for enemy values based on a tier, and the BeatActorComponent fundamental for the music connection. At this class level, there is the virtual method EnemyBeat that calls each beat in the music processed by quartz, as well as all the variables needed to manage the beat duration of each state of this custom StateMachine. With the BeatActorComponent not only are we able to link an event callback each time a beat happens, but we also save the beat duration needed to speed up or dawn the animations. Another important logic that starts from here is the AI activation and deactivation needed to start or stop enemies from their behavior: when entering a new room, the enemies just wander around (with a few codes on top the core logic), and after the player enters their activation range (a basic sphere that checks for overlapping), the enemy core AI logic starts to activate.
In the EnemyPrimite there are also the features for stun, stagger and freeze (added later during dev). The stun and the stagger have the same activation, a melee attack hit, but different effects: the stun disables the movement for a short time period, and the stagger increments the time to reload, adding one beat to wait. The freeze, instead, is a special state that an enemy enters when its HP falls to 0 and the current room is marked as elite. The elite rooms are special combat rooms where there are more enemies than usual, but you just need to survive for a time period to complete and clear the room. So in this room, when an enemy should be killed, instead it is frozen: except for a dedicated VFX, the enemy is stopped, and the AI is disabled while it cannot take any damage. So it waits for a time period to then come back to health and restart the AI.
The EnemyBase is the next derived class that adds some variables for the acceptanceradius and range, but more importantly, it implements the core AI logic of the state machine. Here the method EnemyBeat makes a switch to understand the current state, then each state basically has a condition to check the transition to the next state, while it also triggers virtual methods like ChargeAttack or Attack to be implemented by the future classes. The other simple logic is at begin play to check if AI is active to start a timer to move to player thanks NavMesh or otherwise to make it wander, implements as moving in random direction every tot seconds.

A separate but important logic is the EnemyEliteComponent (an ActorComponent). This logic was unfortunately changed over the course of development, so the important part is taking damage. The idea is that some enemies are stronger than others, so each enemy blueprint has a copy of the EnemyEliteComponent. These enemies have more HP, damage, a dedicated VFX to identify them, and a unique logic for taking damage: this enemy takes only a small percentage of the total damage until it is hit by a perfect combo that breaks the elite shield. A perfect combo is a combo of three attacks, each of which is on beat. After hitting this enemy with a perfect combo the shield breaks, and it starts taking damage as normal.
The EnemyBase at the begin play checks if the EnemyEliteComponent is present, and in this case, it simply clears the OnTakeAnyDamage delegate to then binding the method on the elite component.

From the EnemyBase I derived two classes: EnemyMelee and EnemyRanged. These are the base classes for these two types of enemies, and from those, I derived new classes If a new enemy needed something more.
The EnemyMelee works with a small range, so it needs to be very close to the player to start attacking. The attack part was made with a static mesh component invisible to the player that works as hit box, activating overlap in the attack state and deactivating in the reload state. If there is overlap, the player then receives the damage. This simple logic was reused among several enemies and attacks. For example, the Golem enemy differs in this part from the standard enemy by having a bigger hit box but a slower preview. A derived class from melee is EnemyMeleeMultiAttack, which is the stronger version of the Golem, with the improvement that it hits not only in front of it but also laterally and backward with one beat waiting for these attacks. The last melee class is EnemyDash which simply has the attack mesh around him and moves to the player in the attack state, like dashing. The interesting part of this is that the dash is implemented with two timelines: one for the arrow preview and one for the moving part.
The EnemyRanged on the other side has a big accepting radius to stop at a certain range from the player, then make a check (with a simple sphere trace as big as the projectile hit box) if it sees the player, or better if the line of sight from the enemy and the player is free from obstacles, then in this case shoot a projectile. On top of that, this enemy has few modifications on the movement part, since it was more complex than expected. When the enemy is moving, it tries to reach the player, but if it is too close, then it needs to move in the opposite direction to get a safe distance to shoot. This can cause problems when it reaches the end of a map, like a wall or a big obstacle, so in this situation, it simply finds a random point sufficiently distant from the current position and moves in that direction. After reaching that point, restart chasing the player. Unfortunately, the enemy gets stuck near obstacles or in some of these processes, so I create another on-top logic to check if it is after too much time not reaching the target and to try unstuck with some lateral movement and then restart base logic. The other ranged enemy classes are EnemyRangedTeleport and EnemyRangedMortar: both add an on-top logic to the EnemyBeat methods in order to perform different actions. The EnemyRangedTeleport waits a random number of beats in range before teleporting to a random NavMesh location a certain distance from it. The other particularity is the spawn of different projectiles based on the enemy alternative version to apply different statuses or create a small explosion on contact. The EnemyRangedMortar instead checks when it's the attack state to perform some shots: spawn another different projectile type that starts going to the sky and then falls into the player, causing a little explosion.


Coming to enemy specials, from design, they should be considered all enemies that have a different behavior from the core logic, so in our case both the boss and the jumping enemy, but from my perspective, I want it to differ the boss that derived from the base Enemy class in order to pass power up/status and other checks, while the special one shares more logic with the standard ones like being stun, stagger and freeze. So for this reason, the EnemySpecialJump derived from EnemyPrimitive. In fact, it also uses the EnemyBeat method and standard states to maintain continuity, but it acts differently. Foremost, the enemy does not move, but in the moving state, choose a direction (the 4 bases from him) in which to jump. Then pass into the charge state, where it shows the arrow preview, which indicates where it is going to jump. After a few beats, in the jump state, simply jump in that direction and spawn a paddle under him. When it lands, it activates a hit box to apply damage. On the other hand, the puddle spawns above him, it is a circular region that, if overlapped by the player, causes the status to be added. For this reason, there are 3 variants of these enemies, one for each status. Finally, in the reload state, act as normally, simply await to repeat the process. The interesting part is that the jump was made with a timeline and a float curve with some editable parameters like height and distance to give the designer the best flexibility.
Boss
There is only one boss for this game, and it's found at the end of the second world. Since it is a rogue lite, you have to defeat the boss multiple times to reach the end of the game. The EnemyBoss class derived from Enemy to have a base logic of health and damage and to be counted in every check for enemies, like power-up effects, status, and weapon damage. For this reason, this class manages all the other aspects of the enemy, having its own variables for the state, the attacks, the music and the animations.
From a music point of view, this enemy does not follow the standard core AI logic, but it is more connected with the rhythm and the beats. In particular, the boss has dedicated music: one for each attack and a general to wait for the next one. These songs then have particular beats, called cue points, that coincide with boss attacks. Starting from the beginning, the boss starts sleeping, and after a few dialogues with the player, the boss plays the awake animation and starts the heartbeat with the music. After the first intro music, the boss chooses randomly two from six of its attacks, for each attack, he then plays the corrective music and animations, synching the cue points beat with the preview of the attack or the effective attack damage application. After a series of attacks, rest music plays to give the chance (if not already found) to attack the boss while it waits for the next attack series. At the end of the rest music the boss switches itself with another tree of the arena and restarts selecting new random attacks to launch.
Not only that, but the boss has 2 phases, where in the second phase the BPM of the music rises to increase the tension of the battle. This means that the boss regains all its life, that it chooses 3 attacks each time instead of 2, that each attack, since it's faster, is even more dangerous, and it activates an arena laser that constantly move giving the player even more pressure.
So for the music part, I used Metasound. During the project, this was used by the lead programmer for the base music, with the possible addiction of new layers like the combat and the danger (for having low health). So I took inspiration and used it in a similar but more complex way. I have a Metasound source and a reference for all the songs I need to play. I set up the metasound by creating multiple layers using a stereo mixer node with multiple wave players. The first one for the starting song to be played only with a trigger compare boolean, in this way, I can disable the first song (played only at the beginning of the battle). The other 3 wave players are for the attack's song, with the last checked to be played by another trigger compare bool, to be played only in the second phase. The other logic part was made in the GameInstance blueprint: here there is a method called by the boss every time it restarts a new attack series that passes as a parameter the metasound source with the rest music and the chosen attacks. The logic is to set up the metasound assigning the wave sources of each attack correctly, the bool parameters previously discussed, and all the metasound output: an event to trigger for the cue points where a call on the current attack class was made, the play event to trigger the animation, the nearly finish event to increment the attack index (the finish is linked to the next wave player), and the last to stop and restart attacks. Obviously, I used the play quantize of quartz to keep the music in synch. Even if I prefer using c++, the metasound watch output node does not have a c++ corresponding representation (for my knowledge and the unreal version used), so I left this part on blueprint.
The attacks were implemented with a base class BossAttackBase (UObject) with a variable for damage and the reference of the song, animation and boss itself. Then, there is primarily a virtual method called Attack(int32 CuePointNumber) that is called each time the song passes a cue point. Each attack overrides this function, switching between cue points to activate preview or apply damage through the hit box. There are 6 attacks with the corresponding 6 classes:
-
BossAttackArenaRoots: attack with a series of parallel lines of sharp roots, alternating preview and attack. It's implemented with a dedicated actor for the root already placed in the arena, during cue points, the preview and attack of primary and complementary roots are activated. I created a class called BossRoots to manage activation and deactivation, but also with a code on construction script BP to be able to change runtime the number of roots and see in the arena how they are disposed.
-
BossAttackSingleRoot: attack with a series of single root coming from the ground. It's implemented with a dedicated actor, spawned at the player's ground position, starting with preview enable and disabling preview to apply damage on the next cue point. Then the root is destroyed, and a new one is spawned at the next cue point. This process is repeated a few times during the attack. The Root actor class aims for this role, and it's also the actor used as a child for the arena roots attack.
-
BossAttackProjectile: spawn a homing projectile that slows down on player proximity and explodes at the next cue point. The homing part was made by adding logic to the standard enemy projectile explosion, by setting up a timer for setting the target to the player and a timer to check distance from it and eventually slow down velocity, while the explosion needed to be manually called at the next cue point. This process is repeated a few times during the attack.
-
BossAttackAuraExpansionDamage: attack with a circular laser that, starting from the boss, expands up to a limit. It's implemented with a subclass of the actor used for the explosion, having a simple sphere component and a Niagara effect. It starts spawning at the boss position with a small scale and sets a timer to constantly increase the scale. To apply damage only to the part close to the extreme, I checked if the distance between the actor and the player is greater than the radius minus the damageRadius variable. At the next cue point, the aura stopped and destroyed itself. This process is repeated a few times during the attack.
-
BossAttackCircularZoneDamage: attack with a series of circular roots alternating in space. This is very similar to the arena roots attack, except other than the difference in meshes and previews, the use of a dedicated actor class to ensure a different disposition in the component's hierarchy since all the meshes are at the same position, change only in scale.
-
BossAttackMortar: similar attack to the ranged mortar one, with the difference that the projectile even spawns a random status paddle. It's implemented by spawning the projectile on one cue point and triggering the explosion on the next one. This process is repeated a few times during the attack.
Other interesting logics of the boss is the switch with other trees, implemented with an array of tree to possible switches filled at begin play and a random pickup with an actor location swap during the teleport. It also plays dedicated animations.
Then there is the switch to phase 2, made with a simple check on death, stopping music and attacks, setting up the new BPM, arena damage effect, and starting a new attack with a dedicated animation of reawakening.
The last interesting part of the boss is the replayability, with the dedicated feature of having other enemies in the arena. This was implemented with the BossEnemyManager class, an actor responsible for activating the correct group of enemies. In fact, I created multiple scene components for each successive run, under each, the child actor components of the enemies are put inside. Then, at the begin play I took the bossRunCounter and checked which category I needed to activate. So I save in array the enemies of that run and clear the class of all the other child actors.
I implemented all the enemies of the game with the corrective AI. Since the beginning of design, the enemies have had a simple logic: move to the player, attack it, wait some time to reload, and repeat the process. The attack part is then subdivided into two parts: the charge of the attack with a preview and the real attack. In the beginning, the idea was to divide enemies into 3 categories: melee, ranged and special. The first two categories should follow the core logic with a difference in the proximity distance to the attack: for melee, a very small distance, while for ranged, a bigger one. The last category of special should instead have a dedicated logic.
At the beginning, I was considering using BehaviourTree to implement the AI, since Unreal offers some tools already ready. But after a few studies, I decided to leave this idea and use a simple StateMachine manually implemented. This is mainly for 2 reasons: the base core logic is very simple with few states, and I also needed to make enemies act based on music time. This let me think of preferring a custom logic, even if I left some doubt in the case of the special one.
So I created a hierarchy to manage the categories, but due to new features and aspects of the development of this game, I ended up refactoring the base hierarchy multiple times.