スマホ実機のタッチ座標ズレをどう直したかHow I Fixed Misaligned Touch Coordinates on Physical Phones

開発ログ / タッチ操作 / レイアウトDevelopment Log / Touch Controls / Layout

VOID STRIKERはもともとキーボード操作だけを前提に作り、あとからタッチ操作を追加したブラウザゲームです。PCのブラウザやChromeのデバイスツールバーでは問題なく動いていたのに、実際のスマートフォンで開いてもらうと「タイトル画面のSTARTを押したつもりがHANGARに飛ぶ」「HANGARの項目をタップしたのに1つ下の項目が選択される」といった、タップした場所と実際の判定位置がズレる不具合が報告されました。これはエミュレータや開発者ツールのタッチシミュレーションでは再現しづらく、実機でしか発覚しない厄介な種類のバグでした。

VOID STRIKER was originally built for keyboard controls, with touch controls added later. It worked correctly in desktop browsers and Chrome's device toolbar, but testing on physical smartphones revealed reports such as, "I tapped START on the title screen but opened HANGAR," and, "I tapped an item in HANGAR but selected the item below it." The tap location and the actual hit-detection position did not match. This troublesome class of bug was difficult to reproduce with emulators or developer-tool touch simulation and only became apparent on physical devices.

症状の切り分けNarrowing Down the Symptoms

まず疑ったのは判定ロジック側(HANGARの行のあたり判定やタイトルメニューのY座標)でしたが、コード上は特に間違っていませんでした。次に疑ったのがタッチ座標の変換処理です。VOID STRIKERは内部解像度480×640のcanvasを、実際の画面サイズに合わせてCSSで拡大縮小して表示しています。タップされたブラウザ座標(clientX/clientY)を、この拡大縮小率で割り戻して内部座標に変換する必要があるのですが、この「拡大縮小率」の算出方法に問題がありました。

I first suspected the hit-detection logic, such as the HANGAR row hitboxes and title-menu Y coordinates, but found no error in the code. I then examined the touch-coordinate conversion. VOID STRIKER displays its internal 480×640 canvas scaled with CSS to fit the actual screen. Browser coordinates from a tap (clientX/clientY) must be divided by that scale to convert them into internal coordinates, and the problem was how this scale was calculated.

当時は画面全体を包む要素に transform: scale(...) を使って縮小表示していました。CSSのtransformは見た目のサイズだけを変え、レイアウト計算上の幅・高さ(要素が実際に占めるボックスサイズ)は変化させません。つまりブラウザの目には「480pxという、画面幅を超える大きな要素がある」ように映り続けます。スマートフォンのブラウザは、ページの内容が画面幅に収まらないと判断すると、ページ全体を自動的にズームアウトして収めようとする挙動を持っています。この自動ズームと、こちらが意図して適用していた transform:scale による縮小が二重にかかってしまい、実際に画面へ表示されているサイズと、JavaScriptが「表示サイズのはずだ」と認識している値がズレてしまっていたのです。

At the time, I used transform: scale(...) to shrink the element wrapping the entire screen. A CSS transform changes only the visual size, not the width and height used for layout, or the size of the box the element actually occupies. To the browser, there was still a large 480px element exceeding the screen width. Smartphone browsers may automatically zoom out to fit a page when its content appears wider than the screen. This auto-zoom was applied on top of the intentional scaling from transform:scale, creating a mismatch between the actual displayed size and the value JavaScript treated as the display size.

デバッグ用のオーバーレイを先に作るBuilding a Debug Overlay First

実機でしか再現しないバグは、ログを見るだけでは原因を特定しにくいものです。そこで先に、URLに debugtouch というクエリを含めると、タップした内部座標の位置に十字マーカーと座標の数値を約1秒間描画するデバッグ用オーバーレイを実装しました。これを使い、実機で「ここを押した」という自覚のある位置と、実際に記録された内部座標を見比べることで、ズレの方向と大きさを定量的に観測できるようになりました。原因の仮説を検証するための可視化ツールを先に作る、というのは今回に限らずこの手のバグ調査で有効なアプローチだったと感じています。

Bugs that reproduce only on physical devices are difficult to diagnose from logs alone. I first implemented a debug overlay that, when the URL contains the debugtouch query, draws a crosshair and coordinate values at the tapped internal position for about one second. Comparing the location I knew I had tapped with the recorded internal coordinates on a physical device made it possible to measure the direction and size of the offset. Building a visualization tool before testing the hypothesis proved useful for this kind of investigation beyond this particular bug.

根本修正: 実寸レイアウトへの切り替えThe Root Fix: Switching to an Actual-Size Layout

最終的な修正は、transform:scale をやめて、ゲームを包む要素(#frame)の width/height そのものを、計算した縮小率にもとづくピクセル数で直接指定する方式に切り替えることでした。具体的には scale = min(画面幅 / 480, 画面高さ / 640) * 0.97 を求め、Math.round(480 * scale)px / Math.round(640 * scale)pxを要素の実際の幅・高さとして設定します。こうすればブラウザから見てもレイアウト上のサイズと視覚的なサイズが完全に一致するため、画面幅を超えていると誤認されることがなくなり、ブラウザ側の自動ズームが発動する余地そのものがなくなります。タッチ座標の変換に使う getBoundingClientRect() の値も、この実寸レイアウトと完全に一致するため、二重縮小によるズレは根本的に解消されました。

The final fix was to stop using transform:scale and directly set the game wrapper's (#frame) width/height to pixel values based on the calculated scale. Specifically, the code calculates scale = min(screen width / 480, screen height / 640) * 0.97, then sets Math.round(480 * scale)px / Math.round(640 * scale)px as the element's actual width and height. The layout and visual sizes now match exactly from the browser's perspective, so the browser no longer mistakes the content for something wider than the screen and has no reason to trigger auto-zoom. The values from getBoundingClientRect() used to convert touch coordinates also match this actual-size layout, resolving the offset caused by double scaling at its source.

もう一段階の修正: カーソルがタップに追従しない問題One More Fix: Making the Cursor Follow Taps

座標ズレを直したあとも、もう一つ細かな不具合が残っていました。HANGARやCODE、タイトルメニューをタップすると、正しい項目が実行はされるのに、画面上のカーソル(ハイライト表示)が別の項目に残ったままになる、という表示上の不整合です。原因は単純で、タップ処理が「その場で該当項目を直接実行する」ことだけを行い、キーボード操作用のカーソル位置(titleCursorhangarCursorなど)自体を更新していなかったためでした。修正としては、タップでヒットした項目に一度カーソルを移動させたうえで決定処理を行うようにし、見た目と実際の操作結果を一致させました。

One minor bug remained after fixing the coordinate offset. Tapping HANGAR, CODE, or the title menu executed the correct item, but the on-screen cursor highlight remained on a different item. The cause was simple: tap handling directly executed the item in place without updating the cursor position used by keyboard controls, such as titleCursor or hangarCursor. The fix moves the cursor to the tapped item before running its confirm action, keeping the visual state consistent with the actual result.

学んだことWhat I Learned

この一連の修正を通して、「見た目の拡大縮小」と「レイアウト上のサイズ」を分けて考えることの大切さを再確認しました。CSSのtransformは便利な反面、レイアウトやタッチ座標変換のように「実際の占有スペース」を前提にした計算とは相性が悪い場面があります。モバイル対応のcanvasゲームでは、見た目の縮小と要素の実サイズを一致させておくことが、タッチ精度のバグを未然に防ぐ近道だと感じました。

This series of fixes reinforced the importance of treating visual scaling and layout size as separate concepts. CSS transforms are convenient, but they can be a poor fit for calculations that depend on actual occupied space, such as layout and touch-coordinate conversion. In mobile canvas games, keeping the visual scale aligned with the element's actual dimensions is a direct way to prevent touch-accuracy bugs.