マガジン

スクロールすると画面が真っ白になる:描画範囲の外に行った状態

遊びをつくる

執筆にClaudeを使用しています。

壊れたビー玉コースを四枚の記録カードと虫眼鏡を使って調べ、修理する手元
記事のテーマをもとに制作したAI生成イラスト。

横スクロールのゲームを作っていて、キャラクターを右へ動かし続けると、ある位置で急に画面が真っ白になる場面を考えてみます。コンソールにエラーは出ず、ゲームの処理は動いているように見えます。戻ると絵が復活することもあります。この症状の多くは「何かが壊れた」のではなく、「描くべき物が、描画範囲の外にあると判定された」状態です。 この記事では、カメラと描画範囲の関係を確かめる手順を扱います。見るのは3つの数字です。カメラの位置、キャンバスの大きさ、実際に描いた物の数です。これを画面に出して、白くなる境目を探すと、原因が「範囲の判定」「座標の変換」「数値の破損」のどれかに絞れます。 結果の数字はここには書きません。確かめるのは、手元のゲームです。

この記事の目次
  1. まず「白」の種類を分ける
  2. カメラ・描画範囲・画面の関係を一本の式にする
  3. 数値を画面に出して境目を探す
  4. 数字から原因を絞り込む
  5. 表示サイズが変わることを前提にする
  6. 次の行動

まず「白」の種類を分ける

白い画面には、原因の系統が少なくとも2つあります。ここを最初に分けないと、カメラを疑って時間を失います。

  • 起動した瞬間から白い:素材の読み込み失敗や、最初の描画前の例外が疑われます。アソバースへの投稿では、直下のindex.htmlが入口で、素材は相対パスで指定します。パスの書き方が環境に合っていないと、この系統の白になります。
  • 動かしてから白くなる:カメラや描画範囲が関係している可能性が高い系統です。この記事が扱うのはこちらです。

見分けるには、操作せずに最初の画面が出るかを確認します。出るなら、白くなるまでの操作を1つずつ戻して再現手順を作ります。ここで「右キーを何回押したら」「どの座標まで進んだら」と、境目を具体的に言えるようにしておきます。

カメラ・描画範囲・画面の関係を一本の式にする

2Dゲームの座標は2種類あります。ワールド座標は、物が置かれている世界全体での位置です。画面座標は、キャンバス上のどこに描くかという位置です。両者をつなぐのがカメラで、横方向だけなら次の関係です。

画面のx = ワールドのx − カメラのx

カメラが右へ1000進むと、ワールドのx=1000にある物が画面の左端(x=0)に来ます。見えている範囲は、ワールド座標で言えば「カメラのx」から「カメラのx+キャンバスの幅」までです。

描画の負荷を減らすため、この範囲の外にある物を描かない処理(カリング)を入れることがよくあります。このとき、物の位置を画面の幅と比べてしまう誤りが起きやすくなります。画面の幅は固定の数字ですが、見える範囲は動いています。比べる相手を間違えると、カメラが画面幅を超えて進んだ瞬間にすべての物が「範囲外」になります。これが「スクロールすると白くなる」の典型です。

もう1つ典型があります。毎フレームtranslate(座標の原点をずらす命令)を呼びながら、変換を初期化していない場合です。ずれが積み重なって、描いた物が画面の外へ出続けます。見た目は同じ白でも、描いた物の数は減っていません。次の手順では、この2つを数字で区別します。

数値を画面に出して境目を探す

最小の再現コード

次のコードは、カリングの比較対象を間違えた状態を、キーで入れ替えて見られるようにした教材です。単独のHTMLとして動きます。左右キーでカメラを動かし、bキーで誤った判定に切り替えます。

html
<canvas id="c" width="480" height="240" style="background:#eef"></canvas>
<script>
const c = document.getElementById('c'), g = c.getContext('2d');
const boxes = [];
for (let i = 0; i < 30; i++) boxes.push({ x: i * 100, y: 100, w: 60, h: 60 });
let camX = 0, bug = false, drawn = 0;
addEventListener('keydown', e => {
  if (e.key === 'ArrowRight') camX += 40;
  if (e.key === 'ArrowLeft') camX -= 40;
  if (e.key === 'b') bug = !bug;
  camX = Math.max(0, Math.min(camX, 3000 - c.width));
  draw();
});
function visible(b) {
  const left = bug ? 0 : camX;   // bugが真だと、カメラ位置を無視する
  return b.x + b.w > left && b.x < left + c.width;
}
function draw() {
  g.setTransform(1, 0, 0, 1, 0, 0);
  g.clearRect(0, 0, c.width, c.height);
  g.translate(-camX, 0);
  drawn = 0;
  for (const b of boxes) {
    if (!visible(b)) continue;
    g.fillRect(b.x, b.y, b.w, b.h);
    drawn++;
  }
  g.setTransform(1, 0, 0, 1, 0, 0);
  g.fillText('camX=' + camX + ' drawn=' + drawn + ' bug=' + bug, 8, 16);
}
draw();
</script>

コードを読むと、bugが真のとき、判定に使う左端が常に0になります。このため、描かれるのはワールドx<480付近の箱だけです。一方でtranslate(-camX, 0)は効いているので、カメラが進むほど、それらの箱は画面の左外へ逃げます。結果として「描いた数は0ではないのに、画面には何も映らない」か、カメラがさらに進めば「描いた数も0」になります。左上の文字(変換を戻してから描いています)が数字を示すので、どちらかを自分の目で確かめられます。

自分のゲームで行う手順

  1. 画面の隅に、カメラの位置、キャンバスの幅と高さ、描いた物の数を常時表示します。表示用の文字は、カメラの変換を戻した後に描きます。そうしないと、文字まで一緒に流れてしまいます。
  2. カメラを一定の歩幅で動かし、白くなった時点の3つの数字を書き留めます。同じ操作を3回繰り返し、毎回同じ位置で白くなるかを見ます。位置が毎回違うなら、時間や入力の速さに依存した原因(数値の累積誤差など)を疑います。
  3. 白くなった瞬間に「描いた物の数」が0かどうかで分岐します。

数字から原因を絞り込む

白くなった時点の数字で、疑う場所が変わります。

  • 描いた数が0、疑う場所:カリングの判定、確認の方法:判定を一時的に外して全部描き、復活するか見ます
  • 描いた数は正常なのに白い、疑う場所:座標変換のずれ、確認の方法:毎フレームの先頭で変換を初期化しているか確認します
  • カメラの値がNaN、疑う場所:カメラの計算、確認の方法:0での割り算や、未定義の値との計算を探します
  • カメラが世界の端を超えている、疑う場所:範囲の制限(クランプ)、確認の方法:上限を「世界の幅−画面の幅」で計算しているか確認します
  • キャンバスの幅が0や極端な値、疑う場所:サイズの取得、確認の方法:現在の表示サイズから読み直しているか確認します

NaNは「数ではない」を表す特別な値です。一度カメラに入ると、足しても引いてもNaNのままで、すべての比較が偽になります。全部の物が範囲外に見えて、白くなります。数字の表示がNaNになっていれば、原因はほぼそこです。

起こりやすい誤りも挙げておきます。

  • 原因が1つとは限りません。判定を外して復活しても、それが唯一の原因とは言えないので、外した状態で端まで動かして確かめます。
  • 確認のために入れた表示用コードが、変換の初期化を忘れて同じ誤りを再現することがあります。
  • 一度の再現で判断しないでください。3回同じ位置で起きたかを基準にします。
  • 修正は1回に1か所です。複数を同時に直すと、効いた修正が分かりません。

表示サイズが変わることを前提にする

アソバースの確認済みの仕様から、読み取れることがあります。公式の13作品は、サイトの言語設定の変更やプレイ画面の拡大をしても進行が保たれるように作られています。プレイ画面は、プレイ中に大きさが変わりうるということです。ここから言えるのは、見える範囲の幅を最初に決めた定数で持つと、拡大のあとにカリングの範囲とずれる可能性があるという点です。幅や高さは、描画のたびに現在の値から読むのが安全です。拡大の前後で何が起きるかは、手元のゲームを投稿して確かめます。

また、公式の「WAVE / WAVE」は、狭い画面でステップが横スクロールすると案内されています。画面幅より広い内容を、スクロールで見せる設計です。スクロールの仕組みがキャンバスの座標移動なのか、ページの機能なのかなど、内部の作りは確認済みの情報にはありません。作品を開いて、狭い画面で動かしながら確かめてください。

投稿するゲームはsandbox付きのiframeで動きます。ブラウザで直接開いたときと表示領域が違いうるので、確認はアソバース上の表示でも行ってください。

次の行動

自分のゲームに、カメラの位置、キャンバスの大きさ、描いた物の数を表示する3行を足してください。次に、白くなる位置を3回再現して数字を書き留め、上の表で疑う場所を1つに絞ります。最後に、プレイ画面を拡大した状態でも同じ操作を行い、数字がどう変わるかを確かめます。