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

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=0

360×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

望月 涼太 / polidog

Co-Founder / Web Engineer