Godot 4で2Dゲームを作っていると、Camera2D の「移動制限(limit_*)」設定って地味に面倒ですよね。
タイルマップでステージを組み替えるたびに、インスペクタで limit_left / right / top / bottom を手で調整…
しかもステージごとにカメラ用シーンを分けたり、カメラを継承してシーン増殖したりと、「継承ベース」の設計だと管理がどんどんツラくなります。

そこで今回は、「タイルマップのサイズからカメラの移動範囲を自動で計算してくれる」コンポーネントを作ります。
カメラやステージを継承で増やすのではなく、「LimitSetter コンポーネントをペタッと付けるだけ」で完結させる、合成(Composition)スタイルのやり方ですね。

【Godot 4】タイルマップからカメラ境界を自動計算!「LimitSetter」コンポーネント

今回作る LimitSetter はこんなことをやってくれます:

  • 指定した TileMap の「使用中セルの矩形(used rect)」を取得
  • タイルサイズと掛け合わせて「ワールド座標の矩形」に変換
  • 指定した Camera2Dlimit_left/right/top/bottom を自動設定
  • 必要なら、ゲーム中にタイルマップが変化したときも再計算

つまり、「ステージを作り直しても、カメラ制限は勝手に合う」状態を目指します。
カメラはカメラ、タイルマップはタイルマップ、そして「限界値をつなぐ」のは LimitSetter という独立コンポーネントに任せる、という分離がポイントです。


LimitSetter.gd — フルコード


extends Node
class_name LimitSetter
## タイルマップの使用領域から Camera2D の limit_* を自動設定するコンポーネント。
##
## 想定用途:
## - ステージごとに TileMap のサイズが違っても、カメラ制限を自動で合わせたい
## - カメラやステージを継承で増やさず、「コンポーネントを付けるだけ」で完結させたい

@export var camera: Camera2D:
	## 対象となる Camera2D。
	## - 未設定の場合、親ノードやツリーから自動検索を試みます。
	get:
		return camera
	set(value):
		camera = value

@export var tile_map: TileMap:
	## カメラ制限の元になる TileMap。
	## - ステージ全体を表すタイルマップを指定してください。
	get:
		return tile_map
	set(value):
		tile_map = value

@export var apply_on_ready: bool = true:
	## true の場合、_ready() で一度だけ自動設定を行います。
	get:
		return apply_on_ready
	set(value):
		apply_on_ready = value

@export var apply_on_tree_entered: bool = false:
	## true の場合、ツリーに入るたびに自動設定を行います。
	## シーンを再インスタンス化するタイプのゲームで便利です。
	get:
		return apply_on_tree_entered
	set(value):
		apply_on_tree_entered = value

@export var apply_every_frame: bool = false:
	## true の場合、毎フレーム再計算します。
	## ランタイムで TileMap を書き換えるゲーム(破壊可能マップ等)で使えますが、
	## コストがかかるので必要なときだけ true にしてください。
	get:
		return apply_every_frame
	set(value):
		apply_every_frame = value

@export var margin_left: float = 0.0:
	## 左側に追加で持たせるマージン(ピクセル)。
	## ステージ外も少し見せたい場合などに調整します。
@export var margin_right: float = 0.0:
	## 右側のマージン(ピクセル)。
@export var margin_top: float = 0.0:
	## 上側のマージン(ピクセル)。
@export var margin_bottom: float = 0.0:
	## 下側のマージン(ピクセル)。

@export var use_global_coordinates: bool = true:
	## true の場合、TileMap のワールド座標を元に Camera2D の limit を設定します。
	## false の場合、同じ親を持つローカル座標系を前提にします。
	## 通常は true のままで問題ありません。

@export var debug_print_limits: bool = false:
	## true にすると、計算した limit 値をコンソールに出力します。
	## 値の確認やデバッグに便利です。

func _ready() -> void:
	if apply_on_ready:
		_apply_limits_safely()

func _enter_tree() -> void:
	if apply_on_tree_entered:
		_apply_limits_safely()

func _process(delta: float) -> void:
	if apply_every_frame:
		_apply_limits_safely()

## 安全に limit 設定を行うヘルパー
func _apply_limits_safely() -> void:
	var cam := _get_or_find_camera()
	var map := _get_or_find_tilemap()

	if cam == null or map == null:
		# 見つからない場合は何もしない
		return

	_set_camera_limits_from_tilemap(cam, map)

## camera が未指定なら親やツリーから探す
func _get_or_find_camera() -> Camera2D:
	if camera and is_instance_valid(camera):
		return camera

	# 1. 親から探す
	var parent := get_parent()
	if parent is Camera2D:
		camera = parent
		return camera

	# 2. ツリー全体から最初の Camera2D を探す
	var cameras := get_tree().get_nodes_in_group("Camera2D")
	if cameras.size() > 0 and cameras[0] is Camera2D:
		camera = cameras[0]
		return camera

	# 3. 型検索(Godot 4 では get_nodes_in_group 以外の手段もあるが簡易に)
	for node in get_tree().get_root().get_children():
		if node is Camera2D:
			camera = node
			return camera

	push_warning("LimitSetter: Camera2D が見つかりませんでした。'camera' をインスペクタで指定してください。")
	return null

## tile_map が未指定なら親や兄弟から探す
func _get_or_find_tilemap() -> TileMap:
	if tile_map and is_instance_valid(tile_map):
		return tile_map

	# 1. 親を TileMap とみなす
	var parent := get_parent()
	if parent is TileMap:
		tile_map = parent
		return tile_map

	# 2. 同じ親の子ノードから探す
	if parent:
		for child in parent.get_children():
			if child is TileMap:
				tile_map = child
				return tile_map

	push_warning("LimitSetter: TileMap が見つかりませんでした。'tile_map' をインスペクタで指定してください。")
	return null

## 実際に TileMap から Camera2D の limit_* を計算して設定するコア処理
func _set_camera_limits_from_tilemap(cam: Camera2D, map: TileMap) -> void:
	# TileMap の使用中セルの矩形を取得(タイル座標)
	var used_rect: Rect2i = map.get_used_rect()
	if used_rect.size == Vector2i.ZERO:
		# 何もタイルが置かれていない場合は何もしない
		return

	# タイルサイズ(ピクセル)
	var tile_size: Vector2 = map.tile_set.tile_size

	# タイル座標 → ローカル座標(ピクセル)に変換
	# used_rect.position は「左上のタイル座標」、size は「タイル数」
	var local_min := Vector2(used_rect.position) * tile_size
	var local_max := Vector2(used_rect.position + used_rect.size) * tile_size

	# 必要に応じてグローバル座標に変換
	var min_pos: Vector2 = local_min
	var max_pos: Vector2 = local_max

	if use_global_coordinates:
		min_pos = map.to_global(local_min)
		max_pos = map.to_global(local_max)

	# マージンを適用
	min_pos.x -= margin_left
	min_pos.y -= margin_top
	max_pos.x += margin_right
	max_pos.y += margin_bottom

	# Camera2D の limit_* は「ピクセル座標」で指定する
	cam.limit_left = int(min_pos.x)
	cam.limit_top = int(min_pos.y)
	cam.limit_right = int(max_pos.x)
	cam.limit_bottom = int(max_pos.y)

	if debug_print_limits:
		print("[LimitSetter] limits: left=", cam.limit_left,
			", right=", cam.limit_right,
			", top=", cam.limit_top,
			", bottom=", cam.limit_bottom)

## 外部から明示的に再計算したいとき用のパブリック関数
func apply_now() -> void:
	_apply_limits_safely()

使い方の手順

ここでは代表的な 2 パターンの例で説明します。

例1: 横スクロールのプレイヤー+カメラ+タイルマップ

シーン構成例:

Main (Node2D)
 ├── LevelTileMap (TileMap)
 ├── Player (CharacterBody2D)
 │    ├── Sprite2D
 │    └── CollisionShape2D
 ├── Camera2D
 └── LimitSetter (Node)

手順①: スクリプトを用意

  • LimitSetter.gd をプロジェクトの適当な場所(例: res://addons/components/LimitSetter.gd)に保存。

手順②: コンポーネントノードを追加

  • Main シーンを開く。
  • 右クリック → 「子ノードを追加」 → Node を追加し、名前を LimitSetter に変更。
  • LimitSetter ノードに、先ほどの LimitSetter.gd をアタッチ。

手順③: インスペクタで参照を設定

  • cameraCamera2D ノードをドラッグ&ドロップ。
  • tile_mapLevelTileMap ノードをドラッグ&ドロップ。
  • ステージが固定なら apply_on_ready = true のままでOK。
  • ステージを動的に作り替えるなら apply_every_frame = true か、後述の apply_now() を使います。

手順④: ゲームを実行して確認

  • ゲーム開始時に、タイルマップの使用領域にあわせて Camera2Dlimit_* が自動でセットされます。
  • マップを広げたり縮めたりしても、LimitSetter を付けたシーンを再生すれば自動で追従します。

例2: ステージごとに TileMap が変わるステージセレクト型ゲーム

ステージ側のシーン例:

Stage01 (Node2D)
 ├── TileMap (TileMap)
 ├── Camera2D
 └── LimitSetter (Node)

この構成にしておけば:

  • Stage01 を複製して Stage02, Stage03 を作る
  • 各ステージで TileMap だけを編集する
  • カメラの limit 設定は全部 LimitSetter が自動でやってくれる

つまり、ステージの「見た目」と「カメラ制御」の責務を分離しつつ、
継承ではなく「コンポーネントをペタッと付ける」だけで完結するわけですね。


メリットと応用

  • シーン構造がスッキリする
    カメラ専用のベースシーンを継承して増やす必要がなく、LimitSetter を必要なシーンにだけ付ければOKです。
  • ステージの差し替えに強い
    タイルマップのサイズが変わっても、カメラ制限は自動で再計算されます。
    レベルデザイナーは「タイルを置くこと」だけに集中できます。
  • 複数カメラにも対応しやすい
    画面分割やシネマティック用カメラなど、カメラが増えても
    それぞれに LimitSetter を付けるだけで独立して制御できます。
  • コンポーネント指向の良い練習になる
    「タイルマップ」「カメラ」「境界計算」をそれぞれ独立した責務として切り分ける考え方は、
    他のコンポーネント設計にもそのまま応用できます。

応用として、「特定のイベントでだけ再計算したい」ケースもあります。
例えば、ステージ生成が終わったタイミングで手動で呼ぶ場合:


func _on_stage_generated() -> void:
	# 同じシーン内の LimitSetter を取得して再計算
	var limit_setter := $LimitSetter as LimitSetter
	if limit_setter:
		limit_setter.apply_now()

こんな感じで、「いつ・どこでカメラの制限を決めるか」をシーンごとに柔軟に選べるのが、
コンポーネント方式の気持ちいいところですね。継承ベースでカメラシーンを量産していた人は、ぜひ一度このスタイルを試してみてください。