How To Program A Triple Shot Upgrade in a 2D Top Down Shooter

Godot Version

Godot 4.2

Question

**Title says all. How would I go about coding an upgrade where after the player character picks up a specific item, they temporarily get the ability to fire 3 shots. One shot would fly forward and the other two would fly in a diagonal manner.

Some specifics I’ll note is that my game uses a top down perspective and for shooting, it uses the current mouse position on the screen and shoots whenever I press the left mouse button.

I can also post what I’ve coded so far regarding the shooting controls and powerups if it would be helpful regarding what code I can add in and where. Thanks!**

I aim to manage the number of additional shots dynamically.
For instance, if I decide to offer 5 additional shots instead of 3, I would utilize a variable named shots_remaining and adjust its value within the pickup_powerup() function.
Simultaneously, I’d implement a timer mechanism to reset shots_remaining back to 1 upon its expiration, which would be handled by the reset_powerup() function. Within the shoot() function, I’d instantiate bullet instances corresponding to the current shots_remaining value, adjusting their directions accordingly for diagonal shooting angles.

I see, if possible, do you know of a code that can be used to apply the diagonal shooting angles to the 2 bullets that are meant to travel in diagonal directions?

Kinda new to Godot, but the Vector2 class supports inputs in polar coordinates. I presume you have the mouse x and y in a Vector2 instance. Using the Vector2’s rotated() function, you could take the vector to the mouse, spawn a bullet, rotate by, say, pi/4 radians, spawn a bullet, rotate the mouse vector by -pi/4 radians, spawn a bullet, all in one frame. I haven’t tested it, but that should work.

As I see it:

bullet_instance.global_rotation = global_rotation # for one bullet

# for 3 bullets:

for i in tree_bullets:
    var bullet_instance = bullet.instantiate()
    var inc = (i - 1)* 0.5 # 0.5 rad I guess - not sure rad or deg here
    bullet_instance.global_rotation = (
        global_rotation + inc
    get_tree().root.call_deferred('add_child', bullet_instance)

I never tested it in godot. This is the way I would do it.

All angles are measured in rad in Godot

Hmm, would you mind if I share the code of my Bullet Scene, Bullet Manager Scene, and Player Scene? I feel like showing what I have coded so far might help out here.

Don’t ask to ask, just ask :grinning:

Alright, so here’s the code for my Bullets:

extends Area2D

var speed : int = 600
var direction : Vector2

# Called every frame. 'delta' is the elapsed time since the previous frame. 
#Bullet Physics
func _process(delta):
	position += speed * direction * delta
	

func _on_timer_timeout():
	queue_free()

#Code for killing enemies with bullets
func _on_body_entered(body):
	if body.name == "World":
		queue_free()
	else:
		if body.alive:
			body.die()
			queue_free()

Here’s the code for my Bullet Manager, the scene which handles my bullets in the game:

extends Node2D

#Used to get the scene for bullets
@export var bullet_scene : PackedScene


func _on_player_shoot(pos, dir):
	var bullet = bullet_scene.instantiate()
	add_child(bullet)
	bullet.position = pos
	bullet.direction = dir.normalized()
	bullet.add_to_group("bullets")

Lastly, here’s the code for my player, the controls and powerups are handled here:

extends CharacterBody2D

signal shoot

@onready var hurtblink = $HurtBlink
@onready var hurttimer = $HurtTimer

const START_SPEED : int = 200
const BOOST_SPEED : int = 400
const NORMAL_SHOT : float = 0.5
const FAST_SHOT : float = 0.1
var speed : int
var screen_size : Vector2
var can_shoot : bool

func _ready():
	screen_size = get_viewport_rect().size
	reset()
	hurtblink.play("RESET")
	
func reset():
	can_shoot = true
	position = screen_size/2
	speed = START_SPEED
	$ShotTimer.wait_time = NORMAL_SHOT
	

# Called when the node enters the scene tree for the first time.
func get_input():
	var input_dir = Input.get_vector("left", "right", "up", "down")
	velocity = input_dir.normalized() * speed
	
#Handle shooting when pressing left mouse button
	if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) and can_shoot:
		var dir = get_global_mouse_position() - position
		shoot.emit(position, dir)
		$Shoot.play()
		can_shoot = false
		$ShotTimer.start()
	
	if Input.is_action_pressed("pause"):
		get_tree().paused = true
		$PauseMenu.visible = true

func _physics_process(_delta):
	get_input()
	move_and_slide()
	
	#limit movement to window size
	position = position.clamp(Vector2.ZERO, screen_size)
	
	var mouse = get_local_mouse_position()
	var angle = snappedf(mouse.angle(), PI / 4) / (PI / 4)
	angle = wrapi(int(angle), 0, 8)
	
	$AnimatedSprite2D.animation = "walk" + str(angle)
	
	if velocity.length() != 0:
		$AnimatedSprite2D.play()
	else: 
		$AnimatedSprite2D.stop()
		$AnimatedSprite2D.frame = 1

#Code for when player picks up a speed powerup
func boost():
	$BoostTimer.start()
	$CoffeePickup.play()
	speed = BOOST_SPEED

#Code for when player picks up a firing speed powerup
func quick_fire():
	$FastFireTimer.start()
	$GunPickup.play()
	$ShotTimer.wait_time = FAST_SHOT

#Code for when player picks up an extra life, rest of code is handled in the main game scene
func extralife():
	$LifePickup.play()

func on_hit():
	hurtblink.play("hurtblink")
	hurttimer.start()
	await hurttimer.timeout
	hurtblink.play("RESET")

func _on_shot_timer_timeout():
	can_shoot = true

func _on_boost_timer_timeout():
	speed = START_SPEED


func _on_fast_fire_timer_timeout():
	$ShotTimer.wait_time = NORMAL_SHOT


func _on_resume_button_pressed():
	get_tree().paused = false
	$PauseMenu.visible = false


func _on_quit_button_pressed():
	get_tree().change_scene_to_file("res://scenes/title_screen.tscn")

Let me know if you have any questions. Thanks!

looks like you need to rotate a normalize direction to an angle to ±30° for example. Choose your own degree.
instantiate more 2 bullets and set direction like code below:

second_bullet.vec.rotate(deg2rad(-30))
third_bullet.vec.rotate(deg2rad(30))

and add them as children

Alright, would I need to add this code to my Bullet Manager script?

The Bullet Manager script would be the best place to put it. The best way I could think of implementing this is something like this: Add an argument to the Shoot signal called bullet number or something like that. Then in the Bullet Manager, check the number of bullets, and add that many bullets. The benefit of this is if you wanted to add a rare 5-shot power up, you could. The code might look something like this:

func set_bullet():
	var bullet = bullet_scene.instantiate()
	add_child(bullet)
	bullet.position = pos

func _on_player_shoot(pos, dir, num):
	set_bullet()
	bullet.direction = dir.normalized()
	bullet.add_to_group("bullets")
	if num == 3
		set_bullet()
		bullet.direction = dir.rotated(1/6 * pi).normalized()
#directions in godot use radians and 1/6 pi radians is 30 degrees
#you can change this number if you want.
		bullet.add_to_group("bullets")
		set_bullet()
		bullet.direction = dir.rotated(-1 * 1/6 * pi).normalized()
		bullet.add_to_group("bullets")

I haven’t tested this, though.