I have several enemies walking around in a scene. And I’m considering adding a new type of enemy that can occasionally transform into a nearby enemy’s form and act like them, while maintaining its basic properties (HP/ATK/…). It should be able to transform back also.
Each enemy is a 2D Scene with its own Sprite and Animation Player. Let’s call the transformable enemy A and its transform object B. I can get B’s Scene, but how to temporarily replace A’s animation by B’s?
I only have some rough ideas about it. I’ve tried something like add_child(B.sprite.duplicate()) in the script of A, but the animation wasn’t playing correctly because the animation-related parameters of A and B was mixed toghther. It may be able to fix the animation by setting a series of parameters one by one. However, this enemy is not necessary, I will give it up if it’s too cumbersome. So I just come here to see if there’s any good idea.
Unfortunately, I’m not using AnimatedSprite2D and I don’t want to change that now. What’s more, the sprite sheet for each enemy type has different H/Vframes, so I can’t just update the sprite texture. The parameters of AnimationPlayer should be changed at the same time.
All enemy scenes attach to the same global class Enemy, which defines their actions. In Enemy class, the variable Enemy_save is called many times to get the enemy’s basic parameters, so the enemy scene should not be replaced entirely after transforming.
Therefore, the only method I can think of now is:
-update the Sprite texture
-set the parameters of Sprite and detecter one by one using codes like sprite.hframes = transformed_enemy_node.sprite.hframes
-set the AnimationPlayer parameters one by one using codes like animation.track_get_key_value() and track_set_key_value()
I haven’t test the last step so not sure whether it works. But this process is too cubersome and I’m afraid that if I miss any parameter it may cause more problems.
I understand that feeling. I’m not suggesting you do that. I just was making an assumption based on the information I had.
Your thinking here is one I have encountered a lot professionally in newer developers. “I got it working, so don’t mess with it.” Then, you encounter a problem like this one.
The answer, in my opinion, is to refactor your code to make it easy to do what you want. I know how time-consuming and annoying animations are, and I understand not wanting to change code you’ve spent a lot of time on.
That seems like one of the best options if you want to keep going down this road.
The reason I asked for your scene tree, was so I could suggest others. You have a lot going on. I recommend you export all the parameters you want to change as variables. Then, whenever those variables change, update the various sprites and animation players.
I’ll give you an example.
I too am tired of dealing with 2D animations. They’re fiddly. I found some 2D Chibi sprites that I had used in multiple demos I’ve come across. I liked them, but I didn’t want to load them up for every character I made. It’s a lot.
So I defined my problem. I wanted to be able to load up any sprite with one script. There are ranged and melee characters which have different attack actions (and they’re called different things). Each character has 17 different possible actions, but for some I might only need 4. The sprites were all individual. Each animation was stored in a folder named for the animation it contained.
At first I thought I wanted an import script, but that would need to be assigned to every sprite or sprite sheet. Then I realized what I wanted was a tool script. I wanted to be able to select a root folder for all the animations, and then process all the files, turning each folder into an animation in my AnimatedSprite2D.
So I created this script. Pay close attention in particular to what I’m doing after the sprite_root_folder variable declaration. I think that holds the key to your solution.
@tool
class_name ChibiAnimatedSprite2D extends AnimatedSprite2D
const IDLE = "idle"
@export_dir var sprite_root_folder:
set(value):
sprite_root_folder = value
if sprite_frames:
sprite_frames = null
if value:
_load_sprite_frames()
func _load_sprite_frames() ->void:
#if sprite_frames is null, create a new instance
if not sprite_frames:
sprite_frames = SpriteFrames.new()
#delete default if it exists
sprite_frames.remove_animation(&"default")
#iterate through folders
var dir = DirAccess.open(sprite_root_folder)
if not dir:
return
var animation_list = dir.get_directories()
for animation_dir in animation_list:
var animation_name = animation_dir.get_file().to_snake_case()
sprite_frames.add_animation(animation_name)
sprite_frames.set_animation_speed(animation_name, 24.0)
#Set looping for looping animations
if animation_name.contains(IDLE) or \
animation_name.contains("falling") or \
animation_name.contains("loop") or \
animation_name.contains("running") or \
animation_name.contains("sliding") or \
animation_name.contains("walking"):
sprite_frames.set_animation_loop(animation_name, true)
else:
sprite_frames.set_animation_loop(animation_name, false)
#change to the specific animation directory
dir.change_dir(animation_dir)
var file_list = dir.get_files()
var path = dir.get_current_dir() + "/"
#skip over .import files and uids and add the animation frames
for file in file_list:
if file.ends_with(".png"):
sprite_frames.add_frame(animation_name, load(path + file))
#drop back down a directory for the next loop
dir.change_dir("..")
#set the default animations
set_autoplay(IDLE)
animation = IDLE
This is still a work in progress. I made it last Friday night for fun. I include it because I thought it might inspire you or someone else in the future. There are always different ways to solve problems.
Now to explain the script. The @tool reference at the top of the script makes this script work in the editor. You do not need this. I added it in, because it means in the editor I can select a new folder, and all the animations will load (or change) right then. So since your solution is going to be all in code - you don’t need this. But if you decide you want to be able to update your exported variables and see changes in the editor, you can make it as tool script. It’ll still work normally in the game too.
@export_dir var sprite_root_folder:
set(value):
sprite_root_folder = value
if sprite_frames:
sprite_frames = null
if value:
_load_sprite_frames()
I’m creating an export variable that specifically loads a directory. On the second line, I’m creating a Setter. This says whenever someone assigns a value to this variable, I want to do extra stuff. The first thing you do is set the variable equal to the value. But then you can do other stuff.
The first thing I’m doing is checking the sprite_frames variable. This is an AnimatedSprite2D variable. That if statement says “If there’s anything in this variable.” Another way of saying, “If it’s not null”. Then I want to clear it out and make it null. This is so I don’t try and keep old animations if I’ve switched my sprite.
Then I’m checking to make sure the value passed isn’t null. If it is I do nothing. If it isn’t, I load the folder specified using my _load_sprite_frames() function. The reason for this check is if I hit the reset button in the editor to clear the folder, I want all the frames erased, but I don’t want to try loading a null value.
So now we come back around to your problem. Again, this is based on the information you gave me, so you’ll have to modify it to make it work. I’m writing this assuming you’d put it on the Skeleton node because that’s where all your code is now.
@onready var sprite: Texture2D = $Sprite
@onready var animation_player: AnimationPlayer = $AnimationPlayer
@export var main_sprite: Texture2D:
set(value):
main_sprite = value
sprite.texture = main_sprite
sprite.hframes = transformed_enemy_node.sprite.hframes
func reset_animation_player()
animation_player = $AnimationPlayer
So when you want to set the sprite, you set that value. When you want to set the AnimationPlayer, just give it a new one, and reset it when you’re done.
Again, I get you not wanting to restructure your code, but I would recommend you look at how you might to make this a little easier for yourself in the future. For example, you could also just make a variable @export var shapeshift_appearance: Enemy or something and assign the enemy you want to use and then pull the data out of it you need.
Thank you so much for your detailed and thoughtful reply! It’s very helpful and insightful. I haven’t think of the Setter before, definitely will give it a try.