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
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 angle, angle_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
Post a Comment