I'm trying to make a dash component and

Godot Version 4.7 stable

Hello there, I’m back with another easy to solve (I hope) question, I’m trying to make a dash component, purely as a script, to be attached both in the tree scene of any entity (including the player character) and inside the code of said entity. My question is ¿How do I put a timer inside the script to both put pauses between the dashes and to recharge the dashes (there can be two different timers) without making it a scene? ¿Is it possible to do? or ¿Do I need to do it inside the scene tree of the entity?

Thanks.

You can very easily create Timer nodes as part of a script. You can also define a custom class that is responsible for the Dash logic specifically so that you can then instantiate it in any other components as needed.

@tool
class_name Dash
extends Node
## Defined as a tool script so that Timer nodes can be created while running in
## the inspector.

@export var pause_time: float = 0.0:
    set(value):
        pause_time = value
        if is_node_ready() and _pause_timer != null:
            _pause_timer.wait_time = pause_time
        
@export var recharge_time: float = 0.0:
    set(value):
        recharge_time = value
        if is_node_ready() and _recharge_timer != null:
            _recharge_timer.wait_time = recharge_time

var _pause_timer: Timer = null
var _recharge_timer: Timer = null


func _ready() -> void:
    if _pause_timer== null:
        _pause_timer = _create_timer(pause_time)
    if _recharge_timer == null:
       _recharge_timer = _create_timer(recharge_time)


func _init(new_pause_time: float, new_recharge_time: float) -> void:
    pause_time = new_pause_time
    recharge_time = new_recharge_time


func _create_timer(wait_time: float) -> Timer:
    var new_timer := Timer.new()
    add_child(new_timer)
    new_timer.wait_time = wait_time
    if Engine.is_editor_hint():
        new_timer.set_owner(get_tree().edited_scene_root)
    return new_timer

You can then create a new Dash node anywhere. You will need to add it to the scene tree so that the _ready function is called.

func add_dash() -> void:
    var dash := Dash.new(1.0, 1.0)
    add_child(dash)

You would still need to connect the timer signals and add logic for handling user input.
Not sure if this answers all of your questions, but I hope this is a useful starting point.

Well, sorry for the late response, I’ve been busy and breaking my noggin against this issue, so…

I’ve done something, but I have 2 issues with it, 3 if I’m to nitpick:

1.- The method I’ve implemented doesn’t work, it does everything except dash the character, I think I implemented wrong the math inside the character, it does everything else somewhat good.

2.- The recharge of dashes occurs all at once, instead of one by one, don’t know why.

3.- I have a bajillion of variables, both exported and not, and I’m open to optimization of the code.

Without further ado, here are the blocks of code:

the dash component

class_name Dash extends Node

@export var character_capable_of_dash: CharacterBody2D
@export var dash_force:                float
@export var duration_time:             float
@export var time_between_dashes:       float
@export var cooldown_of_dashes:        float
@export var max_dash_charges:          int

var has_cooldown_finished:             bool    = true
var does_a_dash:                       bool    = false
var is_dashing:                        bool    = false 
var can_dash:                          bool    = false
var dash_direction:                    Vector2 = Vector2.ZERO
var pause_between_timer:               Timer   = null
var duration_of_aplication_timer:      Timer   = null
var recharger_dashes_timer:            Timer   = null
var current_dashes:                    int:  
	set(new_value):
		current_dashes = maxi(0,new_value) 

func _ready():
	if character_capable_of_dash == null:
		return
	if pause_between_timer == null:
		pause_between_timer = create_timer(time_between_dashes)
	if duration_of_aplication_timer == null:
		duration_of_aplication_timer = create_timer(duration_time)
	if recharger_dashes_timer == null:
		recharger_dashes_timer = create_timer(cooldown_of_dashes)

#                                     -> is the return tipe of the data
func create_timer( wait_time: float ) -> Timer:
	var new_timer := Timer.new()
	add_child(new_timer)
	new_timer.wait_time = wait_time
	return new_timer

func make_a_dash(_delta:float) -> void:
	if is_dashing == true and current_dashes <= max_dash_charges and has_cooldown_finished == true:
		check_for_dashes(_delta)
		does_a_dash = true
		check_for_pauses(_delta)
		print("I dashed")
		current_dashes += 1
		dash_direction = dash_direction * dash_force
		duration_of_aplication_timer.start()
		await duration_of_aplication_timer.timeout
		does_a_dash = false
	return

func check_for_dashes(_delta:float) -> void:
	if current_dashes == 0:
		return
	else:
		while current_dashes != 0:
			recharger_dashes_timer.start()
			await recharger_dashes_timer.timeout
			current_dashes -= 1
			print("One Dash Recharged")
			

func check_for_pauses(_delta:float) -> void:
	if does_a_dash == true:
		has_cooldown_finished = false
		await duration_of_aplication_timer.timeout
		pause_between_timer.start()
		await  pause_between_timer.timeout
		print("Cooldown finished")
		has_cooldown_finished = true

the character code

class_name QuackyMainScript extends CharacterBody2D
#The child nodes inside
@onready var player_input = $PlayerInput
@onready var player_movement = $PlayerMovement
@onready var dash = $Dash
#A way to use the sprite to get better control, 
#in this case so that the arm follows the mouse and for the flip in sprite
@export  var Body: Sprite2D
@export  var Arm:  Sprite2D

#what happens every tick inside the program, generally 60 ticks each second
func _physics_process(delta:float) -> void:
	#executes the functions in blue each tick (aprox fps)
	player_input._update()
	player_movement.tick(delta)
	dash.make_a_dash(delta)
	#conects input to velocity in Vector2
	player_movement.direction = player_input.dir_movement
	dash.is_dashing = player_input.dash
	player_movement.direction = player_movement.direction + dash.dash_direction
	
	#makes the arm track the mouse and rotates it so that it looks good
	var mouse_position = get_global_mouse_position()
	Arm.look_at(mouse_position)
	Arm.rotation += deg_to_rad(270)
	#flips the sprite so that it looks normal
	if Body.global_position.x < mouse_position.x:
		Body.flip_h = true
	else:
		Body.flip_h = false

movement component

class_name PlayerMovement extends Node
#it defines the character so its always != null
@export_subgroup("Character")
@export var Player:           CharacterBody2D
@export var Body:             Sprite2D
#our variables to adjust for the best feeling movement
@export_subgroup("Horizontal Movement")
@export var max_speed:        float
@export var acceleration:     float
@export var friction:         float
#to match with input component and other components
var direction: Vector2 = Vector2.ZERO

#what it will send
func tick(delta : float) -> void:
	#return check to avoid crashes
	if Player == null:
		return
	#When there is input being pressed, all in Vector2 format
	if direction != Vector2.ZERO:
		var target_velocity = Vector2(direction * max_speed)
		Player.velocity.x = move_toward(Player.velocity.x,target_velocity.x, acceleration * delta)
		Player.velocity.y = move_toward(Player.velocity.y,target_velocity.y, acceleration * delta)
	#When there is no input, as to simulate momentum and reduce velocity
	else:
		Player.velocity.x = move_toward(Player.velocity.x, 0 , friction * delta)
		Player.velocity.y = move_toward(Player.velocity.y, 0, friction * delta)
	
	Player.move_and_slide()

and input component

class_name PlayerInput extends Node

#variables for each button, meaning the variable called for each
#input, becoming true when interacted as described

#gets a vector from 0 to 1, to connect to the movement component
var dir_movement:    Vector2 = Vector2.ZERO
#to connect to each component, from dashing to shooting and reloading
var start_shooting:  bool = false
var stop_shoting:    bool = false
var start_reloading: bool = false
var dash:            bool = false

#it reads every tick if there was a change, in other words, each time 
#it updates a change from false to true and viceversa it emits signal that it happened
func _update():
	dir_movement = Input.get_vector("move_left","move_right","move_up","move_down")
	start_shooting = Input.is_action_just_pressed("shoot")
	stop_shoting = Input.is_action_just_released("shoot")
	start_reloading = Input.is_action_just_pressed("reload")
	dash = Input.is_action_just_pressed("dash")

Anyway, thanks for reading this beast of spaghetti code!

Your code is extremely over complicated.

Describe exactly how is the dash mechanic supposed to work from the player perspective.

Ok, so it goes like this:

1.- The player will have the ability to dash, which means he will experience an additional velocity (var dash_force)for a limited amount of time, that would be the timer duration_of_aplication_timer applied to the character in player_movement.direction = player_movement.direction + dash.dash_direction in the character script.

2.- The act of dashing has charges, meaning there is a maximum number of dashes that the player can do, however, the dashes recharge with time (it can be imagined as a primitive stamina bar) that would be max_dash_charges and handled in func check_for_dashes()

3.- If the player has multiple dashes stored, they can’t be fired continuously, so the dash ability has a cooldown period, short, but is there. This is handled inside func check_for_pauses()

That would be the short of it, and of course I’m trying to do it as a component because I don’t like overly long code (Ironically enough).

Also, this is done with a momentum based movement component.

So, with a little more investigation I’ve made something that works, maybe a little more complicated than it should be, but serviceable for a component that can accommodate both PC’s and NPC’s as a component node for the dash in GDScript:

class_name Dash extends Node

#I want dash that can be upgraded, and has a cooldown

#for the check of early returns
@export var capable_of_dashing: CharacterBody2D

#This are the variables for the dash itself
@export var dash_speed:    float
@export var dash_duration: float
@export var max_dashes:    int

#This are the variables for the recharge and the cooldown of dashing
@export var dash_cooldown:      float
@export var dash_recharge_time: float

#this are the variables that control the process, including the new timers
var dash_timer:     Timer
var cooldown_timer: Timer
var recharge_timer: Timer
var can_dash:       bool = true
var is_dashing:     bool = false
var current_dashes: int:
set(new_value): current_dashes = maxi(0,new_value)

#the ready function, to create the timers inside the code
func _ready():
 if capable_of_dashing == null:
  return
 if dash_timer == null:
  dash_timer = create_timer(dash_duration)
 if cooldown_timer == null:
  cooldown_timer = create_timer(dash_cooldown)
 if recharge_timer == null:
  recharge_timer = create_timer(dash_recharge_time)

#function to create timers as is needed, less code if necessary to add more
func create_timer( wait_time : float) → Timer:
 var new_timer: Timer = Timer.new()
 add_child(new_timer)
 new_timer.wait_time = wait_time
 return new_timer

#what can put the can_dash variable in false
func conditions_of_dash(_delta:float):

#the function to restore dashes after some time has passed
 if current_dashes == 0:
  return
 else:
 recharge_timer.start()
 await recharge_timer.timeout
 current_dashes -= 1

#The check for dashes in storage
if current_dashes >= max_dashes:
 can_dash = false

#the function of pauses between dashes
if is_dashing == true:
 can_dash = false
 await dash_timer.timeout
 cooldown_timer.start()
 await cooldown_timer.timeout
 can_dash = true

#The dash, finally
func dashing(_delta:float):
 current_dashes += 1
 conditions_of_dash(_delta)
 dash_timer.start()
 await dash_timer.timeout
 is_dashing = false

Now, there are some caveats with this design:

1.- The conditional for can_dash and is_dashing is outside this code, as I’ve used it to connect the function dashing to the player character, so you still have to put the if statement in the character’s code, instead of in the dash component

dash.is_dashing = player_input.dash

	if dash.is_dashing == true and dash.can_dash == true:
		player_movement.max_speed = player_movement.max_speed + dash.dash_speed
		await dash.dashing(delta)
		player_movement.max_speed = player_movement.max_speed - dash.dash_speed

2.- There is no implementation inside the code to ignore gravity, cancel/storing inputs while dashing, and if coded incorrectly (like using acceleration or gravity instead of max_speed It can absolutely tank the performance, that’s the why of the if statement in the player script.

anyhow, thanks for all who commented and I hope this is usefull for those who are begginers, just like me.

This is not a description from the player perspective. The player doesn’t know anything about variables and functions in your code. You even didn’t mention does the player need to hold the action for the entire duration of a dash or it’s instantly triggered and then lasts for some predetermined time.

You still store way too much state than needed, have strangely named functions, keep pieces of dashing logic outside of the dash component, and use multiple awaits (which will likely produce sneaky bugs down the line).

Here’s an example of the whole logic without awaits and redundant state keeping. Call try_dash() whenever the dash action is pressed. Each frame test if dashing in the movement code and if true add the dashing vector to the velocity. This can be done either in player code or in the component itself.

class_name Dash extends Node

@export var dash_speed := 10.0
@export var dash_count_max := 5
@export var replenish_rate := 0.5
@export var dash_duration := 1.0
@export var cooldown_duration := 1.0

var dashing := 0.0 # goes from 1.0 to 0.0, nonzero value means we're currently dashing
var cooldown := 0.0 # goes from cooldown duration to 0.0
var dash_count: float


func _ready() -> void:
	dash_count = dash_count_max


func _process(delta: float) -> void:
	if not dashing:
		dash_count = min(dash_count + replenish_rate * delta, dash_count_max) 
		cooldown = max(cooldown - delta / cooldown_duration, 0.0)
		return
	dashing = max(dashing - delta / dash_duration, 0.0)
	if not dashing:
		cooldown = cooldown_duration


func try_dash() -> void:
	if dashing <= 0.0 and cooldown <= 0.0 and dash_count >= 1.0:
		dash_count -= 1.0
		dashing = 1.0