Godot Version
Godot 4.4.1
Question
I’m trying to make a rotation controls similar to Worms games, but I need help. Basically I just need a simple bug-free aim solution where you only use directional keys (like arrows or WASD): pressing up/down rotates aim within 180 degree vertical angle and pressing left/right flips horizontally to the other side.
I’m planning to make mouse implementation too, but after the core mechanics with keys is working I can continue from there. I’ve been trying to do this for years, but everytime I just get messed up with degrees, radians or whatsoever. I suck at maths.
My current aim node implementation is below. It’s messy and buggy, a guide book example of how to NOT do it. Do you know any better approach?
Summary
@export var aim_speed: float = 6.0
var flip_h: bool = true
var aim_direction: float = 0.0:
set(value):
aim_direction = value
aimed.emit(aim_direction)
get:
return wrapf(aim_direction,-90.0,270.0)
signal aimed(direction)
func side(check: int) -> void:
var prev_side = flip_h
if check > 0: flip_h = true
elif check < 0: flip_h = false
if prev_side != flip_h: aim_direction = 180 - aim_direction
func aim(axis: float, direction: float, speed_factor: float = 3.0) -> void:
var d: float = axis * (aim_speed / speed_factor)
if d != 0.0: #if pressed either up or down
if !flip_h: aim_direction = clamp(aim_direction - d,90,270)
elif aim_direction <= 90: aim_direction = clamp(aim_direction+d,-90,90)
elif aim_direction >= 270: aim_direction = clamp(aim_direction+d,270,450)
Hi!
Here’s a suggestion, probably not the cleaner solution, but when people say they struggle with maths, I tend to suggest using the engine tools directly instead of hard coding some maths you’ll have trouble iterating on.
So, you could split up your character tree into multiple nodes, like this:
The idea being:
- Aim_Flip is centered, and its scale will be used to flip the aim.
- Aim_Pivot_Rotation is also centered, and its rotation will be used to adjust the angle in a 180° cone (it does not need to handle flip, as its parent will do it for him).
- Weapon is just a placeholder sprite to show the result.
The code I wrote is attached on Aim_Flip and looks like this (please note that I wrote it pretty quickly so there may be some obvious improvements left):
func _process(delta: float) -> void:
aim(delta, 90)
flip()
func aim(delta: float, speed: float):
var pivot_rotation = $Aim_Pivot_Rotation
if Input.is_key_pressed(KEY_UP):
pivot_rotation.rotation_degrees -= speed * delta
elif Input.is_key_pressed(KEY_DOWN):
pivot_rotation.rotation_degrees += speed * delta
pivot_rotation.rotation_degrees = clamp(pivot_rotation.rotation_degrees, -90, 90)
func flip():
if Input.is_key_pressed(KEY_LEFT):
scale.x = -1
elif Input.is_key_pressed(KEY_RIGHT):
scale.x = 1
Basically, two functions are called:
- aim(), that handles up/down keys and clamps rotation between -90 and 90. I’m using rotation_degrees, since you don’t like maths, I guessed degrees were easier to understand than radians.

- flip(), that handles left/right keys to scale the weapon.

Once both functions are called, you can access the weapon global rotation to get the current aim, and do whatever you need to do with it (fire bullets, display a preview curve, etc.).
Let me know if that works for you and if you need more explanations/a different suggestion.
1 Like
This works perfectly, thank you! I implemented the idea quite heavily to my architecture where the aim code is already in a separate aimer module. Anyway, that’s great idea to limit rotation always within 180 degrees and handle the rest with flipping. When I tried to use the whole 360 degrees, it always had these weird edge cases where you had to guess when the degree wraps take in place: at 0, -90, 270, 360 or somewhere else? Between -90 and 90 there’s less hassle to deal with. Now I wish I had came up with this simple trick by myself 
My full aimer module:
Summary
class_name Aimer extends Node2D
@export var aim_speed: float = 6.0
@onready var pivot_rotation: Marker2D = Marker2D.new()
signal aimed(direction)
func _ready() -> void:
add_child(pivot_rotation)
func side(check: int) -> void:
if check > 0: scale.x = 1
elif check < 0: scale.x = -1
func aim(axis: float, direction: float, speed_factor: float = 3.0) -> void:
pivot_rotation.rotation_degrees += axis * (aim_speed / speed_factor)
pivot_rotation.rotation_degrees = clamp(pivot_rotation.rotation_degrees, -90, 90)
aimed.emit(rotation_degrees)
queue_redraw()
func _draw() -> void:
var aim_pos = (Vector2.RIGHT*64).rotated(deg_to_rad(pivot_rotation.rotation_degrees))
draw_circle(aim_pos,4.0,Color.RED)
(Not sure if I should also include process delta though.)
And I get the direction from aimer module with: aimer.pivot_rotation.global_rotation_degrees
1 Like
Well, I’d lie if I said I never had this kind of issue a few years ago, and I probably found some help online too
that’s why this forum exists.
Anyway, glad I could help!
1 Like