强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 资源:

  1. 添加 AnimatedSprite2D 节点
  2. 在检查器中创建 SpriteFrames 资源
  3. 在动画面板中添加动画和帧
  4. 设置每帧的 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 - 输入系统