Gravitas - Initial Devlog
Gravitas is a rhythm-based bullethell game where you pilot a ship to the beat and survive obstacles long enough to escape a supermassive blackhole. Here's a not so quick devlog on how the game was built from starting the jam a couple days late to a finished Gravitas game!
INSPIRATION
Given the B1T-jam theme of BEAT and requisite 1-bit (2-color) limited color space, I first started with trying out different interpretations of the theme and see what stuck. Here are a few I ran through:
- Beat em up
- Beets (food?)
- Heartbeat
- Song Beats
- Beats per minute (BPM)
After a few ideas, my next approach was to focus on what I wanted to gain from this jam. What did I want to learn? and potentially what could be built into a project I would want to work on in the future? and the first thing that came to mind as a longtime Realm of the Mad God (ROTMG) player with friends was I have been wanting to make my own bullet-hell game, and this was the perfect excuse to learn the basics of how to do that.
From here, the synergy of rhythm and bullethell games seemed like a natural progression. I wanted the focus of the mechanics to be on dodging and skill expression rather than shooting / defeating enemies. With this direction, the clear inspiration for a game about dodging obstacles to amazing music and visuals would have to be SuperHexagon, Touhou, and Furi.
The 1-bit theme made my mind start on the classic asteroids games. I'm also personally a big fan of space environments and aesthetics so thinking about how that tied in with obstacles with unique mechanics and skill expression, I landed on the basic foundation of the player is caught in the gravitational pull of a supermassive blackhole and must avoid obstacles (asteroids, etc.. ???)
Taking these inspirations into account Retroindiejosh determined that SID / 80s retro music would probably be the best genre fit for our game.
STARTING TO DEVELOP
This workflow is a bit backwards but as an artist, my natural instinct was to start by defining the look of the game and move forward from there. From here this is where I started writing the background "blackhole" shader that would define the look of our main game scene while trying to take into account future uniforms we may want to change as the game progresses such as darkness, blackhole size, timescale, etc. After a few renditions, here's how the shader looked! With an additional dithering two tone dithering shader applied ontop of the game subviewport.
After adjusting the shader's colors, and creating a player sprite!
And adding some particles for some spacey flair!
DEFINING CORE MECHANICS
Now that the visuals of the game are defined, it was now time to work on the most important part of any game, the core mechanics. Since we had already defined the visuals and genre of the game, we can build from there. Here are some of the core mechanics of the game that are key to the identity of Gravitas
-
Orbiting Player movement
- given that the core gameplay revolves around a blackhole, we already have a unique and interesting movement mechanic to work with by restricting the player to move and dodge obstacles in an orbit around said blackhole. Given that the player would be moving in a more or less consistent orbit around the blackhole, the easiest way to implement our player's movement was to give a y-offset that was the desired distance from the blackhole and then make the player a child of a pivot node that was centered at the center of the blackhole.
-
Player Sprinting/Dashing
-
Chronobreak
-
Chronobreak, aka slowing down time, is a tool for players to get out of sticky situations or make skilled maneuvers to catch hearts they would otherwise be unable to grab. This was initially developed as a way to add visual interest and variety to the game, but the mechanic that followed felt natural to developing skill expression and map variety.
-
-
Obstacles
-
Any bullethell is defined by its ability to challenge the player to dodge obstacles. In most bullethells this presents itself as the bullets or enemies that the player must dodge, and in Gravitas its not much different.. we need to define what the obstacles will look like and how the player must interact with them to dodge properly and how and if this can tie into rhythm game mechanics.
In Gravitas' case we want asteroids as an obstacle, which only makes sense to go from outside inwards being sucked towards the blackhole. So naturally we want a counter to that type of obstacle and have an obstacle that originates from the blackhole and travels outwards towards the player, which we'll name Arcbeams.
This gives us two primary types of obstacles Asteroids and Arcbeams. These would allow for a lot of variance in gameplay as Asteroids would be smaller trickier obstacles, while Arcbeams would have variance in size and act as walls the player must maneuver through.
-
-
Playing to the BEAT (rhythm)
-
To start off, the plan was for the game to launch obstacles around the map and at the player to the beat of the music, hoping to make the player feel the music and interact more with the beat many of the visuals of the game were made to pulse to the beat as well.
However, much later in development after playtesting the finished experience I realized that the Beat theme wasn't imposed on the player very clearly in the mechanics of the game, the player could easily miss the fact that the obstacles were playing to the beat. So to more closely represent the theme of Beat and make sure that the rhythm aspect couldn't be ignored by the player, I imposed a restriction on the player where they must move on the beat. Since this restricted the player's freedom of movement and made the game more difficult, skill-based, and sometimes frustrating this didn't seem like a popular change and many players suggested free movement.
-
-
Heart Spawns
-
Heart ups are essentially 1-ups, a pickup that spawns that increases the player's life without limit. This mechanic served two primary purposes, keep the player on the move and interacting with the rhythm movement mechanic, and to provide the player with some cushion to the difficulty and an opportunity to build up a safety net for actively engaging with the game.
-
DESIGNING THE OBSTACLES
Earlier we had defined the two types of obstacles that we would be able to implement for the jam version of the game, Arcbeams that move outwards and Asteroids that move inwards. When creating the obstacles, I wanted everything to reactable for the player so the first thing I made was a warning telegraph which flashed as an obstacle closed in on firing.
Given that the blackhole is a circular shape I thought that it was would be odd if the Arcbeam was a non-curved shape; we also want to have the arcbeams be a variant width to add variety to obstacles, therefore the only shape we could work with would be an Arc. An arc allows us to cover a full range of widths and angles all the way to a full circle. Since we need an arc to have it's size defined dynamically we can't use a sprite, but luckily Godot has a draw_arc function in the draw function which I wrote a helper function that created an animation for Arcbeams spawning and shooting outwards. This solved how to render the arcbeam obstacles, however there was a problem - there are no arc shaped colliders in Godot. I decided to implement an approximation and fire a rectangular collider that roughly matched the size and movement of the arc. However this ran into the problem that the collider doesn't very closely fit the arcbeam when the arc had large curves, and completely failed to cover the arcbeam on degrees larger than PI/4. To remedy this, I just limited all arcbeams to a size of PI/4 and any arc larger was just made through multiple arcbeams together.
Since the desired movement of the Asteroids was also in a orbit around the blackhole, to implement the Asteroids I used the same technique that I used for the player. Simply changing the y-offset of the asteroid made them move inwards towards the blackhole, and consistently rotating the pivot parent at the defined rotation speed created the orbiting inwards effect.
EVENTQUEUE PROCESSOR
After having implemented the functions for shooting obstacles with parameters for varying target positions, offset angles, rotation speeds, widths, and other settings we finally have the basics to be able to start writing the patterns for our map! However, creating hundreds of obstacles with the many varying angles, rotations, and orientations would be a nightmare and a half to do by hand. I wanted the Gravitas to be a catered and composed experience for the jam, so we needed a way to play through spawning events in a defined sequence.
To do this I defined a schema for events, which are all fit into the eventQueue which will then be processed through the game and play all the events at the speed we want (process event queue pulls an item from the queue and processes the event on beat signal). Here is an example of what the first few patterns look like in the eventQueue:
var eventQueue = [
["wait"],
["wait"],
["wait"],
["wait"],
[["arc", "player", 0.0, PI/4.0]],
["wait"],
["wait"],
["wait"],
[["opposite_arc", "player", 0.0, PI/4.0]],
["wait"],
["wait"],
[["asteroid", -PI/2.0, 0.0, 0.0], ["asteroid", PI/2.0, 0.0, 0.0]],
["wait"],
["wait"],
[["opposite_arc", 0.0, 0.0, PI/4.0], ["asteroid", PI/4.0, 0.0, 0.0], ["asteroid", PI/4.0, 0.0, PI/2.0], ["asteroid", PI/4.0, 0.0, PI], ["asteroid", PI/4.0, 0.0, -PI/2.0]],
["wait"],
["wait"],
[["asteroid", 0.0, 0.3, 0.0], ["asteroid", -PI/2.0, 0.3, 0.0], ["asteroid", PI/2.0, 0.3, 0.0],["asteroid", PI, 0.3, 0.0]],
["wait"],
["wait"],
[["arc", "player", -PI/8.0, PI/4.0], ["arc", "player", PI/8.0, PI/4.0]],
["wait"],
["wait"],
[["arc", "player", 0, PI/3.0], ["arc", "player", PI/3.0, PI/3.0], ["arc", "player", -PI/3.0, PI/3.0]],
]
To make designing maps more sustainable and more interesting I came up with several shapes which would be used when composing events to be played, here is what the schema of events looked like:
ARCBEAM: [shape, target, angle_offset, width (should max out width at PI/2.0)]
ASTEROID: [shape, startPos, rotationSpeed, angle_offset]
TESTING OBSTACLES FIRING
Now that the obstacles and colliders were implemented, it was time to test everything together! Here's an example from an early test (had to redo the math calculating arclength and do several offset adjustments):
But here was the first big bug of the jam, sometimes there would be multiple arcbeams firing at the same time but with no visual indicator meaning ghost colliders - an un-ignorable bug. After hours of debugging I tracked down the issue to the system of Arcbeams reserving slots in an array before being instantiated and fired. Each time an arcbeam wanted to be created it must first check for the first available slot and reserve it to be valid and spawned. The problem was the Arcbeams array was being checked each frame to determine if it was far off screen and then it would be culled, however while it was looping through this culling check other sections of the code could still reserve in the array resulting in potentially multiple arcbeams reserving the same slot. This would present itself as multiple colliders being fired since colliders dont reserve a slot, but only one arcbeam being rendered. To solution to this bug was to cull the arcbeams the moment that they had finished their travel animation instead of doing a culling check passthrough everytime, effectively closing the vulnerability and also made it more performant.
Once the bug was fixed this was a clip of a full arcbeam slots test:
MOVING TO THE RHYTHM
Now that all of the systems of the game were implemented I could no longer procrastinate on learning how to make a rhythm game so it was time to dive into Rhythm-notifier and see how it worked! First I installed it and tried to take a look through the included files and .gd script which had some basic usage examples. I tried my hand at replicating the basic use for our game and the basic case worked, here's a small code example of the setup from the jam version of our title RhythmNotifier:
@onready var t:RhythmNotifier = $TitleTheme/RhythmNotifier
t.bpm = title_bpm
t.audio_stream_player = $TitleTheme
t.running = true
t.beats(4.0, true, 1.0).connect(_on_title_blackhole_beat)
This setup does the beat for the title track and any animations for logo beats
However, after this basic setup I went ahead and made our first web build to test the rhythm mechanic with the obstacles and player movement! However, once on the web I quickly ran into a major roadblock.. the beats weren't being played on the web build at all. But we NEED a valid web build for a jam game to gain any traction, so from here I spent half a day trying to debug the web build. Here are some of things I ended up trying:
- Pore over the RhythmNotifier code and documentation
- Look through any similar issues on GitHub, and Google
- Which I did find one lead saying that the module didn't work properly on Godot 4.2 (but I was on Godot 4.3 at the time)
- Contact the other jam participant who recommended the module
- They tried helping me debug the issue and sent me their setup which I had tried to no avail.. so I asked them what their settings/ engine version was..
Turns out the other jam participants that were able to get this module running on their web build were using Godot 4.4 the latest stable version. I thought surely that couldn't be the only problem so I put that in my back pocket and went back to looking through the code and trying different debug settings/ node definitions. However after several frustrating hours, I resigned to updating my Godot to 4.4 as a last ditch effort before I switched to something else for my rhythm tracking.. and the RhythmNotifier web build just worked once I switched to Godot 4.4.
COMPOSING THE GAME LEVEL
Now that we had defined the eventQueue and queue processing system we could fully compose the map and get the game running! But when designing the first map, I ran into the issue that many patterns would be very cumbersome/tedious/long to compose manually. For example, any pattern that needed events to be spaced evenly such as asteroids coming from several angle intervals around the screen.
Such patterns would be much more straightforward to compose if I had a helper script to generate the different intervals for the events.. so I wrote a couple event generator functions which took into consideration which part of the eventQueue it was writing/appending/inserting to and how many events we were generating. Using these helper functions I was able to generate complex patterns of thousands of events with guaranteed accuracy.
Once the necessary systems and tooling were in place to smoothly compose the level it was now time for the hard part of designing a satisfying level that was a self-contained one level experience -- with a difficulty ramp so players could learn the core mechanics, lenient enough so that most players could get through without a ton of bullethell experience, yet challenging for those with more experience.
These are some of the core considerations I tried to tackle while developing the map for Gravitas:
TUTORIALIZATION - DIFFICULTY RAMPING
When introducing the game to new players we need to design the level so that it starts slowly, introducing one by one the different ways that the obstacles can present themselves. Slowly building up to situations where the player is faced with obstacles that need to utilize the movement mechanics of **Chronobreak** and **Sprinting** to get avoid all obstacles and no-hit a section.
BUILDING PATTERNS AND LAYERING MECHANICS
As the game goes on we introduce patterns of obstacles with two main goals:
- Be visually interesting
- Satisfying & Challenging to navigate through
- Build pattern recognition for the player
For example, a pattern may be a cross of arcs firing at 90 degree angles from the center, or waves of asteroids firing at different angles simultaneously. Once we have introduced a pattern to the player we can then layer patterns together to increase complexity and intensity, but this is a careful balance to strike and needs testing to make sure it's still playable and no-hittable with skill!
MATCHING TEMPO OF GAMEPLAY TO INTENSITY OF MUSIC
This one is simple, since we are working with one music track that has both low-intensity and high-intensity climactic sections, we want to match the most engaging high intensity gameplay with layered patterns to coincide with the climactic high-intensity sections of the song. Contrary to that, we want the low-intensity sections of the song to be matched with slower sections where the player can catch a breather ideally while being introduced to a standalone pattern.
Finally with all the mechanics implemented and the map composed, here is what that all looked like together:
THE FINAL STRETCH
With half a day left to go, we were officially in the final stretch of the game jam. With the main gameplay pretty much complete with some basic testing we were looking good, but still had our work cut out for us to be able to submit on time with a good finish.
So what did we have left to do in these final hours?
- game over / game win screen
- game logo
- game prologue comic - a small idea to add a little flair and give a small story to the game
- transition animations
- itch page assets!
- cover art
- screenshots & gameplay videos
- game page text content
- game page category assets
- try out some custom CSS
Once these were done, we were finally ready to submit!
WHATS NEXT?
Takeaway
I initially decided to join this jam on a whim a few days late after seeing Macrow promoting the jam in the microjam that we had both participated in a couple weeks earlier, but i'm super glad that I ended up taking part! I not only got the opportunity to create a project that I'm proud of and got to learn from, I also was able to meet many other super cool game devs and become more involved with the game dev community - something that I've been hoping to do for a while now!
Future Plans!
- Make a basic gameplay trailer, setup steam page
- Make a full post-jam demo!
- Make safeguard to prevent events being stacked on the same beat
- Fix easymode! Introduce mechanics difference for easymode and beatmode. Freedom of movement for those who want it!
- Refactor eventQueue generator and processor to enable custom player maps!
- Show controls in game! telegraph correct key layout
- link to Color Me Silly and other games!
- Continue Development!
CREDITS
not_absent - Programming & Art
retroindiejosh - Music & SFX
Get Gravitas
Gravitas
Fly to the beat to avoid obstacles and escape a Supermassive Blackhole!
Status | Released |
Authors | Not_Absent, Retro Indie Josh |
Genre | Rhythm, Action |
Tags | 1-bit, Bullet Hell, Difficult, Godot, Indie, Pixel Art, Retro, Space |
Leave a comment
Log in with itch.io to leave a comment.