0 votes

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?

in Engine by (20 points)
edited by

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.

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 'castray()' 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.

1 Answer

0 votes

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)
by (20 points)
Welcome to Godot Engine Q&A, where you can ask questions and receive answers from other members of the community.

Please make sure to read How to use this Q&A? before posting your first questions.
Social login is currently unavailable. If you've previously logged in with a Facebook or GitHub account, use the I forgot my password link in the login box to set a password for your account. If you still can't access your account, send an email to webmaster@godotengine.org with your username.