How to get a projectile moving in the same direction as a raycast?

Godot Version

4.4.1

Question

So, in a previous question, I asked how to direct a muzzle raycast to the middle of the player’s screen.

I figured out how to implement that literally 15 minutes ago from typing this.

Now, I need to figure out how to shoot the projectile along the muzzle’s raycast.

The code for projectile shooting is the following:

func primary_action() -> void:
	
	# Update the camera raycast
	Camera_Raycast.force_raycast_update()
	
	# Create collision variables
	var Camera_Raycast_Collision_Point : Vector3 = Camera_Raycast.get_collision_point()
	var Target_Raycast_Location : Vector3 = Muzzle_Raycast.to_local(Camera_Raycast_Collision_Point)
	
	# Update muzzle raycast to go toward camera raycast hit location
	Muzzle_Raycast.target_position = Target_Raycast_Location
	Muzzle_Raycast.force_raycast_update()
	
	# Instantiate projectile
	var Projectile_Instance : Node = Test_Projectile.instantiate()
	
	# Set projectile spawn position at the weapon muzzle
	Projectile_Instance.Projectile_Spawn_Position = Temp_Gun_Position.global_position
	
	# Set projectile spawn rotation
	Projectile_Instance.Projectile_Spawn_Rotation_X = Camera_Reference.global_rotation.x * -1
	Projectile_Instance.Projectile_Spawn_Rotation_Y = Camera_Pitch.global_rotation.y
	Projectile_Instance.Projectile_Spawn_Rotation_Z = Camera_Reference.global_rotation.z * -1
	
	# Spawn projectile in scene
	add_sibling(Projectile_Instance)

I would send a video, but I’m currently live as I write this, and didn’t test how a screen recording would react.

Let me know if you need any more information. Thanks in advance.

Usually you can find the “forward” vector from -transform.basis.z or by multiplying transform.basis * Vector3.FORWARD (this is easier to remember I think).
See a quick demo below


extends Node3D


var direction: Vector3
var speed: float = 5.0


func _unhandled_input(event: InputEvent) -> void:
	if event is InputEventMouseButton:
		if event.button_mask == MOUSE_BUTTON_LEFT:
			direction = transform.basis * Vector3.FORWARD


func _physics_process(delta: float) -> void:
	position += direction * delta * speed
2 Likes

I pulled this from some old code I had. It’s was from a physical projectile that was being shot at a Node3D in the scene.

#Code for pointing towards the target
func point_to_target(target: Node3D) -> void:
	var target_position = target.transform.origin
	var new_transform = self.transform.looking_at(target_position, Vector3.UP)
	self.transform  = self.transform.interpolate_with(new_transform, rotation_speed * delta)

#move to target
func _process(delta: float) -> void:
	position -= transform.basis.z * attack_speed * delta

I think @wchc 's code is cleaner.

3 Likes

Basically the same, but your piece of code additionally rotates the bullet towards the target, I suppose in the style of a homing missile, or something. Cool idea :slight_smile:

3 Likes

When you create the raycast, you give it a position and a target_location. If all you want to do is shoot a projectile along that vector, all you need to do is:

const SHOT_SPEED: float = 200.0 # or whatever

var shot_pos = ray.position
var shot_vec = (ray.position - ray.target_location).normalized() * SHOT_SPEED

Basically, take the ray’s starting position as the shot’s starting position, take the normalized vector from that to the target, scale it by the speed you want the shot to move at.

3 Likes

Yeah it’s a 3D modeled ice shard. I also took out the code where it rises slowly up while aiming and then shoots off.

2 Likes

This is WAY more replies than I thought I’d get. Thank you guys for all this info. I’ll start implementing it in my dev session tomorrow. :+1:

1 Like

I’ll be honest, I’m stumped.

I’m not too sure what about the muzzle raycast’s target_location I need to reference in order for the projectile to move in the same direction.

And I’m not too sure how to ask this question either. So, I’ll explain how my shooting code currently works in more detail.

  1. The shooting system is split into 2 parts: The projectile script and a singular function within the player script.
  2. When the player presses left click, the player script will get the hit location of the camera’s raycast (which is always in the middle of the screen. Then, that hit location is used as the muzzle raycast’s target location.
  3. The projectile is then instantiated. (The projectile script is just a base of variables for the player script to fill in)

The full projectile script is this:
(Also, the projectile node is a CharacterBody3D)

# Projectile stats

var Projectile_Spawn_Position : Vector3

var Projectile_Spawn_Rotation_X : float
var Projectile_Spawn_Rotation_Y : float
var Projectile_Spawn_Rotation_Z : float

var Projectile_Speed : float = 60

var Current_Projectile_Lifetime : float = 0.0
const MAX_PROJECTILE_LIFETIME : float = 10.0

# Nodes
@onready var Projectile_Hitbox : CollisionShape3D = $CollisionShape3D
@onready var Projectile_Mesh : MeshInstance3D = $MeshInstance3D

#------------------------------------------#
# Virtual Functions:
#------------------------------------------#

func _ready() -> void:
	
	# Apply player given stats to the projectile on spawn
	global_position = Projectile_Spawn_Position
	
	global_rotation.x = Projectile_Spawn_Rotation_X
	global_rotation.y = Projectile_Spawn_Rotation_Y
	global_rotation.z = Projectile_Spawn_Rotation_Z
	

func _physics_process(delta: float) -> void:
	
	# Move the projecile forward every physics tick
	velocity -= global_basis * Vector3.FORWARD * Projectile_Speed * delta
	
	# Needed to trigger physics
	move_and_slide()
	
	# Tick projectile lifetime counter
	Current_Projectile_Lifetime += delta
	
	# Delete projectile if lifetime runs out
	if Current_Projectile_Lifetime >= MAX_PROJECTILE_LIFETIME:
		queue_free()
  1. The player script fills in some data, like spawn position, rotation, etc, then spawns the projectile as a sibling.

The full projectile shooting script is the following:

func primary_action() -> void:
	
	# Update the camera raycast
	Camera_Raycast.force_raycast_update()
	
	# Create collision variables
	var Camera_Raycast_Collision_Point : Vector3 = Camera_Raycast.get_collision_point()
	var Target_Raycast_Location : Vector3 = Muzzle_Raycast.to_local(Camera_Raycast_Collision_Point)
	
	# Update muzzle raycast to go toward camera raycast hit location
	Muzzle_Raycast.target_position = Target_Raycast_Location
	Muzzle_Raycast.force_raycast_update()
	
	# Instantiate projectile
	var Projectile_Instance : Node = Test_Projectile.instantiate()
	
	# Set projectile spawn position at the weapon muzzle
	Projectile_Instance.Projectile_Spawn_Position = Muzzle_Raycast.global_position
	
	# Set projectile spawn rotation
	Projectile_Instance.Projectile_Spawn_Rotation_X = Camera_Reference.global_rotation.x * -1
	Projectile_Instance.Projectile_Spawn_Rotation_Y = Camera_Pitch.global_rotation.y
	Projectile_Instance.Projectile_Spawn_Rotation_Z = Camera_Reference.global_rotation.z * -1
		
	# Spawn projectile in scene
	add_sibling(Projectile_Instance)

I’m really not sure how to interpret these responses. I’ve tried to apply them to my scripts/methodology, but I haven’t been successful so far.

This piece of code is a bit flawed. You should define the velocity here, not subtract from it (change -= to just =). Also, when you’re using CharacterBody3D, the delta is already built into the move_and_slide(), don’t multiply your velocity by delta anymore.

	# Move the projecile forward every physics tick
	velocity -= global_basis * Vector3.FORWARD * Projectile_Speed * delta

The fixed code should be the following.

	# Move the projecile forward every physics tick
	velocity = global_basis * Vector3.FORWARD * Projectile_Speed

This should ensure that your bullet travels in the “forward” direction that it is facing.
Now, let’s make sure it is actually facing the correct direction. I see you’re doing some fancy rotation tricks, which can be very much simplified if you just pass the target’s position and use look_at() to rotate your bullet towards the target.

So instead all of this in the bullet script:

func _ready() -> void:
	
	# Apply player given stats to the projectile on spawn
	global_position = Projectile_Spawn_Position
	
	global_rotation.x = Projectile_Spawn_Rotation_X
	global_rotation.y = Projectile_Spawn_Rotation_Y
	global_rotation.z = Projectile_Spawn_Rotation_Z

And this in your shooting script:

	# Set projectile spawn position at the weapon muzzle
	Projectile_Instance.Projectile_Spawn_Position = Muzzle_Raycast.global_position
	
	# Set projectile spawn rotation
	Projectile_Instance.Projectile_Spawn_Rotation_X = Camera_Reference.global_rotation.x * -1
	Projectile_Instance.Projectile_Spawn_Rotation_Y = Camera_Pitch.global_rotation.y
	Projectile_Instance.Projectile_Spawn_Rotation_Z = Camera_Reference.global_rotation.z * -1

Just do something like this, create a new method in the bullet script that accepts 2 things that your bullet needs to know: initial_position and target_position. Use these vectors to apply position and rotation accordingly.

func initialize(initial_position: Vector3, target_position: Vector3) -> void:
	global_position = initial_position
	look_at(target_position)

and then trigger that function in your shooting script right after you initialize the bullet

	# Instantiate projectile
	var Projectile_Instance : Node = Test_Projectile.instantiate()
	
	Test_Projectile.initialize(Muzzle_Raycast.global_position, Muzzle_Raycast.target_position)
	
	# Spawn projectile in scene
	add_sibling(Projectile_Instance)

It should work. If it doesn’t - try to move this function call at the end, after add_sibling(). I haven’t tested it, just written it here raw, so there might be some potential issues with it :slight_smile:

Let me know if that works for you, or if anything needs further explanation.

2 Likes

Thank you so much! This is WAY more specific.

I’ll update you guys tomorrow during or after my dev session.

1 Like

Just as an FYI, the use of -= and delta in my code was because my projectile was an Area3D. Since you’re using a Characterbody3D the physics rules are different, and why you needed those two specific modifications @wchc gave you.

2 Likes

That makes sense. That was completely my fault, I should’ve explained my specific implementation better,

The reason I’m using a CharacterBody3D is because my project, Monkanics, is a multiplayer shooter, and physics aren’t deterministic in Godot.

Since the CharacterBody requires code for all “physics” interactions, it works a lot better for gamestate management.

1 Like

Have you looked at using the Jolt physics engine? I am told it’s much better. I don’t have details on why, but I’m told it’s much better.

1 Like

I’ve already went through my choice of physics engines in one of my previous topics:

Jolt, from my understanding, is just a LOT more refined than Godot’s homemade physics engine. For my specific case, it handles high speed collisions WAY better, making players/projectiles not clip through walls.

I also considered using an plugin physics engine called Rapier. It has a deterministic 3D version. However, I found that the cons outweighed the pros, as I didn’t need full determinism for Monkanics through testing.

Keep in mind, I am HORRIBLE with physics, so I’m just speaking from my limited experience. I’m specialize in game and system design.

I have worked on another project where someone made the design decision to use Rapier. Works fine, though it did make exporting on iOS a bit of a headache.

1 Like

@wchc Ok, I tried implementing your changes.

look_at() doesn’t work unless the projectile is spawned into the scene. But that was easy to fix.

However, the projectile now goes completely off center and only in one direction.

After testing, the issue is with look_at(). The spawn location works fine.

I’ve never implemented look_at() properly, EVER! I’m not sure if it’s a raycast issue, or a look_at() issue. I’ll have to test more.

Edit: After some quick testing look_at() DOES work properly. The issue is with the raycast detection. I’m not sure if it’s the camera raycast, muzzle raycast, or both. I’ll keep you updated.

Edit 2: I figured out it’s the camera raycast that isn’t colliding. Since the camera doesn’t have a new target_location, it messes up the muzzle’s target_location making the projectile move weirdly.

Also, I figured out that the projectiles DO move from the muzzle to the end of the muzzle raycasts successfully. It was just obscured by the camera raycast issue. That’s the next thing I’ll fix.

Also also, I just discovered the reason turning on area collision for the camera raycast broke projectile angles. It was there’s always an Area3D in front of the player I made for melee collision. (Also the camera raycasts does not need to detect areas anyway. Just a neat observation I made)

Edit 3: IT WORKS NOW!!!

The reason was because the camera raycast was too short. It needs to hit a body in order have a target_location. I just needed to make it REALLY LONG.

However, if there is no body, like the sky, the muzzle will fire to the last camera target_position. I actually encountered this problem in Unreal. All I need to do is have a body encapsulating the whole map. Literally an invisible mesh that forms a box.

I need to do a few more tests/tweaks to make sure this projectile shooting system is sound.

Edit 4: I got it tweaked by turning the angle of the camera raycast so it points to the middle of the screen. (A LOT of finicky dial turning)

I’ll go ahead and make another reply with a detailed response on how I solved my issue for posterity. Thanks for the help everyone!

1 Like

Presumably you could detect the lack of a target_location instead, and use a default, rather than having to ensure the raycast always has something to hit?

1 Like

The reason the camera raycast needs to ALWAYS have something to hit is for good reason. The calculation only happens when the player presses the shoot button, not every frame.

The muzzle needs a point of reference for it’s target_location. Which is also the projectile path.

If it doesn’t have this, the projectile will fly where the player doesn’t expect, which is a frustrating player experience.

A default value will never be accurate because the player is always moving.

And if I implemented a system to NOT fire a projectile when the camera raycast doesn’t have a target_location, that also creates a frustration experience for the player. They expect their weapon to shoot when they press the shoot button in the shooter game.

Fair, but typically the answer to that would be just to fire a projectile along the camera’s forward vector, scaled to whatever speed you want the shots to go.

Monkanics is a third-person shooter. Meaning, if the projectile was fired from the camera, it would create some unfair interactions.

For example, you can see an enemy from around a corner, but still be able to shoot them. This is actually a term called corner glitching.

Because of this, the projectile is fired from the muzzle of the player’s weapon, then is rotated toward the crosshair to align with it.

Also, projectile speed, by default, is a set value. Any changes or unique mechanics are not present in the underlying shooting systems. Instead, built on top of them.