Jour 3 — Godot et l'ESP32 parlent avec le monde#
← Jour 2 · Programme · Jour 4 →
- Matin : l'ESP32 devient un clavier ou une manette (HID), par le câble ou en Bluetooth. Rien à installer côté Godot.
- Après-midi : votre projet à partir du HID, qui continue au Jour 4.
1. L'ESP32 devient un clavier ou une manette (HID)#
La façon la plus simple de faire parler l'ESP32 avec Godot : l'ESP32 se fait passer pour un clavier ou une manette USB. C'est ce qu'on appelle un périphérique HID (Human Interface Device).
- Côté Godot, rien à installer : le jeu reçoit des touches ou des mouvements de manette, comme avec un vrai clavier ou une vraie manette.
- Le contrôleur marche aussi avec n'importe quel autre jeu ou logiciel.
- C'est le principe de beaucoup de contrôleurs alternatifs : on cache un ESP32 dans un objet, et l'objet devient une manette.
Préparer le projet PlatformIO#
Le mode HID a besoin d'un autre réglage de l'USB. Dans platformio.ini, remplacez la ligne :
-DARDUINO_USB_MODE=1
par :
-DARDUINO_USB_MODE=0 ; USB en mode TinyUSB : clavier, manette + port série
Le moniteur série fonctionne toujours dans ce mode. Pour les exemples sans clavier ni manette (sections 2 et 3), remettez 1 : l'Upload est plus fiable.
Si l'Upload échoue en mode HID : maintenez BOOT, appuyez sur RST, relâchez BOOT, puis relancez Upload.
1a — Premier test : l'ESP32 tape au clavier#
Ouvrez un éditeur de texte (Bloc-notes, TextEdit…), branchez l'ESP32 et appuyez sur le bouton BOOT de la carte : l'ESP32 tape « bonjour ».
#include <Arduino.h>
#include "USB.h"
#include "USBHIDKeyboard.h"
USBHIDKeyboard Keyboard;
#define BOUTON_PIN 0 // le bouton BOOT de la carte
void setup() {
pinMode(BOUTON_PIN, INPUT_PULLUP);
Keyboard.begin();
USB.begin();
}
void loop() {
if (digitalRead(BOUTON_PIN) == LOW) { // bouton appuyé
Keyboard.print("bonjour ");
delay(500); // pour ne pas taper 50 fois d'affilée
}
}
Piège du clavier AZERTY : l'ESP32 envoie la position des touches sur un clavier américain (QWERTY). Sur un ordinateur réglé en AZERTY,
adevientq,zdevientw,mdevient,… « bonjour » a été choisi parce que toutes ses lettres sont au même endroit sur les deux claviers. Pour les jeux, on utilise des touches qui ne changent pas : flèches, Espace, Entrée.Sur Mac, un « Assistant réglage du clavier » peut s'ouvrir au premier branchement : fermez-le.
1b — Un clavier pour jouer dans Godot#
Deux boutons et un capteur à effet Hall deviennent les flèches gauche, droite et la touche Espace.
| Élément | GPIO | Touche envoyée |
|---|---|---|
| Bouton gauche (vers GND) | 5 | ← |
| Bouton droit (vers GND) | 6 | → |
| Capteur Hall A3144 (aimant) | 4 | Espace |
Côté ESP32 :
#include <Arduino.h>
#include "USB.h"
#include "USBHIDKeyboard.h"
USBHIDKeyboard Keyboard;
#define BOUTON_GAUCHE 5
#define BOUTON_DROITE 6
#define HALL_PIN 4 // A3144 : LOW quand un aimant est proche
// On se souvient de l'état précédent de chaque entrée
bool avant_gauche = false;
bool avant_droite = false;
bool avant_hall = false;
// Appuie sur la touche quand l'entrée devient active,
// la relâche quand elle redevient inactive.
// (Le & permet à la fonction de modifier la variable "avant".)
void suivreTouche(bool actif, bool &avant, uint8_t touche) {
if (actif && !avant) {
Keyboard.press(touche);
}
if (!actif && avant) {
Keyboard.release(touche);
}
avant = actif;
}
void setup() {
pinMode(BOUTON_GAUCHE, INPUT_PULLUP);
pinMode(BOUTON_DROITE, INPUT_PULLUP);
pinMode(HALL_PIN, INPUT_PULLUP);
Keyboard.begin();
USB.begin();
}
void loop() {
suivreTouche(digitalRead(BOUTON_GAUCHE) == LOW, avant_gauche, KEY_LEFT_ARROW);
suivreTouche(digitalRead(BOUTON_DROITE) == LOW, avant_droite, KEY_RIGHT_ARROW);
suivreTouche(digitalRead(HALL_PIN) == LOW, avant_hall, ' '); // ' ' = Espace
delay(10);
}
Pourquoi press / release et pas print ? print tape la touche une seule fois. Avec press, la touche reste enfoncée tant que le bouton est tenu : dans le jeu, la baballe avance tant qu'on appuie.
Côté Godot — la baballe du Jour 1, pilotée au clavier :
extends Node2D
var rayon = 20
var couleur = Color.ORANGE
var couleur_fond = Color("#1d1b33")
var position_cercle = Vector2(0, 0) # où est la baballe (x, y)
var vitesse = 300 # pixels par seconde
func _ready():
RenderingServer.set_default_clear_color(couleur_fond)
position_cercle = get_viewport_rect().size / 2
func _process(delta):
# -1 si ← est enfoncée, +1 si →, 0 sinon
var direction = Input.get_axis("ui_left", "ui_right")
position_cercle.x += direction * vitesse * delta
# Espace (ou Entrée) : nouvelle couleur
if Input.is_action_just_pressed("ui_accept"):
couleur = Color(randf(), randf(), randf())
queue_redraw() # la baballe a bougé : on redessine
func _draw():
draw_circle(position_cercle, rayon, couleur)
Input.get_axis("ui_left", "ui_right")regarde deux touches à la fois et renvoie -1, 0 ou +1.ui_left,ui_rightetui_acceptsont des actions prédéfinies de Godot (flèches, Espace/Entrée). On peut en créer d'autres dans Projet → Paramètres du projet → Contrôles.* delta: la vitesse est en pixels par seconde, quelle que soit la vitesse de l'ordinateur.
Testez d'abord avec le vrai clavier, puis avec l'ESP32 : le jeu ne voit pas la différence !
1c — Une manette pour Godot#
Une manette envoie des axes (joysticks, gâchettes : des valeurs continues) et des boutons. Ici, le capteur Hall SS49E devient un joystick : on approche un aimant d'un côté ou de l'autre pour aller à gauche ou à droite.
| Élément | GPIO | Sur la manette |
|---|---|---|
| Capteur Hall SS49E (analogique, alimenté en 3,3 V) | 4 | stick gauche, axe X |
| Bouton (vers GND) | 5 | bouton 0 |
Côté ESP32 :
#include <Arduino.h>
#include "USB.h"
#include "USBHIDGamepad.h"
USBHIDGamepad Gamepad;
#define HALL_PIN 4 // SS49E : entrée analogique
#define BOUTON_PIN 5
int repos = 0; // valeur du capteur sans aimant
void setup() {
pinMode(BOUTON_PIN, INPUT_PULLUP);
// Calibration : moyenne de 50 mesures sans aimant
long somme = 0;
for (int i = 0; i < 50; i++) {
somme += analogRead(HALL_PIN);
delay(10);
}
repos = somme / 50;
Gamepad.begin();
USB.begin();
}
void loop() {
// Champ magnétique : négatif d'un côté, positif de l'autre
int champ = analogRead(HALL_PIN) - repos;
// Une manette envoie des valeurs entre -127 et 127
int x = map(champ, -800, 800, -127, 127); // ajustez 800 selon vos aimants
x = constrain(x, -127, 127); // on ne dépasse pas les limites
Gamepad.leftStick(x, 0);
if (digitalRead(BOUTON_PIN) == LOW) {
Gamepad.pressButton(0);
} else {
Gamepad.releaseButton(0);
}
delay(10);
}
Côté Godot — étape 1 : que voit Godot ?
Chaque modèle de manette numérote ses boutons et ses axes à sa façon. Avant de coder le jeu, on regarde ce qu'envoie la nôtre :
extends Node2D
func _ready():
for id in Input.get_connected_joypads():
print("Manette ", id, " : ", Input.get_joy_name(id))
func _input(event):
if event is InputEventJoypadButton:
print("Bouton ", event.button_index, " appuyé : ", event.pressed)
if event is InputEventJoypadMotion:
print("Axe ", event.axis, " = ", event.axis_value)
Approchez l'aimant, appuyez sur le bouton, et notez les numéros qui s'affichent dans la Sortie. Les axes vont de -1 à 1.
Côté Godot — étape 2 : la baballe suit l'aimant
extends Node2D
var rayon = 20
var couleur = Color.ORANGE
var couleur_fond = Color("#1d1b33")
var position_cercle = Vector2(0, 0) # où est la baballe (x, y)
func _ready():
RenderingServer.set_default_clear_color(couleur_fond)
func _process(delta):
var taille = get_viewport_rect().size
# Axe X du stick gauche de la manette n°0 : entre -1 et 1
var x = Input.get_joy_axis(0, JOY_AXIS_LEFT_X)
position_cercle = Vector2(taille.x / 2 + x * 300, taille.y / 2)
# Bouton enfoncé : la baballe grossit
if Input.is_joy_button_pressed(0, JOY_BUTTON_A):
rayon = 40
else:
rayon = 20
queue_redraw()
func _draw():
draw_circle(position_cercle, rayon, couleur)
Si la baballe ne réagit pas, remplacez
JOY_AXIS_LEFT_XetJOY_BUTTON_Apar les numéros notés à l'étape 1 (par exempleInput.get_joy_axis(0, 0)).
Ici, l'aimant fonctionne comme un curseur : la position de la baballe suit directement la position de l'aimant. Avec le clavier (1b), on donnait plutôt une direction.
1d — Une manette sans fil (Bluetooth)#
Même manette que 1c, mais sans câble : l'ESP32 devient une manette Bluetooth (BLE). Pratique pour cacher le contrôleur dans un objet qui bouge, qu'on lance, qu'on porte sur soi… Il suffit de l'alimenter avec une batterie USB.
Même montage que 1c : capteur Hall SS49E sur le GPIO 4, bouton sur le GPIO 5.
Préparer le projet :
- Remettez
-DARDUINO_USB_MODE=1dansplatformio.ini: ici, l'USB ne sert plus qu'à envoyer le programme et au moniteur série. - On utilise la librairie ESP32-BLE-Gamepad (de lemmingDev), qui s'appuie sur NimBLE-Arduino. Les deux sont déjà dans
lib_depsduplatformio.ini.
Côté ESP32 :
#include <Arduino.h>
#include <BleGamepad.h>
// Le nom qui apparaîtra dans les réglages Bluetooth de l'ordinateur
BleGamepad manette("Manette Enjmin", "Enjmin", 100);
#define HALL_PIN 4 // SS49E : entrée analogique
#define BOUTON_PIN 5
int repos = 0; // valeur du capteur sans aimant
void setup() {
Serial.begin(115200);
pinMode(BOUTON_PIN, INPUT_PULLUP);
// Calibration : moyenne de 50 mesures sans aimant
long somme = 0;
for (int i = 0; i < 50; i++) {
somme += analogRead(HALL_PIN);
delay(10);
}
repos = somme / 50;
// On enverra nous-mêmes les données, une fois par tour de loop
BleGamepadConfiguration config;
config.setAutoReport(false);
manette.begin(&config);
Serial.println("Manette prête : connectez-la dans les réglages Bluetooth.");
}
void loop() {
if (manette.isConnected()) {
// En Bluetooth, les axes vont de -32767 à 32767 (centre = 0)
int champ = analogRead(HALL_PIN) - repos;
int x = map(champ, -800, 800, -32767, 32767); // ajustez 800 selon vos aimants
x = constrain(x, -32767, 32767);
manette.setX(x);
if (digitalRead(BOUTON_PIN) == LOW) {
manette.press(BUTTON_1);
} else {
manette.release(BUTTON_1);
}
manette.sendReport(); // envoie tout d'un coup
}
delay(10);
}
Connecter la manette :
- Téléversez le programme et ouvrez le moniteur série : « Manette prête » s'affiche.
- Sur l'ordinateur : Réglages Bluetooth → ajouter un appareil → Manette Enjmin.
- Lancez le jeu Godot.
Côté Godot : rien à changer ! Les deux scripts de 1c (« que voit Godot ? » puis « la baballe suit l'aimant ») marchent tels quels : pour Godot, une manette Bluetooth est une manette comme une autre.
À savoir :
- Donnez un nom différent à chaque manette (
"Manette Enjmin 1","Manette Enjmin 2"…) : dans une salle avec 15 ESP32, sinon, impossible de savoir laquelle est la vôtre.- Si vous changez le nom ou que la connexion fait des siennes : oubliez l'appareil dans les réglages Bluetooth, puis reconnectez-le.
- Le Bluetooth ajoute un tout petit peu de latence par rapport au câble, mais rien de gênant pour la plupart des jeux.
- Sur batterie, alimentez la Super Mini par sa prise USB-C (batterie de téléphone) : c'est le plus simple et le plus sûr.
Défis#
- Ajoutez un deuxième bouton qui envoie la touche ↑ (
KEY_UP_ARROW) et fait sauter la baballe. - Utilisez le capteur de distance VL53L0X comme axe Y de la manette : la main qui monte et descend au-dessus du capteur déplace la baballe.
- Remplacez les boutons par des capteurs tactiles (
touchRead, voir Jour 2) : du papier alu ou un fruit devient une touche !
Solution du défi 1 : un bouton pour sauter
Côté ESP32 — le code de 1b, avec un troisième bouton sur le GPIO 7 :
#include <Arduino.h>
#include "USB.h"
#include "USBHIDKeyboard.h"
USBHIDKeyboard Keyboard;
#define BOUTON_GAUCHE 5
#define BOUTON_DROITE 6
#define BOUTON_HAUT 7 // nouveau bouton (vers GND)
#define HALL_PIN 4
bool avant_gauche = false;
bool avant_droite = false;
bool avant_haut = false;
bool avant_hall = false;
void suivreTouche(bool actif, bool &avant, uint8_t touche) {
if (actif && !avant) {
Keyboard.press(touche);
}
if (!actif && avant) {
Keyboard.release(touche);
}
avant = actif;
}
void setup() {
pinMode(BOUTON_GAUCHE, INPUT_PULLUP);
pinMode(BOUTON_DROITE, INPUT_PULLUP);
pinMode(BOUTON_HAUT, INPUT_PULLUP);
pinMode(HALL_PIN, INPUT_PULLUP);
Keyboard.begin();
USB.begin();
}
void loop() {
suivreTouche(digitalRead(BOUTON_GAUCHE) == LOW, avant_gauche, KEY_LEFT_ARROW);
suivreTouche(digitalRead(BOUTON_DROITE) == LOW, avant_droite, KEY_RIGHT_ARROW);
suivreTouche(digitalRead(BOUTON_HAUT) == LOW, avant_haut, KEY_UP_ARROW);
suivreTouche(digitalRead(HALL_PIN) == LOW, avant_hall, ' ');
delay(10);
}
Côté Godot — pour sauter, il faut une vitesse verticale et de la gravité qui fait retomber la baballe :
extends Node2D
var rayon = 20
var couleur = Color.ORANGE
var couleur_fond = Color("#1d1b33")
var position_cercle = Vector2(0, 0) # où est la baballe (x, y)
var vitesse = 300 # vitesse horizontale, en pixels par seconde
var vitesse_y = 0 # vitesse verticale : négative = vers le haut
var gravite = 1500 # la gravité ramène la baballe vers le bas
var sol = 0 # hauteur du sol
func _ready():
RenderingServer.set_default_clear_color(couleur_fond)
position_cercle = get_viewport_rect().size / 2
sol = position_cercle.y
func _process(delta):
var direction = Input.get_axis("ui_left", "ui_right")
position_cercle.x += direction * vitesse * delta
# ↑ : on saute, seulement si on est posé sur le sol
if Input.is_action_just_pressed("ui_up") and position_cercle.y >= sol:
vitesse_y = -600
vitesse_y += gravite * delta # la gravité freine la montée, puis fait retomber
position_cercle.y += vitesse_y * delta
if position_cercle.y > sol: # on ne passe pas à travers le sol
position_cercle.y = sol
vitesse_y = 0
if Input.is_action_just_pressed("ui_accept"):
couleur = Color(randf(), randf(), randf())
queue_redraw() # la baballe a bougé : on redessine
func _draw():
draw_circle(position_cercle, rayon, couleur)
ui_up est l'action prédéfinie de la flèche ↑. Changez -600 (force du saut) et gravite pour régler la hauteur et la durée du saut.
Solution du défi 2 : la distance devient l'axe Y
Côté ESP32 — le code de 1c (manette USB), avec le VL53L0X branché comme au Jour 2 (SDA = GPIO 8, SCL = GPIO 9) :
#include <Arduino.h>
#include <Wire.h>
#include <VL53L0X.h>
#include "USB.h"
#include "USBHIDGamepad.h"
USBHIDGamepad Gamepad;
VL53L0X capteur;
#define HALL_PIN 4 // SS49E : axe X
#define BOUTON_PIN 5
int repos = 0;
void setup() {
pinMode(BOUTON_PIN, INPUT_PULLUP);
long somme = 0;
for (int i = 0; i < 50; i++) {
somme += analogRead(HALL_PIN);
delay(10);
}
repos = somme / 50;
// Capteur de distance (I²C)
Wire.begin(8, 9);
capteur.setTimeout(500);
capteur.init();
capteur.startContinuous();
Gamepad.begin();
USB.begin();
}
void loop() {
// Axe X : l'aimant, comme en 1c
int champ = analogRead(HALL_PIN) - repos;
int x = map(champ, -800, 800, -127, 127);
x = constrain(x, -127, 127);
// Axe Y : la main au-dessus du capteur
// 50 mm (main proche) → -127 (haut) ; 400 mm (main loin) → 127 (bas)
int distance = capteur.readRangeContinuousMillimeters();
int y = 0; // pas de main : le stick reste au centre
if (distance < 2000) {
y = map(distance, 50, 400, -127, 127);
y = constrain(y, -127, 127);
}
Gamepad.leftStick(x, y);
if (digitalRead(BOUTON_PIN) == LOW) {
Gamepad.pressButton(0);
} else {
Gamepad.releaseButton(0);
}
delay(10);
}
Côté Godot — dans le script « la baballe suit l'aimant » de 1c, on lit aussi l'axe Y :
func _process(delta):
var taille = get_viewport_rect().size
var x = Input.get_joy_axis(0, JOY_AXIS_LEFT_X)
var y = Input.get_joy_axis(0, JOY_AXIS_LEFT_Y) # la main : entre -1 (haut) et 1 (bas)
position_cercle = Vector2(taille.x / 2 + x * 300, taille.y / 2 + y * 200)
if Input.is_joy_button_pressed(0, JOY_BUTTON_A):
rayon = 40
else:
rayon = 20
queue_redraw()
Ajustez 50 et 400 dans map selon la hauteur de main qui vous paraît confortable.
Solution du défi 3 : des touches tactiles
Côté ESP32 — le code de 1b, où les deux boutons deviennent deux touches tactiles. Un fil sur le GPIO 1 et un sur le GPIO 2 (GPIO tactiles : 1 à 14), reliés à du papier alu, un fruit…
#include <Arduino.h>
#include "USB.h"
#include "USBHIDKeyboard.h"
USBHIDKeyboard Keyboard;
#define TOUCH_GAUCHE 1
#define TOUCH_DROITE 2
#define HALL_PIN 4
bool avant_gauche = false;
bool avant_droite = false;
bool avant_hall = false;
int repos_gauche = 0; // valeurs sans toucher, mesurées au démarrage
int repos_droite = 0;
void suivreTouche(bool actif, bool &avant, uint8_t touche) {
if (actif && !avant) {
Keyboard.press(touche);
}
if (!actif && avant) {
Keyboard.release(touche);
}
avant = actif;
}
void setup() {
pinMode(HALL_PIN, INPUT_PULLUP);
// Calibration : ne touchez rien pendant le démarrage !
delay(500);
repos_gauche = touchRead(TOUCH_GAUCHE);
repos_droite = touchRead(TOUCH_DROITE);
Keyboard.begin();
USB.begin();
}
void loop() {
// Sur l'ESP32-S3, touchRead AUGMENTE quand on touche.
// On considère qu'on touche au-delà de 1,3 fois la valeur de repos (ajustez si besoin).
bool gauche = touchRead(TOUCH_GAUCHE) > repos_gauche * 1.3;
bool droite = touchRead(TOUCH_DROITE) > repos_droite * 1.3;
suivreTouche(gauche, avant_gauche, KEY_LEFT_ARROW);
suivreTouche(droite, avant_droite, KEY_RIGHT_ARROW);
suivreTouche(digitalRead(HALL_PIN) == LOW, avant_hall, ' ');
delay(10);
}
Côté Godot : rien à changer, le jeu reçoit toujours les flèches.
Si une touche se déclenche toute seule ou pas du tout, regardez les valeurs avec le code Touch du Jour 2 et changez 1.3.
2. Après-midi : votre projet à partir du HID#
L'ESP32 sait maintenant être un clavier ou une manette : à vous de jouer ! En binôme ou trinôme, lancez un petit projet. Il continue au Jour 4.
- Une idée en une phrase : quel objet, quel geste, quel jeu ? Par exemple : « on souffle sur un moulin à vent pour faire avancer un bateau ».
- Un capteur, une action : commencez par un seul capteur relié à une seule touche ou un seul axe de manette. On complique ensuite.
- Le jeu Godot : partez de la baballe de 1b ou 1c, ou d'un exemple du Jour 1, et branchez-y votre contrôleur.
- L'objet : cachez l'ESP32 et le capteur dans quelque chose (carton, jouet, objet détourné…). Le contrôleur, c'est aussi ce qu'on tient dans les mains.
Des pistes avec les capteurs du Jour 2 :
| Capteur | Devient… |
|---|---|
| Touch (papier alu, fruit, eau…) | une touche |
| Aimant + A3144 | une touche (porte, roue qui tourne, objet posé) |
| Aimant + SS49E | un axe de joystick |
| VL53L0X (distance) | un axe : la main qui monte et descend |
| LDR (lumière) | un axe : on cache ou on éclaire |
| Piézo | une touche qu'on frappe |
En fin de journée : chacun.e montre en deux minutes où il.elle en est.
Schémas de montage : Plot Circuits, avec les composants de la bibliothèque Fritzing (CC BY-SA 3.0, adaptés). Ces schémas sont partagés sous la même licence.