These weeks certainly went fast, didn’t they? It’s kind of amusing to think that judging phase ends in less than 24 hours! However, we at Whales And Games still feel like there’s still so much we could talk and post about the game’s development that we didn’t get the chance to cover during this judging phase (for a multitude of reasons).
In similar fashion to how our graphics designer has written a insight about creating the characters and the art direction and to how our music’s composer wrote another in-regards to his workflow and challenges in composing music, now it’s my turn to talk about the programming structures and methodologies I used for developing the different character’s logic in our Ludum Dare 40 game – Jazzy Beats.
Whales And Games ❤ Unity
For Jazzy Beats, we decided to stick to what we know and use Unity as the engine to develop the game once again. It’s been my tool of choice for almost four years now and I’ve used it on the past four Ludum Dare‘s we’ve participated on. As such, it’s an engine I’ve grown comfortable with, both in terms of workflow, as well as in terms of it’s scripting structure (built on top of C#). As for the IDE itself, I use Visual Studio, not only because of all the tools and integrations it offers when paired with Unity, but also because of how standard of an IDE it is for pretty much everything programming, be it standalone programming, be it with other game engines.
Even so, after all these years using the Unity engine and programming, I still find myself learning better and different ways of getting around, and improving my methodologies when it comes to managing code. Looking back to just a few months ago, when we were updating our Ludum Dare 36 game, Colossorama, for it’s anniversary update I ended up spending a great amount of time just restructuring some of it’s early code in order to optimize it and make it easier to introduce the new mechanics we wanted to add with the new update.
The point is, even while working with the same engine for years, you always end up learning more and further stuff that always helps you refine your future projects as you go, and the past projects we’ve developed were what allowed us to develop Jazzy Beats as it currently stands. One of the programming aspects that ended up being the most important for the game’s development that I only really understood the flexibility as off recently, for example, was Inheritance.
The point is, even while working with the same engine for years, you always end up learning more and further stuff that always helps you refine your future projects as you go, and the past projects we’ve developed were what allowed us to develop Jazzy Beats as it currently stands. One of the programming aspects that ended up being the most important for the game’s development that I only really understood the flexibility as off recently, for example, was Inheritance.
The Power of Inheritance
Although Jazzy Beats‘ behaviours and characters aren’t anything ground-breaking or extraordinary, without some code management and methodologies, it would have been hard to develop and iterate on every single character’s behaviours at the iteration rate necessary during a game jam.
As a consequence, one of my priorities when it came to the game’s development was the code’s re-usability, especially when several of the character’s actions are shared between all of them – be it the Player, the Enemy, or the Fans. One basic example of these actions was the basic punch, which all of these past three characters feature. In order to aid with this re-usability, one of the things I turned too was abstraction and inheritance.
Some of the most basic actions are shared between all of the characters, be them controlled by the player or not. For example, all of the characters use and require components such as a Rigidbody2D
, and have variables such as health
, damageDealt
, velocity
and feature functions such as TakeDamage()
and Knockback()
. All of these functions are shared between all of these characters and feature no differences. However, functions that require more specific functionality depending of the character, such as Movement()
and Attacking()
are marked as abstract
and are later overridden, and filled in depending on the character we’re working on.
As such, all of the characters on the game derive from one base and template abstract controller known as the CharController
which has all the base fields and functionality for a working character. This controller is later inherited and expanded into new and more specific controllers such as the PlayerController
in which, for example, the abstract function Movement()
is overridden with the code used for recognizing the player’s input. These controllers are also expanded with new functions exclusive to that character, such as ApplySparkAttack()
function, which allows the player to use their ranged attack and which is exclusive to their controller only.
This allowed any specific base check or tweaking to be made to all the characters at once, if justified, or specifically tweaked according to the functionality of each individual character. With this structure, I was able to save time with several of the programming tasks that would have been needed if I tried to make separate individual controllers for each of the characters.
However, inheritance is always a mold-able and relative. While writing this section, I can think of several points in which this design and template could be improved further upon. However, given the time limit, you can’t be a perfectionist in regards to it either. Don’t let code perfectionism hold you back while there are features you could be implementing at that moment, but make sure that your code is maintainable to the point you can quickly make changes and adaptations to it.
Character Controllers & Pseudo-AI
With inheritance applied to the characters, the next step was to start connecting this controller structure to the actual gameplay logic of the characters. As referred in the last section, since the base CharController
class Movement()
and Attacking()
functions are marked as abstract
, each of the extended controllers have their own different logic for that same function.
For the PlayerController
these functions mostly check which input the player has done, such as applying a velocity to the Rigidbody2D
depending on the axis that the player is currently inputting, and deploying different attacks depending on their button presses. Not too shabby and a set of controls most likely similar to what most Ludum Dare contestants did with their games as well.
It’s worth side-noting that, for the concern of getting up to speed with Player input and being able to include Gamepad Support out of the box, we used the Unity plugin Rewired as an input wrapper, instead of the default one. It’s a great extension that I absolutely recommend to everyone that has been struggling with expanding Unity‘s input for a while now.
However, it’s when it comes to other characters that the controllers need to be thought of differently. As opposed to the Player, neither Enemy nor Fans have a direct input, meaning that all their actions and decisions needed to be made through code. As such, both of these feature what I like to call a pseudo-AI through a set of checks that are made to see what types of actions the controller should make depending on their situation. I call it pseudo-AI because their decisions are made up of conditional statements, rather than an adaptive artificial intelligence algorithm (such as machine learning which would be insane for a game jam).
The most basic example of these condition checks is with the EnemyController
. For Bluessom’s movement and attack patterns, she will check for any opposing-affiliation Fans (in this case, Player fans) in a wide zone around her and target the one closest to her. Depending on the attacks that are currently available and not on cooldown, she will roll a random number to decide which attack she should use on the currently target fan, and will continue attacking them until they eventually turn over to her side again. Since the amount of fans quickly ramps up during the game, eventually, one Bluessom’s attack will hit more than one fan at a time, converting multiple fans in one ago.
The FanController
works in a similar fashion to the Enemy one, except instead of Fans moving only when one of the idols is near them, they’ll will always be walking towards the idol opposed to their affiliation (Bluessom’s – the Enemy – fans will always move towards Jazzchan – the Player). They’ll stop when it’s time to perform an attack, at which point they’ll perform the same base attack (the punch) that both the main characters have, followed by a cooldown to avoid them constantly doing the attack. Fans don’t have any other attacks such as the kick or a ranged attack as to avoid having the player having to think on dodging different Fan attacks, making their main focus avoiding and converting fans as fast as possible.
One of the earlier issues we faced upon once the fan count started increasing was that all the fans targeting a specific idol would eventually converge into a single moving line since they’d all be moving towards a single point (the Player’s position) at the exact same velocity. In order to circumvent this, we introduced a slight random variation in their movement speeds (rolled when the fan is spawned), together with a position displacement that’s added to their target’s position, making each fan target a point around the character, rather than the character’s absolute position itself. These two variations alone are enough to make Fans walk and ‘mob’ in a much more believable fashion.
Finally, for the actual hit-checking when one of the characters performs an attack, each of the controllers will perform a box cast in front of their feet to check for any opposing-affiliation colliders in a small zone in front of them. These box casts are placed by their feet as due to the game world’s perspective, as making a box cast of the same size as of the player would make the check return characters that, perspective-wise, seem above the player.
Pictured is the example of these box casts for the different attacks performed by the player. The yellow box represents the zone checked when the player performs a basic punch and the red one for the kicking. Finally, the green circle shows the area of fans converted when the player performs a Ultimate Sax Drop.
As a somewhat trivia piece, one of the original designs we had in mind for the Ultimate Sax Drop involved the play having to perform multiple combos in order to charge it before being able to use it. However, concerned that it could create a contrasting design with the other abilities, we eventually decided to stick with having a cooldown for it.
Unfortunately, this ended up making the Ultimate a safe-guard ability, causing most players to just stick to just using it by late-game, and avoid using it during the remainder of the game. Most of the feedback in regards to the game turns around this, which goes on to prove that, sometimes, playing risky in terms of the design can be a much more rewarding, and it’s a note we will take in mind for future jams.
The Game Manager & State Management
And, finally, the last thing important to the management of the characters is the dungeon master GameManager
! The Game Manager is a singleton entry that is responsible for managing the overall state of the game and keeping references to other, single-entity objects that are used through the game.
One example of these objects is the FanSpawner
which will continuously spawn more Fans (which always start with affiliation to the Enemy) as to ramp up the game’s difficulty as the game progresses. Having the game starting with only a single Fan allows the player to get a general understanding of the controls before reaching late-game where they have to take multiple Fans into consideration. The FanSpawner
will stop spawning new Fans when the total count of them in-game reaches 40 – because y’know, it’s Ludum Dare 40.
To avoid performance and other issues – especially in WebGL – as the amount of Fans spawned increases, the higher the chance of them spawning without an AudioSource
becomes higher. This not only saves a lot of memory allocated exclusively to audio (which was causing some music cut-off issues in early development), but also stops the player from hearing 40 different AudioSource
s all playing footsteps at the same time.
Finally, the GameManager
also keeps track of the current GameState
which check what state the game is currently on between three different states (Starting
, OnGoing
and Over
). These states exist to stop the remaining objects in the game of performing certain tasks when they’re not supposed too (like the FanSpanwer
spawning Fans after the game is Over
or Starting
). Likewise, the character controllers also check this state for this very same reason, and will, for example, stop characters from moving once the game is Over
.
Some food for thought for the future is creating a state system for game characters. This would allow for more complex logic and conditions that are not exclusively limited to conditional statements, without potentially creating much additional workload when it comes to programming character and character actions during a game jam.
And that pretty much wraps up this overall programming post! Getting it out in time was one big challenge due to the snowball of occurrences that happened during the whole of the judging period, but here it is, even if it’s just a few hours from the end of judging phase. We still have our Timelapse and Post-Mortem on the way which we hope to make available as soon as we can. However, those will most likely have to wait until after the judging to see the light of day.
In behalf of our team thanks for keeping up with us during this Ludum Dare. Yesterday, when Moski made a post about the coverage we got during this edition, we were baffled that we had almost made it to 200 ratings. However, as of today, we’ve managed to smash that very goal! A whale of thank you to everyone who has made that possible and to everyone which has played our game during these last three weeks. The feedback and engagement by all of the other developers was amazing and completely broke all of our expectations. ? If you haven’t had the chance to play and rate Jazzy Beats yet, now would be the perfect time to do it!
If you’d like to keep up with us at Whales And Games after this Ludum Dare ends, we have our very own Discord channel! It’d be wonderful to see you there! Again, Thank you! ?