Create a Texture out of multiple sprites

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By gcardozo

I’m creating a dash effect, you can see it here:
https://twitter.com/gcardozo_/status/1069312705583112192

I have a modular character (head is separated from its body). Is there a way to create a new Texture containing the whole character with the correct scale and position of all modular parts?

Why bake the trail of the modular character into single sprites?

iron_weasel | 2018-12-03 01:30

I’d prefer put all parts of the character into single sprites. Let’s say the character has: a head, a body, arms and legs, all in separate sprites. I’d like to create a single Texture (like a snapshot) of the character so I could put it into a single Spriteinstead of using multiple unnecessary sprites.

gcardozo | 2018-12-03 08:59

I see you simply want to draw your character multiple times with different alpha. I wish this was as simple as just calling a draw_character function as many times as needed without having to bake anything, but using the built-in sprite rendering you get sprites drawn only once…

Perhaps you could actually do this using custom _draw? When you want the trail to happen, gather textures, current frame and offsets of all your character sprites, and use _draw to draw them extra times procedurally to form the trail (hell I can even imagine this could be a plugin :D).

You could achieve the same by cloning all visual nodes of your character as many times as needed, though a bit less efficient.

It is also possible to bake a texture like you said, but it involves an offscreen viewport and waiting one frame of rendering, download the texture back and draw it, which seems more inconvenient.

Zylann | 2018-12-03 13:59

:bust_in_silhouette: Reply From: iron_weasel

OK man. It took me some work but I think I have an answer. First things first, based on my understanding of Godot you don’t really need (or want) to create a texture for this. What you want is use custom drawing. Here is my solution.

extends Node2D

var visual_length = 4
var time_gap = 0.2
var sprite_nodes = []
var trail_elements = []

func _ready():
	var open_nodes = [$'..']
	while open_nodes:
		var n = open_nodes.pop_front()
		open_nodes = open_nodes + n.get_children()
		if n is AnimatedSprite:
			sprite_nodes.append(n)
	_add_to_trail()

func _process(delta):
	self.global_position = Vector2(0,0)

func _draw():
	for te in trail_elements:
		for s in te:
			self.draw_set_transform(s[0][0], s[0][1], s[0][2])
			self.draw_texture(s[1], s[2] + s[1].get_size() * -0.5)

func _add_to_trail():
	while true:
		var te = []
		for n in sprite_nodes:
			var n_pos = n.global_position
			var n_rot = n.global_rotation
			var n_scale = n.global_scale
			n_scale.x = n_scale.x * (-1 if n.flip_h else 1)
			var s_texture = n.frames.get_frame(n.animation, n.frame)
			var s_offset = n.offset
			te.append([[n_pos, n_rot, n_scale], s_texture, s_offset])
		trail_elements.append(te)
		if trail_elements.size() > visual_length:
			trail_elements.pop_front()
		update()
		yield(get_tree().create_timer(time_gap), "timeout")

I will try and explain what is going on.

func _ready():
	var open_nodes = [$'..']
	while open_nodes:
		var n = open_nodes.pop_front()
		open_nodes = open_nodes + n.get_children()
		if n is AnimatedSprite:
			sprite_nodes.append(n)
	_add_to_trail()

In the ready function I scan the parent for all the nodes that are AnimatedSprites and make the list of them for later. I also start a coroutine for adding to the trail.

func _process(delta):
	self.global_position = Vector2(0,0)

The process function is real simple, it just keeps the trail node at the world origin so the trail doesn’t move.

		var te = []
		for n in sprite_nodes:
			var n_pos = n.global_position
			var n_rot = n.global_rotation
			var n_scale = n.global_scale
			n_scale.x = n_scale.x * (-1 if n.flip_h else 1)
			var s_texture = n.frames.get_frame(n.animation, n.frame)
			var s_offset = n.offset
			te.append([[n_pos, n_rot, n_scale], s_texture, s_offset])
		trail_elements.append(te)

The important part here is that each trail_element is a list of textures and their positions, rotations and scales.

	update()

Every time I update the trail I need to call update so the draw function gets called by the engine.

func _draw():
	for te in trail_elements:
		for s in te:
			self.draw_set_transform(s[0][0], s[0][1], s[0][2])
			self.draw_texture(s[1], s[2] + s[1].get_size() * -0.5)

I think this is pretty self explanatory. It loops through all the textures in each trail element. Sets a draw position then draws the texture. There a trail!

Hopefully this gets you started, but it might not cover all your needs. A few things to note:

  1. I am only supporting animated sprite, but you can expand.
  2. I am not doing anything fancy with drawing such as transparency.
  3. I am only supporting centered sprites.
  4. I am only supporting horizontal flipping.
  5. I am not being at all smart with draw order. This might be a big problem for you so you’ll need to figure out how you want to handle that.

Hey thx!
I didn’t have time to try your scripts yet, but I’ll do it! :slight_smile:

gcardozo | 2018-12-06 08:56

Hey, your script helped me to figure out some things and I ended up tweaking my own version. It works for modular characters with multiple sprites, it changes the character speed and calls owner.set_input_enabled(...) to remove/give back character input.

Here it is:

extends Node

# The owner of this Dash node needs to have the following methods:
# func set_input_enabled(enabled:bool)
# func get_speed()
# func set_speed(speed_scalar)
# func get_current_sprites() -> List[Sprite]

export (NodePath) var owner_path
var owner_body
var owner_speed

# list of list: each list contains all necessary sprites to represent a ghost
var ghosts = [] 
var tween

func _ready():
    owner_body = get_node(owner_path)
    var num_sprites = owner_body.get_current_sprites().size()
    for i in range(5): # 5 ghost trails
        var sprites = []
        for j in range(num_sprites): 
            var s = Sprite.new()
            s.set_visible(false)
            add_child(s) 
            sprites.append(s)
        ghosts.append(sprites)
    
    tween = Tween.new()
    add_child(tween)

func dash():
    owner_body.set_input_enabled(false)
    owner_speed = owner_body.get_speed()
    owner_body.set_speed(owner_speed * 3)

    var timer = Timer.new()
    timer.connect("timeout", self, "_return_input_to_owner") 
    add_child(timer) 
    _start_ghost_tweens()
    timer.start(0.25)
    timer.set_one_shot(true)

func _start_ghost_tweens():
    for i in range(ghosts.size()): # ghost trails
        yield(get_tree().create_timer(0.05), "timeout")
        
        for j in range(ghosts[i].size()): # ghost parts (head, body, etc)    
            var owner_sprites = owner_body.get_current_sprites() 
            var ghost_part = ghosts[i][j]

            ghost_part.set_scale(owner_body.global_scale)
            ghost_part.set_position(owner_sprites[j].global_position)
            ghost_part.set_texture(owner_sprites[j].get_texture())
            ghost_part.set_rotation(owner_sprites[j].global_rotation)
            ghost_part.flip_h = owner_sprites[j].flip_h
            ghost_part.set_visible(true)
            
            tween.interpolate_property(
                ghost_part, 
                "modulate", 
                Color(1, 1, 1, 1), 
                Color(1, 1, 1, 0), 
                0.25, 
                Tween.TRANS_LINEAR, 
                Tween.EASE_IN
            )
            if not tween.is_connected("tween_completed", self, "_on_complete_ghost_tween"):
                tween.connect("tween_completed", self, "_on_complete_ghost_tween") 
            tween.start()
        
func _on_complete_ghost_tween(object, key):
    object.set_visible(false)

func _return_input_to_owner():
    owner_body.set_speed(owner_speed)
    owner_body.set_input_enabled(true)

The owner has:

func set_input_enabled(enabled):
    _input_enabled = enabled

func get_current_sprites():
    var head = get_node("YSort/Node2D/Head")
    var body = get_node("YSort/Node2D/Body")
    return [head, body] 

Note:
it might be a good idea to separate the trail effect from the dash logic (the part that removes character input and changes its speed), but for now it does what I need.

gcardozo | 2018-12-15 13:06