Gamepad viewer

Sticks, triggers and buttons read straight from Input and drawn as they move. With nothing plugged in it says so, and the arrow keys still drive the ui vector.

The editor above is running this project. Change a line and it reloads.

Godot classes used

InputProgressBarPanelStyleBoxFlat

The code

scripts/main.gd
extends Node

const STICK_RADIUS: float = 76.0
const KNOB_RADIUS: float = 20.0
const DEAD_ZONE: float = 0.18

const BUTTONS: Array[Dictionary] = [
	{name = 'A', id = JOY_BUTTON_A},
	{name = 'B', id = JOY_BUTTON_B},
	{name = 'X', id = JOY_BUTTON_X},
	{name = 'Y', id = JOY_BUTTON_Y},
	{name = 'LB', id = JOY_BUTTON_LEFT_SHOULDER},
	{name = 'RB', id = JOY_BUTTON_RIGHT_SHOULDER},
	{name = 'L3', id = JOY_BUTTON_LEFT_STICK},
	{name = 'R3', id = JOY_BUTTON_RIGHT_STICK},
	{name = 'Back', id = JOY_BUTTON_BACK},
	{name = 'Start', id = JOY_BUTTON_START},
	{name = 'Up', id = JOY_BUTTON_DPAD_UP},
	{name = 'Down', id = JOY_BUTTON_DPAD_DOWN},
	{name = 'Left', id = JOY_BUTTON_DPAD_LEFT},
	{name = 'Right', id = JOY_BUTTON_DPAD_RIGHT},
]

var device: int = -1
var status: Label
var readout: Label
var knobs: Array[Panel] = []
var pills: Array[StyleBoxFlat] = []
var triggers: Array[ProgressBar] = []


func _ready() -> void:
	var column: VBoxContainer = _root()

	var title: Label = Label.new()
	title.text = 'Gamepad viewer'
	title.add_theme_font_size_override('font_size', 28)
	column.add_child(title)

	status = Label.new()
	column.add_child(status)

	var decks: HBoxContainer = HBoxContainer.new()
	decks.add_theme_constant_override('separation', 40)
	decks.alignment = BoxContainer.ALIGNMENT_CENTER
	column.add_child(decks)

	decks.add_child(_stick('left stick'))
	decks.add_child(_trigger_deck())
	decks.add_child(_stick('right stick'))

	var grid: GridContainer = GridContainer.new()
	grid.columns = 7
	grid.add_theme_constant_override('h_separation', 10)
	grid.add_theme_constant_override('v_separation', 10)
	column.add_child(grid)

	for entry: Dictionary in BUTTONS:
		grid.add_child(_pill(entry.name))

	readout = Label.new()
	readout.add_theme_color_override('font_color', Color('#8da5f3'))
	column.add_child(readout)

	Input.joy_connection_changed.connect(_on_connection_changed)
	_refresh_device()

	print('move the sticks and press the buttons, the arrow keys also drive the ui vector')


func _process(_delta: float) -> void:
	var left: Vector2 = _stick_value(JOY_AXIS_LEFT_X, JOY_AXIS_LEFT_Y)
	var right: Vector2 = _stick_value(JOY_AXIS_RIGHT_X, JOY_AXIS_RIGHT_Y)

	_move_knob(knobs[0], left)
	_move_knob(knobs[1], right)

	triggers[0].value = _axis(JOY_AXIS_TRIGGER_LEFT)
	triggers[1].value = _axis(JOY_AXIS_TRIGGER_RIGHT)

	for i in BUTTONS.size():
		var down: bool = device >= 0 and Input.is_joy_button_pressed(device, BUTTONS[i].id)
		pills[i].bg_color = Color('#8eef97') if down else Color('#2a2f3d')

	var digital: Vector2 = Input.get_vector(&'ui_left', &'ui_right', &'ui_up', &'ui_down')
	readout.text = 'left %.2f, %.2f    right %.2f, %.2f    ui_* vector %.2f, %.2f' % [left.x, left.y, right.x, right.y, digital.x, digital.y]


func _on_connection_changed(_which: int, _connected: bool) -> void:
	_refresh_device()


func _refresh_device() -> void:
	var pads: Array[int] = Input.get_connected_joypads()
	device = pads[0] if pads.size() > 0 else -1

	if device < 0:
		status.text = 'no gamepad connected, plug one in and press a button'
		status.add_theme_color_override('font_color', Color('#fc7f7f'))
		return

	status.text = 'device %d: %s' % [device, Input.get_joy_name(device)]
	status.add_theme_color_override('font_color', Color('#8eef97'))


func _axis(axis: JoyAxis) -> float:
	return Input.get_joy_axis(device, axis) if device >= 0 else 0.0


func _stick_value(x_axis: JoyAxis, y_axis: JoyAxis) -> Vector2:
	var raw: Vector2 = Vector2(_axis(x_axis), _axis(y_axis))

	return Vector2.ZERO if raw.length() < DEAD_ZONE else raw


func _move_knob(knob: Panel, value: Vector2) -> void:
	knob.position = Vector2(STICK_RADIUS, STICK_RADIUS) + value * (STICK_RADIUS - KNOB_RADIUS) - Vector2(KNOB_RADIUS, KNOB_RADIUS)


func _stick(caption: String) -> VBoxContainer:
	var column: VBoxContainer = VBoxContainer.new()
	column.add_theme_constant_override('separation', 10)

	var disc: Panel = _disc(STICK_RADIUS, Color('#1a1e28'), Color('#2a2f3d'))
	disc.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
	column.add_child(disc)

	var knob: Panel = _disc(KNOB_RADIUS, Color('#8da5f3'), Color('#8da5f3'))
	disc.add_child(knob)
	knobs.append(knob)
	_move_knob(knob, Vector2.ZERO)

	var label: Label = Label.new()
	label.text = caption
	label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	label.add_theme_color_override('font_color', Color('#9aa3b5'))
	column.add_child(label)

	return column


func _trigger_deck() -> VBoxContainer:
	var column: VBoxContainer = VBoxContainer.new()
	column.add_theme_constant_override('separation', 10)
	column.alignment = BoxContainer.ALIGNMENT_CENTER

	for caption in ['LT', 'RT']:
		var label: Label = Label.new()
		label.text = caption
		label.add_theme_color_override('font_color', Color('#9aa3b5'))
		column.add_child(label)

		var bar: ProgressBar = ProgressBar.new()
		bar.custom_minimum_size = Vector2(170, 20)
		bar.max_value = 1.0
		bar.show_percentage = false
		bar.add_theme_stylebox_override('background', _bar_style(Color('#1a1e28')))
		bar.add_theme_stylebox_override('fill', _bar_style(Color('#f3d48d')))
		column.add_child(bar)
		triggers.append(bar)

	return column


func _pill(caption: String) -> PanelContainer:
	var style: StyleBoxFlat = StyleBoxFlat.new()
	style.bg_color = Color('#2a2f3d')
	style.set_corner_radius_all(8)
	style.set_content_margin_all(10)

	var box: PanelContainer = PanelContainer.new()
	box.custom_minimum_size = Vector2(74, 0)
	box.add_theme_stylebox_override('panel', style)
	pills.append(style)

	var label: Label = Label.new()
	label.text = caption
	label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	box.add_child(label)

	return box


func _disc(radius: float, fill: Color, border: Color) -> Panel:
	var style: StyleBoxFlat = StyleBoxFlat.new()
	style.bg_color = fill
	style.border_color = border
	style.set_border_width_all(2)
	style.set_corner_radius_all(int(radius))

	var disc: Panel = Panel.new()
	disc.custom_minimum_size = Vector2(radius * 2, radius * 2)
	disc.size = Vector2(radius * 2, radius * 2)
	disc.add_theme_stylebox_override('panel', style)

	return disc


func _bar_style(color: Color) -> StyleBoxFlat:
	var style: StyleBoxFlat = StyleBoxFlat.new()
	style.bg_color = color
	style.set_corner_radius_all(6)

	return style


func _root() -> VBoxContainer:
	var background: ColorRect = ColorRect.new()
	background.color = Color('#12141a')
	add_child(background)
	background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)

	var center: CenterContainer = CenterContainer.new()
	add_child(center)
	center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)

	var column: VBoxContainer = VBoxContainer.new()
	column.add_theme_constant_override('separation', 20)
	center.add_child(column)

	return column

More Input examples