Better way to snap Vector2 to Cardinal directions?

Godot Version

v4.2.2.stable.mono.official [15073afe3]

This is not urgent.

This is my code to rotate a third person camera with a gamepad. And it works. But the way I make it snap to the cardinal directions (to easy rotation on just one axis, and ensure consistent rotation speed) is just quite bad. I tried looking for a method in Vector2 but could not find anything that helps with this.

If anyone has a better way to doing this, please let me know.

the snapping to zero could also be better but its not as bad…

var joystick_camera = Input.get_vector("camera_left", "camera_right", "camera_down", "camera_up")
	
	if joystick_camera.x > CARDINAL_SNAP:
		joystick_camera.x = 1
	if joystick_camera.x < CARDINAL_SNAP * -1:
		joystick_camera.x = -1
	if joystick_camera.y > CARDINAL_SNAP:
		joystick_camera.y = 1
	if joystick_camera.y < CARDINAL_SNAP * -1:
		joystick_camera.y = -1
	
	if abs(joystick_camera.x) < CENTRE_SNAP:
		joystick_camera.x = 0
	if abs(joystick_camera.y) < CENTRE_SNAP:
		joystick_camera.y = 0
	
	if joystick_camera != Vector2.ZERO:
		gimbal.rotation_degrees.y -= joystick_camera.x
		gimbal.rotation_degrees.y = wrapf(gimbal.rotation_degrees.y, 0, 360)
		gimbal.rotation_degrees.x += joystick_camera.y
		gimbal.rotation_degrees.x = clamp(gimbal.rotation_degrees.x, -60, 40)

Input.get_vector() uses a circular deadzone, but in this case a square deadzone would probably work better. If you set appropriate deadzones to the actions in the project setting, you can simplify the code like this:

var joystick_camera = Vector2(
    signf(Input.get_axis("camera_left", "camera_right")),
    signf(Input.get_axis("camera_down", "camera_up"))
)
3 Likes

This sounds like a fun challenge, so basically ‘how would you improve this that still produces the same result’, eh? Since ‘better’ is a very personal thing, as a programmer, and you didn’t exactly specify, I will approach this with my own priorities, which is readable code (even if that makes it longer), and reasonably simple and intuitive to adjust or change in the future. It might make sacrifices on execution speed to maintain readable code, and supporting features that are not currently used. That’s just how I am, but if you have other more specific criteria, say so.

That said. Hmm… how about something like:

const JOYCAM_SUBDIVS : int = 4
const JOYCAM_DEADZONE : float = 0.3
const JOYCAM_TURNSPEED : Vector2 = Vector2(1.0, 0.5)
const _RADIANS_PER_SUBDIV_ : float = TAU / float(JOYCAM_SUBDIVS)
const _JOYCAM_DEADZONE_SQRD_ : float = JOYCAM_DEADZONE * JOYCAM_DEADZONE
const _JOYCAM_DEADZONE_INVSCALE_ : float = 1.0 / ( 1.0 - JOYCAM_DEADZONE );

var any_input : bool = false
var input_snapped : Vector2 = Vector2.ZERO
var input_index : int = -1

var _raw:Vector2 = Input.get_vector("camera_left", "camera_right", "camera_down", "camera_up")

if ( !_raw.is_zero_approx() && _raw.length_squared > _JOYCAM_DEADZONE_SQRD_ ):
    any_input = true
    input_index = int( floor( _raw.angle() / _RADIANS_PER_SUBDIV_ ) )
    var _adj_length = ( _raw.length() -  JOYCAM_DEADZONE ) * _JOYCAM_DEADZONE_INVSCALE_;
    input_snapped = _adj_length * Vector2.from_angle ( float( _angle_index ) * _RADIANS_PER_SUBDIV_ )

if (any_input):
    var _gimbal_rot = gimbal.rotation_degrees
    _gimbal_rot.y = wrapf( _gimbal_rot.y - ( input_snapped.x * JOYCAM_TURNSPEED.x ) )
    _gimbal_rot.x = wrapf( _gimbal_rot.x + ( input_snapped.y * JOYCAM_TURNSPEED.y ) )
    gimbal.rotation_degrees = _gimbal_rot

So this is untested, but I think it’ll go, try it out. If no-go, say so and I’ll spin up a test scene.

edit: some notes maybe good, right? ok so JOYCAM_SUBDIVS to 4 if you only want NSEW, but if you want the diagonals too, try 8. 16 might be cool too. or multiples of 6, for isometric maybe. JOYCAM_DEADZONE is what it sounds like (you call it CENTRE_SNAP). JOYCAM_TURNSPEED scales the turning, and it’s a vector2 so the horizontal and vertical turning can happen at different rates.

if there’s input, any_input becomes true, input_index becomes a valid value (which direction number was indicated), and input_snapped becomes the input snapped to the nearest increment direction. The reason I use the any_input flag, instead of just putting all of it inside the first if…then is because maybe other things provide input, that aren’t the joycam stick. Maybe another script can provide input for cinematics, or maybe there’s network/remote input. So this leaves the option open, structurally (but it probably does add a couple clock cycles overall).

edit (2): Changed one thing about how input length is returned, so at deadzone + 0.0001 it’s basically 0, and at 1.0 it’s 1.0. Should be smoother to use and control.

edit (3): No changes but adding a suggestion: scale the turning by delta, within a process or physics_process. Probably not 100% necessary if this is within (or called by) a physics_process anyways.

1 Like

I see what you mean, but this would also make it, so that if the analog stick is in a diagonal position, that both vertical and horizontal movement would be at maximum speed. Right now I am unsure if this is desirable, but I will try it out.

Thanks!

If you decide that diagonal movement should work with the same speed as cardinal directions, you can achieve this by normalizing the input vector.

joystick_camera = joystick_camera.normalized()

And if you want to disable diagonal movement, the easiest solution is to compare the absolute values of the x and y components.

var joystick_camera = Vector2()
var input_x = Input.get_axis("camera_left", "camera_right")
var input_y = Input.get_axis("camera_down", "camera_up")
if absf(input_x) > absf(input_y):
    joystick_camera.x = signf(input_x)
else:
    joystick_camera.y = signf(input_y)
1 Like

Would clamp help?