mcpField
C
camera-system

camera-system

github.com/jame581開発889177 回インストール4 日前に更新
リポジトリ

camera-system について

Godot 4.3以降で2D・3Dカメラを構築する開発者向けのスキルガイドです。GDScriptとC#の例で、追従や揺れ、画面分割などのパターンを扱います。

主な機能

  1. 01Camera2Dの平滑化、制限、ズーム
  2. 02スムーズ追従と先読みパターン
  3. 03トラウマ値による画面揺れ
  4. 04ルームゾーンとカメラ遷移
  5. 05三人称、オービット、一人称の設定
  6. 06SubViewportによる画面分割

活用シーン

  1. 01タイルマップやレベルにカメラ制限を設定
  2. 02爆発にカメラ揺れを加える
  3. 03ルーム間でカメラを切り替える
  4. 04ローカルマルチプレイの画面分割を構築

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jame581/godotprompter --skill camera-system

インストール後、Claude Code で/camera-systemを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
camera-zones.md
camera3d-patterns.md
split-screen.md
transitions.md

詳細

Godot 4.3 以降のカメラシステム

すべての例は、非推奨 API を使用せずに Godot 4.3 以降を対象としています。まず GDScript を示し、次に C# を示します。

関連スキル: 一人称視点カメラの設定については player-controller、カメラの状態遷移については state-machine、カメラのカリングとパフォーマンスについては godot-optimization、物理補間とカメラのスムージングについては physics-system、キャンバスレイヤー、パララックススクロール、座標変換については 2d-essentials、smoothstep と lerp による補間については math-essentials、カメラの揺れとシネマティックな遷移については tween-animation、Cinemachine 風のカメラアドオンについては phantom-camera を参照してください。


1. Camera2D の基本

主なプロパティ
プロパティ型説明
position_smoothing_enabledbool組み込みの位置スムージング(ターゲットに向かって lerp)を有効にします
position_smoothing_speedfloat組み込みスムージングの速度(既定値は 5.0)
drag_horizontal_enabledbool水平方向のドラッグゾーンを有効にします。ターゲットがゾーンから出たときにのみカメラが移動します
drag_vertical_enabledbool垂直方向のドラッグゾーンを有効にします
limit_leftint左端のピクセル境界 — カメラはここを越えてスクロールしません
limit_rightint右端のピクセル境界
limit_topint上端のピクセル境界
limit_bottomint下端のピクセル境界
zoomVector2ズーム倍率。Vector2(2, 2) = 2 倍にズームイン、Vector2(0.5, 0.5) = ズームアウト

カメラにワールドの外側を表示させないよう、タイルマップまたはレベルの境界に合わせて制限値を設定してください。制限値の単位はタイルではなく、ワールドのピクセルです。


2. スムーズな追従

手動で追従するカメラは、組み込みのスムージングより細かく制御できます。先読み、オフセット、カスタムイージングを追加できます。

GDScript
extends Camera2D

## Target node to follow (assign in Inspector or via code)
@export var target: Node2D

## How quickly the camera catches up to the target (higher = snappier)
@export var follow_speed: float = 8.0

## How far ahead the camera leads in the movement direction
@export var look_ahead_distance: float = 80.0

## How quickly the look-ahead offset responds to direction changes
@export var look_ahead_speed: float = 4.0

var _look_ahead_offset: Vector2 = Vector2.ZERO
var _previous_target_pos: Vector2 = Vector2.ZERO

func _ready() -> void:
    # Disable built-in smoothing — we handle it manually
    position_smoothing_enabled = false
    if target:
        _previous_target_pos = target.global_position
        global_position = target.global_position

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

    # Compute movement direction from last frame
    var move_delta: Vector2 = target.global_position - _previous_target_pos
    _previous_target_pos = target.global_position

    # Smoothly steer look-ahead offset toward movement direction
    var desired_ahead: Vector2 = move_delta.normalized() * look_ahead_distance if move_delta.length() > 0.5 else Vector2.ZERO
    _look_ahead_offset = _look_ahead_offset.lerp(desired_ahead, look_ahead_speed * delta)

    # Lerp camera position toward target + look-ahead
    var desired_pos: Vector2 = target.global_position + _look_ahead_offset
    global_position = global_position.lerp(desired_pos, follow_speed * delta)
C#
using Godot;

public partial class SmoothFollowCamera : Camera2D
{
    [Export] public Node2D Target { get; set; }
    [Export] public float FollowSpeed { get; set; } = 8.0f;
    [Export] public float LookAheadDistance { get; set; } = 80.0f;
    [Export] public float LookAheadSpeed { get; set; } = 4.0f;

    private Vector2 _lookAheadOffset = Vector2.Zero;
    private Vector2 _previousTargetPos = Vector2.Zero;

    public override void _Ready()
    {
        PositionSmoothingEnabled = false;
        if (Target != null)
        {
            _previousTargetPos = Target.GlobalPosition;
            GlobalPosition = Target.GlobalPosition;
        }
    }

    public override void _Process(double delta)
    {
        if (Target == null)
            return;

        float dt = (float)delta;

        Vector2 moveDelta = Target.GlobalPosition - _previousTargetPos;
        _previousTargetPos = Target.GlobalPosition;

        Vector2 desiredAhead = moveDelta.Length() > 0.5f
            ? moveDelta.Normalized() * LookAheadDistance
            : Vector2.Zero;

        _lookAheadOffset = _lookAheadOffset.Lerp(desiredAhead, LookAheadSpeed * dt);

        Vector2 desiredPos = Target.GlobalPosition + _lookAheadOffset;
        GlobalPosition = GlobalPosition.Lerp(desiredPos, FollowSpeed * dt);
    }
}

3. 画面の揺れ

トラウマベースのシステムを使うと、単純な正弦波より自然に見える揺れを生成できます。トラウマが大きいほど激しく揺れ、時間とともに減衰します。オフセットは trauma^2 に比例するため、トラウマ値が小さい場合は控えめな揺れになります。

GDScript
class_name ScreenShakeCamera
extends Camera2D

## Maximum pixel offset during maximum trauma
@export var max_offset: Vector2 = Vector2(20.0, 15.0)

## Maximum rotation offset in degrees during maximum trauma
@export var max_roll: float = 3.0

## Rate at which trauma decays per second (0–1 range)
@export var decay_rate: float = 1.5

var _trauma: float = 0.0  # 0.0 = no shake, 1.0 = maximum shake

# Optional: use noise for smooth, organic shake
var _noise: FastNoiseLite
var _noise_time: float = 0.0
@export var use_noise: bool = true
@export var noise_speed: float = 60.0

func _ready() -> void:
    ignore_rotation = false  # Camera2D ignores its own rotation by default — the roll needs it
    _noise = FastNoiseLite.new()
    _noise.noise_type = FastNoiseLite.TYPE_SIMPLEX
    _noise.seed = randi()

## Call this from any node to trigger a shake (amount in 0–1 range; can stack)
func add_trauma(amount: float) -> void:
    _trauma = minf(_trauma + amount, 1.0)

func _process(delta: float) -> void:
    if _trauma <= 0.0:
        offset = Vector2.ZERO
        rotation = 0.0
        return

    # Decay trauma over time
    _trauma = maxf(_trauma - decay_rate * delta, 0.0)
    _noise_time += delta * noise_speed

    var shake: float = _trauma * _trauma  # squaring gives subtle feel at low trauma

    if use_noise:
        offset.x = max_offset.x * shake * _noise.get_noise_2d(_noise_time, 0.0)
        offset.y = max_offset.y * shake * _noise.get_noise_2d(0.0, _noise_time)
        rotation = deg_to_rad(max_roll) * shake * _noise.get_noise_2d(_noise_time, _noise_time)
    else:
        offset.x = max_offset.x * shake * randf_range(-1.0, 1.0)
        offset.y = max_offset.y * shake * randf_range(-1.0, 1.0)
        rotation = deg_to_rad(max_roll) * shake * randf_range(-1.0, 1.0)

別のノードから揺れを発生させる:

# Any node that can reach the camera
func on_explosion() -> void:
    var cam := get_viewport().get_camera_2d() as ScreenShakeCamera
    if cam:
        cam.add_trauma(0.6)
C#
using Godot;

public partial class ScreenShakeCamera : Camera2D
{
    [Export] public Vector2 MaxOffset { get; set; } = new Vector2(20f, 15f);
    [Export] public float MaxRoll { get; set; } = 3.0f;
    [Export] public float DecayRate { get; set; } = 1.5f;
    [Export] public bool UseNoise { get; set; } = true;
    [Export] public float NoiseSpeed { get; set; } = 60.0f;

    private float _trauma = 0f;
    private float _noiseTime = 0f;
    private FastNoiseLite _noise;

    public override void _Ready()
    {
        IgnoreRotation = false; // Camera2D ignores its own rotation by default — the roll needs it
        _noise = new FastNoiseLite();
        _noise.NoiseType = FastNoiseLite.NoiseTypeEnum.Simplex;
        _noise.Seed = (int)GD.Randi();
    }

    public void AddTrauma(float amount)
    {
        _trauma = Mathf.Min(_trauma + amount, 1.0f);
    }

    public override void _Process(double delta)
    {
        if (_trauma <= 0f)
        {
            Offset = Vector2.Zero;
            Rotation = 0f;
            return;
        }

        float dt = (float)delta;
        _trauma = Mathf.Max(_trauma - DecayRate * dt, 0f);
        _noiseTime += dt * NoiseSpeed;

        float shake = _trauma * _trauma;

        if (UseNoise)
        {
            Offset = new Vector2(
                MaxOffset.X * shake * _noise.GetNoise2D(_noiseTime, 0f),
                MaxOffset.Y * shake * _noise.GetNoise2D(0f, _noiseTime)
            );
            Rotation = Mathf.DegToRad(MaxRoll) * shake * _noise.GetNoise2D(_noiseTime, _noiseTime);
        }
        else
        {
            Offset = new Vector2(
                MaxOffset.X * shake * (float)GD.RandRange(-1.0, 1.0),
                MaxOffset.Y * shake * (float)GD.RandRange(-1.0, 1.0)
            );
            Rotation = Mathf.DegToRad(MaxRoll) * shake * (float)GD.RandRange(-1.0, 1.0);
        }
    }
}

4. カメラゾーン/部屋

部屋を単位とするゲーム(メトロイドヴァニア、見下ろし型ダンジョンなど)では、各部屋に Area2D を配置し、body_entered 時にアクティブな Camera2D の limit_left/limit_right/limit_top/limit_bottom を部屋の境界まで tween するスクリプトを使います。プレイヤーが部屋の境界を越えると、スムーズに遷移します。

CameraZone の実装全体(GDScript と C#)については、references/camera-zones.md を参照してください。


5. Camera3D のパターン

代表的な 3D カメラの設定は 3 種類です。壁との衝突に対応する SpringArm3D を使った三人称追従カメラ、マウスドラッグで回転するオービットカメラ、マウスルックを使った一人称カメラです。

各パターンの GDScript 実装全体については、references/camera3d-patterns.md を参照してください。


6. カメラ遷移

Tween と await(C# では ToSignal)を使った非同期のカメラ遷移です。現在のカメラの位置とズーム(2D)、または FOV(3D)を次のカメラの値まで tween し、その後、次のカメラで make_current() を呼び出すのが基本のパターンです。2D と 3D の両方で動作します。

CameraTransitionManager の実装全体(2D と 3D)については、references/transitions.md を参照してください。


7. 分割画面(ローカルマルチプレイヤー)

複数のカメラを個別の SubViewport にレンダリングし、SubViewportContainer をレイアウト(HBoxContainer、VBoxContainer、または GridContainer)内に配置します。各プレイヤーのカメラは、それぞれのビューポートで current に設定します。

SubViewport のシーンツリー設定と 2 人プレイの分割画面の例については、references/split-screen.md を参照してください。


8. 実装チェックリスト

  • Camera2D の制限がレベル/タイルマップの境界と一致し、ワールド外の領域が見えない
  • スムーズな追従には _physics_process ではなく _process(視覚的な補間)を使用する
  • _trauma が 0.0 になったとき、画面シェイクで offset と rotation がゼロにリセットされる
  • add_trauma() が値を 1.0 にクランプし、シェイクの最大値を超えない
  • カメラゾーンの Area2D のコリジョンレイヤーが、プレイヤーだけがトリガーするように設定されている
  • 壁を避けるため、SpringArm3D のコリジョンマスクにすべての環境レイヤーが含まれている
  • カメラ遷移でトゥイーンの完了を待ってから make_current() を呼び出す
  • ウィンドウのサイズ変更時に、分割画面の SubViewport のサイズが更新される(get_tree().root.size_changed シグナル)
  • audio_listener_enable_2d または audio_listener_enable_3d が true に設定されている SubViewport は1つだけである

コメント