Planking Boarders Devlog #2 - The Final Half

Making Assets
In the first half of this devlog, we went from a basic concept to a few working mechanics for my game Planking Boarders that won the 2025 GameX jam. The next thing I did, rather than making the game over screens like I now realise I should have, was making more sprites - this time the ship!
Ship Sprite
I had the idea to set the games zoom to 100% in the editor and take a screenshot. This was a really quick and easy way to make sure my final sprite almost exactly matches my existing collision shapes in engine, so I don't have to spend time moving all of the primitive shapes around to match a new one.

From there, I just had to clean up the lines from the screenshot and overlay some nice curved lines to form the hulls and platforms. I also used a custom brush to create the background, making it a horizontal plank pattern with small waves as the brush moves up and down.

Water Rehaul

My plan worked! However, the water was starting to look very outdated. I essentially wanted to use two 'masks' - one for the shape of the ships background

It took me a while to figure out how to approach this. I realised that, to make it match the ships background, making a copy of the background sprite would be a good place to start as the alpha would already match. Then all I had to do was make a quick shader set the alpha above a certain UV y-coordinate that defines how full the ship is, so anything above a certain point was transparent.
This meant I had to do a bit of maths to convert between how high I wanted the water to be physically in the game, and then send that to the water shader to calculate where that position would lie within the UV

I was implementing this change, but realised it was somewhat unnecessary. As I wanted to define the time in seconds it takes to fill the ship fully rather than the 'pixels per second' of the water rising, i could just use my existing variable and pass that directly to the shader.
func _process(delta: float) -> void:
var fill_this_frame = get_fill_per_second(current_hole_count)
var delta_progress = fill_this_frame * delta
water_progress = clamp(water_progress + delta_progress, 0.0, 1.0)
(water_sprite.material as ShaderMaterial).set_shader_parameter('current_fill_progress', water_progress)
In the end, it only took one extra line of code in the waters script to update the shader! The last thing i wanted to do was to add a bit of maths to stop the water looking too flat. I decided to use the old reliable - sine waves - to add a slight ripple to the interior water. All this does is move the 'cut off uv coordinate' for making the pixel invisible. The shader itself is only
// This is a 2D shader for the interior water of the ship.
shader_type canvas_item;
//This shader should be applied to a copy of a ships background sprite.
// It will take the ships water fill progress as a float between 0.0 (0%) and 1.0% (100%)
//Rather than using UV 0.0 - 1.0 as the range, we can set custom limits between.
//min_fill_y lets there be a slight bit of water visible even when the ship is empty.
//max_fill_y lets there be air in the top even when the ship is considered full.
// This lets the ship sink before the whole sprite is used, which would cut off
// the waves.
uniform float max_fill_y : hint_range(0, 1) = 0.5;
uniform float min_fill_y : hint_range(0, 1) = 1.0;
// This is what is passed into the shader each frame.
uniform float current_fill_progress : hint_range(0, 1) = 1.0;
uniform sampler2D water_depth_gradient;
// Defines the horizontal speed, wavelength and height for sine waves
uniform bool disable_waves;
uniform float ripple_speed = 0.9;
uniform float ripple_length = 0.1;
uniform float ripple_depth = 0.02;
// Add a sine wave offset to the waters maximum visual height.
float get_wave_height(float uv_x){
if (disable_waves){
return 0.0;
}
float time_offset = TIME * ripple_speed;
return sin((uv_x/ripple_length) + time_offset)*ripple_depth*current_fill_progress;
}
// Called on each pixel rendered.
void fragment() {
//Lerp the water progress between a nicer uv range defined in parameters.
float uv_fill_progress = mix(min_fill_y, max_fill_y, current_fill_progress);
//Use get_wave_height() to check if this pixel is above the waves height or not
if (UV.y - get_wave_height(UV.x) < uv_fill_progress){
COLOR.a = 0.0; // Pixel is higher than wave - make transparent
}
}
This shader is one of my favourite parts about this game. It creates a really convincing effect when shown in the final game, as the water inside the ship has its own separate waves and height. It helps it feels more like a room that is flooding rather than just a copy of the ocean outside that is moved up or down.

Additional Water Details
There is two additional detail to this games 'interior water' inside the ship. Firstly, I mapped the water below the waves to a slight gradient, making the bottom of the water darker.
The other main detail is that the cannonball holes were given particles that adapt to the waters height. If the water is low, leaking water particles are shown dropping into the water as it slowly fills. If the water is higher than the cannonball hole, however, it will actually create bubble particles as trapped air rushes through the hole from the outside. This small change added such a realistic effect and create a sense of urgency and panic as the bubbles begin to rise. I'm super happy with how they look!

One of my biggest regrets with this game is never getting round to music and SFX, because a rushing bubbling sound would really help sell the effect even more.
Navy Boarders
This devlog is getting long and I still want to talk about the difficulty and the extremely stressful final morning, so I'll keep this section short since the code is quite simple. That being, the boarders will follow a path at a set speed and if they reach the end of the path you loose. The main time sink for them was animating. To stick with the anthropromorphic theme of the characters, I chose my second favourite animal - cats - to be the boarders!

Most of their animations are reused from the player, the only difference being a fall animation replacing the jump and giving them a custom head shape and colour palette to match.

This was my progress on the last night before the deadline. The animations were all implemented, although whacking them off the ship didn't quite work yet. Oh, and the weird Godot robot face in the floor was the nail trap mechanic where you could use nails to slow down boarders. Unfortunately I never got time to finalise so it was scrapped!
Difficulty
So how do you make a fun difficulty curve for an endless game? You've already read the answer I landed on - curves! Godot has a really nice inbuilt resource called a Curve2D that lets you create and sample 2D Bezier curves.
All the difficulty does is sample curves, where the x axis is set to the time since the game started in minutes and the y axis is some value that gets harder to keep up with (shorter time between holes breaking, faster water fill speed etc.)

In the example above, the Cannon Hole Break Timer curve starts at (0.0, 45.0). This means that, at 0.0 minutes into the game, it takes a whole 45 seconds for a cannon hole to break after being repaired. This curve descends as time goes on, making the break happen faster and faster and therefore the game harder. At the end of the curve, (7.0, 10.59), we have reached the maximum break speed, with repairs only lasting a measly 10.59 seconds.
Most of the games main variables that involve time and tasks follow curves, the main exception being nails *always* adding 5.0s to the timer before one of you repairs break since it is already a small number.
The mess™️
This is where everything fell apart for the last few hours. I learned a very important lesson with this since it almost cost my entry to the jam entirely - Do NOT leave your export until the last few hours!
My game would work fine in the editor but, upon exporting it and playing it on itch.io, it completely broke. The game would load but nothing would happen - the water was full, you couldn't repair anything, no holes broke and no navy boarders appeared. After spending the last few hours of the jam frantically trying to cross reference errors and scripts, it all lead to the most recent updates to the difficulty. I finally found the error below
load(some_resource_type) #THIS IS BAD AND ONLY WORKS IN EDITOR FOR RESOURCES!
# When game is exported the custom resources are
# converted to binary and have a different file location/type.
ResourceLoader.load(some_resource_type) #this is what I should have used!
and implementing this tiny change fixed everything. With that one line of code, I could finally relax for the first time in 3 days.
Final Thoughts
A few days pass, I try a few of the other games and they are really fun! I like to leave a balanced comment with both what I like and my personal suggestion
Overall, I am extremely happy I both had this opportunity and decided to participate. Game jams are such a close thing to my heart due to so many reasons - the great community and networking opportunities, the forced creativity of having a theme and time limit, the constructive feedback, the friendly competition. It's everything you need to both get the motivation and iterative comments / advice for making a game and I will always highly recommend joining them for any person interested in any role in the games industry.
Thanks for reading my little devlog/recounting/tutorial(?),
Sophie