0 votes

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?

in Engine by (30 points)

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

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.

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.

1 Answer

+2 votes

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.
by (116 points)

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

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.

Welcome to Godot Engine Q&A, where you can ask questions and receive answers from other members of the community.

Please make sure to read Frequently asked questions and How to use this Q&A? before posting your first questions.
Social login is currently unavailable. If you've previously logged in with a Facebook or GitHub account, use the I forgot my password link in the login box to set a password for your account. If you still can't access your account, send an email to [email protected] with your username.