Canvas で書いた Web のゲームを、初めての Godot に数十分で移植できた理由

Web で試作したドット絵のゲームを、Godot に移植しました。Godot を触るのは初めてで、インストールから始めて移植が終わるまで数十分ほどでした。移植は Claude Code に「移植して」と頼んだだけで、作り方は指定していません。
もともと Web で試作して遊び方を固め、そこからスマホアプリとしてリリースするつもりで作り始めたゲームです。移植してみて分かったのは、Canvas で描いていた Web のゲームは、書き方をほとんど変えずに Godot へ移せるということでした。どこがそのまま移り、どこが移らなかったのかを、実際のコードで紹介します。
GT ROAD の Web 版の作り
移植したのは「GT ROAD」という、メカニックから監督を目指すレースチームのゲームです。Web 版は Vite と TypeScript で書き、描画には Canvas 2D を使っています。コードは約 5,200 行です。
作りは単純です。
- 360×640 の仮想画面をオフスクリーンの Canvas に描き、表示用の Canvas へ整数倍に拡大して転送する
- 画面 1 枚ごとに
update/draw/tapを持つSceneを用意し、requestAnimationFrameのループから 1/60 秒の固定ステップで呼ぶ - ゲームの計算(レースのシミュレーション、シーズン、所持金など)は
game/に、描画の部品はengine/に、画面はscenes/に分ける
DOM の部品はほとんど使わず、ボタンも文字も Canvas に直接描いています。この作りが、Godot への移植のしやすさにつながりました。
Canvas の描画命令は _draw() に置き換わる
Godot には、ノードの _draw() の中で draw_rect() などを呼んで図形を直接描く仕組みがあります(Custom drawing in 2D)。毎フレーム queue_redraw() を呼べば _draw() が呼び直されるので、Canvas に毎フレーム描き直す Web 版と同じ考え方で書けます。
Claude Code は、Web 版の engine/draw.ts と同じ呼び方の描画関数を Gfx クラスにまとめ、その中で Godot の描画命令を呼ぶ形にしました。
class_name Gfx
## 描画の小道具。Web 版の engine/draw.ts と同じ呼び方で、いま描いている CanvasItem に描く。
static var ci: CanvasItem
static func rect(x: float, y: float, w: float, h: float, c: Variant) -> void:
ci.draw_rect(Rect2(x, y, w, h), col(c))その結果、各画面の描画コードは行単位で対応しています。タイトル画面の描画を並べると次のとおりです。
// Web 版(TypeScript)
c.fillStyle = '#0b0d14';
c.fillRect(0, 0, 360, 640);
drawTrackside(c, 116, this.game.time * 420, { mood: 'dusk', variant: 1, skyTop: 0 });
drawLogo(c, 30);
panel(c, 70, 100, 220, 24, '#0f1320', '#f2c230');
text(c, 'メカニックから監督へ', 180, 104, { align: 'center', color: '#fff' });# Godot 版(GDScript)
Gfx.rect(0, 0, 360, 640, "#0b0d14")
Scenery.draw_trackside(116, game.time * 420, {"mood": "dusk", "variant": 1, "sky_top": 0})
Logo.draw(30)
Gfx.panel(70, 100, 220, 24, "#0f1320", "#f2c230")
Gfx.text("メカニックから監督へ", 180, 104, {"align": "center"})座標も色も引数の順番も Web 版のままです。変わったのは、Canvas のコンテキスト c を引数で渡さなくなったことと、オブジェクトリテラルが Dictionary になったことくらいです。GDScript は型注釈を書ける言語なので、先ほどの rect(x: float, y: float, ...) -> void のように、型の付け方も TypeScript に近くなります。
画面の切り替えはノードではなくクラスのまま移した
Godot では、画面をシーン(ノードのツリー)として作り、エディタで部品を並べるのが一般的な作り方です。今回の移植ではそうせず、Web 版の Scene インターフェースをそのままなぞった GScene というクラスを作っています。
class_name GScene
extends RefCounted
## 画面 1 枚ぶん。Main が update → draw を毎フレーム呼び、タップを渡す
func enter() -> void:
pass
func update(_dt: float) -> void:
passノードとして置いてあるのは、ゲーム本体の Main(Node2D)1 つだけです。Main は _process() の中で 1/60 秒ごとに update を呼び、_draw() でいまの画面の draw を呼びます。Web 版の requestAnimationFrame のループが、Godot の _process() に置き換わっただけです。
func _process(delta: float) -> void:
# 固定ステップ: フレームレートが落ちてもゲーム内時間は実時間で進む
_acc += minf(0.5, delta)
var steps := 0
while _acc >= STEP and steps < 30:
_acc -= STEP
time += STEP
scene.update(STEP)
steps += 1
queue_redraw()画面のファイルも、scenes/title.ts が scenes/title.gd に、game/season.ts が game/season.gd に、と 1 対 1 で対応しています。Godot のシーンエディタを開かなくても、Web 版のコードを横に置けば、どこが何に当たるのかを読み比べられます。
整数倍の拡大と縦画面は設定だけで済んだ
Web 版では、ドットをにじませずに拡大するために、ウィンドウの大きさから倍率を計算して Canvas のサイズを変え、imageSmoothingEnabled = false で転送する処理を自分で書いていました。
Godot では、この処理が project.godot の設定だけで済みます。
[display]
window/size/viewport_width=360
window/size/viewport_height=640
window/stretch/mode="viewport"
window/stretch/aspect="keep"
window/stretch/scale_mode="integer"
window/handheld/orientation=1
[rendering]
textures/canvas_textures/default_texture_filter=0360×640 の画面に描き、縦横比を保ったまま整数倍に拡大し、テクスチャは補間しない(ニアレストネイバー)という指定です。orientation=1 はスマホで縦画面に固定する指定です。設定の意味は Multiple resolutions にまとまっています。
タップも、InputEventScreenTouch の座標がそのまま 360×640 の座標で届きます。Web 版で書いていた、画面上の座標を仮想画面の座標に戻す計算も要らなくなりました。
Web と同じにはならなかったところ
そのまま移らなかったのは、文字まわりの 2 つです。
1 つ目は文字の描画です。ゲームの文字には DotGothic16 というドット絵風のフォントを使っています。Godot で動かしてみると、文字がにじんでいることに気づきました。Godot はフォントの輪郭から文字を描くため、1 倍の表示では線がにじんだり太ったりします。
直し方は Claude Code が決めました。ドット絵風のゲームにしたいと伝えていたので、それに合わせたのだと思います。Godot 版ではフォントの字形を 16×16 の升目に合わせて 1 文字ずつドット絵に起こし、1 枚のテクスチャに並べて描く DotFont を作りました。大きい文字は、このドット絵を整数倍に拡大して描きます。Web 版にはない、Godot 版だけのコードです。
2 つ目は文字の入力です。チーム名などを入力する画面では、スマホのキーボードを出す必要があります。Canvas に描いた文字ではキーボードが出ないので、入力するときだけ Godot の部品(LineEdit などの Control ノード)を画面の上に重ねています。ゲームの中で Godot のノードを部品として使っているのは、ほぼここだけです。
Android への書き出し
スマホアプリにするのが目的なので、Android への書き出しの設定も入れました。export_presets.cfg に、パッケージ名、対応する CPU(arm64-v8a と x86_64)、アダプティブアイコンの前景と背景、没入モード(ステータスバーを隠す表示)を指定しています。レンダラーは、Godot のレンダラーの中でいちばん古い端末まで対応する GL Compatibility にしました。
いまは APK を書き出す設定を入れたところで、実機ではまだ動かしていません。それでも、Web で試作したゲームを、構成を変えずに Android 向けの書き出し設定まで進められました。

望月 涼太 / polidog
Co-Founder / Web Engineer