Inputs — clavier
Les inputs sur macOS passent par NSEvent — les événements système qu'AppKit distribue à la fenêtre active — il n'existe pas de wrapper C++. L'architecture propre isole ces événements dans une structure InputState C++ que le Renderer consomme chaque frame, sans jamais toucher à ObjC dans la logique de jeu. Pour les manettes on utilise GCController du Framework GameController
Architecture — le flux de données
Afin d'avoir une application codée en langage C++ au maximum, nous avons vu comment appeler nos fonctions draw() et resize() depuis ObjC, le point de départ de l'exécution. Ici, dans un premier temps tu vas récupérer l'état des touches (0 = false, 1 = true) via les notifications mise en place par le système macOS.
Une structure partagée Objective-C / C++
Définie dans notre nouveau fichier InputState.h, partagée via un include simple — pas de cast, pas de bridge.
/// C++ — includes/SharedAPP/InputState.h — update:18/05/26
#ifndef InputState_h
#define InputState_h
#include "simd/simd.h"
struct InputState
{ // État continu : reconstruit intégralement à chaque frame,
// donc rien à remettre à false soi-même
bool keyLeft = false;
bool keyRight = false;
bool keySpace = false;
bool keyA = false;
bool keyZ = false;
bool keyE = false;
bool keyQ = false;
bool keyS = false;
bool keyD = false;
// Multiplicateur continu : 1.0 au repos
float speedMultiplier = 1.0f;
};
#endif /* InputState_h */
Tous les champs ci-dessus sont de l'état : ils décrivent ce qui est enfoncé maintenant, et pollInputState() repart d'un InputState neuf à chaque frame. Une vraie action (sauter, tirer) est différente : elle doit être vraie exactement une frame, puis remise à false par celui qui l'a lue — sinon le personnage saute tant que la touche reste enfoncée.
RenderWindow — capturer les événements
RenderWindow est notre sous-classe de NSWindow. Elle maintient un NSMutableSet des touches enfoncées et accumule le delta souris entre deux appels à pollInputState().
On déclare une fonction ici nommée pollInputState qui renvoie nos inputs sous forme de structure nommée ici InputState
/// Objective-C++ — AppViewController.mm — update:18/05/26 — RenderWindow
#include "../includes/SharedAPP/InputState.h"
@interface RenderWindow : NSWindow
@property (weak) ApplicationController* appCoordinator;
- (InputState)pollInputState; // Ajout à notre objet/class RenderWindow
@end
/// Objective-C++ — AppViewController.mm — update:18/05/26 — RenderWindow
@implementation RenderWindow // Constructeur RenderWindow
{ // déclaration de ivars
NSMutableSet<NSNumber*>* _pressedKeys; // touches actuellement enfoncées
BOOL _commandWasDown; // pour rattraper les keyUp: perdus (voir plus bas)
}
/// Objective-C++ — AppViewController.mm — update:18/05/26 — RenderWindow
- (instancetype)initWithContentRect:(NSRect)rect
styleMask:(NSWindowStyleMask)mask
backing:(NSBackingStoreType)backing
defer:(BOOL)flag
screen:(NSScreen*)screen
{
self = [super initWithContentRect:rect styleMask:mask backing:backing defer:flag screen:screen];
if (self) { _pressedKeys = [NSMutableSet new]; }
return self;
}
/// Objective-C++ — AppViewController.mm — update:18/05/26
// Requis pour recevoir les événements clavier (les 3)
- (BOOL)acceptsFirstResponder { return YES; }
- (BOOL)canBecomeKeyWindow { return YES; }
- (BOOL)canBecomeMainWindow { return YES; }
// Requis pour recevoir les événements souris
- (BOOL)acceptsFirstMouse:(NSEvent *)event { return YES; }
keyDown / keyUp — le set de touches
Le pattern NSMutableSet est la façon correcte de gérer les touches : on ajoute dans keyDown, on retire dans keyUp — à chaque événement, pas à chaque frame. Le set reflète en permanence l'état réel du clavier ; pollInputState() se contente de le lire une fois par frame. Deux touches enfoncées en même temps = deux entrées dans le set, sans effort.
/// Objective-C++ — AppViewController.mm — update:18/05/26 — RenderWindow
- (void)keyDown:(NSEvent *)event
{ // isARepeat = YES (true) quand macOS répète une touche maintenue
if (!event.isARepeat)
[_pressedKeys addObject:@(event.keyCode)];
}
- (void)keyUp:(NSEvent *)event
{
[_pressedKeys removeObject:@(event.keyCode)];
}
flagsChanged — Shift et Ctrl
Les touches modificatrices (⇧ Shift, ⌃ Ctrl, ⌥ Option, ⌘ Command) ne déclenchent jamais keyDown — elles utilisent flagsChanged(). On les mappe sur des keycodes virtuels (0x80, 0x81) pour les inclure dans le même set.
Pourquoi 0x80 et au-dessus ? Parce que les keycodes matériels de macOS s'arrêtent à 0x7E (flèche haut) : tout ce qui est >= 0x80 est libre, aucune collision possible avec une vraie touche. Et pourquoi un code par modificateur plutôt qu'un par touche physique ? Parce que modifierFlags ne dit pas quel Shift est enfoncé — pour ça il faudrait tester le keyCode de l'événement (0x38 ou 0x3C).
/// Objective-C++ — AppViewController.mm — update:18/05/26 — RenderWindow
// Petit utilitaire : pose ou retire un code selon un booléen
- (void)setKey:(uint16_t)code held:(BOOL)held
{
if (held) [_pressedKeys addObject:@(code)];
else [_pressedKeys removeObject:@(code)];
}
- (void)flagsChanged:(NSEvent *)event
{
NSEventModifierFlags f = event.modifierFlags;
[self setKey:0x80 held:(f & NSEventModifierFlagShift) != 0]; // ⇧
[self setKey:0x81 held:(f & NSEventModifierFlagControl) != 0]; // ⌃
[self setKey:0x82 held:(f & NSEventModifierFlagOption) != 0]; // ⌥
[self setKey:0x83 held:(f & NSEventModifierFlagCommand) != 0]; // ⌘
// ⌘ vient d'être relâché : les keyUp: avalés pendant ce temps
// n'arriveront jamais, on relâche tout à la main (voir ci-dessous)
BOOL cmd = (f & NSEventModifierFlagCommand) != 0;
if (_commandWasDown && !cmd) [self releaseAllHardwareKeys];
_commandWasDown = cmd;
}
/// Objective-C++ — AppViewController.mm — update:18/05/26 — RenderWindow
- (void)flagsChanged:(NSEvent *)event
{
if (event.modifierFlags & NSEventModifierFlagShift)
[_pressedKeys addObject:@(0x80)];
else
[_pressedKeys removeObject:@(0x80)];
if (event.modifierFlags & NSEventModifierFlagControl)
[_pressedKeys addObject:@(0x81)];
else
[_pressedKeys removeObject:@(0x81)];
}
Le piège du ⌘ — des touches qui restent coincées
Deux situations font perdre des keyUp : tant que ⌘ Command est enfoncé, macOS ne délivre pas les keyUp des autres touches ; et une fenêtre qui perd le focus (⌘-Tab) cesse de recevoir des événements clavier. Dans les deux cas le set garde des touches « enfoncées » à vie — le personnage part tout seul vers la gauche pour toujours. Le correctif tient en deux méthodes.
/// Objective-C++ — AppViewController.mm — RenderWindow
// On ne relâche que les vraies touches : les codes virtuels >= 0x80
// restent gérés par flagsChanged, qui lui continue d'être appelé
- (void)releaseAllHardwareKeys
{
NSMutableSet<NSNumber*>* keep = [NSMutableSet new];
for (NSNumber* k in _pressedKeys)
if (k.unsignedShortValue >= 0x80) [keep addObject:k];
[_pressedKeys setSet:keep];
}
// La fenêtre perd le focus : plus aucun keyUp: ne viendra, on vide tout
- (void)resignKeyWindow
{
[super resignKeyWindow];
[_pressedKeys removeAllObjects];
_commandWasDown = NO;
}
pollInputState() — snapshot par frame
Appelée une fois par frame dans drawInMTKView:, elle construit un InputState propre depuis l'état courant du set de touches.
/// Objective-C++ — AppViewController.mm — update:18/05/26 — RenderWindow
- (InputState)pollInputState
{
InputState state; // tout est déjà à false / 1.0f
if ([_pressedKeys containsObject:@(0x7B)]) { state.keyLeft = true; }
if ([_pressedKeys containsObject:@(0x7C)]) { state.keyRight = true; }
if ([_pressedKeys containsObject:@(0x31)]) { state.keySpace = true; }
if ([_pressedKeys containsObject:@(0x80)]) { state.speedMultiplier = 1.89f; }
if ([_pressedKeys containsObject:@(0x81)]) { /* Do something */ }
if ([_pressedKeys containsObject:@(0x35)]) { [[NSApplication sharedApplication] terminate:nil]; }
return state;
}
Esc dans notre cas va chercher le sélecteur terminate de notre application afin de stopper la boucle.
drawInMTKView — consommer l'état
C'est ici que l'état est appliqué à la caméra et transmis au GameCoordinator. L'intégration avec le delta time rend le mouvement frame-rate indépendant.
/// Objective-C++ — AppViewController.mm — update:18/05/26 — ApplicationController
- (void)drawInMTKView:(nonnull MTKView *)view
{ // Delta time — cappé à 100ms pour éviter les sauts après suspend
static CFTimeInterval lastTime = CACurrentMediaTime();
CFTimeInterval now = CACurrentMediaTime();
float delta = (float)(now - lastTime);
lastTime = now;
delta = fminf(delta, 0.1f);
// Snapshot des inputs
InputState input = [_window pollInputState];
_renderer->m_input = input;
_renderer->draw((__bridge MTK::View *)view, now, delta);
}
Ta classe principale :
/// C++ — Renderer.hpp — update:18/05/26
#include "../includes/SharedGPU/Renderer_shared.h"
#include "../includes/SharedAPP/InputState.h" // ← inclus
public: // ← en public
~Renderer();
InputState m_input;
Table des keycodes macOS
Les keycodes sont des valeurs matérielles — ils ne changent pas selon la langue du clavier. Indépendants de la disposition AZERTY/QWERTY.
Change la langue du site en anglais pour les Keycodes QWERTY.
| Code | Touche | Code | Touche |
|---|---|---|---|
0x00 | Q | 0x0B | B |
0x01 | S | 0x0C | A |
0x02 | D | 0x0D | Z |
0x03 | F | 0x0E | E |
0x06 | W | 0x0F | R |
0x08 | C | ||
0x11 | T | ||
0x22 | I | ||
0x30 | Tab | ||
0x31 | Espace | ||
0x35 | Escape | ||
0x80 | les 2 Shifts | 0x81 | Control |
0x7B | ← Gauche | ||
0x7C | → Droite | ||
0x7E | ↑ Haut | ||
0x7D | ↓ Bas |
0x00 — qui correspond à la position de A sur QWERTY. Autrement dit, en AZERTY, 0x00 c'est Q, 0x0C c'est A, 0x0D c'est Z, 0x06 c'est W. Les contrôles WASD/ZQSD fonctionnent naturellement si on mappe par keycode et non par caractère.
Table des keycodes macOS — AZERTY
Un keycode désigne une position physique sur le clavier, jamais le caractère imprimé dessus. La touche à droite du Tab renvoie 0x0C qu'on soit en AZERTY, en QWERTY ou en Dvorak — seule la lettre gravée change. Les constantes correspondantes vivent dans <Carbon/HIToolbox/Events.h> et sont nommées d'après le QWERTY US : kVK_ANSI_Q vaut 0x0C, c'est-à-dire le A d'un AZERTY.
Colonne AZERTY = ce qui est gravé sur un clavier français Apple ; le caractère en gris est celui obtenu avec ⇧ Shift. La colonne QWERTY donne la même touche physique vue par un clavier US.
Lettres — rangée du haut
| Code | AZERTY | QWERTY | Constante Carbon |
|---|---|---|---|
0x0C | A | Q | kVK_ANSI_Q |
0x0D | Z | W | kVK_ANSI_W |
0x0E | E | E | kVK_ANSI_E |
0x0F | R | R | kVK_ANSI_R |
0x11 | T | T | kVK_ANSI_T |
0x10 | Y | Y | kVK_ANSI_Y |
0x20 | U | U | kVK_ANSI_U |
0x22 | I | I | kVK_ANSI_I |
0x1F | O | O | kVK_ANSI_O |
0x23 | P | P | kVK_ANSI_P |
0x21 | ^ ¨ | [ { | kVK_ANSI_LeftBracket |
0x1E | $ * | ] } | kVK_ANSI_RightBracket |
Lettres — rangée du milieu
| Code | AZERTY | QWERTY | Constante Carbon |
|---|---|---|---|
0x00 | Q | A | kVK_ANSI_A |
0x01 | S | S | kVK_ANSI_S |
0x02 | D | D | kVK_ANSI_D |
0x03 | F | F | kVK_ANSI_F |
0x05 | G | G | kVK_ANSI_G |
0x04 | H | H | kVK_ANSI_H |
0x26 | J | J | kVK_ANSI_J |
0x28 | K | K | kVK_ANSI_K |
0x25 | L | L | kVK_ANSI_L |
0x29 | M | ; : | kVK_ANSI_Semicolon |
0x27 | ù % | ' " | kVK_ANSI_Quote |
0x2A | ` £ | \ | | kVK_ANSI_Backslash |
Lettres — rangée du bas
| Code | AZERTY | QWERTY | Constante Carbon |
|---|---|---|---|
0x0A | < > | — | kVK_ISO_Section |
0x06 | W | Z | kVK_ANSI_Z |
0x07 | X | X | kVK_ANSI_X |
0x08 | C | C | kVK_ANSI_C |
0x09 | V | V | kVK_ANSI_V |
0x0B | B | B | kVK_ANSI_B |
0x2D | N | N | kVK_ANSI_N |
0x2E | , ? | M | kVK_ANSI_M |
0x2B | ; . | , < | kVK_ANSI_Comma |
0x2F | : / | . > | kVK_ANSI_Period |
0x2C | = + | / ? | kVK_ANSI_Slash |
Rangée des chiffres
| Code | AZERTY | QWERTY | Constante Carbon |
|---|---|---|---|
0x32 | @ # | ` ~ | kVK_ANSI_Grave |
0x12 | & 1 | 1 ! | kVK_ANSI_1 |
0x13 | é 2 | 2 @ | kVK_ANSI_2 |
0x14 | " 3 | 3 # | kVK_ANSI_3 |
0x15 | ' 4 | 4 $ | kVK_ANSI_4 |
0x17 | ( 5 | 5 % | kVK_ANSI_5 |
0x16 | § 6 | 6 ^ | kVK_ANSI_6 |
0x1A | è 7 | 7 & | kVK_ANSI_7 |
0x1C | ! 8 | 8 * | kVK_ANSI_8 |
0x19 | ç 9 | 9 ( | kVK_ANSI_9 |
0x1D | à 0 | 0 ) | kVK_ANSI_0 |
0x1B | ) ° | - _ | kVK_ANSI_Minus |
0x18 | - _ | = + | kVK_ANSI_Equal |
Touches spéciales & navigation
| Code | Touche | Constante Carbon |
|---|---|---|
0x24 | Return ↵ | kVK_Return |
0x30 | Tab ⇥ | kVK_Tab |
0x31 | Espace | kVK_Space |
0x33 | ⌫ Retour arrière | kVK_Delete |
0x35 | ⎋ Escape | kVK_Escape |
0x72 | Aide / Insert | kVK_Help |
0x73 | ↖ Début | kVK_Home |
0x74 | ⇞ Page précédente | kVK_PageUp |
0x75 | ⌦ Suppression avant | kVK_ForwardDelete |
0x77 | ↘ Fin | kVK_End |
0x79 | ⇟ Page suivante | kVK_PageDown |
0x7B | ← Gauche | kVK_LeftArrow |
0x7C | → Droite | kVK_RightArrow |
0x7D | ↓ Bas | kVK_DownArrow |
0x7E | ↑ Haut | kVK_UpArrow |
Modificateurs — reçus par flagsChanged
| Code | Touche | Constante Carbon |
|---|---|---|
0x36 | ⌘ Command droite | kVK_RightCommand |
0x37 | ⌘ Command gauche | kVK_Command |
0x38 | ⇧ Shift gauche | kVK_Shift |
0x39 | ⇪ Verr. majuscule | kVK_CapsLock |
0x3A | ⌥ Option gauche | kVK_Option |
0x3B | ⌃ Control gauche | kVK_Control |
0x3C | ⇧ Shift droite | kVK_RightShift |
0x3D | ⌥ Option droite | kVK_RightOption |
0x3E | ⌃ Control droite | kVK_RightControl |
0x3F | fn | kVK_Function |
Nos codes virtuels — hors plage matérielle
| Code | Touche | Défini par |
|---|---|---|
0x80 | ⇧ Shift (les deux) | flagsChanged |
0x81 | ⌃ Control (les deux) | flagsChanged |
0x82 | ⌥ Option (les deux) | flagsChanged |
0x83 | ⌘ Command (les deux) | flagsChanged |
Pavé numérique
| Code | Touche | Constante Carbon |
|---|---|---|
0x52 | 0 (pavé) | kVK_ANSI_Keypad0 |
0x53 | 1 (pavé) | kVK_ANSI_Keypad1 |
0x54 | 2 (pavé) | kVK_ANSI_Keypad2 |
0x55 | 3 (pavé) | kVK_ANSI_Keypad3 |
0x56 | 4 (pavé) | kVK_ANSI_Keypad4 |
0x57 | 5 (pavé) | kVK_ANSI_Keypad5 |
0x58 | 6 (pavé) | kVK_ANSI_Keypad6 |
0x59 | 7 (pavé) | kVK_ANSI_Keypad7 |
0x5B | 8 (pavé) | kVK_ANSI_Keypad8 |
0x5C | 9 (pavé) | kVK_ANSI_Keypad9 |
0x41 | . (pavé) | kVK_ANSI_KeypadDecimal |
0x43 | * (pavé) | kVK_ANSI_KeypadMultiply |
0x45 | + (pavé) | kVK_ANSI_KeypadPlus |
0x4E | - (pavé) | kVK_ANSI_KeypadMinus |
0x4B | / (pavé) | kVK_ANSI_KeypadDivide |
0x51 | = (pavé) | kVK_ANSI_KeypadEquals |
0x47 | Clear / Verr. num | kVK_ANSI_KeypadClear |
0x4C | ⌤ Enter (pavé) | kVK_ANSI_KeypadEnter |
Touches de fonction & volume
| Code | Touche | Constante Carbon |
|---|---|---|
0x7A | F1 | kVK_F1 |
0x78 | F2 | kVK_F2 |
0x63 | F3 | kVK_F3 |
0x76 | F4 | kVK_F4 |
0x60 | F5 | kVK_F5 |
0x61 | F6 | kVK_F6 |
0x62 | F7 | kVK_F7 |
0x64 | F8 | kVK_F8 |
0x65 | F9 | kVK_F9 |
0x6D | F10 | kVK_F10 |
0x67 | F11 | kVK_F11 |
0x6F | F12 | kVK_F12 |
0x69 | F13 | kVK_F13 |
0x6B | F14 | kVK_F14 |
0x71 | F15 | kVK_F15 |
0x6A | F16 | kVK_F16 |
0x40 | F17 | kVK_F17 |
0x4F | F18 | kVK_F18 |
0x50 | F19 | kVK_F19 |
0x5A | F20 | kVK_F20 |
0x48 | Volume + | kVK_VolumeUp |
0x49 | Volume − | kVK_VolumeDown |
0x4A | Muet | kVK_Mute |
0x00 — la position du A d'un QWERTY. Autrement dit : 0x00 c'est Q, 0x0C c'est A, 0x0D c'est Z, 0x06 c'est W, 0x29 c'est M. En mappant par keycode, les contrôles ZQSD tombent exactement sur les mêmes touches physiques que WASD — sans une ligne de code en plus, et sans jamais lire le caractère.
⇧ Shift gauche et le W. Elle renvoie 0x0A (kVK_ISO_Section) et n'existe tout simplement pas sur un clavier US — ne t'en sers jamais pour une commande obligatoire. À l'autre bout, la touche à gauche du 1 (@ # en AZERTY Apple, ` ~ en QWERTY) est 0x32. Les claviers japonais (JIS) ajoutent encore d'autres codes, absents de la table : 0x5D ¥, 0x5E _, 0x66 英数, 0x68 かな.
0x16 c'est le 6 mais 0x17 c'est le 5, 0x19 le 9 et 0x1A le 7. Jamais de calcul du genre 0x12 + n pour atteindre le chiffre n — toujours passer par la table.2. AZERTY Apple ≠ AZERTY PC sur quatre touches de ponctuation — la table ci-dessus suit le clavier Apple :
0x1C porte ! 8 chez Apple contre _ 8 sur PC, 0x1E $ * contre $ £, 0x2A ` £ contre * µ, et 0x2C = + contre ! §. Les lettres, les chiffres et les accentuées sont identiques des deux côtés — et le keycode, lui, ne bouge dans aucun cas.
Vérifier un code en trois lignes
La table couvre le clavier standard, mais le plus rapide reste de faire parler la machine : ajoute temporairement un NSLog dans keyDown et appuie sur la touche qui t'intéresse.
/// Objective-C++ — debug — à retirer ensuite
- (void)keyDown:(NSEvent *)event
{
NSLog(@"keyCode = 0x%02X caractère = %@",
event.keyCode, event.charactersIgnoringModifiers);
}
Du keycode au libellé à afficher
Un écran de configuration des touches ne peut pas afficher « 0x0C » à l'utilisateur, ni écrire « Q » en dur : il faut demander au système ce que cette position produit sur le clavier actuellement actif. C'est le rôle de UCKeyTranslate, dans le framework Carbon (à ajouter aux Link Binary With Libraries).
/// Objective-C++ — keycode → libellé affichable
#import <Carbon/Carbon.h>
NSString* LabelForKeyCode(uint16_t keyCode)
{
TISInputSourceRef src = TISCopyCurrentKeyboardLayoutInputSource();
CFDataRef data = (CFDataRef)TISGetInputSourceProperty(src, kTISPropertyUnicodeKeyLayoutData);
const UCKeyboardLayout* layout = (const UCKeyboardLayout*)CFDataGetBytePtr(data);
UInt32 deadKeyState = 0;
UniChar chars[4];
UniCharCount length = 0;
UCKeyTranslate(layout, keyCode, kUCKeyActionDisplay, 0,
LMGetKbdType(), kUCKeyTranslateNoDeadKeysBit,
&deadKeyState, sizeof(chars) / sizeof(chars[0]), &length, chars);
CFRelease(src);
return [[NSString stringWithCharacters:chars length:length] uppercaseString];
}
// LabelForKeyCode(0x0C) → "A" sur un AZERTY, "Q" sur un QWERTY
0x0D ; l'interface écrit « Z » ou « W » selon le clavier branché. Mélanger les deux — tester characters au lieu du keyCode — c'est se retrouver avec des commandes qui changent de place d'un utilisateur à l'autre, et qui cassent dès qu'un modificateur est enfoncé.