Sgt. Boomerang Devblog-Part 1

Devblog Part-1 Making the boomerang path

The most crucial part of the game is done! I can now visualize a boomerang path after user input and throw a boomerang on this path. I have received a couple of questions on some of my reddit posts how I did this so I decided to share how I did the implementation of it.

Shortly, I am using a bezier curve to draw the boomerang path, and updating the curve points with mouse input and drag. I am also doing a raycast from the interpolation points in the tangent direction of the curve to check any collisions so that I can visualize the hit points as well. Lets dive deeper into the implementation.

Godot structure

Before diving into the code here is the main structure for the boomerang shooting:

  • Boomerang scene: The boomerang asset which flies on the bezier path, checks collision and has guns on it.
  • BoomerangPath scene: Bezier path visualizer and updater.
  • PlayerInputController scene: Single Node2d with a script on it. Controls input for player and updates boomerang path input as well as boomerang shooting.
  • Player scene: Main player scene, has BoomerangPath and PlayerInputController scenes as child. And Boomerang is being instantiated on runtime.

Boomerang Path

Boomerang path is inheriting from Path2D component. Path2D uses by default a curve asset, that same asset that I used to move the boomerang. In basic path2d updates the curve asset and in boomerang scene I reference the same curve asset and move the boomerang by interpolating this curve asset. Here is how it looks like:

Path here is saved under: res://Objects/BoomerangPath.tres and I use same asset on Boomerang.

The scene consists of these nodes:

BoomerangPath scene
  • Line2D is to visualize the bezier curve.
  • Raycast2D is to check if curve hits a body, so that we can stop visualizing the curve at that point.
  • PreviewFollow is a PathFollow2D node, it is just to show a preview of the flying boomerang.
Boomerang Path Script

In short what I did is:

  • Create 3 points for bezier. First and third points are are going to be player root position and second point is going to be mouse click position.
  • Update 2. point to the mouse position when user clicks.
  • Update 2. point’s in and out positions when user drags the mouse.
  • Loop all points of the curve and check if raycast hits something.
  • Update curve points according the the raycast
  • Draw the line with the curves and gradients colors.

To visualize the bezier curve we would need some parameters:

# Variables
export(float) var curveMultiplier = 2 # multiplier for moving curve points with mouse
export(bool) var clampBezier = true # clamp the curve points so it won't get too big 
export(float) var bezier_radius = 2 # radius for clamping
export(bool) var use_preview = false # user a preview sprite on the curve
export(float) var can_shoot_treshold = 50 # min move threshold of the mouse in oder to shoot 
export(Gradient) var can_shoot_gradient # show a gradient color on line2d when user can shoot. Its green to transparent
export(Gradient) var not_shoot_gradient # show a gradient color on line2d when user can not shoot. Its red to transparent
# Nodes
onready var raycast_path = get_node("RayCast2D")
onready var line = get_node("Line2D")
onready var preview = get_node("PreviewFollow")
# private 
var last_mouse_pos = Vector2.ZERO # stored with first click and used to calculate distance with drag
var edit_Bezier = false # controls editing bezier
var can_shoot # can shoot boolean

First thing to do when scene starts is to reset the curve.

func _ready():
	_reset()
func _reset():
        # hide the line so we don't show anything
	line.hide()
        # clear curve points
	curve.clear_points()
        # add 3 points, first and third points are are going to be player root position and second point is going to be mouse click position
	curve.add_point(position)
	curve.add_point(position)
	curve.add_point(position)
	preview.set_process(use_preview)

Add input functions which will be called from PlayerInputSystem.

func on_pressed(_delta):
	# set mouse position
	var mousepos = get_local_mouse_position()
	last_mouse_pos = mousepos
	# current mouse position is set to pos 1
	curve.set_point_position(1, mousepos)
	edit_Bezier = true
	if use_preview:
		preview.show()
        # we will clear the line 2d points, they will be updated from process function later
	line.clear_points()
	line.show()

func on_released(_delta):
	edit_Bezier = false
	# hide stuff
	line.hide()
	preview.hide()

After this updating the bezier with mouse drag is done in process function. We would need to update point in and point out positions of the curve points to get a smooth bezier curve.

func _update_bezier():
	if not edit_Bezier:
		return
	# Set positions
	var point1 = curve.get_point_position(0)
	var point2 = curve.get_point_position(1)
	var point_in = _get_point_in(point1, point2)
	var reflected_p_in = (point_in - point1).reflect((point2-point1).normalized()) - (point2 * 2) 
	var reflected_p_out = (point2 + (point2 - point_in)) - (point2 * 2)
	# Bezier setup, curve in and out positions
	curve.set_point_out(0, point_in)
	curve.set_point_in(1, point_in)
	curve.set_point_out(1, reflected_p_out)
	curve.set_point_out(2, reflected_p_in)
	var points = curve.get_baked_points()
	var point_index = points.size()
	# Check raycast
	for i in range(0, points.size()):
		if i+1 >= points.size():
			break
		# raycast if it hits something
		raycast_path.position = points[i]
		raycast_path.cast_to = (points[i+1] + points[i]) * 0.01
		raycast_path.force_raycast_update()
		if raycast_path.is_colliding():
			point_index = i
			break
        # resize the points array if we hit something
        # and set the line points
	points.resize(point_index)
	line.points = points

func _get_point_in(point1:Vector2, point2:Vector2)->Vector2:
	# Returns the point in for the bezier curve
        # In this case its the middle point of player root and mouse click position
        var mid = ((point1 + point2) * 0.5)
	var diff = get_local_mouse_position() - last_mouse_pos
	var point_in = mid + diff * curveMultiplier
	_set_can_shoot(diff.length() > can_shoot_treshold)
	# clamp values
	if clampBezier:
		return Util.clamp_vector(point_in, mid, point1.distance_to(point2)* 0.5 * bezier_radius)
	else:
		return point_in

You can read more about bezier curves here: https://docs.godotengine.org/en/stable/tutorials/math/beziers_and_curves.html

Here how end result looks like.

If you like this article please support my work on Patreon, thank you!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Powered by WordPress.com.

Up ↑

%d bloggers like this: