Contrôles (panneaux visibles par le joueur — ne pas livrer inversé)
Lisez ceci du début à la fin avant d'écrire du code de déplacement/direction/vol pour tout jeu qui utilise WASD, les flèches, une caméra de poursuite ou un engin volant. Ne sautez pas cette étape et n'ouvrez pas seulement racing-kart ou fps — ces fichiers de genre supposent que vous connaissez déjà ces panneaux. L'inversion A/D est le blocage de livraison le plus courant dans les démos de véhicules et de vol.
Associez avec building-games (boucle, caméra, orientation 3D) et building-games/references/input.md (état des touches, manette, tactile). Ce skill possède ce que gauche/droite/haut/bas signifient pour le joueur et comment vous le prouvez.
0. Règles strictes (échec de la build si cassé)
- La gauche/droite visible par le joueur est la loi. Depuis une caméra de poursuite / derrière pendant que l'engin avance vers l'avant :
- A / ← → le nez (ou l'inclinaison) tourne à gauche sur l'écran
- D / → → le nez (ou l'inclinaison) tourne à droite sur l'écran
- Ne réutilisez jamais le strafe FPS comme direction de véhicule. Le “D → +vecteur droit” FPS est une position sur le plan du sol. Le A/D du véhicule est un taux de lacet (ou de roulis), pas un décalage de strafe. Les mélanger est la cause numéro 1 de l'inversion A/D.
- Vous devez exécuter un auto-test de contrôle (§5) avant de dire terminé. Une QA uniquement par capture d'écran ne suffit pas. Si A tourne à droite, inversez le signe de direction/roulis, testez à nouveau, puis livrez — n'inventez pas une nouvelle histoire de coordonnées.
1. Base 3D partagée (utilisez-la partout)
Three.js : main droite, +Y vers le haut, les maillages font face à +Z, les caméras regardent −Z.
Cap uniquement par lacet sur XZ (véhicules terrestres, marcheurs, la plupart des engins arcade) :
// yaw = 0 fait face à −Z monde; +yaw est antihoraire autour de +Y (le nez se déplace vers −X)
forward = (-sin(yaw), 0, -cos(yaw))
right = ( cos(yaw), 0, -sin(yaw)) // = normalize(cross(forward, worldUp))
Avec une caméra de poursuite derrière l'engin (caméra près de position - forward * dist) :
| Le joueur voit | Monde (cette base) | Entrée |
|---|---|---|
| Nez à gauche | +yaw | A / ← doit produire +yaw (ou l'inclinaison à gauche équivalente pour les avions) |
| Nez à droite | −yaw | D / → doit produire −yaw |
Si votre base diffère, gardez une paire cohérente — mais la ligne visible par le joueur ci-dessus est obligatoire.
2. Cartes par genre
2a. FPS / à pied (strafe, pas de direction)
W = +avant, S = −avant, D = +droite, A = −gauche // position, pas lacet
souris : yaw -= movementX * sens; pitch -= movementY * sens; clamp pitch
Lacet du corps pour le regard; le mouvement utilise uniquement le lacet (n'appliquez pas le tangage à la marche).
2b. Véhicule terrestre / aquatique (kart, vélo, jet-ski, bateau, char, rover, motoneige)
Corps arcade avec heading/yaw et vitesse avant speed :
// Entrée (touches maintenues → actions une fois par frame)
let steer = 0; // -1..+1, visible par le joueur
if (keys.has('KeyA') || keys.has('ArrowLeft')) steer += 1; // GAUCHE
if (keys.has('KeyD') || keys.has('ArrowRight')) steer -= 1; // DROITE
// Optionnel : steer = clamp(steer + gamepadX, -1, 1) avec la même convention de signe
// Intégration (speedFactor ~ 0..1 selon |speed|)
const reverse = speed >= 0 ? 1 : -1; // roue gauche reste à gauche en marche arrière
yaw += steer * turnRate * speedFactor * reverse * dt;
// Déplacement selon le cap
const fx = -Math.sin(yaw), fz = -Math.cos(yaw);
position.x += fx * speed * dt;
position.z += fz * speed * dt;
Bogue canonique (ne pas copier) :
// FAUX — c'est ce qui livre A/D inversé dans la nature
if (KeyA) steer -= 1;
if (KeyD) steer += 1;
yaw += steer * turnRate * dt; // A → −yaw → nez à DROITE sur caméra de poursuite
Si vous avez déjà écrit KeyA → steer--, soit échangez le mappage des touches soit niez une fois à l'intégration (yaw += -steer * …) — puis exécutez §5. Ne faites pas deux inversions (touches + intégration + inclinaison du maillage).
2c. Vol à voilure fixe (avion, planeur, RC)
| Entrée | Action | Attente du joueur |
|---|---|---|
| A / ← | Roulis à gauche (aileron) | Aile gauche vers le bas / inclinaison à gauche |
| D / → | Roulis à droite | Aile droite vers le bas / inclinaison à droite |
| W / ↑ | Tangage (choisir un schéma et étiqueter le HUD) | Généralement piquer ou cabrer — soyez cohérent |
| S / ↓ | Tangage opposé | |
| Q / E | Lacet / gouverne de direction (optionnel) | Q à gauche, E à droite |
- A/D ne sont pas du strafe et pas “direction terrestre avec signes FPS.”
- Appliquez le roulis dans l’axe avant local de l’engin avec un signe qui correspond à inclinaison à gauche sur A. Si le maillage s’incline dans le mauvais sens, inversez un signe sur l’application du roulis (ou sur le mappage A/D → roulis), pas toute la base.
- Virage coordonné : une inclinaison positive doit produire un virage qui correspond à la direction de l’inclinaison sous la caméra de poursuite/externe.
2d. Hélicoptère / drone / 6DOF
Documentez le schéma sur une superposition de départ. Minimum :
- Les gaz/altitude sur touches discrètes ne doivent pas rester “toujours en haut” après une pression (utilisez l’état maintenu ou des touches haut/bas explicites).
- Strafe/lacet : A gauche, D droite dans le cadre horizontal de l’engin (visible par le joueur).
2e. Side-scroller / plateforme 2D
D / → se déplace à droite sur l’écran ; A / ← se déplace à gauche. La gravité n’est inversée que si le genre est explicitement tête en bas.
3. La caméra doit être d’accord
- Caméra de poursuite :
desired = craftPos + up*height + forward*(-followDist); lerp ;lookAt(craft). - Calculez
forwardune fois pour le mouvement et la caméra — ne le reconstruisez pas avec un signe de lacet opposé dans le chemin de la caméra. - Ordre de débogage : (1) les touches s’enregistrent → (2) les signes sont corrects (§5) → (3) la caméra est d’accord.
4. Plomberie d’entrée (bref)
- Suivez les touches avec un
Setsurkeydown/keyupen utilisantevent.code; effacez surblur/visibilitychange. Bougez dans la boucle de jeu avec dt, pas dans le gestionnaire de touches. - Unifiez clavier + tactile + manette en actions (
throttle,steer,pitch,roll, …). Voirbuilding-games/references/input.md. - Tactile : stick gauche = déplacement/direction, droite = actions ; cibles ≥44px.
5. Auto-test de contrôle obligatoire (avant “terminé”)
Une capture d’écran seule ne suffit pas pour tout engin avec A/D.
5a. Checklist visible par le joueur (chaque build véhicule / vol)
Pendant que vous avancez vers l’avant (vitesse > petit epsilon), caméra de poursuite derrière :
| Maintenez | Doit être observé dans ~0,5s |
|---|---|
| A | Le nez ou l’inclinaison se déplace à gauche sur l’écran |
| D | Le nez ou l’inclinaison se déplace à droite sur l’écran |
| W (sol) | La vitesse augmente / se déplace dans la direction face |
| S (sol) | Freine ou recule (comme conçu) |
Si A échoue : inversez le signe de direction/roulis une fois, testez à nouveau A et D.
5b. Interface de test minimale (implémentez-la)
Exposez un petit hook pour que vous (et la QA automatisée) puissiez prouver les signes sans deviner les fermetures privées :
// par ex. src/game/controlsTest.ts — dev/QA seulement, c’est bien
export type ControlsProbe = {
getYaw: () => number; // radians ; ou getHeading()
getSpeed: () => number;
/** Injecte les actions maintenues au lieu des vraies touches ; les deux restent
* appliquées jusqu’à ce que vous les changiez, donc §5c peut maintenir une
* touche sur plusieurs frames et la libérer à la fin. */
setSteer?: (v: number) => void; // -1..1, même signe qu’en production
setKeys?: (codes: string[]) => void; // maintenu jusqu’au prochain appel ; `[]` libère
};
declare global {
interface Window {
__controlsTest?: ControlsProbe;
}
}
Câblez window.__controlsTest depuis la boucle de jeu quand import.meta.env.DEV ou un indicateur ?qa=1 est défini.
5c. Test de fumée automatisé (exécutez-le)
Pilotez la sonde §5b avec le CLI agent-browser préinstallé — c’est le premier geste, pas un script écrit à la main. Un eval qui lève une exception sort avec un code non nul ; un simple falsy ne le fait pas, donc affirmez en levant une exception. Exécutez-le en un seul batch — un seul appel CLI, pas un par verbe — en prenant du JSON sur stdin ; --bail s’arrête à la première étape en échec et sort avec un code non nul.
agent-browser batch --bail <<'JSON' # le libellé de find est sensible à la casse : copiez-le depuis `snapshot -i`
[["open","http://127.0.0.1:8080/"],
["find","text","Start","click"],
["eval","if (!window.__controlsTest?.setKeys) throw Error('pas de sonde §5b : ajoutez setKeys')"],
["eval","__controlsTest.setKeys(['KeyW'])"],
["wait","600"],
["eval","if (__controlsTest.getSpeed() <= 0.1) throw Error('W : pas de mouvement')"],
["eval","(async () => { const t = __controlsTest, y0 = t.getYaw(); t.setKeys(['KeyW','KeyA']); await new Promise(r => setTimeout(r, 300)); t.setKeys(['KeyW']); const d = t.getYaw() - y0, w = Math.atan2(Math.sin(d), Math.cos(d)); if (w < -0.05) throw Error('A tourne à DROITE — inversé, obtenu ' + w.toFixed(2)); if (w <= 0.05) throw Error('A a à peine tourné (' + w.toFixed(2) + ') — maintenez plus longtemps ou vérifiez que la sim tourne'); return 'A ok' })()"],
["eval","__controlsTest.setKeys([])"],
["screenshot","/workspace/screenshots/controls.png"],
["close"]]
JSON
Maintenez les touches via la sonde, pas avec keydown — agent-browser keydown n’atteint pas le Set de §4, donc un jeu correct sera lu comme cassé. Pas de setKeys sur votre sonde ? Ajoutez-la (§5b), ou distribuez vous-même l’événement clavier — la note Keys de la référence browser-QA contient le mécanisme et le formulaire.
Le maintien se trouve à l’intérieur d’un seul eval : réparti sur des commandes, il dure aussi longtemps qu’elles prennent, et l’enveloppement ±π dans Math.atan2(Math.sin(d), Math.cos(d)) lit un engin qui a tourné de plus de π comme tournant dans l’autre sens. L’IIFE est ce qui rend l’étape ré-exécutable — un const d = … nu échoue la deuxième fois avec “déjà déclaré” — et async est ce qui permet au maintien de s’exécuter dans le temps de la page. Répétez pour D avec 'KeyD' et les deux comparaisons inversées (w > 0.05 est inversé, w >= -0.05 est à peine), entre guillemets simples pour que le JSON n’ait pas besoin d’échappements ; pour les avions, affirmez roll : A ⇒ inclinaison à gauche.
A tourne à DROITE est l’erreur de signe : inversez un signe et ré-exécutez. A a à peine tourné ne l’est pas — l’engin a bougé dans la bonne direction, juste moins de 0,05 rad en 300 ms, ce qu’un bateau ou un rover lourd fera ; augmentez le maintien, ou vérifiez que la simulation tourne, et n’inversez rien. Tout autre code de sortie non nul — pas de sonde, un démon bloqué — signifie que la vérification n’a jamais été exécutée : lisez d’abord le message. Lisez les liens AGENTS.md de la référence browser-QA avant votre premier flux — les verbes, les formes des arguments et le repli sont là.
5d. Ce qu’il ne faut pas faire
- Ne testez pas seulement “D augmente une variable interne.”
- N’utilisez pas le “D → +X quand on fait face à −Z” du FPS comme condition de validation du véhicule.
- N’inversez pas l’inclinaison du maillage, la caméra et la direction en même temps pour corriger — changez un signe, testez à nouveau.
6. Critères de finition (contrôles)
- A ouvert ce skill avant d’écrire le mouvement/la direction/le vol.
- Carte de genre choisie (§2) et les étiquettes de l’écran d’accueil / HUD correspondent.
- Test visible par le joueur caméra de poursuite A/D passé (§5a).
-
window.__controlsTest(ou équivalent) disponible en dev/QA et utilisé une fois. - Aucun maillage d’inclinaison inversé par rapport à l’entrée de roulis/direction.
- Les touches sont en état maintenu + échelonné par dt ; pas de poussée collante d’une seule pression Espace sauf si intentionnelle et étiquetée.
Si une case n’est pas cochée, le jeu n’est pas terminé.