イントロダクション:SubViewportでゲーム表現を次のレベルへ
Godot Engineで開発を進める中で、こんな課題にぶつかったことはありませんか?
- 広大なマップをプレイヤーに分かりやすく提示するため、画面の隅にミニマップを置きたい。
- インベントリ画面で、キャラクターが装備しているアイテムを3Dモデルでプレビューさせたい。
- ポータルや監視カメラのように、別の場所の風景をリアルタイムで映し出す演出がしたい。
これらの課題は、単一のゲーム画面だけでは解決が困難です。しかし、Godotの強力なノードSubViewportを使いこなせば、すべて実現可能です。
SubViewportは、メイン画面とは独立した「仮想スクリーン」を生成し、その描画結果をテクスチャとして自由に再利用できる機能です。これにより、UI、ゲームロジック、グラフィック表現が劇的に向上します。
SubViewportのコア概念:独立したレンダリング世界
SubViewportを理解する鍵は、「メインの描画パイプラインから独立している」という点です。このノード配下に置かれたシーンは、独自のワールド、カメラ、レンダリング設定を持ち、完全に別の場所で描画されます。
そして、その描画結果をメインシーンに持ち込むための橋渡し役がViewportTextureです。SubViewportの出力をこのテクスチャに設定することで、TextureRect(UI用)やSprite2D、さらには3Dモデルのマテリアルに貼り付けることが可能になります。
基本的なセットアップ
- ノード追加: シーンに
SubViewportContainerを追加し、その子としてSubViewportを追加します。(SubViewportContainerを使うと、サイズ管理が容易になります) - コンテンツ作成:
SubViewportの子として、表示したいシーン(例: 2D/3Dノード、カメラ)を構築します。 - 表示:
SubViewportContainerと同じ階層にTextureRectを配置し、そのTextureプロパティに「新規ViewportTexture」を設定。インスペクタでSubViewportのパスを指定します。
活用法1: 高機能なミニマップの実装
ミニマップはSubViewportの代表的な活用例です。プレイヤーを追従するだけの単純なものから、さらに一歩進んだ実装を見ていきましょう。
発展的なGDScript:複数ターゲットの表示とアイコン制御
プレイヤーだけでなく、敵やアイテムもミニマップに表示し、状況に応じてアイコンを変える実践的なコ ードです。
# minimap_controller.gd (SubViewportにアタッチ)
extends SubViewport
@onready var player: CharacterBody2D = get_tree().get_first_node_in_group("player")
@onready var minimap_camera: Camera2D = $MinimapCamera
# ミニマップのスケール(ワールド座標からミニマップ座標への変換比率)
@export var map_scale: float = 0.1
# 敵アイコンへの参照をキャッシュ(毎フレームのノード検索を避ける)
var enemy_icons: Dictionary = {}
# テクスチャをpreloadでキャッシュ(毎フレームのload()を避ける)
const ICON_NORMAL = preload("res://assets/enemy_icon.png")
const ICON_ALERT = preload("res://assets/enemy_alert_icon.png")
func _process(delta: float) -> void:
if not is_instance_valid(player):
return
# カメラをプレイヤーに追従させる
minimap_camera.global_position = player.global_position
# 敵の位置をミニマップ座標に変換して表示
update_enemy_icons()
func update_enemy_icons() -> void:
# 存在する敵を取得してアイコンを更新
for enemy in get_tree().get_nodes_in_group("enemies"):
if not is_instance_valid(enemy):
continue
# アイコンがなければ生成
if not enemy_icons.has(enemy):
var icon = TextureRect.new()
icon.texture = ICON_NORMAL
add_child(icon)
enemy_icons[enemy] = icon
var icon: TextureRect = enemy_icons[enemy]
# 状態に応じてアイコンを変更
icon.texture = ICON_ALERT if enemy.is_in_alert_state() else ICON_NORMAL
# ワールド座標をミニマップのローカル座標に変換
# TextureRectはpositionを使用(global_positionではない)
var relative_pos = enemy.global_position - minimap_camera.global_position
icon.position = relative_pos * map_scale + size / 2
このコードでは、SubViewport自身が敵の位置を監視し、ミニマップ内にアイコンを動的に生成・更新しています。これにより、メインのゲームロジックとミニマップの描画ロジックを分離できます。
活用法2: 2D UIにインタラクティブな3Dプレビューを
インベントリ画面でキャラクターやアイテムの3Dモデルを回転表示できれば、プレイヤーの満足度は大きく向上します。これもSubViewportの得意分野です。
実装手順とインタラクション
- UIシーンに
SubViewportContainerとSubViewportを配置。 SubViewport内にCamera3D、WorldEnvironment(背景や環境光用)、DirectionalLight3D、そして表示したい3Dモデル(MeshInstance3Dなど)を配置します。SubViewportContainerにアタッチしたスクリプトで、マウス入力を受け取り、3Dモデルを回転させます。
# 3d_preview_viewport.gd (SubViewportContainerにアタッチ)
extends SubViewportContainer
@onready var subviewport: SubViewport = $SubViewport
@onready var model: Node3D = $SubViewport/TargetModel # 回転させたいモデル
var is_dragging = false
func _ready() -> void:
# マウス入力を受け取るためにmouse_filterを設定
# デフォルトでは入力を通さない場合があるため明示的に設定
mouse_filter = Control.MOUSE_FILTER_STOP
func _gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT:
is_dragging = event.is_pressed()
if event is InputEventMouseMotion and is_dragging:
# マウスの移動量に応じてモデルをY軸回転させる
model.rotate_y(deg_to_rad(-event.relative.x * 0.5))
SubViewportContainerはGUI入力を受け取れるため、このようなUIとの連携が非常に簡単です。
活用法3: レンダーテクスチャによる高度な視覚効果
SubViewportの真価は、その出力をテクスチャとしてシェーダーと組み合わせることで発揮されます。これにより、静的なUI表示を超えた動的な表現が可能になります。
ユースケース:監視カメラとポストプロセスシェーダー
別の場所を映すカメラの映像に、ノイズや走査線といったエフェクトをかけて「監視カメラらしさ」を演出します。
- 監視したい場所に
SubViewportとCamera3Dを設置します。 - メインシーンで、監視モニターとなる
MeshInstance3D(例:PlaneMesh)を用意します。 - モニターのメッシュに
ShaderMaterialを適用し、以下のシェーダーコードを設定します。
// security_camera_shader.gdshader
shader_type spatial;
uniform sampler2D screen_texture;
uniform float noise_amount = 0.05;
uniform float scanline_intensity = 0.1;
void fragment() {
vec2 uv = UV;
// 走査線
float scanline = sin(uv.y * 800.0) * scanline_intensity;
// ノイズ
float noise = (fract(sin(dot(uv, vec2(12.9898, 78.233))) * 43758.5453) - 0.5) * noise_amount;
vec4 color = texture(screen_texture, uv);
ALBEDO = color.rgb - scanline - noise;
}
- GDScriptで
SubViewportのViewportTextureをシェーダーのscreen_textureパラメ ータに渡します。
# monitor_screen.gd (MeshInstance3Dにアタッチ)
extends MeshInstance3D
@export var camera_viewport: SubViewport
func _ready() -> void:
if not camera_viewport:
return
var material: ShaderMaterial = self.get_surface_override_material(0)
if material:
var texture = camera_viewport.get_texture()
material.set_shader_parameter("screen_texture", texture)
パフォーマンス、よくある間違いとベストプラクティス
SubViewportは強力ですが、無計画な使用はパフォーマンスの低下を招きます。以下の表を参考に、効率的な実装を心がけてください。
| よくある間違い | ベストプラクティス |
|---|---|
Update Modeを常にAlwaysにする | 必要な時だけ更新する。 UI表示ならWhen Visible、手動更新ならDisabledを使い、_processでの毎フレーム更新を避ける。 |
| 不必要に高い解像度を設定する | 解像度を最適化する。 3Dプレビューなら256x256など、表示サイズに合わせた最小限の解像度にする。 |
カメラのculling_maskを意識しない | 描画レイヤーを分離する。 ミニマップ用のカメラにはミニマップに表示すべきオブジェクトのレイヤーのみを描画させ、不要な計算を省く。 |
1つのSubViewportに全てを詰め込む | 役割ごとにSubViewportを分割する。 ミニマップ用、3Dプレビュー用など、責務を分けることで管理しやすくなり、個別の最適化も容易になる。 |
| 代替手段を検討しない | コストと表現を天秤にかける。 静的なプレビューならただの画像、単純なミニマップならロジックベースのUI描画など、より軽量な代替案も常に視野に入れる。 |
特に、モバイルゲーム開発ではSubViewportの使用は慎重に行うべきです。各ビューポートが追加のドローコールとレンダリングパスを発生させることを常に念頭に置きましょう。
次のステップと関連トピック
SubViewportをマスターしたら、さらに高度な表現に挑戦できます。
- 動的なデカール: 銃弾の跡やキャラクターの足跡などを、
SubViewportでレンダリングしてメッシュに投影する。 - 流体シミュレーションの可視化: 計算結果を
SubViewport経由でテクスチャに焼き付け、インタラクティブな水面などを作成する。 - マルチパスレンダリング: 同じシーンを異なるシェーダーや設定で複数回レンダリングし、アウトライン描画や特殊なエフェクトを合成する。
これらのトピッ クについては、Godotの公式ドキュメントやコミュニティのチュートリアルが素晴らしいリソースとなります。
まとめ
本記事では、SubViewportの基本から応用まで、具体的なコード例と最適化の手法を交えて解説しました。SubViewportは単なるUIパーツではなく、Godotのレンダリングパイプラインを拡張し、ゲームの表現力を飛躍させるための重要な鍵です。
| 活用法 | 主な目的 | パフォーマンスの鍵 |
|---|---|---|
| 高機能ミニマップ | ゲーム世界の状況を俯瞰的に提示する。 | Update Modeの最適化、culling_maskによる描画対象の限定。 |
| インタラクティブ3Dプレビュー | 2D UI内で3Dモデルを魅力的に見せる。 | SubViewportの解像度を最小限に抑える。 |
| レンダーテクスチャ | リアルタイムの映像をシェーダーと組み合わせ、高度な視覚効果を生む。 | シェーダーの複雑さとビューポートの更新頻度のバランスを取る。 |
これらのテクニックを武器に、Godotプロジェクトをさらにユニークで魅力的なものにしてください。