文章目录
-
-
- 一、前言
- 二、资源文件说明
-
- 一、二进制文件(pwd文件、aef文件)
- 2、数据格式
-
- 2.1、pwd格式
- 2.2、aef格式
- 三、C#读取二进制文件API
-
- 打开二进制文件:FileStream文件流
- 二进制读取:BinaryReader
- 3.字节序问题:大端小端
- 四、实战
-
- 1、创建Unity工程
- 2、导入pwd和aef文件
- 3.使用16进制查看器(Hex Editor)
- 4.逐字节分析
- 5.写工具脚本:pwd生成png
-
- 5.1、创建FileRead脚本
- 5.2、定义PWDInfo数据结构
- 5.3、封装ReadInt16和ReadInt32方法
- 5.4、封装ReadPWD方法
- 5.5、创建GenResTools脚本
- 5.6、封装保存png图片的方法
- 5.7.自动设置图片属性
- 5.生成精灵小图
- 5.9、遍历pwd生成文件执行
- 5.生成操作菜单png图片
- 6.写工具脚本:aef生成预设文件
-
- 6.1、定义AEFInfo数据结构
- 6.2、封装ReadAEF方法
- 6.3、封装GeneratePreabByAEF方法
- 6.4、封装SaveAniPrefab方法
- 7.编写操作脚本:AniRuntime.cs
- 8.生成执行菜单的预设文件
- 运行测试动画
- 五、工程源码
- 六、完毕
-
一、前言
嗨,大家伙,我是新发。 有同学给我发了私信和邮件,内容如下:
你好,林新发大哥。我叫你。**,是四川98年的小伙子,从小在山寨机上玩武侠网游,悠米游戏平台的天龙传奇,寻秦OL,冒泡平台降龙十八掌,笑傲江湖,傲剑ol等游戏,玩了很多游戏,最喜欢的是天龙传奇和寻秦OL这2款 武侠回合制。 后来学了计算机应用,混到毕业,被中介坑到天津一年五G监督,毕业后很迷茫,终于学会了贷款Unity,不幸的是,学习后,我找到了一家开发了三个月益智游戏的公司。我每天都很忙,但没有任何进步。然后我明白有些东西不合适,但就是不合适。我几乎每天都写代码 Transform 过去过来,我也知道都是简单的东西,但我需要很长时间才能理解这个简单的东西,每天都很痛苦。 后来,我转向了快递行业。除了网站上的电脑硬件问题,我觉得学习是有用的。虽然有时我觉得程序员的未来很好,但在工厂里修理电脑是一种混乱,但它不会像以前那样痛苦。也许我仍然在为自己找借口,不努力工作。 然后我总是在业余时间想起这个小时候的游戏。我知道有人一直在期待爱情发电的复制,近500人在期待。经过无数所谓的众筹开发,群友花钱找工作室开发(到了规定的时间,他说工作室出了问题,两次之后大家才意识到他在和几百人开玩笑)。经过各种鸽子,我终于意识到游戏不可能回来了。 然后我想用素材做一个单机游戏,找回忆,但能力有限,甚至读一个文件 我不明白数据的转换。最后,我问了几个人,找了同学。我还是做不到。主要原因是我缺乏编程能力。最后,我心情不安地给你发了一封私人信件。
也就是说,想在Unity
中逆向寻秦OL
资源(序列帧动画)Unity
中播放。 不幸的是,我小时候从来没有玩过这个游戏,只看过电视剧,或者小时候的电视剧很好看,现在很少看电视剧。 嘛,话说回来,我还是先解决一下这个同学的问题,讲讲如何对二进制资源进行解析并逆向生成Unity
预设文件。
本文的最终效果如下 文章末尾见工程源码。
二、资源文件说明
一、二进制文件(pwd文件、aef文件)
一些资源文件一些资源文件是二进制格式,包括.pwd
、.aef
文件等,
许多游戏将构建自己的二进制资源文件,有两个目的: 1.增加逆向难度; 2.压缩资源。 如果我们只获得二进制资源文件,就很难逆转发布具体内容。一般来说,我们需要配合逆向游戏代码,通过代码分析逻辑逆转资源的数据格式,然后编写工具来分析资源并将其保存为我们可以使用的资源格式。 幸运的是,邮件中提到有人整理了这些格式(.pwd
、.aef
、.mape
)数据规则省去了我逆向代码的过程。让我们先解释一下这些文件的数据格式~
2、数据格式
2.1、pwd格式
pwd
文件本质上是素材文件png
添加一些自定义数据,自带分割png
的数据。 数据格式如下:
长度 | 含义 |
---|---|
2字节 | 当前文件的ID |
4字节 | 图片资源长度 |
前一个字段值的字节数 | 图片资源 |
2字节 | 图片数量可分为小图片 |
然后循环读取以下字段,循环次数是图片可分割的小图数,
长度 | 含义 |
---|---|
2字节 | 坐标x |
2字节 | 坐标y |
2字节 | 小图宽度width |
2字节 | 小图高度height |
画个图方便大家理解,
2.2、aef格式
上面的pwd
文件可以理解为是图集文件,而这里要讲的aef
文件可以理解为序列帧动画文件
,aef
记录了每一帧使用的小图文件和坐标信息等。
数据格式如下:
长度 | 含义 |
---|---|
2字节 | 该文件包含的帧数量 |
后面的数据连续循环上面字段的值,每次循环读取以下的字段
长度 | 含义 |
---|---|
2字节 | 帧ID |
4字节 | 该帧用到的小图数量 |
然后根据该帧用到的小图数量循环读取以下的字段
长度 | 含义 |
---|---|
2字节 | pwd文件的ID |
2字节 | 当前图片的ID |
2字节 | 坐标x |
2字节 | 坐标y |
画个图方便大家理解,
三、C#读取二进制文件的API
我们要在Unity
中去解析pwd
和aef
文件,就要用到读取二进制文件的API
,有必要单独拿出来讲一下。
1、打开二进制文件:FileStream文件流
我们要打开一个二进制文件,可以使用FileStream
类,需要引入命名空间:
using System.IO;
使用方法:
string filePath = "要打开的文件路径";
using (FileStream fs = new FileStream(filePath , FileMode.Open))
{
// TODO 文件流操作
}
上面我们是通过FileStream
自身的构造函数来构建一个FileStream
对象的,我们也可以通过File.Open
来构建FileStream
对象,如下
string filePath = "要打开的文件路径";
using(var fs = File.Open(filePath, FileMode.Open))
{
// TODO 文件流操作
}
注:可能有同学会问,这个
using
是干嘛的? 我们把创建的文件流对象的过程写在using
中,在离开using
作用域时会自动帮助我们释放流所占用的资源,否则我们需要手动调用FileStream
的Dispose
方法来释放资源。
2、二进制读取:BinaryReader
上面我们得到FileStream
对象,接下来就可以使用BinaryReader
来对流进行二进制读取了,例:
string filePath = "要打开的文件路径";
using (FileStream fs = new FileStream(filePath , FileMode.Open))
{
using (BinaryReader br = new BinaryReader(fs))
{
// 读取1个字节
byte a0 = br.ReadByte();
// 读取2个字节,并以小端字节序转为short,需要特别小心!
short a1 = br.ReadInt16();
// 读取4个字节,并以小端字节序转为int,需要特别小心!
int a2 = br.ReadInt32();
// 读取800个字节
byte[] a3 = br.ReadBytes(800);
}
}
3、字节序问题:大端小端
上面代码中ReadInt16
和ReadInt32
需要特别小心字节序问题,什么是字节序呢?为什么要搞字节序这个东西呢?我来给你讲清楚。 我们的计算机内存是以字节为存储单元的,画个图, 我们知道,一个short
是2个字节
,一个int
是4个字节
,现在我问你,假设用0x00000000
和0x00000001
这两个地址对应的2个字节
来表示一个short
,那么这个short
的值是多少? 你可能会回答0x1C09
,因为低地址是0x09
,高地址是0x1C
,组合起来就是0x1C09
,转为十进制就是7177
,
但是,为什么不能是0x091C
呢?谁规定高地址就是高位,低地址就一定是低位呢? 这个,就是字节序问题。 如果是高地址放高位,低地址放低位,就是小端字节序
,这个符合我们人类的思维习惯。(口诀:高高低低为小端)。 反过来就是大端字节序
。虽然说小端字节序符合人类的思维习惯,但却反而不直观,为什么?比如下面这个二进制文件,我圈出来的4个字节
的值你是不是第一反应是0x00000065
(大端字节序),如果你真按小端字节序来思考的话,应该是0x65000000
,因为0x65
的地址是最高的,按小端字节序的话0x65
是放在最高位。不过,这里的二进制文件是按大端字节序存储的,所以答案是0x00000065
。 现在问题又来了,我们如果使用BinaryReader
的ReadInt32()
方法一次性读取4字节
,它是以什么字节序去构造一个int
的呢?C#
默认的字节序是小端字节序,所以如果你用ReadInt32()
会得出错误的答案。 那我们如何正确的读取这4个字节
呢?可以先使用ReadBytes(4)
方法读取四个字节:
// 读取4个字节
byte[] intBytes = br.ReadBytes(4);
这个时候读出来的字节数据是这样的 我们使用Array.Reverse
方法对数据进行反序,
Array.Reverse(intBytes );
反序后变成这样 此时我们在使用BitConverter.ToInt32
方法即可得到正确的值0x00000065
啦(即十进制的101
),
int i = BitConverter.ToInt32(intBytes, 0);
// i的值为0x00000065,即即十进制的101
画个图总结一下,
四、实战
接下来我们就来实战吧,使用C#
的二进制读取的API
来解析寻秦OL
的二进制资源文件并生成Unity
可用的资源。
1、创建Unity工程
Unity
工程名就叫UnityXunqinOL
吧~
2、导入pwd和aef文件
把NPC
的pwd
和aef
导入工程目录中,比如导入10002
这只怪的资源文件, 如下
3、使用十六进制查看器(Hex Editor)
我一般是使用VS Code
码代码,想要使用VS Code
查看二进制文件,可以安装Hex Editor
插件, 安装完毕后,点击你要查看的文件,然后点击Do you want to open it anyway
, 然后点击Hex Editor
, 这样我们就可以以十六进制的方式查看这个二进制文件了,
4、挨个字节分析
现在我们根据上文中讲的pwd
文件的数据格式来分析一下。 前2个字节
是文件ID
,可见10002_1.pwd
的文件ID
是0
, 接下来是4个字节
,表示png
数据长度,为0x000006F5
,转为十进制即1781
字节, 我们推算一下,读完这1781
个字节,就到了2 + 4 + 1781 - 1
的位置(注意字节从0
字节数起,所以这里减1
),即第1786
字节的位置,转为十六进制就是0x000006FA
的位置,我们跳到这里,
再往下2个字节
是小图数量,为0x0013
,即有19
张小图, 再往后就是解析这19
张小图了,以第一张小图为例,可以得出第一张小图的坐标为:x: 0x0000,y: 0x0011
,即:x: 0,y: 17
,宽高为:0x0015 0x0011
,即宽高为:21 x 17
, 后面以此类推。
5、写工具脚本:pwd生成png
5.1、创建FileRead脚本
现在,我们来写工具脚本,让它去读取pwd
文件吧。 新建Editor
文件夹, 新建一个C#
脚本,重命名为FileReader
,如下,
5.2、定义PWDInfo数据结构
先定义数据结构
// pwd数据结构
public struct PWDInfo
{
public short id; // pwd文件id
public int pngLen; // png数据长度
public byte[] png; // png数据
public int splitCnt; // 小图数量
public SpriteInfo[] spriteInfoList; // 小图信息数组
}
// 小图数据结构
public struct SpriteInfo
{
public int index; // 小图索引
public int x; // 坐标x
public int y; // 坐标y
public int width; // 宽度
public int height; // 高度
}
5.3、封装ReadInt16和ReadInt32方法
封装两个Read
方法,里面实现字节反序,解决大小端问题,
/// <summary>
/// 读取2字节
/// </summary>
private static Int16 ReadInt16(BinaryReader br)
{
byte[] bytes = br.ReadBytes(2);
// 反字节序
Array.Reverse(bytes);
return BitConverter.ToInt16(bytes, 0);
}
/// <summary>
/// 读取4字节
/// </summary>
private static Int32 ReadInt32(BinaryReader br)
{
byte[] bytes = br.ReadBytes(4);
// 反字节序
Array.Reverse(bytes);
return BitConverter.ToInt32(bytes, 0);
}
5.4、封装ReadPWD方法
最后封装一个ReadPWD
方法,只需传入pwd
文件路径,即可解析并返回一个PWDInfo
对象,
public static PWDInfo ReadPWD(string pwdFilePath)
{
PWDInfo pwdInfo = new PWDInfo();
using (FileStream fs = new FileStream(pwdFilePath, FileMode.Open))
{
using (BinaryReader br = new BinaryReader(fs))
{
pwdInfo.id = ReadInt16(br);
pwdInfo.pngLen = ReadInt32(br);
// PNG文件资源
pwdInfo.png = br.ReadBytes(pwdInfo.pngLen);
// 切片数量
int spriteCnt = ReadInt16(br);
SpriteInfo[] spriteInfoList = new SpriteInfo[spriteCnt];
for (int i = 0; i < spriteCnt; ++i)
{
// 每个切片的信息
SpriteInfo spriteInfo = new SpriteInfo();
spriteInfo.index = i;
spriteInfo.x = ReadInt16(br);
spriteInfo.y = ReadInt16(br);
spriteInfo.width = ReadInt16(br);
spriteInfo.height = ReadInt16(br);
spriteInfoList[i] = spriteInfo;
}
pwdInfo.spriteInfoList = spriteInfoList;
}
}
return pwdInfo;
}
5.5、创建GenResTools脚本
我们再创建GenResTools
脚本, 由它来暴露一个菜单项,去调用FileReader.ReadPWD
,
[MenuItem("工具/通过PWD生成PNG")]
public static void GeneratePngByPWD()
{
// 扫描PWD文件
var pwdFilePaths = Directory.GetFiles(Application.dataPath + "/NPC/", "*.pwd", SearchOption.AllDirectories);
foreach (var pwdFilePath in pwdFilePaths)
{
// 解析PWD文件
PWDInfo pwdInfo = FileReader.ReadPWD(pwdFilePath);
// TODO 根据PWDInfo生成png图片
}
}
我们要根据PWDInfo
生成png
图片。
5.6、封装保存png图片的方法
我们封装一个保存png
图片的方法,
// GenResTools.cs
/// <summary>
/// 保存图片
/// </summary>
private static void SavePng(string savePath, byte[] data)
{
if (File.Exists(savePath))
{
File.Delete(savePath);
}
File.WriteAllBytes(savePath, data);
AssetDatabase.Refresh();
}
5.7、自动设置图片属性
图片保存后,需要设置图片的属性,比如图片格式设置为Sprite
,过滤模式设置为Point
等,我们封装一个方法来自动完成这些设置,
// GenResTools.cs
/// <summary>
/// 自动设置图集图片格式
/// </summary>
private static void FixSettings(string pngPath)
{
pngPath = pngPath.Replace('\\', '/');
var assetsPath = pngPath.Replace(Application.dataPath, "Assets");
TextureImporter textureImporter = AssetImporter.GetAtPath(assetsPath) as TextureImporter;
textureImporter.textureType = TextureImporterType.Sprite;
textureImporter.spriteImportMode = SpriteImportMode.Single;
textureImporter.wrapMode = TextureWrapMode.Clamp;
textureImporter.filterMode = FilterMode.Point;
textureImporter.isReadable = true;
AssetDatabase.ImportAsset(assetsPath);
AssetDatabase.Refresh();
}
5.8、生成精灵小图
另外,我们还需要根据图集生成精灵小图,再封装一个生成方法,
/// <summary>
/// 从图集中生成精灵图
/// </summary>
private static void GenSprites(string pwdDir, string atlasPath, PWDInfo pwdInfo)
{
atlasPath = atlasPath.Replace('\\', '/');
var assetsPath = atlasPath.Replace(Application.dataPath, "Assets");
var atlasTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(assetsPath);
foreach (SpriteInfo spriteInfo in pwdInfo.spriteInfoList)
{
// 精灵图
var spriteName = Path.GetFileNameWithoutExtension(atlasPath) + "_" + spriteInfo.index + ".png";
var spriteSaveDir = pwdDir + "/sprites/";
if (!Directory.Exists(spriteSaveDir))
{
Directory.CreateDirectory(spriteSaveDir);
}
var spriteSavePath = spriteSaveDir + spriteName;
var spriteTexture = new Texture2D(spriteInfo.width, spriteInfo.height, TextureFormat.RGBA32, false);
for (int y = 0; y < spriteInfo.height; ++y)
{
for (int x = 0; x < spriteInfo.width; ++x)
{
var color = atlasTexture.GetPixel(spriteInfo.x + x, atlasTexture.height - spriteInfo.y - y - 1);
spriteTexture.SetPixel(x, spriteInfo.height - y - 1, color);
}
}
SavePng(spriteSavePath, spriteTexture.EncodeToPNG());
AssetDatabase.Refresh();
FixSettings(spriteSavePath);
}
AssetDatabase.Refresh();
}
这里要注意坐标系的差异,他们是使用
2D
引擎制作的寻秦OL
,使用的坐标系是y
轴朝下的,与Unity
的y
轴方向是相反的,所以读取像素的时候要使用高度减去y
轴坐标。
5.9、遍历pwd文件执行生成
我们完善一下GeneratePngByPWD
方法的逻辑,最终如下,
[MenuItem("工具/通过PWD生成PNG")]
public static void GeneratePngByPWD()
{
// 扫描PWD文件
var pwdFilePaths = Directory.GetFiles(Application.dataPath + "/NPC/", "*.pwd", SearchOption.AllDirectories);
foreach (var pwdFilePath in pwdFilePaths)
{
// 解析PWD文件
PWDInfo pwdInfo = FileReader.ReadPWD(pwdFilePath);
var pwdDir = Path.GetDirectoryName(pwdFilePath);
var atlasName = Path.GetFileNameWithoutExtension(pwdFilePath) + ".png";
var atlasDir = pwdDir + "/atlas/";
if (!Directory.Exists(atlasDir))
{
// 在pwd所在目录中创建atlas文件夹
Directory.CreateDirectory(atlasDir);
}
var atlasPath = Path.Combine(atlasDir, atlasName);
// 保存图片(图集)
SavePng(atlasPath, pwdInfo.png);
// 设置
FixSettings(atlasPath);
// 生成精灵图
GenSprites(pwdDir, atlasPath, pwdInfo);
}
}
5.10、运行菜单生成png图片
点击菜单工具 / 通过PWD生成PNG
,如下,可以看到正常生成了图集和精灵小图, 生成的图集文件如下, 我们可以看到,10002_1
图集生成的小图有19
张,与我们上文的分析结果一致,
6、写工具脚本:aef生成预设文件
接下来就是解析aef
文件,然后去组织这些精灵小图,把它们包装成序列帧。
6.1、定义AEFInfo数据结构
我们先定义AEFInfo
相关的数据结构,如下
// FileReader.cs
public struct AEFInfo
{
// 帧数
public int frameCnt;
public FrameInfo[] frameInfo;
}
public struct FrameInfo
{
public int frameId;
public int pngCnt;
public FrameSpriteInfo[] frameSpriteInfo;
}
public struct FrameSpriteInfo
{
public int pwdId;
public int spriteId;
public float x;
public float y;
}
6.2、封装ReadAEF方法
接着,我们封装一个ReadAEF
方法,去解析aef
文件,并返回AEFInfo
对象,
public static AEFInfo ReadAEF