Peggle game in Godot 3 > Tutorial 02 - Rotate cannon by mouse move

In this part, we will create a cannon and make it react to mouse movement. The behavior will be programmed in the cannon script. There are many ways to implement rotating by mouse logic and we will cover the most essential of them. 

First, let's create a new 2d scene and call it Cannon. Attach a script to it, call it canon.gd




Now add a Sprite and assign this texture to it






The whole texture size is 128x128 but the round base size is 120 only. To make it rotating smoothly set Y offset to 4


Now spawn cannon scene to Game main scene


Then set the cannon position to x: 512 and y: 80. Run the game. You should get this result


Great! Now let's write the script and make it interactive!

This is an important moment that the cannon's muzzle looks down (this is equal to Vector2.DOWN). The programmed behavior depends on this and the program be a little different for the other directions. 
Basically, all we need here is a couple of trigonometric functions



Trigonometric functions to remember

sin(a) = opp/hyp
cos(a) = adj/hyp
tan(a) = opp/adj = sin(a)/cos(a)

Implement rotation


In our case triangle looks like this


And you may guess where the desired angle is located


Now go to cannon.gd and write the script



# cannon.gd
# Variant 1 (find the hypotenuse using Pythagorean theorem)
func _process(delta):
	if get_global_mouse_position().y >= self.position.y:
		var x = get_global_mouse_position().x - self.position.x
		var y = get_global_mouse_position().y - self.position.y
		var c = sqrt(x*x + y*y)
	
		print(" x: ", x, " y: ", y, " c: ", c)
	
		if c != 0:
			var angle = -rad2deg(asin(x/c))
			print("angle:", angle)
			self.rotation_degrees = angle
            

First, we find x and y distance between the mouse cursor and the cannon center. After that, we find c (hypotenuse) by using the Pythagorean theorem and then sin for the angle - x/c. In our case, x is the opposite side to the angle. To get the angle value we call asin(x/c). The returned value is in radians so we convert it to degrees rad2deg(asin(x/c)). The minus here is because when we move to the right, the x value is positive but the angle should be negative. So we add minus in the beginning or let say multiply the value by -1. Then apply the calculated angle to the cannon node.

This code is pretty naive but gives you an understanding of how things work.  We can improve this code by using vector subtraction for distance calculation. Also, we don't need to convert the angle to degrees as Godot can consume radians too. We continue to use rad2deg for print just to see what's going on. 


# Variant 2 (use vector and radians)
func _process(delta):
	if get_global_mouse_position().y >= self.position.y:
		var d = get_global_mouse_position() - self.position
		var c = sqrt(d.x*d.x + d.y*d.y)
	
		print(" x: ", d.x, " y: ", d.y, " c:", c)
	
		if c != 0:
			var angle = -asin(d.x/c)
			print("angle: ", rad2deg(angle))
			self.rotation = angle
            

The code becomes more professional. Great! But this is just the beginning 🤠

Because we know x and y, we can calculate the angle using the arctangent atan function and avoid additional calculation for the hypotenuse. There is a slight difference between atan and atan2 functions that good to know

atan vs. atan2

-pi/2 < atan(opp/adj) < pi/2 (-90 <= angle in degress <= 90 )
-pi < atan2(opp,adj) < pi    (-180 <= angle in degress <= 180 )

More info about the difference:
https://www.mathopenref.com/trigtangent.html
https://stackoverflow.com/questions/283406/what-is-the-difference-between-atan-and-atan2-in-c

Note that atan takes one parameter which is the relation between opposite and adjacent sides, while atan2 takes opposite and adjacent sides as two separate parameters.

Since we limit cannon rotation between -90 and 90, we can use atan as well. I still prefer to use atan2, but you can uncomment the line with atan and play around. 

Here is the code using the arctangent function


# Variant 3 (using atan2)
func _process(delta):
	var d = get_global_mouse_position() - self.position
	if d.y != 0:
		#var angle = -atan(d.x/d.y)  # -90 <= angle <= 90 	
		var angle = -atan2(d.x, d.y) # -180 <= angle <= 180 
	
		print(rad2deg(angle))
		self.rotation = angle

Important! The atan2 function takes the opposite side as the first parameter. In our case, x is the opposite side, so it goes before the y parameter - atan2(d.x, d.y). It's true for atan function parameter as well atan(d.x/d.y)

Now let's back the rotation limit, but instead of get_global_mouse_position().y >= self.position.y we patch y in the distance vector itself. Also, because we use atan2 function now, y can have zero value. The code becomes very neat



# Variant 4 (using atan2 and rotation limit)
func _process(delta):
	var d = get_global_mouse_position() - self.position
	if d.y <= 0: d.y = 0

	var angle = -atan2(d.x, d.y)

	print(rad2deg(angle))
	self.rotation = angle

Cool, huh? Actually, it can be even cooler with Godot "gotchas". Let's take a look!


Extra stuff - Rotate using Godot "gotchas"


In Godot, Vectror2 has a bunch of methods that can help us to implement the same thing even faster. Just go to the Vector2 source code on GitHub https://github.com/godotengine/godot/blob/3.2/core/math/vector2.cpp and take a look at angleangle_to, and angle_to_point methods there.

You can see that angle is actually just atan2 function call. But it takes y as the opposite side, while in our case x is the opposite. To fix this, we need to rotate the cannon to -90 degrees or -PI/2 in radians. Apply angle method to the distance vector d


# Gotcha 1 (angle method)
func _process(delta):
	var d = get_global_mouse_position() - self.position
	var angle = d.angle() - PI/2

	print(rad2deg(angle))
	self.rotation = angle

Another cool method is angle_to_point which calculates the distance d and atan2 at the same time. The code becomes even shorter


# Gotcha 2 (angle_to_point method)
func _process(delta):
	var angle = get_global_mouse_position().angle_to_point(self.position) - PI/2
	self.rotation = angle
	print(rad2deg(angle))

The most interesting is angle_to method. To make it work, need to use directional vectors like Vector.RIGHT or Vector.DOWN with the distance vector d

# Gotcha 3 (angle_to method)
# get_global_mouse_position().angle_to_point(self.position)
# is equal to 
# Vector2.RIGHT.angle_to(get_global_mouse_position() - self.position)    
func _process(delta):
	var d = get_global_mouse_position() - self.position
	var angle = Vector2.RIGHT.angle_to(d) - PI/2
	self.rotation = angle
	print(rad2deg(angle))

Or even better by using Vector.DOWN since our cannon muzzle looks down

# Gotcha 3 (angle_to method with Vector2.DOWN )
# no need -PI/2 with Vector2.DOWN     
func _process(delta):
	var d = get_global_mouse_position() - self.position
	var angle = Vector2.DOWN.angle_to(d)
	self.rotation = angle
	print(rad2deg(angle))
    

And the final gotcha is look_at method that belongs to Node2d itself. It finds the rotation angle and rotates the node all in one. Take a look to the source code here code https://github.com/godotengine/godot/blob/3.2/scene/2d/node_2d.cpp#L376

It reduces the code just to one line. But again, because it uses angle method inside, we need to make y as the opposite side to the rotation angle, so rotate the sprite before the start - in the editor or in _ready callback

# Gotcha 4 (look_at method)
# need to to rotate sprite to -90 before start
func _ready():
	$Sprite.rotation = -PI/2
	
func _process(delta):
	look_at(get_global_mouse_position())


Wrapping up


As you can see, we can implement common rotation logic in many ways. I prefer to go with custom implementation using atan2 function.  Mainly because I understand what happens inside, can easily tweak the logic, and also there are minimum calculations involved. All built-in Godot "gotchas" are very good but need to understand how they work. I would go with angle_to_point because it's simple and does a minimum of calculations.


# I pick Variant 4 (using atan2 and rotation limit)
# because 1. understand what happens inside 
#         2. can easily tweak the logic
#         3. minimum calculations involved
func _process(delta):
	var d = get_global_mouse_position() - self.position
	if d.y <= 0: d.y = 0
	var angle = -atan2(d.x, d.y)
	self.rotation = angle

This rotation logic can be placed on event callback as well. Probably it's even more efficient because calculations happen only on the mouse event. But it can be not suitable for some cases. If you want to use the event callback, here is the code


# Variant in event callback
func _unhandled_input(event):
	if event is InputEventMouse:
		var angle = event.global_position.angle_to_point(self.position) - PI/2
		self.rotation = angle
		print(rad2deg(angle))

Last moment here. To set node rotation you can go in different ways. I prefer to use self.rotation, but you can go with either of them

How to set node rotation

1. self.rotation = angle
2. rotation = angle
3. set_rotation(angle)
4. self.rotation_degrees = rad2deg(angle) #not really efficient since additional calculations involved

Huh, long but useful post, isn't it? 😎

This tutorial branch tutorial-02-rotate-cannon on GitHub https://github.com/v-pukman-gd/pegball-tutorial/tree/tutorial-02-rotate-cannon

The current version of cannon.gd script including all possible solutions: https://github.com/v-pukman-gd/pegball-tutorial/blob/tutorial-02-rotate-cannon/cannon.gd

Comments

Popular posts from this blog

Peggle game in Godot 3 > Tutorial 01 - Create a new project

My Vocabulary App Privacy Policy