Unfinished Projects and Code Snippets

32-Bit Winter Jam 2025 — Tempest: Become Armageddon

The 32-Bit Winter Jam 2025 was a game jam that focused on the style of fifth generation, 32-bit video game consoles like the Sega Saturn and PlayStation. (Styling after other consoles like the Nintendo 64 were also allowed.) The theme was a haiku: 'twas the last resort./Sacrifice was required./Fireworks in snow. The jam lasted from December 12, 2025 to January 4, 2026.

I had joined the team I thought was the most promising and they decided on doing a game inspired by boomer shooters, namely the 1997 game Shadow Warrior for MS-DOS. Our game was going to be set in fuedal Japan, but the twist being that you’d fight technologically advanced space alien invaders. I told them that despite me being better at composing music, my goal was to focus on game programming and that I only knew 2D, so they had the idea to split the game into two sections: a 3D first-person shooter and a 2D side-scrolling shoot ’em up inspired by Gradius Ⅲ. Since the game was set in fuedal Japan, I looked to one of my favorite games appropriate to the fifth generation Shinobi Ⅲ: Return of the Ninja Master for Sega Genesis. It had a few shmup-like sections where you rode a horse (or surfboard) and shot robot enemies, which was very similar to our premise. I ended up playing a lot of Gradius and Shinobi on my RG34XXSP to get ideas for enemy behavior, pacing, and music.

I wondered how I could incorporate the spaceship speed upgrade mechanic from Gradius into our game when the ship, a machine, is replaced with a horse, a living creature. Well, horses naturally get tired, even in stressful situations like a chase between samurais and aliens, and would probably gradually slow down. So it would be up to the player to keep the horse running fast by keeping the guage at the bottom of the HUD full. If you wanted to go faster, you’d press the speedup button and yell “HIYAH!”. If you wanted to slow down, you’d press the slowdown button and yell “WHOA!”. The guage is seperated into 3 sections representing 3 speed indexes. If the guage was in the first section, you’d move slow. If it was in the second, you’d move faster and if it was in the third, you’d move the fastest. The guage would constantly slowly drain to represent the horse’s stamina. This whole idea is probably a bit too complex for something as simple as speed in a shmup, but I thought it would be cool!

Part way through I was told that there would only be one 2D section and that it would take place as the final part of the game. After some thought, I decided a good way to end the game would be with action setpiece cutscenes. One at the beginning where the player explodes out of a gate and one at the end where the player must miraculously jump over a gaping chasm.

I spent a large amount of my time working on the project. When I couldn’t work on it, I was thinking about it. Like at work I would find time to hide in the back and sketch out ideas. Inspired by how NES game level design was designed on paper, while I was at work I taped paper together and drew two long panels across the paper to draft different enemy positions, movement patterns, and which ones would shoot projectiles.

Servants of Nandor
Organizer Phoenix Steele
Game Designer EelShep
3D Programmer Phoenix Steele
EelShep
2D Programmer Lamb (uncredited)
3D Art NotCleanAgain
Wicked Wizard Lizard
2D Art Lamb (uncredited)
Sound Design and Music WillLeitner
Phoenix’s Cat Nandor

Demonstration of movement, shooting mechanic, and pseudo-3D perspective effects. Demonstration of the hawk enemy. It locks in and swoops down at the player's position, but the player is able to dodge out of its path.

A big selling point of the fifth generation of consoles besides 3D polygons was the ability to easily do 2D sprite scaling and rotation. I thought it would be a cool idea to (literally) add depth to the shmup section if we used sprite scaling to simulate a 3D space. This would end up complicating the programming, but I believed I would be able to handle the challenge.

This code lets the score text graphic use two colors. I never really liked how in score-based games the score would usually have a bunch of leading zeroes to show the potential of how high your score could get, but the zeroes would always be the same color as the score—the more important information—making it look kind of hard to read at a glance, in my opinion. So, I thought it would be neat if I could separate the two parts of the score display while keeping the leading zeroes and I came up with this!

func update_score_hud(score: int = data.score, label: RichTextLabel = textbox, color_blank: Color = data.score_hud_color_blank, color_highlight: Color = data.score_hud_color_highlight, number_of_leading_zeroes: int = 8) -> void:
    var tag_blank: String = &"[color=#" + str(color_blank.to_html()) + &"]" # BBCode blank color tag
    var tag_highlight: String = &"[color=#" + str(color_highlight.to_html()) + &"]" # BBCode highlight color tag
    var string: String = tag_blank + str(score).pad_zeros(number_of_leading_zeroes)

    for i in range(tag_blank.length(), string.length()):
        if string[i] != &"0":
            string = string.insert(i, tag_highlight)
            break

    label.text = string

Conceivably, you could go even further with this concept and use a shader to change the colors further, using that function as a way for the shader to index the two colors.

This code was used to simulate 3D perspective. The perspective_speed() function was used to simulate how objects moving far away appear to be moving slower even when they are really moving at the same speed. The perspective_scale() function was used to simulate how objects far away appear smaller when they are really the same size. This code would be applied to all objects that need a pseudo-3D perspective effect.

func perspective_speed(character_body: CharacterBody2D, speed_multiplier_min: float = 0.25, speed_multiplier_max: float = 1.0, boundary_top: float = 80, boundary_bottom: float = 240) -> void:
    var y_position2speed_multiplier_remap: float = remap(character_body.global_position.y, boundary_top, boundary_bottom, speed_multiplier_min, speed_multiplier_max)
    speed_multiplier = y_position2speed_multiplier_remap

...

speed = 100 * speed_multiplier
func perspective_scale(character_body: CharacterBody2D, scale_min: float = 0.75, scale_max: float = 2.0, boundary_top: float = 80, boundary_bottom: float = 240) -> void:
    var y_position2scale_remap: float = clampf(remap(character_body.global_position.y, boundary_top, boundary_bottom, scale_min, scale_max), scale_min, scale_max)
    character_body.scale = Vector2(y_position2scale_remap, y_position2scale_remap)

Bee had an idea for achieving a pseudo-3D perspective where the game internally uses a regular 2D plane to handle positions, collisions, etc. and then the game will project that information in 3D perspective. Unfortunately, I could not figure out how to accomplish this, so I went with the more brute force method by remapping objects’ Y positions to their scale and speed. I believe that Bee’s hypothetical implementation would have been more accurate to how 3D parallax effects work in real life.

Demonstration of the explosion graphics. Perspective effects were not been applied.

The explosion animation was created by me in a pixel art tool I found called LibreSprite, which seemed to be a clone of Aseprite. The animation was inspired by Dazaemon 2. Explosions are something I have never touched before, so I think these sprites turned out pretty good! Working with Godot’s particle nodes is quite the hassle though. I wish you could code the particle’s behavior instead of relying on the pre-made parameters the engine gives you, or at least that coding it yourself would be an option.

Part way through I was told that there would only be one 2D section and that it would take place as the final part of the game. After some thought, I decided a good way to end the game would be with action setpiece cutscenes. One at the beginning where the player explodes out of a gate and one at the end where the player must miraculously jump over a gaping chasm.

It would be called Tempest: Become Armageddon. Despite having left the project, I was still apparently welcome in the Discord server, so I kept my eye on things to see how the game would progress. I ended up being removed from the credits and the game jam submission on Itch and went unacknowledged in the game’s release.Regardless, I rated the game highly based on its own merits and for the support of my team members.

Local Leaderboard

I used to struggle with trying to figure out how to make a simple local leaderboard for arcade-style games. There are tutorials out there, but none of them seem to actually be about arcade-style leaderboards and instead focus on doing something with online leaderboards.

class_name Leaderboard
extends Resource

## Leaderboard class.

export var leaderboard: Array = [
    [30, 20250505, "GURT"], 
    [1030, 20200202, "BIMP"], 
    [30, 20250506, "SPLURT"], 
    [30, 20250504, "PURT"], 
    [1000, 20250101, "MEOW"], 
    [250, 20230606, "MARY"], 
    [500, 20241231, "GARY"]
]


class Sort:
    ## Sorts duplicate scores prioritizing the earlier datetime.
    static func sort_duplicate_scores(a, b) -> bool:
        if a[1] < b[1]:
            return true
        return false

    ## Sorts scores.
    static func sort_scores(a, b) -> bool:
        if a[0] > b[0]:
            return true
        return false


## Sorts the leaderboard array from highest to lowest score and handleds duplicate scores.
## Leaderboard array must be arranged like so: [HISCORE, int(Time.get_datetime_string_from_system(true)), "PLAYER_NAME"]
## Code by Lamb uwu
func sort_leaderboard(lb: Array) -> Array:
    lb.sort_custom(Sort, "sort_duplicate_scores") # This one must be done before sort_scores()
    lb.sort_custom(Sort, "sort_scores")
    return lb


func add_entry(high_score: int, datetime: int, player_name: String, lb: Array = leaderboard) -> void:
    var a: Array = [high_score, datetime, player_name]
    lb.append(a)

I don’t really understand what inner classes (or subclasses) are used for even after watching some tutorials on the topic of classes, but the tutorial for custom sorting algorithms included making a Sort inner class. It could be simplified by putting those static functions in the main Leaderboard class by using self (i.e lb.sort_custom(self, "sort_scores")

Accumulation Motion Blur

Demonstration of an accumulation motion blur visual effect.

This is a visual effect that simulates the characteristic accumulation motion blur effect found in many PlayStation 2 games. Also available on Godot Shaders and git.gay.

GDScript-Only Version

## Creates a PlayStation 2-like accumulation motion blur effect.
##
## Add to _process(). The frame blending effect is applied to the area 
## within the boundaries of the texture_rect node.
## It is recommended to only set the alpha from 0 to less than 1.
func accumulation_motion_blur(texture_rect: TextureRect, alpha: float = 0.5, use_frame_post_draw: bool = true, viewport: Viewport = get_tree().root.get_viewport()) -> void:
    alpha = clamp(alpha, 0.0, 1.0) # Alpha values are 0 through 1

    var image: Image = Image.new()
    var texture: ImageTexture = ImageTexture.new()

    image = viewport.get_texture().get_data() # FORMAT_RGBAH
    image.flip_y() # Images start out upside-down. This turns it rightside-up.
    if use_frame_post_draw:
        yield(VisualServer, "frame_post_draw") # Changes when the effect is rendered. Changes the vibe.
    texture.create_from_image(image) # Turn Image to ImageTexture
    texture_rect.modulate.a = alpha # Changes the opacity of the frame blending
    texture_rect.texture = texture # Applies the image of the previous frame to the texture_rect

GDScript + Godot Shader Language Version

## Godot Accumulation Motion Blur
## By Lamb; MIT license
##
## Use in conjunction with the accumulation_motion_blur shader material.
func accumulation_motion_blur_shader(material: ShaderMaterial, viewport: Viewport = get_tree().root.get_viewport(), post_frame_draw: bool = true) -> void:
    var image: Image = Image.new()
    var texture: ImageTexture = ImageTexture.new()

    image = viewport.get_texture().get_data()
    texture.create_from_image(image)

    if post_frame_draw:
        yield(VisualServer, "frame_post_draw")

    material.set_shader_param("framebuffer", texture)
// Godot Accumulation Motion Blur
// Use in conjunction with the accumulation_motion_blur_shader() GDScript method
// to feed this shader the framebuffer texture.
// Apply this shader material to a ColorRect node to affect visuals underneath it.

shader_type canvas_item;


uniform float alpha: hint_range(0.0, 1.0, 0.001) = 0.5;
uniform float blur = 0.0;
uniform sampler2D framebuffer;


void fragment() {
    COLOR = texture(framebuffer, SCREEN_UV, blur);
    COLOR.a = alpha;
}

Zelda 64-Like

A still shot of a rudimentary 3D forest area with a cabin, waterfall, river, and tree. Created using Godot's 3D shapes and Blender. Uses N64-style textures.

I noticed that there are a distinct lack of 3D Zelda-likes. Plenty of games that are inspired by 3D Zelda, but none that play like Zelda. Nothing that achieves the same gameplay, same atmosphere and vibe, same music qualities, and same visuals as Ocarina of Time, Majora’s Mask, Wind Waker, or Twilight Princess. It really seems like the only major developers interested in creating Zelda-like experiences is Nintendo, but even Nintendo cast aside traditional 3D Zelda gameplay for open world Zelda, for better or for worse and without any hint of returning after nearly a decade. There have been some efforts in the indie scene with games like Legend 64 and Trinity 64, but these are all still in development as of writing this and while the previews all do look very cool, I feel don’t quite hit that Zelda mark. So in May 2025, I decided to try my hand at it.

As it turns out, making a functioning 3D camera is really, really hard.

I then quickly learned just how overwhelming 3D programming is compared to 2D. 3D modelling is a ocean of hidden shortcuts and poor user experience. Blender is too complicated and Blockbench is not complicated enough. I struggle with understanding trigonometry in 2D, but in 3D? Forget about it. Even something as simple as the health bar was too difficult to figure out. (There exist tutorials on Zelda-style hearts health bar on YouTube, but all of them were inaccurate.) It became clear over the course of a few days that this project definitely requires a team of experienced programmers and will have to be shelved for years down my game development journey if ever.

TO DO:

This is your page! Fill it up with your content!!!!

Helpful resources: