第十五章:精灵与纹理

第十五章:精灵与纹理

"精灵是2D游戏的视觉基石,纹理则是精灵的皮肤。"

精灵(Sprite)是2D游戏中最基本的可视元素。本章将全面讲解Godot中的精灵系统、纹理管理、图集优化等核心内容。


15.1 Sprite2D基础

15.1.1 创建精灵

# 编辑器中创建
# 1. 添加Sprite2D节点
# 2. 在检查器中设置Texture属性
# 3. 拖拽图片文件到属性槽

# 代码创建
func create_sprite():
    var sprite = Sprite2D.new()
    sprite.texture = preload("res://assets/player.png")
    sprite.position = Vector2(100, 100)
    add_child(sprite)

15.1.2 核心属性

extends Sprite2D

func _ready():
    # 纹理
    texture = preload("res://icon.png")
    
    # 位置与变换
    position = Vector2(100, 200)
    rotation = PI / 4  # 45度
    scale = Vector2(2, 2)
    
    # 中心点偏移
    centered = true  # 默认居中
    offset = Vector2(0, -16)  # 自定义偏移
    
    # 翻转
    flip_h = false  # 水平翻转
    flip_v = false  # 垂直翻转
    
    # 区域裁剪
    region_enabled = true
    region_rect = Rect2(0, 0, 32, 32)  # 只显示部分纹理
    
    # 帧动画(单张精灵表)
    hframes = 4  # 水平帧数
    vframes = 2  # 垂直帧数
    frame = 0    # 当前帧
    frame_coords = Vector2i(0, 0)  # 帧坐标

15.1.3 颜色与混合

extends Sprite2D

func _ready():
    # 颜色调制
    modulate = Color(1, 0.5, 0.5, 1)  # 红色调
    self_modulate = Color.WHITE  # 不影响子节点
    
    # 可见性
    visible = true
    modulate.a = 0.5  # 半透明
    
    # 混合模式(通过材质设置)
    var material = CanvasItemMaterial.new()
    material.blend_mode = CanvasItemMaterial.BLEND_MODE_ADD  # 加法混合
    # BLEND_MODE_MIX - 正常
    # BLEND_MODE_ADD - 加法(发光效果)
    # BLEND_MODE_SUB - 减法
    # BLEND_MODE_MUL - 乘法
    self.material = material

15.2 纹理类型

15.2.1 Texture2D

# 基本纹理加载
var tex: Texture2D = preload("res://icon.png")

# 从文件加载
var image = Image.load_from_file("user://screenshot.png")
var tex2 = ImageTexture.create_from_image(image)

# 纹理信息
func get_texture_info(tex: Texture2D):
    print("宽度:", tex.get_width())
    print("高度:", tex.get_height())
    print("大小:", tex.get_size())

15.2.2 AtlasTexture(图集纹理)

# 从大图中截取部分作为纹理
func create_atlas_texture():
    var atlas = AtlasTexture.new()
    atlas.atlas = preload("res://spritesheet.png")
    atlas.region = Rect2(0, 0, 64, 64)  # 截取区域
    atlas.margin = Rect2(0, 0, 0, 0)    # 边距
    atlas.filter_clip = true  # 防止边缘渗透
    
    $Sprite2D.texture = atlas

15.2.3 AnimatedTexture(动画纹理)

# 自动播放帧动画的纹理
func create_animated_texture():
    var anim_tex = AnimatedTexture.new()
    anim_tex.frames = 4
    
    for i in range(4):
        var frame_tex = load("res://frames/frame_%d.png" % i)
        anim_tex.set_frame_texture(i, frame_tex)
        anim_tex.set_frame_duration(i, 0.1)  # 每帧0.1秒
    
    anim_tex.one_shot = false  # 循环播放
    anim_tex.pause = false
    
    $Sprite2D.texture = anim_tex

15.2.4 GradientTexture(渐变纹理)

# 程序生成的渐变纹理
func create_gradient():
    var gradient = Gradient.new()
    gradient.set_color(0, Color.RED)
    gradient.set_color(1, Color.BLUE)
    gradient.add_point(0.5, Color.WHITE)
    
    # 1D渐变
    var tex1d = GradientTexture1D.new()
    tex1d.gradient = gradient
    tex1d.width = 256
    
    # 2D渐变
    var tex2d = GradientTexture2D.new()
    tex2d.gradient = gradient
    tex2d.fill = GradientTexture2D.FILL_RADIAL  # 径向渐变
    tex2d.width = 256
    tex2d.height = 256

15.2.5 NoiseTexture(噪声纹理)

# 程序生成的噪声纹理
func create_noise_texture():
    var noise = FastNoiseLite.new()
    noise.noise_type = FastNoiseLite.TYPE_PERLIN
    noise.frequency = 0.05
    noise.fractal_octaves = 4
    
    var tex = NoiseTexture2D.new()
    tex.noise = noise
    tex.width = 256
    tex.height = 256
    tex.seamless = true  # 无缝平铺
    tex.as_normal_map = false  # 生成法线贴图
    
    # 等待生成完成
    await tex.changed
    $Sprite2D.texture = tex

15.3 精灵动画

15.3.1 帧动画

extends Sprite2D

@export var animation_speed: float = 10.0
var frame_time: float = 0.0

func _ready():
    texture = preload("res://spritesheet.png")
    hframes = 8  # 8帧
    vframes = 1

func _process(delta):
    frame_time += delta * animation_speed
    frame = int(frame_time) % hframes

15.3.2 AnimatedSprite2D

extends AnimatedSprite2D

func _ready():
    # 播放动画
    play("idle")
    
    # 动画控制
    animation = "run"
    speed_scale = 1.5  # 播放速度
    flip_h = true      # 水平翻转
    
    # 信号连接
    animation_finished.connect(_on_animation_finished)
    frame_changed.connect(_on_frame_changed)

func _on_animation_finished():
    if animation == "attack":
        play("idle")

func _on_frame_changed():
    # 在特定帧触发效果
    if animation == "attack" and frame == 3:
        emit_attack_hitbox()

# SpriteFrames资源设置
func setup_sprite_frames():
    var frames = SpriteFrames.new()
    
    # 添加动画
    frames.add_animation("walk")
    frames.set_animation_speed("walk", 10)
    frames.set_animation_loop("walk", true)
    
    # 添加帧
    for i in range(4):
        var tex = load("res://walk_%d.png" % i)
        frames.add_frame("walk", tex)
    
    sprite_frames = frames

15.3.3 动画状态机

extends AnimatedSprite2D

enum State { IDLE, RUN, JUMP, FALL, ATTACK }
var current_state: State = State.IDLE

func update_animation(velocity: Vector2, is_on_floor: bool, is_attacking: bool):
    var new_state = current_state
    
    if is_attacking:
        new_state = State.ATTACK
    elif not is_on_floor:
        new_state = State.JUMP if velocity.y < 0 else State.FALL
    elif abs(velocity.x) > 10:
        new_state = State.RUN
    else:
        new_state = State.IDLE
    
    if new_state != current_state:
        current_state = new_state
        _play_state_animation()

func _play_state_animation():
    match current_state:
        State.IDLE:
            play("idle")
        State.RUN:
            play("run")
        State.JUMP:
            play("jump")
        State.FALL:
            play("fall")
        State.ATTACK:
            play("attack")

15.4 纹理导入设置

15.4.1 导入预设

在Godot中,选择图片文件后可在导入面板设置:

常用导入设置:
├── 2D像素艺术
│   ├── Filter: Nearest(最近邻,保持像素清晰)
│   ├── Repeat: Disabled
│   └── Mipmaps: false
├── 2D高清艺术
│   ├── Filter: Linear(线性插值,平滑缩放)
│   ├── Repeat: Disabled
│   └── Mipmaps: true
└── 平铺纹理
    ├── Filter: Linear
    ├── Repeat: Enabled
    └── Mipmaps: true

15.4.2 代码设置过滤

# 对单个纹理设置
func setup_texture_filter():
    # 方法1:通过CanvasItem
    texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
    # TEXTURE_FILTER_PARENT_NODE - 继承父节点
    # TEXTURE_FILTER_NEAREST - 最近邻(像素风格)
    # TEXTURE_FILTER_LINEAR - 线性插值
    # TEXTURE_FILTER_NEAREST_WITH_MIPMAPS
    # TEXTURE_FILTER_LINEAR_WITH_MIPMAPS

# 项目全局设置
# 项目设置 → Rendering → Textures → Canvas Textures → Default Texture Filter

15.4.3 压缩格式

纹理压缩选项:
├── Lossless(无损)- PNG原始质量
├── Lossy(有损)- WebP压缩
├── VRAM Compressed - GPU压缩(推荐移动端)
│   ├── S3TC(桌面)
│   ├── ETC2(移动端)
│   └── ASTC(现代移动端)
└── VRAM Uncompressed - GPU未压缩(最高质量)

15.5 图集优化

15.5.1 为什么使用图集

图集的优势:
1. 减少Draw Call - 多个精灵共用一个纹理
2. 减少内存碎片 - 纹理尺寸优化
3. 加快加载速度 - 一次加载多个图像
4. 减少纹理切换 - GPU性能提升

15.5.2 创建图集

# 方法1:使用外部工具(TexturePacker, FreeTexturePacker等)
# 导出为精灵表 + JSON数据

# 方法2:使用AtlasTexture资源
# 在编辑器中创建AtlasTexture,设置atlas和region

# 方法3:使用region_rect
extends Sprite2D

func use_sprite_region():
    texture = preload("res://spritesheet.png")
    region_enabled = true
    region_rect = Rect2(0, 0, 32, 32)  # 第一个图块

15.5.3 精灵表管理器

# spritesheet_manager.gd
class_name SpritesheetManager
extends RefCounted

var _atlas: Texture2D
var _frame_size: Vector2i
var _columns: int
var _rows: int
var _regions: Dictionary = {}

func _init(atlas_path: String, frame_width: int, frame_height: int):
    _atlas = load(atlas_path)
    _frame_size = Vector2i(frame_width, frame_height)
    _columns = _atlas.get_width() / frame_width
    _rows = _atlas.get_height() / frame_height
    
    _generate_regions()

func _generate_regions():
    for y in range(_rows):
        for x in range(_columns):
            var index = y * _columns + x
            _regions[index] = Rect2(
                x * _frame_size.x,
                y * _frame_size.y,
                _frame_size.x,
                _frame_size.y
            )

func get_texture(frame_index: int) -> AtlasTexture:
    var tex = AtlasTexture.new()
    tex.atlas = _atlas
    tex.region = _regions.get(frame_index, Rect2())
    return tex

func get_texture_by_coord(x: int, y: int) -> AtlasTexture:
    var index = y * _columns + x
    return get_texture(index)

# 使用
var sheet = SpritesheetManager.new("res://tiles.png", 16, 16)
$Sprite2D.texture = sheet.get_texture(5)

15.6 高级精灵技术

15.6.1 精灵着色器

# 闪白效果(受伤)
# flash.gdshader
shader_type canvas_item;

uniform float flash_intensity : hint_range(0.0, 1.0) = 0.0;
uniform vec4 flash_color : source_color = vec4(1.0, 1.0, 1.0, 1.0);

void fragment() {
    vec4 tex_color = texture(TEXTURE, UV);
    COLOR = mix(tex_color, flash_color, flash_intensity);
    COLOR.a = tex_color.a;
}

# 使用
extends Sprite2D

var flash_tween: Tween

func flash():
    if flash_tween:
        flash_tween.kill()
    
    flash_tween = create_tween()
    flash_tween.tween_property(material, "shader_parameter/flash_intensity", 1.0, 0.05)
    flash_tween.tween_property(material, "shader_parameter/flash_intensity", 0.0, 0.15)

15.6.2 精灵轮廓

# outline.gdshader
shader_type canvas_item;

uniform vec4 outline_color : source_color = vec4(0.0, 0.0, 0.0, 1.0);
uniform float outline_width : hint_range(0.0, 10.0) = 1.0;

void fragment() {
    vec2 size = TEXTURE_PIXEL_SIZE * outline_width;
    
    float outline = texture(TEXTURE, UV + vec2(-size.x, 0)).a;
    outline += texture(TEXTURE, UV + vec2(size.x, 0)).a;
    outline += texture(TEXTURE, UV + vec2(0, -size.y)).a;
    outline += texture(TEXTURE, UV + vec2(0, size.y)).a;
    outline = min(outline, 1.0);
    
    vec4 tex_color = texture(TEXTURE, UV);
    
    if (tex_color.a < 0.1 && outline > 0.0) {
        COLOR = outline_color;
    } else {
        COLOR = tex_color;
    }
}

15.6.3 精灵溶解效果

# dissolve.gdshader
shader_type canvas_item;

uniform float dissolve_amount : hint_range(0.0, 1.0) = 0.0;
uniform sampler2D noise_texture;
uniform vec4 edge_color : source_color = vec4(1.0, 0.5, 0.0, 1.0);
uniform float edge_width : hint_range(0.0, 0.2) = 0.05;

void fragment() {
    vec4 tex_color = texture(TEXTURE, UV);
    float noise = texture(noise_texture, UV).r;
    
    float edge = smoothstep(dissolve_amount, dissolve_amount + edge_width, noise);
    
    if (noise < dissolve_amount) {
        discard;
    }
    
    vec4 final_color = mix(edge_color, tex_color, edge);
    COLOR = final_color;
}

# 使用
func dissolve():
    var tween = create_tween()
    tween.tween_property(material, "shader_parameter/dissolve_amount", 1.0, 1.0)
    await tween.finished
    queue_free()

15.7 性能优化

15.7.1 纹理尺寸规范

推荐尺寸(2的幂次方):
├── 16x16 - 小图标
├── 32x32 - 角色/物品
├── 64x64 - 大角色
├── 128x128 - Boss/UI元素
├── 256x256 - 背景元素
├── 512x512 - 大型精灵表
├── 1024x1024 - UI图集
└── 2048x2048 - 背景图集

注意:移动端最大建议2048x2048

15.7.2 减少Draw Call

# 检查Draw Call
# 在调试面板查看:调试器 → 监视器 → 2D

# 优化策略
# 1. 使用图集
# 2. 合并相同纹理的精灵
# 3. 使用CanvasGroup批处理

# CanvasGroup使用
# 将多个精灵放在CanvasGroup下
# CanvasGroup
#   ├── Sprite1
#   ├── Sprite2
#   └── Sprite3

15.7.3 内存管理

# 及时释放不用的纹理
var cached_textures: Dictionary = {}

func load_texture(path: String) -> Texture2D:
    if not cached_textures.has(path):
        cached_textures[path] = load(path)
    return cached_textures[path]

func unload_texture(path: String):
    if cached_textures.has(path):
        cached_textures.erase(path)

func clear_cache():
    cached_textures.clear()

# 场景切换时清理
func _on_scene_changed():
    clear_cache()

15.8 实际案例

15.8.1 完整的角色精灵类

# character_sprite.gd
class_name CharacterSprite
extends AnimatedSprite2D

signal animation_event(event_name: String)

@export var default_animation: String = "idle"

var _flash_material: ShaderMaterial
var _original_material: Material

func _ready():
    _setup_flash_material()
    play(default_animation)

func _setup_flash_material():
    var shader = preload("res://shaders/flash.gdshader")
    _flash_material = ShaderMaterial.new()
    _flash_material.shader = shader
    _original_material = material

func flash_white(duration: float = 0.1):
    material = _flash_material
    _flash_material.set_shader_parameter("flash_intensity", 1.0)
    
    var tween = create_tween()
    tween.tween_property(_flash_material, "shader_parameter/flash_intensity", 0.0, duration)
    tween.tween_callback(func(): material = _original_material)

func set_facing(direction: float):
    flip_h = direction < 0

func play_animation(anim_name: String, force: bool = false):
    if animation != anim_name or force:
        play(anim_name)

func play_one_shot(anim_name: String, return_to: String = "idle"):
    play(anim_name)
    await animation_finished
    play(return_to)

15.8.2 动态精灵加载器

# sprite_loader.gd
class_name SpriteLoader
extends Node

const PLACEHOLDER_PATH = "res://assets/placeholder.png"

var _loading_queue: Array = []
var _is_loading: bool = false

func load_sprite_async(sprite: Sprite2D, path: String):
    _loading_queue.append({"sprite": sprite, "path": path})
    
    # 设置占位图
    sprite.texture = preload(PLACEHOLDER_PATH)
    
    if not _is_loading:
        _process_queue()

func _process_queue():
    _is_loading = true
    
    while _loading_queue.size() > 0:
        var item = _loading_queue.pop_front()
        var sprite = item.sprite
        var path = item.path
        
        if not is_instance_valid(sprite):
            continue
        
        ResourceLoader.load_threaded_request(path)
        
        while true:
            var status = ResourceLoader.load_threaded_get_status(path)
            if status == ResourceLoader.THREAD_LOAD_LOADED:
                var tex = ResourceLoader.load_threaded_get(path)
                if is_instance_valid(sprite):
                    sprite.texture = tex
                break
            elif status == ResourceLoader.THREAD_LOAD_FAILED:
                push_error("加载失败:" + path)
                break
            
            await get_tree().process_frame
    
    _is_loading = false

本章小结

本章全面学习了Godot的精灵与纹理系统:

  1. Sprite2D基础:创建、属性、颜色混合
  2. 纹理类型:Texture2D、AtlasTexture、AnimatedTexture等
  3. 精灵动画:帧动画、AnimatedSprite2D、状态机
  4. 导入设置:过滤模式、压缩格式
  5. 图集优化:创建图集、精灵表管理
  6. 高级技术:着色器效果(闪白、轮廓、溶解)
  7. 性能优化:纹理尺寸、Draw Call、内存管理
  8. 实际案例:角色精灵类、异步加载器

下一章将学习2D变换与坐标系统。


上一章:节点树架构

下一章:2D变换与坐标系

← 返回目录