Help With Projectile Collisions

Godot Version

3.5.3.stable

Question

Hello!

This week I have been trying to get a projectile to collide with other objects and play an animation when colliding.

I have a projectile scene that is preloaded to a turret which then creates an instanced projectile on the world node. Here is what I am working with:


For the projectile:
Screenshot 2024-04-22 183243

And the code for the projectile:

extends Node2D

onready var animationTree = $AnimationTree
onready var animationState = animationTree.get("parameters/playback")

var SPEED = 0

func bulletTravel(delta):
	animationState.travel("Bullet")
	SPEED = -750
	position += transform.y * SPEED * delta

func _physics_process(delta):
	bulletTravel(delta)

For the turret:
Screenshot 2024-04-22 183147

And the code for the turret:

func shoot():
	if Input.is_action_pressed("Shoot"):
		get_tree().get_root().add_child(smallBullet)
		smallBullet.transform = $Muzzle.global_transform

Firstly, how should I make the projectile collide. My thought is to use a Area2D signal (area_entered perhaps?). I was messing around with something like:

func _on_Armor_Hit_area_entered(area):
	SPEED = 0
	animationState.travel("Armor Hit")

This registers the projectile “colliding” and makes the projectile change to the animation needed, but does not stop it from moving. It also only works with the first projectile (I suspect it just gets stuck on an empty frame).

I also need to somehow register a separate hit for a “Shield Hit” after this and also free the projectile after a certain amount of time.

I was also thinking maybe have the area_entered signal to set a bool to true and then have another function that uses that bool to go off of.

How do you think I should handle this? Do I need to rethink my approach? Is there a way to work with what I have?

Thanks!

3 Likes

firstly your bullet doesn’t stop moving because the bulletTravel function sets the speed value to -750 even though you set it to zero. you can fix this by adding a hit variable that is true when the projectile is hit and you will need to put your bulletTravel function in an if statement that runs when the hit variable is false. also set the hit variable to true in the _on_amour_hit_area_entered signal.

I’m not sure why this only works on the first bullet but i hope this helps :wink:

1 Like

I completely forgot that it was being set to -750 constantly from func _physics_process(delta)! I guess that’s what happens when one focuses on not coding for a few days, lol.

I had a suspicion that I would need a bool variable.

Thank you for your input!

I will update this post with working code or partially working code when I get to it~

1 Like

In your shoot function, I noticed a piece of code that might have something to do with it. If you are using object pooling, this is ok, but if you are planning to have it so you can fire miltimple projectiles at once, you will need to change it.

get_tree().get_root().add_child(smallBullet)
smallBullet.transform = $Muzzle.global_transform

That is how it is currently done. The problem here is the second time you shoot, smallBullet is already in the scene, and is being added to the scene again, which you can’t do. To fix this, change the smallBullet variable to a PackedScene. Then, store a new instance of the scene into a variable, then add that variable to the scene and position it. Example:

var bullet = smallBullet.instantiate()
get_tree().get_root().add_child(bullet)
bullet.transform = $Muzzle.global_transform

Next, I noticed a small problem in your bulletTravel function. You can strip out the need of a bool if you remove the SPEED = -750 line. That way, simply using SPEED = 0 would stop the bullet. On creating the variable (var SPEED = 0), you should set that to -750 instead of 0, or the bullet wont start moving.

Finally, to delete the bullet, create a Timer as a child of the bullet scene. Name it “deleteTimer”. In the bullet script, add $deleteTimer.start() to the _on_Armor_Hit_area_entered function. Then connect the timer’s timeout signal to a new method in the bullet’s script, which will call the bullet Node’s queue_free() method.

Hope that helps.

1 Like

I suppose I don’t really understand how a PackedScene works completely.

Why does

var bulletSmall = preload("res://market/ammunition/small/bulletSmall.tscn")

func shoot():
	if Input.is_action_pressed("Shoot"):
			animationState.travel("Shoot")
			var bulletSmall_instance = bulletSmall.instance()
			get_tree().get_root().add_child(bulletSmall_instance)
            bulletSmall_instance.transform = $Muzzle.global_transform

work, but

var bulletSmall = preload("res://market/ammunition/small/bulletSmall.tscn")

func shoot():
	if Input.is_action_pressed("Shoot"):
			animationState.travel("Shoot")
			bulletSmall.instance()
			get_tree().get_root().add_child(bulletSmall)
            bulletSmall.transform = $Muzzle.global_transform

doesn’t?

Anyways, thank you! This helped.
I have a working code now and will post it shortly~

1 Like

With the help of @l10nm4st3r and @Doc_Bron I have some working code! Big thank yous to the both of you~

I still have some more stuff to do with it, but the base is (seemingly) good and working.

For my turret I have:
Screenshot 2024-04-29 143445

extends Sprite

export var bulletSmall_speed = 750

onready var animationTree = $AnimationTree
onready var animationState = animationTree.get("parameters/playback")

onready var shootCooldown = $"Shoot Cooldown"

var bulletSmall = preload("res://market/ammunition/small/bulletSmall.tscn")

func shoot():
	if Input.is_action_pressed("Shoot"):
		if shootCooldown.is_stopped():
			shootCooldown.start()
			animationState.travel("Shoot")
			var bulletSmall_instance = bulletSmall.instance()
			bulletSmall_instance.transform = $Muzzle.global_transform
			get_tree().get_root().add_child(bulletSmall_instance)
	else:
		animationState.travel("Idle")

func track_mouse():
	look_at(get_global_mouse_position())
	rotation_degrees += 90

func _physics_process(delta):
	shoot()
	track_mouse()

For my projectile:
Screenshot 2024-04-29 143633

extends Node2D

onready var animationTree = $AnimationTree
onready var animationState = animationTree.get("parameters/playback")
onready var deleteTimer = $DeleteTimer

var SPEED = -750

func bulletTravel(delta):
	position += transform.y * SPEED * delta

func _ready():
	animationState.travel("bullet")

func _on_Area2D_body_entered(body):
	if !body.is_in_group("self"):
		SPEED = 0
		animationState.travel("armorHit")
		deleteTimer.start()

func _on_deleteTimer_timeout():
	queue_free()

func _physics_process(delta):
	bulletTravel(delta)

Note: The DeleteTimer is set to the length of the animation.


Again, thank you @l10nm4st3r and @Doc_Bron! Hopefully this might help someone in the future as well~

1 Like

The reason it doesn’t work is using PackedScene.instantiate() returns the Node version of a PackedScene. The packed scene is just a resource, which can be saved as a file, but not put into the scene tree as it is. To put it into the tree, you first need to get the node version by calling PackedScene.instantiate().

If just using

bulletSmall.instance()
get_tree().get_root().add_child(bulletSmall)

was enough, it would theoretically turn bulletSmall into a Node, which does not have a instance() method, and therefore this would only work the first time, which wouldn’t be very useful.

A packed scene is basically just a scene file. You see those .tscn files? Using GDScript.load() to load those will return a PackedScene.

Hope that explains it.

1 Like

Okay, I think I get it.

It needs the second variable so that it is not just calling the same, single node over and over again.

I guess I don’t understand why having it as a second variable changes anything though. I suppose I’ll chalk it up to “that’s how the engine works,” lol.

Thank you again! I appreciate it!