株式会社CAENのロゴ

Love2D のゲームを love.js で Web 公開する|AI駆動でハマりどころを全部潰した話

この記事で分かること

  • Love2D(ラブ・ツーディー) は Lua で書ける軽量2Dゲームエンジン。だが Web 公開は鬼門 で、何も知らずに進めると確実にハマる。
  • love.js(Emscripten で WebAssembly 化したやつ)を使えば、ブラウザで Love2D ゲームが動く。スマホでも遊べる。
  • ハマりどころは大きく3つ:canvas スケーリング・Lua 5.1 制約・モバイル操作。AI駆動開発で潰しきった実体験を全部書く。

Love2D × Web 公開を AI に丸投げするとき、この記事の制約をプロンプトに最初から書いておく だけで、罠を全部回避できる。

love.js とは? Love2D を Web で動かす唯一の選択肢

Love2D(公式名 LÖVE) は Lua で書ける2Dゲームエンジンです。インディーゲーム界隈では昔から定番。love.js は、その LÖVE を EmscriptenWebAssembly に変換 したものです。一言でいうと「Love2D ゲームをブラウザで動かす公式テクノロジー」。

僕(大森翔吾)は最近 solo-studio.caen.co.jp という個人ゲーム配信サイトを立てて、Love2D で作った1人プレイ専用のミニゲームを並べています。配信は love.js + Bun サーバー + Cloudflare Tunnel。Discord Bot 経由で AI(Claude Code)にゲームを作らせて、そのまま Web に公開するパイプラインです。これがハマりまくった。

Godot × AI駆動開発 で書いたとおり、Unity / Unreal と違ってエンジン内部がテキスト主体のものは AI 駆動と相性が良い。その中でも Love2D は 「ほぼ main.lua 一枚で動く最小構成」 なので、AI エージェントに丸投げするには Godot よりさらに向いている場面が多い(詳しくは Love2D × AI駆動開発の入門記事)。唯一の弱点が Web 配信の癖の強さで、この記事はそこを丸ごと潰すのが目的です。

ハマりどころ① Lua 5.1 ベースである(goto と bit が使えない)

これは AI に書かせる前に 絶対に伝えておく べき制約。

ネイティブの Love2D は LuaJIT(Lua 5.1 + 拡張)で動きます。一方 love.js(Emscripten 版)は 素の Lua 5.1 ベース。LuaJIT の便利な拡張が 使えません

具体的に踏んだ地雷:

  • goto 文 / ラベル ::label::使えないif not ... then ... end で代替
  • ビット演算ライブラリ bit → love.js 側に存在しないことがある

AI(Claude Code / Codex)は普通に LuaJIT 前提のコードを書いてきます。Web ビルド後に「ローカルでは動いたのに、ブラウザで真っ白」みたいなことが起きる。

対策:プロンプトの冒頭に必ず以下を書く。

このプロジェクトは love.js (Lua 5.1 ベース) で Web ビルドします。
- goto 文・ラベル禁止 → if not 構造で代替
- bit ライブラリ禁止
- love.audio 禁止(love.js では無効化されている)
- LÖVE 11.5 の API を使うこと(12.x は love.js 未対応)

これだけで罠の半分は消えます。

ハマりどころ② canvas スケーリング問題(スマホで画面が切れる)

これが一番苦しんだやつ。docs/emscripten-canvas-scaling.md に全試行を記録してあります。

何が起きるか

conf.lua で 960x540 のウィンドウを指定すると、love.js は HTML に 960x540 固定<canvas> を吐く。スマホ(幅 390px とか)で開くと canvas が画面からはみ出して、ゲーム右側が切れる。

やってはいけないこと

最初に思いつくのは「CSS で canvas を縮小すればいいじゃん」。罠です

// NG:これをやるとゲーム右側が描画されなくなる
gameCanvas.style.width = "390px";
gameCanvas.style.height = "219px";

理由は Emscripten SDL2 の仕様。Emscripten は canvas の CSS 表示サイズを描画バッファサイズとして自動同期 してきます。CSS で 390x219 にした瞬間、描画バッファも 390x219 になり、Love2D が描いている 960x540 のうち左上の一部しか見えなくなる。

じゃあ JS で canvas.width = 960 を直接戻せば? これも NGcanvas.width への直接代入は WebGL コンテキストをリセット してゲーム全体が壊れます。

正解は transform: scale()

唯一の正攻法はこれ。

gameCanvas.style.transformOrigin = "top left";
gameCanvas.style.transform = "scale(" + (viewportWidth / 960) + ")";

transformCSS computed width/height を変えない(視覚的な拡縮だけ)。だから Emscripten のバッファ同期が発火せず、描画バッファは 960x540 のまま、見た目だけスマホ画面に収まる。

さらに、適用タイミングも重要。Emscripten 初期化前にレイアウトを変更するとバッファが壊れます。Module.postMainLoop フックで「初回フレーム後」に1回だけ実行するのが鉄板。

var origPostMainLoop = Module.postMainLoop;
Module.postMainLoop = function () {
  Module.postMainLoop = origPostMainLoop || null;
  applyMobileLayout();
};

ここまでハック禁止・正攻法のみで詰めるのが、AI駆動で品質を保つコツです(→ AI駆動開発のワークフロー)。

ハマりどころ③ Retina 対応とモバイル操作

Retina:論理解像度の2倍にバッファが取られる

conf.luat.window.highdpi = true にすると、Retina ディスプレイでは描画バッファが論理解像度の2倍(1920x1080)になります。これに気づかず固定座標で描くと、画面の左上1/4にだけ絵が出る。

対策はシンプル。love.load で SCALE を計算して love.draw の先頭でスケールをかける。

local SCALE = 1

function love.load()
    SCALE = love.graphics.getWidth() / 960
end

function love.draw()
    love.graphics.scale(SCALE, SCALE)
    -- 以降は 960x540 の論理座標で描画
end

マウス座標を使う場合は SCALE で割って論理座標に戻すこと。

モバイル操作:バーチャルゲームパッドを JS でインジェクト

love.js が吐く HTML には当然タッチ UI がない。Web ビルド後に gamepad.js という独自 JS を追加して、画面下部に十字キーと A/B ボタンを position: fixed; bottom: 0 で表示しています。タッチイベントを Love2D の love.keyboard.isDown("up") 相当にマッピングする発想。

ここも AI に「スマホ用バーチャルパッドを JS で注入する build-web.sh を書いて」と頼めば一発で書いてくれます。

公開:Cloudflare Tunnel で自前配信、もしくは GitHub Pages で静的配信

love.js のビルド成果物は 静的ファイル(HTML / JS / WebAssembly / データファイル)なので、配信は何でも OK。

僕のやり方:Bun サーバー + Cloudflare Named Tunnel + launchd 常駐。Mac mini 上の Bun プロセスがゲームを配信し、Cloudflare Tunnel が solo-studio.caen.co.jp を Mac mini に向けています。サーバーレスにしたい人は GitHub Pages や Vercel に静的アップでも全く問題ない(→ 完全無料で Web サイトを公開する手順)。

まとめ|AI に最初から制約を渡せば、Love2D の Web 公開は普通に勝てる

整理すると、Love2D × love.js × AI駆動の正攻法はこう。

  1. Lua 5.1 制約(goto / bit / audio / 12.x API 禁止)をプロンプト冒頭に固定
  2. canvas スケーリング は CSS width/height ではなく transform: scale()Module.postMainLoop で初期化待ち
  3. Retina 対応t.window.highdpi = truelove.graphics.scale(SCALE, SCALE)
  4. スマホ操作 はバーチャルゲームパッドを JS でインジェクト
  5. 配信 は静的ファイル化されるのでどこでも OK

このレシピを最初から AI に渡しておけば、Love2D Web 公開のハマりどころは8割消えます。AI駆動開発の本質は 「人間が踏んだ地雷を、次の AI セッションに事前に教えておく」 こと(→ Claude Code 完全ガイド)。1回ハマったら、必ずプロジェクトの CLAUDE.md に書き残しましょう。

関連する記事

AI駆動開発のご相談・お仕事のご依頼

株式会社CAEN(代表:大森翔吾)では、Love2D / Godot × AI駆動 によるゲームプロトタイピング、Web 公開パイプライン構築、Discord Bot 連携の自動化支援などを承ります。

「自社のミニゲームを Web で配信したい」「Love2D の Web 公開でハマっている」「AI駆動でゲーム制作パイプラインを組みたい」など、お気軽にご相談ください。