extends Control # VirtualJoystick - Touch joystick for mobile movement control # Provides 8-directional movement input signal direction_changed(direction: Vector2i) signal joystick_released @export var dead_zone: float = 0.2 @export var clamp_zone: float = 0.8 @export var joystick_radius: float = 60.0 @export var knob_radius: float = 25.0 @export var repeat_delay: float = 0.3 # Initial delay before repeat @export var repeat_rate: float = 0.15 # Repeat rate for continuous movement var base_color: Color = Color(1, 1, 1, 0.4) var knob_color: Color = Color(1, 1, 1, 0.7) var pressed_color: Color = Color(0.4, 0.9, 0.4, 0.8) var is_pressed: bool = false var touch_index: int = -1 var center_position: Vector2 var current_direction: Vector2 = Vector2.ZERO var last_grid_direction: Vector2i = Vector2i.ZERO var _repeat_timer: float = 0.0 var _initial_repeat: bool = true func _ready(): _update_minimum_size() center_position = size / 2 # Enable touch input mouse_filter = Control.MOUSE_FILTER_STOP set_process(true) func set_radius(new_radius: float): joystick_radius = new_radius # Proportional knob knob_radius = new_radius * (25.0 / 60.0) _update_minimum_size() # Optional: recalculate center if already initialized if is_inside_tree(): center_position = size / 2 queue_redraw() func _update_minimum_size(): custom_minimum_size = Vector2(joystick_radius * 2 + 40, joystick_radius * 2 + 40) size = custom_minimum_size func _draw(): # Draw base circle var base_circle_color = pressed_color if is_pressed else base_color draw_circle(center_position, joystick_radius, base_circle_color) draw_arc(center_position, joystick_radius, 0, TAU, 64, Color.WHITE, 2.0) # Draw knob var knob_pos = center_position + current_direction * joystick_radius * clamp_zone var knob_circle_color = pressed_color if is_pressed else knob_color draw_circle(knob_pos, knob_radius, knob_circle_color) draw_arc(knob_pos, knob_radius, 0, TAU, 32, Color.WHITE, 1.5) # Draw direction indicators (8 directions) for i in range(8): var angle = i * TAU / 8 var line_start = center_position + Vector2.from_angle(angle) * (joystick_radius * 0.6) var line_end = center_position + Vector2.from_angle(angle) * (joystick_radius * 0.9) draw_line(line_start, line_end, Color(1, 1, 1, 0.3), 2.0) func _process(delta: float): # Handle continuous movement while holding joystick if is_pressed and last_grid_direction != Vector2i.ZERO: _repeat_timer -= delta if _repeat_timer <= 0: emit_signal("direction_changed", last_grid_direction) _repeat_timer = repeat_rate _initial_repeat = false func _gui_input(event: InputEvent): if event is InputEventScreenTouch: if event.pressed: _start_touch(event.index, event.position) elif event.index == touch_index: _end_touch() elif event is InputEventScreenDrag: if event.index == touch_index: _update_touch(event.position) # Mouse support for testing elif event is InputEventMouseButton: if event.button_index == MOUSE_BUTTON_LEFT: if event.pressed: _start_touch(0, event.position) else: _end_touch() elif event is InputEventMouseMotion: if is_pressed and touch_index == 0: _update_touch(event.position) func _start_touch(index: int, pos: Vector2): is_pressed = true touch_index = index _repeat_timer = repeat_delay # Use longer initial delay _initial_repeat = true _update_touch(pos) queue_redraw() func _end_touch(): is_pressed = false touch_index = -1 current_direction = Vector2.ZERO last_grid_direction = Vector2i.ZERO _repeat_timer = 0.0 _initial_repeat = true emit_signal("joystick_released") queue_redraw() func _update_touch(pos: Vector2): var diff = pos - center_position var distance = diff.length() if distance > 0: current_direction = diff.normalized() * clampf(distance / joystick_radius, 0, clamp_zone) else: current_direction = Vector2.ZERO # Convert to 8-directional grid movement var grid_dir = _get_grid_direction(current_direction) if grid_dir != last_grid_direction: last_grid_direction = grid_dir if grid_dir != Vector2i.ZERO: emit_signal("direction_changed", grid_dir) queue_redraw() func _get_grid_direction(dir: Vector2) -> Vector2i: if dir.length() < dead_zone: return Vector2i.ZERO # Determine 8-directional output var angle = dir.angle() # Divide circle into 8 sectors (each 45 degrees) var sector = int(round(angle / (TAU / 8))) % 8 if sector < 0: sector += 8 match sector: 0: return Vector2i(1, 0) # East 1: return Vector2i(1, 1) # Southeast 2: return Vector2i(0, 1) # South 3: return Vector2i(-1, 1) # Southwest 4: return Vector2i(-1, 0) # West 5: return Vector2i(-1, -1) # Northwest 6: return Vector2i(0, -1) # North 7: return Vector2i(1, -1) # Northeast return Vector2i.ZERO func get_direction() -> Vector2i: """Get the current grid direction for polling.""" return last_grid_direction