Created
July 20, 2026 01:42
-
-
Save glinesbdev/89f25d64add38dd8dcf0b6379361cccd to your computer and use it in GitHub Desktop.
Godot 4 Character Creator - Deck Building Tutorial
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| [gd_scene format=3 uid="uid://ivlw4ncmevxs"] | |
| [ext_resource type="Theme" uid="uid://c744t38i4smat" path="res://editor_tools_theme.tres" id="1_5if5c"] | |
| [node name="CharacterCreatorDrawer" type="ScrollContainer" unique_id=440571070] | |
| anchors_preset = 15 | |
| anchor_right = 1.0 | |
| anchor_bottom = 1.0 | |
| grow_horizontal = 2 | |
| grow_vertical = 2 | |
| theme = ExtResource("1_5if5c") | |
| horizontal_scroll_mode = 4 | |
| [node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=1957873731] | |
| layout_mode = 2 | |
| size_flags_horizontal = 3 | |
| size_flags_vertical = 3 | |
| [node name="MarginContainer" type="MarginContainer" parent="VBoxContainer" unique_id=106395072] | |
| layout_mode = 2 | |
| size_flags_vertical = 3 | |
| theme_override_constants/margin_left = 5 | |
| theme_override_constants/margin_top = 5 | |
| theme_override_constants/margin_right = 5 | |
| theme_override_constants/margin_bottom = 5 | |
| [node name="MainContainer" type="VBoxContainer" parent="VBoxContainer/MarginContainer" unique_id=1016908542] | |
| layout_mode = 2 | |
| [node name="TitleLabel" type="Label" parent="VBoxContainer/MarginContainer/MainContainer" unique_id=1666338652] | |
| layout_mode = 2 | |
| text = "Character Creator" | |
| horizontal_alignment = 1 | |
| vertical_alignment = 1 | |
| [node name="HSeparator" type="HSeparator" parent="VBoxContainer/MarginContainer/MainContainer" unique_id=756342745] | |
| layout_mode = 2 | |
| [node name="CharacterStatsContainer" type="VBoxContainer" parent="VBoxContainer/MarginContainer/MainContainer" unique_id=1874111179] | |
| unique_name_in_owner = true | |
| layout_mode = 2 | |
| [node name="Spacer" type="Control" parent="VBoxContainer/MarginContainer/MainContainer" unique_id=301660757] | |
| custom_minimum_size = Vector2(0, 10) | |
| layout_mode = 2 | |
| [node name="AddNewButton" type="Button" parent="VBoxContainer/MarginContainer/MainContainer" unique_id=747396700] | |
| unique_name_in_owner = true | |
| custom_minimum_size = Vector2(20, 10) | |
| layout_mode = 2 | |
| size_flags_horizontal = 0 | |
| text = "Add New" | |
| [node name="CreateButton" type="Button" parent="VBoxContainer/MarginContainer/MainContainer" unique_id=164779730] | |
| unique_name_in_owner = true | |
| custom_minimum_size = Vector2(40, 10) | |
| layout_mode = 2 | |
| size_flags_horizontal = 4 | |
| size_flags_vertical = 10 | |
| text = "Create" | |
| [node name="DeleteCharacterConfirmation" type="ConfirmationDialog" parent="." unique_id=1475399647] | |
| unique_name_in_owner = true | |
| oversampling_override = 1.0 | |
| title = "Delete Character" | |
| initial_position = 4 | |
| size = Vector2i(214, 100) | |
| ok_button_text = "Yes" | |
| dialog_text = "Are you sure you want to delete {character_name}?" | |
| cancel_button_text = "No" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @tool | |
| extends EditorPlugin | |
| enum RESOURCE_TYPE {CARD_PILE, CHARACTER_STATS} | |
| const CHARACTER_DRAWER_SCENE = preload("res://addons/character_creator_drawer/character_creator_drawer.tscn") | |
| const CHARACTER_STATS_ROW = preload("res://addons/character_creator_drawer/character_stats_row.tscn") | |
| var instance: Control | |
| var character_stats_container: Control | |
| var add_new_button: Button | |
| var create_button: Button | |
| var delete_character_confirmation: ConfirmationDialog | |
| var errors: Array[String] = [] | |
| var added_characters: Array[CharacterStatsRow] = [] | |
| var file_dialog: FileDialog | |
| func _disable_plugin() -> void: | |
| if instance: | |
| errors.clear() | |
| for stats in added_characters: | |
| if stats.delete_button.pressed.is_connected(show_delete_popup): | |
| stats.delete_button.pressed.disconnect(show_delete_popup) | |
| stats.queue_free() | |
| added_characters.clear() | |
| remove_control_from_bottom_panel(instance) | |
| instance.queue_free() | |
| func _enter_tree() -> void: | |
| create_file_dialog() | |
| instance = CHARACTER_DRAWER_SCENE.instantiate() | |
| add_control_to_bottom_panel(instance, "Character Creator") | |
| character_stats_container = instance.get_node("%CharacterStatsContainer") | |
| add_new_button = instance.get_node("%AddNewButton") | |
| create_button = instance.get_node("%CreateButton") | |
| delete_character_confirmation = instance.get_node("%DeleteCharacterConfirmation") | |
| add_new_button.pressed.connect(add_new_character) | |
| create_button.pressed.connect(submit_form) | |
| add_new_character() | |
| func _exit_tree() -> void: | |
| if add_new_button and add_new_button.pressed.is_connected(add_new_character): | |
| add_new_button.pressed.disconnect(add_new_character) | |
| if create_button and create_button.pressed.is_connected(submit_form): | |
| create_button.pressed.disconnect(submit_form) | |
| for stats in added_characters: | |
| if stats.delete_button.pressed.is_connected(show_delete_popup): | |
| stats.delete_button.pressed.disconnect(show_delete_popup) | |
| func create_file_dialog() -> void: | |
| if not file_dialog: | |
| file_dialog = FileDialog.new() | |
| file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE | |
| file_dialog.add_filter("*.png, *.jpg") | |
| func add_new_character() -> void: | |
| var stats: CharacterStatsRow = CHARACTER_STATS_ROW.instantiate() as CharacterStatsRow | |
| character_stats_container.add_child(stats) | |
| added_characters.append(stats) | |
| stats.delete_button.pressed.connect(show_delete_popup.bind(stats)) | |
| stats.art_button.pressed.connect(func(): | |
| instance.add_child(file_dialog) | |
| file_dialog.popup_centered_ratio(0.5) | |
| ) | |
| file_dialog.file_selected.connect(func(path: String): update_character_art.call(path, stats)) | |
| func submit_form() -> void: | |
| errors.clear() | |
| if not validate_form(): | |
| print_errors() | |
| return | |
| for index in added_characters.size(): | |
| var stats := added_characters[index] | |
| create_character(stats) | |
| reset_form() | |
| func validate_form() -> bool: | |
| var processed_names: Array[String] = [] | |
| for stats in added_characters: | |
| var character_name = stats.character_name() | |
| if character_name in processed_names: | |
| errors.append("Duplicate character name found: `%s`. Each row must be unique." % character_name) | |
| else: | |
| processed_names.append(character_name) | |
| if stats.character_name().is_empty(): | |
| errors.append("Character name cannot be empty") | |
| return errors.is_empty() | |
| func reset_form() -> void: | |
| for stats in added_characters: | |
| stats.queue_free() | |
| added_characters.clear() | |
| errors.clear() | |
| refresh_file_system() | |
| add_new_character() | |
| func create_character(stats: CharacterStatsRow) -> void: | |
| var character_name := stats.character_name() | |
| var path := "res://characters/%s" % character_name | |
| var dir := create_character_dir(path) | |
| if not dir: | |
| print("Directory `%s` couldn't be created" % path) | |
| return | |
| create_resources(dir.get_current_dir(), stats, character_name) | |
| print_errors() | |
| func create_character_dir(path: String) -> DirAccess: | |
| if not DirAccess.dir_exists_absolute(path): | |
| DirAccess.make_dir_recursive_absolute(path + "/cards") | |
| return DirAccess.open(path) | |
| func create_resources(path: String, stats: CharacterStatsRow, character_name: String) -> void: | |
| var deck_name := "{0}_starting_deck".format([character_name]) | |
| var deck_path := "{0}/{1}.tres".format([path, deck_name]) | |
| var stats_path := "{0}/{1}.tres".format([path, character_name]) | |
| var card_pile := CardPile.new() | |
| card_pile.resource_name = deck_name | |
| card_pile.cards.resize(stats.starting_deck_count()) | |
| if ResourceSaver.save(card_pile, deck_path) != OK: | |
| errors.append("Couldn't create starting deck for `{0}`".format([character_name])) | |
| return | |
| var char_stats := CharacterStats.new() | |
| char_stats.art = stats.art_button.texture_normal | |
| char_stats.max_mana = stats.max_mana() | |
| char_stats.max_health = stats.max_health() | |
| char_stats.cards_per_turn = stats.cards_per_turn() | |
| char_stats.starting_deck = ResourceLoader.load(deck_path) | |
| if ResourceSaver.save(char_stats, stats_path) != OK: | |
| errors.append("Couldn't create character stats for `{0}`".format([character_name])) | |
| func show_delete_popup(stats: CharacterStatsRow) -> void: | |
| var cancel_delete: Callable = func(): | |
| delete_character_confirmation.dialog_text = "Are you sure you want to delete {character_name}?" | |
| delete_character_confirmation.dialog_text = delete_character_confirmation.dialog_text.format({"character_name": stats.character_name()}) | |
| if delete_character_confirmation.confirmed.is_connected(delete_character): | |
| delete_character_confirmation.confirmed.disconnect(delete_character) | |
| if delete_character_confirmation.canceled.is_connected(cancel_delete): | |
| delete_character_confirmation.canceled.disconnect(cancel_delete) | |
| delete_character_confirmation.confirmed.connect(delete_character.bind(stats)) | |
| delete_character_confirmation.canceled.connect(cancel_delete) | |
| delete_character_confirmation.show() | |
| func delete_character(stats: CharacterStatsRow) -> void: | |
| var index := added_characters.find(stats) | |
| if index == -1: return | |
| if added_characters.size() == 1: | |
| reset_form() | |
| return | |
| added_characters.remove_at(index) | |
| stats.queue_free() | |
| func update_character_art(path: String, stats: CharacterStatsRow) -> void: | |
| stats.art_button.texture_normal = load(path) | |
| func print_errors() -> void: | |
| for error in errors: | |
| push_error(error) | |
| func refresh_file_system() -> void: | |
| if Engine.is_editor_hint(): | |
| var fs = EditorInterface.get_resource_filesystem() | |
| if not fs.is_scanning(): | |
| fs.scan() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @tool | |
| class_name CharacterStatsRow | |
| extends HBoxContainer | |
| @onready var art_button: TextureButton = $ArtGroup/ArtButton | |
| @onready var name_line_edit: LineEdit = $NameGroup/NameLineEdit | |
| @onready var starting_deck_count_spin_box: SpinBox = $StartingDeckCountGroup/StartingDeckCountSpinBox | |
| @onready var max_mana_spin_box: SpinBox = $MaxManaGroup/MaxManaSpinBox | |
| @onready var max_health_spin_box: SpinBox = $MaxHealthGroup/MaxHealthSpinBox | |
| @onready var cards_per_turn_spin_box: SpinBox = $CardsPerTurnGroup/CardsPerTurnSpinBox | |
| @onready var delete_button: Button = $DeleteGroup/DeleteButton | |
| func character_name() -> String: | |
| var name := name_line_edit.text.strip_edges() | |
| return name if name else "unnamed" | |
| func starting_deck_count() -> int: | |
| return int(starting_deck_count_spin_box.get_line_edit().text.strip_edges()) | |
| func max_mana() -> int: | |
| return int(max_mana_spin_box.get_line_edit().text.strip_edges()) | |
| func max_health() -> int: | |
| return int(max_health_spin_box.get_line_edit().text.strip_edges()) | |
| func cards_per_turn() -> int: | |
| return int(cards_per_turn_spin_box.get_line_edit().text.strip_edges()) | |
| func clear_fields() -> void: | |
| name_line_edit.text = "" | |
| starting_deck_count_spin_box.get_line_edit().text = "" | |
| max_mana_spin_box.get_line_edit().text = "" | |
| max_health_spin_box.get_line_edit().text = "" | |
| cards_per_turn_spin_box.get_line_edit().text = "" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| [gd_scene format=3 uid="uid://bc5m32xq54tju"] | |
| [ext_resource type="Script" uid="uid://j020l4368jfi" path="res://addons/character_creator_drawer/character_stats_row.gd" id="1_422uy"] | |
| [ext_resource type="Texture2D" uid="uid://cpydvk8mq66o5" path="res://art/tile_0087.png" id="1_w0b54"] | |
| [sub_resource type="Theme" id="Theme_ti64l"] | |
| [node name="CharacterStatsRow" type="HBoxContainer" unique_id=427058106] | |
| script = ExtResource("1_422uy") | |
| [node name="ArtGroup" type="VBoxContainer" parent="." unique_id=67371683] | |
| layout_mode = 2 | |
| [node name="ArtLabel" type="Label" parent="ArtGroup" unique_id=120658590] | |
| layout_mode = 2 | |
| text = "Art" | |
| [node name="ArtButton" type="TextureButton" parent="ArtGroup" unique_id=1297880089] | |
| custom_minimum_size = Vector2(40, 40) | |
| layout_mode = 2 | |
| tooltip_text = "Art for character" | |
| texture_normal = ExtResource("1_w0b54") | |
| ignore_texture_size = true | |
| stretch_mode = 5 | |
| [node name="NameGroup" type="VBoxContainer" parent="." unique_id=914715586] | |
| layout_mode = 2 | |
| size_flags_horizontal = 3 | |
| [node name="NameLabel" type="Label" parent="NameGroup" unique_id=1094818735] | |
| layout_mode = 2 | |
| text = "Name" | |
| [node name="NameLineEdit" type="LineEdit" parent="NameGroup" unique_id=238075225] | |
| custom_minimum_size = Vector2(0, 40) | |
| layout_mode = 2 | |
| theme = SubResource("Theme_ti64l") | |
| select_all_on_focus = true | |
| [node name="StartingDeckCountGroup" type="VBoxContainer" parent="." unique_id=1085978918] | |
| layout_mode = 2 | |
| size_flags_horizontal = 3 | |
| [node name="StartingDeckLabel" type="Label" parent="StartingDeckCountGroup" unique_id=1481403146] | |
| layout_mode = 2 | |
| text = "Starting Deck Amount" | |
| [node name="StartingDeckCountSpinBox" type="SpinBox" parent="StartingDeckCountGroup" unique_id=940492767] | |
| custom_minimum_size = Vector2(0, 40) | |
| layout_mode = 2 | |
| select_all_on_focus = true | |
| [node name="MaxManaGroup" type="VBoxContainer" parent="." unique_id=1157395756] | |
| layout_mode = 2 | |
| size_flags_horizontal = 3 | |
| [node name="MaxManaLabel" type="Label" parent="MaxManaGroup" unique_id=1403376699] | |
| layout_mode = 2 | |
| text = "Mana" | |
| [node name="MaxManaSpinBox" type="SpinBox" parent="MaxManaGroup" unique_id=1368201307] | |
| custom_minimum_size = Vector2(0, 40) | |
| layout_mode = 2 | |
| theme = SubResource("Theme_ti64l") | |
| select_all_on_focus = true | |
| [node name="MaxHealthGroup" type="VBoxContainer" parent="." unique_id=1183418993] | |
| layout_mode = 2 | |
| size_flags_horizontal = 3 | |
| [node name="MaxHealthLabel" type="Label" parent="MaxHealthGroup" unique_id=1890281232] | |
| layout_mode = 2 | |
| text = "Max Health" | |
| [node name="MaxHealthSpinBox" type="SpinBox" parent="MaxHealthGroup" unique_id=71402952] | |
| custom_minimum_size = Vector2(0, 40) | |
| layout_mode = 2 | |
| select_all_on_focus = true | |
| [node name="CardsPerTurnGroup" type="VBoxContainer" parent="." unique_id=1779224932] | |
| layout_mode = 2 | |
| size_flags_horizontal = 3 | |
| [node name="CardsPerTurnLabel" type="Label" parent="CardsPerTurnGroup" unique_id=1649013645] | |
| layout_mode = 2 | |
| text = "Cards Per Turn" | |
| [node name="CardsPerTurnSpinBox" type="SpinBox" parent="CardsPerTurnGroup" unique_id=68314896] | |
| custom_minimum_size = Vector2(0, 40) | |
| layout_mode = 2 | |
| select_all_on_focus = true | |
| [node name="DeleteGroup" type="VBoxContainer" parent="." unique_id=1198990069] | |
| layout_mode = 2 | |
| size_flags_vertical = 4 | |
| [node name="Spacer" type="Control" parent="DeleteGroup" unique_id=2082687789] | |
| custom_minimum_size = Vector2(0, 30) | |
| layout_mode = 2 | |
| [node name="DeleteButton" type="Button" parent="DeleteGroup" unique_id=2036493588] | |
| custom_minimum_size = Vector2(20, 30) | |
| layout_mode = 2 | |
| theme_override_font_sizes/font_size = 16 | |
| text = "X" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| [plugin] | |
| name="CharacterCreator" | |
| description="Drawer to create a new character" | |
| author="" | |
| version="" | |
| script="character_creator_editor_script.gd" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment