Godot 4 GDScript 教程 / 2D 渲染与 Sprite
2D 渲染与 Sprite
2D 渲染是 Godot 最强项之一。本章系统介绍 Sprite2D、动画、相机、视口、光照、Parallax、TileMap 和粒子系统等核心 2D 渲染功能,帮助你构建出色的 2D 游戏。
1. Sprite2D 节点
1.1 基本使用
extends Sprite2D
func _ready() -> void:
# 设置纹理
var tex: Texture2D = load("res://assets/sprites/player.png") as Texture2D
texture = tex
# 常用属性
centered = true # 是否居中
flip_h = false # 水平翻转
flip_v = false # 垂直翻转
offset = Vector2(0, -16) # 偏移
modulate = Color.WHITE # 调制颜色(叠加)
self_modulate = Color.WHITE # 自身调制颜色
visible = true # 可见性
z_index = 0 # 绘制顺序
# 纹理区域(仅绘制纹理的一部分)
region_enabled = true
region_rect = Rect2(0, 0, 32, 32)
func _process(delta: float) -> void:
# 运行时翻转(根据移动方向)
var direction := Input.get_axis("move_left", "move_right")
if direction != 0:
flip_h = direction < 0
1.2 精灵表动画
extends Sprite2D
## 精灵表帧动画控制器
@export var sprite_sheet: Texture2D
@export var frame_size: Vector2i = Vector2i(32, 32)
@export var animations: Dictionary = {
"idle": {"row": 0, "frames": 4, "fps": 8},
"run": {"row": 1, "frames": 8, "fps": 12},
"attack": {"row": 2, "frames": 6, "fps": 15},
}
var current_anim: String = "idle"
var current_frame: int = 0
var frame_timer: float = 0.0
func play(anim_name: String) -> void:
if current_anim == anim_name:
return
current_anim = anim_name
current_frame = 0
frame_timer = 0.0
func _process(delta: float) -> void:
if not animations.has(current_anim):
return
var anim: Dictionary = animations[current_anim]
var fps: int = anim["fps"]
var total_frames: int = anim["frames"]
var row: int = anim["row"]
frame_timer += delta
if frame_timer >= 1.0 / fps:
frame_timer -= 1.0 / fps
current_frame = (current_frame + 1) % total_frames
# 更新纹理区域
region_enabled = true
region_rect = Rect2(
current_frame * frame_size.x,
row * frame_size.y,
frame_size.x,
frame_size.y
)
2. AnimatedSprite2D
2.1 基本配置
extends CharacterBody2D
@onready var anim_sprite: AnimatedSprite2D = $AnimatedSprite2D
func _ready() -> void:
# 播放动画
anim_sprite.play("idle")
# 动画结束信号
anim_sprite.animation_finished.connect(_on_animation_finished)
anim_sprite.frame_changed.connect(_on_frame_changed)
func _physics_process(delta: float) -> void:
var direction := Input.get_axis("move_left", "move_right")
velocity.x = direction * 200.0
move_and_slide()
# 根据状态切换动画
if not is_on_floor():
anim_sprite.play("jump")
elif abs(velocity.x) > 10:
anim_sprite.play("run")
anim_sprite.flip_h = velocity.x < 0
else:
anim_sprite.play("idle")
func attack() -> void:
anim_sprite.play("attack")
func _on_animation_finished() -> void:
if anim_sprite.animation == "attack":
anim_sprite.play("idle")
func _on_frame_changed() -> void:
# 在特定帧触发效果
if anim_sprite.animation == "attack" and anim_sprite.frame == 3:
_deal_damage()
func _deal_damage() -> void:
print("造成伤害!")
2.2 SpriteFrames 编辑器
在编辑器中创建 SpriteFrames 资源:
- 添加 AnimatedSprite2D 节点
- 在检查器中创建 SpriteFrames 资源
- 在动画面板中添加动画和帧
- 设置每帧的 FPS 和循环选项
# 运行时修改 SpriteFrames
extends AnimatedSprite2D
func add_custom_animation(name: String, frames: Array[Texture2D], fps: float = 10.0) -> void:
var sf: SpriteFrames = sprite_frames
if sf.has_animation(name):
sf.clear(name)
else:
sf.add_animation(name)
sf.set_animation_speed(name, fps)
sf.set_animation_loop(name, true)
for frame_tex in frames:
sf.add_frame(name, frame_tex)
💡 提示: AnimatedSprite2D 适合简单的逐帧动画;复杂动画混合请使用 AnimationPlayer + AnimationTree。
3. CanvasLayer 与渲染层级
3.1 CanvasLayer 使用
# CanvasLayer 用于分离渲染层
# 层级越高,渲染越在前面
# 常见层级规划:
# Layer -1: 背景层(远景、天空)
# Layer 0: 默认层(游戏主体)
# Layer 1: 前景层(近景装饰)
# Layer 10: UI 层(HUD、血条)
# Layer 20: 弹窗层(对话框、菜单)
# Layer 30: 过渡层(淡入淡出)
# 在编辑器中设置 CanvasLayer 的 layer 属性
# 也可以代码设置:
@onready var hud_layer: CanvasLayer = $HUDLayer
func _ready() -> void:
hud_layer.layer = 10
hud_layer.visible = true
# 跟随相机(默认 true)
hud_layer.follow_viewport_enabled = false
3.2 Z-index 精细控制
extends Sprite2D
## 同一 CanvasLayer 内的绘制顺序
@export var sorting_layer: int = 0:
set(value):
sorting_layer = value
z_index = value
## 使用 Y-sort 实现伪透视
@export var y_sort: bool = true
func _process(_delta: float) -> void:
if y_sort:
# Y 值越大,绘制越前面(适合俯视角游戏)
z_index = int(position.y)
⚠️ 注意: CanvasLayer 之间的层级不会受 Camera2D 偏移影响(Layer 不跟随相机),适合做固定 HUD。
4. Camera2D
4.1 基本使用
extends Camera2D
@export var target: Node2D
@export var smooth_speed: float = 5.0
@export var offset_ahead: float = 50.0
# 相机边界
@export var limit_left: int = -1000
@export var limit_right: int = 1000
@export var limit_top: int = -500
@export var limit_bottom: int = 500
func _ready() -> void:
# 设置相机限制
limit_left = limit_left
limit_right = limit_right
limit_top = limit_top
limit_bottom = limit_bottom
# 平滑跟随
position_smoothing_enabled = true
position_smoothing_speed = smooth_speed
# 旋转平滑
rotation_smoothing_enabled = true
# 使此相机为当前相机
make_current()
func _physics_process(delta: float) -> void:
if not target:
return
# 平滑跟随目标
var target_pos: Vector2 = target.global_position
# 提前偏移(根据移动方向)
if target is CharacterBody2D:
target_pos += target.velocity.normalized() * offset_ahead
global_position = global_position.lerp(target_pos, smooth_speed * delta)
# 屏幕震动效果
func screen_shake(intensity: float, duration: float) -> void:
var original_offset := offset
var elapsed := 0.0
while elapsed < duration:
offset = original_offset + Vector2(
randf_range(-intensity, intensity),
randf_range(-intensity, intensity)
)
elapsed += get_process_delta_time()
await get_tree().process_frame
offset = original_offset
4.2 高级相机控制
extends Camera2D
## 死区设置 - 目标在死区内移动时相机不跟随
@export var dead_zone_size: Vector2 = Vector2(50, 30)
@export var look_ahead_factor: float = 0.3
func _physics_process(delta: float) -> void:
var target: Node2D = get_parent() # 假设相机是目标的子节点
# 死区逻辑
var diff: Vector2 = target.global_position - global_position
if abs(diff.x) > dead_zone_size.x:
global_position.x = lerp(
global_position.x,
target.global_position.x - sign(diff.x) * dead_zone_size.x,
5.0 * delta
)
if abs(diff.y) > dead_zone_size.y:
global_position.y = lerp(
global_position.y,
target.global_position.y - sign(diff.y) * dead_zone_size.y,
5.0 * delta
)
# 相机缩放(用于特写)
func zoom_to(target_zoom: Vector2, duration: float = 0.5) -> void:
var tween := create_tween()
tween.tween_property(self, "zoom", target_zoom, duration)
func zoom_in() -> void:
zoom_to(Vector2(2, 2), 0.3)
func zoom_out() -> void:
zoom_to(Vector2(1, 1), 0.3)
5. SubViewport 视口
5.1 视口基础
# SubViewport 用于分屏、小地图、实时纹理等
# 1. 在场景中添加 SubViewportContainer + SubViewport
# 2. 在 SubViewport 中放置 3D/2D 内容
# 3. SubViewportContainer 会自动显示视口内容
extends Control
@onready var minimap_viewport: SubViewport = $MinimapContainer/SubViewport
@onready var minimap_camera: Camera2D = $MinimapContainer/SubViewport/Camera2D
func _ready() -> void:
# 配置小地图视口
minimap_viewport.size = Vector2i(200, 200)
minimap_viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
minimap_viewport.transparent_bg = true
5.2 分屏实现
# 双人分屏
extends Control
@onready var viewport1: SubViewport = $SplitContainer/Viewport1
@onready var viewport2: SubViewport = $SplitContainer/Viewport2
func _ready() -> void:
# 水平分屏
viewport1.size = Vector2i(960, 1080)
viewport2.size = Vector2i(960, 1080)
# 每个视口有独立的 Camera2D
var cam1: Camera2D = viewport1.get_node("Camera2D")
var cam2: Camera2D = viewport2.get_node("Camera2D")
cam1.make_current()
cam2.make_current()
6. 2D 光照
6.1 Light2D 类型
# Godot 4 的 2D 光照节点:
# - PointLight2D: 点光源(灯泡、火把)
# - DirectionalLight2D: 方向光(太阳、月光)
# 配置 PointLight2D
extends PointLight2D
func _ready() -> void:
color = Color(1, 0.9, 0.7) # 暖光
energy = 1.5 # 光照强度
texture_scale = 2.0 # 光照范围
# 光照模式
# - ADD: 叠加模式(默认)
# - SUB: 减去模式(阴影)
blend_mode = Light2D.BLEND_MODE_ADD
# 火把闪烁效果
@export var flicker_enabled: bool = true
@export var flicker_intensity: float = 0.2
func _process(delta: float) -> void:
if flicker_enabled:
energy = 1.5 + randf_range(-flicker_intensity, flicker_intensity)
texture_scale = 2.0 + randf_range(-0.1, 0.1)
6.2 光照遮挡
# LightOccluder2D - 光照遮挡器
# 让墙壁等物体投射阴影
# 在编辑器中:
# 1. 添加 LightOccluder2D 节点
# 2. 创建 OccluderPolygon2D 资源
# 3. 编辑遮挡形状
# 动态创建遮挡形状
extends LightOccluder2D
func setup_rect(size: Vector2) -> void:
var polygon := OccluderPolygon2D.new()
polygon.polygon = PackedVector2Array([
Vector2(-size.x / 2, -size.y / 2),
Vector2(size.x / 2, -size.y / 2),
Vector2(size.x / 2, size.y / 2),
Vector2(-size.x / 2, size.y / 2),
])
occluder = polygon
7. Parallax2D 视差层
7.1 Parallax 视差滚动
# Godot 4 中推荐使用 Parallax2D 节点
# 层级规划(远处的层移动慢):
# Layer 0: 天空(scroll_scale = 0.1)
# Layer 1: 远山(scroll_scale = 0.3)
# Layer 2: 树木(scroll_scale = 0.6)
# Layer 3: 地面(scroll_scale = 1.0)
# 在 Parallax2D 节点上设置:
# scroll_scale: Vector2(0.5, 0.5) - 滚动比例
# scroll_offset: Vector2 - 偏移量
# repeat_size: Vector2 - 重复尺寸
extends Parallax2D
@export var auto_scroll_speed: Vector2 = Vector2(-20, 0)
func _process(delta: float) -> void:
# 自动滚动(星空效果)
scroll_offset += auto_scroll_speed * delta
8. TileMap 新系统
8.1 TileMapLayer 使用
# Godot 4.3+ 推荐使用 TileMapLayer 而非 TileMap
extends TileMapLayer
const LAYER_TERRAIN: int = 0
const LAYER_WALLS: int = 1
var map_data: Dictionary = {}
func generate_procedural_map(width: int, height: int) -> void:
clear()
for x in range(width):
for y in range(height):
var tile_type: int = _calculate_tile(x, y, width, height)
set_cell(Vector2i(x, y), LAYER_TERRAIN, Vector2i(tile_type, 0))
func _calculate_tile(x: int, y: int, w: int, h: int) -> int:
# 简单地形生成
if y > h * 0.7:
return 2 # 石头
elif y > h * 0.5:
return 1 # 草地
else:
return 0 # 空地
# 世界坐标 <-> 瓦片坐标转换
func world_to_tile(world_pos: Vector2) -> Vector2i:
return local_to_map(to_local(world_pos))
func tile_to_world(tile_pos: Vector2i) -> Vector2:
return to_global(map_to_local(tile_pos))
# 获取瓦片信息
func get_tile_data(tile_pos: Vector2i) -> TileData:
return get_cell_tile_data(tile_pos)
func is_wall(tile_pos: Vector2i) -> bool:
var data := get_tile_data(tile_pos)
if data:
return data.get_custom_data("is_wall")
return false
8.2 TileSet 自动地形
# 自动地形配置流程:
# 1. 创建 TileSet 资源
# 2. 添加纹理并划分瓦片
# 3. 创建 Terrain(地形类型)
# 4. 为每个瓦片设置地形连接规则
# 5. 使用 set_cells_terrain_connect() 自动匹配
extends TileMapLayer
enum Terrain { GRASS, DIRT, WATER, STONE }
func paint_terrain(center: Vector2i, terrain_type: int, radius: int = 1) -> void:
var cells: Array[Vector2i] = []
for x in range(-radius, radius + 1):
for y in range(-radius, radius + 1):
if Vector2(x, y).length() <= radius:
cells.append(center + Vector2i(x, y))
set_cells_terrain_connect(cells, 0, terrain_type)
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseMotion and Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
var mouse_pos := get_global_mouse_position()
var tile_pos := local_to_map(to_local(mouse_pos))
paint_terrain(tile_pos, Terrain.GRASS, 2)
9. GPU 粒子(GPUParticles2D)
9.1 基本粒子配置
extends GPUParticles2D
func _ready() -> void:
# 粒子数量
amount = 100
# 发射时长(0 = 无限)
lifetime = 2.0
# 发射形状
emitting = true
# 使用 ParticleProcessMaterial
var mat := ParticleProcessMaterial.new()
# 发射方向
mat.direction = Vector3(0, -1, 0)
mat.spread = 30.0
# 速度
mat.initial_velocity_min = 50.0
mat.initial_velocity_max = 150.0
# 重力
mat.gravity = Vector3(0, 200, 0)
# 缩放
mat.scale_min = 0.5
mat.scale_max = 1.5
# 颜色渐变
var gradient := Gradient.new()
gradient.set_color(0, Color(1, 0.5, 0, 1)) # 起始颜色(橙色)
gradient.set_color(1, Color(1, 0, 0, 0)) # 结束颜色(红色透明)
mat.color_ramp = gradient
process_material = mat
# 爆炸粒子效果
func explosion(pos: Vector2) -> void:
global_position = pos
emitting = true
# 单次发射
one_shot = true
explosiveness = 1.0 # 同时发射所有粒子
# 等待粒子播放完成
await finished
queue_free()
9.2 预设粒子效果
# 常见粒子效果配置
# 火焰效果
static func create_fire_material() -> ParticleProcessMaterial:
var mat := ParticleProcessMaterial.new()
mat.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_SPHERE
mat.emission_sphere_radius = 10.0
mat.direction = Vector3(0, -1, 0)
mat.spread = 15.0
mat.initial_velocity_min = 80.0
mat.initial_velocity_max = 120.0
mat.gravity = Vector3(0, -50, 0) # 向上飘
mat.scale_min = 0.3
mat.scale_max = 1.0
mat.damping = 2.0
return mat
# 烟雾效果
static func create_smoke_material() -> ParticleProcessMaterial:
var mat := ParticleProcessMaterial.new()
mat.direction = Vector3(0, -1, 0)
mat.spread = 25.0
mat.initial_velocity_min = 30.0
mat.initial_velocity_max = 60.0
mat.gravity = Vector3(0, -20, 0)
mat.scale_min = 0.5
mat.scale_max = 2.0
mat.damping = 1.0
mat.angular_velocity_min = -90.0
mat.angular_velocity_max = 90.0
return mat
# 雪花效果
static func create_snow_material() -> ParticleProcessMaterial:
var mat := ParticleProcessMaterial.new()
mat.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_BOX
mat.emission_box_extents = Vector3(500, 0, 0)
mat.direction = Vector3(0, 1, 0)
mat.spread = 20.0
mat.initial_velocity_min = 50.0
mat.initial_velocity_max = 100.0
mat.gravity = Vector3(0, 30, 0)
mat.scale_min = 0.2
mat.scale_max = 0.5
return mat
10. 游戏开发场景
场景:像素风格游戏渲染
# 像素风格游戏的渲染设置
extends Node
## 配置像素风格渲染
func setup_pixel_art() -> void:
# 1. 设置纹理过滤为最近邻(像素清晰)
get_viewport().canvas_item_default_texture_filter = Viewport.DEFAULT_TEXTURE_FILTER_NEAREST
# 2. 设置分辨率(低分辨率像素风)
var base_resolution := Vector2i(384, 216) # 16:9 像素分辨率
get_window().content_scale_size = base_resolution
# 3. 窗口拉伸模式
# 项目设置 > 显示 > 窗口 > 拉伸 > 模式: canvas_items
# 项目设置 > 显示 > 窗口 > 拉伸 > 宽高比: keep
func _ready() -> void:
setup_pixel_art()
场景:2D 光照地牢
# 地牢照明系统
extends Node2D
@onready var player_light: PointLight2D = $Player/PointLight2D
@onready var darkness: CanvasModulate = $Darkness
func _ready() -> void:
# 设置全局暗化(模拟黑暗环境)
darkness.color = Color(0.1, 0.1, 0.15, 1.0)
func _process(delta: float) -> void:
# 玩家光源跟随
player_light.global_position = $Player.global_position
# 火把闪烁
player_light.energy = 1.2 + sin(Time.get_ticks_msec() * 0.005) * 0.3
# 动态放置火把
func place_torch(pos: Vector2) -> void:
var torch := PointLight2D.new()
torch.texture = preload("res://assets/light_textures/circle.png")
torch.position = pos
torch.color = Color(1, 0.8, 0.5)
torch.energy = 0.8
torch.texture_scale = 1.5
add_child(torch)
11. 扩展阅读
上一章: 08 - 节点与场景树 下一章: 10 - 输入系统