2D/3D問わず、アクションゲームで「カメラの気持ちよさ」はかなり重要ですよね。Godot標準の Camera2D には drag や limit など便利な機能がありますが、
- プレイヤーの「進行方向」に少しだけカメラを寄せたい
- でもシーンごとに独自スクリプトを書きたくない
- プレイヤーのスクリプトにカメラ制御ロジックを混ぜたくない
……というところで、ちょっと面倒になりがちです。
よくある実装としては、
- プレイヤーを継承したクラスにカメラロジックを書き足す
- カメラ専用の巨大スクリプトで「プレイヤーの速度」や「向き」を直接参照する
といったパターンがありますが、どちらも継承や依存関係がベッタリになりがちで、あとから「別の敵にも同じカメラ演出を使いたい」となったときに辛くなります。
そこで今回は、「進行方向にカメラを少し先読みする」機能を、完全に独立したコンポーネントとして切り出してみましょう。
カメラ本体はそのまま、LookAhead コンポーネントをペタッとアタッチするだけで、どんなキャラクターにも「先読みカメラ」を簡単に追加できるようにします。
【Godot 4】進行方向を先読みして気持ちよく追従!「LookAhead」コンポーネント
今回作る LookAhead は、
- 任意のターゲット(プレイヤーなど)の速度ベクトルを入力として受け取り
- その進行方向に応じてカメラのオフセットを自動調整
- スムージング付きで、急激にガクッと動かない
Camera2Dのoffsetを書き換えるだけのシンプル設計
という、「継承より合成」なカメラ補助コンポーネントです。
フルコード:LookAhead.gd
extends Node
class_name LookAhead
## プレイヤーなどの「進行方向(velocity)」に応じて
## Camera2D の offset を先読みさせるコンポーネント。
##
## 想定:
## - このノードは Camera2D の子、もしくは孫ノードとして配置
## - 外部から velocity を渡すか、ターゲットノードから自動取得する
@export_group("基本設定")
## 先読み対象のノード(例: Player の CharacterBody2D)
## - null の場合は velocity を手動で set_velocity() で渡す運用も可能
@export var target: NodePath
## target から velocity を自動取得するかどうか
## - true: target が CharacterBody2D / RigidBody2D などで velocity を持つ前提
## - false: 外部から set_velocity() を呼び出して更新する
@export var auto_read_velocity: bool = true
## カメラの基準となる Camera2D
## - 空の場合は、自分の親階層から最初に見つかった Camera2D を自動検出
@export var camera_node: NodePath
@export_group("先読みパラメータ")
## 速度ベクトルをどれくらい「先読み距離」に変換するかのスケール
## 例: 先読み量 = velocity * look_ahead_factor
@export var look_ahead_factor: float = 0.15
## 先読み量の最大値(画面座標上のピクセル単位)
## 例: 64 にすると、左右/上下ともに最大 64px までしかオフセットしない
@export var max_look_ahead: float = 96.0
## X 方向の先読みを有効にするか
@export var enable_horizontal: bool = true
## Y 方向の先読みを有効にするか
@export var enable_vertical: bool = false
@export_group("スムージング")
## 先読みオフセットの追従スピード(大きいほど素早く追従)
@export_range(0.0, 20.0, 0.1, "or_greater")
var smoothing_speed: float = 8.0
## 非アクティブ時は offset をゼロに戻すか
@export var reset_when_inactive: bool = true
@export_group("デバッグ")
## デバッグ用に先読みオフセットを表示するか
@export var debug_print: bool = false
## 外部から手動で渡される velocity(auto_read_velocity=false のときに使用)
var _external_velocity: Vector2 = Vector2.ZERO
## 実際に Camera2D に適用している現在のオフセット
var _current_offset: Vector2 = Vector2.ZERO
## 内部で参照するターゲットノード
var _target_node: Node = null
## 内部で参照する Camera2D
var _camera: Camera2D = null
func _ready() -> void:
_resolve_camera()
_resolve_target()
func _process(delta: float) -> void:
if not is_instance_valid(_camera):
# カメラが見つからなければ何もしない
return
var velocity := Vector2.ZERO
if auto_read_velocity:
velocity = _read_velocity_from_target()
else:
velocity = _external_velocity
# 先読みオフセットの目標値を計算
var target_offset := _compute_target_offset(velocity)
# スムージングしながら現在のオフセットを更新
_current_offset = _current_offset.lerp(target_offset, clampf(smoothing_speed * delta, 0.0, 1.0))
# カメラに適用
_camera.offset = _current_offset
if debug_print:
print("LookAhead offset: ", _current_offset, " velocity: ", velocity)
## 外部から velocity を手動で渡したい場合に使用
## 例: プレイヤースクリプト内の _physics_process で set_velocity(velocity)
func set_velocity(v: Vector2) -> void:
_external_velocity = v
## 一時的に先読みを無効化したい場合に使用
## - active = false のとき、reset_when_inactive が true なら offset をゼロに戻す
func set_active(active: bool) -> void:
set_process(active)
if not active and reset_when_inactive:
_current_offset = Vector2.ZERO
if is_instance_valid(_camera):
_camera.offset = Vector2.ZERO
func _resolve_camera() -> void:
if camera_node != NodePath():
var node := get_node_or_null(camera_node)
if node is Camera2D:
_camera = node
else:
push_warning("LookAhead: camera_node は Camera2D ではありません: %s" % [str(node)])
_camera = null
return
# camera_node が指定されていない場合、親階層から Camera2D を探す
var parent := get_parent()
while parent:
if parent is Camera2D:
_camera = parent
return
parent = parent.get_parent()
push_warning("LookAhead: 親階層に Camera2D が見つかりません。camera_node を指定してください。")
_camera = null
func _resolve_target() -> void:
if target == NodePath():
_target_node = null
return
_target_node = get_node_or_null(target)
if _target_node == null:
push_warning("LookAhead: target が見つかりません: %s" % [str(target)])
## target から velocity を取り出す
## - CharacterBody2D: .velocity
## - RigidBody2D: .linear_velocity
## - それ以外: Vector2.ZERO
func _read_velocity_from_target() -> Vector2:
if not is_instance_valid(_target_node):
return Vector2.ZERO
# CharacterBody2D / CharacterBody3D など
if "velocity" in _target_node:
var v = _target_node.velocity
if v is Vector2:
return v
# 3D の場合は XZ を 2D にマッピングするなども可能だが、ここでは 2D 前提
return Vector2(v.x, v.z)
# RigidBody2D / RigidBody3D など
if "linear_velocity" in _target_node:
var lv = _target_node.linear_velocity
if lv is Vector2:
return lv
return Vector2(lv.x, lv.z)
return Vector2.ZERO
## velocity から先読みオフセットの目標値を計算
func _compute_target_offset(velocity: Vector2) -> Vector2:
# 速度ベクトルをスケーリング
var offset := velocity * look_ahead_factor
# X / Y ごとに有効・無効を切り替え
if not enable_horizontal:
offset.x = 0.0
if not enable_vertical:
offset.y = 0.0
# 最大量を制限(円形にクリップ)
if offset.length() > max_look_ahead:
offset = offset.normalized() * max_look_ahead
return offset
使い方の手順
ここでは 2D アクションゲームのプレイヤーを例に、LookAhead を使った先読みカメラを組み込んでみます。
シーン構成例
Player (CharacterBody2D)
├── Sprite2D
├── CollisionShape2D
└── Camera2D
└── LookAhead (Node)
※ LookAhead.gd は res://components/camera/LookAhead.gd など、どこか共通ディレクトリに置いておくと使い回しやすいです。
手順①:スクリプトをプロジェクトに追加
res://components/camera/LookAhead.gdなど好きな場所に、新しい GDScript ファイルを作成。- 上記のフルコードをコピペして保存。
- Godot が
class_name LookAheadを認識するので、シーンツリーから + ノード追加 でLookAheadを検索できるようになります。
手順②:Camera2D に LookAhead コンポーネントをアタッチ
Playerシーンを開く。Playerの子としてCamera2Dを追加(まだなら)。Camera2Dの子としてLookAheadノードを追加。
このとき、親階層の Camera2D を自動検出するので、camera_node の設定は空のままでOKです。
手順③:インスペクタでターゲットを設定
LookAhead ノードを選択し、インスペクタで以下を設定します。
- target:
Player (CharacterBody2D)を指定 - auto_read_velocity:
オン(デフォルト)
→ Player のvelocityプロパティを自動で読み取ります。 - look_ahead_factor: 0.1 ~ 0.2 くらいから調整してみる
- max_look_ahead: 64 ~ 128 くらいが扱いやすいです
- enable_horizontal:
オン - enable_vertical: 横スクロールなら
オフ、全方位ならオン
これだけで、プレイヤーが右に走ればカメラは少し右側に、左に走れば少し左側に寄る「先読みカメラ」が完成します。
手順④:手動で velocity を渡すパターン(応用)
もしターゲット側が velocity プロパティを持っていない場合や、独自の移動ロジックで制御している場合は、外部から手動で velocity を渡す運用もできます。
例:KinematicPlayer という独自ノードにアタッチしたスクリプト側から渡す場合:
# Player.gd (例)
extends CharacterBody2D
@onready var _look_ahead: LookAhead = $Camera2D/LookAhead
func _physics_process(delta: float) -> void:
var input_dir := Input.get_axis("move_left", "move_right")
velocity.x = input_dir * 300.0
# 物理移動
move_and_slide()
# LookAhead に現在の velocity を通知
_look_ahead.set_velocity(velocity)
この場合、LookAhead 側では
- target: 空のまま
- auto_read_velocity:
オフ
に設定しておきましょう。
メリットと応用
LookAhead コンポーネントを使うメリットは、単に「先読みカメラができる」以上に、シーン構造と責務の分離がキレイになる点にあります。
- プレイヤーの移動ロジックと、カメラの演出ロジックを完全に分離できる
- プレイヤーを継承して「PlayerWithCamera」みたいなクラスを増やさなくてよい
- 敵キャラや動く足場にも、同じカメラ演出を簡単に適用できる
- 「シーンごとにカメラの挙動を変えたい」ときも、コンポーネントのパラメータを変えるだけで済む
例えば、ボス戦だけカメラを少しだけ上下にも先読みしたい場合、ボス専用シーンで enable_vertical をオンにするだけで、プレイヤー側のスクリプトは一切触らずに演出を変えられます。
応用例:ダッシュ中だけ先読みを強くする
最後に、ちょっとした改造案として「プレイヤーがダッシュ中だけ先読み量を増やす」例を紹介します。LookAhead コンポーネント自体を直接いじってもいいですが、よりコンポーネント指向にするなら、外側からパラメータを操作する小さなスクリプトを追加するのがオススメです。
# DashLookAheadBooster.gd
extends Node
## ダッシュ中だけ LookAhead の先読み量を増やす補助コンポーネント
@export var look_ahead: LookAhead
@export var dash_flag_path: NodePath
@export var normal_factor: float = 0.15
@export var dash_factor: float = 0.30
var _dash_owner: Node = null
func _ready() -> void:
if dash_flag_path != NodePath():
_dash_owner = get_node_or_null(dash_flag_path)
if look_ahead == null:
push_warning("DashLookAheadBooster: look_ahead が設定されていません。")
func _process(delta: float) -> void:
if look_ahead == null or _dash_owner == null:
return
# 例: _dash_owner に "is_dashing" という bool プロパティがある前提
var is_dashing := false
if "is_dashing" in _dash_owner:
is_dashing = _dash_owner.is_dashing
look_ahead.look_ahead_factor = dash_factor if is_dashing else normal_factor
このように、小さなコンポーネントを組み合わせていくことで、
- プレイヤーのコードは「ダッシュの状態管理」だけに集中
LookAheadは「先読みカメラ」だけに集中DashLookAheadBoosterは「ダッシュとカメラ演出の橋渡し」だけに集中
という、責務がきれいに分離された構成が作れます。
深いノード階層や巨大な継承ツリーに頼らず、「必要な振る舞いを小さなコンポーネントとして後付けしていく」スタイルで、気持ちいいカメラ演出をどんどん積み上げていきましょう。
