GoDot Game
January - March 2024
1. Introduction
This is one of my personal projects—a challenge and something that makes my inner child happy. I decided to work on this project using Godot, an open-source engine that is highly popular in the indie game development scene. The engine features its own scripting language, GDScript, which is essentially a slightly modified version of Python.
I’ve been working on this in my spare time, and while I might release a short demo in the future, I’ve chosen to keep the source code private for now.
The game was initially planned as a classic top-down RPG, inspired by Undertale, but with a modern combat system. My goal was to combine traditional RPG top-down gameplay with a battle system similar to Undertale, incorporating elements from I Wanna Be The Boshy to increase difficulty and complexity.
In this work blog, I will showcase some of the features I’ve implemented and share gameplay videos. The project is still far from complete, with much left to be done. As of now, it contains over 1,500 lines of code and more than 50 original pixel art assets. The game features two distinct gameplay modes: Traditional top-down movement and Combat-focused gameplay.
These two systems function separately and offer different experiences. Additionally, all the art and animations were created by me, requiring a significant amount of effort and dedication.
1.1 Dialogue System
The dialogue system is the only component I took from the Godot Community Asset Store because it saved me a lot of time. While I have developed a dialogue system in Unity before, making a scalable and serializable one from scratch requires a huge amount of work.
Using this asset allowed me to focus on other aspects of the game. I customized it to look better, and all the animations and dialogue tweaks were done by myself.
Source 1: Customized dialogue system in action.
1.2 Combat System
The combat system is inspired by Undertale with a touch of I Wanna Be The Boshy. You control a character inside a battle area where you must shoot at the boss while dodging its attacks.
This system makes the game more challenging, forcing players to decide between attacking, dodging, or doing both at the same time.
Source 2: Combat gameplay demonstration.
For anyone curious, here's the logic behind the battle system.
# Battle Logic Script - A State Machine that takes care of the turns
extends Node
@onready var debug_info = $"Control/Debug/Debug Info"
@onready var combat_ui = $Control/UI
@onready var player = $"Control/Fighting Box/Player"
var combat_started = false
var round_count : int = 0
# Enemy Animation Player
@onready var enemy_animation_player = $"Control/Enemy/Enemy Animation Player"
# Button Animations
@onready var skills_button_animator : AnimationPlayer = $"Control/UI/Skills/Skills AnimationPlayer"
# Damage Indicator
@onready var damage_indicator_prefab = preload("res://Scenes/Battle System/General/Indicators/damage_indicator.tscn")
@onready var damage_indicator_crit_prefab = preload("res://Scenes/Battle System/General/Indicators/damage_indicator_crit.tscn")
@onready var enemy_debuff_indicator_prefab = preload("res://Scenes/Battle System/General/Indicators/enemy_debuffed_indicator.tscn")
@onready var win_lose_prefab = preload("res://Scenes/Battle System/General/Indicators/win_lose_indicator.tscn")
var damage_indicator_crit : bool = false
# Enemy Variables
## Enemy Health
@onready var enemy = $Control/Enemy
@onready var enemy_health_bar : ProgressBar = $"Control/Enemy/Enemy HP Bar"
@onready var enemy_hp_bar_animator : AnimationPlayer = $"Control/Enemy/Enemy HP Bar/Enemy HP AnimationPlayer"
@onready var enemy_hp_bar_timer : Timer = $"Control/Enemy/Enemy HP Bar/Enemy HP Bar Timer"
var was_enemy_hit : bool = false
### SFX
# Victory
@onready var victory_sounds : Node = $"SFX/Victory Sounds"
var victory_sound_played : bool = false
# Lose
@onready var lose_sounds : Node = $"SFX/Lose Sounds"
var lose_sound_played : bool = false
# Damage
@onready var sfx_enemy_hit : AudioStreamPlayer = $"SFX/Enemy Sound Effects/Enemy Hit"
@onready var sfx_enemy_critically_hit : AudioStreamPlayer = $"SFX/Enemy Sound Effects/Enemy Critically Hit"
# Player
@onready var player_animator : AnimationPlayer = $"Control/Fighting Box/Player".get_node("AnimationPlayer")
@onready var vulnerability_timer : Timer = $"Control/Fighting Box/Player".get_node("Vulnerability Timer")
var player_colliding : bool = false
# Dialogues - counts how many times he button has been pressed and then shows a dialogue correspondent to it
@onready var dialogue = $Control/Enemy/Dialogue
## Enemy Attacks Amount (check the dialogue to see how many there are)
@export var attack_dialogues : int = 0
var attack_count : int = 0
## Enemy Talk Amount (check the dialogue to see how many there are)
@export var talk_dialogues : int = 0
var talk_count : int = 0
# Player Attack or Block Buffs Manager
var buffs = [0,1,2]
var buffs_type = ["Attack", "Defense"]
var buff_count = 0
var buff_dictionary = [{},{},{},{},{},{}]
var enemy_hit_animation_is_playing : bool = false
# States defined
enum FIGHT_STATE {
PLAYER_TURN,
COMBAT,
DIALOGUE,
SKILLS,
BAG,
WIN,
LOSE
}
var state = FIGHT_STATE.PLAYER_TURN
# Called when the node enters the scene tree for the first time.
func _ready():
# Enemy Health Bar Setup
enemy_health_bar.visible = false
#print(enemy.health)
enemy_health_bar.value = enemy.health
enemy_health_bar.max_value = enemy.health
dialogue.dialogue_start = "start"
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(_delta):
# Info on the left side of the screen about the state
debug_info.text = "State = " + state_to_string() + "\nRound: " + str(round_count)
#print("BUFF COUNT: " + str(buff_count))
#print("BUFF Dictionary: " + str(buff_dictionary))
#print("Player Attack: " + str(Global.player_attack))
#print("Player Defense: " + str(Global.player_defense))
if Input.is_action_just_pressed("Console"):
state = FIGHT_STATE.PLAYER_TURN
# STATE MACHINE
match state_to_string():
"COMBAT":
# Close the UI
combat_ui.ui_pop_down()
show_node(player)
# When the combat starts, set player position at middle
if combat_started == false:
player.position.x = 108
player.position.y = 84
player.attacks_count = 0
player.right_timer.stop()
player.left_timer.stop()
combat_started = true
player.current_dir = "right"
#print("COMBAT STARTED!!!")
round_count += 1
# Enemy Attack
var enemy_attacks_animation_player : AnimationPlayer = $"Control/Enemy/Enemy Attacks/Enemy Attacks Animation Player"
var random_int_attack = randi_range(0, 1)
enemy_attacks_animation_player.play("attack_" + str(random_int_attack))
#print("ENEMY ATTACK STARTED!!!!!!!")
# Player Animations and Damage
if player_colliding == true:
if Global.player_vulnerable == true:
Global.player_vulnerable = false
vulnerability_timer.start()
print("PLAYER HIT!")
reduce_hp("Player", 1)
var random_int = randi_range(0, 3)
player_animator.play("player_hit_" + str(random_int))
if Global.player_vulnerable == true:
player_animator.stop()
"PLAYER_TURN":
combat_ui.ui_pop_up()
hide_node(player)
check_buff_debuff_duration()
"SKILLS":
pass
"WIN":
if victory_sound_played == false:
combat_ui.ui_pop_down()
combat_ui.ui_hide_all()
hide_node(player)
victory_sound_played = true
print("WIN!!!")
#SceneManager.SwitchScene("Testing_Room")
"LOSE":
if lose_sound_played == false:
print("LOSE!!!")
# Converts from Enumerator Index to a String
func state_to_string() -> String:
match state:
0: return "PLAYER_TURN"
1: return "COMBAT"
2: return "DIALOGUE"
3: return "SKILLS"
4: return "BAG"
5: return "WIN"
6: return "LOSE"
_: return "ERROR! NO STATE"
### BUTTONS
# Attack Button Press
func _on_attack_pressed():
check_ui_state()
state = FIGHT_STATE.COMBAT
combat_started = false
process_player_buffs("Attack", 1)
#show_enemy_dialogue()
# Skills Button Press
func _on_skills_pressed():
#state = FIGHT_STATE.SKILLS
skills_button_animator.play("Button_Pressed")
combat_started = false
#skills_button_animator.get_parent().focus_mode = false
# Show Abilities text
# Block Button Press
func _on_block_pressed():
check_ui_state()
state = FIGHT_STATE.COMBAT
combat_started = false
process_player_buffs("Block", 1)
#show_enemy_dialogue()
# Bag Button Press
func _on_bag_pressed():
check_ui_state()
state = FIGHT_STATE.COMBAT
combat_started = false
#show_enemy_dialogue()
###
# Checks UI State for Animations
func check_ui_state():
if combat_ui.button_state == "Idle":
combat_ui.button_state = "Down"
elif combat_ui.button_state == "None":
combat_ui.button_state = "Up"
# When the player projectile collides with the enemy
func _on_area_2d_body_entered(body):
if body.is_in_group("player_attack_projectile"):
enemy_hit_logic(body)
body.queue_free()
# When the enemy attack projectile collides with the player
func on_enemy_attack_projectile_0_body_entered(body):
if body.is_in_group("player"):
print("PLAYER HIT")
func enemy_hit_logic(projectile_position):
# Instantiate the damage indicator with the y position being a bit random
var damage_indicator
damage_indicator_crit = false
# Damage dealt
enemy_hp_bar_animations()
var damage = reduce_hp("Enemy", Global.player_attack)
# Check if it is normal or crit damage
if damage_indicator_crit == false:
damage_indicator = damage_indicator_prefab.instantiate()
sfx_enemy_hit.play()
else:
damage_indicator = damage_indicator_crit_prefab.instantiate()
sfx_enemy_critically_hit.play()
$"Control/Player Attacks".add_child(damage_indicator)
damage_indicator.position.x = projectile_position.position.x
damage_indicator.position.y = projectile_position.position.y + randi_range(-5, -40)
damage_indicator.get_child(0).text = str(damage)
# Animations and Sound
enemy_animation_player.stop()
var random_animation_number = randi_range(0,4)
enemy_animation_player.play("Enemy_Hit_" + str(random_animation_number))
# If the enemy is dead
if enemy.health <= 0:
enemy_animation_player.stop()
enemy_animation_player.play("Enemy_Death_0")
state = FIGHT_STATE.WIN
# Reduce HP function - either player or enemy
func reduce_hp(target : String, amount : int) -> int:
# Player
if target == "Player":
Global.player_health -= amount
#print("Player Health: " + str(Global.player_health))
# Enemy
elif target == "Enemy":
# Crit Logic
var random_number = randi_range(0, 100)
if Global.player_luck >= random_number:
amount = amount * 2
damage_indicator_crit = true
# Reduce health and change the bar
enemy.health -= amount
enemy_health_bar.value = enemy.health
# print("ENEMY HEALTH: " + str(enemy.health))
return amount
# Enemy HP Bar Animations
func enemy_hp_bar_animations():
enemy_health_bar.visible = true
if was_enemy_hit == false:
enemy_hp_bar_animator.play("Health_Bar_Show")
was_enemy_hit = true
enemy_hp_bar_timer.stop()
enemy_hp_bar_timer.start()
# Check if the Enemy HP AnimationPlayer has finished playing an animation
func _on_enemy_hp_animation_player_animation_finished(anim_name):
if anim_name == "Health_Bar_Show":
enemy_hp_bar_animator.play("Health_Bar_Idle")
# When the Enemy HP Bar Timer goes off
func _on_enemy_hp_bar_timer_timeout():
enemy_hp_bar_animator.play("Health_Bar_Hide")
was_enemy_hit = false
#print("HEALTHBAR HIDE!")
func player_turn() -> void:
state = FIGHT_STATE.PLAYER_TURN
#print("STATE STATE STATE ====== PLAYER_TURN")
func hide_node(node: Node) -> void:
node.process_mode = 4 # process mode 4 = disabled
node.hide()
func show_node(node: Node) -> void:
node.process_mode = 0
node.show()
func show_enemy_dialogue(dialogue_type : String) -> void:
if dialogue_type == "Attack":
attack_count += 1
if attack_count > attack_dialogues: attack_count = 1
dialogue.dialogue_start = "attack_" + str(attack_count)
dialogue.start_dialogue()
elif dialogue_type == "Talk":
talk_count += 1
if talk_count > talk_dialogues: talk_count = 1
dialogue.dialogue_start = "talk_" + str(talk_count)
dialogue.start_dialogue()
func show_buff_debuff_enemy(buff_debuff_text : String) -> void:
var enemy_debuff_indicator
enemy_debuff_indicator = enemy_debuff_indicator_prefab.instantiate()
sfx_enemy_hit.play()
$"Control/Enemy Buff Debuff".add_child(enemy_debuff_indicator)
enemy_debuff_indicator.position.x = enemy.position.x
enemy_debuff_indicator.position.y = enemy.position.y
enemy_debuff_indicator.get_child(0).text = str(buff_debuff_text)
func skill_damage(target : String, amount : int):
if target == "Enemy":
# Instantiate the damage indicator with the y position being a bit random
var damage_indicator
damage_indicator_crit = false
# Damage dealt
enemy_hp_bar_animations()
var damage = reduce_hp("Enemy", amount)
# Check if it is normal or crit damage
if damage_indicator_crit == false:
damage_indicator = damage_indicator_prefab.instantiate()
sfx_enemy_hit.play()
else:
damage_indicator = damage_indicator_crit_prefab.instantiate()
sfx_enemy_critically_hit.play()
$"Control/Enemy Buff Debuff".add_child(damage_indicator)
damage_indicator.position.x = enemy.position.x - randi_range(10, 60)
damage_indicator.position.y = enemy.position.y - randi_range(10, 60)
damage_indicator.get_child(0).text = str(damage)
# Animations and Sound
enemy_animation_player.stop()
var random_animation_number = randi_range(0,4)
enemy_animation_player.play("Enemy_Hit_" + str(random_animation_number))
# If the enemy is dead
if enemy.health <= 0:
state = FIGHT_STATE.WIN
func process_player_buffs(option : String, dauer : int) -> void:
check_buff_debuff_duration()
if option == "Attack":
buff_dictionary[buff_count] = {"Name": "Attack" ,"Percentual-Change" : 0, "Duration": 0, "Buff-Expire": 0, "Buff-Active": false}
if buff_dictionary[buff_count]["Buff-Active"] == false:
buff_dictionary[buff_count]["Buff-Active"] = true
buff_dictionary[buff_count]["Percentual-Change"] = 1.5
buff_dictionary[buff_count]["Duration"] = 1
buff_dictionary[buff_count]["Buff-Expire"] = round_count + buff_dictionary[buff_count]["Duration"]
#print ("Player attack before multiplication: " + str(Global.player_attack))
Global.player_attack = Global.player_attack * buff_dictionary[buff_count]["Percentual-Change"]
#print ("Player attack after multiplication: " + str(Global.player_attack))
buff_count += 1
elif option == "Block":
buff_dictionary[buff_count] = {"Name": "Block" ,"Percentual-Change" : 0, "Duration": 0, "Buff-Expire": 0, "Buff-Active": false}
if buff_dictionary[buff_count]["Buff-Active"] == false:
buff_dictionary[buff_count]["Buff-Active"] = true
buff_dictionary[buff_count]["Percentual-Change"] = 1.5
buff_dictionary[buff_count]["Duration"] = 1
buff_dictionary[buff_count]["Buff-Expire"] = round_count + buff_dictionary[buff_count]["Duration"]
#print ("Player defense before multiplication: " + str(Global.player_defense))
Global.player_defense = Global.player_defense * buff_dictionary[buff_count]["Percentual-Change"]
#print ("Player defense after multiplication: " + str(Global.player_defense))
buff_count += 1
func check_buff_debuff_duration():
# Go through every buff and check if the buff has reached the round of expiration, if so delete the buff
for n in buff_count:
if buff_dictionary[n] != {}:
if round_count >= buff_dictionary[n]["Buff-Expire"] and buff_dictionary[n]["Buff-Active"] == true:
buff_dictionary[n]["Buff-Active"] = false
match(buff_dictionary[n]["Name"]):
"Attack":
Global.player_attack = Global.player_attack / buff_dictionary[n]["Percentual-Change"]
"Block":
Global.player_defense = Global.player_defense / buff_dictionary[n]["Percentual-Change"]
"_":
pass
#print ("Player BUFF REMOVED:" + str(buff_dictionary[n]))
buff_dictionary[n] = {}
if buff_count > 0:
buff_count -= 1
func spawn_win_lose_text() -> void:
# Play random victory sound
var random_number = randi_range(0,2)
victory_sounds.get_child(random_number).play()
var win_lose_text = win_lose_prefab.instantiate()
$Control.add_child(win_lose_text)
Code Snippet 1: Logic of the battle system.
1.3 Console System
To make the game more testable for my friends and myself when exporting it, I decided to add a fully working debugging/cheat console.
Source 3: Console system in action.
2. Future Development
It is unsure if this game is going to be released as my main goal was to learn GDScript and gain a fundamental understanding of game development in Godot.
However, I am sure that at some point in the future, I will develop another game or software using Godot. For anyone interested in game development, I highly recommend trying it out. The community is great, there are plenty of tutorials online, and developing games is a fantastic way to learn programming and design.