123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167 |
- class_name VirtualJoystick
- extends Control
- ## A simple virtual joystick for touchscreens, with useful options.
- ## Github: https://github.com/MarcoFazioRandom/Virtual-Joystick-Godot
- # EXPORTED VARIABLE
- ## The color of the button when the joystick is pressed.
- @export var pressed_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 Joystick_mode {
- FIXED, ## The joystick doesn't move.
- DYNAMIC, ## Every time the joystick area is pressed, the joystick position is set on the touched position.
- FOLLOWING ## When the finger moves outside the joystick area, the joystick will follow it.
- }
- ## If the joystick stays in the same position or appears on the touched position when touch is started
- @export var joystick_mode := Joystick_mode.FIXED
- enum Visibility_mode {
- ALWAYS, ## Always visible
- TOUCHSCREEN_ONLY, ## Visible on touch screens only
- WHEN_TOUCHED ## Visible only when touched
- }
- ## If the joystick is always visible, or is shown only if there is a touchscreen
- @export var visibility_mode := Visibility_mode.ALWAYS
- ## If true, the joystick uses Input Actions (Project -> Project Settings -> Input Map)
- @export var use_input_actions := true
- @export var action_left := "left"
- @export var action_right := "right"
- @export var action_up := "forwards"
- @export var action_down := "backwards"
- # PUBLIC VARIABLES
- ## If the joystick is receiving inputs.
- var is_pressed := false
- # The joystick output.
- var output := Vector2.ZERO
- # PRIVATE VARIABLES
- var _touch_index : int = -1
- @onready var _base := $Base
- @onready var _tip := $Base/Tip
- @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 ProjectSettings.get_setting("input_devices/pointing/emulate_mouse_from_touch"):
- printerr("The Project Setting 'emulate_mouse_from_touch' should be set to False")
- if not ProjectSettings.get_setting("input_devices/pointing/emulate_touch_from_mouse"):
- printerr("The Project Setting 'emulate_touch_from_mouse' should be set to True")
-
- if not DisplayServer.is_touchscreen_available() and visibility_mode == Visibility_mode.TOUCHSCREEN_ONLY :
- hide()
-
- if visibility_mode == Visibility_mode.WHEN_TOUCHED:
- 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 == Joystick_mode.DYNAMIC or joystick_mode == Joystick_mode.FOLLOWING or (joystick_mode == Joystick_mode.FIXED and _is_point_inside_base(event.position)):
- if joystick_mode == Joystick_mode.DYNAMIC or joystick_mode == Joystick_mode.FOLLOWING:
- _move_base(event.position)
- if visibility_mode == Visibility_mode.WHEN_TOUCHED:
- show()
- _touch_index = event.index
- _tip.modulate = pressed_color
- _update_joystick(event.position)
- get_viewport().set_input_as_handled()
- elif event.index == _touch_index:
- _reset()
- if visibility_mode == Visibility_mode.WHEN_TOUCHED:
- hide()
- 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 _get_base_radius() -> Vector2:
- return _base.size * _base.get_global_transform_with_canvas().get_scale() / 2
- func _is_point_inside_base(point: Vector2) -> bool:
- var _base_radius = _get_base_radius()
- 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 _base_radius = _get_base_radius()
- var center : Vector2 = _base.global_position + _base_radius
- var vector : Vector2 = touch_position - center
- vector = vector.limit_length(clampzone_size)
-
- if joystick_mode == Joystick_mode.FOLLOWING and touch_position.distance_to(center) > clampzone_size:
- _move_base(touch_position - vector)
-
- _move_tip(center + vector)
-
- if vector.length_squared() > deadzone_size * deadzone_size:
- is_pressed = true
- output = (vector - (vector.normalized() * deadzone_size)) / (clampzone_size - deadzone_size)
- else:
- is_pressed = false
- output = Vector2.ZERO
-
- if use_input_actions:
- if output.x > 0:
- Input.action_release(action_left)
- Input.action_press(action_right, output.x)
- else:
- Input.action_release(action_right)
- Input.action_press(action_left, -output.x)
- if output.y > 0:
- Input.action_release(action_up)
- Input.action_press(action_down, output.y)
- else:
- Input.action_release(action_down)
- Input.action_press(action_up, -output.y)
- func _reset():
- is_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:
- for action in [action_left, action_right, action_down, action_up]:
- Input.action_release(action)
|