Hi, everyone! I'm writing a 3D isometric in which I have a certain class that is able to shoot bullets that should arc in their trajectory straight towards the target position. Ideally, the bullets should be RigidBodies that stay in the map a while after being shot, however lose their damaging capabilities after first impact.

However, I'm having trouble with figuring out the whole ballistic projection part of the shooting.
Right now, my function, being called from the shooter's script into the bullet's script, is as such:

func launch(shoot_point, target, strength):
	transform = shoot_point.global_transform
	var direction = shoot_point.global_position.direction_to(target.global_position)
	
	apply_central_impulse(direction * strength)

In the shooter's class, it is called as such:

func _shoot(bullet:PackedScene, shoot_point:Marker3D, target:Node3D):
	var b = bullet.instantiate()
	add_child(b)
	b.launch(shoot_point, target, 20)

In which shoot_point is a Marker3D placed where the bullet's supposed to come out.

The "20" strength value is just a placeholder, as I see that this function shouldn't have a strenght value being passed at all, since the strength would be relative to the distance needed to cover.

Right now, the shooting is going like this:

And what I want is a parabola that starts at fixed angle range (it can be 30-60º) and only touches the ground at the target's position, like this:

Perhaps the angle can be the only variable to be solved when calculating the touch point, and the strength remains fixed.
The cannonball's weight is unimportant and will be fixed for everyone that uses this shooting mechanism.

How can I modify my shooting function so that it works properly? Thanks!

    What is apply_central_impulse() ?? Is it a default godot function or is it your own function. (I'm kinda new to 3D Dev_)

      I'm assuming that the targets are at the same height as that from which the bullets are shot (?) Anyway, I'm among the few zoomers who haven't forgotten how to do projectile motion (don't judge >:3), so here:

      Here's how that would translate into code:

      var turret_pos = Vector3(global_position.x, 0, global_position.z)
      var target_pos = Vector3(target.global_position.x, 0, target.global_position.z)
      var gravity = (ProjectSettings.get_setting("physics/3d/default_gravity") * ProjectSettings.get_setting("physics/3d/default_gravity_vector") * gravity_scale).y
      var xz_dir = turret_pos.direction_to(target_pos)
      
      var offset = Vector3(0, tan(angle), 0)
      var direction = (xz_dir + offset).normalized()
      var distance = turret_pos.distance_to(target_pos)
      var strength = sqrt(-distance * gravity / sin(2 * angle))
      
      apply_central_impulse((direction * strength) * mass)

      I'm not really familiar with 3D, so this could probably be optimized (I'm unsure abt the 1st block of variables), but this should work ^^

      saintfrog I want is a parabola that starts at fixed angle range (it can be 30-60º) and only touches the ground at the target's position, like this:

      I was going to recommend using a math function to just have projectile follow a path if you don't care about simulating physics and just want the projectile to go to the target, but I didn't remember the math.

      So I made it again real quick:

      @export var objective : Vector3#where the ball has to hit #if you are setting this from script, don't make it a export
      var start_pos : Vector3 = Vector3.ZERO#where the ball spawns
      
      var dist : float
      
      func _ready():
      	start_pos = global_position
      	dist = (1.0 / start_pos.distance_to(objective))
      
      func _process(delta):
      	var ldist : float = global_position.distance_to(objective) * dist
      	global_position += basis * Vector3.FORWARD * delta#speed. it's moving in the Z direction, so point the projectile before it starts
      	global_position.y = sin(ldist * PI)#move up and down
      	if ldist < 0.1:
      		#the projectile has reach it's destination. here you swap it with an explosion or a rigidbody
      		global_position = start_pos#in my case it's going back the original position

      this code was on a simple meshInstance3D. you don't need physics if you already know the trajectory of the projectile. If you need it to collide with stuff, add some Area and make it change before time.
      In this case I was only able to make it move in one dimension in the short time I had (up and down). a real projectile would also move forward in a non-linear... forward movement.

      an alternative is to sample a Curve to the distance that the projectile needs to travel in the local FORWARD.
      these are visual. for a realistic physics simulation of a projectile, you can't. game physics are an approximate simulation, for performance, not a perfect simulation. In this case you also have to move the global_position across a Curve, you just generate the curve.
      the projectile must also be a non physics object until it hits, destroys itself and spawns an actual physics object.

      Edit: here is a better version:

      var objective : Vector3
      var start_pos : Vector3 = Vector3.ZERO
      var SPEED : float = 4.0
      
      var dist : float
      var active : bool = false
      
      func start():
      	start_pos = global_position
      	var dt : float = start_pos.distance_to(objective)
      	dist = (1.0 / dt)
      	SPEED = dt
      	active = true
      
      func _physics_process(delta):
      	if active:
      		var ldist : float = global_position.distance_to(objective) * dist
      		global_position += basis * (Vector3.FORWARD * asin(ldist * PI * 2)) * SPEED * delta
      		global_position.y = sin(ldist * PI)
      		if ldist < 0.1:
      			queue_free()
      @export var proj : PackedScene
      var params : PhysicsRayQueryParameters3D
      @onready var camera_3d = $"../../Camera3D"
      @onready var node_3d = $"../.."
      
      var gpos : Vector2
      
      func _ready():
      	params = PhysicsRayQueryParameters3D.new()
      
      func _input(event):
      	if event is InputEventMouse:
      		gpos = event.global_position
      
      func _physics_process(delta):
      	if Input.is_action_just_released("Click"):
      		params.from = camera_3d.project_ray_origin(gpos)
      		params.to = params.from + camera_3d.project_ray_normal(gpos) * 100.0
      		params.collision_mask = 1
      		params.collide_with_bodies = true
      		params.collide_with_areas = false
      		var result = get_world_3d().direct_space_state.intersect_ray(params)
      		if result.size() > 0:
      			var tmp : Node3D = proj.instantiate()
      			node_3d.add_child(tmp)
      			tmp.global_position = global_position
      			tmp.start_pos = global_position
      			tmp.objective = result.position
      			tmp.look_at(result.position, Vector3.UP, false)
      			tmp.start()

        Jesusemora Wow :o ... That's really smart from a performance standpoint. Although, wouldn't global_position += basis * Vector3.FORWARD * ... move it only in the global z direction? I makes sense to multiply the transform basis by some start_pos.direction_to(objective). Also, the sine function isn't really a parabola and thus, an imperfect analogue of projectile motion. I think I can improve on that, provided saintfrog states what variables exactly they want to tinker with (The gravity acting on the bullets, which, I assume, needs to be equal, and either the horizontal speed at which the bullet travels or the time they take to impact comes to mind) But, yes, I'm definitely stealing this technique for my game :​D

          8 days later

          Annoying-Cat

          Jesusemora

          Hi! Sorry I didn't reply, I didn't get any notifications and thought no one had replied to the thread!
          I ended up using both of your guys suggestions in different contexts:

          • I used Jesusemora's projectile as a "direct" projectile that travels following the sine-wave pattern that can be used to hit targets further away, and Annoying-Cat's projectile for traditional artillery with fixed degree shooting.

          However, now I'm getting a problem with Annoying-Cat's physical approach, because I got to the point where I'm starting to consider different Y-levels in my game, so I can have situations where the target is further below the player or above. How can I adapt the function to consider the target's height level? I'll limit the power/angle limitations such as very long range shooting or not being able to shoot the moon by only considering the input inside a certain Area3D.

            saintfrog
            I've noticed that these forums have a nasty tendency to do that sometimes, or maybe there is something wrong with our settings? idk
            Anywho~, what kind of game are you making? As in, do you want to have the projectiles land in a certain amount of time (prolly useful for turn-based gameplay, I guess (?)), or do you want every projectile of a certain type to move across tiles with a constant velocity? I've done the code assuming the former, as it builds upon the latter, but in a way that you can change it as you see fit:

            extends RigidBody3D
            
            @export var turret: Node3D # where the projectile is shot from
            @export var target: Node3D # where it lands
            
            @export var time: float = 1 # time it takes to impact
            @export var gravity: float = -9.8
            @export var threshold: float = 0.1 # how close it needs to be to count as a 'hit'
            
            var turret_pos: Vector3
            var target_pos: Vector3
            
            var turret_xz_pos: Vector3
            var target_xz_pos: Vector3
            
            var xz_dist: float
            var xz_dir: Vector3
            var xz_speed: float # may be exported in place of 'time'
            
            var is_active: bool = false
            
            func _start():
            	# asign positions
            	turret_pos = turret.global_position
            	target_pos = target.global_position
            	
            	# truncate them to the horizontal plane
            	turret_xz_pos = Vector3(turret_pos.x, 0, turret_pos.z)
            	target_xz_pos = Vector3(target_pos.x, 0, target_pos.z)
            	
            	# calculate horizontal distance, direction and speed
            	xz_dist = turret_xz_pos.distance_to(target_pos)
            	xz_dir = turret_xz_pos.direction_to(target_pos)
            	xz_speed = xz_dist / time # may be deleted if 'xz_speed' is exported
            	
            	# stolen code
            	is_active = true
            
            func  _physics_process(delta):
            	if is_active:
            		# calculate horizontal position
            		var xz_pos = Vector3(global_position.x, 0, global_position.z)
            		
            		#  calculate distance travelled to use as variant
            		var ldist = xz_dist - xz_pos.distance_to(target_xz_pos)
            		
            		# steal code, then do catnip
            		global_position += basis * xz_dir * xz_speed * delta
            		global_position.y = 0.5 * gravity * pow(ldist / xz_speed, 2) + ((target_pos.y - turret_pos.y) / time - 0.5 * gravity * time) * (ldist / xz_speed) + turret_pos.y
            		
            		if ldist > (xz_dist - threshold):
            			queue_free()
            
            # replace this with whatever you have
            func _input(event):
            	if event.is_action_pressed("Shoot"):
            		_start()

            A lot of this approach is built upon @Jesusemora's code (hopefully they don't mind ^_^;​), as doing this with impulses would have been hell (for me and godot)