ピクセルアートの動き:iPhoneでの歩行、カメラ、ドア
『ポケットモンスター 赤』『ポケットモンスター クリスタルバージョン』『ポケットモンスター エメラルド』は、ゲームボーイとゲームボーイアドバンスの3世代から1本ずつ選んだタイトルです。3本とも16ピクセルのセルを約59.73ヘルツで16フレームかけて歩き、1秒あたり3.73セル進みます。一歩のなかに時計任せの部分はひとつもありません。エメラルドはプレイヤーを1フレームにぴったり1ピクセル動かし、一歩ごとに踏み出しのコマと直立のコマを1枚ずつ見せ、同じフレームで同じピクセル数だけカメラをスクロールさせます。123 赤とクリスタルは、2フレームに1回2ピクセル動かすことで同じ16フレームにたどり着きます。4 エメラルドのドアは4枚のコマを各5フレームずつ表示して開き、プレイヤーは強制的に1歩中へ歩かされ、ドアが閉まり、画面は9段階に刻まれたフェードで暗くなります。次のマップの読み込みが始まるまで、少なくとも79フレーム、1.32秒です。15 Kiradexの世界は経過時間で歩き、カメラにはイージングがかかっています。そのコードのモデル(スマートフォンからのキャプチャではなく、あくまでモデル)によれば、60ヘルツではスプライトがタイルごとに1回2ピクセル跳びます。カメラは60ヘルツの歩行でタイルを1枚進むごとに約1ピクセルずつ遅れていき(5タイル目で9ピクセル、走ると13ピクセル)、止まるとプレイヤーから4から9ピクセル手前で静止します。歩行のコマは独自の時計で進み、エメラルドの1サイクルが2タイル分なのに対して、1サイクルで3.00タイル進みます。6 Appleはゲームに30ヘルツと60ヘルツでの特別な優先度を与えており、RealityKitは通常60で描画します。そこでブリーフの内容は次のとおりです。整数ピクセルで進む固定60ヘルツのティック、歩いた距離で選ぶ歩行のコマ(携帯機のやり方ではなく、私の提案です)、固定したカメラ、自前のアートで再現するエメラルドのドアの儀式、オプションのハプティクスイベント3つ、そして120ではなく60ヘルツ。78 本記事では、原典を逆コンパイルから実測し、iPhone側をAppleのページから読み解き、ブリーフをそれが通るべきチェックとともに示します。
TL;DR
- 一歩は速度ではなく契約です。 エメラルドのステップテーブルはどれも合計16ピクセルになります。歩行は1フレーム1ピクセルを16フレーム(1セル268ミリ秒)、走りとなみのりは2ピクセルを8フレーム、マッハ自転車の最高速は4ピクセルを4フレームで、不均等なのはダート自転車の2-3-3-2-3-3だけです。赤とクリスタルは同じ16フレームを、毎秒30回の更新で2ピクセルずつ跳びながら歩きます。124
- 脚の動きと一歩は、タイミングが合うように作られています。 エメラルドはコマと一歩をそれぞれ別々にフレームで数え、その数を一致させています。歩行は踏み出し、直立、踏み出し、直立を各8フレームで、16フレームのセル2つ分に割り当てます。速い移動ではコマを増やさず表示時間を半分にするので、どの速度でも16ピクセルにつき着地は1回です。止まった状態からの方向転換は8フレーム(134ミリ秒)、歩きながらの方向転換は0フレーム、壁に向かって歩くと32フレームの「ぶつかり」が再生されます。19102
- カメラはプレイヤーそのものです。 エメラルドのカメラはプレイヤーの位置をコピーし、同じフレームで同じピクセル数だけスクロールします。遅れも先回りもなく、マップの端でも止まりません。マップの外側は境界タイルで描かれるからです。ゲームフリークは自転車の前方を見せるカメラを書いておきながら、それをオフにしたまま出荷しました。231112
- ドアは儀式です。 5フレームずつのコマが4枚(335ミリ秒)、16フレームの強制的な一歩、20フレームでドアが閉まり、最後のブレンドが17フレーム目に来て22フレーム目に終わるフェード。マップを読み込めるまで、入力を受け付けない時間が少なくとも79フレーム、1.32秒あります。これはこのシリーズの建物編の記事にある1コマ約84ミリ秒という数字を裏づけ、その記事の元になった調査メモの「4 ticks each (16 frames, about 0.27 s at 59.7 Hz)」(各4ティック、16フレーム、59.7Hzで約0.27秒)を訂正するものです。クリスタルは8フレームで白にフェードし、赤は32フレームで黒にフェードします。1513144
- iPhoneでは60で均等に刻みます。
preferredFrameRateRange(iOS 15.0)はヒントにすぎません。ProMotion対応iPhoneは10から120ヘルツの間を12段階で動きます。60を超えるにはCADisableMinimumFrameDurationOnPhoneが必要です。ゲームには「special priority to 30Hz and 60Hz」(30Hzと60Hzへの特別な優先度)があり、RealityKitは「typically limits the refresh rate」(通常リフレッシュレートを制限する)として60に抑えます。整数ピクセルの歩行は120にしても何も得られず、各ピクセルが2回のリフレッシュにわたって表示されるだけです。1571686 - 現在のKiradexは、実測ではなくモデルで見ています。 コードは経過時間で毎秒4タイル歩くので、安定した60ヘルツのモデルでは1タイルに15フレームかかり、そのうち1フレームで2ピクセル動きます。カメラはイージングのあと丸められ、歩き続けるほど遅れが増し(最初のタイルの後で5ピクセル、5タイル目の後で9、走ると13)、止まると60ヘルツでプレイヤーから4ピクセル、120ヘルツで9ピクセルずれて静止します。6枚のコマからなる歩行を毎秒8コマで再生するので、1サイクルで歩きなら3.00タイル、走りなら4.50タイル進みます。スマートフォンでの実測はまだ一度もしていません。176
- Kiradexにとっての成果はブリーフであって、出荷済みのビルドではありません。 1ピクセルずつ進み、溜まった遅れの扱いを明記した固定60ヘルツのティック、距離で選ぶ歩行のコマ、整数ピクセルに固定したカメラ、刻まれたフェードを含むドアの一連の流れ、目に見える形で拒否される一歩、オプションのハプティクスイベント3つ、120ヘルツを要求しないこと。そしてそれぞれに、モーションログ、キャプチャ、スクリプトのいずれかで検証できるチェックが付いています。
1. エメラルドの歩行:16フレームで16ピクセル
このシリーズの最初の2本は、ピクセルの世界とその住人がどう見えるかを測りました。3本目はその建物を測りました。本記事が測るのは時間です。1フレームに何ピクセル進むのか、一歩に何フレームかかるのか、どのフレームでどのコマが表示されるのか、方向転換、ドア、フェードにそれぞれどれだけかかるのか、そしてそのあいだカメラが何をしているのか。ゲームの読み方はこれまでの記事と同じで、市販されたゲームボーイとゲームボーイアドバンスのゲームをpretが逆コンパイルしたものを、これまでの記事で使ったコミットで読んでいます。pokered d2704a6、pokecrystal 5beda23、pokeemerald 731ad5bです。18 数えるのには小さなスクリプトを5本使いました。どれも注に名前と保存した出力を記しています。
まずひとつの数字を押さえておきます。ほかのすべてがフレーム単位だからです。ゲームボーイアドバンスは2^24ヘルツのクロックで280,896サイクルかけて1フレームを描きます。これは毎秒59.7275フレーム、1フレーム16.743ミリ秒です。GBATEKはこれを「ca. 59.737 Hz」(約59.737Hz)と丸めています。19 初代ゲームボーイの4,194,304ヘルツのクロックを1フレーム70,224ドットで割っても同じ59.7275になり、Pan Docsは「@ 59.7 fps」と書いています。19 本記事のミリ秒はすべて59.7275で換算しています。19
どの速度も、合計16になるテーブルです
エメラルドは歩く者を「速度×時間」で動かしません。フィールドの速度はそれぞれ、フレームごとのピクセル移動量を並べたテーブルです。そしてソースは、すべてのテーブルに共通することをこう書いています。「Over the course of the step animation, these sum to 16 pixels (one full metatile).」(一歩のアニメーションを通じて、これらの合計は16ピクセル、つまりメタタイル1枚分になる)2 sStep1Funcsは1ピクセル移動が16個、sStep2Funcsは2ピクセル移動が8個、sStep4Funcsは4ピクセルが4個、sStep8Funcsは8ピクセルが2個で、sStep3Funcsだけが例外のStep2, Step3, Step3, Step2, Step3, Step3です。2 移動処理のソースに対してスクリプトで数えると、次のようになります。
| 速度定数 | 1フレームあたりのピクセル | 1セルあたりのフレーム | 1秒あたりのセル | 1セルあたりのミリ秒 | 用途 |
|---|---|---|---|---|---|
MOVE_SPEED_NORMAL |
毎フレーム1 | 16 | 3.73 | 267.9 | 歩行、NPCの歩行 |
MOVE_SPEED_FAST_1 |
毎フレーム2 | 8 | 7.47 | 133.9 | 走り、なみのり、氷の上の滑走 |
MOVE_SPEED_FAST_2 |
2, 3, 3, 2, 3, 3 | 6 | 9.95 | 100.5 | ダート自転車、水の流れ |
MOVE_SPEED_FASTER |
毎フレーム4 | 4 | 14.93 | 67.0 | 最高速のマッハ自転車 |
MOVE_SPEED_FASTEST |
8, 8 | 2 | 29.86 | 33.5 | スライド系の移動アクション |
全行の出典:src/event_object_movement.cに対するmeasure_gen3_motion.py。1
覚えておくべき数字は歩行です。毎フレーム1ピクセル、1セル16フレーム、毎秒3.73セル、1セル268ミリ秒で、PlayerWalkNormalと歩くNPCすべてが使っています。1 Bボタンでの走りはPlayerRun、なみのりはPlayerWalkFastで、ソースにはこちらに「same speed as running」(走りと同じ速度)という注釈があります。氷の上の滑走も同じ関数を呼びます。3つとも1フレーム2ピクセル、1セル8フレーム、毎秒7.47セルです。110 イベントシーン用には遅い歩きUpdateWalkSlowAnimもあり、タイマーの値が偶数のときに1ピクセル進むので、1セルに31から32フレームかかります。1
ピクセルが不均等なのはダート自転車の速度だけです。6フレームで2, 3, 3, 2, 3, 3、平均すると1フレーム2.67ピクセル、毎秒9.95セルです。1 この速度にはAcroBikeTransition_MovingからPlayerRideWaterCurrentを経由して到達し、水の流れも同じ関数を使います。120 私の読みでは、この不均等さは16を割り切れない速度を選んだ代償です。1フレーム3ピクセルではセルを行き過ぎてしまうので、テーブルは2と3を交互に並べてぴったり16に着地させています。ほかの速度はすべてセルの約数です。
マッハ自転車はフレーム単位ではなく一歩単位で加速します。sMachBikeSpeedCallbacksはPlayerWalkNormal, PlayerWalkFast, PlayerWalkFasterで、bikeFrameCounterは一歩ごとに1ずつ増えて上限の2に達します。そのため止まった状態からの最初の一歩は16フレーム、2歩目は8フレーム、それ以降は4フレームで、1フレーム4ピクセル、毎秒14.93セルになります。120 自転車が2つの速度の中間にとどまることはありません。一歩はどれもいずれかのテーブルを最後まで実行したもので、速度が変わるのはセルの境界だけです。
赤、クリスタル、エメラルドで測った移動はどれも、16ピクセルのセル1つあたり整数のフレーム数です。Kiradexの数字はコードのモデルであり、キャプチャではありません。14621
16ピクセルにつき着地は1回
歩行アニメーションは、踏み出し、直立、踏み出し、直立です。sAnim_GoSouthはANIMCMD_FRAME(3, 8), (0, 8), (4, 8), (0, 8)で、コマ3を8フレーム、直立のコマ0を8フレーム、コマ4を8フレーム、ふたたび直立のコマを8フレーム、合計32フレームです。これは歩行でセル2つ分にあたります。91 表示時間は正確です。sprite.cはフレームの長さから1を引いた値を遅延カウンタに入れて0まで数え下げるので、長さ8のフレームは画面に8フレーム表示されます。91 したがって一歩ごとに踏み出しのコマが1枚と直立のコマが1枚表示されます。またSetStepAnimHandleAlternationは新しい一歩を毎回サイクルの反対側の半分から始める(animPos = {1, 3, 0, 2})ので、プレイヤーが止まっては歩き出しても、左右の脚は一歩ごとに交互になります。2 このシリーズの最初の記事は同じサイクルをアートの側から「step, stand, step, stand at eight ticks each, which is the bob everyone remembers.」(各8ティックで踏み出し、直立、踏み出し、直立。誰もが覚えているあの上下動だ)と書きました。22
速い移動ではコマはそのままで、表示時間が短くなります。GoFastは各コマを4フレーム(16フレームのサイクル)、GoFasterは2フレーム(8フレーム)、GoFastestは1フレーム(4フレーム)表示します。1 走りには専用のコマがあり、表示時間は不均等です。sAnim_RunSouthは(12, 5), (9, 3), (13, 5), (9, 3)で、16フレームのサイクルです。19
2つのテーブルを並べると、設計が見えてきます。歩行の32フレームのサイクルは16フレームのセル2つ分、走りの16フレームのサイクルは8フレームのセル2つ分、マッハ自転車の8フレームのGoFasterサイクルは4フレームのセル2つ分です。19 どの速度でも、16ピクセルにつき着地が1回あります。1 これはひとつのカウンタではなく、2つの時計です。コマは、sprite.cで各フレームの長さから読み込まれるanimDelayCounterが尽きると進みます。一歩は、NpcTakeStepがスプライトのsTimerで速度のテーブルを1フレームに1要素ずつ引くことで進みます。そしてSetStepAnimHandleAlternationが、一歩の始まりごとにその移動に合ったアニメーションを選びます。92 両者を揃えているのは、どちらも同じフレームで数えられ、移動の種類ごとに長さが一致するよう選ばれているという事実です。私の読みでは、真似すべきはこの点です。各一歩のコマはその一歩とまったく同じフレーム数だけ続くので、コマのリズムが移動からずれていくことはありえません。ただ、これらのテーブルにも私のモデルにも、着地した足が地面のどこにあるかを測るものは含まれていないので、それ以上のことは主張しません。
方向転換、ぶつかり、入力を読むタイミング
一歩は始まったら最後まで進みます。パッドが読まれるのは一歩が完了したときなので、方向キーを押し続ければ一歩と一歩が隙間なくつながり、歩いている最中の方向転換にはコストがかかりません。10 止まった状態からは話が別です。CheckMovementInputNotOnBikeがTURN_DIRECTIONを返すのは、新しい方向が今向いている方向と異なり、かつプレイヤーがまだ動いていないときだけです。このときPlayerTurnInPlaceが速いその場歩きを再生し、InitMoveInPlaceはその長さを8フレームにしています。その場での方向転換に134ミリ秒です。101 これがあるので、プレイヤーは看板や人に向かって一歩踏み出さずに、そちらを向くことができます。壁に向かって歩くと、代わりに遅いその場歩きが32フレーム、536ミリ秒再生され、ぶつかる音が鳴ります。拒否された一歩は、黙って捨てられるのではなく見せられるのです。110 通常の速さとさらに速い(faster)その場歩きは、それぞれ16フレーム(268ミリ秒)と4フレーム(67ミリ秒)です。1
私の読みでは、この4つの数字が、グリッドゲームにおける「レスポンスの良さ」の大部分を占めています。世界がセルの一部だけ動くことはないので、プレイヤーの意図は一歩単位で表現され、遅延は進行中の一歩の残りだけです。歩きなら最大268ミリ秒、走りなら134ミリ秒です。1 止まった状態からの方向転換は遅延ではなく、それ自体が目に見える結果を伴う別のアクションです。
2. エメラルドのカメラ、ドア、フェード、揺れ
カメラには遅れも先回りもない
エメラルドのカメラは、プレイヤーを追いかける目に見えないスプライトです。CameraObject_UpdateMoveは追いかける対象のスプライトのxとyをコピーし、前のフレームとの差をsCamera_MoveXとsCamera_MoveYに保存します。CameraUpdateCallbackがその差をCameraUpdateに渡し、CameraUpdateはちょうどそのピクセル数だけマップをスクロールします。212 フィールドのループは毎フレームRunTasks(); AnimateSprites(); CameraUpdate(); UpdateCameraPanning();をこの順で実行します。AnimateSpritesはスプライトのコールバックをスロット順に実行し、カメラのオブジェクトはプレイヤーのあとに作られる(InitPlayerAvatarのあとにInitCameraUpdateCallback(gPlayerAvatar.spriteId))ので、カメラはプレイヤーがその同じフレームで到達した位置を読むことになります。3 この順序は実行して確かめたのではなくコードを追って読んだものなので、測定ではなく読みです。ただ、そこから導かれる結果は単純です。プレイヤーの移動とスクロールは、同じフレームで、同じピクセル数だけ起こります。画面上ではプレイヤーはまったく動かず、足元で世界が滑っていきます。
Itay KerenはGDC 2015で横スクロールのカメラについて講演し、これをposition-locking(位置ロック)と呼びました。カメラはプレイヤーから離れず、「keeping the car in focus at all times and the camera motion completely predictable.」(車を常に画面の中心にとらえ、カメラの動きを完全に予測可能にする)というものです。23 エメラルドは、彼の定義が答えていないひとつのこと、つまりマップの端で何が起こるかに答えを足しています。止まらないのです。マップのレイアウトの外にあるセルはGetBorderBlockAtを通して読まれ、これはレイアウトの2×2の境界メタタイルを繰り返し、MAPGRID_IMPASSABLEの印を付けて返します。そのため視界はプレイヤーを画面上の同じ位置に保ったまま、外側を境界の木や水で埋めます。11 その代わりとなるKerenのedge-snapping(端へのスナップ)は、「simply snaps the camera to the edge of the level, allowing the character to move away from its anchor point.」(カメラをレベルの端にスナップさせ、キャラクターがアンカー位置から離れて動けるようにする)ものです。23 エメラルドは絵を描くことで、それを必要としない道を選びました。
ゲームフリークが書いて、オフにしたカメラ
field_camera.cには、自転車用の完成した先読みカメラが入っています。CameraPanningCB_PanAheadは縦方向のパンを1回の更新につき2ピクセルずつ、静止時の値32から、進行方向に応じて72またはマイナス8に向けてずらします。どちらに向かっても40ピクセルで、プレイヤーがこれから向かう先へとカメラが先導する仕組みです。12 これが動くのはgUnusedBikeCameraAheadPanbackが真のときだけですが、この変数には(bike.cで)FALSEしか代入されず、その分岐には「this code is never reached.」(このコードには決して到達しない)というコメントが付いています。1220 Kerenの用語でいえばdual-forward-focus(双方向の前方フォーカス)またはtarget-focus(ターゲットフォーカス)で、彼のルール「When you walk left, you want to see more to the left.」(左へ歩くときは、左側をより多く見たい)に従うカメラです。23 オフにされた理由が何であれ、出荷されたゲームはマッハ自転車の1フレーム4ピクセルでさえカメラを固定したままでした。1
ドアが開くのは16フレームではなく20フレーム
field_door.cのドアのテーブルは、各コマが4フレームかかるように読めます。sDoorOpenAnimFramesは{4, -1}, {4, 0}, {4, 0x100}, {4, 0x200}で、閉じたコマに開いたコマ3枚が続きます。24 しかしAnimateDoorFrameはカウンタが0のときに描画し、カウンタがその要素の時間と等しくなったときに次へ進むので、各コマは5回の更新のあいだ表示されます。1コマ84ミリ秒、4枚で335ミリ秒です。241 このシリーズの建物編の記事も、5回の更新、約84ミリ秒という同じ数字を挙げていました。13 その記事の元になった調査メモは「4 ticks each (16 frames, about 0.27 s at 59.7 Hz)」(各4ティック、16フレーム、59.7Hzで約0.27秒)と誤っていました。アプリ自身のドアのコードにあるコメントも同様で、「at about Emerald’s four ticks a frame」(エメラルドの1コマ4ティックくらいで)開くと書き、そのうえで最初のコマを70ミリ秒表示しています。どちらも本記事とブリーフで訂正します。1417
ドアへの出入りそのものを担うTask_DoDoorWarpは5つの状態からなり、どれも飛ばせません。ほかのオブジェクトを止め、ドアの音を鳴らし、プレイヤーの上にあるドアを開ける。MOVEMENT_ACTION_WALK_NORMAL_UPの一歩を強制して戸口に入らせる。プレイヤーが静止したら、ドアを閉めてプレイヤーを隠す。ドアのタスクが終わったら、音楽と画面をフェードさせる。マップを読み込む。25 出るときは儀式を逆にたどります。Task_ExitDoorはドアを最初から開いた状態で見せ、フェードインを待ち、WALK_NORMAL_DOWNの一歩を1回強制し、ドアを閉め、そこで初めて操作を返します。25
上の数とあとで示すフェードのトレースを足し合わせると、ドアが開くのに20フレーム、一歩に16フレーム、ドアが閉まるのに20フレームかかります。続くフェードは17フレーム目に最後のブレンドを行うので、画面が完全に暗くなるのは少なくとも73フレーム後、1.22秒後です。マップの読み込みが始まれるのは、フェードが22フレーム目に非アクティブになり、Task_WarpAndLoadMapがそれを確認して先へ進んだあとです。少なくとも79フレーム、1.32秒です。525 ドアの各段階のあいだの状態遷移や、音楽が止まるのを待つ時間はフレームを増やすことしかないので、どちらの数字も下限です。5
エメラルドの出入りはフレーム数から求めた下限です。Kiradexの行はアプリのドアのコードから読んだもので、スマートフォンで計時したものではありません。151721
フェードは9段階
エメラルドのワープのフェードは、なめらかな傾斜ではありません。BeginNormalPaletteFadeは0から16まで動くブレンド係数に2というステップを設定し、UpdateNormalPaletteFadeは1回目の呼び出しで背景パレットを、次の呼び出しでスプライトパレットをブレンドしてから係数を1ステップ進めます。そしてIsSoftwarePaletteFadeFinishingが最後に5回の呼び出しを追加します。26 各呼び出しがどのフレームに当たるかは、誰が呼ぶかによって決まります。ドアのタスクはRunTasksの中からフェードを開始し、BeginNormalPaletteFadeは自分で1回更新を実行し、その結果をすぐにパレットメモリへコピーして、本来なら次の更新に垂直ブランクを待たせるフラグをクリアします。同じフレームの後半で、OverworldBasicがもう一度UpdatePaletteFadeを呼びます。25263 つまり最初のフレームでは更新が2回(どちらもレベル0)、以降のフレームでは1回ずつ行われます。このスケジュールでpalette.cをPythonに移植し、タスクのフレームを0として数えると、レベルは0, 2, 4と進んで16までの9段階になります。背景パレットはフレーム1でレベル2、フレーム15でレベル16に達し、スプライトパレットは毎回1フレーム遅れ、最後のブレンドはフレーム16(フレーム0から数えて285ミリ秒)、フェードが非アクティブになるのはフレーム21(368ミリ秒)です。5 あるフレームで計算したブレンドは、そのフレームを終える垂直ブランクで画面に届きます。この移植は、事前にフェードが動いていなかったことと、通常のフィールド更新を前提にしています。雨、雪、霧、日陰、日照りが有効な場合、フェードアウトは天候で色付けされた色から同じように進みます(FadeScreenは色付けされたバッファをまずコピーし、それから同じBeginNormalPaletteFadeを呼びます)が、フェードインは天候のコードが実行するので、その経路はシミュレートしていません。5
16分の2ずつ離れた9段階で、背景とスプライトは1フレームずれます。ブリーフはこの傾斜を1枚のベールに置き換えて取り入れます。521
ワープは黒にフェードしますが、例外が一系統あり、その例外には向きがあります。WarpFadeOutScreenはマップ種別の組についてGetMapPairFadeToTypeに問い合わせ、WarpFadeInScreenはGetMapPairFadeFromTypeに問い合わせ、それぞれ答えが真なら白、偽なら黒でFadeScreenを呼びます。25 どちらもsTransitionTypesでその組を引きます。このテーブルの16行は、あらゆるマップ種別からMAP_TYPE_UNDERGROUNDへの出入りすべてで、前者は行の「入る」フラグを、後者は「出る」フラグを返します。これらが真になるのは、洞窟に入る行と洞窟から出る行だけです。27 したがって洞窟に入るときは画面が白へフェードアウトして黒からフェードインし、洞窟から出るときは黒へフェードアウトして白からフェードインします。各行にはそれぞれ専用の洞窟用トランジションルーチンも指定されていますが、私はそれを追っていません。27 もっと遅い白のフェード、遅延8のFadeInFromWhiteは、86または87フレーム後、1.44から1.46秒後に非アクティブになります。これはマップ読み込みのコールバックから開始され、その最初のフレームは追っていないので、移植では両方のケースを示しています。525
揺れはスクリプトで書かれたパン
エメラルドの画面の揺れは、物理的な効果ではなくスクリプトのコマンドです。ShakeCameraは4つのスクリプト変数から縦のパン、横のパン、揺れの回数、揺れどうしの間隔のフレーム数を読み、揺れるたびにパンの符号を反転させます。28 ゲームのスクリプト全体で、9ファイルに24回の呼び出しがあり、スクリプトでそのすべてを数えました。29
| 縦のpx | 横のpx | 揺れの回数 | 間隔のフレーム | 呼び出し数 | 長さ |
|---|---|---|---|---|---|
| 1 | 1 | 8 | 5 | 10 | 40フレーム、670 ms |
| 1 | 1 | 8 | 3 | 4 | 24フレーム、402 ms |
| 1 | 2 | 8 | 5 | 3 | 40フレーム、670 ms |
| 2 | 2 | 8 | 5 | 2 | 40フレーム、670 ms |
| 0 | 3 | 4 | 2 | 2 | 8フレーム、134 ms |
| 1 | 3 | 20 | 5 | 1 | 100フレーム、1,674 ms |
| 1 | 1 | 16 | 3 | 1 | 48フレーム、804 ms |
| 1 | 1 | 32 | 2 | 1 | 64フレーム、1,072 ms |
出典:data/**/*.inc全体に対するmeasure_gen3_shake.py。29
24回のうち10回は同じ揺れです。縦横1ピクセルずつ、8回の反転、5フレーム間隔、670ミリ秒。29 最大は横3ピクセル、最長は100フレーム、1.67秒です。29 建物編の記事で扱ったエレベーターの揺れ(移動した階数に応じて増える回数だけ、3フレームごとに揺れる)は別のルーチンで、この集計には含まれていません。13 私の読みでは、教訓はこの抑制にあります。エメラルドの揺れは1、2ピクセルで、スクリプトが重要だと判断した出来事にだけ使われ、足音やドアに使われることは決してありません。
3. 赤とクリスタル:毎秒30回の更新で同じ一歩
赤は2フレームごとに2ピクセル動く
『ポケットモンスター 赤』にはステップテーブルがありません。フィールドのループOverworldLoopはDelayFrameを呼んでからOverworldLoopLessDelayに流れ込み、そこでもう一度DelayFrameを呼ぶので、世界の更新は2フレームに1回です。30 一歩を始めるとwWalkCounterに8が入り、ループを1周するたびにAdvancePlayerSpriteがこれを1減らし、背景レジスタhSCXとhSCYを、一歩のベクトルを1ビット左シフトした量、つまり2ピクセルだけスクロールします。30 2ピクセルを8周で16フレームに16ピクセル。エメラルドと同じ268ミリ秒、毎秒3.73セルを、毎秒30回の更新で2ピクセルずつ跳びながら進みます。430 自転車は1周ごとにもう1回進める仕組みです。DoBikeSpeedupがAdvancePlayerSpriteをもう一度呼ぶ(サイクリングロードで上、左、右のいずれかを押しているときを除く)ので、一歩は8フレームになります。430
赤の歩行のコマは4周、つまり8フレームごとに切り替わり、直立、踏み出し、直立、反転した踏み出しの4枚をめぐります。したがって赤も一歩ごとに踏み出しのコマを1枚見せます。3122 UpdatePlayerSpriteは1周ごとにアニメーション内カウンタを増やし、それが4に達するとコマを進めます。3122
赤の方向転換はループ1周、2フレームです。止まった状態で新しい方向が入力されると、向きを書き換えて、一歩を踏み出さずにループへ戻ります。30 180度の方向転換では、コードの上に途中の向きが存在しますが、ソース自身のコメントによれば誰の目にも触れません。「It is unlikely for it to ever be visible because DelayFrame is called at the start of OverworldLoop.」(OverworldLoopの冒頭でDelayFrameが呼ばれるので、これが見えることはまずない)30 これはコードから読んだもので、エミュレータで実行してはいません。
ワープは音とフェードだけで、ドアは描かれません。PlayMapChangeSoundはタイルがドア(タイル$0b)ならSFX_GO_INSIDEを、そうでなければSFX_GO_OUTSIDEを鳴らし、続いてGBFadeOutToBlackが8フレーム間隔で4つのパレットを書き込みます。32フレーム、536ミリ秒です。432 赤のフェードインは追っていません。
クリスタルも2フレームごとに更新する
クリスタルは別の仕組みで赤のリズムを受け継いでいます。MaxOverworldDelayはdb 2で、VBlankハンドラがwOverworldDelayを数え下げるので、マップ上のオブジェクトは2フレームに1回更新されます。33 StepVectorsは歩行に2ピクセルの更新を8回、自転車に4ピクセルの更新を4回与えるので、1セルあたり16フレームと8フレームで、赤やエメラルドと同じです。4 遅い歩きは1ピクセルの更新が16回で32フレーム、毎秒1.87セルです。433
クリスタルのドアは白へ、しかも速くフェードします。MapSetupScript_DoorはFadeOutToWhiteで始まり、MapSetupScript_WarpはFadeInFromWhiteで終わります。どちらも2フレーム間隔のパレットのステップ4回で、片道8フレーム、134ミリ秒です。434 方向転換は4つの部分からなるステップ関数StepFunction_Turnで、各部分は次の部分へと流れ込みます。最初の更新では、.init1がOBJECT_STEP_DURATIONを2に設定して.step1に流れ込み、.step1がそれを1まで数え下げます。2回目の更新では、.step1が0まで数え、新しい向きを書き込んでふたたび2を設定する.init2を通って.step2に流れ込み、.step2がそれを1まで数えます。3回目の更新では.step2が0に達し、オブジェクトをSTEP_TYPE_FROM_MOVEMENTに返します。33 命令ごとに再現すると、このルーチンは3回の更新、つまり1回2フレームで6フレームかかり、新しい向きは2回目で書き込まれます。これはルーチン単体の話で、コードから読んだものです。ボタンが押されてからルーチンが始まるまでの時間は追っていません。33
3世代、3つのフェード
| 赤 | クリスタル | エメラルド | |
|---|---|---|---|
| ドア | 描かれない。音はタイルで選ばれる | 描かれない | 5フレームずつのコマ4枚(335 ms)、引き戸または開き戸の音つき |
| ドアの中へ | ドアに乗る一歩 | ドアに乗る一歩 | 上への強制的な一歩(16フレーム)、そのあとドアが閉まる |
| フェードアウト | 黒へ、8フレーム間隔のパレット4つ:32フレーム(536 ms) | 白へ、2フレーム間隔のステップ4回:8フレーム(134 ms) | 黒へ(洞窟に入るときは白へ)、9段階、ドアのタスクのフレームを0として最後のブレンドはフレーム16(285 ms)、フレーム21で非アクティブ |
| フェードイン | 追っていない | 白から、8フレーム | 黒から(洞窟から出るときは白から)、同じ9段階 |
| ドアから外へ | 下への強制的な一歩 | 強制的な一歩 | 開いたドアを表示、下への強制的な一歩、ドアが閉まり、それから操作が戻る |
出典:第1節から第3節の集計。14525 赤がドアから出るときの強制的な一歩、PlayerStepOutFromDoorは建物編の記事によります。13
3本すべてで一致する行こそ、残す価値のある行です。マップ間のフェード、プレイヤーに敷居をまたがせる強制的な一歩、そして儀式のあいだは入力を受け付けないこと。フェードの長さは4倍も違うので、私の読みでは長さは規範ではなくデザイン上の選択です。描かれたドアを前提に組み立てられているのはエメラルドの9段階のフェードで、Kiradexもドアを描いているので、13 ブリーフはこれを採用します。
赤、クリスタル、エメラルドは、ゲームボーイとゲームボーイアドバンスという2つの異なるハードウェアの3世代から1本ずつ選んだものですが、同じ契約に落ち着きました。一歩は16ピクセルで、約59.73ヘルツで16フレームかかり、始まったら中断できません。14 赤はループが1周に2フレーム待つので2ピクセルずつ跳んでそこへ至り、クリスタルは同じ2フレームの遅延と2ピクセルのベクトルで、エメラルドはフルフレームレートと1ピクセルの移動でそこへ至ります。走り、なみのり、自転車は、8フレームまたは4フレームでの同じ契約です。14 これらのゲームが「レールに乗っている」ような感触だと言われるとき、そのレールとはこれのことだと私は考えています。一歩も、コマも、カメラも同じフレームで数えられ、長さが一致するように選ばれているので、ばらばらになりようがないのです。
4. 現代の参照先:Celesteのカメラ、Kerenの語彙、スマートフォン版
カメラをなめらかにすべきとき、すべきでないとき
エメラルドの固定カメラは、Itay Kerenがまとめた語彙のなかのひとつの答えです。その語彙は「Scroll Back: The Theory and Practice of Cameras in Side-Scrollers」(横スクロールのカメラの理論と実践)にあります。これはGDC 2015のIndependent Games Summitでの彼の講演を手直ししたもので、2015年5月11日にGame Developerに掲載されました。23 本記事で使う用語は彼のものです。position-locking(位置ロック)はカメラをプレイヤーに固定します。edge-snapping(端へのスナップ)はレベルの端でカメラを止めます。camera-window(カメラウィンドウ)は、プレイヤーがウィンドウの縁を押したときだけカメラを動かします。lerp-smoothing(lerpスムージング)はカメラを目標に向けてイージングさせるもので、彼はこれを「a standard tool in reducing jarring camera speeds, particularly jumps.」(ぎくしゃくしたカメラの速度、特にジャンプを和らげる標準的な道具)と呼んでいます。target-focus(ターゲットフォーカス)とdual-forward-focus(双方向の前方フォーカス)は、進行方向へカメラを先回りさせます。彼はplatform-snapping(足場へのスナップ)、region-focus(領域フォーカス)、cue attractors(キューによる引き寄せ)も挙げていますが、グリッドを歩くゲームには使い道がありません。23
彼は、そもそもカメラの動きがなぜ重要なのかも述べています。「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.」(視覚と前庭感覚の信号の食い違いは不快感や吐き気につながりうる。3D、特にVRではもっと悪いが、2Dゲームでも十分に起こる)。そして、いちばん素朴な方式が正しいケースも示しています。位置ロックについて、「a crafting adventure game like Terraria, with a small character relative to the screen with pretty small jumps, it works very well.」(Terrariaのようなクラフト系アドベンチャーで、画面に対してキャラクターが小さく、ジャンプもかなり小さいなら、とてもうまく機能する)23 30ピクセルのコレクターがいる見下ろし型の町(このシリーズのキャラクター編の記事がビルド34で移行したサイズです)は、まさにそのケースからジャンプを取り除いたものです。35
もう一方の選択の現代における参照先がCelesteです。開発者たちはPlayerクラスを「as a learning resource and for general interest」(学習用の資料として、また一般的な興味のために)公開しており、MITライセンスはそのコードにだけ適用されます。カメラはその中の数行です。level.Camera.Position = from + (target - from) * (1f - (float)Math.Pow(0.01f / multiplier, Engine.DeltaTime))で、「Camera (lerp by distance using delta-time)」(カメラ。デルタタイムを使い、距離に応じてlerpする)というコメントの下にあります。3637 multiplierが1で目標が静止していれば、フレームレートにかかわらず毎秒距離の99パーセントを詰めます。ただしCelesteの目標は静止していません。プレイヤーの位置と状態から毎フレーム計算し直されるので、この数字はイージングの性質を表すもので、カメラが最終的にどこに落ち着くかを表すものではありません。36 目標は、ゲームの視界の中央にプレイヤーを置いた位置(X - Celeste.GameWidth / 2)を部屋の境界でクランプしたもので、いくつかの状態ではオフセットが加わります。StRedDashのダッシュでは進行方向に48ピクセル先、山頂への打ち上げでは64ピクセル上です。36 このシリーズの最初の記事は、Celesteが世界を320×180で描画し、6倍に拡大していることに触れました。22
私の読みでは、この2つは矛盾しません。Celesteがなめらかにするのは、プラットフォーマーではジャンプのたびに視界が上下に引きずられるからで、Keren自身のlerpスムージングの定義もジャンプについてのものです。グリッドを歩くゲームでは一定の速度でまっすぐ動くので、固定カメラでも和らげるべき衝撃は生じず、なめらかなカメラはもともと均一だった動きの後ろに尾を引かせるだけです。それがKiradexのモデルでどれだけの大きさになるかは、第7節で示します。
他のゲームが速度について教えてくれること
Stardew Valleyはプレイヤーの速度を単位のない数値で示しています。「2 when walking」(歩行時2)、「5 when running」(走行時5)、「6.6 when riding a Horse (7 if the horse was fed a carrot that day)」(馬に乗っているとき6.6、その日に馬にニンジンを与えていれば7)で、1を下回ることはありません。38 wikiは1単位が1ティックあたり何ピクセルなのかを書いていないので、ここではStardewについて毎秒何タイルという数字は出しません。
Sea of Stars、Eastward、CrossCodeのカメラについての一次資料も探しましたが、見つかりませんでした。移動や照明についてのインタビューはあってもカメラの話はなく、Sea of Starsの「Pixel Perfect」オプションを説明しているのは攻略サイトとフォーラムだけでした。Maddy Thorsonのカメラについての講演や記事も見つからなかったので、Celesteはコードから引用しています。39
スマートフォンでは、タップで歩き、逃げ道を用意する
Stardew Valleyのモバイル版には9種類の操作方式があり、デフォルトは「Tap-to-move & Auto-Attack」(タップ移動と自動攻撃)です。「Tap anywhere on screen and the farmer will walk to where you tapped.」(画面のどこかをタップすると、農夫がタップした場所まで歩く)40 指を置いたままにすると「will cause the character to follow the touch」(キャラクターがタッチを追いかける)とあり、wikiはこの追従モードが「is very literal, moving directly towards the finger without routing around blocking objects.」(非常に文字どおりで、障害物を迂回せずに指へ向かってまっすぐ動く)と注意しています。見えないジョイスティックの方式は「the left half of the screen」(画面の左半分)を使い、触れた場所が中心になります。そしてwikiはデフォルトの限界についても正直です。細かな位置合わせが必要な作業のなかには「can not be completed using the default controls; temporarily switching to a control style with a movement joystick is necessary in such cases.」(デフォルトの操作では完了できない。その場合は一時的に移動用ジョイスティックのある操作方式に切り替える必要がある)ものがあるのです。40 これらの方式は、TouchArcadeが2018年11月1日に報じたアップデートで追加されたもので、「the default tap-to-move and auto-attack controls.」(デフォルトのタップ移動と自動攻撃の操作)に戻すトグルも付いていました。41
スクウェア・エニックスのiOS版「ファイナルファンタジー ピクセルリマスター」は、App Storeの履歴で2025年3月11日付のバージョン1.2.0で、歩くか走るかのデフォルト設定を追加しました。「In tap based movement mode the character controlled will always run as the default speed when moving.」(タップ移動モードでは、操作キャラクターは移動時にデフォルトの速度として常に走る)42 モバイル版へのコントローラー対応は、TouchArcadeが2024年1月30日に取り上げたアップデートで先に入っていました。43 タッチでの移動方式そのものを正確に説明した資料は、これらの注記以外には見つけられませんでした。また、私が取得したwikiのページには、Terrariaのモバイル版での移動についての説明もありませんでした。39
AppleのHuman Interface Guidelinesは、プラットフォームの側から同じことを言っています。タッチ操作のゲームについては、「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」(よく使う操作は最低44x44ptにする)、「Always include visible and tactile press states」(押したときの視覚的・触覚的な状態を必ず用意する)とあり、歩きとダッシュについては「consider combining the actions into a single control.」(2つのアクションを1つの操作にまとめることを検討する)とあります。ページの変更履歴によれば、これらのタッチ操作の指針は2025年6月9日のものです。44 WWDC25のタッチ操作のセッションで、Appleは前提をはっきり述べました。「the vast majority of players won’t have a controller available.」(プレイヤーの大多数はコントローラーを持っていない)45
これらの資料のどれも、スマートフォンのゲームではタップで歩くのが決まりだとは言っておらず、私もそうは主張しません。私がそこから受け取るのは、発見ではなくKiradexへの私の推奨としての、Kiradexがすでに半分は従っている方式です。Stardewのモバイル版が出荷しているように、デフォルトはタップで歩く。HIGが勧めるように、精密な操作のために親指を置いた場所に現れるサムスティックを用意する。そして画面に固定された十字キーは置かない。4044
5. 技法を、数字つきのルールとして
第7節のブリーフは、以下のルールに沿って書かれています。どのルールも上の測定にさかのぼれます。「ティック」は1/60秒のシミュレーションの1ステップを指し、59.73ヘルツの1フレームをiPhoneが守れる単位に置き換えたものです。1セル16ティックなら、歩行は毎秒3.73セルではなく3.75セルになります。119
| 要素 | ルール | 出典 |
|---|---|---|
| 歩行 | 16ピクセルのタイルを1ティック1ピクセルで:1タイル16ティック、毎秒3.75タイル | 赤、クリスタル、エメラルドはどれも16ピクセルを16フレームで歩き、毎秒3.73セル |
| 走り | 1ティック2ピクセル、1タイル8ティック。16を割り切れない速度は使わない | エメラルドの走りとなみのりは2、自転車は4。不均等なのはダート自転車の2-3-3だけ |
| 歩行サイクル | どの速度でも16ピクセルにつき着地1回。Kiradexへの私の提案は、歩いた距離でコマを選ぶこと。32ピクセルで6枚なら、表示するコマはfloor(distance × 6 / 32) mod 6 |
エメラルドはコマと一歩を別々にフレームで計時し、長さを一致させている。歩行は16フレームのセル2つにわたる32フレームのサイクル、走りは8フレームのセル2つにわたる16フレームのサイクル |
| 一歩 | 始まったら最後まで進む。入力はタイルの上で読む | エメラルドと赤は一歩が完了したときにパッドを読む |
| 方向転換 | 止まった状態からは8ティック、約133 ms(エメラルドの8フレームは134)。歩行中は0 | エメラルドのWalkInPlaceFastは8。クリスタルの方向転換ルーチンは6フレーム、赤は2 |
| 拒否された一歩 | 無視せずに見せる:ぶつかりを伴う32ティックのその場歩き | エメラルドのWalkInPlaceSlowは32 |
| カメラ | プレイヤーに固定:遅れも先回りもなく、同じティックで同じピクセル数だけ動かす | エメラルドのカメラオブジェクト。ゲームフリークが書いた唯一の先読みはオフ |
| マップの端 | 外側を描いて固定を保つか、クランプする(端へのスナップ)。イージングは決してしない | エメラルドの境界タイル、Kerenの端へのスナップ、Celesteの部屋の境界 |
| ドア | 4枚のコマを各5ティック:1コマ83 ms、開くまで333 ms(エメラルドは84と335) | エメラルドのfield_door.c |
| フェード | 刻まれた9段階を2ティックごと、最後の段階はティック16、全体で18ティック(0.30秒)、アウトもインも。デフォルトは黒。複製ではなく翻案 | エメラルドの通常のフェードは、スプライトパレットでは偶数フレームで各段階に達し(背景パレットより1フレーム遅れ)、最後のブレンドはフレーム16、フレーム21で非アクティブになる。洞窟に入るときは白へフェードアウトし、出るときは白からフェードインする。クリスタルの8フレームが速い側の端、赤の32フレームが遅い側の端 |
| 出入りの儀式 | 入力なしで74ティック、約1.23秒:開く20、中へ一歩16、閉じる20、フェード18 | エメラルドのドアへの出入り:少なくとも73フレームで暗くなり、マップの読み込みは早くてもフレーム79(1.32秒) |
| 揺れ | 1ピクセル、8回の反転、5ティック間隔(40ティック、0.67秒)、スクリプトで決めた出来事にだけ使う | エメラルドの24回の揺れのうち最も多いもの |
| フレームレート | ディスプレイが何をしていても60でシミュレートする。要求するのは120ではなく60 | ゲームに対するAppleの30と60の優先度。RealityKitは通常60で描画する |
| ハプティクス | 一歩ではなく出来事を確認するために使い、オプションにする | ハプティクスの再生についてのAppleのHIG |
| 入力 | 私の推奨:デフォルトはタップで歩く。精密な操作のためのオプションとしてフローティングのサムスティック。固定の十字キーは置かない | Stardewのモバイル版のデフォルト、HIGのフローティングのサムスティック。どちらもルールとして述べてはいない |
表の出典:エメラルドの歩行、アニメーション、ドアの集計。1 コマと一歩の別々のタイマー。92 カメラ。123 フェードと揺れ。529 赤とクリスタル。433 KerenとCeleste。2336 Appleのフレームペーシング、RealityKit、ハプティクスの指針。7846 入力についての参照先。404244
このうち2つのルールには、それぞれ一言説明が必要です。歩行サイクルのルールは、現代のエンジンでいちばん間違えやすいものです。アニメーションシステムは自分の秒数を数え、歩行は世界の中の距離を数えるからです。携帯機は、両方を同じフレームで数え、長さが一致するように選ぶことで2つを揃えていました。フレーム時間が変動するスマートフォンでの私の提案は、歩いた距離からコマを読み取ることです。そうすれば、歩調を合わせるための第2の時計なしに同じ結果が得られます。そしてフレームレートのルールは、意欲が足りないということではありません。1ティックにちょうど1ピクセルずつ動く世界には、ティックとティックのあいだに見せるものがないので、速いディスプレイは同じ絵を繰り返すことしかできません。第6節では、実際にそのとおりになることを示します。
6. Appleのやり方:フレームペーシング、RealityKitの時計、ハプティクス
このシリーズの最初の記事は、Kiradexの世界が動いているエンジンを説明しました。2Dレンダラーとして使うRealityKitのシーン、平行投影のカメラ、テクスチャを貼った四角形を1枚のメッシュにまとめた地面、四角形で描く歩く者たち、そして毎フレーム整数のワールド単位に丸められるすべてのスプライトの位置です。22 本節は、その世界が時間のなかでどう動くかを決めるAppleのドキュメントの部分です。以下のページはどれも2026年10月4日にAppleが公開している状態で読んだもので、記載した対応OSは各ページに書かれているものです。
ProMotionでのフレームペーシング
CADisplayLink.preferredFrameRateRange(iOS 15.0、iPadOS 15.0、Mac Catalyst 15.0、visionOS 1.0)は設定ではなく要求です。15 ページの助言は「Choose a frame rate range that your app can consistently maintain」(アプリが一貫して維持できるフレームレートの範囲を選ぶ)というもので、システムがその要求をどう扱うかも説明しています。「The system typically provides a consistent frame rate by choosing one that’s a factor of the display’s maximum refresh rate.」(システムは通常、ディスプレイの最大リフレッシュレートの約数となるレートを選ぶことで、一貫したフレームレートを提供する)デフォルトでは、範囲はディスプレイの最大値と等しくなっています。15 範囲そのものは、最小、最大、推奨のレートからなるCAFrameRateRange(iOS 15.0)です。47
ProMotionについてのAppleの記事が数字を示しています。ProMotionディスプレイは、iPad Proでは24から120ヘルツ、対応iPhoneでは10から120ヘルツの間で切り替わり、iPhoneのレートは120、80、60、48、40、30、24、20、16、15、12、10ヘルツの12段階です。iPad Proはそのうちの5つです。7 対応機種の一覧には現在、「iPhone 13 Pro and later」(iPhone 13 Pro以降)と並んでiPhone Airと「iPhone 17 and later」(iPhone 17以降)が載っています。7 iPhoneでは、アプリのInfo.plistでCADisableMinimumFrameDurationOnPhone(iOS 15.0)をtrueにしない限り、60を超えることはありません。「If you don’t enable this support, Core Animation won’t access higher frame rates (above 60Hz).」(この対応を有効にしなければ、Core Animationはより高いフレームレート、つまり60Hz超を使わない)716 この記事のなかで、ゲームにとってほかの部分より重要な文が2つあります。ひとつは「In iOS 15 and later, the system provides games with special priority to 30Hz and 60Hz refresh rates to ensure optimal performance」(iOS 15以降、システムは最適なパフォーマンスを確保するため、ゲームに30Hzと60Hzのリフレッシュレートへの特別な優先度を与える)で、CAFrameRateRange(minimum: 30, maximum: 60, preferred: 60)で得られます。7 もうひとつは「Prepare your app to operate at any refresh rate, not just those it requests.」(要求したレートだけでなく、どんなリフレッシュレートでも動作するようアプリを準備する)です。7 そして、アニメーションするものすべてについて。「Always use targetTimestamp to drive any animation, physics, or other time-related content provided in your CADisplayLink callback」(CADisplayLinkのコールバック内で提供するアニメーション、物理、その他時間に関わるコンテンツは、常にtargetTimestampで駆動する)(targetTimestampはiOS 10.0)。748
WWDC21のセッション「Optimize for variable refresh rate displays」(可変リフレッシュレートのディスプレイに最適化する)は、iPad ProのProMotionとMacのAdaptive-Syncディスプレイの両方を扱っています。49 MacのAdaptive-Syncディスプレイについて、このセッションはAppleの以前の指針を変えています。固定レートのディスプレイでは「we’ve previously recommended that you slow down your rendering to hit the next factor of the display’s fastest refresh rate」(ディスプレイの最速リフレッシュレートの次の約数に合うよう描画を遅くすることを、以前は推奨していた)が、Adaptive-Syncでは「You should instead attempt to present frames at the highest rate your app can do so evenly.」(代わりに、アプリが均等に表示できる最高のレートでフレームを表示するよう試みるべきだ)というのです。49 私がここからスマートフォンのために受け取る言葉はevenly(均等に)です。
RealityKitの時計
Kiradexの世界はディスプレイリンクを持っていません。RealityKitのフレームごとのイベントSceneEvents.Update(iOS 13.0)、「An event invoked once per frame interval」(フレーム間隔ごとに1回呼ばれるイベント)で時を刻んでおり、そのdeltaTimeは「The elapsed time since the last update.」(前回の更新からの経過時間)です。5051 RealityView(iOS 18.0)は、フレームごとの処理にまさにその経路を記載しています。「you can use a System or directly subscribe to the engine’s SceneEvents.Update」(Systemを使うか、エンジンのSceneEvents.Updateを直接購読できる)とあり、独自のフレームレート設定は提供していません。52 RealityKitのパフォーマンスについてのAppleの記事は、どのレートを想定すべきかを述べています。「RealityKit typically limits the refresh rate」(RealityKitは通常、リフレッシュレートを制限する)とあり、それをフレームワークが画面のための更新を描画するレートと定義したうえで、「to 60 frames per second (fps).」(毎秒60フレーム、fpsに)と続けます。8 iPhone 18 Pro MaxやiPhone DuoのRealityViewが実際に60と120のどちらで描画するのかは、私は測っていません。第7節では、その測定をブリーフの最初のチェックにしています。
整数ピクセルの歩行に120ヘルツが何ももたらさない理由
算数を示します。第7節で現在のアプリのコードに使うのと同じモデルスクリプトを、今度は提案のほうに当てはめたものです。固定60ヘルツのティックで、1ティック1ピクセル進み、ディスプレイは最新のティックの結果をそのまま表示する世界です。60ヘルツのディスプレイでは、世界のどのピクセル位置もちょうど1回のリフレッシュのあいだ表示されます。6 120ヘルツのディスプレイでは、1秒間の61の位置のうち59がちょうど2回のリフレッシュ、2つが1回のあいだ表示されます。6 80ヘルツのディスプレイでは、表示が1回と2回のリフレッシュで交互になり、42の位置が1回、19の位置が2回表示されます。6 私の読みでは、120ヘルツの場合は60ヘルツの場合を2回ずつ描いたものにすぎず、80ヘルツの場合は不均等なペーシングです。あるピクセルが12.5ミリ秒とどまることもあれば、25ミリ秒とどまることもあります。
したがって、iPhone上のグリッドの世界にとっての問いは120ヘルツではなく、60での均等なペーシングです。固定60ヘルツのシミュレーションのティック、ティックごとの整数の移動、ティックで数えるアニメーション、そして最新のティックを表示するレンダラー。それによって、RealityKitがどのレートを選んでも動きがそれに左右されなくなります。WWDC21のセッションは、時間の差分について関連する点を指摘しています。遅いフレームのせいでディスプレイリンクがコールバックを1回飛ばすと、進めるべき差分は「not 8ms, but rather 16ms」(8msではなく16ms)になります。そしてセッションは、「uses time delta to advance the state of your custom drawing」(時間の差分を使って独自の描画の状態を進める)アプリは、コールバックが飛ばされるたびに「slow down your custom drawing by one frame」(独自の描画が1フレーム分遅れる)と述べています。私はこれを、実際に経過した時間ではなく想定どおりの8ミリ秒で進めるアプリのことだと読んでいます。セッションは、「track of a previous targetTimestamp so that you can advance the state correctly.」(状態を正しく進められるよう、前回のtargetTimestampを記録しておく)ことを勧めています。49
ハプティクス
Core Haptics(iOS 13.0、iPadOS 13.0、Mac Catalyst 13.0、visionOS 1.0)は、辞書から、CHHapticEventオブジェクトの配列から、またはAHAPファイルから、CHHapticPattern、つまり「An object representing a haptic waveform」(ハプティクスの波形を表すオブジェクト)を組み立てます。53 イベントのハプティクスの種類はhapticTransientとhapticContinuousの2つで、transientのほうは「brief impulses that occur at a specific point in time.」(特定の時点で起こる短いインパルス)です。54 それぞれhapticIntensity、hapticSharpness、attackTime、decayTime、releaseTime、sustainedのパラメータを取ります。55 これらを再生するのはCHHapticEngineで、デバイスが再生できるかどうかはcapabilitiesForHardware()が教えてくれます。56
もっと簡単な経路はUIImpactFeedbackGenerator(iOS 10.0)です。「A concrete feedback generator subclass that creates haptics to simulate physical impacts」(物理的な衝撃をシミュレートするハプティクスを作る、具象のフィードバックジェネレータのサブクラス)で、そのスタイル(light、medium、heavy、soft、rigid)は「The mass of the objects in the collision」(衝突する物体の質量)を表します。impactOccurred(intensity:)はiOS 13.0で、ページはinit(style:view:)を「Initializing the feedback generator」(フィードバックジェネレータの初期化)の項に、init(style:)をDeprecated(非推奨)の項に載せています。575859 prepare()がレイテンシを下げるのは、処理する時間があるときだけです。「Calling prepare() and then immediately triggering feedback (without any time in between) does not improve latency」(prepare()を呼んですぐに、間を置かずにフィードバックを発生させても、レイテンシは改善しない)とあり、エンジンは「A short period of time passes (typically seconds).」(短い時間、通常は数秒が経過する)とアイドル状態に戻ります。60 SwiftUIでは、sensoryFeedback(_:trigger:)(iOS 17.0)が「Plays the specified feedback when the provided trigger value changes」(指定したtriggerの値が変わったときに、指定したfeedbackを再生する)もので、.impact(weight:intensity:)も含まれます。61
ハプティクスの再生についてのHIGのページは、デザインの側の半分です。「Avoid overusing haptics」(ハプティクスを使いすぎない)46 その理由として、「Often, the best haptic experience is one that people may not be conscious of, but miss when it’s turned off.」(最良のハプティクス体験は、多くの場合、人が意識していないが、オフにすると物足りなく感じるものだ)。「Make haptics optional.」(ハプティクスはオプションにする)。そして「the intensity and sharpness of a haptic with the intensity and sharpness of the animation it accompanies.」(ハプティクスの強さと鋭さを、それに伴うアニメーションの強さと鋭さに)合わせる。46 鋭さは、「that’s soft, rounded, or organic, or one that’s crisp, precise, or mechanical.」(柔らかく、丸みがあり、有機的なもの、あるいはくっきりと正確で機械的なもの)という体験を伝えることができます。46 そしてカスタムのハプティクスによって、「a collision or a hit」(衝突や打撃)を、「from subtle experiences like the approach of footsteps or a looming danger.」(足音が近づいてくることや迫りくる危険のような、繊細な体験から)大きく異なるものとして感じさせることができます。46 毎秒3.75歩の歩行が何分も続くというのは、まさに最初のルールが想定しているケースです。1
入力
タッチ操作のAPIは、第4節の用語で言えば次のとおりです。Touch Controller(iOS 26.0、iPadOS 26.0、Mac Catalyst 26.0、visionOS 26.0)は、そのページで「Integrate onscreen touch controls into your Metal-based games」(Metalベースのゲームに画面上のタッチ操作を組み込む)と要約されています。ボタン、十字キー、サムスティック、スロットル、タッチパッドを、GCControllerを通して提供するものです。62 そのTCDirectionPad(iOS 26.0、iPadOS 26.0、Mac Catalyst 26.0)は、「to behave as either a composite direction pad」(ひとつの複合的な十字キーとして振る舞う)か、「as four separate buttons.」(4つの独立したボタンとして)振る舞うかを設定できます。6263 それ以前からあるGCVirtualController(iOS 15.0)は、「A software emulation of a real controller that you configure specifically for your game.」(自分のゲーム専用に設定する、実物のコントローラーのソフトウェアエミュレーション)です。64 試したアドレスのひとつ、developer.apple.com/documentation/touchcontrolsは404を返しました。フレームワークのページはtouchcontrollerです。39
コスト
本節の内容は、どれもスマートフォンで計時していません。デバイス上での世界のフレーム時間、60ヘルツのアキュムレータのコスト、世界が画面に出ているあいだハプティクスエンジンを動かし続けるコストは、いずれも未測定です。第7節のブリーフのチェックは、このうち最初の2つを測るように書かれています。
7. ブリーフ:Kiradexが作るものと、通るべきチェック
歩行の現状
2026年10月4日に、Kiradexのリポジトリにある世界の歩行、カメラ、トランジションのコードを、変更を加えずに読み、それをフレームごとに再現するモデルを書きました。この小節の内容はすべて、そのコードから読んだものか、モデルで計算したもののどちらかで、どちらなのかを明記しています。モデルはコードのFloat演算をひとつずつ32ビットで、コードと同じ順序で、Swiftの丸め方で、1フレームをぴったり1/60秒または1/120秒として実行します。マップの端、iPhone Duoの折り目、足元のオフセットは省いていますが、マップの中央をまっすぐ歩く限り、どれも結果を変えません。これはコードのモデルです。キャプチャではなく、スマートフォンでの実測は一度もしていません。176
コードから読んだ、コードがしていること。
- 入力はタップで歩くことだけです。 ステージはタップをワールド上の点に変換し、コレクターかキオスクをタップしたことにするか、
walk(to:)を呼びます。ドラッグも十字キーもなく、アプリのターゲットのどこにもハプティクスの呼び出しはありません。UIImpactFeedbackGenerator、sensoryFeedback、CHHapticを検索しても何も見つかりません。17 - 経路は8方向の最短経路探索です。 ここまでに歩いたコストの順に並べ、残りの距離の見積もりを使わないので、A*ではなくダイクストラ法です。斜めのコストは√2で、壁の角を斜めに横切ることはありません。斜めの一歩は横向きの姿勢をとり、走りの1.6を掛けたあとで進捗を√2で割るので、モデルでは60ヘルツで斜めの一歩に歩きで22フレーム、走りで14フレームかかります。まっすぐな一歩なら15フレームと10フレームです。176
- 速度は時間です。
tilesPerSecondは4、つまり毎秒64ピクセルで、経路が6歩以上になると歩きはその1.6倍の走りになります。そのためアプリでの歩きは1から5タイルで、それより長いものはすべて走りです。各フレームは一歩の進捗にdt × speed / lengthを加え、進捗が1に達すると歩く者は次のタイルにスナップし、進捗は0に戻り、行き過ぎた分は捨てられます。17 - 歩行のコマは時間で選ばれます。 列は
cycle[Int(walker.clock * framesPerSecond) % count]で、framesPerSecondは8、サイクルはフォージが作った6枚組の歩行と走りです。このシリーズのキャラクター編の記事は同じリズムを「the walk at eight frames a second」(毎秒8コマの歩行)と書いており、これはコードを正確に記述しています。1735 - カメラはイージングしてから丸めます。 毎フレーム、プレイヤーに向かって
(target - current) * min(1, dt * 6)だけ動き、それから両方の軸を整数単位に丸めます。マップが視界より大きいときは、マップの範囲にクランプされます。最初の記事の、すべてのスプライトとカメラは「are rounded to whole world units each frame, after easing」(イージングのあと、毎フレーム整数のワールド単位に丸められる)という一文も正確です。1722 - ドアとワープ。 ドアはシートの半開きのコマをすぐに表示し、70ミリ秒後に開いたコマを表示し、その1.4秒後にドアを取り除きます。ワープに乗る一歩は220ミリ秒待ってから場所を切り替えます。建物編の記事は、これを既知の課題として挙げていました。1713
- トランジションはナビゲーションのpushです。 デフォルトのアニメーションのままで、階の移動はステージをidentityで差し替えるだけでフェードはなく、出るときは
dismiss()を呼びます。アプリで唯一のベールはカードビューアの黒いオーバーレイで、.easeOut(duration: 0.25)でアニメーションします。17 - フレームレートの要求はありません。 アプリにもプロジェクトファイルにも、
CADisplayLink、preferredFrameRateRange、CADisableMinimumFrameDurationOnPhoneはありません。世界はイベントのdeltaTimeを使ってSceneEvents.Updateで時を刻みます。17
一定のフレーム時間のもとで、そのコードが画面上で何をするかについてモデルが示すこと。
- 60ヘルツではタイルごとに1回、2ピクセルのつまずき。 60ヘルツでは歩行に1タイル15フレーム、毎秒4.00タイルかかり、各タイルの16ピクセルは1ピクセルの14フレームと2ピクセルの1フレームで構成されます。タイルごとに2ピクセルの跳びが1回あり、5タイルの歩行では5回です。エメラルドはすべてのフレームでぴったり1ピクセル動きます。61
- 120ヘルツでは0と1が不規則に並ぶ。 120ヘルツでは歩行に1タイル30フレームかかり、各タイルの16ピクセルは1ピクセルの16フレームと0ピクセルの14フレームが不規則に混ざったものになります。6
- 走りは定数より遅い。 60ヘルツでは走りに1タイル10フレーム、毎秒6.00タイルかかります。4×1.6なら6.4になるはずですが、各タイルの行き過ぎた分が捨てられるからです。各タイルの16ピクセルは1ピクセルの4フレームと2ピクセルの6フレームです。120ヘルツでは1タイル19フレーム、毎秒6.32タイルになり、走りの速度がフレームレートに依存しています。6
- 追いつかないまま後ろを引きずるカメラ。 60ヘルツで歩いているあいだ、カメラはタイルを1枚進むごとに約1ピクセルずつ遅れていきます。最初のタイルの終わりで5ピクセル、アプリが行う最長の歩行である5タイル目の終わりで9ピクセルです。そして画面上のプレイヤーの位置は、最初のフレーム以降の歩行の73フレームのうち8フレームで変化します。6タイル以上のすべての経路である走りでは、2タイル目以降13ピクセル遅れます。120ヘルツでは、歩きでも走りでも最初のタイルのうちに9に達し、そのまま保たれます。プレイヤーが止まると、カメラは60ヘルツではプレイヤーから4ピクセル、120ヘルツでは9ピクセルずれたところで静止し、そのままになります。差に
min(1, dt × 6)を掛けた値が半ピクセルを下回ると、丸めによって毎フレーム同じ位置が返されるからです。どちら側にずれて止まるかは、最後に歩いた方向で決まります。エメラルドでは、遅れも静止時のずれも0です。6 - コマは独自の時計で進む。 毎秒8コマの6枚組サイクルは0.75秒続きます。60ヘルツで、連続するサイクルの開始どうしのあいだのモデル上の位置から測ると、歩きで3.00タイル、走りで4.50タイル進みます(走りの名目上の毎秒6.4タイルなら4.80ですが、捨てられる行き過ぎ分のせいで、そこには届きません)。エメラルドの着地2回のサイクルは2.00タイルです。私の読みでは、着地の回数より5割増しの地面を進むサイクルは、足が滑っているように見えます。ただしモデルは、足が地面のどこにあるかを測っていません。また、60ヘルツでも120ヘルツでも、15フレームごとにコマが紙一重の状態になります。時計に8を掛けた値がちょうど整数、つまり2枚のコマの境目に乗るのです。アプリのように時計を64ビットではなく32ビットで持つと、60ヘルツで12タイル歩くあいだのそうしたフレーム11個のうち9個で、120ヘルツでは23個のうち14個で、表示されるコマが変わります。6
- 一歩の途中で2回目のタップをすると、歩く者が引き戻される。 これはモデルでもキャプチャでもなく、コードから読んだものです。
walk(to:)は、歩く者のタイルがまだ一歩の出発点であるあいだに進捗を0にするので、次のフレームでは歩く者が最大15ピクセル後ろに描かれます。サーバーから届くほかのコレクターの移動でも同じことが起こります。17
表示フレームごとのピクセル数:原典は均等で、今日のコード(キャプチャではなくモデル)は均等ではなく、固定のティックなら120でも均等です。621
モデルで見た、イージングして丸めるカメラ:歩きでも走りでも後ろを引きずり、歩行が終わると手前で止まります。621
スマートフォンでの実測をしていないので、これはどれも手に持ったときの感触についての判断ではありません。実際のフレーム時間は揺らぐので、1と2の正確なパターンは変わるでしょう。遅れと静止時のずれもフレーム時間に依存します。イージングのステップmin(1, dt × 6)がフレーム時間に依存するからで、モデルが静止時のずれを60ヘルツで4ピクセル、120ヘルツで9ピクセルとしているのはそのためです。私の読みでは、スマートフォンが通常示す範囲の揺らぎであれば、大きさは変わっても消えはしません。条件は、フレームが1/6秒、約167ミリ秒より短いままであることです。その長さになるとイージング係数が1に達し、カメラは1フレームでプレイヤーの位置に着地するので、十分に長いつまずきがあればそのフレームで差は埋まります。176 コマと歩行の食い違いはフレームレートに依存しません。毎秒8コマの時計と毎秒4タイルの歩行では、60ヘルツで1サイクル3.00タイル、120ヘルツでもほぼ同じです。走りのほうは依存し、60ヘルツで1サイクル4.50タイル、120ヘルツで4.75タイルです。タイルごとに捨てる行き過ぎ分がフレームレートに依存するからです。6
ブリーフ
各項目は、Kiradex/World/への変更、その理由、そしてログの行、スクリプト、キャプチャのいずれかで検証できるチェックからなります。どのゲームのアセット、名前、音も提案には含めていません。数字は仕組みであり、アート、音、言葉はKiradex自身のものです。
1. 時計ではなくティックで歩く。
変更。 リグの更新に60ヘルツのアキュムレータを追加します。各コールバックは自分のdtを加えます。その時点でアキュムレータが8ティック分(133ミリ秒)を超える時間を抱えていたら、超過分を捨て、そのミリ秒数をdropped行としてモーションログに書きます。それからアキュムレータにある整数個のティックをすべて実行し、そのたびに1ティック分を差し引きます。アキュムレータは時間を浮動小数点の秒ではなく、整数の単位(ナノ秒、またはモデルで使う1/60,000,000秒)で数えます。DoubleやFloatのアキュムレータでは、10秒間のテストの600番目のティックが、レートによっては境界にわずかに届かず、数が599になってしまうからです。これが溜まった遅れについてのルールです。8ティックまでの遅れは次のコールバックで取り戻し、それを超える分は中断とみなすので、世界は追いつこうと先を急ぐのではなく、止まったところから再開します。8という数は、ProMotion対応iPhoneについてのAppleの一覧にあるすべてのレートを、1コールバックに6ティック必要な10ヘルツまで含めてまかなえるよう選んでいます。7 モデルでは、12のレートそれぞれで10秒分のコールバックを実行すると、ちょうど600ティックが実行され、何も捨てられません。1コールバックあたり4ティックの上限にすると、12ヘルツでは480、10ヘルツでは400しか実行されません。6 60ヘルツで1秒のつまずきがあった場合、このルールでは次のコールバックで8ティック、以降は1ティックずつ実行し、866.7ミリ秒を捨てたとログに残します。溜まった遅れを捨てずに4の上限をかけた場合は、19回連続のコールバックで4ティックずつ実行することになり、これこそこのルールが防ぐための早送りです。6 各歩く者は端数のある進捗の代わりに一歩のティック数を持ち、描画上のオフセットはその数に1ティックあたりのピクセル数を掛けたものになります。厳密な整数なので、歩く者はもう丸める必要がありません。歩きは1ティック1ピクセル、1タイル16ティック、走りは1ティック2ピクセル、8ティックで、経路が6歩以上なら走りという今のルールは維持します。経路探索が許している斜めの一歩は、コードがすでに意図している√2とその順序、つまり走りのペースを掛けてから√2で割る順序を維持します。歩きの斜めは23ティック(16√2は約22.6で、切り上げ)、走りの斜めは12ティック(8√2は約11.3で、切り上げ)で、それぞれfloor(16 × t / 23)またはfloor(16 × t / 12)が変わるティックで動き、各軸で16ピクセル進みます。17 歩きでは23ティックのうち16ティックで1ピクセル、残りの7ティックで0ピクセル。走りでは12ティックのうち8ティックで1ピクセル、4ティックで2ピクセルです。ブリーフのなかで唯一の不均等な移動で、エメラルドではダート自転車が唯一の不均等な移動であるのと同じです。1 捨てるべき行き過ぎ分はありません。
理由。 第5節の歩きと走りのルール。そしてモデルが示した、60ヘルツでのタイルごとの2ピクセルのつまずきと、120ヘルツでの不規則な0と1。61
チェック。 起動引数-motionLogで、毎ティックのティック番号とプレイヤーのx, y、そしてすべてのdropped行をログに出します。各方向に1タイルずつ歩く既存の向きのデモで、スクリプトが次のことを確認します。歩行中のすべてのティックでちょうど1ピクセル動くこと、各タイルが16ティックかかること、2ピクセル動くティックがないこと。各方向に歩きの斜めと走りの斜めを1歩ずつ行う斜めのデモでは、23ティックと12ティック、1歩あたり各軸16ピクセル、軸ごとの移動が歩きでは0か1ピクセル、走りでは1か2ピクセルであること、走りの斜めが歩きの斜めより速いことを確認します。単体テストは、アキュムレータに厳密な整数単位で作ったdtの列を与えます。120から10ヘルツまでの12のiPhoneのレートそれぞれで10秒間なら、何も捨てずに600ティックを実行すること。60ヘルツで1秒の空白があれば、次のコールバックで8ティック、以降は1コールバックに1ティックを実行し、866.7ミリ秒が捨てられたとログに残ること。iPhone 18 Pro MaxとiPhone Duoの内側ディスプレイで取った同じモーションログが、1タイルあたり同じティック数を示すこと。ディスプレイのレートで歩行が変わってはなりません。
2. 歩行のコマは距離で選ぶ。
変更。 この項目は私の提案であって、複製ではありません。エメラルドはコマを一歩に合わせてフレームで計時しており、距離から読み取ってはいません。92 歩いているあいだは、歩き始めてから完了した歩数に現在の一歩のティック数をその長さで割ったものを足した、歩数で数えた歩行の進み具合から列を選びます。walkCycle[floor(steps × walkCycle.count / 2) % walkCycle.count]で、2歩につき着地2回です。走りも同じです。まっすぐな一歩なら、これは歩いた距離を32ピクセルで割ったもので、floor(distancePx × 6 / 32) mod 6になります。斜めの場合は、√2倍の長さではなく一歩そのものを数えるので、歩幅は長くなりますが、着地は変わらずどの一歩でも最初に来ます。まっすぐな歩幅のために描かれたコマを前提にした、私の選択です。歩行が終わったら、エメラルドと同じく直立のコマを表示します。待機、まばたき、エモートの時計はそのままにします。エメラルドのようにティックで計時するサイクルでも一歩に合わせることはできますが、32ティックに6枚のコマでは5ティックと6ティックの不均等な表示時間が必要になります。進み具合で数えれば第2のテーブルが要らず、アプリがどんな移動の種類を追加しても成り立ちます。
理由。 どの速度でも16ピクセルにつき着地1回、そして移動からずれていくことのないリズム。今のサイクルはモデルで歩き3.00タイル、走り4.50タイルです。16 これでキャラクター編の記事の「eight frames a second」(毎秒8コマ)にも決着がつきます。1ティック1ピクセルなら、32ピクセルで6枚のコマは5.33ピクセルごとに切り替わり、毎秒8コマではなく11.25コマになります。35
チェック。 モーションログと表示中の列から確認します。2枚の接地のコマ(フォージのサイクルではwalk_aとwalk_d)が、60ヘルツでも120ヘルツでも、32ピクセルごとの0ピクセルと16ピクセルの位置(前後1ピクセルの誤差を許す)に現れること。斜めのデモでは、各一歩の最初のティックに現れること。
3. 一歩は最後まで進める。タップは残す。フローティングのスティックを足す。
変更。 一歩の途中のタップは、出発点からではなく、いま向かっているタイルから経路を計画し、現在の一歩を先に終わらせます。歩数がリセットされることはありません。サーバーから届くほかのコレクターの移動にも同じルールを適用します。止まった状態から、最初の一歩で向きが変わるタップをした場合は、動き出す前に8ティックかけて方向転換します。プレイヤーの隣の通れないセル、またはプレイヤーの目の前のタイルをタップすると、そちらを向き、項目6の拒否のハプティクスとともに32ティックのぶつかりを再生します。指を置いたままにすると、Stardewのデフォルトと同じくその指を追いかけますが、指がタイルをまたぐたびに既存の経路探索で経路を引き直します。Stardewの追従は障害物を迂回しませんが、私たちのものは迂回できるからです。フローティングのサムスティックは4方向で、デフォルトではオフ、親指を置いた場所に現れ、世界の上に重ねたSwiftUIのドラッグジェスチャで作ります。描画するものはどれも44×44ポイント以上にします。Touch Controllerのサムスティック(iOS 26)がドラッグに取って代わるのは、テストビルドでそれがRealityViewの上に描画されることが確認できた場合だけです。Appleはこのフレームワークを「Metal-based games」(Metalベースのゲーム)向けと説明しており、WWDC25のセッションは「integrates directly with Metal」(Metalと直接統合される)と述べていますが、Kiradexの世界はRealityViewが描いていて、独自のレンダーパスを持たないからです。4562
理由。 第5節の一歩、方向転換、拒否された一歩、入力のルール。14044624517
チェック。 UIテストで東に6タイル先をタップし、その120ミリ秒後に北に6タイル先をタップします。モーションログに、プレイヤーのxが減るティックがひとつもなく、方向転換の前に最初の一歩のタイルが完了していることが示されること。UIテストで家の横の壁をタップします。ログに向きの変化と32ティックのぶつかりが示され、位置が変わらないこと。スティックをオンにして、動き出した最初のティックから60ティックのあいだ右に倒し続けると、ティック48までに3タイルを終え、スティックがまだ倒されているうちに4タイル目を始め、離したあとのティック64でそれを終えること。ログには、プレイヤーが東に4タイル進んだ整数のタイル上にいて、離した時点で進行中だった一歩が完了しており、タイルとタイルのあいだで止まっていないことが示されること。
4. カメラを固定する。
変更。 イージングをやめ、プレイヤーが動くのと同じティックで、整数のワールドピクセルだけからカメラを設定します。camera = clamp(player + foldOffset, low, high)です。各項を整数に保つのは、今は最後の丸めだけがカメラを整数にしているからで、境界と折り目のオフセットには端数があります。プレイヤーの位置は項目1によって整数です。followがポイントにdisplayScale / pixelScaleを掛けて計算する折り目のオフセットは、折り目が変わったときに一度だけ整数のワールドピクセルに丸めます。クランプの境界は内側に向けて、つまり下限は切り上げ、上限は切り捨てで丸めます。ワールドピクセルで見た視界の半分の大きさは整数とは限らないからです。fitは視界のスクリーンピクセルを、1テクセルあたりの整数のスクリーンピクセル数で割ります。たとえば3倍の393×852ポイントの視界なら7になり、視界の幅は168.43ワールドピクセル、半分の幅は84.21です。カメラの境界は85と、マップの幅から85を引いた値になるので、どのフレームもマップの端の外側を見せません。176 マップが視界より小さいときは、今のクランプが許しているとおり境界が入れ替わり、同じく内側に丸めることでマップ全体が視界に収まります。マップへのクランプは維持します。第5節のルールはクランプと外側を描くことのどちらも認めており、Kiradexのカメラがマップの端を越えて森へ出るのは、Duoが半分開いているときに、整数タイル分の余白だけです。17 折り目のオフセットは、折り目が変わるまでは固定として扱います。aからbに変わるときは、iを0から8まで動かしてa + round((b − a) × i / 8)の段階を、フェードと同じリズムの2ティックに1段階で進めます。こうすれば途中の位置がすべて整数ピクセルになります。たとえば0からマイナス37への変化なら、0, −5, −9, −14, −19, −23, −28, −32, −37を通ります。6 森の視差は、固定したカメラから計算し、今と同じく丸めたまま維持します。タップした目的地に向けた先読みはしません。ゲームフリークは先読みを作ってオフにしましたし、タップで歩く場合、目的地はすでに画面上にあります。12
理由。 第5節のカメラのルール。そしてモデルが示した遅れ(歩くにつれて増え、5タイル目で9ピクセル、走りでは13ピクセル)、それによる画面上のプレイヤーの位置の変化、4ピクセルと9ピクセルの静止時のずれ。6
チェック。 モーションログから、開けた広場を歩くあいだのすべてのティックで、カメラからプレイヤーを引いた値が一定(0、または折り目のオフセット)であり、プレイヤーが止まったあとも同じであり、カメラの値がすべて整数であること。単体テストで3倍の393×852ポイントの視界をfitに渡し、プレイヤーをマップの両端まで歩かせ、カメラの位置が整数で、85とマップの幅から85を引いた値で止まることを確認します。続けて同じテストで折り目を変え、カメラが2ティック間隔の整数ピクセルの位置を9つ通って新しいオフセットに達すること、その途中のどのティックでも境界を出ないことを確認します。キャプチャで確かめる場合、歩行のコマは目印になりません。足の形がコマごとに変わるからです。デバッグビルドで、ほかのどこにも使われていない色の1テクセルの目印をプレイヤーのエンティティの位置に描き、開けた広場での6タイルの歩行のすべてのフレームで、スクリプトがそれを見つけます。すべてのフレームで同じスクリーンピクセルにあること。マップの端の近くでは目印が動き、カメラは動かず、その動きは整数ピクセル単位だけであること。
5. ドア、一歩、フェードを、Kiradex自身のアートで。
変更。 ドアに入る場合、つまりドアのシートを持つワープの場合です。今のドアは、歩く者がドアのセルに着地したあとで開きます。一歩のonStepがそのタイルでワープを見つけ、その場でopenDoorを呼ぶのです。17 エメラルドは、閉じたドアの上にプレイヤーを立たせることはありません。TryDoorWarpが発動するのは、下のセルにいるプレイヤーが北のドアに向かって押したときだけで、Task_DoDoorWarpは1セル上のドアを開けてから、その上への一歩を強制します。6525 ブリーフはこれに合わせて、きっかけを1セル手前に移します。
- 経路の次の一歩がドアのワープへの一歩であれば、歩行はその手前のセルで止まり、そこで出入りがティック0から始まります。入力を止め、ドアの音を鳴らします。音はKiradexのものです。
- 今の「すぐに半開きのコマ、70ミリ秒で開いたコマ」の代わりに、5ティックずつの4つのスロットでドアを開けます。エメラルドの4枚のコマを各5フレームで表示するやり方です(1秒60ティックで1スロット83ミリ秒、エメラルドの5フレームは84。全体で20ティック)。スロットは、閉、半開き、開、開です。Kiradexのドアのシートは閉、半開き、開の3枚(フォージの
door_sheet()は3枚を描き、アプリはSpriteSheet.bundled(name, columns: 3, rows: 1)で読み込みます)で、エメラルドのドアは閉と開いたコマ3枚なので、4つ目のスロットでも開いたコマを表示し続けます。フォージが半開きと開のあいだに4枚目のコマを描けば、代わりにそのスロットを埋めることになりますが、それはオプションのアートであって必須ではありません。「four ticks」(4ティック)と書いたコメントを直します。2417 - プレイヤーをそのセルからドアのセルへ、16ティックかけて強制的に1歩歩かせます。町のドアは建物のいちばん下の列にあり、両側と上は建物のセルで、経路は壁の角を斜めに横切らないので、この一歩は必ず下のセルからの上向きの一歩になり、エメラルドと同じです。17
- 歩く者を隠し、スロットを逆順(開、開、半開き、閉)にたどってドアを閉めます。20ティックです。
- 世界の上に黒いベールを、刻まれた9段階の不透明度(0、2/16と進んで16/16まで)でフェードさせます。段階
iはフェードのティック2iに置くので、最後の段階はティック16に来て、ティック17まで保たれます。18ティックです。これは選んだうえでの翻案で、エメラルドのタイミングそのものではありません。エメラルドのスプライトパレットはこのベールと同じ偶数フレームで各段階に達し、背景パレットはその1フレーム前に達し、フレーム16の最後のブレンドのあとで5回の仕上げの更新を実行して、フレーム21で非アクティブになります。1枚のベールには、遅れる第2の層も、仕上げるべきものもありません。5 RealityViewそのものを抱えるビューの不透明度は、決してアニメーションさせないでください。最初の記事は、カードビューアでその教訓を得ています。22 - アニメーションを無効にして行き先をpushし、同じ9段階でベールをフェードアウトさせます。
- 室内のマットに上向きで到着します。出るときは逆です。ドアが開いた状態でドアのセルに到着し、16ティックで下へ強制的に1歩、ドアが閉まり、それから入力を受け付けます。
今はフェードなしでステージを差し替えている階の移動は、ドアも強制的な一歩もなしで、同じベールを使います。今はdismiss()を呼んでいる部屋からの退出は、ベールと外への強制的な一歩を使います。
理由。 第5節のドア、フェード、儀式のルール。今は一歩から220ミリ秒後に場所が切り替わり、ドアは70ミリ秒間隔で2枚のコマを見せ、画面は横へスライドします。1517
チェック。 モーションログは、ドアの下で歩行が止まったティックからティックを数えます。ログには、プレイヤーがまだそのセルにいること、ドアのスロットがティック0、5、10、15(閉、半開き、開、開)にあること、強制的な一歩がティック20から35で1ティック1ピクセルずつ進み、ティック35でドアのセルに達し、それより前には達しないこと、閉じるスロットがティック36、41、46、51にあること、ベールの最初の段階がティック56にあること、そしてpushが早くてもティック74であることが示されます。この流れを経ずにドアのセルで終わる歩行は、チェックに落ちます。モーションログは、ベールの不透明度も毎ティック記録します。出ていくときはティック56から73に9つの異なる値がそれぞれ2ティックずつ、入ってくるときも同じ9つの値です。画面録画では、絵のうち静止している部分で確認します。フレーム全体では役に立ちません。地面の水は、水のあるマップでは毎秒4回コマが変わり、ほかのコレクターが歩いているかもしれないからです。そこでスクリプトは、あらかじめ選んでおいた建物の壁の一部、つまりアニメーションするタイルがなく、録画中に歩く者が上を通らない部分を調べ、出ていくときに9つ、入ってくるときに9つの異なる段階を数え、それぞれが2フレームずつ(60ヘルツで前後1フレームの誤差を許す)保たれていることを確認します。17 ワープの画面録画で、世界が水平方向に平行移動することは決してありません。ナビゲーションのスライドはなしです。
6. ハプティクス:3つのイベントと、スイッチ。
変更。 世界が持つCHHapticEngineをひとつ用意し、世界が表示されたときに開始し、消えたときに停止します。設定にはハプティクスのスイッチを置き、デフォルトでオンにして、システムのハプティクス設定を尊重します。パターンはAHAPファイルとしてバンドルに入れます。
| イベント | パターン | 理由 |
|---|---|---|
| 拒否された一歩(ぶつかり) | hapticTransientをひとつ、強さ0.4、鋭さ0.2 |
HIGの衝撃は「a thud when two heavy objects collide」(2つの重い物体がぶつかるときのドスンという音)。アートが柔らかいので、柔らかく |
| ドアが開く | 開くにつれて絵が変わる各スロットで、強さ0.3、鋭さ0.6のhapticTransient。ティック5の半開きのコマと、ティック10の開いたコマ(83 msと167 ms) |
伴うアニメーションに合わせる |
| カードを掲げる(3Dのカード) | 頂点に達したときに、0.7と0.8のhapticTransient |
世界にある唯一の本物の物体 |
| 足音 | デフォルトではなし | 毎秒3.75歩が何分も続くのは、HIGが警告する使いすぎそのもの |
強さと鋭さの値は私の出発点であって測定値ではなく、デバイスで調整するためのものです。capabilitiesForHardware()がCore Hapticsを使えないと示す場合の代替はUIImpactFeedbackGenerator(style: .soft, view:)で、歩き始めにprepare()を呼びます。
理由。 ハプティクスのルール。第6節で引用したAppleの指針。46565760
チェック。 -hapticLog引数で各イベントをログに出します。スクリプトで行うドアへの出入りでは、項目5の数え方でティック5と10に、ドアのイベントがちょうど2つ記録され、一歩ごとのイベントはひとつも記録されないこと。スイッチをオフにすると、何も記録されないこと。
7. フレームレート:60で、均等に。
変更。 Info.plistは変更しません。CADisableMinimumFrameDurationOnPhoneは追加しないでください。世界は120から何も得られないからです。RealityViewは維持します。項目1のティックによって、RealityViewがどのレートで描画しても動きはそれに左右されません。将来ディスプレイリンクを追加する場合は、Appleのゲーム優先度を得るためにCAFrameRateRange(minimum: 30, maximum: 60, preferred: 60)を設定します。76
チェック。 iPhone 18 Pro Maxと、iPhone Duoの外側と内側のディスプレイで、60秒の歩行のあいだのSceneEvents.Update.deltaTimeのヒストグラムをログに出します。ブリーフは最頻値が16.7ミリ秒だと想定しています。もし8.3なら、項目1のアキュムレータが歩行を1タイル16ティックに保ち、ログがそれを証明します。モーションログのdropped行は、どちらのレートでも通常の歩行ではゼロであること。
8. 揺れは、ひとつの瞬間にだけ。
変更。 整数テクセルのカメラの揺れを、1テクセル、8回の反転、5ティック間隔で、それに値するひとつのイベントにだけ使います。たとえば広場でのレアカードの公開で、カードを掲げるハプティクスと組み合わせます。ドア、一歩、到着には使いません。
理由。 エメラルドの24回の揺れのうち最も多いもの、そしてその抑制。29
チェック。 モーションログで、カメラのオフセットがティック0、5と続いて35までプラス1とマイナス1テクセルを交互に取り、ティック40で0に戻ること。
このブリーフに含めないもの
- グリッド上でのカメラのスムージングや先読み。 なめらかにすべきジャンプはなく、ゲームフリークは先読みをオフにして出荷しました。1223
- 世界のための120ヘルツ。 目標は60での均等なペーシングです。120では各ピクセルが2回表示されるだけです。6
- デフォルトでの一歩ごとのハプティクス。46
- 画面に固定された十字キー。 私の推奨は、タップで歩くことと、オプションのフローティングのスティックです。44
- ゲームのドアやワープの音、ジングルのいずれも。 音は表現であり、Kiradexには自分の音が必要です。
- 自転車、なみのり、氷、水の流れ。 アプリの歩く者たちは歩きと走りしかしません。上の速度は、それが変わるときのために記録しておくものです。17
残る問い:デバイス上でのタイミング
本節のモデルの数字はどれも、フレームのあいだが一定の1/60秒または1/120秒であることを前提にしています。iPhone 18 Pro MaxやiPhone DuoのRealityViewが60と120のどちらで描画するのか、そのdeltaTimeがどれだけ揺らぐのかは未測定です。最初に実行するのは項目7のヒストグラムで、項目1は、その答えがAppleのiPhoneの一覧にあるどのレートであっても歩行に何の変化ももたらさないように書かれています。687
重要なポイント
アートを描く人へ
- 歩行は距離に対して描きましょう。エメラルドの歩行は32ピクセルにわたる2回の踏み出しと2回の直立で、一歩に合ったフレーム数だけ表示されます。速い移動ではコマを増やさずに同じコマを短い表示時間で使い回すので、どの速度でも16ピクセルにつき着地は1回です。192
- 6枚組のサイクルでも構いません。ただし、エンジンがそれを一定の距離にわたって再生することが条件です。別に計時される歩行に対して一定のレートで再生すると、リズムが移動からずれていきます。私たちのコードのモデルでは、1サイクルで歩き3.00タイル、走り4.50タイルです。6
- エメラルドのドアは閉と開いたコマ3枚で、それぞれ84ミリ秒ずつ画面に表示されます。Kiradexのように閉、半開き、開の3枚のシートなら、開いたコマを2スロット表示し続けることで4つのスロットを埋めるか、フォージに4枚目を描かせます。どちらにしても、開いた状態は見る価値のあるものとして描きましょう。12417
エンジンを作る人へ
- 世界は固定のティックで、タイルを割り切る整数ピクセルの移動で進め、入力はタイルの上で読み、レンダラーには最新のティックを表示させましょう。溜まった遅れをどう扱うかを明記してください。私たちのものは8ティックまで取り戻し、残りは捨ててログに残します。お手本はエメラルドのステップテーブルで、どれも合計16になります。216
- カメラは同じティックでプレイヤーに固定し、境界とオフセットを整数ピクセルにして、あとから丸める必要がないようにしましょう。イージングしてから丸めると、歩くプレイヤーの後ろを引きずり、私たちのコードのモデルでは、次に歩くまで4から9ピクセル手前で止まったままになります。36
- iPhoneでは、ピクセルの動きのために120ヘルツを要求しないでください。Appleはゲームに30と60を優先し、RealityKitは通常60で描画し、120での整数ピクセルの歩行は各ピクセルを2回のリフレッシュにわたって表示するだけです。786
- フェードはなめらかな傾斜ではなく段階で。エメラルドのドアのフェードは16分の2ずつ離れた9段階で、最後のブレンドは17フレーム目、フェードの終了は22フレーム目です。私たちの18ティックのベールはその傾斜の翻案であって、複製ではありません。5
- RealityViewを抱えるSwiftUIのビューの不透明度は、決してアニメーションさせないでください。上にベールをかぶせましょう。22
ゲームのループを設計する人へ
- エメラルドでは、ドアへの出入りは入力を受け付けない少なくとも1.32秒の儀式です。そして敷居をまたぐ強制的な一歩があるからこそ、中へ歩いて入ったように読めます。525
- 8フレームのその場での方向転換があれば、プレイヤーは動かずに何かのほうを向けます。32フレームのぶつかりは、一歩が拒否されたことを伝えます。1
- スマートフォンでの私の推奨:Stardewのモバイル版が出荷しているように、デフォルトはタップで歩く。HIGが勧めるように、精密な操作の代替としてフローティングのスティックを用意する。固定の十字キーは置かない。4044
- ハプティクスは足音ではなく出来事を確かめるために使い、スイッチを付けましょう。46
よくある質問
ポケモンのプレイヤーはどれくらいの速さで歩きますか?
赤、クリスタル、エメラルドでは、歩きの一歩は16ピクセルのセル1つを、毎秒59.7275フレームで16フレームかけて進みます。1セル268ミリ秒、毎秒3.73セルです。エメラルドは毎フレーム1ピクセル、赤とクリスタルは2フレームごとに2ピクセル動きます。エメラルドの走りとなみのり、赤とクリスタルの自転車は、1セル8フレーム、毎秒7.47セルです。1419
ポケットモンスター エメラルドの歩行サイクルは何フレームですか?
32フレームにわたる4つの要素です。踏み出しのコマを8フレーム、直立のコマを8フレーム、もう一方の踏み出しを8フレーム、直立を8フレーム。これは歩行でセル2つ分なので、一歩ごとに踏み出しと直立を1回ずつ見せ、脚は一歩ごとに交互になります。走りは、8フレームのセル2つにわたる16フレームのサイクルです。192
ポケモンのカメラはプレイヤーより遅れますか?
いいえ。エメラルドのカメラはプレイヤーの位置をコピーし、同じフレームで同じピクセル数だけマップをスクロールするので、世界が動くあいだプレイヤーは画面上に固定されたままです。マップの端でも止まりません。外側はレイアウトの境界タイルで描かれます。自転車用の先読みカメラはコードに存在しますが、オンになることはありません。231112
ポケットモンスター エメラルドのドアとフェードのトランジションはどれくらいの長さですか?
ドアは5フレームずつのコマ4枚で開き(335ミリ秒)、プレイヤーは16フレームの強制的な一歩で中へ入り、ドアは20フレームで閉まり、画面は9段階でフェードします。最後のブレンドはフェードの17フレーム目、フェードの終了は22フレーム目で、次のマップの読み込みが始まれるまで少なくとも79フレーム、1.32秒です。洞窟に入るときは画面が黒ではなく白へフェードアウトし、洞窟から出るときは白からフェードインします。152527
ピクセルアートのゲームはProMotion対応iPhoneで120Hzで動かすべきですか?
整数ピクセルの動きなら、その必要はありません。60ヘルツのティックごとに1ピクセル動く世界は、120ヘルツでは各ピクセルを2回のリフレッシュにわたって表示するだけで、80では表示時間が不均等になります。ProMotionについてのAppleの記事によれば、ゲームには「special priority to 30Hz and 60Hz」(30Hzと60Hzへの特別な優先度)があり、iPhoneのアプリが60を超えるにはCADisableMinimumFrameDurationOnPhoneを設定する必要があり、RealityKitは通常60で描画します。固定60ヘルツのティックでシミュレートし、ディスプレイはなりゆきに任せましょう。67168
歩くときにスプライトの足が滑るのはなぜですか?
たいていは、歩行のコマがひとつの時計で、移動が別の時計で動いていて、長さが一致していないからです。Kiradexの現在のコードは、毎秒4タイル歩きながら6枚組のサイクルを毎秒8コマで再生するので、コードのモデルが示すとおり、1サイクルで2タイルではなく3タイル進みます。携帯機はどちらもフレームで数え、各一歩のコマをその一歩と同じ長さだけ続けます。歩いた距離からコマを選ぶこと、つまり私がKiradexに提案している方法なら、第2の時計なしに同じ一致が得られます。どちらにしてもリズムが移動からずれることはありえません。足が地面にとどまって見えるかどうかは、コマそのものにも左右されます。619
モバイルのピクセルゲームに仮想十字キーを使うべきですか?
私の資料の読みでは、デフォルトにはすべきではありません。Stardew Valleyのモバイル版のデフォルトはタップ移動で、精密な作業のためのほかの方式のひとつとして見えないジョイスティックがあります。AppleのHIGは、オブジェクトを直接タップすることと、「wherever the player lands their thumb instead of a static thumbstick position.」(固定位置のサムスティックではなく、プレイヤーが親指を置いた場所に)現れるサムスティックを勧めています。4044
このサイトの関連記事:iPhoneでつくるピクセルアートの世界はこのシリーズの最初のガイドで、エメラルドの踏み出しと直立の歩行、この世界が動いているRealityKitのレシピ、カードビューアの不透明度の教訓を扱っています。ピクセルアートの人物:iPhoneでのキャラクターとクリエイターは2本目で、本記事が時計から距離へとタイミングを移す6枚組の歩行を扱っています。ピクセルアートの建物:iPhoneでの家、ホール、室内は3本目で、第2節がタイミングを測ったドア、ワープ、階を扱っています。開発者のためのiPhone DuoとiPhone Duoに向けてアプリを準備するは、ブリーフのカメラが避けて通る2つのディスプレイと折り目を扱っています。RealityKitの空間的メンタルモデルは、SceneEvents.Updateの背後にあるエンティティとシステムのモデルを説明しています。
出典
-
著者による測定、2026年10月4日:
measure_gen3_motion.py(本記事用の著者の証拠フォルダ内)を、pretのpokeemeraldのコミット731ad5bに対して実行。src/event_object_movement.cのステップ関数テーブルとInitMoveInPlaceの長さ、src/data/object_events/object_event_anims.hのアニメーションテーブル、src/sprite.cの遅延カウンタの規則、src/field_door.cのドアのフレームを解析し、フレームを59.7275ヘルツで換算する。出力は隣にmeasure_gen3_motion.out.txtとして保存(速度は毎秒3.73、7.47、9.95、14.93、29.86セル。その場歩きは32、16、8、4フレーム。sAnim_GoSouthは32フレーム。ドアのコマは5回の更新、83.7 ms表示され、4枚で335 ms)。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
pret、
pokeemerald/src/event_object_movement.c(sStep1FuncsからsStep8Funcs、およびコメント「Over the course of the step animation, these sum to 16 pixels (one full metatile)」。sStepTimesと、sTimerで1フレームに1要素ずつステップテーブルを引くNpcTakeStep。一歩の開始時に移動に応じたアニメーションと交互の切り替えを設定するSetStepAnimHandleAlternation。CameraObject_UpdateMove)、コミット731ad5b、2026年10月4日閲覧、https://github.com/pret/pokeemerald/blob/master/src/event_object_movement.c ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
pret、
pokeemerald/src/overworld.c(OverworldBasicの順序RunTasks(); AnimateSprites(); CameraUpdate(); UpdateCameraPanning();と、同じ関数内でその後に続くUpdatePaletteFade()。TransferPlttBufferを呼ぶVBlankCB_Field。InitCameraUpdateCallback(gPlayerAvatar.spriteId)より前のInitPlayerAvatar)およびsrc/sprite.c(AnimateSpritesはコールバックをスロット順に実行する)、2026年10月4日閲覧、https://github.com/pret/pokeemerald/blob/master/src/overworld.c および https://github.com/pret/pokeemerald/blob/master/src/sprite.c 。同じフレームで起こるという結論は著者によるコードの読みであり、実行したものではない。 ↩↩↩↩↩↩↩ -
著者による測定、2026年10月4日:
measure_gen12_motion.py(本記事用の著者の証拠フォルダ内)を、pretのpokeredのd2704a6(home/overworld.asm、home/fade.asm)とpokecrystalの5beda23(engine/overworld/events.asm、engine/overworld/map_objects.asm、data/maps/setup_scripts.asm、engine/tilesets/timeofday_pals.asm)に対して実行。出力はmeasure_gen12_motion.out.txtとして保存(赤:16フレームで16 px、自転車は8フレーム、ワープのフェードは32フレーム。クリスタル:歩行は2 pxの更新が8回、自転車は4 pxが4回、遅い歩きは1 pxが16回で毎秒1.87セル、ドアのフェードは片道8フレーム)。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
著者による測定、2026年10月5日:
measure_gen3_fade.py(本記事用の著者の証拠フォルダ内)。pretのpokeemerald/src/palette.c(731ad5b)からBeginNormalPaletteFade、保留中の転送フラグを含むUpdatePaletteFade、UpdateNormalPaletteFade、IsSoftwarePaletteFadeFinishingをPythonに移植したもので、呼び出し元のスケジュールで実行する。ドアのタスクがRunTasksの中でフェードを開始する(Task_DoDoorWarp、src/field_screen_effect.c)。BeginNormalPaletteFadeは1回更新し、バッファをパレットメモリにコピーしてフラグをクリアする。OverworldBasic(src/overworld.c)が同じフレームでもう一度更新する。以降の各フレームは1回ずつ更新し、VBlankの転送がフラグをクリアする。フレームはタスクのフレームを0として数える。事前にフェードが動いていなかったことを前提とする。雨、雪、霧、日陰、日照りが有効な場合、src/field_weather.cのFadeScreenは天候で色付けされたバッファをコピーしてから同じBeginNormalPaletteFadeを呼ぶ(フェードアウトに対する天候自身のハンドラはDoNothing)ので、フェードアウトのスケジュールは成り立つが、フェードインは天候のコードを通るためシミュレートしていない。ワープ後のフェードインはマップ読み込みのコールバックから始まり、その最初のフレームは追っていないため、両方のケースを示す。出力はmeasure_gen3_fade.out.txtとして保存(レベルは0から16まで2刻み。最初に見えるブレンドはフレーム1。最後のブレンド、つまり16でのスプライトパレットはフレーム16で、フレーム0から数えて285 ms。フレーム21で非アクティブ、368 ms。遅延8のFadeInFromWhiteはフレーム85または86で非アクティブ、1,440または1,457 ms。ドアへの出入り:開く20、一歩16、閉じる20フレーム、最後のフェードのブレンドは少なくとも73フレーム後、1.22 s。Task_WarpAndLoadMapがフェードの非アクティブ化を待つため、WarpIntoMapは早くてもフレーム79、1.32 s)。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
著者によるモデル、2026年10月5日:
measure_kiradex_motion.py(本記事用の著者の証拠フォルダ内)。Kiradex/World/PlazaRig.swiftからtilesPerSecond(4)、framesPerSecond(8)、runPace(1.6)、走りのしきい値(6歩)、カメラのmin(1, dt * 6)を、Kiradex/World/TileMap.swiftからcentreを、scripts/forge/rig.pyから6枚組のwalkとrunのサイクルを読み込み、advance、place、animate、followをフレームごとにモデル化する。すべてのFloat演算を32ビット(numpyのfloat32)でコードの順序どおりに、Swiftの「0から遠い方へ丸める」半端処理で行い、ぴったり1/60 sと1/120 sで、5タイルの歩行、12タイルにわたって保った歩きのペース、12タイルの走りを実行し、それぞれのあとに3 s立ち止まる。マップのクランプ、折り目、足元のオフセットは省く。コードのモデルであり、デバイスからのキャプチャではない。出力はmeasure_kiradex_motion.out.txtとして保存。60 Hzの歩行:1タイル15フレーム、各タイルは1 pxの14フレームと2 pxの1フレーム。カメラの差は最初の5タイルの終わりで5、6、7、8、9 px、歩きのペースを保った場合は9タイル目から13。5タイルの歩行の最初のフレーム以降の73フレームのうち8フレームで画面上の位置が変化。静止時のずれ4 px。120 Hz:1タイル30フレーム、1 pxが16、0が14。差は9、静止時9。60 Hzの走り:1タイル10フレーム、毎秒6.00タイル、1タイルあたり1 pxが4フレーム、2 pxが6フレーム。差は2タイル目から13、静止時4。120 Hzの走り:1タイル19フレーム、毎秒6.32タイル。差9、静止時9。60 Hzでシミュレートした位置から求めた、連続するサイクルの開始どうしのあいだの移動量:歩き3.00タイル、走り4.50タイル(名目上の毎秒6.4タイルなら4.80)。120 Hzでは歩き3.00と2.94、走り4.75。現在のコードでの斜めの一歩:60 Hzで歩き22フレーム、走り14フレーム、120 Hzで43と27。時計、位置、カメラを倍精度にした同じモデルと比べると、位置とカメラの値はすべて変わらず、60 Hzで9個、120 Hzで14個の歩行のコマが異なり、すべて時計に8を掛けた値が整数になるフレームで起こる(12タイルの歩行のうち、そうしたフレームは60 Hzで11個、120 Hzで23個)。提案:固定60 Hzのティックは、60 Hzでは各ピクセルを1回のリフレッシュ、120 Hzでは61の位置のうち59を2回のリフレッシュ、80 Hzでは1回または2回のリフレッシュ(42と19の位置)表示する。アキュムレータ:120から10 Hzまでの12のiPhoneのレートそれぞれで10秒分のコールバックを実行すると、溜まった遅れの上限を8ティックにした場合は600ティックを実行して何も捨てず、1コールバック4ティックの上限では12 Hzで480、10 Hzで400。60 Hzで1 sのつまずきのあと、8ティックのルールは8ティック、以降1コールバック1ティックを実行して866.7 msを捨て、溜まった遅れを保ったまま4ティックの上限をかけると19回連続のコールバックで4ティックずつ実行する。カメラの計算:3倍の393×852 ptの視界に対するfitは、1テクセル7スクリーンピクセル、168.43×365.14ワールドピクセルの視界、半分の幅84.21、内側に丸めた境界85とマップの幅から85を引いた値を与える。0から−37への折り目の変化を9段階で行うと、0、−5、−9、−14、−19、−23、−28、−32、−37を通る。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple Developer Documentation「Optimizing iPhone and iPad apps to support ProMotion displays」(iPhoneの10から120 Hzの範囲と12のレート、対応機種の一覧、
CADisableMinimumFrameDurationOnPhone、30 Hzと60 Hzのゲーム優先度、「Prepare your app to operate at any refresh rate」、targetTimestamp)、2026年10月4日閲覧、https://developer.apple.com/documentation/quartzcore/optimizing-iphone-and-ipad-apps-to-support-promotion-displays ↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple Developer Documentation「Improving the Performance of a RealityKit App」(「RealityKit typically limits the refresh rate」を毎秒60フレームに)、2026年10月4日閲覧、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)およびsrc/sprite.c(フレームの長さから1を引いた値が入るanimDelayCounterをContinueAnimが数え下げ、0になると次のフレームに進む)、2026年10月4日閲覧、https://github.com/pret/pokeemerald/blob/master/src/data/object_events/object_event_anims.h および https://github.com/pret/pokeemerald/blob/master/src/sprite.c ↩↩↩↩↩↩↩↩↩↩↩ -
pret、
pokeemerald/src/field_player_avatar.c(PlayerWalkNormal、PlayerRun、コメント「same speed as running」付きのPlayerWalkFast、CheckMovementInputNotOnBikeとTURN_DIRECTION、PlayerTurnInPlace、壁へのぶつかり)、2026年10月4日閲覧、https://github.com/pret/pokeemerald/blob/master/src/field_player_avatar.c ↩↩↩↩↩ -
pret、
pokeemerald/src/fieldmap.c(GetBorderBlockAt:レイアウトの2×2の境界メタタイル、MAPGRID_IMPASSABLEの印付き)、2026年10月4日閲覧、https://github.com/pret/pokeemerald/blob/master/src/fieldmap.c ↩↩↩ -
pret、
pokeemerald/src/field_camera.c(CameraUpdate。sVerticalCameraPanを静止値32から72またはマイナス8に向けて2ずつ動かすCameraPanningCB_PanAhead。gUnusedBikeCameraAheadPanbackで守られ、「this code is never reached」とコメントされている)、2026年10月4日閲覧、https://github.com/pret/pokeemerald/blob/master/src/field_camera.c ↩↩↩↩↩↩↩↩ -
Blake Crosley「Pixel-Art Structures: Houses, Halls and Interiors on iPhone」、blakecrosley.com、2026年10月3日(エメラルドのドアのコマは5回の更新、約84ミリ秒表示。赤の
PlayerStepOutFromDoor。エメラルドのエレベーターの揺れ。課題として挙げたKiradexの70ミリ秒のドアと220ミリ秒のワープ)、https://blakecrosley.com/blog/pixel-art-structures-on-iphone ↩↩↩↩↩↩ -
建物編の記事のための著者の調査メモ、Kiradexリポジトリ(非公開)、
docs/research/structures/01-structures-in-the-canon.md、2026年10月3日。エメラルドのドアのフレームを「4 ticks each (16 frames, about 0.27 s at 59.7 Hz)」としていた。本記事で示したfield_door.cのAnimateDoorFrameの読みによって訂正。 ↩↩ -
Apple Developer Documentation「preferredFrameRateRange」(
CADisplayLink。iOS 15.0、iPadOS 15.0、Mac Catalyst 15.0、visionOS 1.0)、2026年10月4日閲覧、https://developer.apple.com/documentation/quartzcore/cadisplaylink/preferredframeraterange ↩↩↩ -
Apple Developer Documentation「CADisableMinimumFrameDurationOnPhone」(Information Property Listのキー。iOS 15.0、iPadOS 15.0)、2026年10月4日閲覧、https://developer.apple.com/documentation/bundleresources/information-property-list/cadisableminimumframedurationonphone ↩↩↩
-
著者によるKiradexリポジトリ(非公開)の読み、コミット
b1b78b1、2026年10月4日、読み取りのみ:Kiradex/World/PlazaRig.swift(tilesPerSecond、framesPerSecond、runPace、walk(to:)とそのsteps.count >= 6の走りのルール、進捗のステップがdt * Self.tilesPerSecond * (walker.running ? Self.runPace : 1) / lengthであるadvance、animate、place、視界のスクリーンピクセルを整数のpixelScaleで割るfit、折り目のポイントをdisplayScale / pixelScaleで換算し、min(1, dt * 6)でイージングしてから丸め、Duoが半分開いているときだけbeyondMargin、12タイル分マップの端を越えて森へカメラを出すクランプを持つfollow、折り目にかかわらず森を配置するbuild、水のあるマップで地面の水のコマを毎秒4回変えるripple、openDoorとそのコメント「at about Emerald’s four ticks a frame」)、Kiradex/World/PlazaStage.swift(タップの処理、220ミリ秒のワープの遅延、SceneEvents.Updateの購読、.idによる階の差し替え)、Kiradex/World/TileMap.swift(8方向のpath。ここまでのコストが最小の開いたタイルを取り出し、一歩ごとに1または√2を加え、目的地までの距離の見積もりを使わない。斜めの一歩は両側のセルが歩ける場合を除いて飛ばす)、Kiradex/World/WorldMap.swift(ワープのドアのシート、「closed, half open, open」)、そのシートをSpriteSheet.bundled(name, columns: 3, rows: 1)で読み込み、列1、続いて列2を表示するopenDoor、scripts/forge/kit.py(door_sheet()、「The three frames side by side, 48 × 32」)、scripts/forge/town.pyとscripts/forge/buildings.py(町のドアのワープはどれも建物のいちばん下の列のドアのセルにあり、どのドアについても左、右、上の建物のセルは通れない。出荷されたtown.jsonの町のドア7つすべてで確認)、Kiradex/Views/Card/CardViewer.swift(ベールの.easeOut(duration: 0.25))、scripts/forge/rig.py(サイクル)。UIImpactFeedbackGenerator、sensoryFeedback、CHHaptic、CADisplayLink、preferredFrameRateRange、CADisableMinimumFrameDurationOnPhoneがないことは、2026年10月4日にKiradex/とproject.ymlを検索して確認し、ひとつも見つからなかった。同じ検索でKiradex/にMTKView、MTLRenderCommandEncoder、TouchControllerも見つからなかったので、世界は独自のレンダーパスを持たない。一歩の途中のタップによる引き戻しはwalk(to:)から読んだもので、キャプチャしたものではない。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
pret、
pokered(コミットd2704a6、2026年9月22日)、pokecrystal(5beda23、2026年9月29日)、pokeemerald(731ad5b、2026年10月1日)のシャロークローン。市販されたゲームのコミュニティによる逆コンパイル、https://github.com/pret ↩ -
Martin Korth、GBATEK「LCD Dimensions and Timings」(280,896サイクル、1フレーム16.743 ms、「ca. 59.737 Hz」)、2026年10月4日閲覧、https://problemkaputt.de/gbatek-lcd-dimensions-and-timings.htm 。Pan Docs「Rendering」(「One frame: 70224 dots @ 59.7 fps」)、2026年10月4日閲覧、https://gbdev.io/pandocs/Rendering.html 。59.7275という数字は著者の計算:2^24 / 280,896、および4,194,304 / 70,224。 ↩↩↩↩↩
-
pret、
pokeemerald/src/bike.c(sMachBikeSpeedCallbacks、上限2のbikeFrameCounter、PlayerRideWaterCurrentを呼ぶAcroBikeTransition_Moving、唯一の代入gUnusedBikeCameraAheadPanback = FALSE)、2026年10月4日閲覧、https://github.com/pret/pokeemerald/blob/master/src/bike.c ↩↩↩ -
図は著者のスクリプト
make_figures.py(svgkit.pyを使用)で2026年10月4日に描画。データはディスク上のもののみ:原典の速度はmeasure_gen3_motion.out.txtとmeasure_gen12_motion.out.txtから。Kiradexの歩行とカメラはmeasure_kiradex_motion.py自身のsimulate()から(5タイルの歩行の最初の1秒。60 Hzと120 Hzでの5タイルの歩行と60 Hzでの12タイルの走りのカメラ)、提案のティックのループも同じスクリプトから。エメラルドのフェードはmeasure_gen3_fade.pyのrun()、つまり呼び出し元のスケジュールで実行する移植から、ドアの図のフェードと最も早いマップ読み込みも同じものから。Kiradexのドアのタイミング(70 ms、1,400 ms、220 ms)はb1b78b1のPlazaRig.swiftとPlazaStage.swiftから読んだもの。ゲームのアートは一切使っていない。 ↩↩↩↩↩ -
Blake Crosley「Pixel-Art Worlds on iPhone: What the 16-Bit Masters Knew」、blakecrosley.com、2026年10月3日(エメラルドの歩行「step, stand, step, stand at eight ticks each」、赤の「stand, step, stand, step-flipped」、320×180で描画し6倍に拡大するCeleste、RealityKitのエンジン、「rounded to whole world units each frame, after easing」される位置、カードビューアの不透明度の教訓)、https://blakecrosley.com/blog/pixel-art-world-on-iphone ↩↩↩↩↩↩↩↩
-
Itay Keren「Scroll Back: The Theory and Practice of Cameras in Side-Scrollers」、Game Developer、2015年5月11日。GDC 2015のIndependent Games Summitでの講演を手直ししたもの(position-locking、edge-snapping、camera-window、lerp-smoothing、target-focus、dual-forward-focus、各引用)、2026年10月4日閲覧、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}。カウンタ0で描画し、カウンタがフレームの時間と等しくなると進むAnimateDoorFrame)、2026年10月4日閲覧、https://github.com/pret/pokeemerald/blob/master/src/field_door.c ↩↩↩↩ -
pret、
pokeemerald/src/field_screen_effect.c(Task_DoDoorWarpの各状態。5つ目はWarpFadeOutScreenを呼び、タスクをTask_WarpAndLoadMapに渡す。Task_WarpAndLoadMapはWarpIntoMapの前にPaletteFadeActive()、つまりgPaletteFade.activeと、BGMusicStopped()を待つ。Task_ExitDoor、GetMapPairFadeToTypeを呼ぶWarpFadeOutScreen、GetMapPairFadeFromTypeを呼ぶWarpFadeInScreen、FadeScreen(FADE_FROM_WHITE, 8)を使うFadeInFromWhite)、2026年10月4日閲覧、https://github.com/pret/pokeemerald/blob/master/src/field_screen_effect.c ↩↩↩↩↩↩↩↩↩↩ -
pret、
pokeemerald/src/palette.c(deltaY = 2を設定し、自らUpdatePaletteFadeを呼び、パレットメモリへのCpuCopy32とsPlttBufferTransferPending = FALSEを行うBeginNormalPaletteFade。そのフラグが立っているあいだはすぐに戻り、更新のたびにgPaletteFade_selectedPalettesからフラグを立てるUpdatePaletteFade。フラグをクリアするTransferPlttBuffer。UpdateNormalPaletteFade。IsSoftwarePaletteFadeFinishing)、2026年10月4日閲覧、https://github.com/pret/pokeemerald/blob/master/src/palette.c ↩↩ -
pret、
pokeemerald/src/fldeff_flash.c(MAP_TYPE_UNDERGROUNDへの出入りの16行からなり、各行に「入る」フラグ、「出る」フラグ、トランジションルーチンを持つsTransitionTypes。行の「入る」フラグを返すGetMapPairFadeToTypeと「出る」フラグを返すGetMapPairFadeFromType)、2026年10月4日閲覧、https://github.com/pret/pokeemerald/blob/master/src/fldeff_flash.c ↩↩↩ -
pret、
pokeemerald/src/field_specials.c(VAR_0x8004からVAR_0x8007より縦のパン、横のパン、揺れの回数、遅延を読み、揺れるたびにパンの符号を反転させるShakeCamera)、2026年10月4日閲覧、https://github.com/pret/pokeemerald/blob/master/src/field_specials.c ↩ -
著者による集計、2026年10月4日:
measure_gen3_shake.py(本記事用の著者の証拠フォルダ内)は、pretのpokeemerald(731ad5b)のdata/**/*.inc全体から、special ShakeCameraの呼び出しすべてと、その前にある4つのsetvarの値を読む。出力はmeasure_gen3_shake.out.txtとして保存(9ファイルに24回の呼び出し、表に示した8通りのパラメータとその回数)。 ↩↩↩↩↩↩ -
pret、
pokered/home/overworld.asm(OverworldLoop、OverworldLoopLessDelay、wWalkCounter、AdvancePlayerSprite、DoBikeSpeedup、180度の方向転換についてのコメント)、コミットd2704a6、2026年10月4日閲覧、https://github.com/pret/pokered/blob/master/home/overworld.asm 。方向転換の挙動はコードから読んだもので、エミュレータで実行したものではない。 ↩↩↩↩↩↩ -
pret、
pokered/engine/overworld/movement.asm(UpdatePlayerSprite、4でコマを進めるアニメーション内カウンタ)、2026年10月4日閲覧、https://github.com/pret/pokered/blob/master/engine/overworld/movement.asm ↩↩ -
pret、
pokered/home/fade.asm(GBFadeOutToBlack)およびhome/overworld.asm(PlayMapChangeSound、ドアのタイル$0bならSFX_GO_INSIDE、それ以外はSFX_GO_OUTSIDE)、2026年10月4日閲覧、https://github.com/pret/pokered/blob/master/home/fade.asm ↩ -
pret、
pokecrystal/engine/overworld/events.asm(MaxOverworldDelay: db 2)およびengine/overworld/map_objects.asm(StepVectors。.init1、.step1、.init2、.step2が互いに流れ込むStepFunction_Turnと、ObjectStep_AnonJumptable)、コミット5beda23、2026年10月4日閲覧、https://github.com/pret/pokecrystal/blob/master/engine/overworld/events.asm および https://github.com/pret/pokecrystal/blob/master/engine/overworld/map_objects.asm 。方向転換の3回の更新は、著者がルーチンを命令ごとに再現したもので、エミュレータでの実行ではない。 ↩↩↩↩↩ -
pret、
pokecrystal/data/maps/setup_scripts.asm(FadeOutToWhiteで始まるMapSetupScript_Door、FadeInFromWhiteで終わるMapSetupScript_Warp)およびengine/tilesets/timeofday_pals.asm、2026年10月4日閲覧、https://github.com/pret/pokecrystal/blob/master/data/maps/setup_scripts.asm および 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、2026年10月3日(6枚組の歩行。広場での「the walk at eight frames a second」。「Since publishing」の項にある、TestFlightのビルド34で26ピクセルの人物を32×40のセルの30ピクセルの人物に置き換えたこと。
b1b78b1のscripts/forge/rig.pyはこれをCELL_W, CELL_H = 32, 40と設定している)、https://blakecrosley.com/blog/pixel-art-characters-on-iphone ↩↩↩ -
Celesteの開発者たち、GitHubの
NoelFB/Celesteリポジトリ、Source/Player/Player.cs(「Camera (lerp by distance using delta-time)」の下のカメラの更新と、CameraTargetのゲッター)、2026年10月4日閲覧、https://raw.githubusercontent.com/NoelFB/Celeste/master/Source/Player/Player.cs ↩↩↩↩ -
Celesteの開発者たち、
NoelFB/Celesteリポジトリ、README.md(クラスのファイルを「as a learning resource and for general interest」公開。MITライセンスはそのコードにのみ適用)、2026年10月4日閲覧、https://raw.githubusercontent.com/NoelFB/Celeste/master/README.md ↩ -
Stardew Valley Wiki「Speed」(プレイヤーの基本速度:歩行2、走行5、馬で6.6、ニンジンのあとは7)、2026年10月4日閲覧、https://stardewvalleywiki.com/Speed ↩
-
本記事のための著者の調査メモ、2026年10月4日作成。探したが見つからなかったものを記録している:Sea of Stars、Eastward、CrossCodeのカメラについての一次資料、Maddy Thorsonのカメラについての講演や記事、ピクセルリマスターのタッチ操作の正確な方式、https://terraria.wiki.gg/wiki/Mobile_version にあるTerrariaのモバイル版の移動、404を返した
developer.apple.com/documentation/touchcontrols。 ↩↩↩ -
Stardew Valley Wiki「Mobile Controls」(各方式。デフォルトとしての「Tap-to-move & Auto-Attack」。タップ移動、タッチへの追従、見えないジョイスティック、デフォルト操作の限界についての各引用)、2026年10月4日閲覧、https://stardewvalleywiki.com/Mobile_Controls ↩↩↩↩↩↩↩
-
Jared Nelson「’Stardew Valley’ is Getting A TON of New Control Options in the Next Update」、TouchArcade、2018年11月1日、2026年10月4日閲覧、https://toucharcade.com/2018/11/01/stardew-valley-mobile-controls-update/ ↩
-
App Store、SQUARE ENIXの「FINAL FANTASY」、バージョン履歴(バージョン1.2.0、03/11/2025、およびタップ移動についての注記)、2026年10月4日閲覧、https://apps.apple.com/us/app/final-fantasy/id1492041278 ↩↩
-
Mikhail Madnani、TouchArcade、2024年1月30日、モバイル版にコントローラー対応をもたらしたファイナルファンタジー ピクセルリマスターのアップデートについて、2026年10月4日閲覧、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」(タッチ操作のベストプラクティス。2025年6月9日の変更履歴の項目)、2026年10月4日閲覧、https://developer.apple.com/design/human-interface-guidelines/game-controls ↩↩↩↩↩↩↩
-
Apple、WWDC25セッション209(Touch Controlsフレームワーク、「the vast majority of players won’t have a controller available」、「integrates directly with Metal」)、トランスクリプト2026年10月4日閲覧、https://developer.apple.com/videos/play/wwdc2025/209/ ↩↩↩
-
Apple Human Interface Guidelines「Playing haptics」(ベストプラクティス、カスタムハプティクス、iOSのimpactのカテゴリ、各引用)、2026年10月4日閲覧、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)、2026年10月4日閲覧、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)、2026年10月4日閲覧、https://developer.apple.com/documentation/quartzcore/cadisplaylink/targettimestamp ↩ -
Apple、WWDC21セッション10147「Optimize for variable refresh rate displays」(MacのAdaptive-SyncディスプレイとiPad ProのProMotion、各引用)、トランスクリプト2026年10月4日閲覧、https://developer.apple.com/videos/play/wwdc2021/10147/ ↩↩↩
-
Apple Developer Documentation「SceneEvents.Update」(iOS 13.0、iPadOS 13.0、Mac Catalyst 13.0)、2026年10月4日閲覧、https://developer.apple.com/documentation/realitykit/sceneevents/update ↩
-
Apple Developer Documentation「deltaTime」(
SceneEvents.Update。iOS 13.0)、2026年10月4日閲覧、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。
SystemまたはSceneEvents.Updateによるフレームごとのコード)、2026年10月4日閲覧、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)、2026年10月4日閲覧、https://developer.apple.com/documentation/corehaptics/chhapticpattern 。Core Hapticsフレームワークのページ、https://developer.apple.com/documentation/corehaptics ↩
-
Apple Developer Documentation「CHHapticEvent」および「CHHapticEvent.EventType」(
hapticTransient、hapticContinuous。iOS 13.0)、2026年10月4日閲覧、https://developer.apple.com/documentation/corehaptics/chhapticevent および https://developer.apple.com/documentation/corehaptics/chhapticevent/eventtype ↩ -
Apple Developer Documentation「CHHapticEvent.ParameterID」(iOS 13.0)、2026年10月4日閲覧、https://developer.apple.com/documentation/corehaptics/chhapticevent/parameterid ↩
-
Apple Developer Documentation「CHHapticEngine」(iOS 13.0。
capabilitiesForHardware())、2026年10月4日閲覧、https://developer.apple.com/documentation/corehaptics/chhapticengine ↩↩ -
Apple Developer Documentation「UIImpactFeedbackGenerator」(iOS 10.0、iPadOS 10.0、Mac Catalyst 13.1。「Initializing the feedback generator」の項の
init(style:view:)と、Deprecatedの項のinit(style:))、2026年10月4日閲覧、https://developer.apple.com/documentation/uikit/uiimpactfeedbackgenerator ↩↩ -
Apple Developer Documentation「UIImpactFeedbackGenerator.FeedbackStyle」(iOS 10.0)、2026年10月4日閲覧、https://developer.apple.com/documentation/uikit/uiimpactfeedbackgenerator/feedbackstyle ↩
-
Apple Developer Documentation「impactOccurred(intensity:)」(iOS 13.0、iPadOS 13.0、Mac Catalyst 13.1)、2026年10月4日閲覧、https://developer.apple.com/documentation/uikit/uiimpactfeedbackgenerator/impactoccurred(intensity:) ↩
-
Apple Developer Documentation「prepare()」(
UIFeedbackGenerator。iOS 10.0)、2026年10月4日閲覧、https://developer.apple.com/documentation/uikit/uifeedbackgenerator/prepare() ↩↩ -
Apple Developer Documentation「sensoryFeedback(:trigger:)」および「SensoryFeedback」(SwiftUI。iOS 17.0、iPadOS 17.0、Mac Catalyst 17.0、visionOS 26.0。
impact(weight:intensity:))、2026年10月4日閲覧、https://developer.apple.com/documentation/swiftui/view/sensoryfeedback(:trigger:) および 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)、2026年10月4日閲覧、https://developer.apple.com/documentation/touchcontroller ↩↩↩↩
-
Apple Developer Documentation「TCDirectionPad」(iOS 26.0、iPadOS 26.0、Mac Catalyst 26.0)、2026年10月4日閲覧、https://developer.apple.com/documentation/touchcontroller/tcdirectionpad ↩
-
Apple Developer Documentation「GCVirtualController」(Game Controller。iOS 15.0、iPadOS 15.0、Mac Catalyst 15.0)、2026年10月4日閲覧、https://developer.apple.com/documentation/gamecontroller/gcvirtualcontroller ↩
-
pret、
pokeemerald/src/field_control_avatar.c(TryDoorWarp。押している方向が向いている方向と一致しているときにプレイヤーの前のセルを渡して呼ばれ、その方向が北で、セルがドアのワープである場合にのみワープする)、2026年10月4日閲覧、https://github.com/pret/pokeemerald/blob/master/src/field_control_avatar.c ↩