└── addons └── modular_sprite_animation_factory ├── LICENSE ├── README.md ├── assets ├── checkbox-blank-circle.png ├── checkbox-blank-circle.png.import ├── checkbox-circle.png └── checkbox-circle.png.import ├── dock ├── msaf_dock.gd └── msaf_dock.tscn ├── msaf_logo.png ├── msaf_logo.png.import ├── msaf_part.gd ├── plugin.cfg └── plugin.gd /addons/modular_sprite_animation_factory/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 kyboon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /addons/modular_sprite_animation_factory/README.md: -------------------------------------------------------------------------------- 1 | # ModularSpriteAnimationFactory 2 | ![NeonCatIcon](https://github.com/kyboon/ModularSpriteAnimationFactory/blob/master/addons/modular_sprite_animation_factory/msaf_logo.png) 3 | 4 | A Godot 4 plugin to generate animations for modular 2d sprites. Generated animations will have multiple tracks, one for each Sprite2D node. 5 | 6 | ## Installation 7 | 1. Download the plugin from github and place the `addons` directory to your Godot project root folder. Alternatively, you can install it from the `AssetLib` in the Godot editor. 8 | 2. In the Godot editor, go to `Project` > `Project Settings` > `Plugins` and enable `Modular Sprite Animation Factory`. 9 | 10 | ## Usage 11 | 1. Prepare your sprites. Split it to different parts and convert them to white (or greyscale). Example below: 12 | 13 | Original: 14 | ![Original](https://github.com/kyboon/ModularSpriteAnimationFactory/blob/master/example/assets/character_base_16x16.png) 15 | Head: 16 | ![Head](https://github.com/kyboon/ModularSpriteAnimationFactory/blob/master/example/assets/character_base_head.png) 17 | Body: 18 | ![Head](https://github.com/kyboon/ModularSpriteAnimationFactory/blob/master/example/assets/character_base_body.png) 19 | Eyes: 20 | ![Head](https://github.com/kyboon/ModularSpriteAnimationFactory/blob/master/example/assets/character_base_eyes.png) 21 | Outline: 22 | ![Head](https://github.com/kyboon/ModularSpriteAnimationFactory/blob/master/example/assets/character_base_outlines.png) 23 | 24 | 2. Setup your nodes, it has to be a `Node2D`, **contains an `AnimationPlayer` and at least a `Sprite2D`** among its children. It's recommended to name the `Sprite2D`s accordingly. Example below: 25 | 26 | - `Node2D` 27 | - `AnimationPlayer` 28 | - `Sprite2D` 29 | - `Sprite2D` 30 | - ... more `Sprite2D`s 31 | 32 | ![image](https://github.com/kyboon/ModularSpriteAnimationFactory/assets/24255335/c5050f25-0de4-4c87-ba66-6975068d67ed) 33 | 3. Set the textures of the `Sprite2D`s with your sprites. And set the Hframes and Vframes (under the `Sprite2D > Animation` section), in the example it's a 4x4 spritesheet. 34 | 35 | ![image](https://github.com/kyboon/ModularSpriteAnimationFactory/assets/24255335/03a9659c-f43c-424b-a997-981b6bbd1a71) 36 | 37 | 4. You can now customize your character by setting different colors to each part of the sprite. To do so, in the `CanvasItem > Visibility` section, change the modulate color. You can also change that via a script. Alternatively, you can also customize your character by changing the texture. For example, you can have a `Sprite2D` node named `Hat`, and you can change the character's hat to different styles, instead of just changing the hat color. 38 | 39 | ![image](https://github.com/kyboon/ModularSpriteAnimationFactory/assets/24255335/2ad46b56-953e-4cd3-8344-af3e188a8624) 40 | ![image](https://github.com/kyboon/ModularSpriteAnimationFactory/assets/24255335/ff7a6a07-c91e-47ef-a7eb-7ee72fcfc041) 41 | 42 | 5. When you select the root Node2D, a tab will apear on the right panel, named `MSAF`. You can then manage and generate animations using it. 43 | 44 | ![image](https://github.com/kyboon/ModularSpriteAnimationFactory/assets/24255335/534c81bf-99f8-450d-8a85-a35ffd3902e2) 45 | 46 | 6. The result of the generated animation: 47 | 48 | ![image](https://github.com/kyboon/ModularSpriteAnimationFactory/assets/24255335/d2c96a0b-db67-4e16-ade0-01085408a640) 49 | 50 | -------------------------------------------------------------------------------- /addons/modular_sprite_animation_factory/assets/checkbox-blank-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kyboon/ModularSpriteAnimationFactory/a640231ba66c13791afe00312616b0fe23324977/addons/modular_sprite_animation_factory/assets/checkbox-blank-circle.png -------------------------------------------------------------------------------- /addons/modular_sprite_animation_factory/assets/checkbox-blank-circle.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://rwsj5eomxtuy" 6 | path="res://.godot/imported/checkbox-blank-circle.png-19ed1aa8d0ec722460ac308e98c5b30f.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/modular_sprite_animation_factory/assets/checkbox-blank-circle.png" 14 | dest_files=["res://.godot/imported/checkbox-blank-circle.png-19ed1aa8d0ec722460ac308e98c5b30f.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /addons/modular_sprite_animation_factory/assets/checkbox-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kyboon/ModularSpriteAnimationFactory/a640231ba66c13791afe00312616b0fe23324977/addons/modular_sprite_animation_factory/assets/checkbox-circle.png -------------------------------------------------------------------------------- /addons/modular_sprite_animation_factory/assets/checkbox-circle.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://ckdctfvm4f0t2" 6 | path="res://.godot/imported/checkbox-circle.png-3e5f3f4552812201969d5bcd3ec00ca7.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/modular_sprite_animation_factory/assets/checkbox-circle.png" 14 | dest_files=["res://.godot/imported/checkbox-circle.png-3e5f3f4552812201969d5bcd3ec00ca7.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /addons/modular_sprite_animation_factory/dock/msaf_dock.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Control 3 | 4 | var sprite_node_names 5 | var selected_sprite_nodes: Array[bool] = [] 6 | 7 | var selected_library = "" 8 | var lib_names: Array[StringName] = [] 9 | 10 | var animation_parts: Array[MSAFPart] = [] 11 | 12 | var checked_tex = preload("res://addons/modular_sprite_animation_factory/assets/checkbox-circle.png") 13 | var unchecked_tex = preload("res://addons/modular_sprite_animation_factory/assets/checkbox-blank-circle.png") 14 | 15 | var anim_player: AnimationPlayer 16 | 17 | 18 | func setup_dock(node_names, animation_player): 19 | anim_player = animation_player 20 | sprite_node_names = node_names 21 | 22 | refresh_all() 23 | 24 | func refresh_all(): 25 | update_library_list() 26 | update_sprite_node_list() 27 | update_visible_anim() 28 | update_animation_list() 29 | %ChangeNotAppliedLabel.visible = false 30 | 31 | func update_sprite_node_list(): 32 | %SpriteNodeItemList.clear() 33 | selected_sprite_nodes.clear() 34 | 35 | var existing_selected_nodes: Array[String] = [] 36 | var lib = anim_player.get_animation_library(selected_library) 37 | var has_animation = false 38 | # Check the first animation in the library, to set the default selected state of the sprite nodes 39 | if lib: 40 | if lib.get_animation_list().size() > 0: 41 | var first_anim = lib.get_animation(lib.get_animation_list()[0]) 42 | has_animation = true 43 | for i in first_anim.get_track_count(): 44 | var path = first_anim.track_get_path(i) 45 | if path.get_subname_count() > 0 and path.get_subname(0) == "frame" and path.get_name_count() > 0: 46 | existing_selected_nodes.append(path.get_name(0)) 47 | 48 | for i in sprite_node_names.size(): 49 | if !has_animation or existing_selected_nodes.has(sprite_node_names[i]): 50 | # If no animation in the library, default to true 51 | # Or existing animation contains the node track, then its true as well 52 | selected_sprite_nodes.append(true) 53 | %SpriteNodeItemList.add_item(sprite_node_names[i], checked_tex, true) 54 | else: 55 | selected_sprite_nodes.append(false) 56 | %SpriteNodeItemList.add_item(sprite_node_names[i], unchecked_tex, true) 57 | 58 | %SpriteNodeItemList.set_item_icon_modulate(i, Color.WHITE) 59 | 60 | func update_library_list(): 61 | lib_names = anim_player.get_animation_library_list() 62 | %LibraryItemList.clear() 63 | 64 | for i in lib_names.size(): 65 | var item_string: String 66 | if lib_names[i] == "": 67 | item_string = "[Global]" 68 | else: 69 | item_string = lib_names[i] 70 | 71 | if lib_names[i] == selected_library: 72 | %LibraryItemList.select(i) 73 | item_string += " (selected)" 74 | 75 | %LibraryItemList.add_item(item_string, null, true) 76 | 77 | func update_visible_anim(): 78 | animation_parts.clear() 79 | var lib = anim_player.get_animation_library(selected_library) 80 | 81 | if !lib: 82 | return 83 | 84 | var visible_anims = lib.get_animation_list() 85 | 86 | # convert all animation to MSAFPart 87 | for anim_name in visible_anims: 88 | var animation = lib.get_animation(anim_name) 89 | 90 | var new_part = MSAFPart.new() 91 | new_part.animation_name = anim_name 92 | new_part.loop = animation.loop_mode != Animation.LOOP_NONE 93 | 94 | if animation.get_track_count() > 0: 95 | var key_count = animation.track_get_key_count(0) 96 | if key_count > 1: 97 | new_part.start_index = animation.track_get_key_value(0, 0) 98 | new_part.end_index = animation.track_get_key_value(0, key_count - 1) 99 | new_part.fps = key_count / animation.length 100 | else: 101 | new_part.start_index = animation.track_get_key_value(0, 0) 102 | new_part.end_index = animation.track_get_key_value(0, 0) 103 | new_part.fps = 1 104 | else: 105 | new_part.start_index = -1 106 | new_part.end_index = -1 107 | new_part.fps = -1 108 | 109 | animation_parts.append(new_part) 110 | 111 | func update_animation_list(): 112 | %AnimationItemList.clear() 113 | 114 | for anim_part in animation_parts: 115 | var item_string = anim_part.animation_name 116 | if anim_part.loop: 117 | item_string += " (loop)" 118 | if anim_part.fps > 0: 119 | item_string += " [%d - %d]" % [anim_part.start_index, anim_part.end_index] 120 | item_string += " %.2f fps" % anim_part.fps 121 | else: 122 | item_string += " [? - ?] ? fps" 123 | 124 | %AnimationItemList.add_item(item_string, null, true) 125 | 126 | func _on_library_item_list_item_selected(index): 127 | var previously_selected_lib = selected_library 128 | 129 | if index < lib_names.size(): 130 | selected_library = lib_names[index] 131 | 132 | if previously_selected_lib != selected_library: 133 | if selected_library == "": 134 | print("MSAF Selected library: [Global]") 135 | else: 136 | print("MSAF Selected library: ", selected_library) 137 | refresh_all() 138 | 139 | func _on_sprite_node_item_list_item_selected(index): 140 | # toggle item 141 | if selected_sprite_nodes[index]: 142 | %SpriteNodeItemList.set_item_icon(index, unchecked_tex) 143 | else: 144 | %SpriteNodeItemList.set_item_icon(index, checked_tex) 145 | selected_sprite_nodes[index] = !selected_sprite_nodes[index] 146 | 147 | 148 | func _on_select_all_button_pressed(): 149 | print("MSAF select all sprite nodes") 150 | for i in sprite_node_names.size(): 151 | %SpriteNodeItemList.set_item_icon(i, checked_tex) 152 | selected_sprite_nodes[i] = true 153 | 154 | func _on_select_none_button_pressed(): 155 | print("MSAF select none sprite nodes") 156 | for i in sprite_node_names.size(): 157 | %SpriteNodeItemList.set_item_icon(i, unchecked_tex) 158 | selected_sprite_nodes[i] = false 159 | 160 | func _on_delete_anim_button_pressed(): 161 | var selected_item_indexes = %AnimationItemList.get_selected_items() 162 | if selected_item_indexes.size() > 0: 163 | var selected_index = selected_item_indexes[0] 164 | print("MSAF deleted animation: ", animation_parts[selected_index].animation_name) 165 | animation_parts.remove_at(selected_index) 166 | update_animation_list() 167 | %ChangeNotAppliedLabel.visible = true 168 | 169 | func _on_add_anim_button_pressed(): 170 | print("MSAF added new animation") 171 | 172 | if %AnimNameLineEdit.text == "" or %AnimNameLineEdit.text.contains("/"): 173 | $AcceptDialog.dialog_text = "Invalid animation name" 174 | %AcceptDialog.popup_centered() 175 | return 176 | 177 | if !%StartFrameLineEdit.text.is_valid_int() or !%EndFrameLineEdit.text.is_valid_int(): 178 | $AcceptDialog.dialog_text = "Start and end frame needs to be integer" 179 | %AcceptDialog.popup_centered() 180 | return 181 | 182 | if !%FPSLineEdit.text.is_valid_float() and float(%FPSLineEdit.text) <= 0: 183 | $AcceptDialog.dialog_text = "Invalid FPS number" 184 | %AcceptDialog.popup_centered() 185 | return 186 | 187 | var new_part = MSAFPart.new() 188 | new_part.animation_name = %AnimNameLineEdit.text 189 | new_part.loop = %LoopCheckButton.button_pressed 190 | new_part.start_index = int(%StartFrameLineEdit.text) 191 | new_part.end_index = int(%EndFrameLineEdit.text) 192 | new_part.fps = float(%FPSLineEdit.text) 193 | animation_parts.append(new_part) 194 | 195 | update_animation_list() 196 | %ChangeNotAppliedLabel.visible = true 197 | 198 | func _on_generate_button_pressed(): 199 | var lib = anim_player.get_animation_library(selected_library) 200 | if !lib: 201 | $AcceptDialog.dialog_text = "No library selected" 202 | %AcceptDialog.popup_centered() 203 | return 204 | 205 | if %OverwriteCheckBox.button_pressed: # overwrite is checked 206 | for existing_anim in lib.get_animation_list(): 207 | lib.remove_animation(existing_anim) 208 | 209 | for anim_part in animation_parts: 210 | if anim_part.fps <= 0: 211 | # invalid animation part, skip this one 212 | continue 213 | 214 | if lib.has_animation(anim_part.animation_name): # if already exist 215 | if !%OverwriteCheckBox.button_pressed: # overwrite is not checked 216 | continue # skip this one 217 | 218 | var animation = Animation.new() 219 | animation.length = float(anim_part.end_index - anim_part.start_index + 1) / anim_part.fps 220 | 221 | if anim_part.loop: 222 | animation.loop_mode = Animation.LOOP_LINEAR 223 | else: 224 | animation.loop_mode = Animation.LOOP_NONE 225 | 226 | for i in sprite_node_names.size(): 227 | if !selected_sprite_nodes[i]: 228 | # this sprite node is not selected, skip it 229 | continue 230 | 231 | # add a track for each sprite node 232 | animation.add_track(Animation.TYPE_VALUE) 233 | animation.value_track_set_update_mode(i, Animation.UPDATE_DISCRETE) 234 | animation.track_set_path(i, "%s:frame" % sprite_node_names[i]) 235 | 236 | # add a the frames to the track 237 | for j in range(anim_part.start_index, anim_part.end_index + 1): 238 | animation.track_insert_key(i, float(j - anim_part.start_index) / anim_part.fps, j) 239 | 240 | lib.add_animation(anim_part.animation_name, animation) 241 | 242 | %ChangeNotAppliedLabel.visible = false 243 | refresh_all() 244 | 245 | func _on_refresh_button_pressed(): 246 | refresh_all() 247 | -------------------------------------------------------------------------------- /addons/modular_sprite_animation_factory/dock/msaf_dock.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://dh4eckdue2vu"] 2 | 3 | [ext_resource type="Script" path="res://addons/modular_sprite_animation_factory/dock/msaf_dock.gd" id="1_0o686"] 4 | 5 | [node name="MSAF" type="Control"] 6 | layout_mode = 3 7 | anchors_preset = 15 8 | anchor_right = 1.0 9 | anchor_bottom = 1.0 10 | grow_horizontal = 2 11 | grow_vertical = 2 12 | script = ExtResource("1_0o686") 13 | 14 | [node name="ScrollContainer" type="ScrollContainer" parent="."] 15 | layout_mode = 1 16 | anchors_preset = 15 17 | anchor_right = 1.0 18 | anchor_bottom = 1.0 19 | grow_horizontal = 2 20 | grow_vertical = 2 21 | horizontal_scroll_mode = 0 22 | 23 | [node name="VBoxContainer" type="VBoxContainer" parent="ScrollContainer"] 24 | layout_mode = 2 25 | size_flags_horizontal = 3 26 | theme_override_constants/separation = 10 27 | 28 | [node name="LibraryTitle" type="Label" parent="ScrollContainer/VBoxContainer"] 29 | layout_mode = 2 30 | text = "Animation Library" 31 | horizontal_alignment = 1 32 | 33 | [node name="LibraryItemList" type="ItemList" parent="ScrollContainer/VBoxContainer"] 34 | unique_name_in_owner = true 35 | layout_mode = 2 36 | allow_reselect = true 37 | allow_search = false 38 | auto_height = true 39 | 40 | [node name="Separator0" type="ColorRect" parent="ScrollContainer/VBoxContainer"] 41 | custom_minimum_size = Vector2(0, 2) 42 | layout_mode = 2 43 | 44 | [node name="SpriteNodeTitle" type="Label" parent="ScrollContainer/VBoxContainer"] 45 | layout_mode = 2 46 | text = "Sprite2D nodes" 47 | horizontal_alignment = 1 48 | 49 | [node name="SpriteNodeItemList" type="ItemList" parent="ScrollContainer/VBoxContainer"] 50 | unique_name_in_owner = true 51 | layout_mode = 2 52 | allow_reselect = true 53 | allow_search = false 54 | auto_height = true 55 | icon_scale = 0.05 56 | 57 | [node name="HBoxContainer5" type="HBoxContainer" parent="ScrollContainer/VBoxContainer"] 58 | layout_mode = 2 59 | theme_override_constants/separation = 10 60 | 61 | [node name="SelectAllButton" type="Button" parent="ScrollContainer/VBoxContainer/HBoxContainer5"] 62 | layout_mode = 2 63 | text = "Select all" 64 | 65 | [node name="SelectNoneButton" type="Button" parent="ScrollContainer/VBoxContainer/HBoxContainer5"] 66 | layout_mode = 2 67 | text = "Select none" 68 | 69 | [node name="Separator1" type="ColorRect" parent="ScrollContainer/VBoxContainer"] 70 | custom_minimum_size = Vector2(0, 2) 71 | layout_mode = 2 72 | 73 | [node name="AnimationTitle" type="Label" parent="ScrollContainer/VBoxContainer"] 74 | layout_mode = 2 75 | text = "Animations" 76 | horizontal_alignment = 1 77 | 78 | [node name="AnimationItemList" type="ItemList" parent="ScrollContainer/VBoxContainer"] 79 | unique_name_in_owner = true 80 | layout_mode = 2 81 | allow_search = false 82 | auto_height = true 83 | 84 | [node name="DeleteAnimButton" type="Button" parent="ScrollContainer/VBoxContainer"] 85 | layout_mode = 2 86 | text = "Delete selected animation" 87 | 88 | [node name="NewAnimTitle" type="Label" parent="ScrollContainer/VBoxContainer"] 89 | layout_mode = 2 90 | text = "New animation" 91 | horizontal_alignment = 1 92 | 93 | [node name="HBoxContainer" type="HBoxContainer" parent="ScrollContainer/VBoxContainer"] 94 | layout_mode = 2 95 | 96 | [node name="Label" type="Label" parent="ScrollContainer/VBoxContainer/HBoxContainer"] 97 | custom_minimum_size = Vector2(140, 0) 98 | layout_mode = 2 99 | text = "Animation name: " 100 | 101 | [node name="AnimNameLineEdit" type="LineEdit" parent="ScrollContainer/VBoxContainer/HBoxContainer"] 102 | unique_name_in_owner = true 103 | layout_mode = 2 104 | size_flags_horizontal = 3 105 | placeholder_text = "i.e. Idle, Walk, Run, Jump, etc" 106 | 107 | [node name="HBoxContainer3" type="HBoxContainer" parent="ScrollContainer/VBoxContainer"] 108 | layout_mode = 2 109 | 110 | [node name="Label2" type="Label" parent="ScrollContainer/VBoxContainer/HBoxContainer3"] 111 | custom_minimum_size = Vector2(140, 0) 112 | layout_mode = 2 113 | text = "Loop:" 114 | 115 | [node name="LoopCheckButton" type="CheckButton" parent="ScrollContainer/VBoxContainer/HBoxContainer3"] 116 | unique_name_in_owner = true 117 | layout_mode = 2 118 | 119 | [node name="HBoxContainer2" type="HBoxContainer" parent="ScrollContainer/VBoxContainer"] 120 | layout_mode = 2 121 | 122 | [node name="Label" type="Label" parent="ScrollContainer/VBoxContainer/HBoxContainer2"] 123 | custom_minimum_size = Vector2(140, 0) 124 | layout_mode = 2 125 | text = "Start frame:" 126 | 127 | [node name="StartFrameLineEdit" type="LineEdit" parent="ScrollContainer/VBoxContainer/HBoxContainer2"] 128 | unique_name_in_owner = true 129 | layout_mode = 2 130 | size_flags_horizontal = 3 131 | placeholder_text = "int" 132 | 133 | [node name="HBoxContainer4" type="HBoxContainer" parent="ScrollContainer/VBoxContainer"] 134 | layout_mode = 2 135 | 136 | [node name="Label2" type="Label" parent="ScrollContainer/VBoxContainer/HBoxContainer4"] 137 | custom_minimum_size = Vector2(140, 0) 138 | layout_mode = 2 139 | text = "End frame:" 140 | 141 | [node name="EndFrameLineEdit" type="LineEdit" parent="ScrollContainer/VBoxContainer/HBoxContainer4"] 142 | unique_name_in_owner = true 143 | layout_mode = 2 144 | size_flags_horizontal = 3 145 | placeholder_text = "int" 146 | 147 | [node name="HBoxContainer6" type="HBoxContainer" parent="ScrollContainer/VBoxContainer"] 148 | layout_mode = 2 149 | 150 | [node name="Label2" type="Label" parent="ScrollContainer/VBoxContainer/HBoxContainer6"] 151 | custom_minimum_size = Vector2(140, 0) 152 | layout_mode = 2 153 | text = "FPS:" 154 | 155 | [node name="FPSLineEdit" type="LineEdit" parent="ScrollContainer/VBoxContainer/HBoxContainer6"] 156 | unique_name_in_owner = true 157 | layout_mode = 2 158 | size_flags_horizontal = 3 159 | placeholder_text = "float" 160 | 161 | [node name="AddAnimButton" type="Button" parent="ScrollContainer/VBoxContainer"] 162 | layout_mode = 2 163 | text = "Add new animation" 164 | 165 | [node name="Separator2" type="ColorRect" parent="ScrollContainer/VBoxContainer"] 166 | custom_minimum_size = Vector2(0, 2) 167 | layout_mode = 2 168 | 169 | [node name="ChangeNotAppliedLabel" type="Label" parent="ScrollContainer/VBoxContainer"] 170 | unique_name_in_owner = true 171 | visible = false 172 | custom_minimum_size = Vector2(100, 0) 173 | layout_mode = 2 174 | theme_override_colors/font_color = Color(1, 0.25, 0.25, 1) 175 | text = "*Changes not applied until you click on Generate" 176 | autowrap_mode = 2 177 | 178 | [node name="HBoxContainer7" type="HBoxContainer" parent="ScrollContainer/VBoxContainer"] 179 | layout_mode = 2 180 | 181 | [node name="Label" type="Label" parent="ScrollContainer/VBoxContainer/HBoxContainer7"] 182 | custom_minimum_size = Vector2(140, 0) 183 | layout_mode = 2 184 | text = "Overwrite existing?" 185 | 186 | [node name="OverwriteCheckBox" type="CheckBox" parent="ScrollContainer/VBoxContainer/HBoxContainer7"] 187 | unique_name_in_owner = true 188 | layout_mode = 2 189 | 190 | [node name="GenerateButton" type="Button" parent="ScrollContainer/VBoxContainer"] 191 | layout_mode = 2 192 | text = "Generate" 193 | 194 | [node name="RefreshButton" type="Button" parent="ScrollContainer/VBoxContainer"] 195 | layout_mode = 2 196 | text = "Refresh" 197 | 198 | [node name="AcceptDialog" type="AcceptDialog" parent="."] 199 | unique_name_in_owner = true 200 | title = "Error" 201 | 202 | [connection signal="item_selected" from="ScrollContainer/VBoxContainer/LibraryItemList" to="." method="_on_library_item_list_item_selected"] 203 | [connection signal="item_selected" from="ScrollContainer/VBoxContainer/SpriteNodeItemList" to="." method="_on_sprite_node_item_list_item_selected"] 204 | [connection signal="pressed" from="ScrollContainer/VBoxContainer/HBoxContainer5/SelectAllButton" to="." method="_on_select_all_button_pressed"] 205 | [connection signal="pressed" from="ScrollContainer/VBoxContainer/HBoxContainer5/SelectNoneButton" to="." method="_on_select_none_button_pressed"] 206 | [connection signal="pressed" from="ScrollContainer/VBoxContainer/DeleteAnimButton" to="." method="_on_delete_anim_button_pressed"] 207 | [connection signal="pressed" from="ScrollContainer/VBoxContainer/AddAnimButton" to="." method="_on_add_anim_button_pressed"] 208 | [connection signal="pressed" from="ScrollContainer/VBoxContainer/GenerateButton" to="." method="_on_generate_button_pressed"] 209 | [connection signal="pressed" from="ScrollContainer/VBoxContainer/RefreshButton" to="." method="_on_refresh_button_pressed"] 210 | -------------------------------------------------------------------------------- /addons/modular_sprite_animation_factory/msaf_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kyboon/ModularSpriteAnimationFactory/a640231ba66c13791afe00312616b0fe23324977/addons/modular_sprite_animation_factory/msaf_logo.png -------------------------------------------------------------------------------- /addons/modular_sprite_animation_factory/msaf_logo.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://byxf0djhsanrb" 6 | path="res://.godot/imported/msaf_logo.png-60acddc1a9644cdd9dd997f1d7e8732c.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/modular_sprite_animation_factory/msaf_logo.png" 14 | dest_files=["res://.godot/imported/msaf_logo.png-60acddc1a9644cdd9dd997f1d7e8732c.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /addons/modular_sprite_animation_factory/msaf_part.gd: -------------------------------------------------------------------------------- 1 | extends Object 2 | 3 | class_name MSAFPart 4 | 5 | @export var animation_name: String 6 | @export var start_index: int 7 | @export var end_index: int 8 | @export var fps: float 9 | @export var loop: bool 10 | -------------------------------------------------------------------------------- /addons/modular_sprite_animation_factory/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Modular Sprite Animation Factory" 4 | description="A plugin to generate animations for modular 2d sprites. Generated animations will have multiple tracks, one for each Sprite2D node." 5 | author="kyboon" 6 | version="1.0.1" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/modular_sprite_animation_factory/plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | const AUTOLOAD_NAME = "ModularSpriteAnimationFactory" 5 | 6 | var dock 7 | 8 | func _enter_tree(): 9 | dock = preload("res://addons/modular_sprite_animation_factory/dock/msaf_dock.tscn").instantiate() 10 | 11 | func _exit_tree(): 12 | dock.queue_free() 13 | 14 | func _handles(object): 15 | return object_is_valid_candidate(object) 16 | 17 | func _edit(object): 18 | if object: 19 | if !dock.get_parent(): 20 | add_control_to_dock(EditorPlugin.DOCK_SLOT_RIGHT_UL, dock) 21 | 22 | var sprite_children = get_sprite_2d_children(object) 23 | var anim_children = get_anim_player_children(object) 24 | dock.setup_dock(sprite_children.map(get_node_name), anim_children[0]) 25 | else: 26 | remove_control_from_docks(dock) 27 | 28 | func object_is_valid_candidate(object): 29 | # The heirachy should be as below 30 | # - Node2D 31 | # - AnimationPlayer 32 | # - Sprite2D 1 33 | # - Sprite2D 2 34 | # - ... 35 | 36 | # When selected the parent Node2D, handles it 37 | if object and object is Node2D: 38 | var children = object.get_children() 39 | return children.any(is_anim_player) and children.any(is_sprite_2d) 40 | return false 41 | 42 | func is_sprite_2d(object): 43 | return object is Sprite2D 44 | 45 | func is_anim_player(object): 46 | return object is AnimationPlayer 47 | 48 | func get_node_name(object): 49 | return object.name 50 | 51 | func get_sprite_2d_children(object: Node2D): 52 | var children = object.get_children() 53 | return children.filter(is_sprite_2d) 54 | 55 | func get_anim_player_children(object: Node2D): 56 | var children = object.get_children() 57 | return children.filter(is_anim_player) 58 | --------------------------------------------------------------------------------