Understanding ONE_BY_ONE Attacks

All your Projectiles have the ability to spawn with multiple instances.

In conjunction with this, there is a property called spawn_interval that allows you to set a fixed time interval between each instance generation.

The biggest drawback is that all these instances are created at the very moment they are requested, and are only activated after a period of time to create this “illusion".

And while this behavior is intentional, it can create certain unexpected results on moving objects.

To solve this, we can rely on Attacks.


Overwriting your requested parameters

Continuing with our previous project, we can see that this problem also applies to our bow attack.

Your Attack Blueprints have a section dedicated entirely to the positioning of your attacks.

When you activate position overwriting, you are telling the Attack to no longer obey the requested positions and to use its ProjectileManager2D position instead.

After a brief alteration to our Player's script, our problem is no more.

func _process(_delta: float) -> void:
	var direction: Vector2 = (get_global_mouse_position() - position).normalized()
	bow.position = direction * separation
	bow.rotation = direction.angle()
	
	# New code
	projectile_manager.position = bow.position
	projectile_manager.rotation = bow.rotation

You can also overwrite the direction of your attacks:

Checking this option causes the Attack to ignore the given requested destinations and to use its ProjectileManager2D right direction instead. See Transform2D.x.

At the end of this exercise, your Player's script should look something like this:

extends Node2D

@export var speed: float = 300
@export var separation: int = 30

@onready var projectile_manager: ProjectileManager2D = $ProjectileManager2D

@onready var bow: Sprite2D = $Bow
@onready var animation_player: AnimationPlayer = $AnimationPlayer


func _ready() -> void:
	animation_player.play("idle")
	projectile_manager.get_attack(0).set_on_charge_enter(on_charge_enter).set_on_charge_exit(on_charge_exit)


func _process(_delta: float) -> void:
	var direction: Vector2 = (get_global_mouse_position() - position).normalized()
	bow.position = direction * separation
	bow.rotation = direction.angle()
	
	projectile_manager.position = bow.position
	projectile_manager.rotation = bow.rotation

	if (Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)):
		projectile_manager.request_execution(0, 0, position + (direction * separation), get_global_mouse_position())
	
	var input_direction: Vector2 = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
	position += input_direction * speed * _delta


func on_charge_enter(attack: Attack2D) -> void:
	animation_player.play("charge")
	attack.charge_enter()


func on_charge_exit(attack: Attack2D) -> void:
	animation_player.play("idle")
	attack.charge_exit()

ONE_BY_ONE Attacks

So there is actually a way to prevent your Projectiles from being created all at once. In fact, your Attacks have an entire section dedicated to that.

When you set your attacks to ONE_BY_ONE, your projectiles will be generated, well, one by one.

  • This means that you can interrupt your attack before it is fully completed.
  • Continue your attack from where it was interrupted.
  • Or force your players to reload before they can attack again.
func _input(event):
	if event is InputEventMouseButton:
		if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
			projectile_manager.get_attack(0).reload(projectile_manager.get_projectile(0))