virtual_joystick.gd 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. class_name VirtualJoystick
  2. extends Control
  3. ## A simple virtual joystick for touchscreens, with useful options.
  4. ## Github: https://github.com/MarcoFazioRandom/Virtual-Joystick-Godot
  5. # EXPORTED VARIABLE
  6. ## The color of the button when the joystick is pressed.
  7. @export var pressed_color := Color.GRAY
  8. ## If the input is inside this range, the output is zero.
  9. @export_range(0, 200, 1) var deadzone_size : float = 10
  10. ## The max distance the tip can reach.
  11. @export_range(0, 500, 1) var clampzone_size : float = 75
  12. enum Joystick_mode {
  13. FIXED, ## The joystick doesn't move.
  14. DYNAMIC, ## Every time the joystick area is pressed, the joystick position is set on the touched position.
  15. FOLLOWING ## When the finger moves outside the joystick area, the joystick will follow it.
  16. }
  17. ## If the joystick stays in the same position or appears on the touched position when touch is started
  18. @export var joystick_mode := Joystick_mode.FIXED
  19. enum Visibility_mode {
  20. ALWAYS, ## Always visible
  21. TOUCHSCREEN_ONLY, ## Visible on touch screens only
  22. WHEN_TOUCHED ## Visible only when touched
  23. }
  24. ## If the joystick is always visible, or is shown only if there is a touchscreen
  25. @export var visibility_mode := Visibility_mode.ALWAYS
  26. ## If true, the joystick uses Input Actions (Project -> Project Settings -> Input Map)
  27. @export var use_input_actions := true
  28. @export var action_left := "left"
  29. @export var action_right := "right"
  30. @export var action_up := "forwards"
  31. @export var action_down := "backwards"
  32. # PUBLIC VARIABLES
  33. ## If the joystick is receiving inputs.
  34. var is_pressed := false
  35. # The joystick output.
  36. var output := Vector2.ZERO
  37. # PRIVATE VARIABLES
  38. var _touch_index : int = -1
  39. @onready var _base := $Base
  40. @onready var _tip := $Base/Tip
  41. @onready var _base_default_position : Vector2 = _base.position
  42. @onready var _tip_default_position : Vector2 = _tip.position
  43. @onready var _default_color : Color = _tip.modulate
  44. # FUNCTIONS
  45. func _ready() -> void:
  46. if ProjectSettings.get_setting("input_devices/pointing/emulate_mouse_from_touch"):
  47. printerr("The Project Setting 'emulate_mouse_from_touch' should be set to False")
  48. if not ProjectSettings.get_setting("input_devices/pointing/emulate_touch_from_mouse"):
  49. printerr("The Project Setting 'emulate_touch_from_mouse' should be set to True")
  50. if not DisplayServer.is_touchscreen_available() and visibility_mode == Visibility_mode.TOUCHSCREEN_ONLY :
  51. hide()
  52. if visibility_mode == Visibility_mode.WHEN_TOUCHED:
  53. hide()
  54. func _input(event: InputEvent) -> void:
  55. if event is InputEventScreenTouch:
  56. if event.pressed:
  57. if _is_point_inside_joystick_area(event.position) and _touch_index == -1:
  58. 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)):
  59. if joystick_mode == Joystick_mode.DYNAMIC or joystick_mode == Joystick_mode.FOLLOWING:
  60. _move_base(event.position)
  61. if visibility_mode == Visibility_mode.WHEN_TOUCHED:
  62. show()
  63. _touch_index = event.index
  64. _tip.modulate = pressed_color
  65. _update_joystick(event.position)
  66. get_viewport().set_input_as_handled()
  67. elif event.index == _touch_index:
  68. _reset()
  69. if visibility_mode == Visibility_mode.WHEN_TOUCHED:
  70. hide()
  71. get_viewport().set_input_as_handled()
  72. elif event is InputEventScreenDrag:
  73. if event.index == _touch_index:
  74. _update_joystick(event.position)
  75. get_viewport().set_input_as_handled()
  76. func _move_base(new_position: Vector2) -> void:
  77. _base.global_position = new_position - _base.pivot_offset * get_global_transform_with_canvas().get_scale()
  78. func _move_tip(new_position: Vector2) -> void:
  79. _tip.global_position = new_position - _tip.pivot_offset * _base.get_global_transform_with_canvas().get_scale()
  80. func _is_point_inside_joystick_area(point: Vector2) -> bool:
  81. var x: bool = point.x >= global_position.x and point.x <= global_position.x + (size.x * get_global_transform_with_canvas().get_scale().x)
  82. var y: bool = point.y >= global_position.y and point.y <= global_position.y + (size.y * get_global_transform_with_canvas().get_scale().y)
  83. return x and y
  84. func _get_base_radius() -> Vector2:
  85. return _base.size * _base.get_global_transform_with_canvas().get_scale() / 2
  86. func _is_point_inside_base(point: Vector2) -> bool:
  87. var _base_radius = _get_base_radius()
  88. var center : Vector2 = _base.global_position + _base_radius
  89. var vector : Vector2 = point - center
  90. if vector.length_squared() <= _base_radius.x * _base_radius.x:
  91. return true
  92. else:
  93. return false
  94. func _update_joystick(touch_position: Vector2) -> void:
  95. var _base_radius = _get_base_radius()
  96. var center : Vector2 = _base.global_position + _base_radius
  97. var vector : Vector2 = touch_position - center
  98. vector = vector.limit_length(clampzone_size)
  99. if joystick_mode == Joystick_mode.FOLLOWING and touch_position.distance_to(center) > clampzone_size:
  100. _move_base(touch_position - vector)
  101. _move_tip(center + vector)
  102. if vector.length_squared() > deadzone_size * deadzone_size:
  103. is_pressed = true
  104. output = (vector - (vector.normalized() * deadzone_size)) / (clampzone_size - deadzone_size)
  105. else:
  106. is_pressed = false
  107. output = Vector2.ZERO
  108. if use_input_actions:
  109. if output.x > 0:
  110. Input.action_release(action_left)
  111. Input.action_press(action_right, output.x)
  112. else:
  113. Input.action_release(action_right)
  114. Input.action_press(action_left, -output.x)
  115. if output.y > 0:
  116. Input.action_release(action_up)
  117. Input.action_press(action_down, output.y)
  118. else:
  119. Input.action_release(action_down)
  120. Input.action_press(action_up, -output.y)
  121. func _reset():
  122. is_pressed = false
  123. output = Vector2.ZERO
  124. _touch_index = -1
  125. _tip.modulate = _default_color
  126. _base.position = _base_default_position
  127. _tip.position = _tip_default_position
  128. if use_input_actions:
  129. for action in [action_left, action_right, action_down, action_up]:
  130. Input.action_release(action)