Godotでエンディングのスタッフロールを作ろうとすると、けっこう面倒ですよね。
ラベルを縦にズラッと並べてアニメーションさせたり、シーンごとにテキストをコピペしたり…。
しかも「ちょっと名前を追加したい」「ローカライズしたい」となると、またシーンを開いて編集して…と管理コストが地味に重くなります。

継承ベースで CreditScene.gd みたいな専用シーンを作る方法もありますが、
「エンディングだけじゃなく、タイトル画面の隅っこでスタッフロール流したい」みたいなときに、
同じようなシーンを量産することになりがちです。

そこで今回は、どんなシーンにもポン付けできる「コンポーネント」として、
テキストファイルを読み込んで自動スクロールするスタッフロール用コンポーネント
「CreditRoll」 を用意しました。
ラベルやアニメーションは全部コンポーネント側で面倒を見てくれるので、
使う側は「どのテキストを、どのスピードで流すか」だけ指定してあげればOKです。

【Godot 4】シーンにポン付けでエンディング!「CreditRoll」コンポーネント

この「CreditRoll」は、任意のノード(たとえば ControlCanvasLayer)にアタッチして使う、
コンポーネント指向のスタッフロール実装です。

  • テキストファイル(UTF-8)から内容を読み込む
  • 下から上へ一定速度でスクロールする
  • スクロール完了時にシグナルで通知
  • インスペクターから速度・マージン・フォントサイズなどを調整可能

という、よくあるスタッフロールの要件を一通りカバーしています。
ラベルやタイマーを個別に管理する必要はありません。


フルコード(GDScript / Godot 4)


extends Control
class_name CreditRoll
"""
テキストファイルからスタッフロールを読み込み、
下から上へ自動スクロールさせるコンポーネント。

使い方:
- 任意の Control 系ノードにこのスクリプトをアタッチ
- `credit_file` に .txt ファイルを指定
- シーンを再生すると自動でスクロール開始
"""

## スクロールが終わったときに発火するシグナル
signal roll_finished

@export_file("*.txt")
var credit_file: String = "" :
	set(value):
		credit_file = value
		# エディタ上で変更されたときも即時反映したい場合はここで再読み込みしてもよい
		# ただし、実行中のみ読み込みたいので _ready でロードするようにしています

@export_range(10.0, 500.0, 1.0, "or_greater")
var scroll_speed: float = 80.0
## 1秒あたり何ピクセル上に動かすか。値が大きいほど速くスクロールします。

@export_range(0.0, 200.0, 1.0, "or_greater")
var bottom_margin: float = 40.0
## コンポーネントの下端から、最初の行が現れるまでのマージン(ピクセル)。

@export_range(0.0, 200.0, 1.0, "or_greater")
var top_margin: float = 40.0
## 最後の行が上に抜け切るまでの余白。大きいほど「余韻」が長くなります。

@export var auto_start: bool = true
## true の場合、_ready で自動的にスクロールを開始します。

@export var loop: bool = false
## true にすると、スクロール終了後に最初から繰り返します。

@export var font_size: int = 24
## デフォルトフォントを使う場合の文字サイズ。独自フォントを使う場合は label 側で上書きも可。

@export var line_spacing: float = 4.0
## 行間をどれだけ空けるか(ピクセル単位)。

@export_multiline
var fallback_text: String = "STAFF ROLL\nYour Name Here" :
	## credit_file が見つからない場合に表示するテキスト
	set(value):
		fallback_text = value

# 内部で使うラベル
var _label: Label
# スクロール中かどうか
var _is_scrolling: bool = false
# 現在のY座標
var _current_y: float = 0.0
# テキストの合計高さ
var _content_height: float = 0.0

func _ready() -> void:
	# レイアウトの基本設定
	anchor_left = 0.0
	anchor_top = 0.0
	anchor_right = 1.0
	anchor_bottom = 1.0
	offset_left = 0.0
	offset_top = 0.0
	offset_right = 0.0
	offset_bottom = 0.0
	mouse_filter = Control.MOUSE_FILTER_IGNORE

	_create_label()
	_load_text()

	# サイズ変更に追従するためのシグナル接続
	resized.connect(_on_resized)

	if auto_start:
		start_roll()

func _process(delta: float) -> void:
	if not _is_scrolling:
		return

	_current_y -= scroll_speed * delta
	_update_label_position()

	# コンテンツ全体が上に抜けたかどうかを判定
	if _current_y + _content_height < -top_margin:
		if loop:
			_reset_position()
		else:
			_is_scrolling = false
			emit_signal("roll_finished")

func _on_resized() -> void:
	# コンテナのサイズが変わったら、テキストの幅を更新して高さを再計算
	if is_instance_valid(_label):
		_label.size.x = size.x
		_recalculate_content_height()
		_update_label_position()

func _create_label() -> void:
	# 既にあれば再利用
	if is_instance_valid(_label):
		return

	_label = Label.new()
	_label.name = "CreditLabel"
	_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
	_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP
	_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
	_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	_label.size_flags_vertical = Control.SIZE_SHRINK_CENTER
	_label.position = Vector2.ZERO
	_label.size = size

	# デフォルトフォントサイズを設定(プロジェクト設定のフォントが使われます)
	var theme_font_size: int = font_size
	_label.add_theme_font_size_override("font_size", theme_font_size)

	add_child(_label)

func _load_text() -> void:
	var text_to_use := ""

	if credit_file != "" and ResourceLoader.exists(credit_file):
		# res:// or user:// のテキストファイルを読み込む
		var file := FileAccess.open(credit_file, FileAccess.READ)
		if file:
			text_to_use = file.get_as_text()
		else:
			push_warning("CreditRoll: Failed to open credit_file: %s. Using fallback_text." % credit_file)
	else:
		if credit_file != "":
			push_warning("CreditRoll: credit_file not found: %s. Using fallback_text." % credit_file)

	if text_to_use == "":
		text_to_use = fallback_text

	# 行間を反映するため、Label の custom_constants を使う
	_label.add_theme_constant_override("line_spacing", int(line_spacing))
	_label.text = text_to_use

	# テキストをセットしたら高さを再計算
	await get_tree().process_frame
	_recalculate_content_height()
	_reset_position()

func _recalculate_content_height() -> void:
	# Label の最適サイズからコンテンツ高さを取得
	_label.size.x = size.x
	var min_size := _label.get_minimum_size()
	_content_height = min_size.y

func _reset_position() -> void:
	# 下端から bottom_margin だけ下に隠れた位置からスタート
	_current_y = size.y + bottom_margin
	_update_label_position()

func _update_label_position() -> void:
	if not is_instance_valid(_label):
		return
	_label.position = Vector2(0, _current_y)

# === 公開API ===

## スタッフロールを開始(または再開)します
func start_roll() -> void:
	if not is_instance_valid(_label):
		_create_label()
	_load_text()
	_is_scrolling = true

## スクロールを一時停止します
func pause_roll() -> void:
	_is_scrolling = false

## スクロールを再開します(位置はそのまま)
func resume_roll() -> void:
	_is_scrolling = true

## スクロール位置をリセットし、すぐに開始します
func restart_roll() -> void:
	_reset_position()
	_is_scrolling = true

## 外部からテキストを直接渡してスタッフロールを更新したい場合に使います
func set_text(text: String) -> void:
	if not is_instance_valid(_label):
		_create_label()
	_label.text = text
	_recalculate_content_height()
	_reset_position()

使い方の手順

ここからは、実際にシーンに組み込む手順を見ていきましょう。

手順①: テキストファイルを用意する

まずはスタッフロール用のテキストファイルを作成します。
プロジェクト内に res://credits/ フォルダを作って、そこに staff_roll.txt を置くイメージです。

res://credits/staff_roll.txt

中身の例:

GAME TITLE
"The Great Adventure"

DIRECTOR
Alice Example

PROGRAMMER
Bob Coder
Charlie Script

ART
Daisy Painter

SPECIAL THANKS
You, the Player!

手順②: シーンにコンポーネントをアタッチする

スタッフロールを表示したいシーンを開き、
たとえばエンディング専用のシーンをこんな構成にします:

EndingScene (Control)
 ├── ColorRect          # 背景の黒フェードなど
 ├── TitleLabel         # "THE END" など
 └── CreditRoll (Control)  # ★このノードに CreditRoll.gd をアタッチ

CreditRoll ノードを選択し、インスペクターから以下を設定します:

  • credit_file: res://credits/staff_roll.txt
  • scroll_speed: 80〜120 くらい(好みで調整)
  • bottom_margin: 40〜80
  • top_margin: 40〜120
  • font_size: 24〜32
  • auto_start: チェックON(シーン開始と同時に流したい場合)

手順③: プレイヤーやタイトル画面にも「ポン付け」する

このコンポーネントはどこにでも付けられるので、
例えばタイトル画面の隅っこで常にスタッフロールを流す、ということも簡単です。

TitleScreen (Control)
 ├── Background (TextureRect)
 ├── Menu (VBoxContainer)
 └── CreditRoll (Control)  # 右下あたりに小さく配置して常時スクロール

この場合は、CreditRollanchor を右下寄せにして、
scroll_speed を小さめに(例: 20〜40)設定すると、「じわ〜っとクレジットが流れている」感じになります。

手順④: スクロール終了時にシーン遷移する

エンディング用途では、「スクロールが終わったらタイトルに戻る」みたいな処理をしたくなりますよね。
そのために roll_finished シグナルを用意してあります。

EndingScene の構成例:

EndingScene (Control)
 ├── ColorRect
 ├── TitleLabel
 └── CreditRoll (Control)
      └── CreditRoll.gd (Script)

EndingScene のスクリプト側でシグナルを拾ってシーン遷移しましょう。


extends Control

@onready var credit_roll: CreditRoll = $CreditRoll

func _ready() -> void:
	credit_roll.roll_finished.connect(_on_roll_finished)

func _on_roll_finished() -> void:
	# スクロールが終わったらタイトル画面へ戻る例
	get_tree().change_scene_to_file("res://scenes/TitleScreen.tscn")

メリットと応用

この「CreditRoll」コンポーネントを使うと、継承ベースで専用シーンを作るよりも、圧倒的に管理が楽になります。

  • テキストは .txt ファイルに集約されるので、ローカライズや修正がしやすい
  • どのシーンにも CreditRoll ノードをポン付けするだけで、同じ挙動を再利用できる
  • ノード階層は「表示側」と「ロジック側」が分離されているので、シーン構造がスッキリする
  • スクロール速度・フォントサイズ・マージンなどをインスペクターから変えるだけで、演出バリエーションを簡単に作れる

たとえば:

  • 敵キャラ図鑑の解説文を下から上に流す
  • ステージ開始前に「注意事項」をスクロール表示する
  • アーケード風に「ランキング」を延々と流し続ける

といった用途にも、そのまま流用できます。
コンポーネントとして分離しておくことで、「スタッフロール専用」から「スクロールテキスト汎用コンポーネント」へと進化させやすいのが良いところですね。

簡単な改造案:途中でフェードアウトする

例えば「最後までスクロールしきる前に、フェードアウトで締めたい」という場合、
roll_finished を待たずに、任意のタイミングでフェードアウトを始める関数を追加できます。


## ラベルを徐々にフェードアウトさせる簡易アニメーション
func fade_out(duration: float = 1.0) -> void:
	if not is_instance_valid(_label):
		return
	var tween := create_tween()
	tween.tween_property(_label, "modulate:a", 0.0, duration)
	tween.finished.connect(func():
		_is_scrolling = false
		emit_signal("roll_finished"))

これを CreditRoll に足しておけば、外側から


$CreditRoll.fade_out(2.0)

のように呼び出すだけで、2秒かけてフェードアウトしながらスタッフロールを締めることができます。
このように、コンポーネントとして切り出しておくと、「演出をちょっと足す」改造も安全かつ局所的に行えるのが嬉しいところですね。