Le même vaisseau sur chaque écran

Un classement quotidien n'est équitable que si voler sur le même tronçon de grotte est tout aussi difficile sur un vieux petit téléphone, une tablette à 144 Hz et un navigateur de bureau. Voici ce qu'il a fallu faire, et ce que je n'ai pas pu égaliser.

Devlog · 20 septembre 2026 · tous les articles

Le billet sur la grotte quotidienne parle de rendre le monde identique pour chaque joueur. Celui-ci parle de l'autre moitié de l'équité : faire en sorte que le vaisseau se comporte de façon identique. Il y a deux façons distinctes qu'un appareil peut changer la façon dont le jeu se joue, et elles nécessitent deux corrections distinctes. L'une est la fréquence de rafraîchissement de l'écran. L'autre est la taille de l'écran.

Problème un : le taux de rafraîchissement

Le vaisseau a une position et une vitesse. À chaque image, la gravité ou la poussée change la vitesse, et la vitesse change la position. La façon évidente de l'écrire est :

vy += accel * dt;     // update velocity
py += vy * dt;        // then move by the NEW velocity

Ça a l'air inoffensif, mais c'est subtilement faux. La seconde ligne fait comme si le vaisseau avait déjà passé toute l'image à voyager à sa vitesse de fin d'image. Si le vaisseau accélère, cela dépasse de la moitié de accel × dt² à chaque image. Le hic, c'est que dt, la durée de l'image, est différente sur chaque appareil, donc le dépassement l'est aussi, et un vaisseau volait sur une trajectoire légèrement différente à chaque taux de rafraîchissement.

Je l'ai mesuré avec une poussée maintenue pendant une demi-seconde. La réponse exacte est 225 pixels. L'ancien intégrateur donnait 228 à 144 Hz, 232 à 60 Hz et 240 à 30 Hz. Trois appareils, trois distances différentes, pour la même entrée.

La correction est la règle du trapèze, qui utilise la moyenne de la vitesse avant et après l'image :

const vyPrev = vy;
vy += accel * dt;
py += (vyPrev + vy) * 0.5 * dt;   // average speed over the frame

Pour une accélération constante, c'est exact, quelle que soit la durée de l'image. Rejouer un schéma d'entrée identique donne maintenant un écart de 0,000 pixel entre 12 et 144 Hz. C'était 4,1 pixels avant. Un test vérifie les deux moitiés : que le nouvel intégrateur concorde sur tous les taux de rafraîchissement et que l'ancien divergeait réellement, donc si quelqu'un l'annule, le test échoue au lieu que le classement dérive silencieusement.

Deux réserves honnêtes

Cela n'a pas comblé l'écart de score entre les taux de rafraîchissement. Je m'y attendais. Un pilote simulé marquait toujours environ un tiers de plus à 144 Hz qu'à 60 Hz après coup. Cet écart n'est pas de la physique. C'est la résolution d'entrée : un pouce ne peut changer d'appuyé à relâché qu'à la limite d'une image, donc un écran plus rapide donne réellement un contrôle plus fin. Aucun intégrateur ne peut corriger ça, et un bot qui inverse son entrée à chaque image exagère cela par rapport à un humain. La moitié physique est maintenant exacte, et le reste est une propriété du matériel.

C'est un très léger nerf à 60 Hz. Une montée d'une demi-seconde couvre maintenant 225 pixels au lieu de 232. J'ai laissé cela non compensé. Si un test sur appareil réel trouvait un jour le vaisseau mou, le levier est la constante de poussée, environ 3% de plus rétablirait l'ancienne manœuvre d'une demi-seconde. C'est un bon moment pour dire que la poussée a déjà été changée deux fois sur retour des joueurs, et cette histoire est la raison pour laquelle je suis prudent avant d'y toucher.

Problème deux : la hauteur de l'écran

La gravité en pixels par seconde au carré signifie des choses très différentes sur un téléphone haut de 375 points et une tablette haute de 520 points. Si les constantes étaient les mêmes, la même montée traverserait une fraction bien plus petite de l'écran sur la tablette, et le jeu paraîtrait plus lourd.

Les constantes sont donc définies par rapport à une hauteur de référence de 440 points, la hauteur en paysage du plus grand iPhone, où la sensation du jeu a été réglée et testée. Sur tout autre écran, la gravité, la poussée et la vitesse terminale sont toutes multipliées par H / 440. Comme le demi-écart du corridor s'échelonne aussi avec la hauteur, chaque trajectoire est géométriquement similaire à celle de référence : la même fraction du corridor traversée par seconde, le même temps pour aller d'un mur à l'autre, la même réactivité ressentie. Un écran plus grand obtient une accélération plus raide en pixels, et un plus petit une plus douce, par conception.

Cela vient avec une règle pour tout code futur : tout ce qui raisonne sur le mouvement vertical doit être basé sur des ratios. Une fraction de la vitesse terminale, une fraction de la hauteur. Jamais une constante brute en pixels par seconde comparée à la vitesse du vaisseau, parce que ce nombre n'est correct que sur un seul appareil. Si un littéral est inévitable, il est multiplié par la même échelle.

La version web suit la même règle. La compilation navigateur limite son canevas à 956 sur 440, la même empreinte que ce téléphone de référence, donc le jeu dans un navigateur en est une copie pixel et physique exacte. Une compilation web antérieure avait son propre multiplicateur de sensation, qui n'existait que pour combattre la mollesse causée par un plafond différent, et il a été supprimé une fois le plafond aligné.

Problème trois : la largeur de l'écran

La hauteur est l'axe vertical. La largeur est l'axe horizontal, et elle est plus subtile. Les obstacles sont espacés à des distances fixes dans le monde. La grotte défile à une vitesse qui inclut un facteur pour la largeur de l'écran. Donc sur un écran plus large, les mêmes obstacles défilent plus vite en termes d'écran, et le temps dont tu disposes pour réagir à chacun diminue proportionnellement à la largeur.

C'était une vraie faille. Une grande tablette Android d'environ 1280 points de large affrontait la grotte partagée environ 1,75 fois plus vite qu'un petit téléphone. La largeur est donc plafonnée à 956 points sur chaque plateforme, la largeur du plus large iPhone en paysage. Un appareil plus large que ça obtient des bandes noires à gauche et à droite. C'est un no-op sur les iPhone et cela ne plafonne que les grandes tablettes Android et les navigateurs de bureau.

Le plafond a un résidu que je n'ai pas corrigé. Le rayon de collision du joueur s'échelonne avec la largeur tandis que le corridor s'échelonne avec la hauteur, donc sur une tablette plus carrée le vaisseau est un peu plus petit par rapport à l'écart que sur un téléphone, environ 20% plus indulgent. Le corriger signifie changer la zone de collision, ce qui est un changement de sensation nécessitant son propre test de jeu, et cela penche dans la direction indulgente, donc ça peut attendre.

Tester l'équité au lieu de l'affirmer

Rien de tout cela n'est visible en jouant. On ne peut pas sentir quelques pixels de dérive, et personne ne joue la même grotte sur six appareils à la fois. Chaque affirmation a donc un test qui la rejoue : le test physique intègre une entrée fixe à des taux de rafraîchissement de 12 à 144 Hz, et un test de grotte rejoue les vrais générateurs sur six tailles d'appareil et compare le résultat octet par octet. Une propriété d'équité qu'on ne teste pas s'est déjà, d'après mon expérience, silencieusement cassée.

En résumé, TUNL est équitable entre écrans de trois façons distinctes (taux de rafraîchissement, hauteur et largeur), chacune avec une cause différente, une correction différente et un résidu différent. Tu peux voir le résultat par toi-même, sur n'importe quel appareil que tu as, dans la grotte d'aujourd'hui.

Joue la grotte du jour