Unity逆向实战:手把手教你解析《寻秦OL》的.pwd和.aef动画文件(附完整C#源码)

Unity逆向实战:手把手教你解析《寻秦OL》的.pwd和.aef动画文件(附完整C#源码) Unity逆向工程实战从二进制动画文件到可运行游戏角色在游戏开发领域逆向工程是一项极具挑战性又充满乐趣的技术活动。许多经典游戏使用自定义二进制格式存储资源当官方工具不再可用时理解这些格式并重建工具链就成为了保留游戏记忆的关键技能。本文将带你深入探索如何解析.pwd图集和.aef动画文件并将它们转换为Unity可用的资源。1. 准备工作与环境搭建1.1 理解二进制资源文件游戏开发者常使用自定义二进制格式存储资源主要出于两个目的资源保护防止轻易被提取和修改性能优化紧凑的存储格式减少加载时间我们面对的.pwd和.aef文件就是典型的自定义格式.pwd包含图集数据和子图元信息.aef定义动画序列和帧组成1.2 创建解析工程在Unity中新建项目建议使用2021或更高版本。项目结构可如下组织Assets/ ├── Editor/ # 工具脚本 ├── Resources/ # 原始二进制文件 ├── Sprites/ # 生成的精灵图 └── Prefabs/ # 最终动画预设安装必要的工具VS Code Hex Editor插件用于二进制查看Unity最新稳定版2. 二进制文件解析核心技术2.1 文件读取基础C#提供了强大的二进制处理API核心类是FileStream和BinaryReaderusing System.IO; string filePath path/to/file.pwd; using (FileStream fs new FileStream(filePath, FileMode.Open)) using (BinaryReader br new BinaryReader(fs)) { // 读取操作将在这里进行 }注意使用using语句确保文件流正确释放避免资源泄漏2.2 处理字节序问题字节序Endianness是二进制解析中最常见的坑之一。我们定义两个辅助方法处理大小端转换private static short ReadInt16BE(BinaryReader br) { byte[] bytes br.ReadBytes(2); Array.Reverse(bytes); // 大端转小端 return BitConverter.ToInt16(bytes, 0); } private static int ReadInt32BE(BinaryReader br) { byte[] bytes br.ReadBytes(4); Array.Reverse(bytes); return BitConverter.ToInt32(bytes, 0); }2.3 解析.pwd文件结构.pwd文件格式可分解为偏移量长度描述0x002文件ID0x024PNG数据长度0x06NPNG数据...2子图数量...8×N子图信息(x,y,width,height)对应的数据结构public struct PWDInfo { public short id; public int pngLength; public byte[] pngData; public SpriteInfo[] sprites; } public struct SpriteInfo { public int x; public int y; public int width; public int height; }3. 实现资源转换工具链3.1 从.pwd生成Unity精灵图创建编辑器工具脚本主要流程读取.pwd文件提取PNG数据并保存为纹理根据子图信息切割图集设置正确的导入设置关键代码片段[MenuItem(Tools/Generate Sprites from PWD)] public static void GenerateSprites() { string[] pwdFiles Directory.GetFiles(Assets/Resources/, *.pwd); foreach (string pwdPath in pwdFiles) { PWDInfo info ReadPWD(pwdPath); // 保存主纹理 string pngPath Path.ChangeExtension(pwdPath, .png); File.WriteAllBytes(pngPath, info.pngData); // 处理导入设置 TextureImporter importer AssetImporter.GetAtPath(pngPath) as TextureImporter; importer.textureType TextureImporterType.Sprite; importer.spriteImportMode SpriteImportMode.Multiple; // 设置子图元数据 ListSpriteMetaData metaData new ListSpriteMetaData(); foreach (var sprite in info.sprites) { metaData.Add(new SpriteMetaData { name ${Path.GetFileNameWithoutExtension(pwdPath)}_{sprite.x}_{sprite.y}, rect new Rect(sprite.x, sprite.y, sprite.width, sprite.height), pivot new Vector2(0.5f, 0.5f) }); } importer.spritesheet metaData.ToArray(); AssetDatabase.ImportAsset(pngPath, ImportAssetOptions.ForceUpdate); } }3.2 解析.aef动画文件.aef文件结构分析偏移量长度描述0x002总帧数...可变帧数据块每帧数据块包含帧ID使用的子图数量每个子图的引用信息(pwdID, spriteID, x, y)对应的数据结构public struct AEFInfo { public int frameCount; public FrameData[] frames; } public struct FrameData { public int frameID; public SpriteReference[] sprites; } public struct SpriteReference { public int pwdID; public int spriteID; public float x; public float y; }4. 构建动画系统4.1 创建动画预设生成器实现一个编辑器工具将.aef转换为Unity预制件[MenuItem(Tools/Generate Animation Prefabs)] public static void GeneratePrefabs() { string[] aefFiles Directory.GetFiles(Assets/Resources/, *.aef); foreach (string aefPath in aefFiles) { AEFInfo info ReadAEF(aefPath); GameObject prefab new GameObject(Path.GetFileNameWithoutExtension(aefPath)); // 为每帧创建子对象 for (int i 0; i info.frameCount; i) { GameObject frameObj new GameObject($Frame_{i}); frameObj.transform.SetParent(prefab.transform); frameObj.SetActive(i 0); // 默认只激活第一帧 // 添加帧中的所有精灵 foreach (var spriteRef in info.frames[i].sprites) { GameObject spriteObj new GameObject($Sprite_{spriteRef.spriteID}); SpriteRenderer renderer spriteObj.AddComponentSpriteRenderer(); // 加载对应的精灵 string spritePath $Sprites/{spriteRef.pwdID}_{spriteRef.spriteID}; renderer.sprite Resources.LoadSprite(spritePath); spriteObj.transform.SetParent(frameObj.transform); spriteObj.transform.localPosition new Vector3(spriteRef.x, spriteRef.y, 0); } } // 添加动画控制器 prefab.AddComponentAnimationController(); // 保存为预制件 PrefabUtility.SaveAsPrefabAsset(prefab, $Assets/Prefabs/{prefab.name}.prefab); DestroyImmediate(prefab); } }4.2 实现运行时动画播放创建简单的动画控制器脚本public class AnimationController : MonoBehaviour { public float frameRate 12f; private float timer; private int currentFrame; private Transform[] frames; void Start() { // 获取所有帧对象 frames new Transform[transform.childCount]; for (int i 0; i transform.childCount; i) { frames[i] transform.GetChild(i); } } void Update() { timer Time.deltaTime; if (timer 1f / frameRate) { timer 0; NextFrame(); } } void NextFrame() { // 隐藏当前帧 frames[currentFrame].gameObject.SetActive(false); // 前进到下一帧 currentFrame (currentFrame 1) % frames.Length; // 显示新帧 frames[currentFrame].gameObject.SetActive(true); } }5. 高级技巧与优化5.1 坐标系转换处理不同游戏引擎可能使用不同的坐标系系统。在转换过程中需要特别注意Unity使用Y轴向上而许多2D引擎使用Y轴向下坐标原点可能不同中心vs左下角可能需要缩放因子调整精灵大小处理代码示例// 转换Y坐标从向下为正向向上 float ConvertY(float originalY, float imageHeight) { return imageHeight - originalY; }5.2 内存与性能优化处理大量动画资源时需要考虑纹理压缩使用合适的压缩格式减少内存占用合批优化确保精灵使用相同的材质以实现合批对象池对频繁创建销毁的对象使用对象池模式5.3 扩展工具功能可以进一步增强工具链批量处理支持拖放文件夹批量转换预览功能在编辑器中直接预览动画错误恢复添加健壮的错误处理机制元数据导出生成资源使用情况报告// 示例添加进度条显示 EditorUtility.DisplayProgressBar(Processing, Generating sprites..., progress); // ... EditorUtility.ClearProgressBar();逆向工程不仅是技术挑战更是对游戏开发本质的深入理解。通过构建这样的工具链你不仅能复活经典游戏资源还能获得对资源管线和动画系统的深刻认知。