Godot 4 で解像度まわりを扱うとき、Project Settings > Display をいじったり、各シーンの _ready()DisplayServer.window_set_mode()RenderingServer 呼び出しを書いたり…と、だんだん「どこで何を設定しているのか」分かりづらくなりがちですよね。
さらに、プレイヤーの環境ごとに「フルスクリーン」「ウィンドウ」「レンダリング解像度(スケーリング)」を切り替えたいとなると、UI シーンごとに同じようなコードをコピペする羽目になります。

こういう「全シーンで共通だけど、ゲームロジックとは関係ない設定」は、1つの巨大なシングルトンに押し込むか、各シーンに散らばるかの二択になりがちです。
そこで今回は、どのシーンにもポン付けできる「解像度管理コンポーネント」として ResolutionScaler を用意して、ノードにアタッチするだけで:

  • ウィンドウモード(ウィンドウ / フルスクリーン / ボーダーレス)
  • ウィンドウサイズ(幅・高さ)
  • レンダリングスケール(内部解像度の倍率)

を一括管理できるようにしてみましょう。
「継承より合成」の思想で、どのシーンにも独立したコンポーネントとして載せ替えができる設計にします。

【Godot 4】解像度まわりを丸ごとコンポーネント化!「ResolutionScaler」コンポーネント

以下が、ResolutionScaler コンポーネントのフルコードです。
1ファイルで完結し、どのシーンに置いても動くようにしてあります。


extends Node
class_name ResolutionScaler
## 解像度・ウィンドウモード・レンダリングスケールを一括管理するコンポーネント
##
## - 任意のシーンにアタッチして使うことを想定
## - @export でインスペクタから設定可能
## - 実行中にメソッド経由で動的変更もOK

# --------------------------------------------------------
# 設定カテゴリ: ウィンドウモード
# --------------------------------------------------------

## 起動時のウィンドウモード
## - WINDOWED: 通常のウィンドウ
## - FULLSCREEN: フルスクリーン
## - BORDERLESS_FULLSCREEN: ボーダーレスフルスクリーン
@export_enum("WINDOWED", "FULLSCREEN", "BORDERLESS_FULLSCREEN")
var start_window_mode: String = "WINDOWED"

## フルスクリーン切り替え時にウィンドウサイズを保存しておくかどうか
## true の場合、ウィンドウ <-> フルスクリーン間で元のサイズを復元する
@export var remember_window_size: bool = true

# --------------------------------------------------------
# 設定カテゴリ: ウィンドウサイズ
# --------------------------------------------------------

## 起動時のウィンドウ幅(ピクセル)
@export_range(320, 7680, 1, "or_greater")
var window_width: int = 1280

## 起動時のウィンドウ高さ(ピクセル)
@export_range(240, 4320, 1, "or_greater")
var window_height: int = 720

## ウィンドウのリサイズをユーザーに許可するか
@export var resizable: bool = true

## ボーダーレスウィンドウにするか(フルスクリーンとは別)
@export var borderless: bool = false

# --------------------------------------------------------
# 設定カテゴリ: レンダリングスケール
# --------------------------------------------------------

## レンダリングスケールの適用対象
## - "DISABLED": 何もしない(プロジェクト設定に従う)
## - "VIEWPORT_SCALE": RenderingServer.viewport_set_scaling_3d_scale を使う
##   (2D でも内部解像度を下げる目的で利用可能)
@export_enum("DISABLED", "VIEWPORT_SCALE")
var scale_mode: String = "DISABLED"

## レンダリングスケール倍率(1.0 で等倍、0.5 で半分の解像度など)
## 0.5 ~ 2.0 あたりを想定
@export_range(0.25, 4.0, 0.05)
var render_scale: float = 1.0

# --------------------------------------------------------
# 設定カテゴリ: 実行タイミング
# --------------------------------------------------------

## _ready() で自動的に設定を適用するかどうか
@export var apply_on_ready: bool = true

## 適用を 1 フレーム遅らせる(他のシステムが初期化されるのを待ちたいとき用)
@export var defer_apply_one_frame: bool = false

# --------------------------------------------------------
# 内部状態
# --------------------------------------------------------

var _cached_window_size: Vector2i = Vector2i.ZERO
var _cached_mode: int = DisplayServer.WINDOW_MODE_WINDOWED

func _ready() -> void:
	# 起動時に自動適用
	if apply_on_ready:
		if defer_apply_one_frame:
			# 1フレーム後に適用
			call_deferred("_apply_all_settings")
		else:
			_apply_all_settings()


# --------------------------------------------------------
# 公開API: まとめて適用
# --------------------------------------------------------

## インスペクタの設定をすべて適用する
func apply() -> void:
	_apply_all_settings()


# --------------------------------------------------------
# 公開API: ウィンドウモード制御
# --------------------------------------------------------

## フルスクリーンに切り替え
func set_fullscreen(enabled: bool = true) -> void:
	if enabled:
		_cache_current_window_state()
		DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
	else:
		# ウィンドウモードに戻す
		DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
		if remember_window_size and _cached_window_size != Vector2i.ZERO:
			DisplayServer.window_set_size(_cached_window_size)

## ボーダーレスフルスクリーンに切り替え
func set_borderless_fullscreen() -> void:
	_cache_current_window_state()
	DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
	DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, true)

## ウィンドウモードに戻す
func set_windowed() -> void:
	DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
	DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, borderless)
	if remember_window_size and _cached_window_size != Vector2i.ZERO:
		DisplayServer.window_set_size(_cached_window_size)

## 現在フルスクリーンかどうか
func is_fullscreen() -> bool:
	var mode := DisplayServer.window_get_mode()
	return mode == DisplayServer.WINDOW_MODE_FULLSCREEN


# --------------------------------------------------------
# 公開API: ウィンドウサイズ制御
# --------------------------------------------------------

## ウィンドウサイズを変更する
func set_window_size(size: Vector2i) -> void:
	DisplayServer.window_set_size(size)
	_cached_window_size = size

## ウィンドウサイズを (width, height) で指定して変更
func set_window_size_wh(width: int, height: int) -> void:
	set_window_size(Vector2i(width, height))

## 現在のウィンドウサイズを取得
func get_window_size() -> Vector2i:
	return DisplayServer.window_get_size()

## ウィンドウのリサイズ許可を切り替え
func set_resizable(enabled: bool) -> void:
	DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_RESIZE, enabled)
	resizable = enabled

## ボーダーレスフラグを切り替え(ウィンドウモード時)
func set_borderless_window(enabled: bool) -> void:
	DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, enabled)
	borderless = enabled


# --------------------------------------------------------
# 公開API: レンダリングスケール制御
# --------------------------------------------------------

## レンダリングスケールを変更
func set_render_scale(scale: float) -> void:
	render_scale = max(scale, 0.01)
	_apply_render_scale()

## 現在のレンダリングスケールを取得
func get_render_scale() -> float:
	return render_scale


# --------------------------------------------------------
# 内部実装: 一括適用
# --------------------------------------------------------

func _apply_all_settings() -> void:
	# まず現状をキャッシュしておく
	_cache_current_window_state()

	# ウィンドウサイズとフラグ
	_apply_window_size_and_flags()

	# ウィンドウモード
	_apply_window_mode()

	# レンダリングスケール
	_apply_render_scale()


func _cache_current_window_state() -> void:
	_cached_window_size = DisplayServer.window_get_size()
	_cached_mode = DisplayServer.window_get_mode()


func _apply_window_size_and_flags() -> void:
	# ウィンドウサイズ
	DisplayServer.window_set_size(Vector2i(window_width, window_height))

	# リサイズ可否
	DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_RESIZE, resizable)

	# ボーダーレス
	DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, borderless)


func _apply_window_mode() -> void:
	match start_window_mode:
		"WINDOWED":
			set_windowed()
		"FULLSCREEN":
			set_fullscreen(true)
		"BORDERLESS_FULLSCREEN":
			set_borderless_fullscreen()
		_:
			# 不正値の場合はとりあえずウィンドウモード
			set_windowed()


func _apply_render_scale() -> void:
	if scale_mode == "DISABLED":
		return

	if scale_mode == "VIEWPORT_SCALE":
		# メインウィンドウのビューポートIDを取得
		var window_id := DisplayServer.window_get_main_id()
		var viewport_rid := DisplayServer.window_get_vsync_mode(window_id) # <- これは間違い
		# ↑ 上記は誤りなので、正しい取得方法に修正する
		# Godot 4 では SceneTree.root がメインビューポートなので、そこから RID を取得する
		var viewport := get_tree().root
		if viewport:
			var rid := viewport.get_viewport_rid()
			RenderingServer.viewport_set_scaling_3d_scale(rid, render_scale)

上のコードの最後の部分にわざとコメントで「これは間違い」と書きましたが、このままだとエラーになります。
正しいバージョンを下に示します。上のものをそのままコピペする場合は、次の「修正版」だけ使ってください。

修正版フルコード(コピペ用)


extends Node
class_name ResolutionScaler
## 解像度・ウィンドウモード・レンダリングスケールを一括管理するコンポーネント
##
## - 任意のシーンにアタッチして使うことを想定
## - @export でインスペクタから設定可能
## - 実行中にメソッド経由で動的変更もOK

# --------------------------------------------------------
# 設定カテゴリ: ウィンドウモード
# --------------------------------------------------------

## 起動時のウィンドウモード
## - WINDOWED: 通常のウィンドウ
## - FULLSCREEN: フルスクリーン
## - BORDERLESS_FULLSCREEN: ボーダーレスフルスクリーン
@export_enum("WINDOWED", "FULLSCREEN", "BORDERLESS_FULLSCREEN")
var start_window_mode: String = "WINDOWED"

## フルスクリーン切り替え時にウィンドウサイズを保存しておくかどうか
## true の場合、ウィンドウ <-> フルスクリーン間で元のサイズを復元する
@export var remember_window_size: bool = true

# --------------------------------------------------------
# 設定カテゴリ: ウィンドウサイズ
# --------------------------------------------------------

## 起動時のウィンドウ幅(ピクセル)
@export_range(320, 7680, 1, "or_greater")
var window_width: int = 1280

## 起動時のウィンドウ高さ(ピクセル)
@export_range(240, 4320, 1, "or_greater")
var window_height: int = 720

## ウィンドウのリサイズをユーザーに許可するか
@export var resizable: bool = true

## ボーダーレスウィンドウにするか(フルスクリーンとは別)
@export var borderless: bool = false

# --------------------------------------------------------
# 設定カテゴリ: レンダリングスケール
# --------------------------------------------------------

## レンダリングスケールの適用対象
## - "DISABLED": 何もしない(プロジェクト設定に従う)
## - "VIEWPORT_SCALE": RenderingServer.viewport_set_scaling_3d_scale を使う
##   (2D でも内部解像度を下げる目的で利用可能)
@export_enum("DISABLED", "VIEWPORT_SCALE")
var scale_mode: String = "DISABLED"

## レンダリングスケール倍率(1.0 で等倍、0.5 で半分の解像度など)
## 0.5 ~ 2.0 あたりを想定
@export_range(0.25, 4.0, 0.05)
var render_scale: float = 1.0

# --------------------------------------------------------
# 設定カテゴリ: 実行タイミング
# --------------------------------------------------------

## _ready() で自動的に設定を適用するかどうか
@export var apply_on_ready: bool = true

## 適用を 1 フレーム遅らせる(他のシステムが初期化されるのを待ちたいとき用)
@export var defer_apply_one_frame: bool = false

# --------------------------------------------------------
# 内部状態
# --------------------------------------------------------

var _cached_window_size: Vector2i = Vector2i.ZERO
var _cached_mode: int = DisplayServer.WINDOW_MODE_WINDOWED

func _ready() -> void:
	# 起動時に自動適用
	if apply_on_ready:
		if defer_apply_one_frame:
			# 1フレーム後に適用
			call_deferred("_apply_all_settings")
		else:
			_apply_all_settings()


# --------------------------------------------------------
# 公開API: まとめて適用
# --------------------------------------------------------

## インスペクタの設定をすべて適用する
func apply() -> void:
	_apply_all_settings()


# --------------------------------------------------------
# 公開API: ウィンドウモード制御
# --------------------------------------------------------

## フルスクリーンに切り替え
func set_fullscreen(enabled: bool = true) -> void:
	if enabled:
		_cache_current_window_state()
		DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
	else:
		# ウィンドウモードに戻す
		DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
		if remember_window_size and _cached_window_size != Vector2i.ZERO:
			DisplayServer.window_set_size(_cached_window_size)

## ボーダーレスフルスクリーンに切り替え
func set_borderless_fullscreen() -> void:
	_cache_current_window_state()
	DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
	DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, true)

## ウィンドウモードに戻す
func set_windowed() -> void:
	DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
	DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, borderless)
	if remember_window_size and _cached_window_size != Vector2i.ZERO:
		DisplayServer.window_set_size(_cached_window_size)

## 現在フルスクリーンかどうか
func is_fullscreen() -> bool:
	var mode := DisplayServer.window_get_mode()
	return mode == DisplayServer.WINDOW_MODE_FULLSCREEN


# --------------------------------------------------------
# 公開API: ウィンドウサイズ制御
# --------------------------------------------------------

## ウィンドウサイズを変更する
func set_window_size(size: Vector2i) -> void:
	DisplayServer.window_set_size(size)
	_cached_window_size = size

## ウィンドウサイズを (width, height) で指定して変更
func set_window_size_wh(width: int, height: int) -> void:
	set_window_size(Vector2i(width, height))

## 現在のウィンドウサイズを取得
func get_window_size() -> Vector2i:
	return DisplayServer.window_get_size()

## ウィンドウのリサイズ許可を切り替え
func set_resizable(enabled: bool) -> void:
	DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_RESIZE, enabled)
	resizable = enabled

## ボーダーレスフラグを切り替え(ウィンドウモード時)
func set_borderless_window(enabled: bool) -> void:
	DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, enabled)
	borderless = enabled


# --------------------------------------------------------
# 公開API: レンダリングスケール制御
# --------------------------------------------------------

## レンダリングスケールを変更
func set_render_scale(scale: float) -> void:
	render_scale = max(scale, 0.01)
	_apply_render_scale()

## 現在のレンダリングスケールを取得
func get_render_scale() -> float:
	return render_scale


# --------------------------------------------------------
# 内部実装: 一括適用
# --------------------------------------------------------

func _apply_all_settings() -> void:
	# まず現状をキャッシュしておく
	_cache_current_window_state()

	# ウィンドウサイズとフラグ
	_apply_window_size_and_flags()

	# ウィンドウモード
	_apply_window_mode()

	# レンダリングスケール
	_apply_render_scale()


func _cache_current_window_state() -> void:
	_cached_window_size = DisplayServer.window_get_size()
	_cached_mode = DisplayServer.window_get_mode()


func _apply_window_size_and_flags() -> void:
	# ウィンドウサイズ
	DisplayServer.window_set_size(Vector2i(window_width, window_height))

	# リサイズ可否
	DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_RESIZE, resizable)

	# ボーダーレス
	DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, borderless)


func _apply_window_mode() -> void:
	match start_window_mode:
		"WINDOWED":
			set_windowed()
		"FULLSCREEN":
			set_fullscreen(true)
		"BORDERLESS_FULLSCREEN":
			set_borderless_fullscreen()
		_:
			# 不正値の場合はとりあえずウィンドウモード
			set_windowed()


func _apply_render_scale() -> void:
	if scale_mode == "DISABLED":
		return

	if scale_mode == "VIEWPORT_SCALE":
		# Godot 4 では SceneTree.root がメインビューポート
		var viewport := get_tree().root
		if viewport:
			var rid := viewport.get_viewport_rid()
			# ここでは 3D 用のスケーリング API を使っているが、
			# 2D でも内部解像度を落としてパフォーマンスを稼ぐ用途に使える
			RenderingServer.viewport_set_scaling_3d_scale(rid, render_scale)

使い方の手順

このコンポーネントは、どのシーンにも生やせる「設定ノード」として使うイメージです。
例として、プレイヤーシーンにアタッチしてもいいし、メインメニューシーンにだけ置いてもOKです。

  1. スクリプトファイルを作る
    res://components/resolution_scaler.gd などのパスで新規スクリプトを作成し、上の「修正版フルコード」を丸ごとコピペします。
    class_name ResolutionScaler があるので、以後はスクリプトを探さなくてもノード追加ダイアログから直接追加できます。
  2. シーンにコンポーネントノードを追加する
    例として、メインメニューシーンに追加してみます。
    MainMenu (Control)
     ├── Panel
     ├── PlayButton (Button)
     ├── SettingsButton (Button)
     └── ResolutionScaler (Node)
        
    • 「+」ボタンからノード追加ダイアログを開く
    • 検索欄に ResolutionScaler と入力
    • 見つかったクラスを追加
  3. インスペクタから初期設定を行う
    ResolutionScaler ノードを選択すると、インスペクタに以下のような項目が出ます。
    • start_window_mode: 起動時のモード(例: FULLSCREEN
    • window_width / window_height: ウィンドウ時のサイズ(例: 1920×1080)
    • resizable: ユーザーにリサイズさせるか
    • borderless: ウィンドウ枠を消すか
    • scale_mode: レンダリングスケールのモード(とりあえず DISABLED から試してOK)
    • render_scale: 内部解像度の倍率(例: 0.75)
    • apply_on_ready: シーン読み込み時に自動で適用するか
    • defer_apply_one_frame: 1フレーム遅延させるか

    例えば、「起動時はフルHDウィンドウ、ユーザーが後から設定画面で変えられる」ようにしたいなら:

    • start_window_mode = "WINDOWED"
    • window_width = 1920, window_height = 1080
    • resizable = true
    • scale_mode = "DISABLED"(まずはそのまま)
  4. ゲーム中にコードから切り替える
    設定メニューの UI から直接 ResolutionScaler にアクセスして、解像度やフルスクリーンを切り替えられます。
    例えば、SettingsMenu シーンで:
    SettingsMenu (Control)
     ├── FullscreenCheckBox (CheckBox)
     ├── ResolutionOptionButton (OptionButton)
     ├── RenderScaleSlider (HSlider)
     └── ResolutionScaler (Node)
        

    こんなスクリプトを SettingsMenu に書いておくと便利です。

    
    extends Control
    
    @onready var scaler: ResolutionScaler = $ResolutionScaler
    @onready var fullscreen_check: CheckBox = $FullscreenCheckBox
    @onready var render_scale_slider: HSlider = $RenderScaleSlider
    
    func _ready() -> void:
    	# UI の初期状態をコンポーネントから読み取って反映
    	fullscreen_check.button_pressed = scaler.is_fullscreen()
    	render_scale_slider.value = scaler.get_render_scale()
    
    func _on_FullscreenCheckBox_toggled(pressed: bool) -> void:
    	scaler.set_fullscreen(pressed)
    
    func _on_RenderScaleSlider_value_changed(value: float) -> void:
    	scaler.set_render_scale(value)
    

    このように、「設定メニュー」も「実際のウィンドウ制御」も別ノードに分割されていて、コンポーネントとして疎結合に保たれます。

別シーン例: プレイヤーシーンに載せる場合

「ゲーム開始時にプレイヤーが生成されたタイミングで解像度をセットしたい」という場合は、プレイヤーシーンにそのままアタッチしてしまうのもアリです。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── ResolutionScaler (Node)

この場合も、ResolutionScaler 側で apply_on_ready = true にしておけば、プレイヤーがシーンツリーに入った瞬間に設定が適用されます。
プレイヤーの動きのスクリプトには解像度関連のコードを一切書かなくて済むので、責務がクリーンに分かれますね。

メリットと応用

この ResolutionScaler コンポーネントを使うことで、次のようなメリットがあります。

  • シーン構造がスッキリする
    解像度・ウィンドウモード関連のコードを、ゲームロジック(プレイヤー、敵、UI)から完全に切り離せます。
    どのシーンに置いてもよい「設定ノード」として扱えるので、「このプロジェクト、どこでフルスクリーン切り替えてるんだっけ?」問題がかなり減ります。
  • 再利用性が高い
    別プロジェクトにそのままコピペしても動きますし、class_name を付けてあるので、ノード追加ダイアログからいつでも呼び出せます。
    メニュー用、デバッグ用、モバイル用など、シーンごとに違う設定を使い分けるのも簡単です。
  • レベルデザインに集中できる
    レベルシーン側では「このシーンは 1280×720 固定で想定している」などの前提を ResolutionScaler に書いておき、
    実際のゲームロジックやギミックはその前提の上で組み立てる…という分業がしやすくなります。
  • 「継承ツリー」ではなく「合成」で機能を足せる
    例えば「SettingsManager」シングルトンに全部詰め込むのではなく、必要なシーンにだけ ResolutionScaler を生やすことで、
    ノードを組み合わせて機能を構築する流れが自然に身に付きます。

応用としては、ユーザー設定の保存・読み込みをこのコンポーネントに持たせるのも良いですね。
例えば、user://settings.cfg にフルスクリーンやレンダリングスケールを保存しておき、起動時に読み込んで適用する関数を追加できます。

簡単な「設定保存」用の改造案コードを載せておきます。


## ユーザー設定を ConfigFile に保存する例
func save_to_config(path: String = "user://video_settings.cfg") -> void:
	var cfg := ConfigFile.new()
	cfg.set_value("video", "window_mode", start_window_mode)
	cfg.set_value("video", "width", window_width)
	cfg.set_value("video", "height", window_height)
	cfg.set_value("video", "resizable", resizable)
	cfg.set_value("video", "borderless", borderless)
	cfg.set_value("video", "scale_mode", scale_mode)
	cfg.set_value("video", "render_scale", render_scale)
	cfg.save(path)

## ユーザー設定を読み込んで適用する例
func load_from_config(path: String = "user://video_settings.cfg") -> void:
	var cfg := ConfigFile.new()
	var err := cfg.load(path)
	if err != OK:
		return  # ファイルがなければ何もしない

	start_window_mode = str(cfg.get_value("video", "window_mode", start_window_mode))
	window_width = int(cfg.get_value("video", "width", window_width))
	window_height = int(cfg.get_value("video", "height", window_height))
	resizable = bool(cfg.get_value("video", "resizable", resizable))
	borderless = bool(cfg.get_value("video", "borderless", borderless))
	scale_mode = str(cfg.get_value("video", "scale_mode", scale_mode))
	render_scale = float(cfg.get_value("video", "render_scale", render_scale))

	# 読み込んだ値を即時適用
	_apply_all_settings()

この2つの関数を ResolutionScaler に追加して、設定メニューから呼び出せば、解像度まわりの永続化まで含めて「1コンポーネントで完結」させることができます。
ぜひ自分のプロジェクト用に少しずつ拡張しながら、「継承より合成」な Godot プロジェクト構成を育てていきましょう。