第二十一章: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相机系统:
- Camera2D基础:创建、属性、多相机切换
- 相机跟随:简单跟随、平滑跟随、前瞻、死区
- 边界限制:内置边界、动态边界、房间系统
- 缩放控制:基本缩放、平滑缩放
- 相机特效:屏幕震动、闪烁、慢动作
- 高级系统:多目标相机、电影式相机
- 实际案例:完整相机控制器
下一章将通过一个完整的2D游戏实战案例,综合运用前面学到的所有知识。
上一章:TileMap与地图编辑
下一章:2D游戏实战案例