Pixel-Art Motion: Walks, Cameras and Doors on iPhone
Pokémon Red, Crystal and Emerald, one game from each of the three Game Boy and Game Boy Advance generations, all walk a 16-pixel cell in 16 frames at about 59.73 hertz, 3.73 cells a second, and nothing about the step is left to a clock: Emerald moves the player exactly one pixel a frame, shows one stride drawing and one standing drawing per step, and scrolls the camera on the same frame by the same pixels.123 Red and Crystal reach the same sixteen frames by moving two pixels every second frame.4 Emerald’s door opens in four drawings held five frames each, the player is walked one forced step in, the door shuts, and the screen fades in nine stepped levels: at least 79 frames, 1.32 seconds, before the next map can begin to load.15 Kiradex’s world walks by elapsed time and eases its camera, and a model of that code (a model, not a capture from a phone) says the sprite hitches two pixels once a tile at 60 hertz, the camera falls about a pixel further behind with each tile of a walk at 60 hertz (9 pixels by the fifth, and 13 on a run) and comes to rest 4 to 9 pixels short, and the walk drawings keep a clock of their own, one cycle to 3.00 tiles where Emerald’s covers two.6 Apple gives games special priority at 30 and 60 hertz and RealityKit typically renders at 60, so the brief is a fixed 60-hertz tick with whole-pixel steps, walk drawings chosen by the distance walked (my proposal, not how the handhelds do it), a locked camera, Emerald’s door ceremony in our own art, three optional haptic events, and 60 hertz rather than 120.78 This is the canon measured from the decompilations, the iPhone side from Apple’s pages, and the brief with the checks it must pass.
TL;DR
- A step is a contract, not a speed. Every Emerald step table sums to 16 pixels: the walk is 1 pixel a frame for 16 frames (268 milliseconds a cell), running and surfing 2 for 8, the Mach Bike’s top speed 4 for 4, and only the Acro Bike’s 2-3-3-2-3-3 is uneven. Red and Crystal walk the same 16 frames in 2-pixel jumps at 30 updates a second.124
- The legs and the step are timed to match. Emerald counts the drawings and the step in frames, separately, and makes the counts agree: the walk is stride, stand, stride, stand at 8 frames a drawing over two 16-frame cells; faster gaits halve the hold rather than add drawings, so there is one footfall per 16 pixels at every speed. Turning from standing takes 8 frames (134 milliseconds), turning while walking none, and walking into a wall plays a 32-frame bump.19102
- The camera is the player. Emerald’s camera copies the player’s position and scrolls by the same pixels on the same frame, with no lag and no lead, and never stops at a map’s edge because the outside is drawn from border tiles. Game Freak wrote a camera that leads the bike and shipped it switched off.231112
- A door is a ceremony. Four drawings of five frames (335 milliseconds), a forced step of 16 frames, the door closing in 20, and a fade whose last blend lands on its 17th frame and which finishes on its 22nd: at least 79 frames, 1.32 seconds, with no input before the map can load. That confirms this series’ structures post, about 84 milliseconds a drawing, and corrects the research notes behind it, which had “4 ticks each (16 frames, about 0.27 s at 59.7 Hz).” Crystal fades to white in 8 frames; Red fades to black in 32.1513144
- On iPhone, pace evenly at 60.
preferredFrameRateRange(iOS 15.0) is a hint; ProMotion iPhones run 10 to 120 hertz in twelve steps; above 60 needsCADisableMinimumFrameDurationOnPhone; games get “special priority to 30Hz and 60Hz”; RealityKit “typically limits the refresh rate” to 60. A whole-pixel walk gains nothing at 120: each pixel is simply held for two refreshes.1571686 - Kiradex today, modeled rather than measured. The code walks at 4 tiles a second by elapsed time, so a model at a steady 60 hertz takes 15 frames a tile and moves 2 pixels on one of them; it eases and rounds the camera, which falls further behind as a walk goes on (5 pixels after the first tile, 9 after the fifth, 13 on a run) and comes to rest 4 pixels off the player at 60 hertz and 9 at 120; its six-drawing walk at 8 a second covers 3.00 tiles a cycle walking and 4.50 running. No phone has been measured.176
- What this gives Kiradex: a brief, not a shipped build. A fixed 60-hertz tick with 1-pixel steps and a stated backlog rule, walk drawings chosen by distance, a locked camera on whole pixels, the full door sequence with a stepped fade, a refused step shown, three optional haptic events, no request for 120 hertz, and for each a check that a motion log, a capture or a script can verify.
1. Emerald’s walk: sixteen pixels in sixteen frames
The first two posts in this series measured what a pixel world and its people look like; the third measured its buildings. This one measures time: how many pixels a frame, how many frames a step, which drawing shows on which frame, how long a turn, a door and a fade take, and what the camera does while all of that happens. The games are read the way the earlier posts read them, from pret’s decompilations of the published Game Boy and Game Boy Advance games, at the commits the earlier posts used: pokered d2704a6, pokecrystal 5beda23, pokeemerald 731ad5b.18 Five small scripts did the counting; each is named in the notes with its saved output.
One number first, because everything else is in frames. The Game Boy Advance draws a frame in 280,896 cycles of a 2^24-hertz clock, which is 59.7275 frames a second, 16.743 milliseconds a frame; GBATEK rounds it to “ca. 59.737 Hz”.19 The original Game Boy’s 4,194,304-hertz clock over 70,224 dots a frame gives the same 59.7275; Pan Docs says “@ 59.7 fps”.19 Milliseconds in this post use 59.7275.19
Every speed is a table that sums to sixteen
Emerald does not move a walker by speed times time. Each overworld speed is a table of per-frame pixel moves, and the source says what every table has in common: “Over the course of the step animation, these sum to 16 pixels (one full metatile).”2 sStep1Funcs is sixteen one-pixel moves, sStep2Funcs eight two-pixel moves, sStep4Funcs four of four, sStep8Funcs two of eight, and sStep3Funcs is the odd one, Step2, Step3, Step3, Step2, Step3, Step3.2 Counted by script against the movement source, that gives:
| Speed constant | Pixels per frame | Frames per cell | Cells per second | Milliseconds per cell | Used for |
|---|---|---|---|---|---|
MOVE_SPEED_NORMAL |
1 every frame | 16 | 3.73 | 267.9 | the walk; NPC walks |
MOVE_SPEED_FAST_1 |
2 every frame | 8 | 7.47 | 133.9 | running, surfing, ice slides |
MOVE_SPEED_FAST_2 |
2, 3, 3, 2, 3, 3 | 6 | 9.95 | 100.5 | the Acro Bike, water currents |
MOVE_SPEED_FASTER |
4 every frame | 4 | 14.93 | 67.0 | the Mach Bike at top speed |
MOVE_SPEED_FASTEST |
8, 8 | 2 | 29.86 | 33.5 | slide movement actions |
Source for every row: measure_gen3_motion.py over src/event_object_movement.c.1
The walk is the number to remember: one pixel on every frame, sixteen frames a cell, 3.73 cells a second, 268 milliseconds a cell, used by PlayerWalkNormal and by every walking NPC.1 Running with the B button is PlayerRun, and surfing is PlayerWalkFast, which the source annotates “same speed as running”; ice slides call the same function. All three are two pixels a frame, eight frames a cell, 7.47 cells a second.110 There is also a slow walk, UpdateWalkSlowAnim, that steps one pixel on even timer values, 31 to 32 frames a cell, for scripted scenes.1
The Acro Bike is the only speed whose pixels are uneven: 2, 3, 3, 2, 3, 3 over six frames, 2.67 pixels a frame on average, 9.95 cells a second.1 It is reached through PlayerRideWaterCurrent from AcroBikeTransition_Moving, the same function the water currents use.120 On my reading the unevenness is the price of a speed that does not divide sixteen: three pixels a frame would overshoot the cell, so the table alternates twos and threes to land on sixteen exactly. Every other speed is a whole divisor of the cell.
The Mach Bike accelerates by step rather than by frame. sMachBikeSpeedCallbacks is PlayerWalkNormal, PlayerWalkFast, PlayerWalkFaster, and bikeFrameCounter rises by one per step to a cap of 2, so the first step from standing takes 16 frames, the second 8, and every later step 4: four pixels a frame, 14.93 cells a second.120 The bike never sits between two speeds. Each step is one of the tables, run to completion, and the speed only changes at a cell boundary.
Every gait measured in Red, Crystal and Emerald is a whole number of frames per 16-pixel cell; Kiradex’s numbers are a model of its code, not a capture.14621
One footfall per sixteen pixels
The walk animation is stride, stand, stride, stand. sAnim_GoSouth is ANIMCMD_FRAME(3, 8), (0, 8), (4, 8), (0, 8): drawing 3 for eight frames, the standing drawing 0 for eight, drawing 4 for eight, the standing drawing again for eight, 32 frames in all, which is two cells of walking.91 The hold is exact: sprite.c loads the frame’s duration minus one into the delay counter and counts down to zero, so a frame of duration 8 is on screen for eight frames.91 Each step therefore shows one stride drawing and one standing drawing, and SetStepAnimHandleAlternation starts every new step at the other half of the cycle (animPos = {1, 3, 0, 2}), so the left and right legs alternate step by step even when the player stops and starts.2 This series’ first post described the same cycle from the art side: “step, stand, step, stand at eight ticks each, which is the bob everyone remembers.”22
Faster gaits keep the drawings and shorten the hold. GoFast holds each drawing four frames (a 16-frame cycle), GoFaster two (8 frames), GoFastest one (4 frames).1 The run has drawings of its own, held unevenly: sAnim_RunSouth is (12, 5), (9, 3), (13, 5), (9, 3), a 16-frame cycle.19
Put the two tables side by side and the design shows. The walk’s 32-frame cycle covers two cells of 16 frames; the run’s 16-frame cycle covers two cells of 8; the Mach Bike’s 8-frame GoFaster cycle covers two cells of 4.19 At every speed, one footfall lands per 16 pixels.1 These are two clocks, not one counter. The drawing advances when animDelayCounter, loaded from each frame’s duration in sprite.c, runs out; the step advances when NpcTakeStep indexes the speed’s table with the sprite’s sTimer, one entry a frame; and SetStepAnimHandleAlternation picks the gait’s animation when each step begins.92 What keeps them together is that both are counted in the same frames and the durations were chosen to agree, gait by gait. On my reading, that is the thing to copy: the drawings’ cadence cannot drift away from the movement, because each step’s drawings last exactly as many frames as the step does. Nothing in these tables or in my model measures where a planted foot sits against the ground, so I do not claim more than that.
Turning, bumping and when input is read
A step, once begun, finishes; the pad is read when it completes, so a held direction chains steps with no gap and a change of direction while walking costs nothing.10 From standing, it is different. CheckMovementInputNotOnBike returns TURN_DIRECTION only when the new direction differs from the facing and the player is not already moving, and PlayerTurnInPlace plays the fast walk-in-place, which InitMoveInPlace gives a duration of 8 frames: 134 milliseconds of turning on the spot.101 That is the mechanic that lets a player face a sign or a person without stepping toward them. Walking into a wall plays the slow walk-in-place instead, 32 frames, 536 milliseconds, with a bump sound: a refused step is shown, not swallowed.110 The normal and faster walk-in-place actions are 16 frames (268 milliseconds) and 4 (67).1
On my reading, those four numbers are most of what “responsive” means in a grid game. The world never moves a fraction of a cell, so the player’s intent is expressed in whole steps, and the only latency is the remainder of the step in progress: at most 268 milliseconds walking, 134 running.1 The turn from standing is not latency but a separate action with its own visible result.
2. Emerald’s camera, doors, fades and shakes
The camera has no lag and no lead
Emerald’s camera is an invisible sprite that follows the player. CameraObject_UpdateMove copies the followed sprite’s x and y and stores the difference from the last frame in sCamera_MoveX and sCamera_MoveY; CameraUpdateCallback hands that difference to CameraUpdate, which scrolls the map by exactly that many pixels.212 The overworld loop runs RunTasks(); AnimateSprites(); CameraUpdate(); UpdateCameraPanning(); in that order every frame, and because AnimateSprites runs sprite callbacks in slot order and the camera object is created after the player (InitPlayerAvatar, then InitCameraUpdateCallback(gPlayerAvatar.spriteId)), the camera reads the position the player reached on that same frame.3 I traced that order through the code rather than running it, so it is a reading, not a measurement; but the result it implies is simple. The player’s move and the scroll land on the same frame, by the same pixels. On screen, the player never moves at all while the world slides under them.
Itay Keren’s name for this, in his GDC 2015 talk on side-scroller cameras, is position-locking: the camera stays on the player, “keeping the car in focus at all times and the camera motion completely predictable.”23 Emerald adds one thing his definition leaves open, which is what happens at the edge of the map. It never stops. Cells outside a map’s layout are read through GetBorderBlockAt, which returns the layout’s 2-by-2 border metatiles repeated and marked MAPGRID_IMPASSABLE, so the view keeps the player in the same screen position and fills the outside with border trees or water.11 Keren’s edge-snapping, the alternative, “simply snaps the camera to the edge of the level, allowing the character to move away from its anchor point.”23 Emerald drew its way out of needing it.
The camera Game Freak wrote and switched off
field_camera.c contains a finished look-ahead for the bike. CameraPanningCB_PanAhead shifts the vertical pan by 2 pixels per update, from its resting value of 32 toward 72 or toward minus 8 depending on the direction of travel, which is 40 pixels either way: a camera that leads the player into where they are going.12 It runs only if gUnusedBikeCameraAheadPanback is true, the variable is only ever set to FALSE (in bike.c), and the branch carries the comment “this code is never reached.”1220 In Keren’s vocabulary it is a dual-forward-focus or target-focus, the camera that follows his rule “When you walk left, you want to see more to the left.”23 Whatever the reason it was left off, the shipped game kept the camera locked even at the Mach Bike’s 4 pixels a frame.1
A door opens in twenty frames, not sixteen
The door table in field_door.c reads as if each drawing takes four frames: sDoorOpenAnimFrames is {4, -1}, {4, 0}, {4, 0x100}, {4, 0x200}, closed plus three open drawings.24 But AnimateDoorFrame draws when its counter is 0 and advances when the counter equals the entry’s time, so each drawing is held for five updates: 84 milliseconds a drawing, 335 for the four.241 This series’ structures post gave the same figure, five updates and about 84 milliseconds.13 The research notes that post was written from had it wrong, “4 ticks each (16 frames, about 0.27 s at 59.7 Hz),” and so does a comment in the app’s own door code, which says it opens “at about Emerald’s four ticks a frame” and then holds the first drawing 70 milliseconds; both are corrected here and in the brief.1417
The entry itself, Task_DoDoorWarp, is five states with nothing skippable: freeze the other objects, play the door sound and open the door above the player; force a MOVEMENT_ACTION_WALK_NORMAL_UP step into the doorway; when the player stands still, close the door and hide the player; when the door task ends, fade the music and the screen; load the map.25 Leaving runs the ceremony backwards: Task_ExitDoor shows the door already open, waits for the fade-in, forces one WALK_NORMAL_DOWN step, closes the door, and only then gives the controls back.25
Added up from the counts above and the fade’s trace below, the door opens in 20 frames, the step takes 16 and the door closes in 20; the fade that follows makes its last blend on its 17th frame, so the screen is fully dark after at least 73 frames, 1.22 seconds. The map cannot begin to load until the fade has gone inactive, on its 22nd frame, and Task_WarpAndLoadMap has seen that and moved on: at least 79 frames, 1.32 seconds.525 The state changes between the door’s phases and the wait for the music to stop can only add frames, so both are lower bounds.5
Emerald’s entry is a lower bound from its frame counts; Kiradex’s row is read from the app’s door code, not timed on a phone.151721
A fade is nine levels
A warp fade in Emerald is not a smooth ramp. BeginNormalPaletteFade sets a step of 2 on a blend coefficient that runs from 0 to 16, UpdateNormalPaletteFade blends the background palettes on one call and the sprite palettes on the next and then steps the coefficient, and IsSoftwarePaletteFadeFinishing adds five calls at the end.26 Which frame each call lands on depends on who calls it. The door’s task starts the fade from inside RunTasks, and BeginNormalPaletteFade runs one update itself, copies the result to palette memory at once and clears the flag that would otherwise make the next update wait for the vertical blank; later in the same frame, OverworldBasic calls UpdatePaletteFade again.25263 So the first frame gets two updates, both at level 0, and every later frame one. A Python port of palette.c with that schedule, counting the task’s frame as 0, gives nine levels, 0, 2, 4 and so on to 16: the background palettes reach level 2 on frame 1 and level 16 on frame 15, the sprite palettes a frame behind each time, the last blend on frame 16 (285 milliseconds counting frame 0), and the fade inactive on frame 21 (368 milliseconds).5 A blend computed on a frame reaches the screen at the vertical blank that ends it. The port assumes no fade was running before and the normal overworld update; with rain, snow, fog, shade or drought active, the fade out runs the same way from the weather-tinted colors (FadeScreen copies the tinted buffer first and then calls the same BeginNormalPaletteFade), but the fade in is run by the weather code, and that path was not simulated.5
Nine levels two sixteenths apart, background and sprites a frame apart: the ramp the brief adapts to one veil.521
Warps fade to black, with one family of exceptions, and the exception has a direction. WarpFadeOutScreen asks GetMapPairFadeToType about the pair of map types, WarpFadeInScreen asks GetMapPairFadeFromType, and each calls FadeScreen with white when the answer is true and black otherwise.25 Both look the pair up in sTransitionTypes, whose 16 rows are every map type into and out of MAP_TYPE_UNDERGROUND; the first returns a row’s enter flag and the second its exit flag, which are true only on the rows into a cave and only on the rows out of one.27 So going into a cave the screen fades out to white and back in from black, and coming out of one it fades out to black and back in from white. Each row also names a cave transition routine of its own, which I did not trace.27 A slower white fade, FadeInFromWhite with a delay of 8, goes inactive after 86 or 87 frames, 1.44 to 1.46 seconds; it is started from the map-load callback, whose first frame I did not trace, and the port gives both cases.525
Shake is a scripted pan
Screen shake in Emerald is a script command, not a physics effect. ShakeCamera reads a vertical pan, a horizontal pan, a number of shakes and the frames between shakes from four script variables and negates the pan on each shake.28 Across the game’s scripts there are 24 calls in 9 files, and a script counted all of them:29
| Vertical px | Horizontal px | Shakes | Frames apart | Calls | Duration |
|---|---|---|---|---|---|
| 1 | 1 | 8 | 5 | 10 | 40 frames, 670 ms |
| 1 | 1 | 8 | 3 | 4 | 24 frames, 402 ms |
| 1 | 2 | 8 | 5 | 3 | 40 frames, 670 ms |
| 2 | 2 | 8 | 5 | 2 | 40 frames, 670 ms |
| 0 | 3 | 4 | 2 | 2 | 8 frames, 134 ms |
| 1 | 3 | 20 | 5 | 1 | 100 frames, 1,674 ms |
| 1 | 1 | 16 | 3 | 1 | 48 frames, 804 ms |
| 1 | 1 | 32 | 2 | 1 | 64 frames, 1,072 ms |
Source: measure_gen3_shake.py across data/**/*.inc.29
Ten of the 24 are the same shake: one pixel each way, eight flips, five frames apart, 670 milliseconds.29 The largest is 3 pixels horizontal; the longest is 100 frames, 1.67 seconds.29 The elevator’s shake, which the structures post covered (a shake every three frames for a count that grows with the floors traveled), is a separate routine and not in this count.13 On my reading, the restraint is the lesson: a shake in Emerald is a pixel or two, used for an event the script has decided matters, never for a footstep or a door.
3. Red and Crystal: the same step at 30 updates a second
Red moves two pixels every second frame
Pokémon Red has no step tables. Its overworld loop, OverworldLoop, calls DelayFrame and falls into OverworldLoopLessDelay, which calls it again, so the world updates once every two frames.30 A step sets wWalkCounter to 8, and each pass AdvancePlayerSprite decrements it and scrolls the background registers hSCX and hSCY by the step vector shifted left once: 2 pixels.30 Eight passes of 2 pixels is 16 pixels in 16 frames, the same 268 milliseconds and 3.73 cells a second as Emerald, in 2-pixel jumps at 30 updates a second.430 The bike is a second advance per pass: DoBikeSpeedup calls AdvancePlayerSprite again (except on Cycling Road while up, left or right is held), so a step takes 8 frames.430
Red’s walk drawings change every four passes, eight frames, through a cycle of four images, stand, step, stand and the step flipped, so it too shows one stride drawing per step.3122 UpdatePlayerSprite increments the intra-animation counter each pass and advances the drawing when it reaches 4.3122
Red turns in one loop pass, two frames: a new direction from standing writes the facing and returns to the loop without stepping.30 Its 180-degree turn has an intermediate facing in the code that, by the source’s own comment, nobody sees: “It is unlikely for it to ever be visible because DelayFrame is called at the start of OverworldLoop.”30 I read that from the code and did not run it in an emulator.
The warp is a sound and a fade with no door drawn. PlayMapChangeSound plays SFX_GO_INSIDE when the tile is a door (tile $0b) and SFX_GO_OUTSIDE otherwise, then GBFadeOutToBlack writes four palettes, holding each for eight frames: 32 frames, 536 milliseconds.432 I did not trace Red’s fade back in.
Crystal updates every second frame too
Crystal keeps Red’s cadence with a different mechanism. MaxOverworldDelay is db 2, and the VBlank handler counts wOverworldDelay down, so map objects update every second frame.33 StepVectors gives the walk eight updates of 2 pixels and the bike four updates of 4: 16 frames and 8 frames a cell, the same as Red and Emerald.4 The slow step is sixteen updates of 1 pixel, 32 frames, 1.87 cells a second.433
Crystal’s doors fade to white, and fast. MapSetupScript_Door begins with FadeOutToWhite and MapSetupScript_Warp ends with FadeInFromWhite; each is four palette steps two frames apart, 8 frames, 134 milliseconds each way.434 Its turn is a four-part step function, StepFunction_Turn, and the parts fall through into each other. On the first update, .init1 sets an OBJECT_STEP_DURATION of 2 and falls into .step1, which counts it down to 1; on the second, .step1 counts it to 0 and falls through .init2, which writes the new facing and sets 2 again, into .step2, which counts it to 1; on the third, .step2 reaches 0 and hands the object back to STEP_TYPE_FROM_MOVEMENT.33 Replayed instruction by instruction, the routine takes three updates, six frames at two frames an update, with the new facing written on the second. That is the routine alone, read from the code; I did not trace the time from the button press to the routine’s start.33
Three generations, three fades
| Red | Crystal | Emerald | |
|---|---|---|---|
| Door | none drawn; the sound is chosen by tile | none drawn | 4 drawings of 5 frames (335 ms), with a sliding or hinged sound |
| Into the door | the step that lands on it | the step that lands on it | a forced step up (16 frames), then the door closes |
| Fade out | to black, 4 palettes held 8 frames each: 32 frames (536 ms) | to white, 4 steps 2 frames apart: 8 frames (134 ms) | to black (to white going into a cave), 9 levels, last blend on frame 16 counting the door task’s frame as 0 (285 ms), inactive on frame 21 |
| Fade in | not traced | from white, 8 frames | from black (from white coming out of a cave), the same 9 levels |
| Out of the door | a forced step down | a forced step | the door shown open, a forced step down, the door closes, then control |
Sources: the counts in sections 1 to 3;14525 Red’s forced step out of a door, PlayerStepOutFromDoor, is from the structures post.13
The rows that agree across all three are the ones worth keeping: a fade between maps, a forced step that carries the player over the threshold, and no input during the ceremony. The fade lengths differ by a factor of four, so on my reading the length is a design choice rather than a canon. Emerald’s nine-level fade is the one built around drawn doors, and Kiradex draws its doors,13 so it is the one the brief takes.
Red, Crystal and Emerald, one game from each of the three generations on the Game Boy and the Game Boy Advance, two different machines, settled on the same contract: a step is 16 pixels, takes 16 frames at about 59.73 hertz, and cannot be interrupted once begun.14 Red gets there in 2-pixel jumps because its loop waits two frames a pass; Crystal by the same two-frame delay with 2-pixel vectors; Emerald at the full frame rate with 1-pixel moves. Running, surfing and the bike are the same contract at 8 or 4 frames.14 When people say these games feel “on rails,” I think this is the rail: the step, the drawing and the camera are all counted in the same frames, with durations chosen to agree, so they cannot drift apart.
4. The modern references: Celeste’s camera, Keren’s vocabulary, and the phone ports
When a camera should smooth, and when it should not
Emerald’s locked camera is one answer in a vocabulary that Itay Keren set out in “Scroll Back: The Theory and Practice of Cameras in Side-Scrollers,” a modified version of his talk at the Independent Games Summit at GDC 2015, published on Game Developer on May 11, 2015.23 The terms I use in this post are his. Position-locking fixes the camera on the player. Edge-snapping stops it at the level’s edge. A camera-window moves the camera only when the player pushes the window’s edge. Lerp-smoothing eases the camera toward its target, and he calls it “a standard tool in reducing jarring camera speeds, particularly jumps.” Target-focus and dual-forward-focus lead the player into the direction of travel. He also names platform-snapping, region-focus and cue attractors, which a grid walker has no use for.23
He gives the reason camera motion matters at all: “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.” And he gives the case where the plainest scheme is right: position-locking, for “a crafting adventure game like Terraria, with a small character relative to the screen with pretty small jumps, it works very well.”23 A top-down town with a 30-pixel collector, the figure this series’ characters post moved to at build 34, is that case with the jumps taken out.35
Celeste is the modern reference for the other choice. Its developers published the Player class “as a learning resource and for general interest,” with the MIT license covering that code only, and the camera is a few lines in it: level.Camera.Position = from + (target - from) * (1f - (float)Math.Pow(0.01f / multiplier, Engine.DeltaTime)), under the comment “Camera (lerp by distance using delta-time)”.3637 With a multiplier of 1 and a target that holds still, that closes 99 percent of the gap every second whatever the frame rate; Celeste’s target does not hold still, since it is recomputed from the player’s position and state every frame, so that figure describes the easing, not where the camera ends up.36 The target is the player centered in the game’s view (X - Celeste.GameWidth / 2), clamped to the room’s bounds, with offsets for a few states: 48 pixels ahead in the direction of the StRedDash dash, 64 up for the summit launch.36 The first post in this series noted that Celeste renders its world at 320 by 180 and multiplies by six.22
The two are not in conflict, on my reading. Celeste smooths because a platformer’s jumps would drag the view up and down with every leap; Keren’s own definition of lerp-smoothing is about jumps. A grid walker moves at a constant speed in straight lines, so a lock produces no jolt to smooth away, and a smoothed camera only adds a trail behind a motion that was already even. Section 7 shows what that trail measures to in Kiradex’s model.
What the other games tell us about speed
Stardew Valley states its player speed as a unitless stat: “2 when walking,” “5 when running,” “6.6 when riding a Horse (7 if the horse was fed a carrot that day),” never below 1.38 The wiki does not say what one unit is in pixels per tick, so no tiles-a-second figure is given here for Stardew.
I also looked for a primary source on the cameras of Sea of Stars, Eastward and CrossCode, and found none: interviews about traversal and lighting, nothing on the camera, and Sea of Stars’ “Pixel Perfect” option described only by guides and forums. No Maddy Thorson camera talk or article turned up either, which is why Celeste is cited from its code.39
On a phone: tap to walk, with an escape hatch
Stardew Valley’s mobile version ships nine control schemes, and its default is “Tap-to-move & Auto-Attack”: “Tap anywhere on screen and the farmer will walk to where you tapped.”40 Keeping a finger down “will cause the character to follow the touch,” and the wiki warns that follow mode “is very literal, moving directly towards the finger without routing around blocking objects.” The invisible-joystick scheme takes “the left half of the screen” with its center wherever you touch. And the wiki is honest about the default’s limit: some tasks that need careful positioning “can not be completed using the default controls; temporarily switching to a control style with a movement joystick is necessary in such cases.”40 The schemes arrived in an update TouchArcade reported on November 1, 2018, with a toggle that drops back to “the default tap-to-move and auto-attack controls.”41
Square Enix’s Pixel Remaster of the first Final Fantasy on iOS added a walk-or-run default in its version 1.2.0, dated March 11, 2025 in the app’s App Store history (the series ships as separate apps, and only this one was read): “In tap based movement mode the character controlled will always run as the default speed when moving.”42 Controller support had come to the mobile versions in an update TouchArcade covered on January 30, 2024.43 I could not find the exact touch movement scheme documented anywhere beyond those notes, nor a description of Terraria’s mobile movement on the wiki page I fetched.39
Apple’s Human Interface Guidelines say the same thing from the platform’s side. For touch games: “consider letting players tap objects to select them instead of adding a virtual selection button”; “For movement control, opt to show a virtual thumbstick wherever the player lands their thumb instead of a static thumbstick position”; “Make sure frequently used controls are a minimum size of 44x44 pt”; “Always include visible and tactile press states”; and for walking and sprinting, “consider combining the actions into a single control.” The page’s change log dates those touch-control practices to June 9, 2025.44 At WWDC25, Apple’s session on touch controls put the premise plainly: “the vast majority of players won’t have a controller available.”45
None of these sources says tap to walk is the rule for phone games, and I do not claim it. What I take from them, as my recommendation for Kiradex rather than a finding, is the scheme Kiradex already half follows: tap to walk as the default, as Stardew’s mobile version ships it; a thumbstick that appears where the thumb lands, as the HIG advises, for precision; and no fixed on-screen d-pad.4044
5. The craft, as rules with numbers
These are the rules the brief in section 7 is written against, each traced to a measurement above. “Tick” means one simulation step of 1/60 of a second, which is how a 59.73-hertz frame becomes something an iPhone can hold to; at 16 ticks a cell, the walk is 3.75 cells a second instead of 3.73.119
| Element | Rule | Source |
|---|---|---|
| Walk | 1 pixel a tick on 16-pixel tiles: 16 ticks a tile, 3.75 tiles a second | Red, Crystal and Emerald all walk 16 pixels in 16 frames, 3.73 cells a second |
| Run | 2 pixels a tick, 8 ticks a tile; no speed that does not divide 16 | Emerald’s run and surf are 2, the bike 4; only the Acro Bike’s 2-3-3 is uneven |
| Walk cycle | one footfall per 16 pixels at every speed; my proposal for Kiradex is to choose the drawing by distance walked, for six drawings over 32 pixels drawing floor(distance × 6 / 32) mod 6 |
Emerald times drawings and steps in frames, separately, with matching lengths: its walk is a 32-frame cycle over two 16-frame cells, its run a 16-frame cycle over two 8-frame cells |
| Step | once begun, it finishes; input is read at the tile | Emerald and Red read the pad when the step completes |
| Turn | 8 ticks, about 133 ms (Emerald’s 8 frames are 134), from standing; none while walking | Emerald WalkInPlaceFast is 8; Crystal’s turn routine 6 frames; Red’s 2 |
| Refused step | shown, not ignored: a 32-tick walk in place with a bump | Emerald WalkInPlaceSlow is 32 |
| Camera | locked to the player: no lag, no lead, moved in the same tick by the same pixels | Emerald’s camera object; the one lead Game Freak wrote is switched off |
| Map edge | draw the outside and keep the lock, or clamp (edge-snapping); never ease | Emerald’s border tiles; Keren’s edge-snapping; Celeste’s room bounds |
| Door | 4 drawings held 5 ticks each: 83 ms a drawing, 333 ms to open (Emerald’s 84 and 335) | Emerald field_door.c |
| Fade | 9 stepped levels, one every 2 ticks, the last on tick 16, 18 ticks (0.30 s) in all, out and in; black by default. An adaptation, not a copy | Emerald’s normal fade reaches each level on the same even frames in its sprite palettes, a frame after its background palettes, makes its last blend on frame 16 and goes inactive on frame 21; going into a cave it fades out to white, and coming out it fades in from white; Crystal’s 8 frames is the fast end, Red’s 32 the slow end |
| Entry ceremony | 74 ticks, about 1.23 s, with no input: open 20, step in 16, close 20, fade 18 | Emerald’s door entry: dark after at least 73 frames, the map load no earlier than frame 79 (1.32 s) |
| Shake | 1 pixel, 8 flips, 5 ticks apart (40 ticks, 0.67 s), for scripted events only | the commonest of Emerald’s 24 shakes |
| Frame rate | simulate at 60 whatever the display does; request 60, not 120 | Apple’s game priority at 30 and 60; RealityKit typically renders at 60 |
| Haptics | confirm events, not steps; make them optional | Apple’s HIG on playing haptics |
| Input | my recommendation: tap to walk by default; a floating thumbstick as the precision option; no fixed d-pad | Stardew mobile’s default, the HIG’s floating thumbstick; not a rule either states |
Sources for the table: Emerald’s walk, animation and door counts;1 its separate drawing and step timers;92 its camera;123 its fade and shakes;529 Red and Crystal;433 Keren and Celeste;2336 Apple’s frame pacing, RealityKit and haptics guidance;7846 the input references.404244
Two of those rules need a sentence each. The walk-cycle rule is the one that is easiest to get wrong in a modern engine, because an animation system counts its own seconds while a walk counts the world’s distance. The handhelds kept the two together by counting both in the same frames with lengths chosen to agree; on a phone, where the frame time varies, my proposal is to read the drawing from the distance walked, which gives the same result without a second clock to keep in step. And the frame-rate rule is not a lack of ambition. A world that moves one whole pixel per tick has nothing to show between ticks, so a faster display can only repeat the same picture, and section 6 shows that this is exactly what it does.
6. The Apple way: frame pacing, RealityKit’s clock, and haptics
The first post in this series set out the engine Kiradex’s world runs on: a RealityKit scene used as a 2D renderer, an orthographic camera, the ground as one mesh of textured quads, walkers as quads, and every sprite’s position rounded to whole world units each frame.22 This section is the part of Apple’s documentation that decides how that world moves in time. Every page below was read as Apple publishes it on October 4, 2026, and the availability given is the one each page states.
Frame pacing on ProMotion
CADisplayLink.preferredFrameRateRange (iOS 15.0, iPadOS 15.0, Mac Catalyst 15.0, visionOS 1.0) is a request, not a setting.15 The page’s advice is “Choose a frame rate range that your app can consistently maintain,” and it describes what the system does with the request: “The system typically provides a consistent frame rate by choosing one that’s a factor of the display’s maximum refresh rate.” By default the range equals the display’s maximum.15 The range itself is a CAFrameRateRange (iOS 15.0) of a minimum, a maximum and a preferred rate.47
Apple’s article on ProMotion gives the numbers. ProMotion displays switch between 24 and 120 hertz on iPad Pro and between 10 and 120 on supported iPhones, and the iPhone’s rates are twelve steps: 120, 80, 60, 48, 40, 30, 24, 20, 16, 15, 12 and 10 hertz; the iPad Pro’s are five of them.7 The device list now names iPhone Air and “iPhone 17 and later” alongside “iPhone 13 Pro and later.”7 On iPhone, nothing above 60 happens unless the app’s Info.plist sets CADisableMinimumFrameDurationOnPhone (iOS 15.0) to true: “If you don’t enable this support, Core Animation won’t access higher frame rates (above 60Hz).”716 Two sentences in the article matter more to a game than the rest. One: “In iOS 15 and later, the system provides games with special priority to 30Hz and 60Hz refresh rates to ensure optimal performance,” reached with CAFrameRateRange(minimum: 30, maximum: 60, preferred: 60).7 The other: “Prepare your app to operate at any refresh rate, not just those it requests.”7 And for anything that animates: “Always use targetTimestamp to drive any animation, physics, or other time-related content provided in your CADisplayLink callback” (targetTimestamp is iOS 10.0).748
The WWDC21 session “Optimize for variable refresh rate displays” covers both ProMotion on iPad Pro and Adaptive-Sync displays on the Mac.49 For the Mac’s Adaptive-Sync displays it changes Apple’s earlier guidance: on a fixed-rate display, “we’ve previously recommended that you slow down your rendering to hit the next factor of the display’s fastest refresh rate”; on Adaptive-Sync, “You should instead attempt to present frames at the highest rate your app can do so evenly.”49 The word I take from it for a phone is evenly.
RealityKit’s clock
Kiradex’s world does not own a display link. It ticks on RealityKit’s per-frame event, SceneEvents.Update (iOS 13.0), “An event invoked once per frame interval,” whose deltaTime is “The elapsed time since the last update.”5051 RealityView (iOS 18.0) documents exactly that route for per-frame work, “you can use a System or directly subscribe to the engine’s SceneEvents.Update,” and offers no frame-rate setting of its own.52 Apple’s RealityKit performance article says what rate to expect: “RealityKit typically limits the refresh rate,” which it defines as the rate at which the framework renders updates for the screen, “to 60 frames per second (fps).”8 Whether RealityView on an iPhone 18 Pro Max or an iPhone Duo actually renders at 60 or 120 is not something I have measured, and section 7 makes that measurement the brief’s first check.
Why 120 hertz does nothing for a whole-pixel walk
Here is the arithmetic, from the same model script that section 7 uses for the app’s current code, applied to the proposal instead: a world that steps on a fixed 60-hertz tick, one pixel a tick, with the display showing whatever the latest tick produced. On a 60-hertz display, every world pixel is shown for exactly one refresh.6 On a 120-hertz display, over one second, 59 of the 61 positions are held for exactly two refreshes and two for one.6 On an 80-hertz display the holds alternate between one and two refreshes, 42 positions held once and 19 twice.6 On my reading, the 120-hertz case is the 60-hertz case drawn twice, and the 80-hertz case is uneven pacing: a pixel that sometimes stays for 12.5 milliseconds and sometimes for 25.
So the question for a grid world on an iPhone is not 120 hertz but even pacing at 60: a fixed 60-hertz simulation tick, integer moves per tick, animations counted in ticks, and the renderer showing the latest tick. That is also what makes the motion independent of whatever rate RealityKit picks. The WWDC21 session makes the related point about time deltas. When a slow frame makes the display link skip a callback, the delta to advance by is “not 8ms, but rather 16ms”; the session then says that an app which “uses time delta to advance the state of your custom drawing” will “slow down your custom drawing by one frame” every time a callback is skipped, which I read as an app that advances by the expected 8 milliseconds rather than the time actually elapsed, and it says you “can instead keep track of a previous targetTimestamp so that you can advance the state correctly.”49
Haptics
Core Haptics (iOS 13.0, iPadOS 13.0, Mac Catalyst 13.0, visionOS 1.0) builds a CHHapticPattern, “An object representing a haptic waveform,” from dictionaries, from arrays of CHHapticEvent objects, or from an AHAP file.53 Events are of two haptic types, hapticTransient and hapticContinuous; transient ones are “brief impulses that occur at a specific point in time.”54 Each takes the parameters hapticIntensity, hapticSharpness, attackTime, decayTime, releaseTime and sustained.55 A CHHapticEngine plays them, and capabilitiesForHardware() says whether the device can.56
The simpler route is UIImpactFeedbackGenerator (iOS 10.0), “A concrete feedback generator subclass that creates haptics to simulate physical impacts,” whose styles describe “The mass of the objects in the collision” (light, medium, heavy, soft, rigid); impactOccurred(intensity:) is iOS 13.0, and the page lists init(style:view:) under “Initializing the feedback generator” and init(style:) under Deprecated.575859 prepare() lowers latency only with time to work: “Calling prepare() and then immediately triggering feedback (without any time in between) does not improve latency,” and the engine returns to idle after “A short period of time passes (typically seconds).”60 In SwiftUI, sensoryFeedback(_:trigger:) (iOS 17.0) “Plays the specified feedback when the provided trigger value changes,” including .impact(weight:intensity:).61
The HIG’s page on playing haptics is the design half. “Avoid overusing haptics,”46 with the reason: “Often, the best haptic experience is one that people may not be conscious of, but miss when it’s turned off.” “Make haptics optional.” Match “the intensity and sharpness of a haptic with the intensity and sharpness of the animation it accompanies.”46 Sharpness can convey an experience “that’s soft, rounded, or organic, or one that’s crisp, precise, or mechanical.”46 And custom haptics can make “a collision or a hit” feel very different “from subtle experiences like the approach of footsteps or a looming danger.”46 A walk of 3.75 steps a second for minutes at a time is the case the first rule is about.1
Input
The touch-control APIs are in section 4’s terms. Touch Controller (iOS 26.0, iPadOS 26.0, Mac Catalyst 26.0, visionOS 26.0) is summed up on its page as “Integrate onscreen touch controls into your Metal-based games”: buttons, direction pads, thumbsticks, throttles and touchpads, surfaced through a GCController.62 Its TCDirectionPad (iOS 26.0, iPadOS 26.0, Mac Catalyst 26.0) can be configured “to behave as either a composite direction pad” or “as four separate buttons.”6263 The older GCVirtualController (iOS 15.0) is “A software emulation of a real controller that you configure specifically for your game.”64 One address I tried, developer.apple.com/documentation/touchcontrols, returned a 404; the framework’s page is touchcontroller.39
What it costs
Nothing in this section has been timed on a phone. The world’s frame time on device, the cost of a 60-hertz accumulator, and the cost of a haptic engine kept running while the world is on screen are all unmeasured; the brief’s checks in section 7 are written to measure the first two.
7. The brief: what Kiradex builds and the checks it must pass
Where the walk stands today
I read the world’s walking, camera and transition code in the Kiradex repository on October 4, 2026, without changing it, and wrote a frame-by-frame model of it. Everything in this subsection is either read from that code or computed by the model, and it says which. The model does each of the code’s Float operations in 32 bits, in the code’s order, with Swift’s rounding, at an exact 1/60 or 1/120 of a second per frame, and leaves out the map’s edges, the fold of the iPhone Duo and the feet offset, none of which change a straight walk in the middle of the map. It is a model of the code. It is not a capture, and no phone has been measured.176
What the code does, read from it:
- Input is tap to walk, and only that. The stage turns a tap into a world point, then taps a collector, the kiosk, or calls
walk(to:). There is no drag, no d-pad, and no haptic call anywhere in the app’s target: a search forUIImpactFeedbackGenerator,sensoryFeedbackandCHHapticfinds nothing.17 - The path is an eight-way shortest-path search, ordered by the cost walked so far with no estimate of the distance left, which makes it Dijkstra’s search rather than A*; diagonals cost √2 and never cut a wall’s corner. A diagonal step faces sideways and divides its progress by √2 after the run’s 1.6 is applied, so in the model a diagonal step takes 22 frames walking and 14 running at 60 hertz, against 15 and 10 for a straight one.176
- Speed is time.
tilesPerSecondis 4, which is 64 pixels a second, and the walk becomes a run at 1.6 times that when the path is 6 steps or more, so a walk in the app is 1 to 5 tiles and anything longer is run. Each frame addsdt × speed / lengthto a step’s progress, and when progress reaches 1 the walker snaps to the next tile and progress goes back to 0, discarding the overshoot.17 - The walk drawing is chosen by time. The column is
cycle[Int(walker.clock * framesPerSecond) % count], withframesPerSecond8 and the forge’s six-drawing walk and run cycles. This series’ characters post reported the same cadence, “the walk at eight frames a second,” and that describes the code accurately.1735 - The camera eases, then rounds. Each frame it moves
(target - current) * min(1, dt * 6)toward the player and then rounds both axes to whole units; it is clamped to the map when the map is larger than the view. The first post’s sentence, that every sprite and the camera “are rounded to whole world units each frame, after easing,” is also accurate.1722 - Doors and warps. The door shows the sheet’s half-open drawing at once and the open one after 70 milliseconds, and removes the door 1.4 seconds later; the step onto the warp waits 220 milliseconds and then changes place. The structures post described this as a known gap.1713
- Transitions are navigation pushes with their default animation; a floor change swaps the stage by identity with no fade; leaving calls
dismiss(). The only veil in the app is the card viewer’s black overlay, animated with.easeOut(duration: 0.25).17 - No frame-rate request. No
CADisplayLink,preferredFrameRateRangeorCADisableMinimumFrameDurationOnPhonein the app or its project file; the world ticks onSceneEvents.Updatewith the event’sdeltaTime.17
What the model says that code does on screen, at a steady frame time:
- A two-pixel hitch once a tile at 60 hertz. At 60 hertz a walk takes 15 frames a tile, 4.00 tiles a second, and each tile’s 16 pixels come as 14 frames of 1 pixel and one frame of 2: one 2-pixel jump per tile, five in a five-tile walk. Emerald moves exactly 1 pixel on every frame.61
- Irregular 0s and 1s at 120 hertz. At 120 hertz the walk takes 30 frames a tile and each tile’s 16 pixels come as 16 frames of 1 pixel and 14 of none, irregularly interleaved.6
- The run is slower than its constant. At 60 hertz the run takes 10 frames a tile, 6.00 tiles a second, where 4 times 1.6 would be 6.4, because each tile’s overshoot is thrown away; each tile’s 16 pixels come as 4 frames of 1 pixel and 6 of 2. At 120 hertz it takes 19 frames a tile, 6.32 tiles a second, so the run’s speed depends on the frame rate.6
- A trailing camera that never arrives. While walking at 60 hertz the camera falls about a pixel further behind with each tile, from 5 pixels at the end of the first tile to 9 at the end of the fifth, the longest walk the app makes, and the player’s position on screen changes on 8 of the walk’s 73 frames after the first. On a run, which is every path of 6 tiles or more, it trails by 13 from the second tile on. At 120 hertz it reaches 9 within the first tile, walking or running, and then holds. When the player stops, the camera comes to rest 4 pixels off the player at 60 hertz and 9 at 120, and stays there: once the gap times
min(1, dt × 6)is under half a pixel, rounding returns the same position every frame. Which side it rests on depends on the direction of the last walk. Emerald’s trail and rest offset are both zero.6 - The drawings keep their own clock. The six-drawing cycle at 8 a second lasts 0.75 seconds. Measured from the model’s positions between the starts of successive cycles at 60 hertz, it covers 3.00 tiles walking and 4.50 running (4.80 at the run’s nominal 6.4 tiles a second, which the dropped overshoot never lets it reach); Emerald’s cycle of two footfalls covers 2.00 tiles. On my reading, a cycle that covers half again as much ground as its footfalls shows as feet sliding, but the model does not measure where a foot sits on the ground. The drawing also sits on a knife edge every 15th frame, at 60 and at 120 hertz, where the clock times 8 lands exactly on a whole number, the boundary between two drawings: keeping the clock in 32 bits, as the app does, rather than 64 changes which drawing shows on 9 of the 11 such frames in 12 tiles of walking at 60 hertz and 14 of 23 at 120.6
- A second tap mid-step snaps the walker back. This one is read from the code, not modeled or captured:
walk(to:)sets the progress to 0 while the walker’s tile is still the step’s origin, so the next frame draws the walker up to 15 pixels behind where it was; other collectors’ moves from the server do the same.17
Pixels per displayed frame: the canon is even, today’s code (a model, not a capture) is not, and a fixed tick is even at 120 too.621
The eased, rounded camera, as modeled: it trails while walking or running and stops short when the walk ends.621
None of this is a judgment on how it feels in the hand, because no phone has been measured. Real frame times jitter, which will change the exact pattern of 1s and 2s. The trail and the resting offset depend on the frame time too, because the easing step min(1, dt × 6) does, which is why the model gives 4 pixels at rest at 60 hertz and 9 at 120. On my reading, jitter within the range a phone normally shows will change their size, not remove them; the condition is that frames stay shorter than 1/6 of a second, about 167 milliseconds, because at that length the easing factor reaches 1 and the camera lands on the player in one frame, so a long enough hitch closes the gap on that frame.176 The mismatch between the drawings and the walk does not depend on the frame rate: a drawing clock of 8 a second against a walk of 4 tiles a second gives 3.00 tiles a cycle at 60 hertz and about the same at 120. The run’s does, 4.50 tiles a cycle at 60 hertz and 4.75 at 120, because the overshoot it drops at each tile does.6
The brief
Each item is a change to Kiradex/World/, the reason for it, and a check that a log line, a script or a capture can verify. No asset, name or sound from any of the games is proposed: the numbers are mechanics, and the art, sounds and words are Kiradex’s own.
1. Walk on a tick, not a clock.
Change. Add a 60-hertz accumulator to the rig’s update. Each callback adds its dt; if the accumulator then holds more than 8 ticks of time (133 milliseconds), the excess is dropped and written to the motion log as a dropped line with its milliseconds; then every whole tick in the accumulator runs, each subtracting one tick. The accumulator counts time in whole integer units (nanoseconds, or the model’s 1/60,000,000 of a second), never in a floating-point second: with a Double or Float accumulator the 600th tick of a ten-second test lands one rounding short of its boundary at some rates and the count comes out 599. That is the backlog rule: up to 8 ticks of lateness is made up on the next callback, and anything beyond is treated as a suspension, so the world resumes where it stopped rather than racing to catch up. Eight is chosen to cover every rate on Apple’s list for ProMotion iPhones, down to 10 hertz, which needs 6 ticks a callback.7 In the model, ten seconds of callbacks at each of the twelve rates runs exactly 600 ticks and drops nothing, where a cap of 4 ticks a callback would run 480 at 12 hertz and 400 at 10.6 After a one-second hitch at 60 hertz, the rule runs 8 ticks on the next callback and 1 on each after it, and logs 866.7 milliseconds dropped; the same hitch with the backlog kept under a cap of 4 runs 4 ticks on 19 callbacks in a row, the fast-forward the rule exists to prevent.6 Each walker keeps a step tick count instead of a fractional progress, and its drawn offset is that count times its pixels per tick: exact integers, so walkers no longer need rounding. The walk is 1 pixel a tick, 16 ticks a tile; the run is 2 pixels a tick, 8 ticks, keeping today’s rule that a path of 6 steps or more is a run. Diagonal steps, which the path search allows, keep the √2 the code already intends and its order, the run’s pace applied before the division by √2: a walking diagonal takes 23 ticks (16√2 is about 22.6, rounded up) and a running diagonal 12 (8√2 is about 11.3, rounded up), each moving 16 pixels on each axis on the ticks where floor(16 × t / 23) or floor(16 × t / 12) changes.17 Walking, that is 1 pixel on 16 of the 23 ticks and none on the other 7; running, 1 pixel on 8 of the 12 ticks and 2 on 4, the one uneven gait in the brief, as the Acro Bike’s is the one uneven gait in Emerald.1 There is no overshoot to discard.
Why. The rules for walk and run in section 5; and the model’s 2-pixel hitch per tile at 60 hertz and irregular 0s and 1s at 120.61
Checks. A -motionLog launch argument logs the tick and the player’s x, y on every tick, and every dropped line. On the existing facing demo, which walks one tile each way, a script asserts that every walking tick moves exactly 1 pixel, each tile takes 16 ticks, and no tick moves 2. A diagonal demo, one walking and one running diagonal step each way, asserts 23 ticks and 12, 16 pixels on each axis per step, per-axis moves of 0 or 1 pixel walking and 1 or 2 running, and a running diagonal faster than a walking one. Unit tests feed the accumulator made-up dt sequences as exact integer units: ten seconds at each of the twelve iPhone rates from 120 down to 10 hertz runs 600 ticks with nothing dropped, and a one-second gap at 60 hertz runs 8 ticks on the next callback, then 1 a callback, with 866.7 milliseconds logged as dropped. The same motion log on an iPhone 18 Pro Max and on the inner display of an iPhone Duo gives the same ticks per tile: the display’s rate must not change the walk.
2. Choose the walk drawing by distance.
Change. This item is my proposal, not a copy: Emerald times its drawings in frames, matched to its steps, and does not read them from distance.92 While walking, choose the column from the walk’s progress counted in steps, the steps completed since the walk began plus the current step’s ticks over its length: walkCycle[floor(steps × walkCycle.count / 2) % walkCycle.count], two footfalls per two steps, and the same for the run. On a straight step that is the distance walked over 32 pixels, floor(distancePx × 6 / 32) mod 6. On a diagonal it counts the step rather than its √2 length, so a footfall still lands at the start of every step with a longer stride, my choice for drawings made for straight strides. Show the standing drawing when the walk ends, as Emerald does. Keep the idle, blink and emote clocks as they are. A tick-timed cycle could match the steps too, as Emerald’s does, but six drawings over 32 ticks would need uneven holds of 5 and 6 ticks; counting progress needs no second table and holds for any gait the app adds.
Why. One footfall per 16 pixels at every speed, and a cadence that cannot drift away from the movement; today’s cycle covers 3.00 tiles walking and 4.50 running in the model.16 This also settles the characters post’s “eight frames a second”: at 1 pixel a tick, six drawings over 32 pixels change every 5.33 pixels, which is 11.25 drawings a second, not 8.35
Check. From the motion log plus the column shown: the two contact drawings (walk_a and walk_d in the forge’s cycle) appear at 0 and 16 pixels, give or take one, of every 32, at 60 and at 120 hertz; on the diagonal demo they appear on the first tick of each step.
3. Finish the step; keep the tap; add a floating stick.
Change. A tap mid-step plans from the tile being walked to, not from the origin, and the current step finishes first; the step count is never reset. Other collectors’ server moves get the same rule. From standing, a tap whose first step changes facing turns for 8 ticks before moving. A tap on a blocked cell next to the player, or on the tile in front of one, turns to face it and plays a 32-tick bump with the refusal haptic of item 6. Holding a finger down follows it, as Stardew’s default does, but routed by the existing path search each time the finger crosses a tile, since Stardew’s follow does not route around obstacles and ours can. A floating thumbstick, four-way, off by default, appears where the thumb lands, built on a SwiftUI drag gesture over the world; anything drawn is at least 44 by 44 points. Touch Controller’s thumbstick (iOS 26) replaces the drag only if a test build shows it drawing over the RealityView: Apple describes the framework as for “Metal-based games,” its WWDC25 session says it “integrates directly with Metal,” and Kiradex’s world is drawn by RealityView with no render pass of its own.4562
Why. The step, turn, refused-step and input rules of section 5.14044624517
Checks. A UI test taps a tile 6 east, then 120 milliseconds later a tile 6 north; the motion log shows no tick on which the player’s x decreases, and the first step’s tile is completed before the turn. A UI test taps the wall beside the house; the log shows a facing change and a 32-tick bump and no change of position. With the stick on, a drag held right for 60 ticks from the first tick of movement completes three tiles by tick 48, starts a fourth while the stick is still held, and finishes it on tick 64 after the release: the log shows the player 4 tiles east, on a whole tile, with the step in progress at the release completed and no stop between tiles.
4. Lock the camera.
Change. Replace the easing with a camera set in the same tick as the player moves, from whole world pixels only: camera = clamp(player + foldOffset, low, high). Each term is kept whole, because today only the final rounding makes the camera whole: the bounds and the fold offset are fractional. The player’s position is whole by item 1. The fold offset, which follow computes in points times displayScale / pixelScale, is rounded to whole world pixels once, when the fold changes. The clamp’s bounds are rounded inward, the lower up and the upper down, because the view’s half-size in world pixels need not be whole: fit divides the view’s screen pixels by a whole number of screen pixels a texel, so an illustrative 393-by-852-point view at 3x gets 7, a view 168.43 world pixels wide and a half-width of 84.21, and the camera’s bounds become 85 and the map’s width minus 85, so no frame shows past the map’s edge.176 When the map is smaller than the view the bounds swap, as today’s clamp allows, and the same inward rounding keeps the whole map in view. Keep the clamp to the map; the section 5 rule allows clamping or drawing the outside, and Kiradex’s camera goes past the map’s edge into the woods only when the Duo is half open, by a margin of whole tiles.17 Treat the fold offset as fixed until the fold changes; when it changes from a to b, step it through a + round((b − a) × i / 8) for i from 0 to 8, one level every 2 ticks, the fade’s cadence, so every position on the way is a whole pixel; a change from 0 to minus 37, for example, passes 0, −5, −9, −14, −19, −23, −28, −32 and −37.6 Keep the woods’ parallax, computed from the locked camera and rounded as today. No look-ahead toward the tapped destination: Game Freak built one and left it off, and a tap-to-walk destination is already on screen.12
Why. The camera rule of section 5, and the model’s trail, which grows through a walk to 9 pixels by its fifth tile and is 13 on a run, its changes of the player’s place on the screen, and its 4 and 9 pixel resting offsets.6
Checks. From the motion log, the camera minus the player is constant on every tick of a walk in the open plaza (zero, or the fold offset) and the same after the player stops, and every camera value is a whole number. A unit test calls fit with the 393-by-852-point view at 3x, walks the player to both edges of a map, and asserts whole camera positions that stop at 85 and at the map’s width minus 85; the same test then changes the fold and asserts that the camera reaches the new offset through 9 whole-pixel positions, 2 ticks apart, and that no tick in between leaves the bounds. For a capture, the walk drawings will not do as a marker, because the feet change shape from drawing to drawing; a debug build draws a one-texel marker, in a color used nowhere else, at the player entity’s position, and a script finds it in every frame of a 6-tile walk in the open plaza: the same screen pixel in every frame. Near the map’s edge the marker moves and the camera does not, by whole pixels only.
5. The door, the step and the fade, in Kiradex’s own art.
Change. Entering a door, which is a warp with a door sheet. Today the door opens after the walker has landed on the door cell: the step’s onStep finds the warp at that tile and calls openDoor there.17 Emerald never lets the player stand in a closed door: TryDoorWarp fires only when the player, on the cell below, pushes north into a door, and Task_DoDoorWarp opens the door one cell up and then forces the step onto it.6525 The brief moves the trigger back one cell to match:
- When the path’s next step is onto a door warp, the walk stops on the cell before it, and the entry starts there, on tick 0. Freeze input and play the door’s sound, a Kiradex sound.
- Open the door in four slots of 5 ticks, Emerald’s four drawings at its five frames each (83 milliseconds a slot at 60 ticks a second, where Emerald’s five frames are 84; 20 ticks in all), in place of today’s half-open drawing at once and open at 70 milliseconds: closed, half, open, open. Kiradex’s door sheet has three drawings, closed, half open and open (the forge’s
door_sheet()draws three, and the app loads it withSpriteSheet.bundled(name, columns: 3, rows: 1)), where Emerald’s door is closed plus three, so the open drawing holds through the fourth slot. A fourth drawing from the forge, between half and open, would fill that slot instead; it is optional art, not a requirement. Fix the comment that says “four ticks.”2417 - Walk the player one forced step from that cell onto the door cell, 16 ticks. The town’s doors sit in a building’s bottom row with the building’s cells on both sides and above, and the path never cuts a wall’s corner, so this step is always up from the cell below, as Emerald’s is.17
- Hide the walker and close the door, the slots backwards (open, open, half, closed), 20 ticks.
- Fade a black veil over the world in 9 stepped opacity levels (0, 2/16 and so on to 16/16), level
ion the fade’s tick2i, so the last level lands on tick 16 and holds through tick 17: 18 ticks. That is an adaptation, chosen, not Emerald’s timing. Emerald’s sprite palettes reach each level on the same even frames as this veil, its background palettes a frame earlier, and after the last blend on frame 16 it runs five finishing updates before going inactive on frame 21; one veil has no second layer to lag and nothing to finish.5 Never animate the opacity of the view that holds the RealityView itself; the card viewer taught the first post that lesson.22 - Push the destination with animations disabled, and fade the veil back out in the same 9 levels.
- Arrive on the inside mat facing up. Leaving reverses it: arrive on the door cell with the door open, a forced step down of 16 ticks, the door closes, then input.
Floors, which today swap the stage with no fade, take the same veil with no door and no forced step. Leaving a room, which today calls dismiss(), takes the veil and the forced step out.
Why. The door, fade and ceremony rules of section 5; today the place changes 220 milliseconds after the step, the door shows two drawings 70 milliseconds apart, and the screen slides sideways.1517
Checks. The motion log counts ticks from the one on which the walk stops below the door. It shows the player still on that cell, and the door’s slots at ticks 0, 5, 10 and 15 (closed, half, open, open); the forced step on ticks 20 to 35, 1 pixel a tick, reaching the door cell on tick 35 and never before; the closing slots at ticks 36, 41, 46 and 51; the veil’s first level at tick 56; and the push no earlier than tick 74. A walk that ends on a door cell without this sequence fails the check. The motion log also records the veil’s opacity on every tick: 9 distinct values, each held 2 ticks, on ticks 56 to 73 on the way out, and the same 9 on the way in. A screen recording confirms it on a part of the picture that holds still: the whole frame will not do, because the ground’s water, on a map that has it, changes drawing 4 times a second and other collectors may walk, so a script samples a patch of building wall chosen in advance, with no animated tile and no walker over it in the recording, and counts 9 distinct levels on the way out and 9 on the way in, each held 2 frames, give or take one at 60 hertz.17 In a screen recording of a warp, the world never translates horizontally: no navigation slide.
6. Haptics: three events, and a switch.
Change. One CHHapticEngine owned by the world, started when it appears and stopped when it disappears, with a Haptics switch in settings that is on by default and respects the system’s haptics setting. The patterns, kept as AHAP files in the bundle:
| Event | Pattern | Why |
|---|---|---|
| Refused step (the bump) | one hapticTransient, intensity 0.4, sharpness 0.2 |
the HIG’s impact is “a thud when two heavy objects collide”;46 soft, because the art is soft |
| Door opens | a hapticTransient at 0.3 intensity and 0.6 sharpness on each slot that changes the picture as it opens, the half-open drawing at tick 5 and the open one at tick 10 (83 and 167 ms) |
match the animation it accompanies |
| Card raised (the 3D card) | a hapticTransient at 0.7 and 0.8 when it reaches the top |
the one real object in the world |
| Footsteps | none by default | 3.75 steps a second for minutes is the overuse the HIG warns about |
The intensity and sharpness values are my starting points, not measurements, and they are for tuning on a device. UIImpactFeedbackGenerator(style: .soft, view:), with prepare() called when a walk starts, is the fallback where capabilitiesForHardware() says Core Haptics is unavailable.
Why. The haptics rule; Apple’s guidance quoted in section 6.46565760
Check. A -hapticLog argument logs each event. A scripted door entry logs exactly 2 door events, at ticks 5 and 10 of item 5’s count, and none per step; with the switch off, none at all.
7. Frame rate: 60, evenly.
Change. None to the Info.plist: do not add CADisableMinimumFrameDurationOnPhone, because the world gains nothing from 120. Keep the RealityView; the tick of item 1 makes the motion independent of whatever rate it renders at. If a display link is ever added, set CAFrameRateRange(minimum: 30, maximum: 60, preferred: 60) for Apple’s game priority.76
Checks. Log a histogram of SceneEvents.Update.deltaTime over a 60-second walk on an iPhone 18 Pro Max and on an iPhone Duo, outer and inner displays. The brief assumes the mode is 16.7 milliseconds; if it is 8.3, item 1’s accumulator still holds the walk at 16 ticks a tile, and the log proves it. The motion log’s dropped lines: none in a normal walk, at either rate.
8. Shake, for one moment only.
Change. A camera shake of whole texels, 1 texel, 8 flips, 5 ticks apart, used for one event that earns it, for example a rare card’s reveal in the plaza, with the card-raised haptic. Not for doors, steps or arrivals.
Why. The commonest of Emerald’s 24 shakes, and their restraint.29
Check. The motion log shows the camera offset alternating plus and minus 1 texel at ticks 0, 5 and so on to 35, and back to 0 at tick 40.
Not in this brief
- Camera smoothing or look-ahead on the grid. There are no jumps to smooth, and Game Freak shipped its look-ahead switched off.1223
- 120 hertz for the world. Even pacing at 60 is the goal; 120 shows each pixel twice.6
- Haptics per step by default.46
- A fixed on-screen d-pad. My recommendation is tap to walk plus an optional floating stick.44
- Any of the games’ door or warp sounds or jingles. Sounds are expression, and Kiradex needs its own.
- Bikes, surfing, ice and currents. The app’s walkers walk and run and nothing else, and the speeds above are recorded for when that changes.17
The open question: timing on a device
Every model number in this section assumes a steady 1/60 or 1/120 of a second between frames. Whether RealityView on an iPhone 18 Pro Max or an iPhone Duo renders at 60 or 120, and how much its deltaTime jitters, is unmeasured; item 7’s histogram is the first thing to run, and item 1 is written so that the answer changes nothing about the walk at any rate on Apple’s iPhone list.687
Key Takeaways
If you draw the art
- Draw the walk for a distance. Emerald’s walk is two strides and two stands over 32 pixels, held in frames that match its steps, and faster gaits reuse the drawings with shorter holds rather than adding frames, so one footfall lands per 16 pixels at every speed.192
- A six-drawing cycle is fine, provided the engine plays it over a fixed distance; played at a fixed rate against a walk timed separately, its cadence drifts from the movement, 3.00 tiles a cycle walking and 4.50 running in the model of ours.6
- Emerald’s door is closed plus three drawings, each on screen for 84 milliseconds. A sheet of three, closed, half and open, as Kiradex’s is, fills the four slots by holding the open drawing for two of them, or the forge draws a fourth; either way, draw the open state as something worth seeing.12417
If you build the engine
- Step the world on a fixed tick with whole-pixel moves that divide the tile, read input at the tile, and let the renderer show the latest tick. Say what happens to a backlog: ours makes up to 8 ticks and drops the rest, logged. Emerald’s step tables are the model: every one sums to 16.216
- Lock the camera to the player in the same tick, with whole-pixel bounds and offsets, so nothing needs rounding afterward. Easing then rounding trails a walking player and, in the model of our code, stops 4 to 9 pixels short until the next walk.36
- On iPhone, do not ask for 120 hertz for pixel motion. Apple prioritizes 30 and 60 for games, RealityKit typically renders at 60, and a whole-pixel walk at 120 only holds each pixel for two refreshes.786
- Fade in steps, not a smooth ramp. Emerald’s door fade is nine levels two sixteenths apart, its last blend on the 17th frame and the fade finished on the 22nd; our 18-tick veil is an adaptation of that ramp, not a copy.5
- Never animate the opacity of the SwiftUI view that holds a RealityView; put a veil over it.22
If you design the loop
- A door entry is a ceremony of at least 1.32 seconds in Emerald with no input, and the forced step over the threshold is what makes it read as walking in.525
- Turning in place for 8 frames lets a player face something without moving; a 32-frame bump tells them a step was refused.1
- On a phone, my recommendation: tap to walk as the default, as Stardew’s mobile version ships it, a floating stick as the precision fallback, as the HIG advises, and no fixed d-pad.4044
- Haptics confirm events, not footsteps, and come with a switch.46
FAQ
How fast does the player walk in Pokémon?
In Red, Crystal and Emerald, a walking step covers one 16-pixel cell in 16 frames at 59.7275 frames a second: 268 milliseconds a cell, 3.73 cells a second. Emerald moves 1 pixel every frame; Red and Crystal move 2 pixels every second frame. Running and surfing in Emerald, and the bikes in Red and Crystal, take 8 frames a cell, 7.47 cells a second.1419
How many frames is a walk cycle in Pokémon Emerald?
Four entries over 32 frames: a stride drawing for 8 frames, the standing drawing for 8, the other stride for 8, standing for 8. That is two cells of walking, so each step shows one stride and one stand, and the legs alternate step by step. The run is a 16-frame cycle over two 8-frame cells.192
Does the camera in Pokémon Emerald lag behind the player?
No. Emerald’s camera copies the player’s position and scrolls the map by the same pixels on the same frame, so the player stays fixed on screen while the world moves. It does not stop at a map’s edge either: the outside is drawn from the layout’s border tiles. A look-ahead camera for the bike exists in the code but is never switched on.231112
How long is the door and fade transition in Pokémon Emerald?
The door opens in four drawings of five frames (335 milliseconds), the player takes one forced 16-frame step in, the door closes in 20 frames, and the screen fades in nine levels, the last blend on the fade’s 17th frame and the fade finished on its 22nd: at least 79 frames, 1.32 seconds, before the next map can begin to load. Going into a cave, the screen fades out to white instead of black, and coming out of one, it fades back in from white.152527
Should a pixel-art game run at 120 Hz on a ProMotion iPhone?
Not for whole-pixel motion. A world that moves one pixel per 60-hertz tick only shows each pixel for two refreshes at 120 hertz, and uneven holds at 80. Apple’s ProMotion article says games get “special priority to 30Hz and 60Hz,” an iPhone app must set CADisableMinimumFrameDurationOnPhone to go above 60, and RealityKit typically renders at 60. Simulate on a fixed 60-hertz tick and let the display be whatever it is.67168
Why do my sprite’s feet slide when it walks?
Usually because the walk drawings run on one clock and the movement on another, with lengths that do not agree. Kiradex’s current code plays a six-drawing cycle at 8 drawings a second while walking 4 tiles a second, so one cycle covers 3 tiles instead of 2, as a model of the code shows. The handhelds count both in frames and make each step’s drawings last as long as the step; choosing the drawing from the distance walked, which is what I propose for Kiradex, gets the same agreement without a second clock. Either way the cadence cannot drift away from the movement; whether a foot stays planted also depends on the drawings themselves.619
Should a mobile pixel game use a virtual d-pad?
Not as the default, on my reading of the sources. Stardew Valley’s mobile default is tap to move, with an invisible joystick among its other schemes for precise tasks; Apple’s HIG recommends tapping objects directly and a thumbstick that appears “wherever the player lands their thumb instead of a static thumbstick position.”4044
Related on this site: Pixel-Art Worlds on iPhone is the first guide in this series, with Emerald’s step-and-stand walk, the RealityKit recipe this world runs on and the card viewer’s opacity lesson; Pixel-Art People: Characters and a Creator on iPhone is the second, with the six-drawing walk whose timing this post moves from a clock to a distance; Pixel-Art Structures: Houses, Halls and Interiors on iPhone is the third, with the doors, warps and floors whose timing section 2 measures; iPhone Duo for Developers and Prepare Your App for iPhone Duo cover the two displays and the fold the brief’s camera keeps clear of; RealityKit’s Spatial Mental Model explains the entity and system model behind SceneEvents.Update.
Sources
-
Author’s measurement, October 4, 2026:
measure_gen3_motion.py, in the author’s evidence folder for this post, run over pret’spokeemeraldat commit731ad5b; it parses the step-function tables andInitMoveInPlacedurations insrc/event_object_movement.c, the animation tables insrc/data/object_events/object_event_anims.h, the delay-counter rule insrc/sprite.cand the door frames insrc/field_door.c, and converts frames at 59.7275 hertz. Output saved asmeasure_gen3_motion.out.txtbeside it (speeds 3.73, 7.47, 9.95, 14.93 and 29.86 cells a second; walk-in-place 32, 16, 8 and 4 frames;sAnim_GoSouth32 frames; door drawings held 5 updates, 83.7 ms, 335 ms for four). ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
pret,
pokeemerald/src/event_object_movement.c(sStep1FuncstosStep8Funcsand the comment “Over the course of the step animation, these sum to 16 pixels (one full metatile)”;sStepTimesandNpcTakeStep, which indexes the step table withsTimer, one entry a frame;SetStepAnimHandleAlternation, which sets the gait’s animation and the alternation when a step begins;CameraObject_UpdateMove), commit731ad5b, accessed October 4, 2026, https://github.com/pret/pokeemerald/blob/master/src/event_object_movement.c. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
pret,
pokeemerald/src/overworld.c(OverworldBasic’s orderRunTasks(); AnimateSprites(); CameraUpdate(); UpdateCameraPanning();followed later in the same function byUpdatePaletteFade();VBlankCB_FieldcallingTransferPlttBuffer;InitPlayerAvatarbeforeInitCameraUpdateCallback(gPlayerAvatar.spriteId)) andsrc/sprite.c(AnimateSpritesruns callbacks in slot order), accessed October 4, 2026, https://github.com/pret/pokeemerald/blob/master/src/overworld.c and https://github.com/pret/pokeemerald/blob/master/src/sprite.c. The same-frame conclusion is the author’s reading of the code, not a run. ↩↩↩↩↩↩↩ -
Author’s measurement, October 4, 2026:
measure_gen12_motion.py, in the author’s evidence folder for this post, run over pret’spokeredatd2704a6(home/overworld.asm,home/fade.asm) andpokecrystalat5beda23(engine/overworld/events.asm,engine/overworld/map_objects.asm,data/maps/setup_scripts.asm,engine/tilesets/timeofday_pals.asm). Output saved asmeasure_gen12_motion.out.txt(Red: 16 px in 16 frames, bike 8 frames, warp fade 32 frames; Crystal: walk 8 updates of 2 px, bike 4 of 4, slow 16 of 1 at 1.87 cells a second, door fade 8 frames each way). ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Author’s measurement, October 5, 2026:
measure_gen3_fade.py, in the author’s evidence folder for this post, a Python port ofBeginNormalPaletteFade,UpdatePaletteFadewith its pending-transfer flag,UpdateNormalPaletteFadeandIsSoftwarePaletteFadeFinishingfrom pret’spokeemerald/src/palette.cat731ad5b, run on its callers’ schedule: the door task starts the fade insideRunTasks(Task_DoDoorWarp,src/field_screen_effect.c);BeginNormalPaletteFadeupdates once, copies the buffer to palette memory and clears the flag;OverworldBasic(src/overworld.c) updates again in the same frame; each later frame updates once, and the VBlank transfer clears the flag. Frames count from the task’s frame as 0. It assumes no fade was running; with rain, snow, fog, shade or drought active,FadeScreeninsrc/field_weather.ccopies the weather-tinted buffer and then calls the sameBeginNormalPaletteFade(the weather’s own handler for the fade out isDoNothing), so the fade-out schedule holds, while the fade in goes through the weather code and was not simulated; the fade in after a warp starts from the map-load callback, whose first frame was not traced, so both cases are given. Output saved asmeasure_gen3_fade.out.txt(levels 0 to 16 in steps of 2; first visible blend on frame 1; last blend, the sprite palettes at 16, on frame 16, 285 ms counting frame 0; inactive on frame 21, 368 ms;FadeInFromWhitewith delay 8 inactive on frame 85 or 86 counting from frame 0, that is after 86 or 87 frames, 1,440 or 1,457 ms; door entry: open 20, step 16 and close 20 frames, the last fade blend after at least 73 frames, 1.22 s, andWarpIntoMapno earlier than frame 79, 1.32 s, sinceTask_WarpAndLoadMapwaits for the fade to go inactive). ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Author’s model, October 5, 2026:
measure_kiradex_motion.py, in the author’s evidence folder for this post, readstilesPerSecond(4),framesPerSecond(8),runPace(1.6), the run threshold (6 steps) and the camera’smin(1, dt * 6)fromKiradex/World/PlazaRig.swift,centrefromKiradex/World/TileMap.swift, and the six-drawingwalkandruncycles fromscripts/forge/rig.py, and modelsadvance,place,animateandfollowframe by frame with everyFloatoperation done in 32 bits (numpyfloat32) in the code’s order and Swift’s half-away-from-zero rounding, at an exact 1/60 and 1/120 s, for a 5-tile walk, the walking pace held for 12 tiles, and a 12-tile run, each followed by 3 s of standing; map clamping, the fold and the feet offset are left out. It is a model of the code, not a capture from a device. Output saved asmeasure_kiradex_motion.out.txt. Walk at 60 Hz: 15 frames a tile, each tile 14 frames of 1 px and one of 2; camera gap 5, 6, 7, 8 and 9 px at the ends of the first five tiles, 13 from the ninth when the walking pace is held; on-screen position changing on 8 of a 5-tile walk’s 73 moving frames after the first (the model’s walk has 74 moving frames, the last tile’s final frame counting as standing); resting offset 4 px. At 120 Hz: 30 frames a tile, 16 of 1 px and 14 of 0; gap 9; rest 9. Run at 60 Hz: 10 frames a tile, 6.00 tiles a second, 4 frames of 1 px and 6 of 2 a tile; gap 13 from the second tile; rest 4. Run at 120 Hz: 19 frames a tile, 6.32 tiles a second; gap 9; rest 9. Cycle travel from simulated positions between successive starts of a cycle at 60 Hz: 3.00 tiles walking, 4.50 running (4.80 at the nominal 6.4 tiles a second); at 120 Hz, 3.00 and 2.94 walking, 4.75 running. Diagonal steps in the current code: 22 frames walking and 14 running at 60 Hz, 43 and 27 at 120. Against the same model run with the clock, positions and camera in double precision, every position and camera value is unchanged and 9 walking drawings at 60 Hz and 14 at 120 differ, all on frames where the clock times 8 is a whole number (11 such frames in 12 tiles of walking at 60 Hz, 23 at 120). The proposal: a fixed 60 Hz tick holds each pixel for 1 refresh at 60 Hz, 2 refreshes for 59 of 61 positions at 120 Hz, and 1 or 2 refreshes, 42 and 19 positions, at 80 Hz. The accumulator: ten seconds of callbacks at each of the twelve iPhone rates from 120 to 10 Hz runs 600 ticks with a backlog limit of 8 ticks and nothing dropped, against 480 at 12 Hz and 400 at 10 Hz with a cap of 4 ticks a callback; after a 1 s hitch at 60 Hz, the 8-tick rule runs 8 ticks, then 1 a callback, dropping 866.7 ms, while a 4-tick cap that keeps the backlog runs 4 ticks on 19 consecutive callbacks. The camera arithmetic:fiton a 393 by 852 pt view at 3x gives 7 screen pixels a texel and a view 168.43 by 365.14 world pixels, half-width 84.21, inward-rounded bounds 85 and the map’s width minus 85; a fold change from 0 to −37 in 9 levels passes 0, −5, −9, −14, −19, −23, −28, −32, −37. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple Developer Documentation, “Optimizing iPhone and iPad apps to support ProMotion displays” (the 10 to 120 Hz iPhone range and its twelve rates; the device list;
CADisableMinimumFrameDurationOnPhone; the 30 and 60 Hz game priority; “Prepare your app to operate at any refresh rate”;targetTimestamp), accessed October 4, 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” to 60 frames per second), accessed October 4, 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) andsrc/sprite.c(animDelayCounterloaded with the frame’s duration minus one, counted down byContinueAnim, the next frame taken when it reaches zero), accessed October 4, 2026, https://github.com/pret/pokeemerald/blob/master/src/data/object_events/object_event_anims.h and https://github.com/pret/pokeemerald/blob/master/src/sprite.c. ↩↩↩↩↩↩↩↩↩↩↩ -
pret,
pokeemerald/src/field_player_avatar.c(PlayerWalkNormal,PlayerRun,PlayerWalkFastwith the comment “same speed as running”,CheckMovementInputNotOnBikeandTURN_DIRECTION,PlayerTurnInPlace, the wall bump), accessed October 4, 2026, https://github.com/pret/pokeemerald/blob/master/src/field_player_avatar.c. ↩↩↩↩↩ -
pret,
pokeemerald/src/fieldmap.c(GetBorderBlockAt: the layout’s 2-by-2 border metatiles, markedMAPGRID_IMPASSABLE), accessed October 4, 2026, https://github.com/pret/pokeemerald/blob/master/src/fieldmap.c. ↩↩↩ -
pret,
pokeemerald/src/field_camera.c(CameraUpdate;CameraPanningCB_PanAhead, which movessVerticalCameraPanby 2 toward 72 or minus 8 from a rest of 32, guarded bygUnusedBikeCameraAheadPanbackand commented “this code is never reached”), accessed October 4, 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, October 3, 2026 (Emerald’s door frame held five updates, about 84 milliseconds; Red’s
PlayerStepOutFromDoor; Emerald’s elevator shake; Kiradex’s 70-millisecond door and 220-millisecond warp listed among the gaps), https://blakecrosley.com/blog/pixel-art-structures-on-iphone. ↩↩↩↩↩↩ -
Author’s research notes for the structures post, Kiradex repository (private),
docs/research/structures/01-structures-in-the-canon.md, October 3, 2026, which gave Emerald’s door frames as “4 ticks each (16 frames, about 0.27 s at 59.7 Hz)”; corrected by the reading ofAnimateDoorFrameinfield_door.cgiven in this post. ↩↩ -
Apple Developer Documentation, “preferredFrameRateRange” (
CADisplayLink; iOS 15.0, iPadOS 15.0, Mac Catalyst 15.0, visionOS 1.0), accessed October 4, 2026, https://developer.apple.com/documentation/quartzcore/cadisplaylink/preferredframeraterange. ↩↩↩ -
Apple Developer Documentation, “CADisableMinimumFrameDurationOnPhone” (Information Property List key; iOS 15.0, iPadOS 15.0), accessed October 4, 2026, https://developer.apple.com/documentation/bundleresources/information-property-list/cadisableminimumframedurationonphone. ↩↩↩
-
Author’s read of the Kiradex repository (private) at commit
b1b78b1, October 4, 2026, read-only:Kiradex/World/PlazaRig.swift(tilesPerSecond,framesPerSecond,runPace,walk(to:)and itssteps.count >= 6run rule,advance, whose progress step isdt * Self.tilesPerSecond * (walker.running ? Self.runPace : 1) / length,animate,place,fit, which divides the view’s screen pixels by a wholepixelScale,follow, which converts the fold’s points bydisplayScale / pixelScale, eases bymin(1, dt * 6)and rounds afterward, and whose clamp lets the camera past the map’s edge into the woods only when the Duo is half open, bybeyondMargin, 12 tiles, whilebuildlays the woods whatever the fold,ripple, which changes the ground’s water drawing 4 times a second on a map with water,openDoorand its comment “at about Emerald’s four ticks a frame”),Kiradex/World/PlazaStage.swift(the tap handling, the 220-millisecond warp delay, theSceneEvents.Updatesubscription, the floor swap by.id),Kiradex/World/TileMap.swift(the eight-waypath, which takes the open tile with the lowest cost so far, adds 1 or √2 a step, and uses no distance-to-goal estimate, and whose diagonal steps are skipped unless both side cells are walkable),Kiradex/World/WorldMap.swift(a warp’s door sheet, “closed, half open, open”),openDoorloading that sheet withSpriteSheet.bundled(name, columns: 3, rows: 1)and showing column 1 and then column 2,scripts/forge/kit.py(door_sheet(), “The three frames side by side, 48 × 32”),scripts/forge/town.pyandscripts/forge/buildings.py(each town door warp sits on the door cell in a building’s bottom row, and the building’s cells left of, right of and above every door are blocked; checked for all seven town doors in the shippedtown.json),Kiradex/Views/Card/CardViewer.swift(the veil’s.easeOut(duration: 0.25)) andscripts/forge/rig.py(the cycles). The absence ofUIImpactFeedbackGenerator,sensoryFeedback,CHHaptic,CADisplayLink,preferredFrameRateRangeandCADisableMinimumFrameDurationOnPhoneis a search overKiradex/andproject.ymlon October 4, 2026, which found none; the same search finds noMTKView,MTLRenderCommandEncoderorTouchControllerinKiradex/, so the world has no render pass of its own. The snap-back on a mid-step tap is read fromwalk(to:), not captured. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
pret, shallow clones of
pokered(commitd2704a6, September 22, 2026),pokecrystal(5beda23, September 29, 2026) andpokeemerald(731ad5b, October 1, 2026), the community decompilations of the published games, https://github.com/pret. ↩ -
Martin Korth, GBATEK, “LCD Dimensions and Timings” (280,896 cycles and 16.743 ms a frame, “ca. 59.737 Hz”), accessed October 4, 2026, https://problemkaputt.de/gbatek-lcd-dimensions-and-timings.htm; Pan Docs, “Rendering” (“One frame: 70224 dots @ 59.7 fps”), accessed October 4, 2026, https://gbdev.io/pandocs/Rendering.html. The 59.7275 figure is the author’s arithmetic: 2^24 / 280,896, and 4,194,304 / 70,224. ↩↩↩↩↩
-
pret,
pokeemerald/src/bike.c(sMachBikeSpeedCallbacks,bikeFrameCountercapped at 2,AcroBikeTransition_MovingcallingPlayerRideWaterCurrent, and the only assignmentgUnusedBikeCameraAheadPanback = FALSE), accessed October 4, 2026, https://github.com/pret/pokeemerald/blob/master/src/bike.c. ↩↩↩ -
Figures drawn by the author’s script
make_figures.py(withsvgkit.py), October 4, 2026, from data on disk only: the canon’s speeds frommeasure_gen3_motion.out.txtandmeasure_gen12_motion.out.txt; Kiradex’s walk and camera frommeasure_kiradex_motion.py’s ownsimulate()(the first second of a 5-tile walk; the camera for a 5-tile walk at 60 and 120 Hz and a 12-tile run at 60 Hz), and the proposal’s tick loop from the same script; Emerald’s fade frommeasure_gen3_fade.py’srun(), the port with its callers’ schedule, and the door chart’s fade and earliest map load from the same; Kiradex’s door timings (70 ms, 1,400 ms, 220 ms) read fromPlazaRig.swiftandPlazaStage.swiftatb1b78b1. No game art is used. ↩↩↩↩↩ -
Blake Crosley, “Pixel-Art Worlds on iPhone: What the 16-Bit Masters Knew,” blakecrosley.com, October 3, 2026 (Emerald’s walk “step, stand, step, stand at eight ticks each”; Red’s “stand, step, stand, step-flipped”; Celeste at 320 by 180 multiplied by six; the RealityKit engine; positions “rounded to whole world units each frame, after easing”; the card viewer’s opacity lesson), https://blakecrosley.com/blog/pixel-art-world-on-iphone. ↩↩↩↩↩↩↩↩
-
Itay Keren, “Scroll Back: The Theory and Practice of Cameras in Side-Scrollers,” Game Developer, May 11, 2015, a modified version of his talk at the Independent Games Summit, GDC 2015 (position-locking, edge-snapping, camera-window, lerp-smoothing, target-focus, dual-forward-focus; the quotations), accessed October 4, 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, which draws at counter 0 and advances when the counter equals the frame’s time), accessed October 4, 2026, https://github.com/pret/pokeemerald/blob/master/src/field_door.c. ↩↩↩↩ -
pret,
pokeemerald/src/field_screen_effect.c(Task_DoDoorWarp’s states, the fifth callingWarpFadeOutScreenand handing the task toTask_WarpAndLoadMap, which waits onPaletteFadeActive(), that isgPaletteFade.active, andBGMusicStopped()beforeWarpIntoMap;Task_ExitDoor,WarpFadeOutScreencallingGetMapPairFadeToType,WarpFadeInScreencallingGetMapPairFadeFromType, andFadeInFromWhitewithFadeScreen(FADE_FROM_WHITE, 8)), accessed October 4, 2026, https://github.com/pret/pokeemerald/blob/master/src/field_screen_effect.c. ↩↩↩↩↩↩↩↩↩↩ -
pret,
pokeemerald/src/palette.c(BeginNormalPaletteFadewithdeltaY = 2, its own call toUpdatePaletteFade, itsCpuCopy32to palette memory andsPlttBufferTransferPending = FALSE;UpdatePaletteFade, which returns at once while that flag is set and sets it fromgPaletteFade_selectedPalettesafter each update;TransferPlttBuffer, which clears it;UpdateNormalPaletteFade;IsSoftwarePaletteFadeFinishing), accessed October 4, 2026, https://github.com/pret/pokeemerald/blob/master/src/palette.c. ↩↩ -
pret,
pokeemerald/src/fldeff_flash.c(sTransitionTypeswith its 16 rows to and fromMAP_TYPE_UNDERGROUND, each with an enter flag, an exit flag and a transition routine;GetMapPairFadeToTypereturning a row’s enter flag andGetMapPairFadeFromTypeits exit flag), accessed October 4, 2026, https://github.com/pret/pokeemerald/blob/master/src/fldeff_flash.c. ↩↩↩ -
pret,
pokeemerald/src/field_specials.c(ShakeCamera, reading vertical pan, horizontal pan, number of shakes and delay fromVAR_0x8004toVAR_0x8007, negating the pan on each shake), accessed October 4, 2026, https://github.com/pret/pokeemerald/blob/master/src/field_specials.c. ↩ -
Author’s count, October 4, 2026:
measure_gen3_shake.py, in the author’s evidence folder for this post, reads everyspecial ShakeCameracall and the foursetvarvalues before it acrossdata/**/*.incin pret’spokeemeraldat731ad5b. Output saved asmeasure_gen3_shake.out.txt(24 calls in 9 files, the eight parameter sets and their counts in the table). ↩↩↩↩↩↩ -
pret,
pokered/home/overworld.asm(OverworldLoop,OverworldLoopLessDelay,wWalkCounter,AdvancePlayerSprite,DoBikeSpeedup, and the 180-degree turn’s comment), commitd2704a6, accessed October 4, 2026, https://github.com/pret/pokered/blob/master/home/overworld.asm. The turn behavior is read from the code, not run in an emulator. ↩↩↩↩↩↩ -
pret,
pokered/engine/overworld/movement.asm(UpdatePlayerSprite, the intra-animation counter advancing the drawing at 4), accessed October 4, 2026, https://github.com/pret/pokered/blob/master/engine/overworld/movement.asm. ↩↩ -
pret,
pokered/home/fade.asm(GBFadeOutToBlack) andhome/overworld.asm(PlayMapChangeSound,SFX_GO_INSIDEfor door tile$0b, otherwiseSFX_GO_OUTSIDE), accessed October 4, 2026, https://github.com/pret/pokered/blob/master/home/fade.asm. ↩ -
pret,
pokecrystal/engine/overworld/events.asm(MaxOverworldDelay: db 2) andengine/overworld/map_objects.asm(StepVectors;StepFunction_Turn, whose.init1,.step1,.init2and.step2fall through into each other, andObjectStep_AnonJumptable), commit5beda23, accessed October 4, 2026, https://github.com/pret/pokecrystal/blob/master/engine/overworld/events.asm and https://github.com/pret/pokecrystal/blob/master/engine/overworld/map_objects.asm. The turn’s three updates are the author’s instruction-by-instruction replay of the routine, not an emulator run. ↩↩↩↩↩ -
pret,
pokecrystal/data/maps/setup_scripts.asm(MapSetupScript_Doorbeginning withFadeOutToWhite,MapSetupScript_Warpending withFadeInFromWhite) andengine/tilesets/timeofday_pals.asm, accessed October 4, 2026, https://github.com/pret/pokecrystal/blob/master/data/maps/setup_scripts.asm and 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, October 3, 2026 (the six-frame walk; “the walk at eight frames a second” in the plaza; under “Since publishing,” the 26-pixel figure replaced at TestFlight build 34 by a 30-pixel figure in a 32-by-40 cell, which
scripts/forge/rig.pyatb1b78b1sets asCELL_W, CELL_H = 32, 40), https://blakecrosley.com/blog/pixel-art-characters-on-iphone. ↩↩↩ -
Celeste’s developers, the
NoelFB/Celesterepository on GitHub,Source/Player/Player.cs(the camera update under “Camera (lerp by distance using delta-time)” and theCameraTargetgetter), accessed October 4, 2026, https://raw.githubusercontent.com/NoelFB/Celeste/master/Source/Player/Player.cs. ↩↩↩↩ -
Celeste’s developers,
NoelFB/Celesterepository,README.md(the class files released “as a learning resource and for general interest”; the MIT License applying only to that code), accessed October 4, 2026, https://raw.githubusercontent.com/NoelFB/Celeste/master/README.md. ↩ -
Stardew Valley Wiki, “Speed” (the player’s base speed: 2 walking, 5 running, 6.6 on a horse, 7 after a carrot), accessed October 4, 2026, https://stardewvalleywiki.com/Speed. ↩
-
Author’s research notes for this post, compiled October 4, 2026, recording what was searched for and not found: a primary source on the cameras of Sea of Stars, Eastward or CrossCode; a Maddy Thorson camera talk or article; the Pixel Remasters’ exact touch scheme; Terraria’s mobile movement on https://terraria.wiki.gg/wiki/Mobile_version; and
developer.apple.com/documentation/touchcontrols, which returned 404. ↩↩↩ -
Stardew Valley Wiki, “Mobile Controls” (the schemes; “Tap-to-move & Auto-Attack” as the default; the quotations on tap-to-move, follow-the-touch, the invisible joystick and the default controls’ limits), accessed October 4, 2026, https://stardewvalleywiki.com/Mobile_Controls. ↩↩↩↩↩↩↩
-
Jared Nelson, “‘Stardew Valley’ is Getting A TON of New Control Options in the Next Update,” TouchArcade, November 1, 2018, accessed October 4, 2026, https://toucharcade.com/2018/11/01/stardew-valley-mobile-controls-update/. ↩
-
App Store, “FINAL FANTASY” by SQUARE ENIX, version history (version 1.2.0, 03/11/2025, and the tap-based movement note), accessed October 4, 2026, https://apps.apple.com/us/app/final-fantasy/id1492041278. ↩↩
-
Mikhail Madnani, TouchArcade, January 30, 2024, on the Final Fantasy Pixel Remaster update that brought controller support to mobile, accessed October 4, 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” (touch-control best practices; change log entry of June 9, 2025), accessed October 4, 2026, https://developer.apple.com/design/human-interface-guidelines/game-controls. ↩↩↩↩↩↩↩
-
Apple, WWDC25 session 209 (the Touch Controls framework; “the vast majority of players won’t have a controller available”; “integrates directly with Metal”), transcript accessed October 4, 2026, https://developer.apple.com/videos/play/wwdc2025/209/. ↩↩↩
-
Apple Human Interface Guidelines, “Playing haptics” (best practices, custom haptics and the iOS impact category; the quotations), accessed October 4, 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), accessed October 4, 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), accessed October 4, 2026, https://developer.apple.com/documentation/quartzcore/cadisplaylink/targettimestamp. ↩ -
Apple, WWDC21 session 10147, “Optimize for variable refresh rate displays” (Adaptive-Sync displays on the Mac and ProMotion on iPad Pro; the quotations), transcript accessed October 4, 2026, https://developer.apple.com/videos/play/wwdc2021/10147/. ↩↩↩
-
Apple Developer Documentation, “SceneEvents.Update” (iOS 13.0, iPadOS 13.0, Mac Catalyst 13.0), accessed October 4, 2026, https://developer.apple.com/documentation/realitykit/sceneevents/update. ↩
-
Apple Developer Documentation, “deltaTime” (
SceneEvents.Update; iOS 13.0), accessed October 4, 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; per-frame code through a
SystemorSceneEvents.Update), accessed October 4, 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), accessed October 4, 2026, https://developer.apple.com/documentation/corehaptics/chhapticpattern; Core Haptics framework page, https://developer.apple.com/documentation/corehaptics. ↩
-
Apple Developer Documentation, “CHHapticEvent” and “CHHapticEvent.EventType” (
hapticTransient,hapticContinuous; iOS 13.0), accessed October 4, 2026, https://developer.apple.com/documentation/corehaptics/chhapticevent and https://developer.apple.com/documentation/corehaptics/chhapticevent/eventtype. ↩ -
Apple Developer Documentation, “CHHapticEvent.ParameterID” (iOS 13.0), accessed October 4, 2026, https://developer.apple.com/documentation/corehaptics/chhapticevent/parameterid. ↩
-
Apple Developer Documentation, “CHHapticEngine” (iOS 13.0;
capabilitiesForHardware()), accessed October 4, 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:)under “Initializing the feedback generator”, andinit(style:)under Deprecated), accessed October 4, 2026, https://developer.apple.com/documentation/uikit/uiimpactfeedbackgenerator. ↩↩ -
Apple Developer Documentation, “UIImpactFeedbackGenerator.FeedbackStyle” (iOS 10.0), accessed October 4, 2026, https://developer.apple.com/documentation/uikit/uiimpactfeedbackgenerator/feedbackstyle. ↩
-
Apple Developer Documentation, “impactOccurred(intensity:)” (iOS 13.0, iPadOS 13.0, Mac Catalyst 13.1), accessed October 4, 2026, https://developer.apple.com/documentation/uikit/uiimpactfeedbackgenerator/impactoccurred(intensity:). ↩
-
Apple Developer Documentation, “prepare()” (
UIFeedbackGenerator; iOS 10.0), accessed October 4, 2026, https://developer.apple.com/documentation/uikit/uifeedbackgenerator/prepare(). ↩↩ -
Apple Developer Documentation, “sensoryFeedback(:trigger:)” and “SensoryFeedback” (SwiftUI; iOS 17.0, iPadOS 17.0, Mac Catalyst 17.0, visionOS 26.0;
impact(weight:intensity:)), accessed October 4, 2026, https://developer.apple.com/documentation/swiftui/view/sensoryfeedback(:trigger:) and 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), accessed October 4, 2026, https://developer.apple.com/documentation/touchcontroller. ↩↩↩↩
-
Apple Developer Documentation, “TCDirectionPad” (iOS 26.0, iPadOS 26.0, Mac Catalyst 26.0), accessed October 4, 2026, https://developer.apple.com/documentation/touchcontroller/tcdirectionpad. ↩
-
Apple Developer Documentation, “GCVirtualController” (Game Controller; iOS 15.0, iPadOS 15.0, Mac Catalyst 15.0), accessed October 4, 2026, https://developer.apple.com/documentation/gamecontroller/gcvirtualcontroller. ↩
-
pret,
pokeemerald/src/field_control_avatar.c(TryDoorWarp, called with the cell in front of the player while the held direction matches the facing, which warps only when that direction is north and the cell is a door warp), accessed October 4, 2026, https://github.com/pret/pokeemerald/blob/master/src/field_control_avatar.c. ↩