[GBA教程]pokeemerald实现动态地图色板功能

[GBA教程]pokeemerald实现动态地图色板功能

注意:

1.该功能目前仍然处于实验阶段,自行斟酌使用!

2.该功能适用于对地图块色板处理不太精通但对美工有色板需求的,如果不太强求美工,用行走图代替房子,将地图切成多个区域并使用多个T1依旧是最优解!

3.目前暗古神坛重制版已投入使用,可以参考暗古神坛重制版的实机效果考虑是否使用。

在三代原版及其改版游戏中,一张完整的地图画面通常由主图块集(Primary Tileset)副图块集(Secondary Tileset)中的图块共同拼接、组合而成。这种双图块集的架构为地图创作提供了更丰富的素材与更大的设计灵活性。

一张地图可用的背景色板总数通常为13块(该常量可在 include\fieldmap.h 中通过 NUM_PALS_TOTAL 定义查看),其余3块色板则固定用于游戏界面元素,如地图名称弹窗、选择框边框及对话框等。

这一总数在主图块集(Primary Tileset,简称T0)副图块集(Secondary Tileset,简称T1)之间进行分配。具体分配数量由 NUM_PALS_IN_PRIMARY 定义决定:T0使用从0开始的连续若干块色板,而T1则使用剩余的色板(即总数减去T0使用的数量)。

地图编辑工具 Porymap 严格遵循此规则:它会从T0的色板文件夹加载编号为0至5的色板文件(.pal),并从T1的色板文件夹加载编号6至13的色板文件,以此确保改版编辑过程中的素材调用与游戏实际运行时的硬件限制保持一致。

受到动态行走图色板机制的启发,我们可以对副图块集(Secondary Tileset, T1)的色板加载方式进行革新,将其改造为一种动态加载形式。

问:为什么不把T0也用上呢?

答:T0在两张连续的地图切换时不会重新加载,没有给他添加动态的必要,并且如果T0和T1都弄成动态的可能会在性能上有一定的风险。

其核心原理是:当地图需要加载T1中的特定图块时,系统将不再直接将预设的tilemap数据复制给bg的tilemap内存,而是实时参照一片专用于动态色板管理的内存区域。根据该内存中记录的信息,系统会为当前图块动态分配其所需的、正确的色板编号并生成新的tilemap数据复制给bg的tilemap内存。

这一改动能将色板的使用从静态分配中解放出来,允许在游戏运行时将地图块色板加载到空闲的色板区域内,更灵活地复用与调配色板资源。

然而,现有地图块数据结构中,标识色板槽位(PAL Slot)的字段仅为4位(最大值为0xF),无法容纳所需动态色板标识(Tag)的更大数值范围。

u16 tileId:10;
u16 xflip:1;
u16 yflip:1;
u16 palette:4;
// 该结构同时也是gba map的结构,原版游戏会直接将数据复制给bg的tilemap

因此,实现此方案需对数据结构及配套工具进行系统性升级,具体分为以下三个目标:

一、升级地图块数据结构并定制编辑工具
将地图块数据中的色板槽位字段从 4位扩充至8位(为了对齐直接改成了16位),以支持扩展的动态色板标识。需基于Porymap源码,修改并编译出能完全兼容此新数据格式的专属版本,确保其可正确读取与写入。

二、批量迁移现有地图数据
使用新版工具,对项目中所有现有地图的图块数据进行批量转换与更新,将其全面迁移至新的数据结构。请注意,此变更将使每个地图块的数据内存占用大致扩大一倍。

三、修改游戏运行时逻辑以支持动态映射
在游戏的核心源码中,修改地图加载逻辑。当读取新版地图块数据时,需能根据其8位的动态色板标识,通过查询动态色板管理内存,实时映射并加载正确的色板

已知潜在风险与注意事项:

  1. 地图衔接风险:在两张地图的衔接边界处,若双方均使用了具有相同动态色板标识的T1图块,可能导致渲染异常。

  2. 性能影响:若单帧内需加载的动态色板数量过大,理论上可能引发瞬时卡顿,但目前在测试中尚未触发此情况。

  3. 机能限制:即使是使用了动态地图色板,但是在GBA单屏幕内的色板数量依旧不变,也就是同屏幕下(240×160像素内,但是地图加载的预载范围会比屏幕尺寸更大一点)使用的动态色板数量依旧是总数减T0


一、修改Porymap源码

可以参考该改动自行制作一个新版本的专属版本的porymap Comparing huderlem:master…Bubble791:master · huderlem/porymap

同时也提供了一个动态编译的版本

 
porymap6.2.0hack

该版本将地图块结构从u16扩充到了u32 意味着地图块数据会扩大一倍

二、现有地图数据迁移

使用万能的AI生成一个python脚本,将现有的所有tileset转换为新的格式

#!/usr/bin/env python3
"""
GBA反编译项目Tileset数据结构转换脚本
功能:将 tileset 中的 metatiles.bin 文件从旧结构转换为新结构
旧结构 (2 bytes):
    uint16_t tileId:10;
    uint16_t xflip:1;
    uint16_t yflip:1;
    uint16_t palette:4;

新结构 (4 bytes):
    uint32_t tileId:10;
    uint32_t xflip:1;
    uint32_t yflip:1;
    uint32_t tileIndex:4;
    uint32_t palette:8;
    uint32_t unused:8;
"""

import os
import struct
import sys
from pathlib import Path

# 定义新旧结构格式
OLD_STRUCT_FORMAT = '<H'  # 2字节小端序uint16_t
NEW_STRUCT_FORMAT = '<I'  # 4字节小端序uint32_t

def parse_old_tile_data(old_data):
    """解析旧的tile数据 (16位)"""
    tileId   = (old_data >> 0) & 0x3FF   # 10 bits
    xflip    = (old_data >> 10) & 0x1    # 1 bit
    yflip    = (old_data >> 11) & 0x1    # 1 bit
    palette  = (old_data >> 12) & 0xF    # 4 bits
    return tileId, xflip, yflip, palette

def build_new_tile_data(tileId, xflip, yflip, palette):
    """构建新的tile数据 (32位)"""
    # 注意:tileIndex字段需要你根据实际情况计算或传递
    # 这里暂时设为0,你需要根据业务逻辑调整
    tileIndex = 0  # 4 bits - 需要你补充这个值的来源
    
    new_data = 0
    new_data |= (tileId & 0x3FF) << 0      # bit 0-9: tileId
    new_data |= (xflip & 0x1) << 10        # bit 10: xflip
    new_data |= (yflip & 0x1) << 11        # bit 11: yflip
    new_data |= (tileIndex & 0xF) << 12    # bit 12-15: tileIndex
    new_data |= (palette & 0xFF) << 16     # bit 16-23: palette (8位)
    # bit 24-31: unused (保持为0)
    
    return new_data

def convert_metatiles_file(input_path, output_path=None):
    """转换单个metatiles.bin文件"""
    if output_path is None:
        output_path = input_path
    
    try:
        with open(input_path, 'rb') as f:
            old_data = f.read()
        
        # 检查文件大小是否为偶数(旧结构每个tile占2字节)
        if len(old_data) % 2 != 0:
            print(f"警告: {input_path} 文件大小不是2的倍数")
        
        # 计算tile数量
        tile_count = len(old_data) // 2
        print(f"正在转换 {input_path}: {tile_count} 个tiles")
        
        # 转换数据
        new_bytes = bytearray()
        for i in range(tile_count):
            # 读取旧数据
            old_value = struct.unpack(OLD_STRUCT_FORMAT, 
                                     old_data[i*2:(i+1)*2])[0]
            
            # 解析字段
            tileId, xflip, yflip, palette = parse_old_tile_data(old_value)
            
            # 构建新数据
            # 注意:这里需要你确定tileIndex的值如何计算
            # 示例:使用tileId的低4位作为tileIndex
            tileIndex = tileId & 0xF
            
            new_value = 0
            new_value |= (tileId & 0x3FF) << 0
            new_value |= (xflip & 0x1) << 10
            new_value |= (yflip & 0x1) << 11
            new_value |= (tileIndex & 0xF) << 12
            new_value |= (palette & 0xFF) << 16
            
            # 打包为新格式
            new_bytes.extend(struct.pack(NEW_STRUCT_FORMAT, new_value))
        
        # 写入新文件
        with open(output_path, 'wb') as f:
            f.write(new_bytes)
        
        print(f"转换完成: {input_path} → {output_path}")
        print(f"  大小变化: {len(old_data)} 字节 → {len(new_bytes)} 字节")
        return True
        
    except Exception as e:
        print(f"转换失败 {input_path}: {e}")
        return False

def find_and_convert_metatiles(root_dir, backup=True):
    """查找并转换所有metatiles.bin文件"""
    root_path = Path(root_dir)
    
    # 查找primary和secondary目录
    for tileset_type in ['primary', 'secondary']:
        tileset_dir = root_path / tileset_type
        if not tileset_dir.exists():
            print(f"警告: {tileset_dir} 不存在,跳过")
            continue
        
        print(f"\n处理 {tileset_type} tilesets...")
        
        # 查找所有子文件夹中的metatiles.bin
        metatile_files = list(tileset_dir.rglob('metatiles.bin'))
        
        if not metatile_files:
            print(f"在 {tileset_dir} 中未找到metatiles.bin文件")
            continue
        
        for metatile_file in metatile_files:
            print(f"\n处理: {metatile_file.relative_to(root_path)}")
            
            # 备份原文件
            if backup:
                backup_file = metatile_file.with_suffix('.bin.bak')
                if not backup_file.exists():
                    import shutil
                    shutil.copy2(metatile_file, backup_file)
                    print(f"已备份原文件到: {backup_file.name}")
            
            # 转换文件
            convert_metatiles_file(str(metatile_file))

def main():
    """主函数"""
    # 设置工作目录
    if len(sys.argv) > 1:
        project_dir = sys.argv[1]
    else:
        # 默认当前目录
        project_dir = '.'
    
    project_path = Path(project_dir)
    
    if not project_path.exists():
        print(f"错误: 目录不存在 {project_dir}")
        sys.exit(1)
    
    print("=" * 60)
    print("GBA反编译项目Tileset数据结构转换工具")
    print("=" * 60)
    print(f"项目目录: {project_path.absolute()}")
    print()
    
    # 询问是否创建备份
    backup_choice = input("是否创建备份文件? (y/n, 默认y): ").strip().lower()
    backup = not backup_choice.startswith('n')
    
    # 开始转换
    find_and_convert_metatiles(project_path, backup)
    
    print("\n" + "=" * 60)
    print("转换完成!")
    print("=" * 60)
    
    # 重要提示
    print("\n重要注意事项:")
    print("1. 此脚本将 tileIndex 字段设置为 tileId 的低4位")
    print("2. 如果你有特殊的 tileIndex 计算逻辑,请修改 build_new_tile_data 函数")
    print("3. 转换后需要同步修改游戏源码中的相关结构定义和加载逻辑")
    print("4. 建议在转换前使用版本控制系统备份整个项目")

if __name__ == "__main__":
    main()

将上面的python脚本保存到项目路径下的data/tileset/文件夹里执行

用修改后的porymap打开项目,确保一切处理无误

三、游戏中处理代码

四、最终效果

 

注意左侧BG的调色板变动,该地图不同区域的三种房子一共有9块色板

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容