v0.1.8.2 - First and Jam Release


An in-depth, a bit long devlog about my thoughts and process behind the game's design. This devlog also contains screenshots and some code to visualize how some things work.

My Thoughts and Process

Originally titled in my head and folder Knives n' Kitten, my ideas for the game now titled Kunai Kitten, have been changed into a more workable scope to make it work and complete it.

After a while I managed to complete a game, i.e. most elements I want to include made it to the release. There's a working main menu, score, game over, credits, and help screens, as well as a very simple boss fight, dialogue system, and even a super/skill. Bonus is that only a single spacebar controls all of it.

The game itself was far from what I originally planned. It's supposed to be a roguelike with cards/scrolls that can buff or nerf the player based on their contract details, solely to follow the agreement theme in this jam. The story was also cut down into incoherent parts and were far from what I originally intended. Due to skill issue I threw these ideas to the garbage bin (for now).

Here are some of the original notes I have in PasteBin: https://pastebin.com/A6LiuCSv

Trust me, I know the plot given there is generic but I had a plot twist in mind :>

Anyways, because of these I made it simpler and aimed for the bug theme instead. My take to the theme is simple and literal: enemies are bugs. It's plain but it works for me given the constraints and lack of other ideas.

I just recycled my core game mechanic/loop from my previous work Orbit Block. Additionally, I reinvented the wheel due to losing my original source code from that project (and that I want to figure it out from scratch). It's important for me to finish the main core mechanic as there's no game if there's only a menu.

After the main game loop was done I worked on the rest: combo system, super/skill, score system, dialogue system, game over, and all of the menus respectively.

Technical Stuff

So uhhhh we need to get everything working on a single button, yeah? Well, not actually because when I double-checked the guidelines it's just optional. Still, my stubborn brain decided to go anyways because at the time I'm having an art block (it makes sense, trust me).

Core Mechanics

The aim rotates on its own and the player can change its directions-all solely for the purpose of using 1 button only when shooting enemies.

The Aim Controls

I want a rotating guide line to help the player aim on the enemies, and have its direction change whenever the spacebar is being pressed. It also attacks at press and therefore allows the player to rapidly tap the key to make the aim a bit more precise.

  1. AimLine is a RayCast2D, and I used it simply for visual convenience in the Editor.
  2. GuideLine is a Line2D. It's the actually drawn animated line you see in gameplay. It uses a shader coded by cpt_metal at Godot Shaders to make the marching ant effect.
  3. Pointer is just a Position2D, a position hint in the Editor. Good for making guides of needed coordinates. It also acts as the spawn point for the player's attack projectiles.
  4. GuideLineEnd is yet another Position2D. Its coordinates are passed as a 2nd Vector2 for the GuideLine's PoolVector2Array of points, i.e. what creates a single line. It's specifically used here to know what end or how long the GuideLine ends. The GuideLine starts from the AimLine's global_position.

With those Nodes setup properly, I just need to animate their rotation per frame. Also flip the AnimatedSprite depending where direction the player faces.

func _physics_process(delta: float) -> void:
    aimline.rotation += rotation_speed * delta
    if aimline.rotation_degrees >= 360 or aimline.rotation_degrees <= -360:
        aimline.rotation_degrees = 0
    if guidelineEnd.global_position.x >= 360:
        flip_h = false
        hitbox.position.x = offset.x + 8
    else:
        flip_h = true
        hitbox.position.x = offset.x - 8
By the way, even if I like math a bit I suck at it, and so I can't handle/tell which specific range of rotation_degrees of the AimLine I have to base on for flipping the character's sprite. So I went with an easier solution: check if the AimLine's GuideLineEnd is behind or beyond the half screen size, i.e. 360 px.

Then change its rotation direction whenever the spacebar key is pressed, as well as spawn an attack projectile.

func _input(event : InputEvent) -> void:
    if event.is_action_pressed("spacebar"):
        var bullet = protobull.instance()
        var direction = (pointer.global_position - aimline.global_position).normalized()
        bullet.global_position = pointer.global_position
        bullet.global_rotation = aimline.global_rotation
        bullet.direction = direction
        bullet.damage = atkDmg
        bullet.bulletSpeed = bulletSpeed
        get_parent().add_child(bullet)
        rotation_speed *= -0.5
        yield(get_tree().create_timer(0.5), "timeout")
        rotation_speed *= 2

With that the setup for aim guide and attack projectiles are mostly done.

And so I setup the enemies. They're just your generic Sprite with an Area2D child that detects the bullets. The Sprite2D bullets also have monitorable Area2D child. The function that runs on signal only checks for which collision_mask the Area2Ds belong to, so that they are independent from checking names of the Nodes which can be complicated and uncertain.

The enemies also have their own AimLine and Pointer but targeted towards the player only. Yes I know I could've just used their own global_position and player's but it didn't occured to me at the time. Additionally, I thought this was more convenient due to the familiar Node structure, and I know where they're aiming at thru the Editor just like the player.

Then the bugs learned to shoot as well, added their hit flash shader made by triangledevv, and handled the damage system on both sides. After that I worked on one of my favorite parts: the UI.

The HUD System

When I had the idea for the protagonist's character design, I thought that it would be cool for their scarf to be represented as the health bar in their HP HUD.

The Stylized HP Bar

It's made up of Control Nodes and it breaks down into 2 main parts: the Viewport "HP" and TextureRect "HPTexture".


All Nodes in the Viewport are treated as a single ViewportTexture, which then the TextureRect uses. Now that we have a single and whole HP bar, we can add an outline shader, which in this case I used the 2D outline /inline, configured for spritesheets by Juulpower.

Since it treats HP's children Nodes as a whole texture, the outline includes the CPUParticles2D "ShadowZone" too.

Now for the actual elements that are being rendered; they are the TextureProgress "Progress" and TextureRect "ScarfDecor" Nodes. Because Progress is a TextureNode, I just made a stylized HP bar. The ScarfDecor is just the static scarf that overlays on the character's head here.

Under the ScarfDecor is a TextureRect "Avatar" node. It has an AnimatedTexture resource as its texture, that is then animated by an AnimationPlayer depending on the player's state (e.g. healing, or being hit by an attack).

And with all of that we now have a working, outlined and animated stylized HP bar for the main character. Yippee! Skibidi!

By the way, the boss bar UI follows almost the same structure as the HP HUD.

The Score and Combo Counter

The score counter simply displays the score value in the Global.gd Singleton (Autoload), and the same applies for combo as well. Both the score and combo displays are RichTextLabels so that I could style them via BBCode whenever I want.

Each 10th hit the combo text is styled with the [rainbow] tag, and then shows its CPUParticles2D "Blitz".

That pretty much covers the UI for the actual gameplay itself. Now let's talk about the menus.

Scenes and Global Script

The  main menu, help, credits, game over screen, and gameplay Scene itself are navigated through the use of Global.gd Singleton. It handles the switch between all Scenes, as well as keeping and changing all persistent data.

Global.gd doesn't handle the background music, rather, each scenes have their own BGM and is set ti autoplay.

Main Menu

The main menu itself doesn't have any buttons. In fact, it only relies on a Timer which increments a variable each its timeout signal, which then the number determines what RichTextLabel will be edited thru BBCode and animated by Tween.

var menu_item := 4
func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("spacebar"):
        match menu_item:
            0: Global.go_to_gameplay()
            1: Global.go_to_help_page()
            2: Global.go_to_score_page()
            3: Global.go_to_credits_page()
            4: Global.quit_game()
func _on_timeout() -> void:
    menu_item += 1
    if menu_item == 5:
        menu_item = 0
    match menu_item:
        0:
            quit.bbcode_text = "QUIT"
            tween.interpolate_property(quit, "rect_position:x", quit.rect_position.x, 0, 0.5, Tween.TRANS_ELASTIC, Tween.EASE_OUT)
            tween.start()
            startgame.bbcode_text = "> START GAME"
            tween.interpolate_property(startgame, "rect_position:x", startgame.rect_position.x, 20, 0.5, Tween.TRANS_ELASTIC, Tween.EASE_OUT)
            tween.start()
        1:
            startgame.bbcode_text = "START GAME"
            tween.interpolate_property(startgame, "rect_position:x", startgame.rect_position.x, 0, 0.5, Tween.TRANS_ELASTIC, Tween.EASE_OUT)
            tween.start()
            help.bbcode_text = "> HELP"
            tween.interpolate_property(help, "rect_position:x", help.rect_position.x, 20, 0.5, Tween.TRANS_ELASTIC, Tween.EASE_OUT)
            tween.start()
        ...

With that we don't have to work with buttons which can be a bit more complex (at least for me)--just trick the player into thinking that they're actually choosing and pressing a button.

Making every thing as simple as possible is a must regardless if it's for a jam or not, it's more manageable that way obviously.

Given all of these I covered pretty much most of the elements I want to function in this short project. However, we haven't talked about the dialogue system and the super/skill yet--so bear with me, we're almost done!

My Simple, Linear Dialogue System

My dialogue system is linear and so it doesn't present any alternative route, and why is that? I wanted to make it simple so I only used a single animation in an AnimationPlayer to animate all of its scenes.

But you're thinking, "well you could've just called some methods by frames and even add more animations", and yes you're right but I don't have really much time for this game alone and so I went again with a simpler solution.

Don't get me wrong, I still called functions on specific frames, specifically the stop_timeline() and stop_act() methods.


  • stop_timeline() literally stops the timeline and waits for another allowed spacebar input to continue the timeline once again
  • stop_act() stops the 'act' the characters are currently in and playing with. The 2 acts are the opening and ending acts, i.e. the introduction to boss, and the post-boss fight speeches.

There are also other functions to hide the dialogue as well as the characters themselves whenever they are not needed on the screen.

By the way just a side note, if you look at the FileSystem files at the screenshot above, there's an AnimationWaitTest.gd script. and it means exactly what its name states--it's the prototype for this dialogue system. I had to make it first before plunging in this dialogue design, just so I know that it's feasible to be included and function given the limited time. And it did worked. So it's really important to work on your prototypes first before the rest of the things you'd like to add.

Anyways, back to the functions, the _unhandled_input(_event) method handles the spacebar inputs and continues the animation only when necessary.

func _unhandled_input(event: InputEvent) -> void:
    if allowed_to_press == true and Global.boss_count == 0:
        if event.is_action_pressed("spacebar"):
            continue_timeline()
    if Global.boss_count == 1 and Global.boss_bar.value >= 500:
        if event.is_action_pressed("spacebar"):
            continue_timeline()

From what I understand of it, between the _unhandled_input() and _input() methods, the latter is ideally used for controlling a character and alike, while the _unhandled_input() are mostly used for handling controls for the UI, e.g. menu, pause menu, etc., which is why I used it here.


The mentioned functions also make it more safe and convenient for me to handle the controls with a single button, for as you can see here a Dialogue scene instance is directly a child of the Prototype main scene and they ("TestPlayer") share the same spacebar key for inputs.

Moving on from inputs, and going back to the animation part, it's actually pretty simple: the characters are made of AnimatedSprites and the dialogue panel and texts are composed of a NinePatchRect and a RichTextLabel.


  • AnimatedSprites for characters - in this example, the "Protagonist" as I call him here (I haven't decided for a name yet at the time) has different animations with only a single frame each. The AnimationPlayer "Player" can simply choose which non-looping animation, or single frame they would play from the AnimatedSprite Protagonist.
  • NinePatchRect for the dialogue panel - to keep the sizes of the edges/borders evenly when being stretched, I used its 9-patch capability.
  • RichTextLabel for the texts - good for adding effects. I used the [shake], [wave], [rainbow], and [color] tags at certain points.

Aside from these elements, there is also a "ContinueIndicator" Sprite. It's the arrow thing that indicates when the text is visible 100% (I'm sorry I don't know what it's called), and that it's finally okay to accept a spacebar input to continue the timeline. It has its own AnimationPlayer to animate itself hovering.

The rest of animated values are the modulates, positions, BBCode texts, and the visibility percent of the texts. There is also a "DarkenBG" Panel which is just a simple dark opaque texture to darken the game screen when the dialogue scenes are up. Lastly, a clone of the character without the Area2D, as well as copy of the boss' sprite.

The 2 are animated in place of the actual characters whenever the actual game scene's process is paused. I pause the main scene Prototype itself when the Dialogue scene is visible in order to pause all process and prevent the input on propagating there, i.e. still shooting bullets when it shouldn't have during Dialogue scenes.

Overall, that pretty much covers the dialogue system.

Super Skill Blitz

Skills and a super are elements that I really would like to try my hands on creating, and so I was stubborn on implementing them for the first time even if it's a jam entry that's very time constrained.

What I had in mind was very simple: hide the player itself, and just show an animation that depicts their super skill. Of course others have done the same already and I'm just reinventing the wheel, regardless, I'm happy with this thought as for someone who's lacking in experience this is a viable option.


As you can see here, the "SlashAnimation" is just a Node2D with 3 identical Sprites, as well as the Sprite of the character in a stylized manner. The "Player" AnimationPlayer transforms the 3 "Slash" Sprites' position and scale, so that they would stretch out and form slash motions when played. After transforming the 3 slashes, the Sprite of the player shows up.

SlashAnimation's Player only plays after accumulating some points by holding the spacebar when eligible for super skill:

func _process(delta: float) -> void:
    if Input.is_action_pressed("spacebar"):
        if Global.can_blitz == true:
            Global.blitz_charge += delta
            if Global.blitz_charge > 1:
                Global.camera.zoom = lerp(Global.camera.zoom, Vector2(0.9, 0.9), 0.5)
                Global.camera.add_trauma(1.0)
                play("charge")
                charge.visible = true
                charge.emitting = true
            if Global.blitz_charge > 2:
                Global.camera.zoom = lerp(Global.camera.zoom, Vector2(0.85, 0.85), 0.5)
            if Global.blitz_charge > 2.5:
                Global.camera.zoom = lerp(Global.camera.zoom, Vector2.ONE, 0.5)
                Global.start_blitz()
    if Input.is_action_just_released("spacebar"):
        charge.visible = false
        charge.emitting = false
        play("idle")

While charging for the super skill, the camera also shakes in the process. The dark particles around the player also shows whenever the charge is more than 1.0.

Aaaand that's all for the game design and other elements in code! (I think. Well I could've forgotten other things to talk about in this subject :3)

Assets

There's the impulse, the need and want to create something original, especially when a piece, may it be art or a game in this case, would represent some aspects of your life or yourself. Or you just simply want to create for the process and art of it; that you just want to share something out to the world.

By doing so it consumes a lot of time especially in game jam in this case. However, as much as possible, I want my creation (if solo dev) to be mostly made with all of my effort and works. I know I am powerless on my own, but I have enough power to do whatever I can complete--I just need to believe in myself and sacrifice my sleep.

And so here we are, I made all the sprites as well as the backgrounds (for good practice, as well as to fight art block and burnout in art). Also I just really want to make something 100% made by me that I'll be proud of, still, I need more practice and study in other fields.

Sprites

It's the same usual workflow I have with sprites. I have a base sprite with their default expression, then I copy it to the next frame to make either another move or facial expression, and repeat the same process as much as I need to.


Background

Compared to sprites I'm less experienced to backgrounds, but still I draw and give them a shot in jams.

Backgrounds tend to be bigger, of course, as they often fill most of the canvas. Given the limited time, I can only make very simple backgrounds that are enough to fit the ambience I need in a scene.

There's no need for so much details when most of time spent on jam games are quite short. If it is a full release outside of jam then you're free to add as much details as you need, even in terms of polishing it.


The volt bar thingy is originally intended to be the indicator/UI for the super skill blitz. I was handling other stuff and it was complicated for me to test and finish at the time, and so I abandoned the idea and went to use the combo counter instead.

I made a layered mockup in order to know the sizes of the game's sprites and backgrounds. It's important to setup a mockup to get a clear view how you would like the game to look like.


To save more time, I divided the original size into half; from 720x480 to 360x240 canvas. This also makes the pixels more visible when upscaled 2x in-game. The same thing is applied to sprites as relative to the size of the background. Speaking of saving more time, I also just reused most colors in the palette, which can be seen in the background at the bottom.

In order to give more life to both backgrounds and remove the flat feel, I used a god rays shader made by pend00, as well as emitted some yellow CPUParticles2D.

There were supposed to be plants and mushrooms lying around, but they weren't my priority for the assets and so I didn't bothered to make and include them.

Audio

I composed BGM many years ago (I forgot when exactly) and they were not that good as expected. Still, they kinda worked and I'm happy that I applied them on some old games. Despite of it, I haven't really composed anything in these years and so it would only be time consuming to try on producing a decent BGM.

The solution for this is for now is surf the net for assets that can be used with their permissible license or author's consent. With that I just went to opengameart.org and downloaded musics that I feel that they can resonate with the vibes the game tries to convey. And so I just trusted my feelings when I listened to the BGMs I used here.

After these, every menu and screen related to the actual gameplay have their own playing audio in the background. Meaning that they aren't handled by the Global.gd--this also makes it convenient and less confusing when handling multiple musics (at least in my case).

Fonts

I just surfed the net again as I can't afford on making my own fonts given the limited time and experience I have.

With all of these, the graphics, audio, and logic behind the games' elements have been covered. In the next subject I'll talk about the other problems I encountered and how I dealt with them.

Issues

Here are the things I encountered and have solved in some ways.

  • pausing the process of certain Scenes
  • handling persistent data on the Global.gd Singleton.
  • animating the dialogues

Main Pause

Pausing the main scene, gameplay itself was a nightmare for me. It took me 3 days if I remember correctly on fixing the issue.

So the problem with this is that I would like to pause the process of the Prototype main scene, the level scene itself, and continue the process of the Dialogue instance, which is a child of Prototype in this case. I need the parent Prototype's process to pause whenever the dialogue scenes are visible so no inputs will be made, when ideally I want and need the inputs to only work on the dialogue itself.


The solution I have was both Global.gd singleton and Dialogue nodes' pause_mode to be set to PAUSE_MODE_PROCESS, while the parent Prototype is set to PAUSE_MODE_STOP.

There wasn't really anything being called in process or physics process within the Dialogue scene and so it's still fine to run even when everything's (or the parent Prototype, in this case) is paused. On the other hand the Prototype scene handles stuff in both its process and physics process so it's necessary to pause when needed to.

Global Persistent Data

The Global.gd script handles all data that is being shared and used across all Scenes, and this includes the value for the player's health, boss bar, score, combo, and even the super skill of the player.

There were methods that had conflict with one another, which I'll be honest I completely forgot most of it, but still I managed to fix few problems regarding the health of the player, health of the boss, score, combo, and even the super skill itself. Most of their conflict lies on triggering the scores before and after the boss fight, as transitioning to this part of the game involves toggling the parent Prototype and child Dialogue's process. Furthermore, for some reason, the reset of scores and combo just stopped working whenever you start the Prototype Scene--I just put a bandaid of resetting these values when you go back on the main menu.

There is also a problem which I haven't been able to solve: whenever you go super and when its SlashAnimation ended, the process of the parent Prototype should pause. I tried a lot of things which I also forgot by this point and none of them worked. And so I just left it there, waiting to be uncovered by a player who supers at the right moment it reaches enough score to trigger the boss fight right after the animation ended.

func start_boss_fight() -> void:
    stage_hide()
    boss_bar.max_value = 500
    boss_bar.value = 0
    dialogue_scene.start()
func start_dialogue_again() -> void:
    stage_hide()
    dialogue_scene.start()
func end_dialogue() -> void:
    boss_life = boss_bar.value
    stage_show()
func stage_hide() -> void:
    player.hide()
    enemy_layer.hide()
    boss_itself.hide()
    ...
    slash_animation.hide()
    slash_animation.player.stop(true)
    get_tree().paused = true
func stage_show() -> void:
    get_tree().paused = false
    player.show()
    ...
    if boss_count == 1:
        boss_itself.show()
func start_boss_attack() -> void:
    boss_itself.allow_attack()

Regardless, it was really a headache to deal with and most of the time were spent dealing with these bugs. The good thing is that most things works stable and as intended (for now)--because I haven't given up.

Dialogue Animation

This gave me a heart attack (figuratively speaking). I don't know if it has something to do with changing the interpolation mode of BBCode_text values via AnimationPlayer, but for some reason it really messed up all the values of all keyframes for the BBCode_text.

For instance, opening and closing tags of BBCode started appearing when they don't have to.


The fix was to revert its interpolation mode from cubic to linear, and painstakingly adjust all the keyframes.

Yes, I could've just undo these actions directly in the Editor, but the thing is that it doesn't allow it. It only reverted some of the actions involving the Nodes and even values on the AnimationPlayer, but not the interpolation mode for some reason. You can undo everything, except this one--I saw a glimpse of hell at that time.

Anyways, it was still fixed in a rather acceptable, functioning state so it's good.

Few Things I'd Like to Add

Overall things have gone well with this short work. It was far from what I originally planned to do, but still I made it work and completed it as well.

Even so still I'd like to add the scenes with their partner, as well as more additional dialogue involving the bugs' motivation behind their plotting and actions, and a rather more interesting boss fight. The story on my head would've been more lively and fun to see (as how I see it at least). I was also into making the bugs' behaviour a bit more complex, but it's time consuming as I'm still inexperienced on it, so I ditched the idea and went along with their dull motions.

Regardless, this work and jam altogether was a great experience for me. I learned how to work with whatever I have, as well as to work on a feasible scope. I also learned that prototyping before making the assets is really important--it will save you more time and from headache later on, as well as you have a clear understanding (mostly) of what you have to work with, and simply make plans along the way.

Another thing that made me happy with the jam are the submissions. I'm happy to see the Godot ecosystem grow this far compared to the very first time I used it. Although I've yet to test more games in the jam since life's been busy, and not all of the submissions work properly on my end.

Anyways, it was really fun and stressful, quite an experience as I've said. I made a lot of mistakes, learned from them, and I'm looking forward on making even more mistakes ahead of the road. To learn, of course.

Thank you for reading my rather messy thoughts in this devlog. Until next time :3c

Files

Kunai_Kitten_Windows_x32.zip 16 MB
15 days ago
Kunai_Kitten_Windows_x64.zip 16 MB
15 days ago
Kunai_Kitten_Linux_x32.zip 16 MB
15 days ago
Kunai_Kitten_Linux_x64.zip 16 MB
15 days ago

Get Kunai Kitten

Download NowName your own price

Leave a comment

Log in with itch.io to leave a comment.