第二十一章:2D相机系统

第二十一章:2D相机系统

"相机是玩家观察游戏世界的窗口,好的相机控制让游戏体验更加流畅。"

相机系统控制着玩家看到的游戏画面。本章将全面讲解Godot的Camera2D,包括跟随、边界限制、平滑移动、屏幕效果等内容。


21.1 Camera2D基础

21.1.1 创建相机

extends Node2D

func _ready():
    # 代码创建相机
    var camera = Camera2D.new()
    camera.enabled = true
    camera.make_current()  # 设为当前相机
    add_child(camera)

# 相机继承关系
# 通常将Camera2D作为玩家的子节点
# Player (CharacterBody2D)
#   └── Camera2D

21.1.2 基本属性

extends Camera2D

func _ready():
    # 启用状态
    enabled = true
    
    # 偏移(相对于节点位置)
    offset = Vector2(0, -50)  # 向上偏移50像素
    
    # 锚点模式
    anchor_mode = ANCHOR_MODE_DRAG_CENTER
    # ANCHOR_MODE_FIXED_TOP_LEFT - 固定左上角
    # ANCHOR_MODE_DRAG_CENTER - 拖拽居中
    
    # 旋转跟随
    ignore_rotation = true  # 不跟随父节点旋转
    
    # 缩放
    zoom = Vector2(2, 2)  # 2倍放大

21.1.3 多相机切换

extends Node

var cameras: Array[Camera2D] = []
var current_index: int = 0

func _ready():
    cameras = get_tree().get_nodes_in_group("cameras")

func switch_camera(index: int):
    if index >= 0 and index < cameras.size():
        current_index = index
        cameras[index].make_current()

func next_camera():
    current_index = (current_index + 1) % cameras.size()
    cameras[current_index].make_current()

# 平滑切换
func transition_to_camera(target: Camera2D, duration: float = 0.5):
    var current = get_viewport().get_camera_2d()
    if current == target:
        return
    
    var start_pos = current.global_position
    var start_zoom = current.zoom
    
    target.make_current()
    
    var tween = create_tween()
    tween.tween_property(target, "global_position", target.global_position, duration)\
        .from(start_pos)
    tween.parallel().tween_property(target, "zoom", target.zoom, duration)\
        .from(start_zoom)

21.2 相机跟随

21.2.1 简单跟随

extends Camera2D

@export var target: Node2D

func _process(delta: float):
    if target:
        global_position = target.global_position

21.2.2 平滑跟随

extends Camera2D

@export var target: Node2D
@export var smoothing_speed: float = 5.0

# 方法1:使用内置平滑
func _ready():
    position_smoothing_enabled = true
    position_smoothing_speed = 5.0

# 方法2:自定义平滑
func _process(delta: float):
    if target:
        global_position = global_position.lerp(
            target.global_position,
            smoothing_speed * delta
        )

# 方法3:使用move_toward
func _process(delta: float):
    if target:
        var target_pos = target.global_position
        global_position = global_position.move_toward(
            target_pos,
            smoothing_speed * 100 * delta
        )

21.2.3 带前瞻的跟随

extends Camera2D

@export var target: CharacterBody2D
@export var look_ahead_factor: float = 0.3
@export var look_ahead_speed: float = 3.0

var look_ahead_offset: Vector2 = Vector2.ZERO

func _process(delta: float):
    if not target:
        return
    
    # 计算前瞻偏移
    var target_offset = target.velocity * look_ahead_factor
    look_ahead_offset = look_ahead_offset.lerp(target_offset, look_ahead_speed * delta)
    
    # 应用位置
    var target_pos = target.global_position + look_ahead_offset
    global_position = global_position.lerp(target_pos, 5.0 * delta)

21.2.4 死区跟随

extends Camera2D

@export var target: Node2D
@export var dead_zone: Vector2 = Vector2(100, 50)
@export var follow_speed: float = 5.0

func _process(delta: float):
    if not target:
        return
    
    var target_pos = target.global_position
    var diff = target_pos - global_position
    
    # 只有超出死区才移动
    var move_x = 0.0
    var move_y = 0.0
    
    if abs(diff.x) > dead_zone.x:
        move_x = diff.x - sign(diff.x) * dead_zone.x
    
    if abs(diff.y) > dead_zone.y:
        move_y = diff.y - sign(diff.y) * dead_zone.y
    
    var move = Vector2(move_x, move_y)
    global_position = global_position.lerp(global_position + move, follow_speed * delta)

21.3 边界限制

21.3.1 使用内置边界

extends Camera2D

func _ready():
    # 启用边界限制
    limit_left = 0
    limit_top = 0
    limit_right = 1920
    limit_bottom = 1080
    
    # 边界平滑
    limit_smoothed = true

# 从TileMap获取边界
func set_limits_from_tilemap(tilemap: TileMap):
    var rect = tilemap.get_used_rect()
    var tile_size = tilemap.tile_set.tile_size
    
    limit_left = rect.position.x * tile_size.x
    limit_top = rect.position.y * tile_size.y
    limit_right = rect.end.x * tile_size.x
    limit_bottom = rect.end.y * tile_size.y

21.3.2 动态边界

extends Camera2D

var current_room: Rect2

func set_room(room_rect: Rect2):
    current_room = room_rect
    
    limit_left = int(room_rect.position.x)
    limit_top = int(room_rect.position.y)
    limit_right = int(room_rect.end.x)
    limit_bottom = int(room_rect.end.y)

# 平滑过渡到新房间
func transition_to_room(new_room: Rect2, duration: float = 0.5):
    var tween = create_tween()
    
    tween.tween_property(self, "limit_left", int(new_room.position.x), duration)
    tween.parallel().tween_property(self, "limit_top", int(new_room.position.y), duration)
    tween.parallel().tween_property(self, "limit_right", int(new_room.end.x), duration)
    tween.parallel().tween_property(self, "limit_bottom", int(new_room.end.y), duration)
    
    current_room = new_room

21.4 缩放控制

21.4.1 基本缩放

extends Camera2D

@export var min_zoom: float = 0.5
@export var max_zoom: float = 3.0
@export var zoom_speed: float = 0.1

func _unhandled_input(event: InputEvent):
    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():
    zoom = (zoom + Vector2.ONE * zoom_speed).clamp(
        Vector2.ONE * min_zoom,
        Vector2.ONE * max_zoom
    )

func zoom_out():
    zoom = (zoom - Vector2.ONE * zoom_speed).clamp(
        Vector2.ONE * min_zoom,
        Vector2.ONE * max_zoom
    )

21.4.2 平滑缩放

extends Camera2D

var target_zoom: Vector2 = Vector2.ONE

func _process(delta: float):
    zoom = zoom.lerp(target_zoom, 10.0 * delta)

func set_zoom_level(level: float, smooth: bool = true):
    if smooth:
        target_zoom = Vector2.ONE * level
    else:
        zoom = Vector2.ONE * level
        target_zoom = zoom

# 缩放到特定点
func zoom_to_point(point: Vector2, zoom_level: float, duration: float = 0.5):
    var tween = create_tween()
    tween.tween_property(self, "global_position", point, duration)
    tween.parallel().tween_property(self, "zoom", Vector2.ONE * zoom_level, duration)

21.5 相机特效

21.5.1 屏幕震动

extends Camera2D

var shake_intensity: float = 0.0
var shake_duration: float = 0.0
var shake_frequency: float = 30.0
var shake_timer: float = 0.0

func _process(delta: float):
    if shake_duration > 0:
        shake_duration -= delta
        shake_timer += delta
        
        # 使用噪声或随机产生抖动
        var shake_offset = Vector2(
            randf_range(-1, 1) * shake_intensity,
            randf_range(-1, 1) * shake_intensity
        )
        offset = shake_offset
        
        # 衰减
        shake_intensity = lerp(shake_intensity, 0.0, delta * 5)
    else:
        offset = Vector2.ZERO

func shake(intensity: float = 10.0, duration: float = 0.3):
    shake_intensity = intensity
    shake_duration = duration

# 使用噪声的高质量震动
var noise: FastNoiseLite

func _ready():
    noise = FastNoiseLite.new()
    noise.noise_type = FastNoiseLite.TYPE_SIMPLEX
    noise.frequency = 0.5

func shake_noise(intensity: float = 10.0, duration: float = 0.3):
    shake_intensity = intensity
    shake_duration = duration
    shake_timer = 0.0

func _get_noise_offset() -> Vector2:
    var x = noise.get_noise_2d(shake_timer * shake_frequency, 0) * shake_intensity
    var y = noise.get_noise_2d(0, shake_timer * shake_frequency) * shake_intensity
    return Vector2(x, y)

21.5.2 相机闪烁

extends Camera2D

@onready var flash_rect: ColorRect  # 全屏ColorRect

func flash(color: Color = Color.WHITE, duration: float = 0.1):
    if not flash_rect:
        _create_flash_rect()
    
    flash_rect.color = color
    flash_rect.visible = true
    
    var tween = create_tween()
    tween.tween_property(flash_rect, "color:a", 0.0, duration)
    tween.tween_callback(func(): flash_rect.visible = false)

func _create_flash_rect():
    var canvas_layer = CanvasLayer.new()
    canvas_layer.layer = 100
    add_child(canvas_layer)
    
    flash_rect = ColorRect.new()
    flash_rect.set_anchors_preset(Control.PRESET_FULL_RECT)
    flash_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE
    canvas_layer.add_child(flash_rect)

21.5.3 慢动作效果

extends Camera2D

func slow_motion(time_scale: float = 0.2, duration: float = 0.5):
    Engine.time_scale = time_scale
    
    # 创建计时器(使用真实时间)
    await get_tree().create_timer(duration * time_scale).timeout
    
    # 恢复正常
    var tween = create_tween()
    tween.tween_method(
        func(scale): Engine.time_scale = scale,
        time_scale, 1.0, 0.3 / time_scale  # 调整恢复时间
    )

# 带缩放的慢动作
func hit_stop(duration: float = 0.1, zoom_amount: float = 0.1):
    Engine.time_scale = 0.0
    
    var original_zoom = zoom
    zoom += Vector2.ONE * zoom_amount
    
    await get_tree().create_timer(duration).timeout
    
    Engine.time_scale = 1.0
    
    var tween = create_tween()
    tween.tween_property(self, "zoom", original_zoom, 0.1)

21.6 高级相机系统

21.6.1 多目标相机

extends Camera2D

@export var targets: Array[Node2D] = []
@export var padding: Vector2 = Vector2(100, 100)
@export var min_zoom: float = 0.5
@export var max_zoom: float = 2.0

func _process(delta: float):
    if targets.is_empty():
        return
    
    var bounds = _calculate_bounds()
    var center = bounds.get_center()
    
    # 移动到中心
    global_position = global_position.lerp(center, 5.0 * delta)
    
    # 调整缩放以包含所有目标
    var viewport_size = get_viewport_rect().size
    var target_size = bounds.size + padding * 2
    
    var zoom_x = viewport_size.x / target_size.x
    var zoom_y = viewport_size.y / target_size.y
    var target_zoom = min(zoom_x, zoom_y)
    target_zoom = clamp(target_zoom, min_zoom, max_zoom)
    
    zoom = zoom.lerp(Vector2.ONE * target_zoom, 3.0 * delta)

func _calculate_bounds() -> Rect2:
    if targets.is_empty():
        return Rect2()
    
    var min_pos = targets[0].global_position
    var max_pos = targets[0].global_position
    
    for target in targets:
        min_pos.x = min(min_pos.x, target.global_position.x)
        min_pos.y = min(min_pos.y, target.global_position.y)
        max_pos.x = max(max_pos.x, target.global_position.x)
        max_pos.y = max(max_pos.y, target.global_position.y)
    
    return Rect2(min_pos, max_pos - min_pos)

func add_target(target: Node2D):
    if target not in targets:
        targets.append(target)

func remove_target(target: Node2D):
    targets.erase(target)

21.6.2 电影式相机

# cinematic_camera.gd
extends Camera2D

signal sequence_finished

var waypoints: Array[Dictionary] = []
var current_waypoint: int = 0
var is_playing: bool = false

func add_waypoint(position: Vector2, zoom: float = 1.0, duration: float = 1.0, ease_type: int = Tween.EASE_IN_OUT):
    waypoints.append({
        "position": position,
        "zoom": zoom,
        "duration": duration,
        "ease": ease_type
    })

func play_sequence():
    if waypoints.is_empty():
        return
    
    is_playing = true
    current_waypoint = 0
    _play_next_waypoint()

func _play_next_waypoint():
    if current_waypoint >= waypoints.size():
        is_playing = false
        sequence_finished.emit()
        return
    
    var wp = waypoints[current_waypoint]
    
    var tween = create_tween()
    tween.tween_property(self, "global_position", wp.position, wp.duration)\
        .set_ease(wp.ease)
    tween.parallel().tween_property(self, "zoom", Vector2.ONE * wp.zoom, wp.duration)\
        .set_ease(wp.ease)
    tween.tween_callback(_on_waypoint_reached)

func _on_waypoint_reached():
    current_waypoint += 1
    _play_next_waypoint()

func stop_sequence():
    is_playing = false

func clear_waypoints():
    waypoints.clear()

21.7 实际案例

21.7.1 完整相机控制器

# game_camera.gd
class_name GameCamera
extends Camera2D

signal camera_shake_started
signal camera_shake_ended

# 跟随设置
@export_group("Follow")
@export var target: Node2D
@export var follow_speed: float = 5.0
@export var look_ahead: float = 50.0
@export var dead_zone: Vector2 = Vector2(20, 20)

# 边界设置
@export_group("Limits")
@export var use_limits: bool = true
@export var limit_rect: Rect2 = Rect2(0, 0, 1920, 1080)

# 震动设置
@export_group("Shake")
@export var shake_decay: float = 5.0

var _shake_intensity: float = 0.0
var _look_ahead_offset: Vector2 = Vector2.ZERO
var _noise: FastNoiseLite
var _shake_time: float = 0.0

func _ready():
    _setup_noise()
    _apply_limits()
    position_smoothing_enabled = true
    position_smoothing_speed = follow_speed

func _setup_noise():
    _noise = FastNoiseLite.new()
    _noise.noise_type = FastNoiseLite.TYPE_SIMPLEX
    _noise.frequency = 1.0

func _apply_limits():
    if use_limits:
        limit_left = int(limit_rect.position.x)
        limit_top = int(limit_rect.position.y)
        limit_right = int(limit_rect.end.x)
        limit_bottom = int(limit_rect.end.y)

func _process(delta: float):
    _update_follow(delta)
    _update_shake(delta)

func _update_follow(delta: float):
    if not target:
        return
    
    # 计算前瞻
    if target is CharacterBody2D:
        var velocity_offset = target.velocity.normalized() * look_ahead
        _look_ahead_offset = _look_ahead_offset.lerp(velocity_offset, 3.0 * delta)
    
    # 目标位置
    var target_pos = target.global_position + _look_ahead_offset
    
    # 应用死区
    var diff = target_pos - global_position
    if abs(diff.x) < dead_zone.x:
        target_pos.x = global_position.x
    if abs(diff.y) < dead_zone.y:
        target_pos.y = global_position.y
    
    global_position = global_position.lerp(target_pos, follow_speed * delta)

func _update_shake(delta: float):
    if _shake_intensity > 0:
        _shake_time += delta
        
        var shake_offset = Vector2(
            _noise.get_noise_2d(_shake_time * 50, 0),
            _noise.get_noise_2d(0, _shake_time * 50)
        ) * _shake_intensity
        
        offset = shake_offset
        _shake_intensity = max(0, _shake_intensity - shake_decay * delta)
        
        if _shake_intensity <= 0:
            camera_shake_ended.emit()
    else:
        offset = Vector2.ZERO

func shake(intensity: float = 10.0):
    if _shake_intensity <= 0:
        camera_shake_started.emit()
    _shake_intensity = max(_shake_intensity, intensity)
    _shake_time = 0.0

func set_target(new_target: Node2D, instant: bool = false):
    target = new_target
    if instant and target:
        global_position = target.global_position

func zoom_to(level: float, duration: float = 0.5):
    var tween = create_tween()
    tween.tween_property(self, "zoom", Vector2.ONE * level, duration)\
        .set_trans(Tween.TRANS_SINE)

func move_to(pos: Vector2, duration: float = 0.5):
    var old_target = target
    target = null
    
    var tween = create_tween()
    tween.tween_property(self, "global_position", pos, duration)
    tween.tween_callback(func(): target = old_target)

func set_room_limits(room: Rect2, transition: bool = true):
    limit_rect = room
    
    if transition:
        var tween = create_tween()
        tween.tween_property(self, "limit_left", int(room.position.x), 0.3)
        tween.parallel().tween_property(self, "limit_top", int(room.position.y), 0.3)
        tween.parallel().tween_property(self, "limit_right", int(room.end.x), 0.3)
        tween.parallel().tween_property(self, "limit_bottom", int(room.end.y), 0.3)
    else:
        _apply_limits()

本章小结

本章全面学习了Godot的2D相机系统:

  1. Camera2D基础:创建、属性、多相机切换
  2. 相机跟随:简单跟随、平滑跟随、前瞻、死区
  3. 边界限制:内置边界、动态边界、房间系统
  4. 缩放控制:基本缩放、平滑缩放
  5. 相机特效:屏幕震动、闪烁、慢动作
  6. 高级系统:多目标相机、电影式相机
  7. 实际案例:完整相机控制器

下一章将通过一个完整的2D游戏实战案例,综合运用前面学到的所有知识。


上一章:TileMap与地图编辑

下一章:2D游戏实战案例

← 返回目录