【游戏开发实战】Unity逆向怀旧经典游戏《寻秦OL》,解析二进制动画文件生成预设并播放(资源逆向 | 二进制 | C#)
2022/1/4 1:08:11
本文主要是介绍【游戏开发实战】Unity逆向怀旧经典游戏《寻秦OL》,解析二进制动画文件生成预设并播放(资源逆向 | 二进制 | C#),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
文章目录
- 一、前言
- 二、资源文件说明
- 1、二进制文件(pwd文件、aef文件)
- 2、数据格式
- 2.1、pwd格式
- 2.2、aef格式
- 三、C#读取二进制文件的API
- 1、打开二进制文件:FileStream文件流
- 2、二进制读取:BinaryReader
- 3、字节序问题:大端小端
- 四、实战
- 1、创建Unity工程
- 2、导入pwd和aef文件
- 3、使用十六进制查看器(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.8、生成精灵小图
- 5.9、遍历pwd文件执行生成
- 5.10、运行菜单生成png图片
- 6、写工具脚本:aef生成预设文件
- 6.1、定义AEFInfo数据结构
- 6.2、封装ReadAEF方法
- 6.3、封装GeneratePreabByAEF方法
- 6.4、封装SaveAniPrefab方法
- 7、编写运行时脚本:AniRuntime.cs
- 8、执行菜单生成预设文件
- 9、运行测试动画
- 五、工程源码
- 六、完毕
一、前言
嗨,大家伙,我是新发。
有同学私信并给我发了封邮件,内容如下:
邮件内容:
林新发大哥你好,我叫**,是个四川98年的小伙,因为从小在山寨机上玩武侠网游,悠米游戏平台的天龙传奇,寻秦OL,冒泡平台降龙十八掌,笑傲江湖,傲剑ol等游戏,玩了很多游戏,最喜欢的还是天龙传奇和寻秦OL这2款 武侠回合制。
后来学了计算机应用,然后混到了毕业,被中介坑到天津当了1年5G督导,后来毕业很迷茫,最后贷款学了Unity,非常遗憾,学完后找了一个公司开发了3个月的益智类游戏,每天都很忙,但是并没有任何进步,然后我就明白了,有些东西不适合,它就是不适合,我每天写代码几乎都是 Transform 过去过来,我也知道全是浅显的东西,但是这浅显的东西我都需要花很久才能明白,每天都很煎熬。
后来转行快递行业,每天除了场地上的电脑硬件问题,这才感到学有所用,虽然有时也会觉得程序员前途很好,厂里面修电脑就是混日子,但是不会像以前那么煎熬了,或许我内心还是在给自己不努力找借口。
然后就是空闲时间老是想起这个小时候的游戏,知道有人用爱发电在复刻一直在期待,将近500多个人在期待,经过无数所谓的众筹请人开发,群友自己花钱找工作室开发(到规定时间他就说工作室出问题,2次后大家才明白他在和几百人开玩笑),各种被鸽之后,终于明白这个游戏不可能回来的了。
然后就想自己拿素材做单机小游戏,寻找一下回忆,但是能力有限,连一个文件的读取 数据的转换都弄不明白,最后问了几个人,也找同学弄了一下,还是不行,主要原因还是自己编程能力不足,最后经过忐忑的心情给你发了私信。
就是说,想在Unity
中逆向寻秦OL
的资源(序列帧动画),并可以在Unity
中播放。
遗憾的是我小时候没玩过这个游戏,只看过寻秦记电视剧,还是小时候的电视剧好看呀,现在都很少看电视剧了。
嘛,话说回来,我还是先解决一下这个同学的问题,讲讲如何对二进制资源进行解析并逆向生成Unity
预设文件。
本文最终效果如下
工程源码见文章末尾。
二、资源文件说明
1、二进制文件(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(string aefFilePath) { AEFInfo aefInfo = new AEFInfo(); using (FileStream fs = new FileStream(aefFilePath, FileMode.Open)) { using (BinaryReader br = new BinaryReader(fs)) { aefInfo.frameCnt = ReadInt16(br); aefInfo.frameInfo = new FrameInfo[aefInfo.frameCnt]; for (int i = 0; i < aefInfo.frameCnt; ++i) { FrameInfo frameInfo = new FrameInfo(); // 跳过文件中的frameId,自行使用i作为frameId br.ReadInt16(); frameInfo.frameId = i; frameInfo.pngCnt = ReadInt32(br); frameInfo.frameSpriteInfo = new FrameSpriteInfo[frameInfo.pngCnt]; for (int j = 0; j < frameInfo.pngCnt; ++j) { FrameSpriteInfo spriteInfo = new FrameSpriteInfo(); spriteInfo.pwdId = ReadInt16(br) + 1; spriteInfo.spriteId = ReadInt16(br) - 1; spriteInfo.x = ReadInt16(br)/100f; spriteInfo.y = 1 - ReadInt16(br)/100f; frameInfo.frameSpriteInfo[j] = spriteInfo; } aefInfo.frameInfo[i] = frameInfo; } } } return aefInfo; }
这里需要注意,我们是使用
SpriteRenderer
组件来渲染图像,世界空间下的坐标是像素坐标的100
倍,所以这里算坐标的时候除以100f
。
6.3、封装GeneratePreabByAEF方法
最后,我们封装一个GeneratePreabByAEF
,去扫描aef
文件,调用FileReader.ReadAEF
,得到AEFInfo
对象,再根据AEFInfo
对象去生成预设文件,如下
[MenuItem("工具/通过AEF生成预设")] public static void GeneratePreabByAEF() { // 扫描AEF文件 var aefFilePaths = Directory.GetFiles(Application.dataPath + "/NPC/", "*.aef", SearchOption.AllDirectories); foreach (var aefFilePath in aefFilePaths) { // 解析AEF文件 AEFInfo aefInfo = FileReader.ReadAEF(aefFilePath); // 根据AEF信息生成动画预设文件 SaveAniPrefab(aefFilePath, aefInfo); } }
6.4、封装SaveAniPrefab方法
其中,生成预设的方法SaveAniPrefab
如下,原理就是动态生成GameObject
,动态挂脚本,设置成员,最后使用PrefabUtility.SaveAsPrefabAsset
方法把GameObject
保存为预设,
/// <summary> /// 根据AEF信息生成动画预设文件 /// </summary> private static void SaveAniPrefab(string aefFile, AEFInfo aefInfo) { // 前缀 var aefName = Path.GetFileNameWithoutExtension(aefFile); var prefix = aefName.Substring(0, aefName.IndexOf("_")); var eafDir = Path.GetDirectoryName(aefFile); var spriteDir = eafDir.Replace('\\', '/') + "/sprites/"; var spriteAssetDir = spriteDir.Replace(Application.dataPath, "Assets/"); var aniObj = new GameObject("ani_" + aefName); var aniRuntime = aniObj.AddComponent<AniRuntime>(); aniRuntime.frameObjs = new GameObject[aefInfo.frameCnt]; foreach (var frame in aefInfo.frameInfo) { // 创建帧 var frameObj = new GameObject("frame_" + frame.frameId); frameObj.transform.SetParent(aniObj.transform, false); foreach (var spriteInfo in frame.frameSpriteInfo) { // 一帧可能由多张图片组成,这里取去生成一帧中的图片 var spriteObj = new GameObject("sprite_" + spriteInfo.spriteId); var renderer = spriteObj.AddComponent<SpriteRenderer>(); var sprPath = spriteAssetDir + prefix + "_" + spriteInfo.pwdId + "_" + spriteInfo.spriteId + ".png"; var spriteRes = AssetDatabase.LoadAssetAtPath<Sprite>(sprPath); if (null == spriteRes) { Debug.LogError("缺少资源:" + sprPath + "\n请检查PWD文件生成PNG的步骤是否正常"); } renderer.sprite = spriteRes; spriteObj.transform.SetParent(frameObj.transform, false); spriteObj.transform.localPosition = new Vector3(spriteInfo.x, spriteInfo.y, 0); } if (frame.frameId >= 0 && frame.frameId < aefInfo.frameCnt) aniRuntime.frameObjs[frame.frameId] = frameObj; else Debug.LogError("Illegal frameId: " + frame.frameId); frameObj.SetActive(frame.frameId == 0); } aniObj.transform.localPosition = new Vector3(0, -6.5f, 0); aniObj.transform.localScale = Vector3.one * 5; // 生成预设 var prefabDir = Application.dataPath + "/Prefabs/"; if (!Directory.Exists(prefabDir)) { Directory.CreateDirectory(prefabDir); } prefabDir = prefabDir.Replace(Application.dataPath, "Assets/"); PrefabUtility.SaveAsPrefabAsset(aniObj, prefabDir + aniObj.name + ".prefab"); GameObject.DestroyImmediate(aniObj); }
7、编写运行时脚本:AniRuntime.cs
创建一个AniRuntime.cs
脚本,用于运行时执行序列帧的显示,
这里我只是简单的对序列帧进行隐藏和激活,纯粹作为演示,实际项目中不建议这么做,
using UnityEngine; public class AniRuntime : MonoBehaviour { [SerializeField] public GameObject[] frameObjs; public float frameInterval = 0.1f; private float timer; private int curFrame; void Update() { timer += Time.deltaTime; if (timer >= frameInterval) { timer = 0; ++curFrame; if (curFrame >= frameObjs.Length) { curFrame = 0; } for (int i = 0; i < frameObjs.Length; ++i) { if(null != frameObjs[i]) frameObjs[i].SetActive(curFrame == i); } } } }
8、执行菜单生成预设文件
点击菜单工具 / 通过AEF生成预设
,生成预设文件,如下,
生成的预设文件的子节点是按帧来分组的,
一帧里面有n
张小图,如下,
9、运行测试动画
我们把预设拖到场景中,运行Unity
,效果如下,
我们丢一些其他怪物的pwd
和aef
文件到工程中,生成预设,运行预览效果如下,
五、工程源码
本文工程我已上传到GitCode
,感兴趣的同学可自行下载学习,
地址:https://gitcode.net/linxinfa/UnityXunqinOL
注:我使用的Unity版本是2021.1.7.f1c1
六、完毕
好了,就写到这里吧。
我是新发,https://blog.csdn.net/linxinfa
一个在小公司默默奋斗的Unity
开发者,希望可以帮助更多想学Unity
的人,共勉~
这篇关于【游戏开发实战】Unity逆向怀旧经典游戏《寻秦OL》,解析二进制动画文件生成预设并播放(资源逆向 | 二进制 | C#)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2022-03-01沐雪多租宝商城源码从.NetCore3.1升级到.Net6的步骤
- 2024-12-06使用Microsoft.Extensions.AI在.NET中生成嵌入向量
- 2024-11-18微软研究:RAG系统的四个层次提升理解与回答能力
- 2024-11-15C#中怎么从PEM格式的证书中提取公钥?-icode9专业技术文章分享
- 2024-11-14云架构设计——如何用diagrams.net绘制专业的AWS架构图?
- 2024-05-08首个适配Visual Studio平台的国产智能编程助手CodeGeeX正式上线!C#程序员必备效率神器!
- 2024-03-30C#设计模式之十六迭代器模式(Iterator Pattern)【行为型】
- 2024-03-29c# datetime tryparse
- 2024-02-21list find index c#
- 2024-01-24convert toint32 c#