Correct transform for rotating laser turret?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By unwiseone

I’m trying to create a laser weapon for my game but I’ve been struggling to get both the damage (raycast) and beam (Line2D) to both go to the correct target position. Currently, the the Line2D representing the beam is drawing in the correction direction, but when the player fires any direction other than towards (1, 0), the raycast doesn’t appear to cast in the expected direction.

I think I’ve narrowed the problem to how I am applying transforms to the raycast and the Line2D, but I’ve reached end of my knowledge on how to proceed further to correct it. This the relevant part of the player scene:

- Player (KinematicBody2D; scene root)
  - BodyPivot (Position2D)
    - Body (Sprite)
    - CurrentWeaponSprite (Sprite)
      - Muzzle (Position2D)
  - Inventory (Node)
    - LaserRifle (Area2D; scene instance)
      - Sprite

The ‘CurrentWeaponSprite’ node rotates like turret to aim wherever the player moves the mouse, while all the other node do not rotate.

I’m using the following code:

"""LaserRifle.gd"""

extends Area2D

var DAMAGE = 5
shot_duration = 0.25

knockback_speed = 50
knockback_duration = 0.12
stun_duration = 0.25

var beam
var beam_color = Color.crimson


func shoot(projectile_start_position):
    muzzle_position = projectile_start_position
    var result = _cast_ray(muzzle_position)

    if result:
        _create_beam(muzzle_position, result)
        _inflict_damage(result)
        yield(get_tree().create_timer(shot_duration), "timeout")
        _destroy_beam()


func _cast_ray(projectile_start_position):
    var CurrentWeaponSprite = CarriedBy.current_weapon_sprite()
    var space_state = get_world_2d().direct_space_state
    var result = space_state.intersect_ray(projectile_start_position, projectile_start_position + CarriedBy.transform.x * 1000, [self, CarriedBy])


func _create_beam(muzzle_position, result):
    var CurrentWeaponSprite = CarriedBy.current_weapon_sprite() # The var CarriedBy is a reference to the 'actor' using the weapon.

    beam = Line2D.new()
    CurrentWeaponSprite.get_node("Muzzle").add_child(beam) # 'Muzzle' node is a Position2D node.
    beam.width = 2
    beam.default_color = beam_color
    beam.end_cap_mode = Line2D.LINE_CAP_ROUND
    beam.add_point(CurrentWeaponSprite.get_node("Muzzle").position)
    beam.add_point(CarriedBy.transform.xform_inv(result.position))global coordiantes to local.


func _destroy_beam():
    beam.queue_free()


func _inflict_damage(collision_info):
    if collision_info.collider.has_node("Health"):
        collision_info.collider.take_damage(DAMAGE, self, stun_duration, knockback_speed, knockback_duration)

Any suggestions on how I might fix this issue?

Note that:

beam.add_point(CurrentWeaponSprite.get_node("Muzzle").position)
beam.add_point(CarriedBy.transform.xform_inv(result.position))global coordiantes to local.

Because you put the beam as child of the muzzle shooting it, none of that code is required, because the beam already rotates based on where that weapon is looking at. It might as well be this:

beam.add_point(Vector2(0, 0))
beam.add_point(Vector2(1000, 0))

Are you sure your raycast is shooting at the right direction? Try to print its positions to make sure. If they are correct, your problem is the line.

Zylann | 2019-09-06 12:54

On r/Godot, someone suggested changing CarriedBy.transform.x to CarriedBy.global_transform.x. With that change, the raycast is now firing in the correct direction. I confirmed this by adding the following two lines to the end of the ‘_cast_ray()’ function, before the return…

if result:
    _debug_draw_triangle(result.position)

… with this function definition…

func _debug_draw_triangle(hit_result_position):
	var triangle = Polygon2D.new()
	LevelYSort.add_child(triangle) # LevelYSort is "get_node('/root/Level/YSort')".
	triangle.global_position = hit_result_position
	triangle.polygon = [Vector2(0, 0), Vector2(-5, 10), Vector2(5, 10)]

I’m still trying to figure out the correct transform for drawing the Line2D beam’s second point (i.e. ‘endpoint’), however. I’ll experiment further and hopeful stumble across a solution.

unwiseone | 2019-09-08 21:03

:bust_in_silhouette: Reply From: unwiseone

I was finally able to get this working. Extrapolating from Zylann’s suggestion, rather than futzing around with transforms, for the create_beam() and update_beam_endpoint() functions I am instead taking just the ‘x’ global coordinate of the raycast hit position and using the to_local() function from Node2D to convert it to a local coordinate. I also changed CarriedBy.transform.x to CarriedBy.global_transform.x in the cast_ray() function. This is the full updated script:

"""LaserRifle.gd"""

extends Area2D

var id = "laser_rifle"
var item_name = "Laser-Rifle"
var desc = "A laser-rifle."

var fire_type : String = "instant_hit"
var DAMAGE : = 5
var knockback_speed : = 50
var knockback_duration : = 0.12 # Measured in seconds
var stun_duration : = 0.25 # Measured in seconds

var ammo_max = 75
var ammo_current = ammo_max
var cooldown = 1
var shot_duration : = 0.25
var damage_delay : = 1.0
var beam : Line2D
var beam_color : = Color.crimson

var hit_result # Dictionary


func _ready() -> void:
	ammo_current = ammo_max
	$CooldownTimer.one_shot = true
	$CooldownTimer.wait_time = cooldown
	$DamageDelay.one_shot = true
	$DamageDelay.wait_time = damage_delay


func _process(delta : float) -> void:
	if hit:
		var muzzle_global_position : Vector2 = CarriedBy.current_weapon_sprite().get_node("Muzzle").global_position
		var result : Dictionary = cast_ray(muzzle_global_position)
		update_beam_endpoint(result)
		hit = result
		
		if $DamageDelay.is_stopped():
			_inflict_damage(result)
			$DamageDelay.start()


func use_item_with(projectile_start_position : Vector2) -> bool:
	if can_fire() == true:
		shoot(projectile_start_position)
		ammo_current -= 1
		if CarriedBy.TYPE == "PLAYER":
			emit_signal("player_ammo_count_changed", ammo_current, ammo_max)
		$CooldownTimer.start()
		return true
	else:
		print("Waiting for cooldown.")
		return false


func can_fire() -> bool:
	if $CooldownTimer.is_stopped() and disabled != true and ammo_current > 0:
		return true
	else:
		return false


func shoot(muzzle_global_position : Vector2) -> void:
	print("Firing...")
	var result : Dictionary = cast_ray(muzzle_global_position)

	if result:
		if !hit:
			create_beam(muzzle_global_position, result)
		else:
			update_beam_endpoint(result)

		hit = result
		_inflict_damage(result)
		$DamageDelay.start()

		yield(get_tree().create_timer(shot_duration), "timeout")
		destroy_beam()
		hit = null


func cast_ray(projectile_start_position : Vector2) -> Dictionary:
	var CurrentWeaponSprite : Sprite = CarriedBy.current_weapon_sprite()

	var space_state : Physics2DDirectSpaceState = get_world_2d().direct_space_state
	var result : Dictionary = space_state.intersect_ray(projectile_start_position, projectile_start_position + CurrentWeaponSprite.global_transform.x * 1000, [self, CarriedBy])

	return result


func create_beam(muzzle_global_position, result) -> void:
	var Muzzle : Position2D = CarriedBy.current_weapon_sprite().get_node("Muzzle")
	var start_point : Vector2 = Muzzle.to_local(muzzle_global_position)
	var end_point_x_coord : int = Muzzle.to_local(result.position).x

	beam = Line2D.new()
	Muzzle.add_child(beam)

	beam.width = 2
	beam.default_color = beam_color
	beam.end_cap_mode = Line2D.LINE_CAP_ROUND
	beam.add_point(start_point)
	beam.add_point(Vector2(end_point_x_coord, 0))


func update_beam_endpoint(result) -> void:
	print("Updating beam.")
	var Muzzle : Position2D = CarriedBy.current_weapon_sprite().get_node("Muzzle")
	var end_point_x_coord : int = Muzzle.to_local(result.position).x
	beam.points[1] = Vector2(end_point_x_coord, 0)


func destroy_beam() -> void:
	beam.queue_free()


func _inflict_damage(collision_info : Dictionary) -> void:
	if collision_info.collider.has_node("Health"):
		collision_info.collider.take_damage(DAMAGE, self, stun_duration, knockback_speed, knockback_duration)