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.
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 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:
The scene consists of these nodes:
- 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
If you like this article please support my work on Patreon, thank you!