Games × Web
遠くの敵の動きが、少しだけカクつく。
あれは手抜きではなく、設計だ。
限られた計算を、気づかれる所に配っている。
遠くにいるモンスターだけ、
動きがほんの少し粗い。
『モンスターハンターワイルズ』の体験版を触っていて、そう感じた。目の前の相手は滑らかに動くのに、遠くの群れは、よく見るとコマ送りのように見える。そういえば『星のカービィ ディスカバリー』でも、離れた敵の動きが似た感じだった。
この記事が答える問い
限られた計算を、
どこに配ればいいのか?
調べてみると、これは不具合ではなく、意図して入れられた仕組みだった。ゲームは1コマを描くのに使える時間が決まっていて、その中に収まるように、プレイヤーが気づきにくい所から計算を削っている。
結論を先に置く。ゲームの最適化の根っこには、二つの考え方がある。予算を先に決めることと、知覚されない所から削ることだ。この二つは、Webでもそのまま使える。効くのは体感速度だけではない。サーバー代にも効く。
先に断っておくと、筆者はゲーム開発者ではない。Webのシステムを作っている側から、公開されている資料とプレイした体感をもとに整理した。
Chapter 01
ざっくり言うと——1コマに使える時間は決まっている。機種が変わると予算の大きさは変わるが、配り方の仕組みは共通にできる。
1000 ÷ 60 ≒ 16.7 ms
60fpsで動かすなら、1コマの計算と描画を16.7ミリ秒に収める(30fpsなら33.3ミリ秒)
ゲームは、1秒間に何十枚もの絵を描き直している。60fpsなら、1枚あたり16.7ミリ秒。この中に、敵の動き、物理、音、描画のすべてを収めなければならない。はみ出せばコマが落ちて、画面がカクつく。ゲームの処理には、最初から予算がある。
最近は、同じゲームが複数の機種で出ることが多い。PS5のCPUはAMDのx86-64、Nintendo Switch 2のCPUはArmのCortex-A78Cで、命令の体系からして違う。それでも同時に出せるのは、違いの多くをゲームエンジンが吸収しているからだ。
カプコンは統合報告書で、内製の RE ENGINE を「マルチプラットフォームに対応できる」エンジンと説明している。いまはすべてのタイトルをこのエンジンで開発しているという。Switch 2の発売日に『ストリートファイター6』と『祇(くにつがみ)』の2本を出せたのも、このエンジンで効率よく作れた成果だとしている。
命令やOSの違いを吸収したあとに残るのが、性能の差だ。据え置き機と携帯機では、同じ16.7ミリ秒の中でできる計算の量がまるで違う。そこで、予算そのものを機種ごとに変えられるようにしておく。
Unreal Engine には「Animation Budget Allocator」という仕組みがある。キャラクターのアニメーションに使ってよい時間を、ミリ秒で決めておく。収まらないときは、重要度の低いキャラクターから更新の頻度を落とす。この予算は、品質設定を通じて機種ごとに変えられる。
この章を一言で
予算の大きさは機種ごとに、配り方の仕組みは共通に。
Chapter 02
ざっくり言うと——遠くのものは粗く描き、動きの計算も間引く。間引いた分は、時間をずらして平らにならす。
予算に収めるための代表的な手は、三つある。
LOD(Level of Detail)
遠いものほど、ポリゴンの少ない粗いモデルに差し替える。画面の上で数ピクセルにしかならないなら、細部は見えない。
更新の間引き
遠いキャラクターの動きは、毎コマではなく2〜4コマに1回だけ計算する。冒頭の「コマ送りに見える」の正体はこれだ。
カリング
画面の外にあるもの、何かの陰に隠れたものは、そもそも描かない。
『星のカービィ ディスカバリー』は、レビューで「カービィから少し離れた敵は、低いフレームレートで動く」と書かれていた。もともと30fpsのゲームで、その半分になる場面もあったという。Switch 2 Edition では、この間引きがほとんど目立たなくなった。間引かれるのは、ずっと遠くのものだけになったと報じられている。予算が増えた分を、遠くに配り直したと読める。
『モンスターハンターワイルズ』のPC版には、遠くのものの動きを間引く距離と頻度を決める設定項目がある、とプレイヤーの間で話されている。手の抜き方が、プレイヤーが選べる「つまみ」になっているわけだ。
言葉で読むより、動かしたほうが早い。下のデモで確かめてほしい。
Demo
中央の円がプレイヤーで、ドラッグすると動かせる。生き物はどれも、動く前に重い「骨格の計算」をする。「遠くほど間引く」に切り替えると、点線の輪の外側ほど計算の回数が減る。
点線の輪の内側から:毎フレーム → 2フレームに1回 → 4フレームに1回 → 8フレームに1回(4以上は点だけで描く)
計算の時間
0.0 ms
このフレームで計算した数
0
削った計算
0%
直近3秒の計算時間(点線は60fpsの1フレーム=16.7ms)
数字は、いま読んでいる端末で実際に計った値。重さは端末によって違う。このデモ自体も、画面から外れると止まる。
間引くと、計算の量は目に見えて減る。手元のMacで1000体を動かすと、1コマで計算する数がおよそ8割減った。それでも、プレイヤーの周りの動きは何も変わらない。
もう一つ試してほしいのが、「更新をずらす」を切った状態だ。間引いた生き物が全員同じコマで計算すると、4コマや8コマごとに計算時間の山ができる。平均は減っても、山のコマでは予算からはみ出すかもしれない。間引いた計算は、コマをまたいで均等にばらす。ここまでやって、ようやく画面が安定する。
どれを間引くかは、距離だけで決まるわけではない。Unreal Engine の「Significance Manager」は、物ごとの重要度を計算する仕組みだ。重要度の低い物は、さりげない効果音やパーティクルを省く、といった使い方が公式の説明に挙がっている。遠くても、プレイヤーが狙っている相手は削らない。近くても、背景の小物は削ってよい。
この章を一言で
気づかれない所から削り、削った分は時間をずらしてならす。
Chapter 03
ざっくり言うと——Webの「遠く」は、画面の外、裏のタブ、いま見ていない情報。ここを毎回きっちり計算しているなら、削れる。
Webの画面にも「遠く」はある。スクロールしないと見えない行、裏に回ったタブ、開いてはいるが誰も見ていないダッシュボードの一角。ゲームの三つの手は、それぞれ次のように持ち込める。
カリング → 見えない所は作らない
content-visibility: auto で、画面外の要素の描画を省く(2024年9月から主要ブラウザで使える)loading="lazy" で、近づいてから読み込む更新の間引き → 見ていないものは、ゆっくり更新する
LOD → 遠くは粗く、近づいたら細かく
srcset で、表示する大きさに合った解像度を出し分ける更新をずらす → 重い処理を細切れにする
scheduler.yield() は Chrome・Edge・Firefox で使えるが、Safari は未対応なので setTimeout で代わりにするたとえば、ダッシュボードの一部を定期的に更新するなら、こう書ける。見えているときは10秒ごと、画面外なら60秒ごと、裏のタブでは止める。
// 見えているかどうかで、更新の間隔を変える
let onScreen = true;
let timer;
function schedule() {
clearTimeout(timer);
if (document.hidden) return; // 裏のタブ:止める
const ms = onScreen ? 10_000 : 60_000; // 画面外:間引く
timer = setTimeout(async () => {
await refresh();
schedule();
}, ms);
}
new IntersectionObserver(([e]) => {
onScreen = e.isIntersecting;
schedule();
}).observe(widget);
document.addEventListener('visibilitychange', schedule);
schedule();
ブラウザ自身も、同じ発想で動いている。裏に回ったタブでは requestAnimationFrame をほぼ止め、タイマーも間引く。ただ、ブラウザが間引くのは「いつ動くか」までで、サーバーへの問い合わせを要るかどうかは、アプリにしか判断できない。そこはアプリ側で決める。
この章を一言で
「全部を、毎回」をやめる。
Chapter 04
ざっくり言うと——削る技術は、予算があって初めて働く。画面の速さには時間の予算を、サーバーには回数の予算を置く。
ゲームの最適化で一番大事なのは、個々の技よりも、先に予算が決まっていることだと思う。16.7ミリ秒という上限があるから、「どこを削っても気づかれないか」を考える文化が育った。予算がなければ、削る理由がない。
Webの画面の速さにも、予算の物差しがある。たとえば操作への反応の速さを表す INP は、実際のページ表示の75%で200ミリ秒以内に反応できていれば「良い」とされる。ゲームの16.7ミリ秒と同じように、この数字を先に置き、超えたら直す。手元の速いPCではなく、遅い端末と遅い回線で測る。
サーバー側にも予算が置ける。こちらの単位は時間ではなく、回数だ。従量課金のサービスでは、請求はおおむね呼び出しの回数に比例する。
試算:30秒ごとに更新するダッシュボード
1日8時間開きっぱなし
960 回
1人・1日あたり
実際に見ているのが3分の1なら
320 回
裏のタブで止めた場合
1000人で減る回数
64万 回
1日あたり
見ている時間の割合は、説明のための仮定
遠くの敵の動きを間引くのと、裏のタブで問い合わせを止めるのは、同じ判断だ。誰も見ていない所の計算は、利用者に何も届けていない。それを削ると、画面は軽くなり、請求も小さくなる。
負荷が高いときに品質を落とす、という手もゲームから借りられる。ゲームは重い場面で、気づかれない程度に解像度を下げて、コマ落ちを防ぐ。Webでも、混み合ったときにおすすめ欄のような「なくても困らない機能」を一時的に止めて、本体の画面を守れる。それには、どの機能が削れるのかを、平時のうちに決めておく必要がある。
この章を一言で
予算が先、削り方は後。
Appendix
| ゲーム | やっていること | Webでは |
|---|---|---|
| フレーム予算 | 1コマ16.7msに収める | INPなどの目標値、サーバーの呼び出し回数の上限 |
| LOD | 遠いものは粗いモデルにする | srcset、ぼかした仮の画像 |
| 更新の間引き | 遠いキャラは数コマに1回だけ動かす | 画面外・裏のタブの更新を減らす、鮮度に段階をつける |
| カリング | 見えないものは描かない | 仮想スクロール、content-visibility、loading="lazy" |
| 更新をずらす | 間引いた計算をコマにばらす | 長い処理を細切れにする、定期処理の時刻をばらす |
| 重要度 | 大事なものに計算を厚く配る | 画面の主役から先に読み込み、更新する |
| 動的解像度 | 重い場面だけ解像度を下げる | 混雑時に、なくても困らない機能を止める |
並べてみると、ゲームの最適化は「計算を速くする技術」というより、「計算を配る技術」だと分かる。何を速くするかの前に、何をやらないかを決めている。遠くの敵がコマ送りに見えたら、それは誰かが予算と相談して、そこなら気づかれないと判断した跡だ。
Fact-check
仕組みの説明はエンジンの公式ドキュメントとメーカーの公式資料を、各作品の挙動は技術レビューを選んだ:カプコン 統合報告書2025(RE ENGINE)/Epic Games Animation Budget Allocator・Significance Manager/Switch 2 の仕様は Nintendo Life(Digital Foundry の分析)/『星のカービィ ディスカバリー』の間引きは GamingBolt のレビュー/『モンスターハンターワイルズ』の設定項目は Steam コミュニティの議論(プレイヤーによる説明)/MDN content-visibility・scheduler.yield()・Page Visibility API(裏のタブでの間引きを含む)/web.dev INP。冒頭の体感は筆者がプレイした印象。デモの数値は読者の端末で計った値で、ゲームの実際の処理とは別物。手法の対応づけは筆者の整理。