マガジン

スマホゲームのバッテリー消費を抑える描画設定

遊びをつくる

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

大きな操作ボタンと横向きの砂時計、複数の操作道具が並ぶ紙と木のゲーム風景
記事のテーマをもとに制作したAI生成イラスト。

スマホでブラウザゲームを遊んでいると、数十分でデバイスが熱くなり、バッテリーが急速に減ることがあります。これはゲームの「描画処理」が原因であることがほとんどです。作る側の視点では、バッテリー消費を減らすことはユーザー体験の改善であり、「重い・熱い」という理由で離脱されるリスクを下げることでもあります。 この記事では、HTMLとJavaScriptで作るブラウザゲームを対象に、描画まわりの設定と実装上の判断基準を具体的に示します。タッチ操作のUI設計や画面サイズ対応の話は別の機会に譲り、ここではバッテリーと熱に直結する「いかに描画の負荷を減らすか」に絞って解説します。

この記事の目次
  1. バッテリーを消費する描画の正体
  2. フレームレートを必要な分だけに抑える
  3. 差分描画で再描画範囲を絞る
  4. WebGLとCanvasの選択基準
  5. 画面非表示時に描画を止める
  6. 画像リソースと色の扱い
  7. 次の行動

バッテリーを消費する描画の正体

スマホのバッテリーが減る主な原因は、CPUとGPUの継続的な高負荷です。ブラウザゲームで特に問題になるのは次の二点です。

一つ目はフレームレートの過剰な追求です。PCと同じように毎秒60フレームや120フレームを出そうとすると、GPUは常にフル稼働します。パズルゲームやカードゲームのように動きが少ないジャンルでは、そこまでの更新頻度は不要です。

二つ目は画面全体の再描画を毎フレーム行うことです。canvasのclearRectで全体を消して描き直す処理を、変化のない場面でも繰り返すと、無駄なGPU呼び出しが積み重なります。

この二点を意識して設計を変えるだけで、消費電力を体感できるレベルで下げられます。

フレームレートを必要な分だけに抑える

requestAnimationFrame(以下rAF)はブラウザのリフレッシュレートに合わせてコールバックを呼び出します。多くのスマホは60Hzまたは90Hz以上で動くため、何も制限しなければそのレートで描画ループが回り続けます。

フレームレートを意図的に下げるには、タイムスタンプを使って呼び出し間隔を制御します。

javascript
let lastTime = 0;
const TARGET_FPS = 30;
const INTERVAL = 1000 / TARGET_FPS;

function loop(timestamp) {
  requestAnimationFrame(loop);
  if (timestamp - lastTime < INTERVAL) return;
  lastTime = timestamp;
  update();
  draw();
}
requestAnimationFrame(loop);

このコードでは毎フレームrAFは呼ばれますが、INTERVALに満たない場合はupdateとdrawをスキップします。結果として実質30FPSで動作します。ジャンルの目安としては、アクションゲームは30〜60FPS、パズルやカードは15〜30FPSでも十分な場合がほとんどです。

さらに踏み込むなら、操作イベントが発生したフレームだけを描画する「オンデマンド描画」が理想的です。カードを動かす、ボタンを押すといった操作がないあいだは描画ループ自体を止め、入力を受け取ったときだけ再描画します。これはパズルゲームやボードゲームで特に効果的で、アイドル時のバッテリー消費をほぼゼロに近づけられます。

差分描画で再描画範囲を絞る

毎フレームcanvas全体を塗り直すのではなく、変化した領域だけを再描画する方法です。clearRectとdrawImageを全体に使うのではなく、変更のあったスプライトやセルの座標と大きさだけを対象にします。

実装の考え方は「ダーティフラグ」パターンです。各オブジェクトにisDirtyというフラグを持たせ、状態が変化したものだけtrueにします。描画ループではisDirtyがtrueのオブジェクトだけ処理し、フラグをfalseに戻します。

javascript
function draw() {
  for (const obj of objects) {
    if (!obj.isDirty) continue;
    ctx.clearRect(obj.x, obj.y, obj.width, obj.height);
    obj.render(ctx);
    obj.isDirty = false;
  }
}

ただし、オブジェクトが重なる場合は前のフレームの残像が残らないよう、消去領域の計算に注意が必要です。背景が静止画であれば、canvasを二層に分けて背景レイヤーは一度だけ描画し、動くオブジェクトだけを上のレイヤーで差分処理すると管理がしやすくなります。

WebGLとCanvasの選択基準

canvasの描画コンテキストには2dとwebglの二種類があります。単純に「WebGLのほうが速い」と思われがちですが、バッテリーの観点では必ずしもそうではありません。

2dコンテキストは比較的シンプルな処理に最適化されており、ブラウザが内部でGPUアクセラレーションを適切に管理します。スプライトを動かす程度のゲームであれば2dのほうがオーバーヘッドが少なく、バッテリーにも優しい傾向があります。

webglが有利になるのは、パーティクルを数千個出す、シェーダーエフェクトを多用するなど、GPU本来の並列処理能力を使いきる場面です。そこまでの規模でないゲームにWebGLを使うと、初期化コストと状態管理のオーバーヘッドが無駄に乗ります。

判断基準としては、画面上で同時に動くオブジェクトが100個以下、かつシェーダーを使わないなら2dを選ぶのが無難です。複雑なエフェクトが必要になってから移行を検討しても遅くありません。

画面非表示時に描画を止める

タブをバックグラウンドに移したり、スマホの画面が暗くなった状態でも描画ループが動き続けていると、バッテリーを無駄に消費します。Page Visibility APIを使えば、タブが非表示になったタイミングを検知してループを停止できます。

javascript
document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    cancelAnimationFrame(animationId);
  } else {
    animationId = requestAnimationFrame(loop);
  }
});

animationIdはrAFの戻り値を保存しておく変数です。これだけで、ユーザーがホーム画面に戻っているあいだの無駄な処理を完全に止められます。実装コストが低い割に効果が大きいため、すべてのゲームで入れておくことを勧めます。

あわせて、長時間操作がなかった場合にフレームレートを自動で下げる「アイドル検知」も有効です。最後の操作から一定時間が経過したらTARGET_FPSを5〜10程度に落とし、操作があれば元に戻すという仕組みです。数十行で実装でき、放置プレイが多いゲームほど効果が出ます。

画像リソースと色の扱い

描画負荷は処理だけでなく、使う画像の形式と大きさにも依存します。スプライトシートを使わず個別の画像ファイルを何十枚もdrawImageで呼ぶと、テクスチャの切り替えコストが積み重なります。可能であれば複数のスプライトを一枚の画像にまとめ、座標指定で切り出す方式にすると描画呼び出しの回数が減ります。

画像の解像度はスマホの実画面サイズに合わせて用意します。Retina相当の高解像度ディスプレイに対応するために2倍サイズの画像を用意することは一般的ですが、それ以上の解像度の画像をスケールダウンして使うのはデコードコストの無駄です。devicePixelRatioを参照して必要な解像度を動的に判断し、無駄に大きい画像を読み込まない設計が理想的です。

また、半透明(アルファ値が0〜255の中間)のオブジェクトを多数重ねると、ブレンド計算がGPUに負荷をかけます。デザイン上不要な半透明は避け、完全に不透明か完全に透明(描画しない)の二択に整理するだけで処理が軽くなることがあります。

次の行動

まず手元のゲームのフレームレートを30FPSに落とし、ブラウザの開発者ツールのパフォーマンスタブでCPU使用率がどう変わるかを確認してみてください。次にPage Visibility APIによるループ停止を追加し、バックグラウンド時の挙動をテストします。これら二つを実装したゲームをアソバースに投稿して、実際のスマホで体感を確かめるのが最も早い検証方法です。