mcpField
C
camera-system

camera-system

github.com/jame581开发889177 次安装5 天前更新
代码仓库

关于 camera-system

面向 Godot 4.3+ 开发者的 2D 与 3D 摄像机系统技能指南,提供 GDScript 和 C# 示例。内容涵盖平滑跟随、震动、镜头切换和分屏等模式。

主要功能

  1. 01Camera2D 平滑、边界与缩放
  2. 02平滑跟随和前视模式
  3. 03基于创伤值的屏幕震动
  4. 04房间区域与摄像机过渡
  5. 05第三人称、环绕及第一人称设置
  6. 06SubViewport 分屏布局

适用场景

  1. 01为 TileMap 或关卡设置镜头边界
  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+ 中的摄像机系统

所有示例均适用于 Godot 4.3+,不使用已弃用的 API。先展示 GDScript,再展示 C#。

相关技能: player-controller 用于第一人称摄像机设置,state-machine 用于摄像机状态转换,godot-optimization 用于摄像机剔除和性能优化,physics-system 用于物理插值和摄像机平滑,2d-essentials 用于画布图层、视差滚动和坐标转换,math-essentials 用于 smoothstep 和基于 lerp 的插值,tween-animation 用于摄像机抖动和电影式转场,phantom-camera 用于 Cinemachine 风格的摄像机插件。


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 的系统比简单的正弦波能产生更自然的抖动效果。trauma 越高,抖动越剧烈;trauma 会随时间衰减;偏移量与 trauma^2 成比例,因此较小的 trauma 值带来的抖动较为轻微。

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 补间到房间边界。玩家穿过房间边界时会平滑过渡。

完整的 GDScript + C# CameraZone 实现,请参阅 references/camera-zones.md。


5. Camera3D 模式

三种经典的 3D 摄像机设置:使用 SpringArm3D 的第三人称跟随(可处理墙壁碰撞)、通过鼠标拖动旋转的轨道摄像机、通过鼠标视角控制的第一人称摄像机。

各种模式的完整 GDScript 实现,请参阅 references/camera3d-patterns.md。


6. 摄像机转场

通过 Tween + await(C# 中使用 ToSignal)实现异步摄像机转场。模式如下:将当前摄像机的位置和缩放(2D)或 FOV(3D)补间到下一台摄像机的对应值,然后在下一台摄像机上调用 make_current()。适用于 2D 和 3D。

完整的 CameraTransitionManager 实现(2D 和 3D),请参阅 references/transitions.md。


7. 分屏(本地多人游戏)

将多台摄像机渲染到不同的 SubViewport,然后通过布局容器(HBoxContainer、VBoxContainer 或 GridContainer)排列 SubViewportContainer。将每位玩家的摄像机设为其视口的 current。

SubViewport 场景树设置和双人分屏示例,请参阅 references/split-screen.md。


8. 实现清单

  • Camera2D 限制与关卡/瓦片地图边界匹配,确保不会看到世界范围外的边缘
  • 平滑跟随使用 _process(视觉插值),而非 _physics_process
  • 当 _trauma 达到 0.0 时,屏幕震动会将 offset 和 rotation 重置为零
  • add_trauma() 会将数值限制在 1.0,不会超过最大震动幅度
  • 摄像机区域 Area2D 的碰撞层已设置为仅由玩家触发
  • SpringArm3D 碰撞遮罩包含所有环境层,以避免摄像机穿墙
  • 摄像机过渡会等待补间完成后再调用 make_current()
  • 窗口大小调整时,会更新分屏 SubViewport 的尺寸(get_tree().root.size_changed 信号)
  • 只有一个 SubViewport 将 audio_listener_enable_2d 或 audio_listener_enable_3d 设为 true

评论

支持 Markdown