Having some some trouble coding with raycasts

Godot Version

4.2.1

Question

Hey all, I’m currently working on an ability system and I’m having some trouble figuring out how to properly use the Raycast2D in gdscript.



#-------------------------------------------
func _process(_delta):
	print(raycast.is_colliding())
	if raycast.is_colliding():
		$".".global_position.x = raycast.get_collider().global_position.x
		anim_player.play("Explosion")
		raycast.enabled = false
	elif raycast.is_colliding() == false:
		#self.queue_free()
		pass
#-------------------------------------------

So, the general idea is that if the raycast hit’s an enemy it should instatiate at the enemy position and if it dosen’t collide it should just delete itself instantly (the instantiation happens elsewhere)

The only problem is the raycast.is_colliding returns false before it finishes the path? When debugging it returns false, false, false, true every time which triggers the queue_free()

Is there another way to code this? or another way to fix it?

Any and all help is appreciated :slight_smile:

This sounds like a “have a cake and eat it too” -situation… Delete itself instantly if it doesn’t hit, but not before it hits anything.

I guess you need to define more conditions that need to be fulfilled. Like, should this raycast only happen when some body enters the area? It’s not clear what you are trying to code.

1 Like

It’s not clear what you are trying to code.

When the player presses a key, a raycast is sent out to check if any enemies are in the raycasts path. If this happens to return true an explosion is triggered at the enemys position. Although if it returns false (e.g. no enemies are in the range of the raycast) nothing should happen.

Essentially i would like the raycast to finish checking for the entire path/range/length before returning anything if possible.

Ok, I get it now.

I usually do it so that it’s the player who has the ray, not the bullet. Then spawn the explosion if ray hits. If it doesn’t hit, no need to instantiate anything, so no need to delete anything either.

But that’s not why this is failing.

The raycast does check for the full length that you set it to. Maybe it’s not long enough, or it’s pointing the wrong way or something. Or forgot to enable it in the inspector or messed up the collision masks.

Is your bullet a moving object that needs to keep checking while it travels? If this is the case, you can just remove the elif part and come up with some other way to delete it. Maybe a timer. This would work for slow bullets or missiles. Slow enough to see I mean.

Your current code looks like it could work for a fast oneshot check. Like for bullets that are so fast, that there’s no need to actually have anything flying. Most games do pistols and machine guns with just a raycast, without really instantiating any bullets.

1 Like

Thats what’s confusing me too. The way it should work is one instant check where it checks the entire target_position

Your current code looks like it could work for a fast oneshot check.

That is it’s intended purpose. It should be a onetime check that emits from the player to check for any enemies (if true) it should then instantiate a stationary explosion at the enemy position that deals damage.
I have fixed the first issue, but i have encountered a new one aswell. Now if the raycast is enabled but it has no target it instantiates the explosion at the players position instead (the player dosen’t take any damage so it’s purely a visibility issue)

New code:

func _physics_process(_delta):
	if raycast.enabled:
		raycast.force_raycast_update()
	print(raycast.is_colliding())
	if raycast.is_colliding() and raycast.get_collider().is_in_group("enemy"):
		$".".global_position.x = raycast.get_collider().global_position.x
		raycast.enabled = false
		anim_player.play("Explosion")
#something here that prevents or fixes the issue when raycast has no enemy target

Try checking Exclude Parent and maybe unchecking Hit Back Faces from inspector.
If that doesn’t help, you could try this:

func _physics_process(_delta):
	if raycast.enabled:
		# If it's enabled, you shouldn't need to force update
		raycast.force_raycast_update()
	print(raycast.is_colliding())
	if raycast.is_colliding():
		# Only call get_collider if is_colliding is true
		if raycast.get_collider().is_in_group("enemy"):
			# You might not need this, but you can make sure the hit point is far enough from the player
			if raycast.get_collision_point().distance_to(raycast.global_position) > 1.0: # Adjust value to fit
				$".".global_position.x = raycast.get_collider().global_position.x
				raycast.enabled = false
				anim_player.play("Explosion")
1 Like

Exclude parent is enabled, I cant seem to find hit Back faces in the inspector.

The script didn’t change much. when printing is_colliding() the result was the same.
I have a sneaking suspicion that the ray cast isn’t an instant check since it prints false, true, false. Despite the fact that it should hit everytime.

I think I’m just gonna do the programming equivalent of duct-taping it together by just making the animations not visible and setting monitoring to false for the area

You set enabled = false when it hits, and also set it’s position. But you are force_raycast_updating still, so enabled=false does nothing and ray is casted again in the next frame. This time from enemys.x position (but not .y) because you moved it there. Target position might be in a weird place now. Maybe this is why it doesn’t register a hit anymore.

Is anything else setting the raycasts position than this code? Does something move it back to player’s position? I’m thinking the raycast might be switching positions every frame. Also curious how you set the target_position?

1 Like

Sorry for late reply,

Target position is set manually at Vector2(300,0) (this is temporary as i will fix directional issues when it works properly)

But you are force_raycast_updating still, so enabled=false does nothing

I think it’s fine since force_raycast_updating happens if raycast.enable = true

Is anything else setting the raycasts position than this code? Does something move it back to player’s position?

Yes (this is one ability the player can pickup and i have another script that also sets the position for all abilities - this position is set at the middle of the player + an offset that is defined by the ability), but the issue is really just that the animation is playing. preferably i would like for either the animation to not be visible at all or just not play to begin with.
I just can’t fathom why it seemingly isn’t an instant check that either is or isnt true for the duration of the raycast existing

Oh yeah, it’s behind an if-statement. Then it’s the force_raycast_update that’s just useless there.

Quick fix would be the distance check I wrote above. Unless something fishy is going on. And I’m sure there is something.

Flipflopping between true and false is definitely not something the raycast does by themself. Some code is making it do that and I’m guessing it’s with the positioning. Maybe two scripts fighting over it’s position. Could be something else too, like targeting or enabling. I don’t know.

You should probably use the debugger and go step by step to see what’s really happening.

  • play until the buggy part
  • pause
  • keep hitting F11 to step trough the code as it executes
1 Like

Flipflopping between true and false is definitely not something the raycast does by themself. Some code is making it do that and I’m guessing it’s with the positioning.

After doing some testing i realised that raycast.is_colliding() and raycast.get_collider() will return either false or null if the raycast is disabled which is what’s causing the issue since that the moment the raycast.is_colliding() returns true the raycast is disabled which causes raycast.is_colliding()` to return false which then triggers the queue_free().

The code that makes it flipflop is the line that sets raycast.enabled = false so relying on whether or not it’s enabled is inevitably going to cause some malfunction

My first thought was to use raycast.get_collider().is_in_group("enemy") as a boolean. If false the raycast should queue_free. Although it seems that as long as raycast.get_collider().is_in_group("enemy") is attempting to call on a null instance (raycast dosen’t hit an enemy) it crashes.

I don’t really know how to go forward from here

Yes, that’s why you should call them on separate lines like this:

Oh, man. I thought this would be so easy that I don’t even have to open Godot for it. Now I finally tried and realized the root cause for all the confusion:
Change _physics_process to _process!!! (omg)

Turns out on the first frame, raycast really does return false when it should be true. I guess _physics_process is called before the raycast happens for the first time, so that explain the need to force it.
But _process happens after all the physics, so hit is detected on the first frame.

Here’s my full code that works:

extends Node3D

@onready var raycast = $RayCast3D

func _process(delta):
	if(raycast.enabled):
		if raycast.is_colliding():
			print("Hit something")
			if raycast.get_collider().is_in_group("enemy"):
				print("Detected enemy!")
				print("Set position, disable ray and explode")
				global_position = raycast.get_collider().global_position
				raycast.enabled = false
		else:
			print("no hit, deleting")
			queue_free()
	else:
		print("Explosion anim is playing now")
2 Likes

…I started out with some different code that used _process, but someone in the godot discord said to change it to _physics_process…
I’m at a loss for words
Thank you for your patience and help. I cant put into words how thankful i am :slight_smile: