class_name VirtualJoystick extends Control # https://github.com/MarcoFazioRandom/Virtual-Joystick-Godot #### EXPORTED VARIABLE #### ## The color modulation of the button when the joystick is in use. See [member CanvasItem.modulate]. @export var pressed_color: Color = Color.GRAY ## If the input is inside this range, the output is zero. @export_range(0, 200, 1) var deadzone_size : float = 10 ## The max distance the tip can reach. @export_range(0, 500, 1) var clampzone_size : float = 75 enum JoystickMode { FIXED, ## The joystick doesn't move. Default. DYNAMIC ## Every time the joystick area is pressed, the joystick position is set on the touched position. } ## Whether the joystick should move on pressed. @export var joystick_mode: JoystickMode = JoystickMode.FIXED enum VisibilityMode { ALWAYS, ## Always visible. Default. TOUCHSCREEN_ONLY ## Visible on touch screens only. } ## Whether or not the joystick is visible if there is no touchscreen @export var visibility_mode: VisibilityMode = VisibilityMode.ALWAYS ## If [code]true[/code], the value returned from [method Input.get_axis] using ## [member action_left], [member action_right], [member action_up], and [member action_down] ## are overridden with [member output] for each axis such that ## [code]Input.get_axis(action_left, action_right)[/code] should always equal [member output.x] and ## [code]Input.get_axis(action_up, action_down)[/code] should always equal [member output.y][br] ## See [b]Project → Project Settings → Input Map[/b][br] ## If you want to change the hotkeys for the default actions make sure [b]Show Built-in Action[/b] is checked. @export var use_input_actions := true ## Input action for left direction @export var action_left := "ui_left" ## Input action for right direction @export var action_right := "ui_right" ## Input action for up direction @export var action_up := "ui_up" ## Input action for down direction @export var action_down := "ui_down" #### PUBLIC VARIABLES #### ## If [code]true[/code], the joystick's state is pressed. Means the joystick will receive inputs. var pressed := false : get = is_pressed func is_pressed() -> bool: return pressed ## Current position of the joystick. The value's length ranges from [code]0[/code] to [code]1.0[/code]. var output := Vector2.ZERO : get = get_output func get_output() -> Vector2: return output #### PRIVATE VARIABLES #### var _touch_index : int = -1 @onready var _base := $Base @onready var _tip := $Base/Tip @onready var _base_radius = _base.size * _base.get_global_transform_with_canvas().get_scale() / 2 @onready var _base_default_position : Vector2 = _base.position @onready var _tip_default_position : Vector2 = _tip.position @onready var _default_color : Color = _tip.modulate #### FUNCTIONS #### func _ready() -> void: if not DisplayServer.is_touchscreen_available() and visibility_mode == VisibilityMode.TOUCHSCREEN_ONLY: hide() func _input(event: InputEvent) -> void: if event is InputEventScreenTouch: if event.pressed: if _is_point_inside_joystick_area(event.position) and _touch_index == -1: if joystick_mode == JoystickMode.DYNAMIC or (joystick_mode == JoystickMode.FIXED and _is_point_inside_base(event.position)): if joystick_mode == JoystickMode.DYNAMIC: _move_base(event.position) _touch_index = event.index _tip.modulate = pressed_color _update_joystick(event.position) get_viewport().set_input_as_handled() elif event.index == _touch_index: _reset() get_viewport().set_input_as_handled() elif event is InputEventScreenDrag: if event.index == _touch_index: _update_joystick(event.position) get_viewport().set_input_as_handled() func _move_base(new_position: Vector2) -> void: _base.global_position = new_position - _base.pivot_offset * get_global_transform_with_canvas().get_scale() func _move_tip(new_position: Vector2) -> void: _tip.global_position = new_position - _tip.pivot_offset * _base.get_global_transform_with_canvas().get_scale() func _is_point_inside_joystick_area(point: Vector2) -> bool: var x: bool = point.x >= global_position.x and point.x <= global_position.x + (size.x * get_global_transform_with_canvas().get_scale().x) var y: bool = point.y >= global_position.y and point.y <= global_position.y + (size.y * get_global_transform_with_canvas().get_scale().y) return x and y func _is_point_inside_base(point: Vector2) -> bool: var center : Vector2 = _base.global_position + _base_radius var vector : Vector2 = point - center if vector.length_squared() <= _base_radius.x * _base_radius.x: return true else: return false func _update_joystick(touch_position: Vector2) -> void: var center : Vector2 = _base.global_position + _base_radius var vector : Vector2 = touch_position - center vector = vector.limit_length(clampzone_size) _move_tip(center + vector) if vector.length_squared() > deadzone_size * deadzone_size: pressed = true output = (vector - (vector.normalized() * deadzone_size)) / (clampzone_size - deadzone_size) else: pressed = false output = Vector2.ZERO if use_input_actions: _update_input_actions() func _update_input_actions(): if output.x < 0: Input.action_press(action_left, -output.x) elif Input.is_action_pressed(action_left): Input.action_release(action_left) if output.x > 0: Input.action_press(action_right, output.x) elif Input.is_action_pressed(action_right): Input.action_release(action_right) if output.y < 0: Input.action_press(action_up, -output.y) elif Input.is_action_pressed(action_up): Input.action_release(action_up) if output.y > 0: Input.action_press(action_down, output.y) elif Input.is_action_pressed(action_down): Input.action_release(action_down) func _reset(): pressed = false output = Vector2.ZERO _touch_index = -1 _tip.modulate = _default_color _base.position = _base_default_position _tip.position = _tip_default_position if use_input_actions: if Input.is_action_pressed(action_left): Input.action_release(action_left) if Input.is_action_pressed(action_right): Input.action_release(action_right) if Input.is_action_pressed(action_down): Input.action_release(action_down) if Input.is_action_pressed(action_up): Input.action_release(action_up) func _on_button_pressed(): Input.action_press("jump")