© 2023 by Andrew Jonhardt. Proudly created with Wix.com

  • Andrew Jonhardt

Red Bop Blue full breakdown

So, per my last post, I joined a week long game jam creatively titled "Fighting game jam (2?)". I found and joined the jam a full day late. Normally, I would not do or encourage this. However, in this case, I happened to have an idea a few days prior that I could easily plan out as a fighting game. For more details on all that, check out my previous post. Today's post is gonna be long enough as it is.


I was informed last week that my board game prototype, FTR, is going to be streamed on July 7th. I plan to post on Twitter and Facebook and briefly here on the day of the stream with a link to it, and then follow up with a full post the following week. I'll probably switch gears back to working on FTR as my full time project. I'm expecting a return to board game design to feel like whiplash after everything I've done and learned this past week, and it's my hope that posting a full breakdown of my jam game will lessen the effect. It probably won't.


Here's the end result of my design (the only submission to the jam, which is disappointing):

https://dgalga.itch.io/red-bop-blue


My goals for Red Bop Blue were as follows:

  • Local-only 1v1.

  • "Family friendly" per the jam rules.

  • 2 onscreen characters composed of "programmer art" (blocks or poor sketches of characters with primitive animations).

  • Avoid using paint programs (GIMP, Krita, etc) as much as possible; stick to Godot tools.

  • 1 to 4 moves for each character.

  • 1 stage.

  • Lifebars for both characters.

  • A win counter for both characters.

  • All damage tied to the character's weapons.

  • A way for swords to clash or block each other.

As you can see, I achieved all of this and more:


The outcome of all of my effort isn't fancy, and is very broken, and I learned alot in making it. What follows is a breakdown of what I learned with my code.


Section 1: Skeleton

My current favorite thing about Godot is the instancing and scene structure. Making, working on, and importing multiple scenes at a time is something I'm quickly getting used to. I used this approach to construct a baseline character, or a character all others will contain attributes of, with a baseline script that will extend to all other characters:


As you can see, the basic character is composed of ColorRect, KinematicBody2D, and Collision2D nodes. The ColorRect nodes, which are literally just blocks of color, were part of an effort to see how much I can prototype in Godot without needing to use a paint program.


Having the ability to speedily test ideas is something I value highly in an engine. Making assets takes time and, unless said assets are demanded by the design of the game, is something I try to avoid as much as possible in the early stages of a design.


Most engines have some simple pre-built assets. Unity has a number of 3D objects to choose from, for example. Godot is the first engine I've encountered to incorporate an easy 2D color block, and I found the ColorRect node to be easier to prototype with over Unity's 3D blocks and spheres.


Returning to the basic character, the script I used was short and sweet:

extends KinematicBody2D

const MOVE_SPEED = 150

const SWORD_ALIGN_SPEED = 50

const SWORD_LIMIT = 200

These are the values for all character move speed, character sword movement up and down speed, and the limit for how far a character's arm can move up or down.


The second basic object I worked on was the sword. I only need 1, since both characters would be using the same one. Surprisingly, the sword turned into the biggest headache of the whole project.

Each of these 3 nodes served as root at one point, I swear.

An issue I have with Godot is that only Sprites, or the ColorRect in this case (it's classified as a sprite), allow for pivot rotation point adjustment on import to other scenes. This is why Blue's sword rotates differently than Red's. The pivot point for the sword is at the top left of the object (that little orange cross), and I simply rotated Red and Blue's swords so that the pivot point is touching Red and Blue's arms. Red and Blue are facing opposite directions, so the rotation in animations is altered to be overhand for Red and underhand for Blue.


In the end, I decided it was more important to import the sword as an Area2D node so I could access the associated Area2D signals to speed up development. With more time, I would've had more reason to try and find a solution that allowed the swords to always rotate the same direction.


If you don't know what Godot signals are, think of them as special scripting components. They help to establish a relationship between the origin of a signal and the script you're using it in, without you needing to code in any awareness between the objects of each other. I don't know of any way to use signals between scenes. I've only ever gotten them to work between objects that are in the same scene.


The script I used for the sword was a little more complex than the base character script:

extends Area2D


var obj_name


func _on_body_entered(body): #check for any player entering damage area and pass back to gamestate to resolve

obj_name = body.get_name()

Global.gameState.resolve_dmg(obj_name)


func _on_Sword_area_entered(area): #check for swords crossed

if area.name == "SwordTemp":

Global.swords_crossed = true


func _on_Sword_area_exited(area): #check for swords uncrossed

Global.swords_crossed = false


The full script has connections to scripts that I haven't even mentioned yet. Global is just a storage space that all other scripts are aware of. No processing happens in Global, but scripts can make calls to Global to reference other objects or scripts that would otherwise be invisible due to the scripts/objects existing in different scenes. However, there's still enough here to detail what the sword was supposed to be doing:

  • All damage was handled through sword collision. If the sword collides with a physics body (either player), it gets the name of the player and passes it back to a script that tracks the overall state of the game. The gameState script then deducts health appropriately.

  • As an Area2D object, the sword itself is not a physics object. So, in order to interact with itself for sword crossing/blocking/etc events, the sword has to check for collisions with "area" nodes. The area entered and exit nodes simply check if the sword has collided with another version of itself, and then if it's stopped colliding with a version of itself.

After creating the base character and the sword, I created a scene for the main stage. The main stage is literally just a container for other objects to be imported into, and there's not much to say about it as a result.


Section 2: The Players

I didn't make the player characters the way I imagine a professional-level fighting game would make then: build 1 character off of an import of the base character scene, and then import the character twice into the main scene to serve as player 1 and player 2. I didn't believe I would be able to figure this out easily and within the time alotted, so I didn't even try.


Instead, I manually built a player 1 scene, and player 2 scene, and called them Blue and Red.


All of the scripts on Red and Blue are exactly the same save for the animations, which I couldn't find a way to copy. The only real difference between them is that I started and finished Red first.

When Red vs Blue, Red best color.

The greyed out nodes in the image above are everything I imported from the base character scene. The additions for each character are clear: Red and Blue each have their own arm, an AnimationPlayer node, and a separate instance of the sword.


Speaking of the AnimationPlayer, guess what the 2nd biggest headache in making the game was?

Break a leg! Hahaha...ha...

Godot has built in animation system which, combined with the ColorRect nodes, saved me alot of time I would otherwise have had to spend making custom sprites. That being said, the existing animation system is not perfect.


For starters, the animation system doesn't like collision nodes. Both players have 1 leg with a collision box on it as part of a quick and dirty "get off me!" kick move. That leg, as you can see above and in the video gameplay, breaks constantly.


If you're only moving sprites around, or if you have only very basic movements/needs for the Godot animation system, it's perfect. However, if you have a sequence that requires alot of moving pieces, like this:

Prepare for pain.

Anyhoo, on to the player scripts:

extends "res://Scripts/BaseChar.gd"


var velocity = Vector2()

var ori_screen_size

var keep_onscreen

onready var P2Arm = $RArm


func _ready():

ori_screen_size = get_viewport_rect().size

keep_onscreen = ori_screen_size.x/4 #The screen is 1024, but the game doesn't handle things as such


func _physics_process(delta):

update_move(delta)

update_sword(delta)

move_and_slide(velocity)

self.position.x = clamp(position.x,-keep_onscreen,keep_onscreen) #-220,220) these are the values I had to set to figure out how to keep the player object onscreen... weird

self.position.y = 2


func update_move(delta): #players normally move only L and R

if Input.is_action_pressed('ui_p2_right') and not Input.is_action_pressed("ui_p2_strikemod"):

velocity.x = MOVE_SPEED

elif Input.is_action_pressed('ui_p2_left') and not Input.is_action_pressed("ui_p2_strikemod") and not Global.swords_crossed:

velocity.x = -MOVE_SPEED

else:

velocity.x = 0


func update_sword(delta): #rotate sword up and down

var sword_rot = P2Arm.get_rotation()

if Input.is_action_pressed("ui_p2_swordup") and sword_rot > -SWORD_LIMIT:

sword_rot = sword_rot - (SWORD_ALIGN_SPEED/2)

P2Arm.set_rotation(sword_rot)

elif Input.is_action_pressed("ui_p2_sworddown") and sword_rot < SWORD_LIMIT:

sword_rot = sword_rot + (SWORD_ALIGN_SPEED/2)

P2Arm.set_rotation(sword_rot)

if Input.is_action_pressed("ui_p2_strikemod") and Input.is_action_pressed("ui_p2_right"):

$AnimationPlayer.play("str_slash")

elif Input.is_action_pressed("ui_p2_strikemod") and Input.is_action_pressed("ui_p2_left"):

$AnimationPlayer.play("forward_stab")

elif Input.is_action_pressed("ui_p2_strikemod") and Input.is_action_pressed("ui_p2_swordup"):

$AnimationPlayer.play("kick")

elif Input.is_action_pressed("ui_p2_strikemod") and Input.is_action_pressed("ui_p2_sworddown"):

$AnimationPlayer.play("low_slash")


There's alot going on here, so I'm going to break it down by function:

  • _ready(): In this function, we are getting the current screensize so I can always have the player onscreen. I was surprised at how difficult this was to figure out. Unity screen limiting can be simple X and Y values, but in Godot I have to divide by 4 to even get close to what I want.

  • _physics_process(delta): This is calling my custom functions to be updated every physics frame (only on the frames when physics is processed versus normal delta-bound processing), moves the player according to passed values, continuing to ensure the player is locked onscreen, and locking down the Y value for the player. Some animations at certain angles physically move the player up and down and, as there's no way for the player to compensate, I came up with self.position.y as a quick and dirty solution.

  • update_move(delta): Sets the values that move the player according to the time values passed down from _physics_process(delta). Player input is read, and you move right, left, or stop. If swords are touching, you cannot move forward (only back).

  • update_sword(delta): This function rotates the player's arms up or down, and initiates attack animations if the correct movement button is pressed while the attack modifier button is held down.

All of the above is fairly standard, which was perfect for this project. Figuring out a turn-based movement system, for example, would probably not be something I could figure out in 1 week with my current skills.


Section 3: The UI

The UI system was where my existing skills were tested the most. The UI amounts to 1 entire scene built around the game camera:

What we got in this scene:

  • Healthbars for Red and Blue that use a color gradient to symbolize reductions in health.

  • Various Label nodes to provide text feedback to the player (number of wins, whose health is whose, a round timer).

  • A timer for the round (and that will end the round if the timer runs out) and a timer to allow for enough processing time to pass when a round ends. I had some issues early on with wins being recorded incorrectly, or not be recorded at all, and through testing I found that a 1 to 2 second timer helped avoid this issue completely.

  • An exit prompt. This was added when I realized I had a few hours before the game was due, and was supposed to be little more than a quick and dirty way to quit the game. Oh, it became a headache, believe me.

And now, for the piece de restroom that tied everything together, the gamestate script!

extends Area2D


var screen_size

var gameState

var blue_wins = 0

var red_wins = 0

var swords_crossed = false


Gotcha! That's the Global script! This is the gamestate script:

extends Node


onready var P1Life = $P1Healthbar

onready var P2Life = $P2Healthbar

onready var EndText = $GameOver #hidden text that appears when someone wins

onready var RoundTime = $RoundTimer #timer node in scene

onready var VisibleTime = $VisualTimer #Clock the player sees

onready var ResetTimer = $Reset


var player_life_start = 30 #all player life the same

var player1_life

var player2_life

var update_time_track = 60 #amount of time in the round


var cur_dmg = 10 #all attacks do the same dmg


func _ready():

Global.gameState = self

EndText.visible = false

VisibleTime.text = str(update_time_track)

P1Life.max_value = player_life_start

P2Life.max_value = player_life_start

player1_life = player_life_start

player2_life = player_life_start

update_life()

$BlueWins.text = "Wins: " + str(Global.blue_wins)

$RedWins.text = " Wins: " + str(Global.red_wins)


func _input(event): #spawn the popup to exit the game & manage in-game pausing

if event.is_action_pressed("ui_cancel"):

if $ExitPrompt.visible:

$ExitPrompt.visible = false

else:

$ExitPrompt.visible = true

get_tree().paused = !get_tree().paused


func resolve_dmg(name): #since all damage is currently the same, just resolve damage all in this function

#This is how switchcase works in Godot! Adding here in case, idk, I ever want to have more characters.

match name:

"Red":

player2_life -= cur_dmg #calculate changes to healthbars

update_life()

"Blue":

player1_life -= cur_dmg

update_life()


func update_life(): #update healthbars & confirm if win. Elifs don't work for double-KO.

var dual_ko_check = 0

if player1_life > 0:

P1Life.value = player1_life

else:

P1Life.value = 0 #ensures lifebar goes down alla way (won't otherwise)

dual_ko_check += 1

ResetTimer.start()

ko_check(dual_ko_check)

if player2_life > 0:

P2Life.value = player2_life

else:

P2Life.value = 0

dual_ko_check += 2

ResetTimer.start()

ko_check(dual_ko_check)


func ko_check(ko_var): #check how KO occured, and calculate points + reveal winner in text

match ko_var:

1:

get_tree().paused = true #adding pause under the KO check function avoids further damage

EndText.visible = true

EndText.text = "Round over! Red wins!"

Global.red_wins += 1

2:

get_tree().paused = true

EndText.visible = true

EndText.text = "Round over! Blue wins!"

Global.blue_wins += 1

3:

get_tree().paused = true

EndText.visible = true

EndText.text = "Dual KO!" #don't set points here, because the previous switches will trigger and both players already get +1 as a result


func reset_arena(): #reset scene for additional play

get_tree().reload_current_scene()


func _on_RoundTimer_timeout(): #using a signal from the timer to confirm when it runs out

update_time_track -= 1 #need a variable to increment, as it appears you cannot increment off the timer itself

VisibleTime.text = str(update_time_track) #update the clock the player sees.

if update_time_track == 0:

get_tree().paused = true

if player1_life > player2_life:

EndText.visible = true

EndText.text = "Timeout! Blue wins this round!"

Global.blue_wins += 1

ResetTimer.start()

if player2_life > player1_life:

EndText.visible = true

EndText.text = "Timeout! Red wins this round!"

Global.red_wins += 1

ResetTimer.start()

if player1_life == player2_life:

EndText.visible = true

EndText.text = "Timeout! Tie!"

ResetTimer.start()


func _on_Reset_timeout(): #reset game after providing enough time to get everything sorted.

get_tree().paused = false

reset_arena()


func _on_ExitPrompt_visibility_changed(): #this is the only thing I found to unpause when the dialogue is closed through Cancel or X

if !$ExitPrompt.visible:

get_tree().paused = false


Eyes glazing over? Don't worry, mine are alittle, too. There's alot to process here. Let me break it down:

  • _ready(): First, we gotta let the Global script know who the gamestate is, so that happens here. Next is all just making sure everything is set to the default it's supposed to: Healthbars set to full, pulling the current win count from Global (Global persists between wins/restarts) and ensuring the win counts are up to date, etc.

  • _input(event): Did you press the Esc key? Pause the game and pop up the quit menu. Which, turns out pausing the game is really easy (versus Unity, where I still don't know how to pause). All you needed is get_tree().paused = [true for paused or false for unpaused]. The official documentation is actually really useful here for the curious: https://docs.godotengine.org/en/3.1/tutorials/misc/pausing_games.html

  • resolve_dmg(name): This function modifies healthbars by comparing the name returned from the sword script to the "Red" and "Blue" strings. This is also my first attempt at making a switchcase in Godot and, I gotta say, I vastly prefer matchcase over the C# switchcase structure. That much less muss/fuss.

  • update_life(): Literally what the name implies. It updates lifebars and checks for a potential KO.

  • ko_check(ko_var): It's absurdly easy to get a double KO in Red Bop Blue. This whole function exists because my existing code was not handling the result well. Red or Blue would get a point, or sometimes double points. I don't recall exactly, but I hated the result. So, I stripped my existing KO code out of update_life() and isolated it all to ko_check until the current solution was found. Currently, those who get KOs get points which carry over to later rounds. Also, 2nd match catch. I definitely like these things.

  • reset_arena(): Does what it says; if someone gets a KO, or if time runs out, this function resets the entire scene back to how it was at the start of the game.

  • _on_RoundTimer_timeout(): This function is actually a signal from the RoundTimer node. Timers work a little oddly in Godot, by which I mean the RoundTimer isn't actually the clock for the match. The variable update_time_track is. What RoundTimer does is, every time it expires (every second), update_time_track is decreased by 1 and then used to update the VisualTimer Label that players actually see while playing. Wacky, right? The rest of the code in this function is about handling timeouts.

  • _on_Reset_timeout(): This function is another signal, this time from the Reset timer node, and exists to solve 2 problems: The game failing to update correctly when a round ends, and to unpause the game before it gets reset. I had to pause the game when someone reached 0 health, as it was possible to continue scoring while an opponent was at 0 before the scene reset, but resetting the scene doesn't unpause the game!

  • _on_ExitPrompt_visibility_changed(): The final function in my code, this signal originates from the ExitPrompt, and represents the only solution I found for if a player clicks cancel or the X on the ExitPrompt AcceptDialogue node. See, AcceptDialogue nodes have easy code for if you click OK. Programming a function for when you do anything else gets alot murkier, though. So, this function checks if the ExitPrompt is hidden, but only when the visibility of the object changes, and then unpauses the game if true.


Woof, that was a lot to go over. Here's the ExitPrompt code, if anyone's curious:

extends ConfirmationDialog


func _on_ExitPrompt_confirmed():

get_tree().quit()


That's it. That's not necessarily everything I learned or made for the project, but that's all I'm going over from it. I hope it's useful to someone. I recall trying to find an example fighting game project when I was working in Unity, and never finding anything this detailed.


Next week is back to FTR and physical game design. And much, much shorter posting. Until then.