Le mouvement en pixel art : marche, caméra et portes sur iPhone
Pokémon Version Rouge, Pokémon Version Cristal et Pokémon Version Émeraude, un jeu par génération parmi les trois de la Game Boy et de la Game Boy Advance, parcourent tous une cellule de 16 pixels en 16 images à environ 59,73 hertz, soit 3,73 cellules par seconde, et rien dans ce pas n’est confié à une horloge : Émeraude déplace le joueur d’exactement un pixel par image, affiche un dessin d’enjambée et un dessin debout par pas, et fait défiler la caméra sur la même image, du même nombre de pixels.123 Rouge et Cristal arrivent aux mêmes seize images en avançant de deux pixels une image sur deux.4 La porte d’Émeraude s’ouvre en quatre dessins tenus cinq images chacun, le joueur est conduit d’un pas forcé à l’intérieur, la porte se referme, puis l’écran s’assombrit en neuf niveaux étagés : au moins 79 images, 1,32 seconde, avant que la carte suivante puisse commencer à se charger.15 Le monde de Kiradex fait marcher ses personnages selon le temps écoulé et lisse sa caméra, et un modèle de ce code (un modèle, pas une capture faite sur un téléphone) indique que le sprite fait un saut de deux pixels une fois par tuile à 60 hertz, que la caméra prend environ un pixel de retard de plus à chaque tuile d’une marche à 60 hertz (9 pixels à la cinquième, et 13 en course) et s’immobilise 4 à 9 pixels trop court, et que les dessins de marche suivent leur propre horloge, un cycle pour 3,00 tuiles là où celui d’Émeraude en couvre deux.6 Apple accorde aux jeux une priorité spéciale à 30 et 60 hertz et RealityKit effectue généralement son rendu à 60 ; le cahier des charges prévoit donc un tick fixe à 60 hertz avec des pas en pixels entiers, des dessins de marche choisis selon la distance parcourue (ma proposition, pas la méthode des consoles portables), une caméra fixe, le cérémonial de la porte d’Émeraude dans nos propres graphismes, trois événements haptiques facultatifs, et 60 hertz plutôt que 120.78 Voici le canon mesuré à partir des décompilations, le côté iPhone d’après les pages d’Apple, et le cahier des charges avec les vérifications qu’il doit passer.
En bref
- Un pas est un contrat, pas une vitesse. Chaque table de pas d’Émeraude totalise 16 pixels : la marche, c’est 1 pixel par image pendant 16 images (268 millisecondes par cellule), la course et le surf 2 pendant 8, la vitesse maximale du Vélo Course (Mach Bike) 4 pendant 4, et seul le 2-3-3-2-3-3 du Vélo Cross (Acro Bike) est irrégulier. Rouge et Cristal parcourent les mêmes 16 images par sauts de 2 pixels, à 30 mises à jour par seconde.124
- Les jambes et le pas sont synchronisés. Émeraude compte les dessins et le pas en images, séparément, et fait concorder les deux décomptes : la marche est enjambée, debout, enjambée, debout, à 8 images par dessin sur deux cellules de 16 images ; les allures plus rapides divisent la durée d’affichage par deux au lieu d’ajouter des dessins, si bien qu’il y a un appui au sol tous les 16 pixels, à toutes les vitesses. Se tourner à l’arrêt prend 8 images (134 millisecondes), se tourner en marchant ne prend rien, et marcher contre un mur joue un heurt de 32 images.19102
- La caméra, c’est le joueur. La caméra d’Émeraude copie la position du joueur et défile du même nombre de pixels sur la même image, sans retard ni avance, et ne s’arrête jamais au bord d’une carte, car l’extérieur est dessiné à partir de tuiles de bordure. Game Freak a écrit une caméra qui devance le vélo et l’a livrée désactivée.231112
- Une porte est un cérémonial. Quatre dessins de cinq images (335 millisecondes), un pas forcé de 16 images, la porte qui se referme en 20, et un fondu dont le dernier mélange tombe sur sa 17e image et qui se termine sur sa 22e : au moins 79 images, 1,32 seconde, sans aucune commande, avant que la carte puisse se charger. Cela confirme l’article de cette série consacré aux structures, environ 84 millisecondes par dessin, et corrige les notes de recherche sur lesquelles il s’appuyait, qui indiquaient « 4 ticks each (16 frames, about 0.27 s at 59.7 Hz). » (4 ticks chacun, soit 16 images, environ 0,27 s à 59,7 Hz). Cristal fait un fondu au blanc en 8 images ; Rouge un fondu au noir en 32.1513144
- Sur iPhone, une cadence régulière à 60.
preferredFrameRateRange(iOS 15.0) est une indication ; les iPhone ProMotion fonctionnent de 10 à 120 hertz en douze paliers ; dépasser 60 exigeCADisableMinimumFrameDurationOnPhone; les jeux bénéficient d’une « special priority to 30Hz and 60Hz » (priorité spéciale à 30 et 60 Hz) ; RealityKit « typically limits the refresh rate » (limite généralement la fréquence de rafraîchissement) à 60. Une marche en pixels entiers ne gagne rien à 120 : chaque pixel est simplement tenu pendant deux rafraîchissements.1571686 - Kiradex aujourd’hui, modélisé et non mesuré. Le code fait marcher à 4 tuiles par seconde selon le temps écoulé, si bien qu’un modèle à 60 hertz constants met 15 images par tuile et avance de 2 pixels sur l’une d’elles ; il lisse puis arrondit la caméra, qui prend de plus en plus de retard au fil d’une marche (5 pixels après la première tuile, 9 après la cinquième, 13 en course) et s’immobilise à 4 pixels du joueur à 60 hertz et à 9 à 120 ; son cycle de marche de six dessins à 8 par seconde couvre 3,00 tuiles par cycle en marchant et 4,50 en courant. Aucun téléphone n’a été mesuré.176
- Ce que cela apporte à Kiradex : un cahier des charges, pas une version livrée. Un tick fixe à 60 hertz avec des pas de 1 pixel et une règle explicite pour le retard accumulé, des dessins de marche choisis selon la distance, une caméra fixe sur des pixels entiers, la séquence complète de la porte avec un fondu par paliers, un pas refusé rendu visible, trois événements haptiques facultatifs, aucune demande de 120 hertz, et pour chaque point une vérification qu’un journal de mouvement, une capture ou un script peut confirmer.
1. La marche d’Émeraude : seize pixels en seize images
Les deux premiers articles de cette série ont mesuré l’apparence d’un monde en pixels et de ses habitants ; le troisième a mesuré ses bâtiments. Celui-ci mesure le temps : combien de pixels par image, combien d’images par pas, quel dessin s’affiche sur quelle image, combien de temps durent une rotation, une porte et un fondu, et ce que fait la caméra pendant tout ce temps. Les jeux sont lus comme dans les articles précédents, à partir des décompilations par pret des jeux Game Boy et Game Boy Advance publiés, aux commits utilisés précédemment : pokered d2704a6, pokecrystal 5beda23, pokeemerald 731ad5b.18 Cinq petits scripts ont fait le décompte ; chacun est nommé dans les notes avec sa sortie enregistrée.
Un chiffre d’abord, car tout le reste se compte en images. La Game Boy Advance dessine une image en 280 896 cycles d’une horloge à 2^24 hertz, soit 59,7275 images par seconde et 16,743 millisecondes par image ; GBATEK l’arrondit à « ca. 59.737 Hz ».19 L’horloge à 4 194 304 hertz de la Game Boy d’origine, divisée par 70 224 points par image, donne les mêmes 59,7275 ; Pan Docs indique « @ 59.7 fps ».19 Les millisecondes de cet article sont calculées sur la base de 59,7275.19
Chaque vitesse est une table dont la somme fait seize
Émeraude ne déplace pas un personnage selon la vitesse multipliée par le temps. Chaque vitesse sur la carte du monde est une table de déplacements en pixels par image, et le code source dit ce que toutes ces tables ont en commun : « Over the course of the step animation, these sum to 16 pixels (one full metatile). » (Sur toute l’animation du pas, leur somme fait 16 pixels, soit une metatile entière.)2 sStep1Funcs compte seize déplacements d’un pixel, sStep2Funcs huit de deux, sStep4Funcs quatre de quatre, sStep8Funcs deux de huit, et sStep3Funcs fait exception : Step2, Step3, Step3, Step2, Step3, Step3.2 Compté par script dans le code du déplacement, cela donne :
| Constante de vitesse | Pixels par image | Images par cellule | Cellules par seconde | Millisecondes par cellule | Utilisée pour |
|---|---|---|---|---|---|
MOVE_SPEED_NORMAL |
1 à chaque image | 16 | 3,73 | 267,9 | la marche ; la marche des PNJ |
MOVE_SPEED_FAST_1 |
2 à chaque image | 8 | 7,47 | 133,9 | course, surf, glissades sur la glace |
MOVE_SPEED_FAST_2 |
2, 3, 3, 2, 3, 3 | 6 | 9,95 | 100,5 | le Vélo Cross, les courants marins |
MOVE_SPEED_FASTER |
4 à chaque image | 4 | 14,93 | 67,0 | le Vélo Course à pleine vitesse |
MOVE_SPEED_FASTEST |
8, 8 | 2 | 29,86 | 33,5 | actions de déplacement par glissement |
Source de chaque ligne : measure_gen3_motion.py sur src/event_object_movement.c.1
C’est le chiffre de la marche qu’il faut retenir : un pixel à chaque image, seize images par cellule, 3,73 cellules par seconde, 268 millisecondes par cellule, utilisé par PlayerWalkNormal et par chaque PNJ qui marche.1 Courir avec le bouton B, c’est PlayerRun, et surfer, c’est PlayerWalkFast, que le code annote « same speed as running » (même vitesse que la course) ; les glissades sur la glace appellent la même fonction. Les trois avancent de deux pixels par image, huit images par cellule, 7,47 cellules par seconde.110 Il existe aussi une marche lente, UpdateWalkSlowAnim, qui avance d’un pixel sur les valeurs paires du minuteur, 31 à 32 images par cellule, pour les scènes scriptées.1
Le Vélo Cross est la seule vitesse aux pixels irréguliers : 2, 3, 3, 2, 3, 3 sur six images, 2,67 pixels par image en moyenne, 9,95 cellules par seconde.1 On l’atteint par PlayerRideWaterCurrent depuis AcroBikeTransition_Moving, la même fonction que celle des courants marins.120 À mon sens, cette irrégularité est le prix d’une vitesse qui ne divise pas seize : trois pixels par image dépasseraient la cellule, si bien que la table alterne des deux et des trois pour tomber exactement sur seize. Toutes les autres vitesses sont des diviseurs entiers de la cellule.
Le Vélo Course accélère par pas plutôt que par image. sMachBikeSpeedCallbacks vaut PlayerWalkNormal, PlayerWalkFast, PlayerWalkFaster, et bikeFrameCounter augmente de un à chaque pas jusqu’à un plafond de 2, si bien que le premier pas depuis l’arrêt prend 16 images, le deuxième 8, et chaque pas suivant 4 : quatre pixels par image, 14,93 cellules par seconde.120 Le vélo ne se trouve jamais entre deux vitesses. Chaque pas est l’une des tables, exécutée jusqu’au bout, et la vitesse ne change qu’à une frontière de cellule.
Chaque allure mesurée dans Rouge, Cristal et Émeraude correspond à un nombre entier d’images par cellule de 16 pixels ; les chiffres de Kiradex sont un modèle de son code, pas une capture.14621
Un appui tous les seize pixels
L’animation de marche est enjambée, debout, enjambée, debout. sAnim_GoSouth vaut ANIMCMD_FRAME(3, 8), (0, 8), (4, 8), (0, 8) : le dessin 3 pendant huit images, le dessin debout 0 pendant huit, le dessin 4 pendant huit, de nouveau le dessin debout pendant huit, 32 images en tout, soit deux cellules de marche.91 La durée est exacte : sprite.c charge la durée de l’image moins un dans le compteur de délai et décompte jusqu’à zéro, si bien qu’une image de durée 8 reste à l’écran pendant huit images.91 Chaque pas affiche donc un dessin d’enjambée et un dessin debout, et SetStepAnimHandleAlternation démarre chaque nouveau pas sur l’autre moitié du cycle (animPos = {1, 3, 0, 2}), de sorte que la jambe gauche et la jambe droite alternent pas après pas, même quand le joueur s’arrête puis repart.2 Le premier article de cette série décrivait le même cycle du point de vue des graphismes : « step, stand, step, stand at eight ticks each, which is the bob everyone remembers. » (pas, debout, pas, debout, à huit ticks chacun, le petit balancement dont tout le monde se souvient.)22
Les allures plus rapides conservent les dessins et raccourcissent leur durée. GoFast tient chaque dessin quatre images (un cycle de 16 images), GoFaster deux (8 images), GoFastest une (4 images).1 La course a ses propres dessins, aux durées inégales : sAnim_RunSouth vaut (12, 5), (9, 3), (13, 5), (9, 3), un cycle de 16 images.19
Mettez les deux tables côte à côte et la conception apparaît. Le cycle de 32 images de la marche couvre deux cellules de 16 images ; le cycle de 16 images de la course couvre deux cellules de 8 ; le cycle GoFaster de 8 images du Vélo Course couvre deux cellules de 4.19 À toutes les vitesses, un appui tombe tous les 16 pixels.1 Ce sont deux horloges, pas un seul compteur. Le dessin avance quand animDelayCounter, chargé dans sprite.c à partir de la durée de chaque image, arrive à son terme ; le pas avance quand NpcTakeStep indexe la table de la vitesse avec le sTimer du sprite, une entrée par image ; et SetStepAnimHandleAlternation choisit l’animation de l’allure au début de chaque pas.92 Ce qui les maintient ensemble, c’est que les deux se comptent dans les mêmes images et que les durées ont été choisies pour concorder, allure par allure. À mon sens, c’est là ce qu’il faut reprendre : la cadence des dessins ne peut pas se décaler par rapport au mouvement, parce que les dessins de chaque pas durent exactement autant d’images que le pas lui-même. Rien dans ces tables ni dans mon modèle ne mesure où se trouve un pied posé par rapport au sol, je n’affirme donc rien de plus.
Se tourner, se heurter, et quand la commande est lue
Un pas commencé se termine ; la croix directionnelle est lue quand il s’achève, si bien qu’une direction maintenue enchaîne les pas sans interruption et qu’un changement de direction en marchant ne coûte rien.10 À l’arrêt, c’est différent. CheckMovementInputNotOnBike ne renvoie TURN_DIRECTION que lorsque la nouvelle direction diffère de l’orientation et que le joueur n’est pas déjà en mouvement, et PlayerTurnInPlace joue la marche sur place rapide, à laquelle InitMoveInPlace donne une durée de 8 images : 134 millisecondes pour pivoter sur place.101 C’est ce mécanisme qui permet au joueur de faire face à un panneau ou à une personne sans faire un pas vers eux. Marcher contre un mur joue à la place la marche sur place lente, 32 images, 536 millisecondes, avec un bruit de choc : un pas refusé est montré, pas avalé.110 Les marches sur place normale et plus rapide durent 16 images (268 millisecondes) et 4 (67).1
À mon sens, ces quatre chiffres représentent l’essentiel de ce que « réactif » veut dire dans un jeu sur grille. Le monde ne se déplace jamais d’une fraction de cellule, l’intention du joueur s’exprime donc en pas entiers, et la seule latence est le reste du pas en cours : au plus 268 millisecondes en marchant, 134 en courant.1 La rotation depuis l’arrêt n’est pas une latence, mais une action distincte avec son propre résultat visible.
2. La caméra, les portes, les fondus et les secousses d’Émeraude
La caméra n’a ni retard ni avance
La caméra d’Émeraude est un sprite invisible qui suit le joueur. CameraObject_UpdateMove copie le x et le y du sprite suivi et stocke la différence par rapport à l’image précédente dans sCamera_MoveX et sCamera_MoveY ; CameraUpdateCallback transmet cette différence à CameraUpdate, qui fait défiler la carte d’exactement autant de pixels.212 La boucle de la carte du monde exécute RunTasks(); AnimateSprites(); CameraUpdate(); UpdateCameraPanning(); dans cet ordre à chaque image, et comme AnimateSprites exécute les callbacks des sprites dans l’ordre des emplacements et que l’objet caméra est créé après le joueur (InitPlayerAvatar, puis InitCameraUpdateCallback(gPlayerAvatar.spriteId)), la caméra lit la position que le joueur a atteinte sur cette même image.3 J’ai suivi cet ordre dans le code sans l’exécuter ; c’est donc une lecture, pas une mesure, mais le résultat qu’elle implique est simple. Le déplacement du joueur et le défilement tombent sur la même image, du même nombre de pixels. À l’écran, le joueur ne bouge jamais, tandis que le monde glisse sous ses pieds.
Itay Keren appelle cela, dans sa conférence de la GDC 2015 sur les caméras de jeux à défilement horizontal, le position-locking (verrouillage sur la position) : la caméra reste sur le joueur, « keeping the car in focus at all times and the camera motion completely predictable. » (en gardant la voiture au point en permanence et le mouvement de caméra entièrement prévisible.)23 Émeraude ajoute une chose que sa définition laisse ouverte : ce qui se passe au bord de la carte. Elle ne s’arrête jamais. Les cellules situées hors de la disposition d’une carte sont lues via GetBorderBlockAt, qui renvoie les metatiles de bordure 2 par 2 de la disposition, répétées et marquées MAPGRID_IMPASSABLE, si bien que la vue garde le joueur à la même position à l’écran et remplit l’extérieur d’arbres ou d’eau de bordure.11 L’edge-snapping (aimantation au bord) de Keren, l’autre option, « simply snaps the camera to the edge of the level, allowing the character to move away from its anchor point. » (aimante simplement la caméra au bord du niveau, laissant le personnage s’éloigner de son point d’ancrage.)23 Émeraude s’en est passée en dessinant.
La caméra que Game Freak a écrite puis désactivée
field_camera.c contient une anticipation complète pour le vélo. CameraPanningCB_PanAhead décale le panoramique vertical de 2 pixels par mise à jour, depuis sa valeur de repos de 32 vers 72 ou vers moins 8 selon le sens de déplacement, soit 40 pixels dans un sens comme dans l’autre : une caméra qui devance le joueur vers l’endroit où il va.12 Elle ne s’exécute que si gUnusedBikeCameraAheadPanback est vrai, la variable n’est jamais mise qu’à FALSE (dans bike.c), et la branche porte le commentaire « this code is never reached. » (ce code n’est jamais atteint.)1220 Dans le vocabulaire de Keren, c’est un dual-forward-focus ou un target-focus, la caméra qui suit sa règle : « When you walk left, you want to see more to the left. » (Quand on marche vers la gauche, on veut voir davantage à gauche.)23 Quelle que soit la raison pour laquelle elle est restée désactivée, le jeu livré a gardé la caméra verrouillée même aux 4 pixels par image du Vélo Course.1
Une porte s’ouvre en vingt images, pas en seize
La table des portes dans field_door.c se lit comme si chaque dessin prenait quatre images : sDoorOpenAnimFrames vaut {4, -1}, {4, 0}, {4, 0x100}, {4, 0x200}, la porte fermée plus trois dessins ouverts.24 Mais AnimateDoorFrame dessine quand son compteur vaut 0 et avance quand le compteur égale la durée de l’entrée, si bien que chaque dessin reste affiché pendant cinq mises à jour : 84 millisecondes par dessin, 335 pour les quatre.241 L’article de cette série sur les structures donnait le même chiffre, cinq mises à jour et environ 84 millisecondes.13 Les notes de recherche à partir desquelles cet article a été écrit se trompaient, « 4 ticks each (16 frames, about 0.27 s at 59.7 Hz), » (4 ticks chacun, soit 16 images, environ 0,27 s à 59,7 Hz), tout comme un commentaire dans le code des portes de l’application elle-même, qui dit qu’elle s’ouvre « at about Emerald’s four ticks a frame » (à peu près aux quatre ticks par image d’Émeraude), puis tient le premier dessin 70 millisecondes ; les deux sont corrigés ici et dans le cahier des charges.1417
L’entrée elle-même, Task_DoDoorWarp, comporte cinq états dont aucun ne peut être sauté : figer les autres objets, jouer le son de la porte et ouvrir la porte au-dessus du joueur ; forcer un pas MOVEMENT_ACTION_WALK_NORMAL_UP dans l’embrasure ; quand le joueur est immobile, refermer la porte et masquer le joueur ; quand la tâche de la porte se termine, faire un fondu sur la musique et l’écran ; charger la carte.25 La sortie déroule le cérémonial à l’envers : Task_ExitDoor montre la porte déjà ouverte, attend le fondu d’ouverture, force un pas WALK_NORMAL_DOWN, referme la porte, et seulement ensuite rend les commandes.25
En additionnant les décomptes ci-dessus et la trace du fondu ci-dessous, la porte s’ouvre en 20 images, le pas en prend 16 et la porte se referme en 20 ; le fondu qui suit effectue son dernier mélange sur sa 17e image, si bien que l’écran est entièrement sombre après au moins 73 images, 1,22 seconde. La carte ne peut commencer à se charger qu’une fois le fondu devenu inactif, sur sa 22e image, et que Task_WarpAndLoadMap l’a constaté et poursuivi : au moins 79 images, 1,32 seconde.525 Les changements d’état entre les phases de la porte et l’attente de l’arrêt de la musique ne peuvent qu’ajouter des images, ce sont donc deux bornes inférieures.5
L’entrée d’Émeraude est une borne inférieure tirée de ses décomptes d’images ; la ligne de Kiradex est lue dans le code des portes de l’application, pas chronométrée sur un téléphone.151721
Un fondu, c’est neuf niveaux
Un fondu de téléportation dans Émeraude n’est pas une rampe lisse. BeginNormalPaletteFade fixe un pas de 2 sur un coefficient de mélange qui va de 0 à 16, UpdateNormalPaletteFade mélange les palettes d’arrière-plan lors d’un appel et les palettes de sprites lors du suivant, puis fait avancer le coefficient, et IsSoftwarePaletteFadeFinishing ajoute cinq appels à la fin.26 L’image sur laquelle tombe chaque appel dépend de qui l’appelle. La tâche de la porte lance le fondu depuis RunTasks, et BeginNormalPaletteFade exécute lui-même une mise à jour, copie immédiatement le résultat dans la mémoire des palettes et efface l’indicateur qui, sinon, ferait attendre la mise à jour suivante jusqu’au retour vertical ; plus tard dans la même image, OverworldBasic appelle de nouveau UpdatePaletteFade.25263 La première image reçoit donc deux mises à jour, toutes deux au niveau 0, et chaque image suivante une seule. Un portage Python de palette.c suivant ce calendrier, en comptant l’image de la tâche comme 0, donne neuf niveaux, 0, 2, 4 et ainsi de suite jusqu’à 16 : les palettes d’arrière-plan atteignent le niveau 2 à l’image 1 et le niveau 16 à l’image 15, les palettes de sprites une image plus tard à chaque fois, le dernier mélange à l’image 16 (285 millisecondes en comptant l’image 0), et le fondu devient inactif à l’image 21 (368 millisecondes).5 Un mélange calculé sur une image atteint l’écran au retour vertical qui termine cette image. Le portage suppose qu’aucun fondu n’était en cours et que la mise à jour normale de la carte du monde s’applique ; avec la pluie, la neige, le brouillard, l’ombre ou la sécheresse actifs, le fondu de fermeture se déroule de la même façon à partir des couleurs teintées par la météo (FadeScreen copie d’abord le tampon teinté, puis appelle le même BeginNormalPaletteFade), mais le fondu d’ouverture est géré par le code météo, et ce chemin n’a pas été simulé.5
Neuf niveaux espacés de deux seizièmes, arrière-plan et sprites à une image d’écart : la rampe que le cahier des charges adapte à un seul voile.521
Les téléportations font un fondu au noir, avec une seule famille d’exceptions, et cette exception a un sens. WarpFadeOutScreen interroge GetMapPairFadeToType sur la paire de types de cartes, WarpFadeInScreen interroge GetMapPairFadeFromType, et chacune appelle FadeScreen avec du blanc quand la réponse est vraie, du noir sinon.25 Toutes deux cherchent la paire dans sTransitionTypes, dont les 16 lignes couvrent chaque type de carte à l’entrée et à la sortie de MAP_TYPE_UNDERGROUND ; la première renvoie l’indicateur d’entrée d’une ligne et la seconde son indicateur de sortie, vrais uniquement sur les lignes qui entrent dans une grotte et uniquement sur celles qui en sortent.27 En entrant dans une grotte, l’écran fait donc un fondu au blanc puis réapparaît depuis le noir, et en en sortant, un fondu au noir puis réapparaît depuis le blanc. Chaque ligne nomme aussi sa propre routine de transition de grotte, que je n’ai pas suivie.27 Un fondu blanc plus lent, FadeInFromWhite avec un délai de 8, devient inactif après 86 ou 87 images, 1,44 à 1,46 seconde ; il est lancé depuis le callback de chargement de la carte, dont je n’ai pas suivi la première image, et le portage donne les deux cas.525
La secousse est un panoramique scripté
La secousse d’écran dans Émeraude est une commande de script, pas un effet physique. ShakeCamera lit un panoramique vertical, un panoramique horizontal, un nombre de secousses et le nombre d’images entre secousses dans quatre variables de script, et inverse le panoramique à chaque secousse.28 Les scripts du jeu en contiennent 24 appels répartis dans 9 fichiers, et un script les a tous comptés :29
| Vertical px | Horizontal px | Secousses | Images d’écart | Appels | Durée |
|---|---|---|---|---|---|
| 1 | 1 | 8 | 5 | 10 | 40 images, 670 ms |
| 1 | 1 | 8 | 3 | 4 | 24 images, 402 ms |
| 1 | 2 | 8 | 5 | 3 | 40 images, 670 ms |
| 2 | 2 | 8 | 5 | 2 | 40 images, 670 ms |
| 0 | 3 | 4 | 2 | 2 | 8 images, 134 ms |
| 1 | 3 | 20 | 5 | 1 | 100 images, 1 674 ms |
| 1 | 1 | 16 | 3 | 1 | 48 images, 804 ms |
| 1 | 1 | 32 | 2 | 1 | 64 images, 1 072 ms |
Source : measure_gen3_shake.py sur data/**/*.inc.29
Dix des 24 sont la même secousse : un pixel dans chaque sens, huit inversions, cinq images d’écart, 670 millisecondes.29 La plus ample fait 3 pixels à l’horizontale ; la plus longue dure 100 images, 1,67 seconde.29 La secousse de l’ascenseur, que l’article sur les structures a traitée (une secousse toutes les trois images, un nombre de fois qui augmente avec les étages parcourus), est une routine distincte et ne figure pas dans ce décompte.13 À mon sens, la leçon est la retenue : une secousse dans Émeraude, c’est un pixel ou deux, réservée à un événement que le script a jugé important, jamais à un pas ou à une porte.
3. Rouge et Cristal : le même pas à 30 mises à jour par seconde
Rouge avance de deux pixels une image sur deux
Pokémon Version Rouge n’a pas de tables de pas. Sa boucle de carte du monde, OverworldLoop, appelle DelayFrame puis tombe dans OverworldLoopLessDelay, qui l’appelle à nouveau, si bien que le monde se met à jour une image sur deux.30 Un pas fixe wWalkCounter à 8, et à chaque passage AdvancePlayerSprite le décrémente et fait défiler les registres d’arrière-plan hSCX et hSCY du vecteur de pas décalé d’un bit vers la gauche : 2 pixels.30 Huit passages de 2 pixels font 16 pixels en 16 images, les mêmes 268 millisecondes et 3,73 cellules par seconde qu’Émeraude, par sauts de 2 pixels à 30 mises à jour par seconde.430 Le vélo ajoute une seconde avancée par passage : DoBikeSpeedup appelle de nouveau AdvancePlayerSprite (sauf sur la Piste Cyclable (Cycling Road) tant que haut, gauche ou droite est maintenu), si bien qu’un pas prend 8 images.430
Les dessins de marche de Rouge changent tous les quatre passages, soit huit images, selon un cycle de quatre images, debout, pas, debout et pas en miroir, si bien qu’il affiche lui aussi un dessin d’enjambée par pas.3122 UpdatePlayerSprite incrémente le compteur interne à l’animation à chaque passage et fait avancer le dessin quand il atteint 4.3122
Rouge se tourne en un passage de boucle, deux images : une nouvelle direction depuis l’arrêt écrit l’orientation et revient dans la boucle sans faire de pas.30 Son demi-tour comporte dans le code une orientation intermédiaire que, selon le commentaire du code lui-même, personne ne voit : « It is unlikely for it to ever be visible because DelayFrame is called at the start of OverworldLoop. » (Il est peu probable qu’elle soit jamais visible, car DelayFrame est appelé au début d’OverworldLoop.)30 Je l’ai lu dans le code et ne l’ai pas exécuté dans un émulateur.
La téléportation, c’est un son et un fondu, sans porte dessinée. PlayMapChangeSound joue SFX_GO_INSIDE quand la tuile est une porte (tuile $0b) et SFX_GO_OUTSIDE sinon, puis GBFadeOutToBlack écrit quatre palettes, en maintenant chacune pendant huit images : 32 images, 536 millisecondes.432 Je n’ai pas suivi le fondu de réapparition de Rouge.
Cristal se met lui aussi à jour une image sur deux
Cristal conserve la cadence de Rouge avec un autre mécanisme. MaxOverworldDelay vaut db 2, et le gestionnaire VBlank décompte wOverworldDelay, si bien que les objets de la carte se mettent à jour une image sur deux.33 StepVectors donne à la marche huit mises à jour de 2 pixels et au vélo quatre de 4 : 16 images et 8 images par cellule, comme Rouge et Émeraude.4 Le pas lent compte seize mises à jour de 1 pixel, 32 images, 1,87 cellule par seconde.433
Les portes de Cristal font un fondu au blanc, et rapide. MapSetupScript_Door commence par FadeOutToWhite et MapSetupScript_Warp se termine par FadeInFromWhite ; chacun compte quatre étapes de palette espacées de deux images, 8 images, 134 millisecondes dans chaque sens.434 Sa rotation est une fonction de pas en quatre parties, StepFunction_Turn, dont les parties s’enchaînent les unes dans les autres. À la première mise à jour, .init1 fixe une OBJECT_STEP_DURATION de 2 et tombe dans .step1, qui la décompte jusqu’à 1 ; à la deuxième, .step1 la décompte jusqu’à 0 et traverse .init2, qui écrit la nouvelle orientation et remet 2, pour tomber dans .step2, qui la décompte jusqu’à 1 ; à la troisième, .step2 atteint 0 et rend l’objet à STEP_TYPE_FROM_MOVEMENT.33 Rejouée instruction par instruction, la routine prend trois mises à jour, six images à raison de deux images par mise à jour, la nouvelle orientation étant écrite à la deuxième. C’est la routine seule, lue dans le code ; je n’ai pas suivi le temps entre l’appui sur le bouton et le début de la routine.33
Trois générations, trois fondus
| Rouge | Cristal | Émeraude | |
|---|---|---|---|
| Porte | aucune dessinée ; le son dépend de la tuile | aucune dessinée | 4 dessins de 5 images (335 ms), avec un son de porte coulissante ou à charnières |
| Entrée dans la porte | le pas qui arrive dessus | le pas qui arrive dessus | un pas forcé vers le haut (16 images), puis la porte se referme |
| Fondu de sortie | au noir, 4 palettes maintenues 8 images chacune : 32 images (536 ms) | au blanc, 4 étapes à 2 images d’intervalle : 8 images (134 ms) | au noir (au blanc en entrant dans une grotte), 9 niveaux, dernier mélange à l’image 16 en comptant l’image de la tâche de la porte comme 0 (285 ms), inactif à l’image 21 |
| Fondu d’entrée | non suivi | depuis le blanc, 8 images | depuis le noir (depuis le blanc en sortant d’une grotte), les mêmes 9 niveaux |
| Sortie par la porte | un pas forcé vers le bas | un pas forcé | la porte montrée ouverte, un pas forcé vers le bas, la porte se referme, puis les commandes |
Sources : les décomptes des sections 1 à 3 ;14525 le pas forcé de Rouge en sortant d’une porte, PlayerStepOutFromDoor, provient de l’article sur les structures.13
Les lignes sur lesquelles les trois s’accordent sont celles qui méritent d’être conservées : un fondu entre les cartes, un pas forcé qui fait franchir le seuil au joueur, et aucune commande pendant le cérémonial. Les durées de fondu diffèrent d’un facteur quatre ; à mon sens, la durée est donc un choix de conception plutôt qu’un canon. Le fondu à neuf niveaux d’Émeraude est celui qui a été conçu autour de portes dessinées, et Kiradex dessine ses portes,13 c’est donc lui que retient le cahier des charges.
Rouge, Cristal et Émeraude, un jeu par génération parmi les trois de la Game Boy et de la Game Boy Advance, deux machines différentes, ont adopté le même contrat : un pas fait 16 pixels, prend 16 images à environ 59,73 hertz et ne peut pas être interrompu une fois commencé.14 Rouge y parvient par sauts de 2 pixels parce que sa boucle attend deux images par passage ; Cristal par le même délai de deux images avec des vecteurs de 2 pixels ; Émeraude à la fréquence d’images complète avec des déplacements de 1 pixel. La course, le surf et le vélo suivent le même contrat à 8 ou 4 images.14 Quand on dit que ces jeux donnent l’impression d’être « sur des rails », je pense que c’est là le rail : le pas, le dessin et la caméra se comptent tous dans les mêmes images, avec des durées choisies pour concorder, si bien qu’ils ne peuvent pas se désynchroniser.
4. Les références modernes : la caméra de Celeste, le vocabulaire de Keren et les portages sur téléphone
Quand une caméra doit lisser, et quand elle ne le doit pas
La caméra verrouillée d’Émeraude est une réponse parmi d’autres dans un vocabulaire qu’Itay Keren a exposé dans « Scroll Back: The Theory and Practice of Cameras in Side-Scrollers », version remaniée de sa conférence à l’Independent Games Summit de la GDC 2015, publiée sur Game Developer le 11 mai 2015.23 Les termes que j’emploie dans cet article sont les siens. Le position-locking fixe la caméra sur le joueur. L’edge-snapping l’arrête au bord du niveau. Une camera-window ne déplace la caméra que lorsque le joueur pousse le bord de la fenêtre. Le lerp-smoothing fait glisser la caméra vers sa cible, et il le qualifie de « a standard tool in reducing jarring camera speeds, particularly jumps. » (outil standard pour atténuer les vitesses de caméra brusques, en particulier lors des sauts.) Le target-focus et le dual-forward-focus font devancer le joueur dans le sens du déplacement. Il nomme aussi le platform-snapping, le region-focus et les cue attractors, dont un personnage qui marche sur une grille n’a que faire.23
Il donne la raison pour laquelle le mouvement de caméra compte : « conflicting sensory signals (Visual vs. Vestibular) may lead to discomfort and nausea, and though it’s worse in 3D (especially VR), it is still very much in effect in 2D games. » (des signaux sensoriels contradictoires, visuels contre vestibulaires, peuvent provoquer inconfort et nausée ; c’est pire en 3D, surtout en VR, mais le phénomène reste bien réel dans les jeux en 2D.) Et il donne le cas où le schéma le plus simple est le bon : le position-locking pour « a crafting adventure game like Terraria, with a small character relative to the screen with pretty small jumps, it works very well. » (un jeu d’aventure et d’artisanat comme Terraria, avec un personnage petit par rapport à l’écran et des sauts assez modestes, fonctionne très bien.)23 Une ville vue de dessus avec un collectionneur de 30 pixels, la taille adoptée par l’article de cette série sur les personnages à la build 34, correspond exactement à ce cas, sauts en moins.35
Celeste est la référence moderne pour l’autre choix. Ses développeurs ont publié la classe Player « as a learning resource and for general interest, » (comme ressource d’apprentissage et par intérêt général), la licence MIT ne couvrant que ce code, et la caméra y tient en quelques lignes : level.Camera.Position = from + (target - from) * (1f - (float)Math.Pow(0.01f / multiplier, Engine.DeltaTime)), sous le commentaire « Camera (lerp by distance using delta-time) » (caméra : interpolation selon la distance, avec delta-time).3637 Avec un multiplicateur de 1 et une cible immobile, cela comble 99 pour cent de l’écart chaque seconde, quelle que soit la fréquence d’images ; or la cible de Celeste n’est pas immobile, puisqu’elle est recalculée à chaque image à partir de la position et de l’état du joueur, si bien que ce chiffre décrit le lissage, pas l’endroit où la caméra finit par se placer.36 La cible est le joueur centré dans la vue du jeu (X - Celeste.GameWidth / 2), limitée aux bornes de la salle, avec des décalages pour quelques états : 48 pixels d’avance dans la direction du dash StRedDash, 64 vers le haut pour le lancement du sommet.36 Le premier article de cette série notait que Celeste effectue le rendu de son monde en 320 par 180 et le multiplie par six.22
À mon sens, les deux ne se contredisent pas. Celeste lisse parce que les sauts d’un jeu de plateforme tireraient la vue vers le haut et vers le bas à chaque bond ; la définition même du lerp-smoothing selon Keren porte sur les sauts. Un personnage sur une grille se déplace à vitesse constante en ligne droite ; un verrouillage ne produit donc aucune secousse à lisser, et une caméra lissée ne fait qu’ajouter une traîne derrière un mouvement déjà régulier. La section 7 montre ce que mesure cette traîne dans le modèle de Kiradex.
Ce que les autres jeux nous apprennent sur la vitesse
Stardew Valley exprime la vitesse du joueur comme une statistique sans unité : « 2 when walking, » (2 en marchant), « 5 when running, » (5 en courant), « 6.6 when riding a Horse (7 if the horse was fed a carrot that day), » (6,6 à cheval, 7 si le cheval a mangé une carotte ce jour-là), jamais en dessous de 1.38 Le wiki ne dit pas à combien de pixels par tick correspond une unité ; aucun chiffre en tuiles par seconde n’est donc donné ici pour Stardew.
J’ai aussi cherché une source primaire sur les caméras de Sea of Stars, Eastward et CrossCode, sans en trouver : des entretiens sur les déplacements et l’éclairage, rien sur la caméra, et l’option « Pixel Perfect » de Sea of Stars décrite seulement par des guides et des forums. Aucune conférence ni aucun article de Maddy Thorson sur la caméra n’est apparu non plus, ce qui explique que Celeste soit cité à partir de son code.39
Sur téléphone : toucher pour marcher, avec une porte de sortie
La version mobile de Stardew Valley propose neuf schémas de commande, et celui par défaut est « Tap-to-move & Auto-Attack » (toucher pour se déplacer et attaque automatique) : « Tap anywhere on screen and the farmer will walk to where you tapped. » (Touchez n’importe où sur l’écran et le fermier marchera jusqu’à l’endroit touché.)40 Garder le doigt appuyé « will cause the character to follow the touch, » (fait suivre le contact au personnage), et le wiki prévient que le mode suivi « is very literal, moving directly towards the finger without routing around blocking objects. » (est très littéral et se dirige droit vers le doigt sans contourner les obstacles.) Le schéma à joystick invisible occupe « the left half of the screen » (la moitié gauche de l’écran), centré là où vous touchez. Et le wiki est honnête sur la limite du schéma par défaut : certaines tâches qui exigent un positionnement précis « can not be completed using the default controls; temporarily switching to a control style with a movement joystick is necessary in such cases. » (ne peuvent pas être accomplies avec les commandes par défaut ; il faut alors passer temporairement à un schéma doté d’un joystick de déplacement.)40 Ces schémas sont arrivés avec une mise à jour dont TouchArcade a rendu compte le 1er novembre 2018, avec une option permettant de revenir à « the default tap-to-move and auto-attack controls. » (les commandes par défaut, toucher pour se déplacer et attaque automatique.)41
Le Pixel Remaster du premier FINAL FANTASY de Square Enix sur iOS a ajouté un réglage par défaut marche ou course dans sa version 1.2.0, datée du 11 mars 2025 dans l’historique de l’app sur l’App Store (la série est publiée sous forme d’apps distinctes, et je n’ai consulté que celle-ci) : « In tap based movement mode the character controlled will always run as the default speed when moving. » (En mode de déplacement au toucher, le personnage contrôlé courra toujours par défaut lors de ses déplacements.)42 La prise en charge des manettes était arrivée sur les versions mobiles avec une mise à jour couverte par TouchArcade le 30 janvier 2024.43 Je n’ai trouvé nulle part de documentation du schéma exact de déplacement tactile au-delà de ces notes, ni de description du déplacement de Terraria sur mobile sur la page du wiki que j’ai consultée.39
Les Human Interface Guidelines d’Apple disent la même chose du point de vue de la plateforme. Pour les jeux tactiles : « consider letting players tap objects to select them instead of adding a virtual selection button » (envisagez de laisser les joueurs toucher les objets pour les sélectionner plutôt que d’ajouter un bouton de sélection virtuel) ; « For movement control, opt to show a virtual thumbstick wherever the player lands their thumb instead of a static thumbstick position » (pour le déplacement, affichez un stick virtuel là où le joueur pose le pouce plutôt qu’à une position fixe) ; « Make sure frequently used controls are a minimum size of 44x44 pt » (les commandes fréquentes doivent mesurer au moins 44 × 44 pt) ; « Always include visible and tactile press states » (prévoyez toujours des états d’appui visibles et tactiles) ; et pour la marche et le sprint, « consider combining the actions into a single control. » (envisagez de réunir les deux actions dans une seule commande.) Le journal des modifications de la page date ces pratiques pour les commandes tactiles du 9 juin 2025.44 À la WWDC25, la session d’Apple sur les commandes tactiles posait clairement la prémisse : « the vast majority of players won’t have a controller available. » (la grande majorité des joueurs n’auront pas de manette à disposition.)45
Aucune de ces sources ne dit que toucher pour marcher est la règle pour les jeux sur téléphone, et je ne le prétends pas. Ce que j’en retiens, comme recommandation pour Kiradex et non comme constat, c’est le schéma que Kiradex suit déjà à moitié : toucher pour marcher par défaut, comme le propose la version mobile de Stardew ; un stick qui apparaît là où se pose le pouce, comme le conseillent les HIG, pour la précision ; et pas de croix directionnelle fixe à l’écran.4044
5. Le savoir-faire, en règles chiffrées
Voici les règles par rapport auxquelles est rédigé le cahier des charges de la section 7, chacune rattachée à une mesure ci-dessus. « Tick » désigne un pas de simulation de 1/60 de seconde ; c’est ainsi qu’une image à 59,73 hertz devient quelque chose qu’un iPhone peut tenir ; à 16 ticks par cellule, la marche fait 3,75 cellules par seconde au lieu de 3,73.119
| Élément | Règle | Source |
|---|---|---|
| Marche | 1 pixel par tick sur des tuiles de 16 pixels : 16 ticks par tuile, 3,75 tuiles par seconde | Rouge, Cristal et Émeraude parcourent tous 16 pixels en 16 images, 3,73 cellules par seconde |
| Course | 2 pixels par tick, 8 ticks par tuile ; aucune vitesse qui ne divise pas 16 | la course et le surf d’Émeraude font 2, le vélo 4 ; seul le 2-3-3 du Vélo Cross est irrégulier |
| Cycle de marche | un appui tous les 16 pixels à toutes les vitesses ; ma proposition pour Kiradex est de choisir le dessin selon la distance parcourue, soit, pour six dessins sur 32 pixels, le dessin floor(distance × 6 / 32) mod 6 |
Émeraude cadence dessins et pas en images, séparément, avec des longueurs qui concordent : sa marche est un cycle de 32 images sur deux cellules de 16 images, sa course un cycle de 16 images sur deux cellules de 8 images |
| Pas | une fois commencé, il se termine ; la commande est lue à la tuile | Émeraude et Rouge lisent la croix directionnelle quand le pas s’achève |
| Rotation | 8 ticks, environ 133 ms (les 8 images d’Émeraude font 134), depuis l’arrêt ; aucune en marchant | Émeraude WalkInPlaceFast vaut 8 ; la routine de rotation de Cristal 6 images ; celle de Rouge 2 |
| Pas refusé | montré, pas ignoré : une marche sur place de 32 ticks avec un heurt | Émeraude WalkInPlaceSlow vaut 32 |
| Caméra | verrouillée sur le joueur : ni retard ni avance, déplacée au même tick du même nombre de pixels | l’objet caméra d’Émeraude ; la seule anticipation écrite par Game Freak est désactivée |
| Bord de carte | dessiner l’extérieur et garder le verrouillage, ou borner (edge-snapping) ; jamais lisser | les tuiles de bordure d’Émeraude ; l’edge-snapping de Keren ; les bornes de salle de Celeste |
| Porte | 4 dessins tenus 5 ticks chacun : 83 ms par dessin, 333 ms pour l’ouverture (84 et 335 dans Émeraude) | Émeraude field_door.c |
| Fondu | 9 niveaux étagés, un tous les 2 ticks, le dernier au tick 16, 18 ticks (0,30 s) en tout, en sortie comme en entrée ; noir par défaut. Une adaptation, pas une copie | le fondu normal d’Émeraude atteint chaque niveau sur les mêmes images paires dans ses palettes de sprites, une image après ses palettes d’arrière-plan, fait son dernier mélange à l’image 16 et devient inactif à l’image 21 ; en entrant dans une grotte, il fait un fondu au blanc, et en en sortant, il réapparaît depuis le blanc ; les 8 images de Cristal sont l’extrémité rapide, les 32 de Rouge l’extrémité lente |
| Cérémonial d’entrée | 74 ticks, environ 1,23 s, sans commande : ouverture 20, pas vers l’intérieur 16, fermeture 20, fondu 18 | l’entrée par une porte dans Émeraude : sombre après au moins 73 images, chargement de la carte au plus tôt à l’image 79 (1,32 s) |
| Secousse | 1 pixel, 8 inversions, 5 ticks d’écart (40 ticks, 0,67 s), uniquement pour les événements scriptés | la plus courante des 24 secousses d’Émeraude |
| Fréquence d’images | simuler à 60 quoi que fasse l’écran ; demander 60, pas 120 | la priorité accordée par Apple aux jeux à 30 et 60 ; RealityKit effectue généralement son rendu à 60 |
| Retour haptique | confirmer des événements, pas des pas ; le rendre facultatif | les HIG d’Apple sur la lecture des retours haptiques |
| Commandes | ma recommandation : toucher pour marcher par défaut ; un stick flottant en option de précision ; pas de croix directionnelle fixe | le schéma par défaut de Stardew sur mobile, le stick flottant des HIG ; aucune des deux sources n’en fait une règle |
Sources du tableau : les décomptes de la marche, de l’animation et des portes d’Émeraude ;1 ses minuteurs distincts pour le dessin et le pas ;92 sa caméra ;123 son fondu et ses secousses ;529 Rouge et Cristal ;433 Keren et Celeste ;2336 les recommandations d’Apple sur la cadence d’images, RealityKit et l’haptique ;7846 les références sur les commandes.404244
Deux de ces règles méritent chacune une phrase. La règle du cycle de marche est la plus facile à mal appliquer dans un moteur moderne, parce qu’un système d’animation compte ses propres secondes tandis qu’une marche compte la distance parcourue dans le monde. Les consoles portables maintenaient les deux ensemble en les comptant dans les mêmes images, avec des longueurs choisies pour concorder ; sur un téléphone, où la durée d’image varie, je propose de lire le dessin à partir de la distance parcourue, ce qui donne le même résultat sans seconde horloge à garder synchronisée. Quant à la règle de la fréquence d’images, ce n’est pas un manque d’ambition. Un monde qui avance d’un pixel entier par tick n’a rien à montrer entre deux ticks ; un écran plus rapide ne peut donc que répéter la même image, et la section 6 montre que c’est exactement ce qu’il fait.
6. La manière d’Apple : cadence d’images, horloge de RealityKit et haptique
Le premier article de cette série présentait le moteur sur lequel tourne le monde de Kiradex : une scène RealityKit utilisée comme moteur de rendu 2D, une caméra orthographique, le sol sous la forme d’un unique maillage de quads texturés, les personnages sous forme de quads, et la position de chaque sprite arrondie à des unités entières du monde à chaque image.22 Cette section couvre la partie de la documentation d’Apple qui détermine comment ce monde se déplace dans le temps. Chaque page ci-dessous a été lue telle qu’Apple la publiait le 4 octobre 2026, et la disponibilité indiquée est celle que mentionne chaque page.
La cadence d’images sur ProMotion
CADisplayLink.preferredFrameRateRange (iOS 15.0, iPadOS 15.0, Mac Catalyst 15.0, visionOS 1.0) est une demande, pas un réglage.15 Le conseil de la page est « Choose a frame rate range that your app can consistently maintain, » (choisissez une plage de fréquences d’images que votre app peut tenir de façon constante), et elle décrit ce que le système fait de la demande : « The system typically provides a consistent frame rate by choosing one that’s a factor of the display’s maximum refresh rate. » (Le système fournit généralement une fréquence régulière en choisissant un diviseur de la fréquence de rafraîchissement maximale de l’écran.) Par défaut, la plage est égale au maximum de l’écran.15 La plage elle-même est un CAFrameRateRange (iOS 15.0) composé d’un minimum, d’un maximum et d’une fréquence préférée.47
L’article d’Apple sur ProMotion donne les chiffres. Les écrans ProMotion basculent entre 24 et 120 hertz sur l’iPad Pro et entre 10 et 120 sur les iPhone compatibles, et les fréquences de l’iPhone forment douze paliers : 120, 80, 60, 48, 40, 30, 24, 20, 16, 15, 12 et 10 hertz ; celles de l’iPad Pro en sont cinq.7 La liste des appareils mentionne désormais l’iPhone Air et « iPhone 17 and later » (iPhone 17 et modèles ultérieurs) à côté de « iPhone 13 Pro and later. » (iPhone 13 Pro et modèles ultérieurs).7 Sur iPhone, rien au-delà de 60 ne se produit à moins que l’Info.plist de l’app ne règle CADisableMinimumFrameDurationOnPhone (iOS 15.0) sur true : « If you don’t enable this support, Core Animation won’t access higher frame rates (above 60Hz). » (Sans cette prise en charge, Core Animation n’accède pas aux fréquences supérieures à 60 Hz.)716 Deux phrases de l’article comptent davantage pour un jeu que le reste. La première : « In iOS 15 and later, the system provides games with special priority to 30Hz and 60Hz refresh rates to ensure optimal performance, » (à partir d’iOS 15, le système accorde aux jeux une priorité spéciale pour les fréquences de 30 et 60 Hz afin de garantir des performances optimales), obtenue avec CAFrameRateRange(minimum: 30, maximum: 60, preferred: 60).7 La seconde : « Prepare your app to operate at any refresh rate, not just those it requests. » (Préparez votre app à fonctionner à n’importe quelle fréquence de rafraîchissement, pas seulement à celles qu’elle demande.)7 Et pour tout ce qui s’anime : « Always use targetTimestamp to drive any animation, physics, or other time-related content provided in your CADisplayLink callback » (utilisez toujours targetTimestamp pour piloter l’animation, la physique ou tout autre contenu lié au temps dans votre callback CADisplayLink) (targetTimestamp date d’iOS 10.0).748
La session de la WWDC21 « Optimize for variable refresh rate displays » couvre à la fois ProMotion sur iPad Pro et les écrans Adaptive-Sync sur Mac.49 Pour les écrans Adaptive-Sync du Mac, elle modifie la recommandation antérieure d’Apple : sur un écran à fréquence fixe, « we’ve previously recommended that you slow down your rendering to hit the next factor of the display’s fastest refresh rate » (nous recommandions jusqu’ici de ralentir le rendu pour atteindre le diviseur suivant de la fréquence maximale de l’écran) ; sur Adaptive-Sync, « You should instead attempt to present frames at the highest rate your app can do so evenly. » (Essayez plutôt de présenter les images à la fréquence la plus élevée que votre app peut tenir de manière régulière.)49 Le mot que j’en retiens pour un téléphone est evenly, régulièrement.
L’horloge de RealityKit
Le monde de Kiradex ne possède pas de display link. Il avance sur l’événement par image de RealityKit, SceneEvents.Update (iOS 13.0), « An event invoked once per frame interval, » (un événement déclenché une fois par intervalle d’image), dont le deltaTime est « The elapsed time since the last update. » (le temps écoulé depuis la dernière mise à jour.)5051 RealityView (iOS 18.0) documente précisément cette voie pour le travail par image, « you can use a System or directly subscribe to the engine’s SceneEvents.Update, » (vous pouvez utiliser un System ou vous abonner directement à SceneEvents.Update du moteur), et ne propose aucun réglage de fréquence d’images qui lui soit propre.52 L’article d’Apple sur les performances de RealityKit indique la fréquence à attendre : « RealityKit typically limits the refresh rate, » (RealityKit limite généralement la fréquence de rafraîchissement), qu’il définit comme la fréquence à laquelle le framework effectue le rendu des mises à jour pour l’écran, « to 60 frames per second (fps). » (à 60 images par seconde.)8 Je n’ai pas mesuré si RealityView effectue réellement son rendu à 60 ou à 120 sur un iPhone 18 Pro Max ou un iPhone Duo, et la section 7 fait de cette mesure la première vérification du cahier des charges.
Pourquoi 120 hertz n’apporte rien à une marche en pixels entiers
Voici le calcul, tiré du même script de modélisation que celui utilisé à la section 7 pour le code actuel de l’app, appliqué cette fois à la proposition : un monde qui avance sur un tick fixe à 60 hertz, un pixel par tick, l’écran affichant ce que le dernier tick a produit. Sur un écran à 60 hertz, chaque pixel du monde est affiché pendant exactement un rafraîchissement.6 Sur un écran à 120 hertz, en une seconde, 59 des 61 positions sont tenues pendant exactement deux rafraîchissements et deux pendant un seul.6 Sur un écran à 80 hertz, les durées alternent entre un et deux rafraîchissements, 42 positions tenues une fois et 19 deux fois.6 À mon sens, le cas à 120 hertz est le cas à 60 hertz dessiné deux fois, et le cas à 80 hertz est une cadence irrégulière : un pixel qui reste tantôt 12,5 millisecondes, tantôt 25.
Pour un monde en grille sur iPhone, la question n’est donc pas 120 hertz mais une cadence régulière à 60 : un tick de simulation fixe à 60 hertz, des déplacements entiers par tick, des animations comptées en ticks, et un moteur de rendu qui affiche le dernier tick. C’est aussi ce qui rend le mouvement indépendant de la fréquence que choisit RealityKit. La session de la WWDC21 fait une remarque voisine sur les écarts de temps. Lorsqu’une image lente fait sauter un callback au display link, l’écart dont il faut avancer est « not 8ms, but rather 16ms » (non pas 8 ms, mais 16 ms) ; la session ajoute qu’une app qui « uses time delta to advance the state of your custom drawing » (utilise l’écart de temps pour faire avancer l’état de son dessin personnalisé) va « slow down your custom drawing by one frame » (ralentir son dessin personnalisé d’une image) chaque fois qu’un callback est sauté, ce que je comprends comme une app qui avance des 8 millisecondes attendues plutôt que du temps réellement écoulé, et elle dit qu’on « can instead keep track of a previous targetTimestamp so that you can advance the state correctly. » (peut plutôt garder une trace du targetTimestamp précédent afin de faire avancer l’état correctement.)49
Haptique
Core Haptics (iOS 13.0, iPadOS 13.0, Mac Catalyst 13.0, visionOS 1.0) construit un CHHapticPattern, « An object representing a haptic waveform, » (un objet représentant une forme d’onde haptique), à partir de dictionnaires, de tableaux d’objets CHHapticEvent ou d’un fichier AHAP.53 Les événements sont de deux types haptiques, hapticTransient et hapticContinuous ; les transitoires sont des « brief impulses that occur at a specific point in time. » (impulsions brèves qui se produisent à un instant précis.)54 Chacun accepte les paramètres hapticIntensity, hapticSharpness, attackTime, decayTime, releaseTime et sustained.55 Un CHHapticEngine les joue, et capabilitiesForHardware() indique si l’appareil en est capable.56
La voie la plus simple est UIImpactFeedbackGenerator (iOS 10.0), « A concrete feedback generator subclass that creates haptics to simulate physical impacts, » (une sous-classe concrète de générateur de retour qui crée des retours haptiques simulant des chocs physiques), dont les styles décrivent « The mass of the objects in the collision » (la masse des objets lors de la collision) (light, medium, heavy, soft, rigid) ; impactOccurred(intensity:) date d’iOS 13.0, et la page range init(style:view:) sous « Initializing the feedback generator » et init(style:) sous Deprecated.575859 prepare() ne réduit la latence que s’il a le temps d’agir : « Calling prepare() and then immediately triggering feedback (without any time in between) does not improve latency, » (appeler prepare() puis déclencher immédiatement le retour, sans délai entre les deux, n’améliore pas la latence), et le moteur repasse au repos après que « A short period of time passes (typically seconds). » (une courte période s’est écoulée, généralement quelques secondes.)60 En SwiftUI, sensoryFeedback(_:trigger:) (iOS 17.0) « Plays the specified feedback when the provided trigger value changes, » (joue le retour indiqué lorsque la valeur de déclenchement fournie change), y compris .impact(weight:intensity:).61
La page des HIG sur la lecture des retours haptiques en constitue la moitié conceptuelle. « Avoid overusing haptics, » (évitez d’abuser des retours haptiques)46 avec la raison : « Often, the best haptic experience is one that people may not be conscious of, but miss when it’s turned off. » (Souvent, la meilleure expérience haptique est celle dont on n’a pas conscience, mais qui manque quand on la désactive.) « Make haptics optional. » (Rendez les retours haptiques facultatifs.) Faites correspondre « the intensity and sharpness of a haptic with the intensity and sharpness of the animation it accompanies. » (l’intensité et la netteté d’un retour haptique à celles de l’animation qu’il accompagne.)46 La netteté peut traduire une expérience « that’s soft, rounded, or organic, or one that’s crisp, precise, or mechanical. » (douce, arrondie ou organique, ou au contraire vive, précise ou mécanique.)46 Et des retours haptiques personnalisés peuvent rendre « a collision or a hit » (une collision ou un coup) très différents « from subtle experiences like the approach of footsteps or a looming danger. » (d’expériences subtiles comme des pas qui approchent ou un danger imminent.)46 Une marche à 3,75 pas par seconde pendant des minutes est exactement le cas que vise la première règle.1
Commandes
Les API de commandes tactiles, dans les termes de la section 4. Touch Controller (iOS 26.0, iPadOS 26.0, Mac Catalyst 26.0, visionOS 26.0) est résumé sur sa page par « Integrate onscreen touch controls into your Metal-based games » (intégrez des commandes tactiles à l’écran dans vos jeux basés sur Metal) : boutons, croix directionnelles, sticks, manettes de gaz et pavés tactiles, exposés via un GCController.62 Son TCDirectionPad (iOS 26.0, iPadOS 26.0, Mac Catalyst 26.0) peut être configuré « to behave as either a composite direction pad » (pour se comporter comme une croix directionnelle composite) ou « as four separate buttons. » (comme quatre boutons distincts.)6263 Le plus ancien GCVirtualController (iOS 15.0) est « A software emulation of a real controller that you configure specifically for your game. » (une émulation logicielle d’une vraie manette, que vous configurez spécifiquement pour votre jeu.)64 Une adresse que j’ai essayée, developer.apple.com/documentation/touchcontrols, a renvoyé une erreur 404 ; la page du framework est touchcontroller.39
Ce que cela coûte
Rien dans cette section n’a été chronométré sur un téléphone. La durée d’image du monde sur l’appareil, le coût d’un accumulateur à 60 hertz et le coût d’un moteur haptique maintenu en marche tant que le monde est à l’écran ne sont pas mesurés ; les vérifications du cahier des charges, à la section 7, sont rédigées pour mesurer les deux premiers.
7. Le cahier des charges : ce que Kiradex construit et les vérifications à passer
Où en est la marche aujourd’hui
J’ai lu le code de la marche, de la caméra et des transitions du monde dans le dépôt de Kiradex le 4 octobre 2026, sans le modifier, et j’en ai écrit un modèle image par image. Tout ce qui figure dans cette sous-section est soit lu dans ce code, soit calculé par le modèle, et il est précisé lequel des deux. Le modèle effectue chacune des opérations Float du code sur 32 bits, dans l’ordre du code, avec l’arrondi de Swift, à exactement 1/60 ou 1/120 de seconde par image, et laisse de côté les bords de la carte, la charnière de l’iPhone Duo et le décalage des pieds, dont aucun ne modifie une marche en ligne droite au milieu de la carte. C’est un modèle du code. Ce n’est pas une capture, et aucun téléphone n’a été mesuré.176
Ce que fait le code, d’après sa lecture :
- La commande, c’est toucher pour marcher, et rien d’autre. La scène convertit un toucher en point du monde, puis touche un collectionneur ou le kiosque, ou appelle
walk(to:). Pas de glisser, pas de croix directionnelle, et aucun appel haptique nulle part dans la cible de l’app : une recherche deUIImpactFeedbackGenerator,sensoryFeedbacketCHHapticne trouve rien.17 - Le chemin est une recherche du plus court chemin dans huit directions, ordonnée selon le coût parcouru jusque-là, sans estimation de la distance restante, ce qui en fait une recherche de Dijkstra plutôt qu’un A* ; les diagonales coûtent √2 et ne coupent jamais l’angle d’un mur. Un pas en diagonale est orienté de côté et divise sa progression par √2 après application du facteur de course de 1,6, si bien que, dans le modèle, un pas en diagonale prend 22 images en marchant et 14 en courant à 60 hertz, contre 15 et 10 pour un pas en ligne droite.176
- La vitesse, c’est du temps.
tilesPerSecondvaut 4, soit 64 pixels par seconde, et la marche devient une course à 1,6 fois cette vitesse quand le chemin compte 6 pas ou plus ; une marche dans l’app fait donc de 1 à 5 tuiles, et tout ce qui est plus long se fait en courant. Chaque image ajoutedt × speed / lengthà la progression d’un pas, et quand la progression atteint 1, le personnage saute à la tuile suivante et la progression revient à 0, l’excédent étant jeté.17 - Le dessin de marche est choisi selon le temps. La colonne est
cycle[Int(walker.clock * framesPerSecond) % count], avecframesPerSecondà 8 et les cycles de marche et de course de six dessins de la forge. L’article de cette série sur les personnages indiquait la même cadence, « the walk at eight frames a second » (la marche à huit images par seconde), ce qui décrit fidèlement le code.1735 - La caméra lisse, puis arrondit. À chaque image, elle se déplace de
(target - current) * min(1, dt * 6)vers le joueur puis arrondit les deux axes à des unités entières ; elle est bornée à la carte quand la carte est plus grande que la vue. La phrase du premier article selon laquelle chaque sprite et la caméra « are rounded to whole world units each frame, after easing » (sont arrondis à des unités entières du monde à chaque image, après lissage) est elle aussi exacte.1722 - Portes et téléportations. La porte affiche immédiatement le dessin entrouvert de la planche et le dessin ouvert au bout de 70 millisecondes, puis retire la porte 1,4 seconde plus tard ; le pas sur la téléportation attend 220 millisecondes puis change de lieu. L’article sur les structures décrivait cela comme une lacune connue.1713
- Les transitions sont des push de navigation avec leur animation par défaut ; un changement d’étage remplace la scène par identité, sans fondu ; quitter appelle
dismiss(). Le seul voile de l’app est la superposition noire de la visionneuse de cartes, animée avec.easeOut(duration: 0.25).17 - Aucune demande de fréquence d’images. Pas de
CADisplayLink, depreferredFrameRateRangeni deCADisableMinimumFrameDurationOnPhonedans l’app ou son fichier de projet ; le monde avance surSceneEvents.Updateavec ledeltaTimede l’événement.17
Ce que ce code produit à l’écran selon le modèle, à durée d’image constante :
- Un saut de deux pixels une fois par tuile à 60 hertz. À 60 hertz, une marche prend 15 images par tuile, 4,00 tuiles par seconde, et les 16 pixels de chaque tuile se répartissent en 14 images de 1 pixel et une image de 2 : un saut de 2 pixels par tuile, cinq sur une marche de cinq tuiles. Émeraude avance d’exactement 1 pixel à chaque image.61
- Des 0 et des 1 irréguliers à 120 hertz. À 120 hertz, la marche prend 30 images par tuile et les 16 pixels de chaque tuile se répartissent en 16 images de 1 pixel et 14 sans déplacement, entrelacées de façon irrégulière.6
- La course est plus lente que sa constante. À 60 hertz, la course prend 10 images par tuile, 6,00 tuiles par seconde, alors que 4 fois 1,6 donnerait 6,4, parce que l’excédent de chaque tuile est jeté ; les 16 pixels de chaque tuile se répartissent en 4 images de 1 pixel et 6 de 2. À 120 hertz, elle prend 19 images par tuile, 6,32 tuiles par seconde ; la vitesse de course dépend donc de la fréquence d’images.6
- Une caméra qui traîne et n’arrive jamais. En marchant à 60 hertz, la caméra prend environ un pixel de retard de plus à chaque tuile, de 5 pixels à la fin de la première tuile à 9 à la fin de la cinquième, la plus longue marche que fasse l’app, et la position du joueur à l’écran change sur 8 des 73 images de la marche après la première. En course, c’est-à-dire sur tout chemin de 6 tuiles ou plus, elle traîne de 13 à partir de la deuxième tuile. À 120 hertz, elle atteint 9 dès la première tuile, en marchant comme en courant, puis s’y maintient. Quand le joueur s’arrête, la caméra s’immobilise à 4 pixels de lui à 60 hertz et à 9 à 120, et y reste : dès que l’écart multiplié par
min(1, dt × 6)est inférieur à un demi-pixel, l’arrondi renvoie la même position à chaque image. Le côté où elle s’immobilise dépend de la direction de la dernière marche. Dans Émeraude, la traîne et le décalage au repos sont tous deux nuls.6 - Les dessins suivent leur propre horloge. Le cycle de six dessins à 8 par seconde dure 0,75 seconde. Mesuré à partir des positions du modèle entre les débuts de cycles successifs à 60 hertz, il couvre 3,00 tuiles en marchant et 4,50 en courant (4,80 à la vitesse nominale de course de 6,4 tuiles par seconde, que l’excédent jeté ne lui permet jamais d’atteindre) ; le cycle de deux appuis d’Émeraude couvre 2,00 tuiles. À mon sens, un cycle qui couvre moitié plus de terrain que ses appuis se voit comme des pieds qui glissent, mais le modèle ne mesure pas où un pied se pose au sol. Le dessin se trouve aussi sur le fil du rasoir toutes les 15 images, à 60 comme à 120 hertz, quand l’horloge multipliée par 8 tombe exactement sur un nombre entier, la frontière entre deux dessins : conserver l’horloge sur 32 bits, comme le fait l’app, plutôt que sur 64, change le dessin affiché sur 9 des 11 images de ce type en 12 tuiles de marche à 60 hertz, et sur 14 des 23 à 120.6
- Un second toucher en plein pas fait reculer le personnage. Ce point est lu dans le code, ni modélisé ni capturé :
walk(to:)remet la progression à 0 alors que la tuile du personnage est encore l’origine du pas, si bien que l’image suivante dessine le personnage jusqu’à 15 pixels en arrière de l’endroit où il se trouvait ; les déplacements d’autres collectionneurs venant du serveur font de même.17
Pixels par image affichée : le canon est régulier, le code d’aujourd’hui (un modèle, pas une capture) ne l’est pas, et un tick fixe reste régulier à 120 aussi.621
La caméra lissée et arrondie, selon le modèle : elle traîne pendant la marche ou la course et s’arrête trop court à la fin de la marche.621
Rien de tout cela ne préjuge de la sensation en main, puisqu’aucun téléphone n’a été mesuré. Les durées d’image réelles fluctuent, ce qui modifiera la répartition exacte des 1 et des 2. La traîne et le décalage au repos dépendent eux aussi de la durée d’image, parce que le pas de lissage min(1, dt × 6) en dépend, d’où les 4 pixels au repos à 60 hertz et les 9 à 120 dans le modèle. À mon sens, une fluctuation dans la plage qu’un téléphone présente normalement en modifiera l’ampleur sans les supprimer ; la condition est que les images restent plus courtes que 1/6 de seconde, environ 167 millisecondes, car à cette durée le facteur de lissage atteint 1 et la caméra rejoint le joueur en une image, si bien qu’un à-coup assez long comble l’écart sur cette image.176 Le décalage entre les dessins et la marche ne dépend pas de la fréquence d’images : une horloge de dessin à 8 par seconde face à une marche de 4 tuiles par seconde donne 3,00 tuiles par cycle à 60 hertz et à peu près autant à 120. Celui de la course en dépend, 4,50 tuiles par cycle à 60 hertz et 4,75 à 120, parce que l’excédent qu’elle jette à chaque tuile en dépend.6
Le cahier des charges
Chaque point comprend une modification de Kiradex/World/, sa justification, et une vérification qu’une ligne de journal, un script ou une capture peut confirmer. Aucun élément graphique, aucun nom ni aucun son tiré des jeux n’est proposé : les chiffres relèvent de la mécanique, tandis que les graphismes, les sons et les mots appartiennent à Kiradex.
1. Marcher au tick, pas à l’horloge.
Modification. Ajouter un accumulateur à 60 hertz à la mise à jour du rig. Chaque callback ajoute son dt ; si l’accumulateur contient alors plus de 8 ticks de temps (133 millisecondes), l’excédent est jeté et inscrit dans le journal de mouvement sous la forme d’une ligne dropped avec ses millisecondes ; ensuite, chaque tick entier présent dans l’accumulateur s’exécute, chacun soustrayant un tick. L’accumulateur compte le temps en unités entières (nanosecondes, ou le 1/60 000 000 de seconde du modèle), jamais en secondes à virgule flottante : avec un accumulateur Double ou Float, le 600e tick d’un test de dix secondes tombe, à certaines fréquences, à une erreur d’arrondi de sa limite, et le décompte donne 599. C’est la règle du retard accumulé : jusqu’à 8 ticks de retard sont rattrapés au callback suivant, et tout ce qui dépasse est traité comme une suspension, si bien que le monde reprend là où il s’était arrêté au lieu de courir pour rattraper son retard. Le chiffre de huit est choisi pour couvrir toutes les fréquences de la liste d’Apple pour les iPhone ProMotion, jusqu’à 10 hertz, qui exige 6 ticks par callback.7 Dans le modèle, dix secondes de callbacks à chacune des douze fréquences exécutent exactement 600 ticks sans rien jeter, alors qu’un plafond de 4 ticks par callback en exécuterait 480 à 12 hertz et 400 à 10.6 Après un à-coup d’une seconde à 60 hertz, la règle exécute 8 ticks au callback suivant puis 1 à chacun des suivants, et inscrit 866,7 millisecondes jetées ; le même à-coup avec le retard conservé sous un plafond de 4 exécute 4 ticks sur 19 callbacks d’affilée, l’avance rapide que la règle existe pour empêcher.6 Chaque personnage conserve un nombre de ticks de pas au lieu d’une progression fractionnaire, et son décalage dessiné est ce nombre multiplié par ses pixels par tick : des entiers exacts, si bien que les personnages n’ont plus besoin d’arrondi. La marche, c’est 1 pixel par tick, 16 ticks par tuile ; la course, 2 pixels par tick, 8 ticks, en conservant la règle actuelle selon laquelle un chemin de 6 pas ou plus se fait en courant. Les pas en diagonale, que la recherche de chemin autorise, conservent le √2 que le code prévoit déjà et son ordre, c’est-à-dire l’allure de course appliquée avant la division par √2 : une diagonale en marchant prend 23 ticks (16√2 vaut environ 22,6, arrondi à l’entier supérieur) et une diagonale en courant 12 (8√2 vaut environ 11,3, arrondi à l’entier supérieur), chacune déplaçant 16 pixels sur chaque axe sur les ticks où floor(16 × t / 23) ou floor(16 × t / 12) change.17 En marchant, cela fait 1 pixel sur 16 des 23 ticks et aucun sur les 7 autres ; en courant, 1 pixel sur 8 des 12 ticks et 2 sur 4, la seule allure irrégulière du cahier des charges, comme le Vélo Cross est la seule allure irrégulière d’Émeraude.1 Il n’y a aucun excédent à jeter.
Pourquoi. Les règles de marche et de course de la section 5 ; et, dans le modèle, le saut de 2 pixels par tuile à 60 hertz et les 0 et 1 irréguliers à 120.61
Vérifications. Un argument de lancement -motionLog consigne le tick et les x, y du joueur à chaque tick, ainsi que chaque ligne dropped. Sur la démo d’orientation existante, qui fait une tuile dans chaque direction, un script vérifie que chaque tick de marche avance d’exactement 1 pixel, que chaque tuile prend 16 ticks et qu’aucun tick n’avance de 2. Une démo de diagonales, un pas en diagonale en marchant et un en courant dans chaque direction, vérifie 23 ticks et 12, 16 pixels sur chaque axe par pas, des déplacements par axe de 0 ou 1 pixel en marchant et de 1 ou 2 en courant, et une diagonale en courant plus rapide qu’une diagonale en marchant. Des tests unitaires fournissent à l’accumulateur des séquences de dt fictives en unités entières exactes : dix secondes à chacune des douze fréquences de l’iPhone, de 120 à 10 hertz, exécutent 600 ticks sans rien jeter, et un trou d’une seconde à 60 hertz exécute 8 ticks au callback suivant, puis 1 par callback, avec 866,7 millisecondes consignées comme jetées. Le même journal de mouvement sur un iPhone 18 Pro Max et sur l’écran intérieur d’un iPhone Duo donne le même nombre de ticks par tuile : la fréquence de l’écran ne doit pas modifier la marche.
2. Choisir le dessin de marche selon la distance.
Modification. Ce point est ma proposition, pas une copie : Émeraude cadence ses dessins en images, en accord avec ses pas, et ne les lit pas à partir de la distance.92 Pendant la marche, choisir la colonne d’après la progression de la marche comptée en pas, c’est-à-dire les pas terminés depuis le début de la marche plus les ticks du pas en cours divisés par sa longueur : walkCycle[floor(steps × walkCycle.count / 2) % walkCycle.count], deux appuis pour deux pas, et de même pour la course. Sur un pas en ligne droite, cela revient à la distance parcourue sur 32 pixels, floor(distancePx × 6 / 32) mod 6. Sur une diagonale, on compte le pas plutôt que sa longueur en √2, si bien qu’un appui tombe toujours au début de chaque pas, avec une enjambée plus longue ; c’est mon choix pour des dessins conçus pour des enjambées en ligne droite. Afficher le dessin debout à la fin de la marche, comme le fait Émeraude. Laisser les horloges d’attente, de clignement et d’émotes telles quelles. Un cycle cadencé au tick pourrait lui aussi concorder avec les pas, comme celui d’Émeraude, mais six dessins sur 32 ticks exigeraient des durées inégales de 5 et 6 ticks ; compter la progression ne nécessite pas de seconde table et vaut pour toute allure que l’app ajoutera.
Pourquoi. Un appui tous les 16 pixels à toutes les vitesses, et une cadence qui ne peut pas se décaler par rapport au mouvement ; le cycle actuel couvre 3,00 tuiles en marchant et 4,50 en courant dans le modèle.16 Cela tranche aussi les « eight frames a second » (huit images par seconde) de l’article sur les personnages : à 1 pixel par tick, six dessins sur 32 pixels changent tous les 5,33 pixels, soit 11,25 dessins par seconde, pas 8.35
Vérification. À partir du journal de mouvement et de la colonne affichée : les deux dessins de contact (walk_a et walk_d dans le cycle de la forge) apparaissent à 0 et à 16 pixels, à un près, sur chaque tranche de 32, à 60 comme à 120 hertz ; sur la démo de diagonales, ils apparaissent au premier tick de chaque pas.
3. Finir le pas ; garder le toucher ; ajouter un stick flottant.
Modification. Un toucher en plein pas planifie à partir de la tuile vers laquelle on marche, pas depuis l’origine, et le pas en cours se termine d’abord ; le compteur de pas n’est jamais remis à zéro. Les déplacements serveur des autres collectionneurs suivent la même règle. À l’arrêt, un toucher dont le premier pas change l’orientation fait pivoter pendant 8 ticks avant le déplacement. Un toucher sur une cellule bloquée voisine du joueur, ou sur la tuile située devant lui, le fait se tourner vers elle et joue un heurt de 32 ticks avec le retour haptique de refus du point 6. Garder un doigt appuyé le fait suivre, comme dans le schéma par défaut de Stardew, mais en passant par la recherche de chemin existante chaque fois que le doigt franchit une tuile, puisque le suivi de Stardew ne contourne pas les obstacles et que le nôtre le peut. Un stick flottant, à quatre directions, désactivé par défaut, apparaît là où se pose le pouce, construit sur un geste de glisser SwiftUI au-dessus du monde ; tout élément dessiné mesure au moins 44 sur 44 points. Le stick de Touch Controller (iOS 26) ne remplace le glisser que si une build de test montre qu’il se dessine par-dessus la RealityView : Apple présente le framework comme destiné aux « Metal-based games » (jeux basés sur Metal), sa session de la WWDC25 dit qu’il « integrates directly with Metal, » (s’intègre directement à Metal), et le monde de Kiradex est dessiné par RealityView sans passe de rendu propre.4562
Pourquoi. Les règles du pas, de la rotation, du pas refusé et des commandes de la section 5.14044624517
Vérifications. Un test d’interface touche une tuile 6 cases à l’est, puis 120 millisecondes plus tard une tuile 6 cases au nord ; le journal de mouvement ne montre aucun tick où le x du joueur diminue, et la tuile du premier pas est atteinte avant le changement de direction. Un test d’interface touche le mur à côté de la maison ; le journal montre un changement d’orientation et un heurt de 32 ticks, sans changement de position. Avec le stick activé, un glisser maintenu vers la droite pendant 60 ticks à partir du premier tick de mouvement termine trois tuiles au tick 48, en commence une quatrième alors que le stick est encore maintenu, et la termine au tick 64 après le relâchement : le journal montre le joueur 4 tuiles à l’est, sur une tuile entière, le pas en cours au moment du relâchement étant terminé, sans arrêt entre les tuiles.
4. Verrouiller la caméra.
Modification. Remplacer le lissage par une caméra placée au même tick que le déplacement du joueur, uniquement à partir de pixels entiers du monde : camera = clamp(player + foldOffset, low, high). Chaque terme reste entier, car aujourd’hui seul l’arrondi final rend la caméra entière : les bornes et le décalage de charnière sont fractionnaires. La position du joueur est entière grâce au point 1. Le décalage de charnière, que follow calcule en points multipliés par displayScale / pixelScale, est arrondi une fois à des pixels entiers du monde, au moment où la charnière change. Les bornes sont arrondies vers l’intérieur, la basse vers le haut et la haute vers le bas, car la demi-taille de la vue en pixels du monde n’est pas forcément entière : fit divise les pixels d’écran de la vue par un nombre entier de pixels d’écran par texel, si bien qu’une vue d’exemple de 393 sur 852 points en 3x obtient 7, une vue de 168,43 pixels du monde de large et une demi-largeur de 84,21, et les bornes de la caméra deviennent 85 et la largeur de la carte moins 85, de sorte qu’aucune image ne montre ce qui se trouve au-delà du bord de la carte.176 Quand la carte est plus petite que la vue, les bornes s’inversent, comme le permet la borne actuelle, et le même arrondi vers l’intérieur garde toute la carte dans la vue. Conserver la limitation à la carte ; la règle de la section 5 autorise à borner ou à dessiner l’extérieur, et la caméra de Kiradex ne dépasse le bord de la carte vers les bois que lorsque le Duo est à moitié ouvert, d’une marge de tuiles entières.17 Traiter le décalage de charnière comme fixe jusqu’à ce que la charnière change ; quand il passe de a à b, le faire progresser par a + round((b − a) × i / 8) pour i de 0 à 8, un niveau tous les 2 ticks, la cadence du fondu, si bien que chaque position intermédiaire est un pixel entier ; un passage de 0 à moins 37, par exemple, passe par 0, −5, −9, −14, −19, −23, −28, −32 et −37.6 Conserver la parallaxe des bois, calculée à partir de la caméra verrouillée et arrondie comme aujourd’hui. Pas d’anticipation vers la destination touchée : Game Freak en a construit une et l’a laissée désactivée, et la destination d’un toucher pour marcher est déjà à l’écran.12
Pourquoi. La règle de caméra de la section 5, et la traîne du modèle, qui augmente au fil d’une marche jusqu’à 9 pixels à la cinquième tuile et atteint 13 en course, ses changements de la position du joueur à l’écran, et ses décalages au repos de 4 et 9 pixels.6
Vérifications. D’après le journal de mouvement, la caméra moins le joueur est constante à chaque tick d’une marche sur la place dégagée (zéro, ou le décalage de charnière) et reste la même après l’arrêt du joueur, et chaque valeur de caméra est un nombre entier. Un test unitaire appelle fit avec la vue de 393 sur 852 points en 3x, fait marcher le joueur jusqu’aux deux bords d’une carte et vérifie des positions de caméra entières qui s’arrêtent à 85 et à la largeur de la carte moins 85 ; le même test change ensuite la charnière et vérifie que la caméra atteint le nouveau décalage en passant par 9 positions en pixels entiers, espacées de 2 ticks, et qu’aucun tick intermédiaire ne sort des bornes. Pour une capture, les dessins de marche ne conviennent pas comme repère, car la forme des pieds change d’un dessin à l’autre ; une build de débogage dessine un repère d’un texel, dans une couleur utilisée nulle part ailleurs, à la position de l’entité du joueur, et un script le retrouve dans chaque image d’une marche de 6 tuiles sur la place dégagée : le même pixel d’écran sur chaque image. Près du bord de la carte, c’est le repère qui bouge et non la caméra, uniquement par pixels entiers.
5. La porte, le pas et le fondu, dans les graphismes de Kiradex.
Modification. Entrer par une porte, c’est-à-dire par une téléportation dotée d’une planche de porte. Aujourd’hui, la porte s’ouvre une fois que le personnage est arrivé sur la cellule de la porte : le onStep du pas trouve la téléportation sur cette tuile et y appelle openDoor.17 Émeraude ne laisse jamais le joueur se tenir dans une porte fermée : TryDoorWarp ne se déclenche que lorsque le joueur, sur la cellule du dessous, pousse vers le nord contre une porte, et Task_DoDoorWarp ouvre la porte une cellule plus haut puis force le pas jusqu’à elle.6525 Le cahier des charges recule le déclencheur d’une cellule pour faire de même :
- Quand le pas suivant du chemin mène sur une téléportation de porte, la marche s’arrête sur la cellule précédente, et l’entrée commence là, au tick 0. Geler les commandes et jouer le son de la porte, un son de Kiradex.
- Ouvrir la porte en quatre créneaux de 5 ticks, les quatre dessins d’Émeraude à cinq images chacun (83 millisecondes par créneau à 60 ticks par seconde, là où les cinq images d’Émeraude font 84 ; 20 ticks en tout), à la place de l’actuel dessin entrouvert immédiat et du dessin ouvert à 70 millisecondes : fermée, entrouverte, ouverte, ouverte. La planche de porte de Kiradex compte trois dessins, fermée, entrouverte et ouverte (le
door_sheet()de la forge en dessine trois, et l’app la charge avecSpriteSheet.bundled(name, columns: 3, rows: 1)), là où la porte d’Émeraude compte la porte fermée plus trois, si bien que le dessin ouvert se prolonge sur le quatrième créneau. Un quatrième dessin issu de la forge, entre entrouverte et ouverte, remplirait ce créneau à la place ; c’est un graphisme facultatif, pas une exigence. Corriger le commentaire qui parle de « four ticks » (quatre ticks).2417 - Faire faire au joueur un pas forcé de cette cellule jusqu’à la cellule de la porte, 16 ticks. Les portes de la ville se trouvent dans la rangée inférieure d’un bâtiment, avec des cellules du bâtiment de part et d’autre et au-dessus, et le chemin ne coupe jamais l’angle d’un mur ; ce pas se fait donc toujours vers le haut depuis la cellule du dessous, comme celui d’Émeraude.17
- Masquer le personnage et refermer la porte, les créneaux à l’envers (ouverte, ouverte, entrouverte, fermée), 20 ticks.
- Faire apparaître sur le monde un voile noir en 9 niveaux d’opacité étagés (0, 2/16 et ainsi de suite jusqu’à 16/16), le niveau
iau tick2idu fondu, si bien que le dernier niveau tombe au tick 16 et se maintient jusqu’au tick 17 : 18 ticks. C’est une adaptation, choisie, et non le minutage d’Émeraude. Les palettes de sprites d’Émeraude atteignent chaque niveau sur les mêmes images paires que ce voile, ses palettes d’arrière-plan une image plus tôt, et après le dernier mélange à l’image 16, il exécute cinq mises à jour de finition avant de devenir inactif à l’image 21 ; un voile unique n’a pas de seconde couche en retard ni rien à finir.5 Ne jamais animer l’opacité de la vue qui contient la RealityView elle-même ; la visionneuse de cartes a appris cette leçon au premier article.22 - Pousser la destination avec les animations désactivées, et faire disparaître le voile selon les mêmes 9 niveaux.
- Arriver sur le paillasson intérieur, orienté vers le haut. La sortie inverse le tout : arrivée sur la cellule de la porte avec la porte ouverte, un pas forcé vers le bas de 16 ticks, la porte se referme, puis les commandes.
Les étages, qui aujourd’hui remplacent la scène sans fondu, reçoivent le même voile, sans porte ni pas forcé. Quitter une pièce, qui aujourd’hui appelle dismiss(), reçoit le voile et le pas forcé vers l’extérieur.
Pourquoi. Les règles de la porte, du fondu et du cérémonial de la section 5 ; aujourd’hui, le lieu change 220 millisecondes après le pas, la porte affiche deux dessins à 70 millisecondes d’intervalle, et l’écran glisse sur le côté.1517
Vérifications. Le journal de mouvement compte les ticks à partir de celui où la marche s’arrête sous la porte. Il montre le joueur encore sur cette cellule, et les créneaux de la porte aux ticks 0, 5, 10 et 15 (fermée, entrouverte, ouverte, ouverte) ; le pas forcé aux ticks 20 à 35, 1 pixel par tick, atteignant la cellule de la porte au tick 35 et jamais avant ; les créneaux de fermeture aux ticks 36, 41, 46 et 51 ; le premier niveau du voile au tick 56 ; et le push pas avant le tick 74. Une marche qui se termine sur une cellule de porte sans cette séquence échoue à la vérification. Le journal de mouvement consigne aussi l’opacité du voile à chaque tick : 9 valeurs distinctes, chacune tenue 2 ticks, aux ticks 56 à 73 à l’aller, et les mêmes 9 au retour. Un enregistrement d’écran le confirme sur une partie de l’image qui reste immobile : l’image entière ne convient pas, car l’eau du sol, sur une carte qui en comporte, change de dessin 4 fois par seconde et d’autres collectionneurs peuvent se déplacer ; un script échantillonne donc un pan de mur de bâtiment choisi à l’avance, sans tuile animée ni personnage qui passe dessus dans l’enregistrement, et compte 9 niveaux distincts à l’aller et 9 au retour, chacun tenu 2 images, à une près à 60 hertz.17 Dans un enregistrement d’écran d’une téléportation, le monde ne se déplace jamais horizontalement : aucun glissement de navigation.
6. Haptique : trois événements, et un interrupteur.
Modification. Un seul CHHapticEngine détenu par le monde, démarré quand il apparaît et arrêté quand il disparaît, avec un interrupteur Retours haptiques dans les réglages, activé par défaut et respectant le réglage haptique du système. Les motifs, conservés sous forme de fichiers AHAP dans le bundle :
| Événement | Motif | Pourquoi |
|---|---|---|
| Pas refusé (le heurt) | un hapticTransient, intensité 0,4, netteté 0,2 |
l’impact des HIG est « a thud when two heavy objects collide » (un bruit sourd quand deux objets lourds se heurtent)46 ; doux, parce que les graphismes sont doux |
| Ouverture de porte | un hapticTransient d’intensité 0,3 et de netteté 0,6 sur chaque créneau qui change l’image pendant l’ouverture, le dessin entrouvert au tick 5 et le dessin ouvert au tick 10 (83 et 167 ms) |
s’accorder à l’animation qu’il accompagne |
| Carte levée (la carte en 3D) | un hapticTransient à 0,7 et 0,8 quand elle atteint le sommet |
le seul objet réel du monde |
| Pas | aucun par défaut | 3,75 pas par seconde pendant des minutes, c’est l’abus contre lequel mettent en garde les HIG |
Les valeurs d’intensité et de netteté sont mes points de départ, pas des mesures, et sont destinées à être ajustées sur un appareil. UIImpactFeedbackGenerator(style: .soft, view:), avec prepare() appelé au début d’une marche, sert de solution de repli là où capabilitiesForHardware() indique que Core Haptics n’est pas disponible.
Pourquoi. La règle haptique ; les recommandations d’Apple citées à la section 6.46565760
Vérification. Un argument -hapticLog consigne chaque événement. Une entrée par une porte scriptée consigne exactement 2 événements de porte, aux ticks 5 et 10 du décompte du point 5, et aucun par pas ; avec l’interrupteur désactivé, aucun du tout.
7. Fréquence d’images : 60, régulièrement.
Modification. Aucune dans l’Info.plist : ne pas ajouter CADisableMinimumFrameDurationOnPhone, car le monde ne gagne rien à 120. Conserver la RealityView ; le tick du point 1 rend le mouvement indépendant de la fréquence à laquelle elle effectue son rendu. Si un display link est un jour ajouté, régler CAFrameRateRange(minimum: 30, maximum: 60, preferred: 60) pour bénéficier de la priorité accordée par Apple aux jeux.76
Vérifications. Consigner un histogramme de SceneEvents.Update.deltaTime sur une marche de 60 secondes sur un iPhone 18 Pro Max et sur un iPhone Duo, écrans extérieur et intérieur. Le cahier des charges suppose que le mode est de 16,7 millisecondes ; s’il est de 8,3, l’accumulateur du point 1 maintient quand même la marche à 16 ticks par tuile, et le journal le prouve. Les lignes dropped du journal de mouvement : aucune lors d’une marche normale, à l’une comme à l’autre fréquence.
8. Une secousse, pour un seul moment.
Modification. Une secousse de caméra en texels entiers, 1 texel, 8 inversions, 5 ticks d’écart, réservée à un événement qui la mérite, par exemple la révélation d’une carte rare sur la place, accompagnée du retour haptique de la carte levée. Pas pour les portes, les pas ou les arrivées.
Pourquoi. La plus courante des 24 secousses d’Émeraude, et leur retenue.29
Vérification. Le journal de mouvement montre le décalage de la caméra alternant entre plus et moins 1 texel aux ticks 0, 5 et ainsi de suite jusqu’à 35, puis revenant à 0 au tick 40.
Ce qui ne figure pas dans ce cahier des charges
- Lissage ou anticipation de la caméra sur la grille. Il n’y a pas de sauts à lisser, et Game Freak a livré son anticipation désactivée.1223
- 120 hertz pour le monde. L’objectif est une cadence régulière à 60 ; 120 montre chaque pixel deux fois.6
- Un retour haptique à chaque pas par défaut.46
- Une croix directionnelle fixe à l’écran. Ma recommandation est toucher pour marcher, plus un stick flottant facultatif.44
- Les sons ou jingles de porte ou de téléportation des jeux. Les sons relèvent de l’expression, et Kiradex a besoin des siens.
- Vélos, surf, glace et courants. Les personnages de l’app marchent et courent, rien de plus, et les vitesses ci-dessus sont consignées pour le jour où cela changera.17
La question ouverte : le minutage sur un appareil
Chaque chiffre du modèle dans cette section suppose un intervalle constant de 1/60 ou 1/120 de seconde entre les images. Savoir si RealityView effectue son rendu à 60 ou à 120 sur un iPhone 18 Pro Max ou un iPhone Duo, et dans quelle mesure son deltaTime fluctue, n’a pas été mesuré ; l’histogramme du point 7 est la première chose à lancer, et le point 1 est rédigé pour que la réponse ne change rien à la marche, quelle que soit la fréquence de la liste des iPhone d’Apple.687
Points clés
Si vous dessinez les graphismes
- Dessinez la marche pour une distance. La marche d’Émeraude, c’est deux enjambées et deux positions debout sur 32 pixels, tenues pendant des images qui correspondent à ses pas, et les allures plus rapides réutilisent les dessins avec des durées plus courtes au lieu d’ajouter des images, si bien qu’un appui tombe tous les 16 pixels à toutes les vitesses.192
- Un cycle de six dessins convient, à condition que le moteur le joue sur une distance fixe ; joué à une fréquence fixe face à une marche cadencée séparément, sa cadence se décale par rapport au mouvement, 3,00 tuiles par cycle en marchant et 4,50 en courant dans le modèle de notre code.6
- La porte d’Émeraude, c’est la porte fermée plus trois dessins, chacun à l’écran pendant 84 millisecondes. Une planche de trois, fermée, entrouverte et ouverte, comme celle de Kiradex, remplit les quatre créneaux en tenant le dessin ouvert sur deux d’entre eux, ou bien la forge en dessine un quatrième ; dans les deux cas, dessinez l’état ouvert comme quelque chose qui vaut la peine d’être vu.12417
Si vous construisez le moteur
- Faites avancer le monde sur un tick fixe avec des déplacements en pixels entiers qui divisent la tuile, lisez les commandes à la tuile, et laissez le moteur de rendu afficher le dernier tick. Dites ce qu’il advient d’un retard accumulé : le nôtre rattrape jusqu’à 8 ticks et jette le reste, en le consignant. Les tables de pas d’Émeraude sont le modèle : chacune totalise 16.216
- Verrouillez la caméra sur le joueur au même tick, avec des bornes et des décalages en pixels entiers, pour que rien n’ait besoin d’être arrondi ensuite. Lisser puis arrondir fait traîner la caméra derrière un joueur qui marche et, dans le modèle de notre code, l’arrête 4 à 9 pixels trop court jusqu’à la marche suivante.36
- Sur iPhone, ne demandez pas 120 hertz pour du mouvement en pixels. Apple donne la priorité à 30 et 60 pour les jeux, RealityKit effectue généralement son rendu à 60, et une marche en pixels entiers à 120 ne fait que tenir chaque pixel pendant deux rafraîchissements.786
- Faites vos fondus par paliers, pas en rampe lisse. Le fondu de porte d’Émeraude compte neuf niveaux espacés de deux seizièmes, son dernier mélange à la 17e image et le fondu terminé à la 22e ; notre voile de 18 ticks est une adaptation de cette rampe, pas une copie.5
- N’animez jamais l’opacité de la vue SwiftUI qui contient une RealityView ; posez un voile par-dessus.22
Si vous concevez la boucle de jeu
- Dans Émeraude, une entrée par une porte est un cérémonial d’au moins 1,32 seconde sans commande, et c’est le pas forcé par-dessus le seuil qui la fait lire comme une entrée à pied.525
- Pivoter sur place pendant 8 images permet au joueur de faire face à quelque chose sans bouger ; un heurt de 32 images lui signale qu’un pas a été refusé.1
- Sur téléphone, ma recommandation : toucher pour marcher par défaut, comme le propose la version mobile de Stardew, un stick flottant comme solution de repli pour la précision, comme le conseillent les HIG, et pas de croix directionnelle fixe.4044
- Les retours haptiques confirment des événements, pas des pas, et s’accompagnent d’un interrupteur.46
Questions fréquentes
À quelle vitesse le joueur marche-t-il dans Pokémon ?
Dans Rouge, Cristal et Émeraude, un pas de marche parcourt une cellule de 16 pixels en 16 images à 59,7275 images par seconde : 268 millisecondes par cellule, 3,73 cellules par seconde. Émeraude avance de 1 pixel à chaque image ; Rouge et Cristal avancent de 2 pixels une image sur deux. La course et le surf dans Émeraude, ainsi que les vélos dans Rouge et Cristal, prennent 8 images par cellule, 7,47 cellules par seconde.1419
Combien d’images compte un cycle de marche dans Pokémon Émeraude ?
Quatre entrées sur 32 images : un dessin d’enjambée pendant 8 images, le dessin debout pendant 8, l’autre enjambée pendant 8, debout pendant 8. Cela représente deux cellules de marche, si bien que chaque pas montre une enjambée et une position debout, et que les jambes alternent pas après pas. La course est un cycle de 16 images sur deux cellules de 8 images.192
La caméra de Pokémon Émeraude est-elle en retard sur le joueur ?
Non. La caméra d’Émeraude copie la position du joueur et fait défiler la carte du même nombre de pixels sur la même image, si bien que le joueur reste fixe à l’écran pendant que le monde se déplace. Elle ne s’arrête pas non plus au bord d’une carte : l’extérieur est dessiné à partir des tuiles de bordure de la disposition. Une caméra d’anticipation pour le vélo existe dans le code, mais n’est jamais activée.231112
Combien de temps dure la transition de porte et de fondu dans Pokémon Émeraude ?
La porte s’ouvre en quatre dessins de cinq images (335 millisecondes), le joueur fait un pas forcé de 16 images vers l’intérieur, la porte se referme en 20 images, et l’écran s’assombrit en neuf niveaux, le dernier mélange sur la 17e image du fondu et le fondu terminé sur sa 22e : au moins 79 images, 1,32 seconde, avant que la carte suivante puisse commencer à se charger. En entrant dans une grotte, l’écran fait un fondu au blanc au lieu du noir, et en en sortant, il réapparaît depuis le blanc.152527
Un jeu en pixel art doit-il tourner à 120 Hz sur un iPhone ProMotion ?
Pas pour un mouvement en pixels entiers. Un monde qui avance d’un pixel par tick à 60 hertz ne fait que montrer chaque pixel pendant deux rafraîchissements à 120 hertz, et avec des durées inégales à 80. L’article d’Apple sur ProMotion indique que les jeux bénéficient d’une « special priority to 30Hz and 60Hz, » (priorité spéciale à 30 et 60 Hz), qu’une app iPhone doit définir CADisableMinimumFrameDurationOnPhone pour dépasser 60, et RealityKit effectue généralement son rendu à 60. Simulez sur un tick fixe à 60 hertz et laissez l’écran faire ce qu’il fait.67168
Pourquoi les pieds de mon sprite glissent-ils quand il marche ?
Généralement parce que les dessins de marche suivent une horloge et le mouvement une autre, avec des longueurs qui ne concordent pas. Le code actuel de Kiradex joue un cycle de six dessins à 8 dessins par seconde tout en marchant à 4 tuiles par seconde, si bien qu’un cycle couvre 3 tuiles au lieu de 2, comme le montre un modèle du code. Les consoles portables comptent les deux en images et font durer les dessins de chaque pas autant que le pas ; choisir le dessin selon la distance parcourue, ce que je propose pour Kiradex, obtient la même concordance sans seconde horloge. Dans les deux cas, la cadence ne peut pas se décaler par rapport au mouvement ; qu’un pied reste bien posé dépend aussi des dessins eux-mêmes.619
Un jeu en pixels sur mobile doit-il utiliser une croix directionnelle virtuelle ?
Pas par défaut, selon ma lecture des sources. Le schéma par défaut de Stardew Valley sur mobile est le toucher pour se déplacer, avec un joystick invisible parmi ses autres schémas pour les tâches de précision ; les HIG d’Apple recommandent de toucher directement les objets et un stick qui apparaît « wherever the player lands their thumb instead of a static thumbstick position. » (là où le joueur pose le pouce, plutôt qu’à une position fixe.)4044
Sur ce site, à lire aussi : Des mondes en pixel art sur iPhone, le premier guide de cette série, avec la marche pas-debout d’Émeraude, la recette RealityKit sur laquelle tourne ce monde et la leçon d’opacité de la visionneuse de cartes ; Des personnages en pixel art : personnages et éditeur sur iPhone, le deuxième, avec la marche en six dessins dont cet article fait passer le minutage d’une horloge à une distance ; Des structures en pixel art : maisons, halls et intérieurs sur iPhone, le troisième, avec les portes, les téléportations et les étages dont la section 2 mesure le minutage ; L’iPhone Duo pour les développeurs et Préparer votre app pour l’iPhone Duo traitent des deux écrans et de la charnière que la caméra du cahier des charges évite ; Le modèle mental spatial de RealityKit explique le modèle d’entités et de systèmes qui sous-tend SceneEvents.Update.
Sources
-
Mesure de l’auteur, 4 octobre 2026 :
measure_gen3_motion.py, dans le dossier de preuves de l’auteur pour cet article, exécuté surpokeemeraldde pret au commit731ad5b; il analyse les tables de fonctions de pas et les durées deInitMoveInPlacedanssrc/event_object_movement.c, les tables d’animation danssrc/data/object_events/object_event_anims.h, la règle du compteur de délai danssrc/sprite.cet les images de porte danssrc/field_door.c, et convertit les images à 59,7275 hertz. Sortie enregistrée sousmeasure_gen3_motion.out.txtà côté (vitesses de 3,73, 7,47, 9,95, 14,93 et 29,86 cellules par seconde ; marche sur place de 32, 16, 8 et 4 images ;sAnim_GoSouth32 images ; dessins de porte tenus 5 mises à jour, 83,7 ms, 335 ms pour quatre). ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
pret,
pokeemerald/src/event_object_movement.c(sStep1FuncsàsStep8Funcset le commentaire « Over the course of the step animation, these sum to 16 pixels (one full metatile) » ;sStepTimesetNpcTakeStep, qui indexe la table de pas avecsTimer, une entrée par image ;SetStepAnimHandleAlternation, qui fixe l’animation de l’allure et l’alternance au début d’un pas ;CameraObject_UpdateMove), commit731ad5b, consulté le 4 octobre 2026, https://github.com/pret/pokeemerald/blob/master/src/event_object_movement.c. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
pret,
pokeemerald/src/overworld.c(l’ordre deOverworldBasic,RunTasks(); AnimateSprites(); CameraUpdate(); UpdateCameraPanning();, suivi plus loin dans la même fonction parUpdatePaletteFade();VBlankCB_Fieldqui appelleTransferPlttBuffer;InitPlayerAvataravantInitCameraUpdateCallback(gPlayerAvatar.spriteId)) etsrc/sprite.c(AnimateSpritesexécute les callbacks dans l’ordre des emplacements), consulté le 4 octobre 2026, https://github.com/pret/pokeemerald/blob/master/src/overworld.c et https://github.com/pret/pokeemerald/blob/master/src/sprite.c. La conclusion « sur la même image » est la lecture du code par l’auteur, pas une exécution. ↩↩↩↩↩↩↩ -
Mesure de l’auteur, 4 octobre 2026 :
measure_gen12_motion.py, dans le dossier de preuves de l’auteur pour cet article, exécuté surpokeredde pret àd2704a6(home/overworld.asm,home/fade.asm) etpokecrystalà5beda23(engine/overworld/events.asm,engine/overworld/map_objects.asm,data/maps/setup_scripts.asm,engine/tilesets/timeofday_pals.asm). Sortie enregistrée sousmeasure_gen12_motion.out.txt(Rouge : 16 px en 16 images, vélo 8 images, fondu de téléportation 32 images ; Cristal : marche en 8 mises à jour de 2 px, vélo 4 de 4, lent 16 de 1 à 1,87 cellule par seconde, fondu de porte 8 images dans chaque sens). ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Mesure de l’auteur, 5 octobre 2026 :
measure_gen3_fade.py, dans le dossier de preuves de l’auteur pour cet article, un portage Python deBeginNormalPaletteFade, deUpdatePaletteFadeavec son indicateur de transfert en attente, deUpdateNormalPaletteFadeet deIsSoftwarePaletteFadeFinishingtirés depokeemerald/src/palette.cde pret à731ad5b, exécuté selon le calendrier de leurs appelants : la tâche de la porte lance le fondu dansRunTasks(Task_DoDoorWarp,src/field_screen_effect.c) ;BeginNormalPaletteFadeeffectue une mise à jour, copie le tampon dans la mémoire des palettes et efface l’indicateur ;OverworldBasic(src/overworld.c) effectue une nouvelle mise à jour dans la même image ; chaque image suivante en effectue une, et le transfert VBlank efface l’indicateur. Les images sont comptées à partir de l’image de la tâche, numérotée 0. Le portage suppose qu’aucun fondu n’était en cours ; avec la pluie, la neige, le brouillard, l’ombre ou la sécheresse actifs,FadeScreendanssrc/field_weather.ccopie le tampon teinté par la météo puis appelle le mêmeBeginNormalPaletteFade(le gestionnaire propre à la météo pour le fondu de fermeture estDoNothing), si bien que le calendrier du fondu de fermeture reste valable, tandis que le fondu d’ouverture passe par le code météo et n’a pas été simulé ; le fondu d’ouverture après une téléportation part du callback de chargement de la carte, dont la première image n’a pas été suivie, d’où les deux cas donnés. Sortie enregistrée sousmeasure_gen3_fade.out.txt(niveaux de 0 à 16 par pas de 2 ; premier mélange visible à l’image 1 ; dernier mélange, les palettes de sprites à 16, à l’image 16, 285 ms en comptant l’image 0 ; inactif à l’image 21, 368 ms ;FadeInFromWhiteavec un délai de 8 inactif à l’image 85 ou 86 en comptant à partir de l’image 0, soit après 86 ou 87 images, 1 440 ou 1 457 ms ; entrée par une porte : ouverture 20, pas 16 et fermeture 20 images, le dernier mélange du fondu après au moins 73 images, 1,22 s, etWarpIntoMapau plus tôt à l’image 79, 1,32 s, puisqueTask_WarpAndLoadMapattend que le fondu devienne inactif). ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Modèle de l’auteur, 5 octobre 2026 :
measure_kiradex_motion.py, dans le dossier de preuves de l’auteur pour cet article, littilesPerSecond(4),framesPerSecond(8),runPace(1,6), le seuil de course (6 pas) et lemin(1, dt * 6)de la caméra dansKiradex/World/PlazaRig.swift,centredansKiradex/World/TileMap.swift, et les cycleswalketrunde six dessins dansscripts/forge/rig.py, puis modéliseadvance,place,animateetfollowimage par image, chaque opérationFloatétant effectuée sur 32 bits (numpyfloat32) dans l’ordre du code et avec l’arrondi de Swift « au plus proche, loin de zéro », à exactement 1/60 et 1/120 s, pour une marche de 5 tuiles, l’allure de marche maintenue sur 12 tuiles et une course de 12 tuiles, chacune suivie de 3 s à l’arrêt ; la limitation à la carte, la charnière et le décalage des pieds sont omis. C’est un modèle du code, pas une capture faite sur un appareil. Sortie enregistrée sousmeasure_kiradex_motion.out.txt. Marche à 60 Hz : 15 images par tuile, chaque tuile composée de 14 images de 1 px et d’une de 2 ; écart de caméra de 5, 6, 7, 8 et 9 px à la fin des cinq premières tuiles, 13 à partir de la neuvième quand l’allure de marche est maintenue ; position à l’écran changeant sur 8 des 73 images en mouvement d’une marche de 5 tuiles après la première (la marche du modèle compte 74 images en mouvement, la dernière image de la dernière tuile comptant comme arrêt) ; décalage au repos de 4 px. À 120 Hz : 30 images par tuile, 16 de 1 px et 14 de 0 ; écart 9 ; repos 9. Course à 60 Hz : 10 images par tuile, 6,00 tuiles par seconde, 4 images de 1 px et 6 de 2 par tuile ; écart 13 à partir de la deuxième tuile ; repos 4. Course à 120 Hz : 19 images par tuile, 6,32 tuiles par seconde ; écart 9 ; repos 9. Distance par cycle à partir des positions simulées entre débuts de cycles successifs à 60 Hz : 3,00 tuiles en marchant, 4,50 en courant (4,80 aux 6,4 tuiles par seconde nominales) ; à 120 Hz, 3,00 et 2,94 en marchant, 4,75 en courant. Pas en diagonale dans le code actuel : 22 images en marchant et 14 en courant à 60 Hz, 43 et 27 à 120. Comparé au même modèle exécuté avec l’horloge, les positions et la caméra en double précision, chaque position et chaque valeur de caméra restent inchangées et 9 dessins de marche à 60 Hz et 14 à 120 diffèrent, tous sur des images où l’horloge multipliée par 8 est un nombre entier (11 images de ce type en 12 tuiles de marche à 60 Hz, 23 à 120). La proposition : un tick fixe à 60 Hz tient chaque pixel pendant 1 rafraîchissement à 60 Hz, 2 rafraîchissements pour 59 des 61 positions à 120 Hz, et 1 ou 2 rafraîchissements, pour 42 et 19 positions, à 80 Hz. L’accumulateur : dix secondes de callbacks à chacune des douze fréquences de l’iPhone, de 120 à 10 Hz, exécutent 600 ticks avec une limite de retard de 8 ticks et rien de jeté, contre 480 à 12 Hz et 400 à 10 Hz avec un plafond de 4 ticks par callback ; après un à-coup de 1 s à 60 Hz, la règle des 8 ticks exécute 8 ticks, puis 1 par callback, en jetant 866,7 ms, tandis qu’un plafond de 4 ticks qui conserve le retard exécute 4 ticks sur 19 callbacks consécutifs. Le calcul de la caméra :fitsur une vue de 393 sur 852 pt en 3x donne 7 pixels d’écran par texel et une vue de 168,43 sur 365,14 pixels du monde, demi-largeur 84,21, bornes arrondies vers l’intérieur à 85 et à la largeur de la carte moins 85 ; un changement de charnière de 0 à −37 en 9 niveaux passe par 0, −5, −9, −14, −19, −23, −28, −32, −37. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple Developer Documentation, « Optimizing iPhone and iPad apps to support ProMotion displays » (la plage de 10 à 120 Hz de l’iPhone et ses douze fréquences ; la liste des appareils ;
CADisableMinimumFrameDurationOnPhone; la priorité accordée aux jeux à 30 et 60 Hz ; « Prepare your app to operate at any refresh rate » ;targetTimestamp), consulté le 4 octobre 2026, https://developer.apple.com/documentation/quartzcore/optimizing-iphone-and-ipad-apps-to-support-promotion-displays. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple Developer Documentation, « Improving the Performance of a RealityKit App » (« RealityKit typically limits the refresh rate » à 60 images par seconde), consulté le 4 octobre 2026, https://developer.apple.com/documentation/realitykit/improving-the-performance-of-a-realitykit-app. ↩↩↩↩↩↩↩
-
pret,
pokeemerald/src/data/object_events/object_event_anims.h(sAnim_GoSouth,sAnim_GoFastSouth,sAnim_GoFasterSouth,sAnim_GoFastestSouth,sAnim_RunSouth) etsrc/sprite.c(animDelayCounterchargé avec la durée de l’image moins un, décompté parContinueAnim, l’image suivante prise quand il atteint zéro), consulté le 4 octobre 2026, https://github.com/pret/pokeemerald/blob/master/src/data/object_events/object_event_anims.h et https://github.com/pret/pokeemerald/blob/master/src/sprite.c. ↩↩↩↩↩↩↩↩↩↩↩ -
pret,
pokeemerald/src/field_player_avatar.c(PlayerWalkNormal,PlayerRun,PlayerWalkFastavec le commentaire « same speed as running »,CheckMovementInputNotOnBikeetTURN_DIRECTION,PlayerTurnInPlace, le heurt contre un mur), consulté le 4 octobre 2026, https://github.com/pret/pokeemerald/blob/master/src/field_player_avatar.c. ↩↩↩↩↩ -
pret,
pokeemerald/src/fieldmap.c(GetBorderBlockAt: les metatiles de bordure 2 par 2 de la disposition, marquéesMAPGRID_IMPASSABLE), consulté le 4 octobre 2026, https://github.com/pret/pokeemerald/blob/master/src/fieldmap.c. ↩↩↩ -
pret,
pokeemerald/src/field_camera.c(CameraUpdate;CameraPanningCB_PanAhead, qui déplacesVerticalCameraPande 2 vers 72 ou moins 8 depuis une valeur de repos de 32, protégé pargUnusedBikeCameraAheadPanbacket commenté « this code is never reached »), consulté le 4 octobre 2026, https://github.com/pret/pokeemerald/blob/master/src/field_camera.c. ↩↩↩↩↩↩↩↩ -
Blake Crosley, « Pixel-Art Structures: Houses, Halls and Interiors on iPhone », blakecrosley.com, 3 octobre 2026 (l’image de porte d’Émeraude tenue cinq mises à jour, environ 84 millisecondes ; le
PlayerStepOutFromDoorde Rouge ; la secousse de l’ascenseur d’Émeraude ; la porte à 70 millisecondes et la téléportation à 220 millisecondes de Kiradex rangées parmi les lacunes), https://blakecrosley.com/blog/pixel-art-structures-on-iphone. ↩↩↩↩↩↩ -
Notes de recherche de l’auteur pour l’article sur les structures, dépôt Kiradex (privé),
docs/research/structures/01-structures-in-the-canon.md, 3 octobre 2026, qui donnaient les images de porte d’Émeraude comme « 4 ticks each (16 frames, about 0.27 s at 59.7 Hz) » ; corrigé par la lecture deAnimateDoorFramedansfield_door.cprésentée dans cet article. ↩↩ -
Apple Developer Documentation, « preferredFrameRateRange » (
CADisplayLink; iOS 15.0, iPadOS 15.0, Mac Catalyst 15.0, visionOS 1.0), consulté le 4 octobre 2026, https://developer.apple.com/documentation/quartzcore/cadisplaylink/preferredframeraterange. ↩↩↩ -
Apple Developer Documentation, « CADisableMinimumFrameDurationOnPhone » (clé de l’Information Property List ; iOS 15.0, iPadOS 15.0), consulté le 4 octobre 2026, https://developer.apple.com/documentation/bundleresources/information-property-list/cadisableminimumframedurationonphone. ↩↩↩
-
Lecture par l’auteur du dépôt Kiradex (privé) au commit
b1b78b1, 4 octobre 2026, en lecture seule :Kiradex/World/PlazaRig.swift(tilesPerSecond,framesPerSecond,runPace,walk(to:)et sa règle de coursesteps.count >= 6,advance, dont le pas de progression estdt * Self.tilesPerSecond * (walker.running ? Self.runPace : 1) / length,animate,place,fit, qui divise les pixels d’écran de la vue par unpixelScaleentier,follow, qui convertit les points de la charnière pardisplayScale / pixelScale, lisse parmin(1, dt * 6)puis arrondit, et dont la borne ne laisse la caméra dépasser le bord de la carte vers les bois que lorsque le Duo est à moitié ouvert, debeyondMargin, 12 tuiles, tandis quebuilddispose les bois quelle que soit la charnière,ripple, qui change le dessin de l’eau du sol 4 fois par seconde sur une carte comportant de l’eau,openDooret son commentaire « at about Emerald’s four ticks a frame »),Kiradex/World/PlazaStage.swift(la gestion du toucher, le délai de téléportation de 220 millisecondes, l’abonnement àSceneEvents.Update, le remplacement d’étage par.id),Kiradex/World/TileMap.swift(lepathà huit directions, qui prend la tuile ouverte au coût le plus bas jusque-là, ajoute 1 ou √2 par pas et n’utilise aucune estimation de la distance au but, et dont les pas en diagonale sont ignorés sauf si les deux cellules latérales sont praticables),Kiradex/World/WorldMap.swift(la planche de porte d’une téléportation, « closed, half open, open »),openDoorqui charge cette planche avecSpriteSheet.bundled(name, columns: 3, rows: 1)et affiche la colonne 1 puis la colonne 2,scripts/forge/kit.py(door_sheet(), « The three frames side by side, 48 × 32 »),scripts/forge/town.pyetscripts/forge/buildings.py(chaque téléportation de porte de la ville se trouve sur la cellule de porte de la rangée inférieure d’un bâtiment, et les cellules du bâtiment à gauche, à droite et au-dessus de chaque porte sont bloquées ; vérifié pour les sept portes de la ville dans letown.jsonlivré),Kiradex/Views/Card/CardViewer.swift(le.easeOut(duration: 0.25)du voile) etscripts/forge/rig.py(les cycles). L’absence deUIImpactFeedbackGenerator,sensoryFeedback,CHHaptic,CADisplayLink,preferredFrameRateRangeetCADisableMinimumFrameDurationOnPhonerésulte d’une recherche surKiradex/etproject.ymlle 4 octobre 2026, qui n’en a trouvé aucun ; la même recherche ne trouve niMTKView, niMTLRenderCommandEncoder, niTouchControllerdansKiradex/, le monde n’a donc pas de passe de rendu propre. Le recul lors d’un toucher en plein pas est lu danswalk(to:), pas capturé. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
pret, clones superficiels de
pokered(commitd2704a6, 22 septembre 2026),pokecrystal(5beda23, 29 septembre 2026) etpokeemerald(731ad5b, 1er octobre 2026), les décompilations communautaires des jeux publiés, https://github.com/pret. ↩ -
Martin Korth, GBATEK, « LCD Dimensions and Timings » (280 896 cycles et 16,743 ms par image, « ca. 59.737 Hz »), consulté le 4 octobre 2026, https://problemkaputt.de/gbatek-lcd-dimensions-and-timings.htm ; Pan Docs, « Rendering » (« One frame: 70224 dots @ 59.7 fps »), consulté le 4 octobre 2026, https://gbdev.io/pandocs/Rendering.html. Le chiffre de 59,7275 est le calcul de l’auteur : 2^24 / 280 896, et 4 194 304 / 70 224. ↩↩↩↩↩
-
pret,
pokeemerald/src/bike.c(sMachBikeSpeedCallbacks,bikeFrameCounterplafonné à 2,AcroBikeTransition_Movingqui appellePlayerRideWaterCurrent, et la seule affectationgUnusedBikeCameraAheadPanback = FALSE), consulté le 4 octobre 2026, https://github.com/pret/pokeemerald/blob/master/src/bike.c. ↩↩↩ -
Figures dessinées par le script de l’auteur
make_figures.py(avecsvgkit.py), 4 octobre 2026, uniquement à partir de données sur disque : les vitesses du canon tirées demeasure_gen3_motion.out.txtetmeasure_gen12_motion.out.txt; la marche et la caméra de Kiradex tirées dusimulate()propre àmeasure_kiradex_motion.py(la première seconde d’une marche de 5 tuiles ; la caméra pour une marche de 5 tuiles à 60 et 120 Hz et une course de 12 tuiles à 60 Hz), et la boucle de ticks de la proposition tirée du même script ; le fondu d’Émeraude tiré durun()demeasure_gen3_fade.py, le portage avec le calendrier de ses appelants, ainsi que le fondu et le chargement de carte le plus précoce du graphique de la porte ; les minutages de porte de Kiradex (70 ms, 1 400 ms, 220 ms) lus dansPlazaRig.swiftetPlazaStage.swiftàb1b78b1. Aucun graphisme des jeux n’est utilisé. ↩↩↩↩↩ -
Blake Crosley, « Pixel-Art Worlds on iPhone: What the 16-Bit Masters Knew », blakecrosley.com, 3 octobre 2026 (la marche d’Émeraude « step, stand, step, stand at eight ticks each » ; celle de Rouge « stand, step, stand, step-flipped » ; Celeste en 320 par 180 multiplié par six ; le moteur RealityKit ; les positions « rounded to whole world units each frame, after easing » ; la leçon d’opacité de la visionneuse de cartes), https://blakecrosley.com/blog/pixel-art-world-on-iphone. ↩↩↩↩↩↩↩↩
-
Itay Keren, « Scroll Back: The Theory and Practice of Cameras in Side-Scrollers », Game Developer, 11 mai 2015, version remaniée de sa conférence à l’Independent Games Summit, GDC 2015 (position-locking, edge-snapping, camera-window, lerp-smoothing, target-focus, dual-forward-focus ; les citations), consulté le 4 octobre 2026, https://www.gamedeveloper.com/design/scroll-back-the-theory-and-practice-of-cameras-in-side-scrollers. ↩↩↩↩↩↩↩↩
-
pret,
pokeemerald/src/field_door.c(sDoorOpenAnimFrames{4, -1}, {4, 0}, {4, 0x100}, {4, 0x200};AnimateDoorFrame, qui dessine quand le compteur vaut 0 et avance quand le compteur égale la durée de l’image), consulté le 4 octobre 2026, https://github.com/pret/pokeemerald/blob/master/src/field_door.c. ↩↩↩↩ -
pret,
pokeemerald/src/field_screen_effect.c(les états deTask_DoDoorWarp, dont le cinquième appelleWarpFadeOutScreenet confie la tâche àTask_WarpAndLoadMap, qui attendPaletteFadeActive(), c’est-à-diregPaletteFade.active, etBGMusicStopped()avantWarpIntoMap;Task_ExitDoor,WarpFadeOutScreenqui appelleGetMapPairFadeToType,WarpFadeInScreenqui appelleGetMapPairFadeFromType, etFadeInFromWhiteavecFadeScreen(FADE_FROM_WHITE, 8)), consulté le 4 octobre 2026, https://github.com/pret/pokeemerald/blob/master/src/field_screen_effect.c. ↩↩↩↩↩↩↩↩↩↩ -
pret,
pokeemerald/src/palette.c(BeginNormalPaletteFadeavecdeltaY = 2, son propre appel àUpdatePaletteFade, sonCpuCopy32vers la mémoire des palettes etsPlttBufferTransferPending = FALSE;UpdatePaletteFade, qui retourne aussitôt tant que cet indicateur est levé et le lève à partir degPaletteFade_selectedPalettesaprès chaque mise à jour ;TransferPlttBuffer, qui l’efface ;UpdateNormalPaletteFade;IsSoftwarePaletteFadeFinishing), consulté le 4 octobre 2026, https://github.com/pret/pokeemerald/blob/master/src/palette.c. ↩↩ -
pret,
pokeemerald/src/fldeff_flash.c(sTransitionTypeset ses 16 lignes vers et depuisMAP_TYPE_UNDERGROUND, chacune avec un indicateur d’entrée, un indicateur de sortie et une routine de transition ;GetMapPairFadeToTypequi renvoie l’indicateur d’entrée d’une ligne etGetMapPairFadeFromTypeson indicateur de sortie), consulté le 4 octobre 2026, https://github.com/pret/pokeemerald/blob/master/src/fldeff_flash.c. ↩↩↩ -
pret,
pokeemerald/src/field_specials.c(ShakeCamera, qui lit le panoramique vertical, le panoramique horizontal, le nombre de secousses et le délai dansVAR_0x8004àVAR_0x8007, en inversant le panoramique à chaque secousse), consulté le 4 octobre 2026, https://github.com/pret/pokeemerald/blob/master/src/field_specials.c. ↩ -
Décompte de l’auteur, 4 octobre 2026 :
measure_gen3_shake.py, dans le dossier de preuves de l’auteur pour cet article, lit chaque appelspecial ShakeCameraet les quatre valeurssetvarqui le précèdent dansdata/**/*.incdepokeemeraldde pret à731ad5b. Sortie enregistrée sousmeasure_gen3_shake.out.txt(24 appels dans 9 fichiers, les huit jeux de paramètres et leurs effectifs dans le tableau). ↩↩↩↩↩↩ -
pret,
pokered/home/overworld.asm(OverworldLoop,OverworldLoopLessDelay,wWalkCounter,AdvancePlayerSprite,DoBikeSpeedup, et le commentaire sur le demi-tour), commitd2704a6, consulté le 4 octobre 2026, https://github.com/pret/pokered/blob/master/home/overworld.asm. Le comportement de rotation est lu dans le code, pas exécuté dans un émulateur. ↩↩↩↩↩↩ -
pret,
pokered/engine/overworld/movement.asm(UpdatePlayerSprite, le compteur interne à l’animation qui fait avancer le dessin à 4), consulté le 4 octobre 2026, https://github.com/pret/pokered/blob/master/engine/overworld/movement.asm. ↩↩ -
pret,
pokered/home/fade.asm(GBFadeOutToBlack) ethome/overworld.asm(PlayMapChangeSound,SFX_GO_INSIDEpour la tuile de porte$0b, sinonSFX_GO_OUTSIDE), consulté le 4 octobre 2026, https://github.com/pret/pokered/blob/master/home/fade.asm. ↩ -
pret,
pokecrystal/engine/overworld/events.asm(MaxOverworldDelay: db 2) etengine/overworld/map_objects.asm(StepVectors;StepFunction_Turn, dont.init1,.step1,.init2et.step2s’enchaînent les uns dans les autres, etObjectStep_AnonJumptable), commit5beda23, consulté le 4 octobre 2026, https://github.com/pret/pokecrystal/blob/master/engine/overworld/events.asm et https://github.com/pret/pokecrystal/blob/master/engine/overworld/map_objects.asm. Les trois mises à jour de la rotation proviennent d’une relecture instruction par instruction de la routine par l’auteur, pas d’une exécution dans un émulateur. ↩↩↩↩↩ -
pret,
pokecrystal/data/maps/setup_scripts.asm(MapSetupScript_Doorqui commence parFadeOutToWhite,MapSetupScript_Warpqui se termine parFadeInFromWhite) etengine/tilesets/timeofday_pals.asm, consulté le 4 octobre 2026, https://github.com/pret/pokecrystal/blob/master/data/maps/setup_scripts.asm et https://github.com/pret/pokecrystal/blob/master/engine/tilesets/timeofday_pals.asm. ↩ -
Blake Crosley, « Pixel-Art People: Characters and a Creator on iPhone », blakecrosley.com, 3 octobre 2026 (la marche en six images ; « the walk at eight frames a second » sur la place ; sous « Since publishing », le personnage de 26 pixels remplacé à la build TestFlight 34 par un personnage de 30 pixels dans une cellule de 32 sur 40, que
scripts/forge/rig.pyàb1b78b1définit commeCELL_W, CELL_H = 32, 40), https://blakecrosley.com/blog/pixel-art-characters-on-iphone. ↩↩↩ -
Les développeurs de Celeste, le dépôt
NoelFB/Celestesur GitHub,Source/Player/Player.cs(la mise à jour de la caméra sous « Camera (lerp by distance using delta-time) » et l’accesseurCameraTarget), consulté le 4 octobre 2026, https://raw.githubusercontent.com/NoelFB/Celeste/master/Source/Player/Player.cs. ↩↩↩↩ -
Les développeurs de Celeste, dépôt
NoelFB/Celeste,README.md(les fichiers de classes publiés « as a learning resource and for general interest » ; la licence MIT ne s’appliquant qu’à ce code), consulté le 4 octobre 2026, https://raw.githubusercontent.com/NoelFB/Celeste/master/README.md. ↩ -
Stardew Valley Wiki, « Speed » (la vitesse de base du joueur : 2 en marchant, 5 en courant, 6,6 à cheval, 7 après une carotte), consulté le 4 octobre 2026, https://stardewvalleywiki.com/Speed. ↩
-
Notes de recherche de l’auteur pour cet article, compilées le 4 octobre 2026, consignant ce qui a été cherché sans être trouvé : une source primaire sur les caméras de Sea of Stars, Eastward ou CrossCode ; une conférence ou un article de Maddy Thorson sur la caméra ; le schéma tactile exact des Pixel Remaster ; le déplacement de Terraria sur mobile sur https://terraria.wiki.gg/wiki/Mobile_version ; et
developer.apple.com/documentation/touchcontrols, qui a renvoyé une erreur 404. ↩↩↩ -
Stardew Valley Wiki, « Mobile Controls » (les schémas ; « Tap-to-move & Auto-Attack » par défaut ; les citations sur le toucher pour se déplacer, le suivi du contact, le joystick invisible et les limites des commandes par défaut), consulté le 4 octobre 2026, https://stardewvalleywiki.com/Mobile_Controls. ↩↩↩↩↩↩↩
-
Jared Nelson, « ‘Stardew Valley’ is Getting A TON of New Control Options in the Next Update », TouchArcade, 1er novembre 2018, consulté le 4 octobre 2026, https://toucharcade.com/2018/11/01/stardew-valley-mobile-controls-update/. ↩
-
App Store, « FINAL FANTASY » de SQUARE ENIX, historique des versions (version 1.2.0, 03/11/2025, et la note sur le déplacement au toucher), consulté le 4 octobre 2026, https://apps.apple.com/us/app/final-fantasy/id1492041278. ↩↩
-
Mikhail Madnani, TouchArcade, 30 janvier 2024, sur la mise à jour de Final Fantasy Pixel Remaster qui a apporté la prise en charge des manettes sur mobile, consulté le 4 octobre 2026, https://toucharcade.com/2024/01/30/final-fantasy-pixel-remaster-mobile-controller-support-update-boosts-cheats-font-not-fixed-steam-deck-patch-notes/. ↩
-
Apple Human Interface Guidelines, « Game controls » (bonnes pratiques pour les commandes tactiles ; entrée du journal des modifications du 9 juin 2025), consulté le 4 octobre 2026, https://developer.apple.com/design/human-interface-guidelines/game-controls. ↩↩↩↩↩↩↩
-
Apple, session 209 de la WWDC25 (le framework Touch Controls ; « the vast majority of players won’t have a controller available » ; « integrates directly with Metal »), transcription consultée le 4 octobre 2026, https://developer.apple.com/videos/play/wwdc2025/209/. ↩↩↩
-
Apple Human Interface Guidelines, « Playing haptics » (bonnes pratiques, retours haptiques personnalisés et catégorie impact d’iOS ; les citations), consulté le 4 octobre 2026, https://developer.apple.com/design/human-interface-guidelines/playing-haptics. ↩↩↩↩↩↩↩↩↩
-
Apple Developer Documentation, « CAFrameRateRange » (iOS 15.0, iPadOS 15.0, Mac Catalyst 15.0, visionOS 1.0), consulté le 4 octobre 2026, https://developer.apple.com/documentation/quartzcore/caframeraterange. ↩
-
Apple Developer Documentation, « targetTimestamp » (
CADisplayLink; iOS 10.0, iPadOS 10.0, Mac Catalyst 13.1, visionOS 1.0), consulté le 4 octobre 2026, https://developer.apple.com/documentation/quartzcore/cadisplaylink/targettimestamp. ↩ -
Apple, session 10147 de la WWDC21, « Optimize for variable refresh rate displays » (écrans Adaptive-Sync sur Mac et ProMotion sur iPad Pro ; les citations), transcription consultée le 4 octobre 2026, https://developer.apple.com/videos/play/wwdc2021/10147/. ↩↩↩
-
Apple Developer Documentation, « SceneEvents.Update » (iOS 13.0, iPadOS 13.0, Mac Catalyst 13.0), consulté le 4 octobre 2026, https://developer.apple.com/documentation/realitykit/sceneevents/update. ↩
-
Apple Developer Documentation, « deltaTime » (
SceneEvents.Update; iOS 13.0), consulté le 4 octobre 2026, https://developer.apple.com/documentation/realitykit/sceneevents/update/deltatime. ↩ -
Apple Developer Documentation, « RealityView » (iOS 18.0, iPadOS 18.0, Mac Catalyst 18.0, visionOS 1.0 ; code par image via un
SystemouSceneEvents.Update), consulté le 4 octobre 2026, https://developer.apple.com/documentation/realitykit/realityview. ↩ -
Apple Developer Documentation, « CHHapticPattern » (Core Haptics ; iOS 13.0, iPadOS 13.0, Mac Catalyst 13.0, visionOS 1.0), consulté le 4 octobre 2026, https://developer.apple.com/documentation/corehaptics/chhapticpattern ; page du framework Core Haptics, https://developer.apple.com/documentation/corehaptics. ↩
-
Apple Developer Documentation, « CHHapticEvent » et « CHHapticEvent.EventType » (
hapticTransient,hapticContinuous; iOS 13.0), consulté le 4 octobre 2026, https://developer.apple.com/documentation/corehaptics/chhapticevent et https://developer.apple.com/documentation/corehaptics/chhapticevent/eventtype. ↩ -
Apple Developer Documentation, « CHHapticEvent.ParameterID » (iOS 13.0), consulté le 4 octobre 2026, https://developer.apple.com/documentation/corehaptics/chhapticevent/parameterid. ↩
-
Apple Developer Documentation, « CHHapticEngine » (iOS 13.0 ;
capabilitiesForHardware()), consulté le 4 octobre 2026, https://developer.apple.com/documentation/corehaptics/chhapticengine. ↩↩ -
Apple Developer Documentation, « UIImpactFeedbackGenerator » (iOS 10.0, iPadOS 10.0, Mac Catalyst 13.1 ;
init(style:view:)sous « Initializing the feedback generator », etinit(style:)sous Deprecated), consulté le 4 octobre 2026, https://developer.apple.com/documentation/uikit/uiimpactfeedbackgenerator. ↩↩ -
Apple Developer Documentation, « UIImpactFeedbackGenerator.FeedbackStyle » (iOS 10.0), consulté le 4 octobre 2026, https://developer.apple.com/documentation/uikit/uiimpactfeedbackgenerator/feedbackstyle. ↩
-
Apple Developer Documentation, « impactOccurred(intensity:) » (iOS 13.0, iPadOS 13.0, Mac Catalyst 13.1), consulté le 4 octobre 2026, https://developer.apple.com/documentation/uikit/uiimpactfeedbackgenerator/impactoccurred(intensity:). ↩
-
Apple Developer Documentation, « prepare() » (
UIFeedbackGenerator; iOS 10.0), consulté le 4 octobre 2026, https://developer.apple.com/documentation/uikit/uifeedbackgenerator/prepare(). ↩↩ -
Apple Developer Documentation, « sensoryFeedback(:trigger:) » et « SensoryFeedback » (SwiftUI ; iOS 17.0, iPadOS 17.0, Mac Catalyst 17.0, visionOS 26.0 ;
impact(weight:intensity:)), consulté le 4 octobre 2026, https://developer.apple.com/documentation/swiftui/view/sensoryfeedback(:trigger:) et https://developer.apple.com/documentation/swiftui/sensoryfeedback. ↩ -
Apple Developer Documentation, « Touch Controller » (iOS 26.0, iPadOS 26.0, Mac Catalyst 26.0, visionOS 26.0), consulté le 4 octobre 2026, https://developer.apple.com/documentation/touchcontroller. ↩↩↩↩
-
Apple Developer Documentation, « TCDirectionPad » (iOS 26.0, iPadOS 26.0, Mac Catalyst 26.0), consulté le 4 octobre 2026, https://developer.apple.com/documentation/touchcontroller/tcdirectionpad. ↩
-
Apple Developer Documentation, « GCVirtualController » (Game Controller ; iOS 15.0, iPadOS 15.0, Mac Catalyst 15.0), consulté le 4 octobre 2026, https://developer.apple.com/documentation/gamecontroller/gcvirtualcontroller. ↩
-
pret,
pokeemerald/src/field_control_avatar.c(TryDoorWarp, appelé avec la cellule située devant le joueur tant que la direction maintenue correspond à l’orientation, et qui ne téléporte que si cette direction est le nord et que la cellule est une téléportation de porte), consulté le 4 octobre 2026, https://github.com/pret/pokeemerald/blob/master/src/field_control_avatar.c. ↩