LT and RT trigger issue or bug on older Android versions

Godot Version

Godot 4.6

Question

Hi, I’m coding a controller diagnostic software on Godot 4.6, and while testing it on Android, I discovered something strange.

As a reminder, the trigger axis is a gauge with the RT axis at the top and the LT axis at the bottom.

On recent versions of Android (Android 15), when I press LT, the stick moves down as expected, and when I press RT, the stick moves up.

However, when I’m on an older version of Android (Android 11 and Android 8 are the oldest versions I’ve tested), when I press LT, the stick moves up, and when I press RT, the stick moves down. I don’t know if this is a bug I just discovered or how older versions of Android handle analog triggers.

extends Node2D

# Rayon maximal pour les sticks virtuels
var trigger_radius: float = 400.0

# Références aux sticks virtuels des triggers
@onready var base_trigger: Sprite2D = $BaseTrigger
@onready var stick_triggers = [
	$StickTriggerUn,   
	$StickTriggerDeux, 
	$StickTriggerTrois, 
	$StickTriggerQuatre 
]

# Labels de thème
@onready var classic_label     = $ClassicLabel
@onready var anniversary_label = $AnniversaryALabel
@onready var dark_label        = $DarkLabel

# Mappage spécifique des axes des gâchettes LT et RT
var mapping_triggers = {
	"Xbox One Elite 2 Controller": { "LT": 2, "RT": 5 },  
	"Generic X-Box pad": { "LT": 2, "RT": 5 },  
	"Xbox Series X Controller": { "LT": 4, "RT": 5 },  
	"default": { "LT": 4, "RT": 5 }  
}

# Seuil mort (deadzone)
var deadzone_threshold: float = 0.1

# Centre de la base des sticks
var center_position: Vector2

# Stockage des mappings détectés
var manette_mappings = {}

# Stockage des manettes connectées (reçu du parent)
var manettes_actuelles = []

# Enum pour les thèmes
enum ThemeType { CLASSIQUE, ANNIVERSAIRE_5, SOMBRE }

func _ready():
	if base_trigger != null:
		center_position = base_trigger.position
	
	for stick in stick_triggers:
		if stick != null:
			stick.position = center_position

func _process(_delta):
	# Mise à jour des positions des triggers
	for i in range(len(manettes_actuelles)):
		if i < len(stick_triggers):
			_update_trigger_position(manettes_actuelles[i], stick_triggers[i])

# Fonction appelée par le parent pour synchroniser la liste
func update_controller_list(new_list: Array):
	manettes_actuelles = new_list.duplicate()
	_update_mappings()
	_reset_unused_sticks()

# Réinitialiser les sticks non utilisés au centre
func _reset_unused_sticks():
	for i in range(len(stick_triggers)):
		if i >= len(manettes_actuelles):
			if stick_triggers[i]:
				stick_triggers[i].position = center_position

func _update_mappings():
	manette_mappings.clear()
	
	for device in manettes_actuelles:
		var manette_nom = Input.get_joy_name(device)
		
		if "Elite" in manette_nom:
			manette_mappings[device] = mapping_triggers["Xbox One Elite 2 Controller"]
		elif manette_nom in mapping_triggers:
			manette_mappings[device] = mapping_triggers[manette_nom]
		else:
			manette_mappings[device] = mapping_triggers["default"]

func _update_trigger_position(device_id: int, stick: Node2D):
	if not stick or device_id not in manette_mappings:
		return

	var axes = manette_mappings[device_id]
	var trigger_down = Input.get_joy_axis(device_id, axes["LT"])
	var trigger_up   = Input.get_joy_axis(device_id, axes["RT"])

	# Correction spécifique Android
	if OS.get_name() in ["Linux", "MacOS", "Android"]:
		trigger_down = _correct_trigger_value(trigger_down)
		trigger_up   = _correct_trigger_value(trigger_up)

	trigger_down = (trigger_down + 1) / 2  
	trigger_up   = (trigger_up + 1) / 2      

	if trigger_down < deadzone_threshold:
		trigger_down = 0.0
	if trigger_up < deadzone_threshold:
		trigger_up = 0.0

	var vertical_movement = (trigger_down - trigger_up) * trigger_radius
	if abs(vertical_movement) < 0.01:
		vertical_movement = 0.0

	stick.position = center_position + Vector2(0, vertical_movement)

func _correct_trigger_value(value: float) -> float:
	return clamp(value - 0.5, 0.0, 1.0) if OS.get_name() in ["Linux", "MacOS", "Android"] else value

# 🎨 GESTION DES THÈMES
func on_theme_changed(theme_type: ThemeType):
	match theme_type:
		ThemeType.CLASSIQUE:
			_apply_classique_theme()
		ThemeType.ANNIVERSAIRE_5:
			_apply_anniversaire_theme()
		ThemeType.SOMBRE:
			_apply_sombre_theme()

func _apply_classique_theme():
	if classic_label: classic_label.visible = true
	if anniversary_label: anniversary_label.visible = false
	if dark_label: dark_label.visible = false

	var tex = load("res://gaugetrigger.png")
	if tex:
		base_trigger.texture = tex

	if stick_triggers.size() > 0 and stick_triggers[0]:
		stick_triggers[0].texture = load("res://cursordpad.png")
	if stick_triggers.size() > 1 and stick_triggers[1]:
		stick_triggers[1].texture = load("res://cursorstick.png")
	if stick_triggers.size() > 2 and stick_triggers[2]:
		stick_triggers[2].texture = load("res://cursortrigger.png")
	if stick_triggers.size() > 3 and stick_triggers[3]:
		stick_triggers[3].texture = load("res://cursordyellow.png")

func _apply_anniversaire_theme():
	if classic_label: classic_label.visible = false
	if anniversary_label: anniversary_label.visible = true
	if dark_label: dark_label.visible = false

	var tex = load("res://gaugetrigger.png")
	if tex:
		base_trigger.texture = tex

	for stick in stick_triggers:
		if stick:
			stick.texture = load("res://cursorstick.png")

func _apply_sombre_theme():
	if classic_label: classic_label.visible = false
	if anniversary_label: anniversary_label.visible = false
	if dark_label: dark_label.visible = true

	for stick in stick_triggers:
		if stick:
			stick.texture = load("res://cursordarkmode.png")

It’s not a Godot bug. Godot uses an external library for handling all controller inputs. So it’s either the library, or how older version of Android interact with that library. But since that library is pretty much the gold standard, based on your testing I’d say it’s an Android bug that they clearly fixed at some point.

Okay, so it’s an Android bug that has since been fixed, but I wonder if it’s possible to modify the script so that it works well on both new and old versions, unless there’s nothing to be done about it.

Detect the version and for older versions, reverse it.

func _ready() -> void:
	if OS.get_name() == "Android":
		var android_version: String = OS.get_version()
		print("Android version: ", android_version)

And offer your users the possibility of inverting mappings of all analog axis with a toggle to resolve observed problems.
My Xbox controller didn’t change behavior from Android 14 to 15, so I suspect it’s not an universal bug on pre 15. Might be a combo of Android distro, handset model and specific SOCs.
Cheers !

1 Like