Skip to content

资源转换管线 — 练习

练习 1:分析 generate_wasm_assets.py 的扫描逻辑

目标:理解 Python 脚本如何从 C 源代码中提取资源依赖信息。

任务

  1. 阅读 generate_wasm_assets.py 的主函数,描述其工作流程
  2. 找到用于匹配 INCGFXINCBIN 宏的正则表达式,解释其匹配模式
  3. 统计一个典型目录(如 graphics/pokemon/)下的资源文件数量,估算这些资源转换后的 C 数组总大小
参考答案

1. 工作流程

2. 正则表达式分析

python
# 典型的匹配模式
INCGFX_PATTERN = r'INCGFX\s*\(\s*"([^"]+)"\s*\)'
INCBIN_PATTERN = r'INCBIN\s*\(\s*"([^"]+)"\s*\)'
  • INCGFX\s*\( — 匹配 "INCGFX" 后跟可选空白和左括号
  • \s*"([^"]+)"\s* — 匹配引号内的文件路径(捕获组)
  • \) — 匹配右括号

捕获组 ([^"]+) 提取资源文件的相对路径。

3. 资源估算

graphics/pokemon/ 为例:

资源类型数量单个大小总大小估算
精灵正面图 (.4bpp)~4001-4 KB~800 KB
精灵背面图 (.4bpp)~4001-2 KB~400 KB
精灵调色板 (.pal)~40032 字节~12 KB
图标 (.4bpp)~400256 字节~100 KB
合计~2000~1.3 MB

转换为 C 数组后,每个字节表示为 0xHH, 形式(约 6 字节/原始字节),源码膨胀约 6 倍。但编译后的 .wasm.o 中数据以二进制存储,体积与原始数据接近。


练习 2:理解地图数据的转换过程

目标:掌握 wasm_asm_data.py 如何将 GBA 汇编地图数据转换为 C 数组。

任务

  1. 找到项目中一个地图数据的汇编源文件(如 data/maps/PetalburgCity/ 下的 .s 文件)
  2. 描述该文件中的数据结构(.byte.halfword.word 指令的含义)
  3. 说明 wasm_asm_data.py 如何处理 .align 对齐指令
参考答案

1. 地图汇编文件示例

asm
    .include "constants/map_groups.inc"
    .include "constants/flags.inc"

    .global gMapHeader_PetalburgCity
gMapHeader_PetalburgCity:
    .byte 30, 20           /* 地图宽度, 高度 (图块数) */
    .byte MAP_GROUP_PETALBURG, MAP_NUM_PETALBURG_CITY  /* 地图组, 编号 */
    .byte MAP_LAYOUT_PETALBURG_CITY  /* 布局 ID */
    .byte 0                /* 填充 */
    .halfword FLAG_VISITED_PETALBURG  /* 访问标志 */
    .byte WEATHER_SUNNY    /* 天气类型 */

    .global gMapLayout_PetalburgCity
gMapLayout_PetalburgCity:
    .byte 30, 20           /* 宽度, 高度 */
    .word 0x0600E800       /* 图块映射表地址 (VRAM 偏移) */
    .word gMetatiles_PetalburgCity  /* 元图块数据指针 */
    .word gMetatileAttr_PetalburgCity /* 元图块属性指针 */

2. 数据指令含义

汇编指令大小说明
.byte value1 字节 (8位)小数值、枚举常量、标志位
.halfword value2 字节 (16位)标志位组合、坐标、计数器
.word value4 字节 (32位)内存地址、指针、大数值
.ascii "text"N 字节字符串数据(无终止符)
.space NN 字节零填充/对齐

3. .align 对齐处理

.align n 要求下一条数据对齐到 2^n 字节边界。在转换为 C 数组时,wasm_asm_data.py 需要:

python
def handle_align(current_offset, alignment):
    """计算并插入填充字节"""
    boundary = 1 << alignment  # 2^n
    padding = (boundary - (current_offset % boundary)) % boundary
    return b'\x00' * padding

在 C 数组输出中体现为:

c
// .align 2 后插入 2 字节对齐填充
const u8 mapData[] = {
    0x1E, 0x14,           // 宽度, 高度
    0x00, 0x00,           // .align 2 填充
    0x08, 0xE8, 0x00, 0x06  // 4 字节地址 (对齐后)
};

对齐的正确处理对于游戏代码中的指针访问至关重要——GBA 的 16 位和 32 位内存访问必须在偶数/4 字节对齐地址上进行。


练习 3:设计自定义资源的转换流程

目标:能够为新类型的资源设计完整的转换管线。

任务

  1. 假设你要添加一个新的精灵动画数据(JSON 格式),设计从 JSON 到 WASM 可用数据的完整转换流程
  2. 说明转换后的数据格式应满足哪些约束(对齐、字节序、与游戏代码的接口)
  3. 编写伪代码描述转换脚本的核心逻辑
参考答案

1. 完整转换流程设计

2. 数据约束

约束说明
字节序GBA 使用小端序(Little-Endian),与 WASM 默认一致,无需转换
对齐结构体必须自然对齐(u16 对齐到 2 字节,u32 对齐到 4 字节)
指针大小WASM32 中指针为 4 字节,与 GBA ARM 一致
字符串编码游戏使用自定义字符编码,非标准 ASCII/Unicode
数组长度数组长度需硬编码或通过终止标记表示

3. 转换脚本伪代码

python
import json
import struct

def convert_sprite_animation(json_path, output_path):
    """将 JSON 动画定义转换为 C 数组"""

    # 1. 读取并解析 JSON
    with open(json_path) as f:
        anim_data = json.load(f)

    # 2. 验证格式
    validate_animation_schema(anim_data)

    # 3. 构建 C 头文件
    lines = []
    lines.append("// 自动生成 - 请勿手动修改")
    lines.append(f"// 源文件: {json_path}")
    lines.append("#include 'global.h'")
    lines.append("")

    # 4. 为每个动画序列生成结构体数组
    for anim in anim_data['animations']:
        name = anim['name']
        frames = anim['frames']

        lines.append(f"const struct SpriteFrameImage {name}_images[] = {{")
        for frame in frames:
            tile_num = frame['tile']
            x_offset = frame.get('x_offset', 0)
            y_offset = frame.get('y_offset', 0)
            # 注意: GBA 结构体字段顺序必须与 C 定义一致
            lines.append(f"    {{{tile_num}, {x_offset}, {y_offset}}},")
        lines.append("};")
        lines.append("")

        # 5. 生成动画序列表
        lines.append(f"const union AnimCmd {name}_anim[] = {{")
        for i, frame in enumerate(frames):
            duration = frame['duration']
            lines.append(f"    ANIMCMD_FRAME({frame['tile']}, {duration}),")
        lines.append("    ANIMCMD_END,")
        lines.append("};")
        lines.append("")

    # 6. 写入输出文件
    with open(output_path, 'w') as f:
        f.write('\n'.join(lines))

关键设计决策

  • 输出 C 源文件而非原始二进制,便于编译器优化和符号管理
  • 使用 const 限定符,数据放入 WASM 的只读数据段
  • 结构体布局必须与 include/sprite.h 中的定义完全匹配
  • ANIMCMD_FRAMEANIMCMD_END 宏来自项目的头文件,保持与现有代码一致

拓展挑战

  1. 资源压缩对比:选择一组图形资源,分别以未压缩、LZ77 压缩、Run-Length 压缩格式存储,对比 WASM 模块体积和运行时解压性能。分析在 WASM 环境下哪种策略最优。

  2. 增量资源更新:当前每次构建都会重新处理所有资源。设计一个增量构建方案,通过哈希值检测变化的资源文件,只重新转换修改过的部分。

  3. 自定义 INCGFX 格式:选择一种新的图形格式(如 WebP 或 SVG),编写 Python 转换器将其转为 GBA 的 4bpp/8bpp Tile 格式。需要处理颜色量化、调色板生成和 Tile 切割。

  4. 资源热加载:设计一种在开发阶段无需重新编译整个 WASM 模块就能更新图形资源的方案。提示:考虑将资源数据放在 WASM 内存中的独立区域,通过 JavaScript 动态加载。

  5. 地图数据验证器:编写一个验证工具,检查转换后的地图数据是否符合 GBA 的限制(如地图尺寸限制、Tile 数量限制、调色板数量限制)。这在 GBA 硬件模拟 中有相关约束说明。

  6. 资源使用分析:统计项目中所有 INCGFXINCBIN 引用的资源,生成一份报告,包含:资源类型分布、总大小、按模块分类的大小占比。这有助于识别优化目标。