2D/3D問わず、アクションゲームで「カメラの気持ちよさ」はかなり重要ですよね。Godot標準の Camera2D には draglimit など便利な機能がありますが、

  • プレイヤーの「進行方向」に少しだけカメラを寄せたい
  • でもシーンごとに独自スクリプトを書きたくない
  • プレイヤーのスクリプトにカメラ制御ロジックを混ぜたくない

……というところで、ちょっと面倒になりがちです。

よくある実装としては、

  • プレイヤーを継承したクラスにカメラロジックを書き足す
  • カメラ専用の巨大スクリプトで「プレイヤーの速度」や「向き」を直接参照する

といったパターンがありますが、どちらも継承や依存関係がベッタリになりがちで、あとから「別の敵にも同じカメラ演出を使いたい」となったときに辛くなります。

そこで今回は、「進行方向にカメラを少し先読みする」機能を、完全に独立したコンポーネントとして切り出してみましょう。
カメラ本体はそのまま、LookAhead コンポーネントをペタッとアタッチするだけで、どんなキャラクターにも「先読みカメラ」を簡単に追加できるようにします。


【Godot 4】進行方向を先読みして気持ちよく追従!「LookAhead」コンポーネント

今回作る LookAhead は、

  • 任意のターゲット(プレイヤーなど)の速度ベクトルを入力として受け取り
  • その進行方向に応じてカメラのオフセットを自動調整
  • スムージング付きで、急激にガクッと動かない
  • Camera2Doffset を書き換えるだけのシンプル設計

という、「継承より合成」なカメラ補助コンポーネントです。


フルコード: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.gdres://components/camera/LookAhead.gd など、どこか共通ディレクトリに置いておくと使い回しやすいです。

手順①:スクリプトをプロジェクトに追加

  1. res://components/camera/LookAhead.gd など好きな場所に、新しい GDScript ファイルを作成。
  2. 上記のフルコードをコピペして保存。
  3. Godot が class_name LookAhead を認識するので、シーンツリーから + ノード追加LookAhead を検索できるようになります。

手順②:Camera2D に LookAhead コンポーネントをアタッチ

  1. Player シーンを開く。
  2. Player の子として Camera2D を追加(まだなら)。
  3. 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 は「ダッシュとカメラ演出の橋渡し」だけに集中

という、責務がきれいに分離された構成が作れます。
深いノード階層や巨大な継承ツリーに頼らず、「必要な振る舞いを小さなコンポーネントとして後付けしていく」スタイルで、気持ちいいカメラ演出をどんどん積み上げていきましょう。