godot-camera-systems

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Camera Systems

相机系统

Expert guidance for creating smooth, responsive cameras in 2D and 3D games.
为2D和3D游戏创建流畅、响应式相机的专业指南。

NEVER Do

绝对不要做的事

  • NEVER use
    global_position = target.global_position
    every frame
    — Instant position matching causes jittery movement. Use
    lerp()
    or
    position_smoothing_enabled = true
    .
  • NEVER forget
    limit_smoothed = true
    for Camera2D
    — Hard limits cause sudden stops at edges. Smoothing prevents jarring halts.
  • NEVER use offset for permanent camera positioning
    offset
    is for shake/sway effects only. Use
    position
    for permanent camera placement.
  • NEVER enable multiple Camera2D nodes simultaneously — Only one camera can be active. Others must have
    enabled = false
    .
  • NEVER use SpringArm3D without collision mask — SpringArm clips through walls if
    collision_mask
    is empty. Set to world layer

  • 绝对不要每帧都使用
    global_position = target.global_position
    ——即时位置匹配会导致抖动。请使用
    lerp()
    或设置
    position_smoothing_enabled = true
  • 绝对不要忘记为Camera2D设置
    limit_smoothed = true
    ——硬边界限制会导致相机在边缘突然停止。平滑处理可避免这种突兀的停顿。
  • 绝对不要用offset做永久相机定位——
    offset
    仅用于震动/摇摆效果。永久定位请使用
    position
    属性。
  • 绝对不要同时启用多个Camera2D节点——同一时间只能有一个相机处于激活状态,其他相机必须设置
    enabled = false
  • 绝对不要在没有碰撞掩码的情况下使用SpringArm3D——如果
    collision_mask
    为空,SpringArm会穿墙。请将其设置为对应世界层。

Available Scripts

可用脚本

MANDATORY: Read before implementing camera behaviors.
强制要求:在实现相机行为前请先阅读。

camera_follow_2d.gd

camera_follow_2d.gd

Smooth camera following with look-ahead prediction, deadzones, and boundary limits.
具备前瞻预测、死区和边界限制功能的平滑相机跟随脚本。

camera_shake_trauma.gd

camera_shake_trauma.gd

Trauma-based camera shake using Perlin noise - industry-standard screenshake implementation. Uses FastNoiseLite for smooth camera shake (squared trauma for feel) with automatic decay.
基于创伤系统的相机震动脚本,使用Perlin噪声——行业标准的屏幕震动实现方案。通过FastNoiseLite实现流畅的相机震动(采用平方创伤值提升手感),并支持自动衰减。

phantom_decoupling.gd

phantom_decoupling.gd

Phantom camera pattern: separates "where we look" from "what we follow". Camera follows phantom node, enabling cinematic offsets and area bounds.

虚拟相机模式:将“观察位置”与“跟随目标”分离。相机跟随虚拟节点,支持实现过场动画偏移和区域边界限制。

Camera2D Basics

Camera2D基础

gdscript
extends Camera2D

@export var target: Node2D
@export var follow_speed := 5.0

func _process(delta: float) -> void:
    if target:
        global_position = global_position.lerp(
            target.global_position,
            follow_speed * delta
        )
gdscript
extends Camera2D

@export var target: Node2D
@export var follow_speed := 5.0

func _process(delta: float) -> void:
    if target:
        global_position = global_position.lerp(
            target.global_position,
            follow_speed * delta
        )

Position Smoothing

位置平滑

gdscript
extends Camera2D

func _ready() -> void:
    # Built-in smoothing
    position_smoothing_enabled = true
    position_smoothing_speed = 5.0
gdscript
extends Camera2D

func _ready() -> void:
    # 内置平滑功能
    position_smoothing_enabled = true
    position_smoothing_speed = 5.0

Camera Limits

相机边界限制

gdscript
extends Camera2D

func _ready() -> void:
    # Constrain camera to level bounds
    limit_left = 0
    limit_top = 0
    limit_right = 1920
    limit_bottom = 1080
    
    # Smooth against limits
    limit_smoothed = true
gdscript
extends Camera2D

func _ready() -> void:
    # 将相机限制在关卡范围内
    limit_left = 0
    limit_top = 0
    limit_right = 1920
    limit_bottom = 1080
    
    # 平滑触达边界
    limit_smoothed = true

Camera Shake

相机震动

gdscript
extends Camera2D

var shake_amount := 0.0
var shake_decay := 5.0

func _process(delta: float) -> void:
    if shake_amount > 0:
        shake_amount = max(shake_amount - shake_decay * delta, 0)
        offset = Vector2(
            randf_range(-shake_amount, shake_amount),
            randf_range(-shake_amount, shake_amount)
        )
    else:
        offset = Vector2.ZERO

func shake(intensity: float) -> void:
    shake_amount = intensity
gdscript
extends Camera2D

var shake_amount := 0.0
var shake_decay := 5.0

func _process(delta: float) -> void:
    if shake_amount > 0:
        shake_amount = max(shake_amount - shake_decay * delta, 0)
        offset = Vector2(
            randf_range(-shake_amount, shake_amount),
            randf_range(-shake_amount, shake_amount)
        )
    else:
        offset = Vector2.ZERO

func shake(intensity: float) -> void:
    shake_amount = intensity

Usage:

使用示例:

$Camera2D.shake(10.0) # Screen shake on explosion
undefined
$Camera2D.shake(10.0) # 爆炸时触发屏幕震动
undefined

Zoom Controls

缩放控制

gdscript
extends Camera2D

@export var zoom_speed := 0.1
@export var min_zoom := 0.5
@export var max_zoom := 2.0

func _unhandled_input(event: InputEvent) -> void:
    if event is InputEventMouseButton:
        if event.button_index == MOUSE_BUTTON_WHEEL_UP:
            zoom_in()
        elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
            zoom_out()

func zoom_in() -> void:
    zoom = zoom.move_toward(
        Vector2.ONE * max_zoom,
        zoom_speed
    )

func zoom_out() -> void:
    zoom = zoom.move_toward(
        Vector2.ONE * min_zoom,
        zoom_speed
    )
gdscript
extends Camera2D

@export var zoom_speed := 0.1
@export var min_zoom := 0.5
@export var max_zoom := 2.0

func _unhandled_input(event: InputEvent) -> void:
    if event is InputEventMouseButton:
        if event.button_index == MOUSE_BUTTON_WHEEL_UP:
            zoom_in()
        elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
            zoom_out()

func zoom_in() -> void:
    zoom = zoom.move_toward(
        Vector2.ONE * max_zoom,
        zoom_speed
    )

func zoom_out() -> void:
    zoom = zoom.move_toward(
        Vector2.ONE * min_zoom,
        zoom_speed
    )

Look-Ahead Camera

前瞻相机

gdscript
extends Camera2D

@export var look_ahead_distance := 50.0
@export var target: CharacterBody2D

func _process(delta: float) -> void:
    if target:
        var look_ahead := target.velocity.normalized() * look_ahead_distance
        global_position = target.global_position + look_ahead
gdscript
extends Camera2D

@export var look_ahead_distance := 50.0
@export var target: CharacterBody2D

func _process(delta: float) -> void:
    if target:
        var look_ahead := target.velocity.normalized() * look_ahead_distance
        global_position = target.global_position + look_ahead

Split-Screen (Multiple Cameras)

分屏(多相机)

gdscript
undefined
gdscript
undefined

Player 1 Camera

玩家1相机

@onready var cam1: Camera2D = $Player1/Camera2D
@onready var cam1: Camera2D = $Player1/Camera2D

Player 2 Camera

玩家2相机

@onready var cam2: Camera2D = $Player2/Camera2D
func _ready() -> void: # Split viewport cam1.anchor_mode = Camera2D.ANCHOR_MODE_DRAG_CENTER cam2.anchor_mode = Camera2D.ANCHOR_MODE_DRAG_CENTER
undefined
@onready var cam2: Camera2D = $Player2/Camera2D
func _ready() -> void: # 分割视口 cam1.anchor_mode = Camera2D.ANCHOR_MODE_DRAG_CENTER cam2.anchor_mode = Camera2D.ANCHOR_MODE_DRAG_CENTER
undefined

Camera3D Patterns

Camera3D模式

Third-Person Camera

第三人称相机

gdscript
extends Camera3D

@export var target: Node3D
@export var distance := 5.0
@export var height := 2.0
@export var rotation_speed := 3.0

var rotation_angle := 0.0

func _process(delta: float) -> void:
    if not target:
        return
    
    # Rotate around target
    rotation_angle += Input.get_axis("camera_left", "camera_right") * rotation_speed * delta
    
    # Calculate position
    var offset := Vector3(
        sin(rotation_angle) * distance,
        height,
        cos(rotation_angle) * distance
    )
    
    global_position = target.global_position + offset
    look_at(target.global_position, Vector3.UP)
gdscript
extends Camera3D

@export var target: Node3D
@export var distance := 5.0
@export var height := 2.0
@export var rotation_speed := 3.0

var rotation_angle := 0.0

func _process(delta: float) -> void:
    if not target:
        return
    
    # 围绕目标旋转
    rotation_angle += Input.get_axis("camera_left", "camera_right") * rotation_speed * delta
    
    # 计算位置
    var offset := Vector3(
        sin(rotation_angle) * distance,
        height,
        cos(rotation_angle) * distance
    )
    
    global_position = target.global_position + offset
    look_at(target.global_position, Vector3.UP)

First-Person Camera

第一人称相机

gdscript
extends Camera3D

@export var mouse_sensitivity := 0.002
@export var max_pitch := deg_to_rad(80)

var pitch := 0.0

func _ready() -> void:
    Input.mouse_mode = Input.MOUSE_MODE_CAPTURED

func _input(event: InputEvent) -> void:
    if event is InputEventMouseMotion:
        # Yaw (horizontal)
        get_parent().rotate_y(-event.relative.x * mouse_sensitivity)
        
        # Pitch (vertical)
        pitch -= event.relative.y * mouse_sensitivity
        pitch = clamp(pitch, -max_pitch, max_pitch)
        rotation.x = pitch
gdscript
extends Camera3D

@export var mouse_sensitivity := 0.002
@export var max_pitch := deg_to_rad(80)

var pitch := 0.0

func _ready() -> void:
    Input.mouse_mode = Input.MOUSE_MODE_CAPTURED

func _input(event: InputEvent) -> void:
    if event is InputEventMouseMotion:
        # 偏航(水平旋转)
        get_parent().rotate_y(-event.relative.x * mouse_sensitivity)
        
        # 俯仰(垂直旋转)
        pitch -= event.relative.y * mouse_sensitivity
        pitch = clamp(pitch, -max_pitch, max_pitch)
        rotation.x = pitch

Camera Transitions

相机过渡

gdscript
undefined
gdscript
undefined

Smooth camera position change

平滑移动相机到目标位置

func move_to_position(target_pos: Vector2, duration: float = 1.0) -> void: var tween := create_tween() tween.tween_property(self, "global_position", target_pos, duration) tween.set_ease(Tween.EASE_IN_OUT) tween.set_trans(Tween.TRANS_CUBIC)
undefined
func move_to_position(target_pos: Vector2, duration: float = 1.0) -> void: var tween := create_tween() tween.tween_property(self, "global_position", target_pos, duration) tween.set_ease(Tween.EASE_IN_OUT) tween.set_trans(Tween.TRANS_CUBIC)
undefined

Cinematic Cameras

过场动画相机

gdscript
undefined
gdscript
undefined

Camera path following

相机路径跟随

extends Path2D
@onready var path_follow: PathFollow2D = $PathFollow2D @onready var camera: Camera2D = $PathFollow2D/Camera2D
func play_cutscene(duration: float) -> void: var tween := create_tween() tween.tween_property(path_follow, "progress_ratio", 1.0, duration) await tween.finished
undefined
extends Path2D
@onready var path_follow: PathFollow2D = $PathFollow2D @onready var camera: Camera2D = $PathFollow2D/Camera2D
func play_cutscene(duration: float) -> void: var tween := create_tween() tween.tween_property(path_follow, "progress_ratio", 1.0, duration) await tween.finished
undefined

Best Practices

最佳实践

1. One Active Camera

1. 单一激活相机

gdscript
undefined
gdscript
undefined

Only one Camera2D should be enabled at a time

同一时间只能有一个Camera2D处于启用状态

Others should have enabled = false

其他相机需设置enabled = false

undefined
undefined

2. Parent Camera to Player

2. 将相机作为玩家的子节点

gdscript
undefined
gdscript
undefined

Scene structure:

场景结构:

Player (CharacterBody2D)

Player (CharacterBody2D)

└─ Camera2D

└─ Camera2D

undefined
undefined

3. Use Anchors for UI

3. 为UI使用锚点

gdscript
undefined
gdscript
undefined

Camera doesn't affect UI positioned with anchors

使用锚点定位的UI不会受相机影响

UI stays in screen space

UI将保持在屏幕空间内

undefined
undefined

4. Deadzone for Platformers

4. 平台游戏的死区设置

gdscript
extends Camera2D

func _ready() -> void:
    drag_horizontal_enabled = true
    drag_vertical_enabled = true
    drag_left_margin = 0.3
    drag_right_margin = 0.3
gdscript
extends Camera2D

func _ready() -> void:
    drag_horizontal_enabled = true
    drag_vertical_enabled = true
    drag_left_margin = 0.3
    drag_right_margin = 0.3

Reference

参考资料

Related

相关内容

  • Master Skill: godot-master
  • 核心技能:godot-master