128 lines
4.3 KiB
GDScript
128 lines
4.3 KiB
GDScript
class_name TouchJoystick
|
|
extends Control
|
|
|
|
# Touch joystick for steering the fleet. Lives in the bottom-left corner.
|
|
#
|
|
# Behaves like a "floating" stick within its own rect: the base re-centres
|
|
# on wherever the finger first lands, so the player doesn't have to hit an
|
|
# exact spot on a phone screen they can't feel. The knob then follows the
|
|
# finger out to max_radius, and `direction` reports the offset normalised
|
|
# against that radius (so magnitude is 0..1, usable for throttle later).
|
|
#
|
|
# Releasing puts BOTH the base and the knob back in the middle of the rect, so
|
|
# an idle stick always looks the same and always looks idle.
|
|
#
|
|
# Only claims touches that start inside its own rect - anything else falls
|
|
# through untouched, which is what keeps pinch-to-zoom working everywhere
|
|
# else on screen.
|
|
|
|
signal direction_changed(direction: Vector2)
|
|
signal released
|
|
|
|
# How far the knob travels from centre, as a fraction of the control's
|
|
# shorter side. Derived rather than a fixed number of pixels so that
|
|
# enlarging the stick's rect enlarges the ring the player actually sees -
|
|
# otherwise the touch area grows and the visible control stays small.
|
|
# The scene zones are deliberately wider than they are tall. Forty percent
|
|
# makes the resting ring just over twice its former radius while leaving room
|
|
# for the floating base to move inside the touch rect.
|
|
const RADIUS_RATIO: float = 0.40
|
|
|
|
@export var max_radius: float = 90.0
|
|
@export var deadzone: float = 0.12
|
|
@export var glow_color: Color = Color(0.25, 0.85, 1.0)
|
|
@export var core_color: Color = Color(1.0, 1.0, 1.0)
|
|
|
|
var direction: Vector2 = Vector2.ZERO
|
|
var active_touch_index: int = -1
|
|
var base_pos: Vector2 = Vector2.ZERO
|
|
var knob_pos: Vector2 = Vector2.ZERO
|
|
|
|
|
|
func _ready() -> void:
|
|
resized.connect(_relayout)
|
|
_relayout()
|
|
|
|
|
|
# Where the stick sits when nobody is touching it: the middle of its own rect.
|
|
# The base only moves away from here while a finger is down (see handle_touch),
|
|
# and returns the moment it lifts.
|
|
func home_pos() -> Vector2:
|
|
return size / 2.0
|
|
|
|
|
|
func _relayout() -> void:
|
|
base_pos = home_pos()
|
|
knob_pos = base_pos
|
|
max_radius = minf(size.x, size.y) * RADIUS_RATIO
|
|
queue_redraw()
|
|
|
|
|
|
func is_active() -> bool:
|
|
return active_touch_index != -1
|
|
|
|
|
|
# Returns true if the event was consumed, so the caller knows not to treat
|
|
# it as a world gesture (a fleet move, or one finger of a pinch).
|
|
func handle_touch(event: InputEventScreenTouch) -> bool:
|
|
var local: Vector2 = event.position - global_position
|
|
if event.pressed:
|
|
if active_touch_index != -1:
|
|
return false
|
|
if not Rect2(Vector2.ZERO, size).has_point(local):
|
|
return false
|
|
active_touch_index = event.index
|
|
base_pos = local
|
|
knob_pos = local
|
|
_update_direction()
|
|
return true
|
|
|
|
if event.index != active_touch_index:
|
|
return false
|
|
active_touch_index = -1
|
|
# Back to the middle of the rect, BASE included. This used to reset the
|
|
# knob onto base_pos and leave the base wherever the finger had landed, so
|
|
# a released stick stayed parked off-centre - which reads as a control
|
|
# still in use, and left the ring somewhere different every time.
|
|
base_pos = home_pos()
|
|
knob_pos = base_pos
|
|
direction = Vector2.ZERO
|
|
queue_redraw()
|
|
direction_changed.emit(direction)
|
|
released.emit()
|
|
return true
|
|
|
|
|
|
func handle_drag(event: InputEventScreenDrag) -> bool:
|
|
if event.index != active_touch_index:
|
|
return false
|
|
knob_pos = event.position - global_position
|
|
_update_direction()
|
|
return true
|
|
|
|
|
|
func _update_direction() -> void:
|
|
var offset: Vector2 = knob_pos - base_pos
|
|
if offset.length() > max_radius:
|
|
offset = offset.normalized() * max_radius
|
|
knob_pos = base_pos + offset
|
|
var raw: Vector2 = offset / max_radius
|
|
direction = Vector2.ZERO if raw.length() < deadzone else raw
|
|
queue_redraw()
|
|
direction_changed.emit(direction)
|
|
|
|
|
|
func _draw() -> void:
|
|
var ring: PackedVector2Array = PackedVector2Array()
|
|
var segments: int = 28
|
|
for i in range(segments):
|
|
var a: float = (float(i) / segments) * TAU
|
|
ring.append(base_pos + Vector2(cos(a), sin(a)) * max_radius)
|
|
NeonDraw.draw_glowing_polygon(self, ring, glow_color, 0.55, core_color)
|
|
|
|
var knob: PackedVector2Array = PackedVector2Array()
|
|
var knob_radius: float = max_radius * 0.34
|
|
for i in range(segments):
|
|
var a: float = (float(i) / segments) * TAU
|
|
knob.append(knob_pos + Vector2(cos(a), sin(a)) * knob_radius)
|
|
NeonDraw.draw_glowing_polygon(self, knob, glow_color, 1.0 if is_active() else 0.75, core_color)
|