1. <dd id="erndk"></dd>
                1. 【游戲開發實戰】Unity逆向懷舊經典游戲《尋秦OL》,解析二進制動畫文件生成預設并播放(資源逆向 | 二進制 | C#)

                  互聯網 2022/1/4 1:08:11

                  文章目錄 一、前言二、資源文件說明1、二進制文件(pwd文件、aef文件)2、數據格式2.1、pwd格式2.2、aef格式三、C#讀取二進制文件的API1、打開二進制文件:FileStream文件流2、二進制讀?。築inaryReader3、字節序問題:大端小端 四、實戰1、創建Unity工程2、導入pwd和aef…

                  文章目錄

                      • 一、前言
                      • 二、資源文件說明
                        • 1、二進制文件(pwd文件、aef文件)
                        • 2、數據格式
                          • 2.1、pwd格式
                          • 2.2、aef格式
                      • 三、C#讀取二進制文件的API
                        • 1、打開二進制文件:FileStream文件流
                        • 2、二進制讀?。築inaryReader
                        • 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中去解析pwdaef文件,就要用到讀取二進制文件的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作用域時會自動幫助我們釋放流所占用的資源,否則我們需要手動調用FileStreamDispose方法來釋放資源。

                  2、二進制讀?。築inaryReader

                  上面我們得到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、字節序問題:大端小端

                  上面代碼中ReadInt16ReadInt32需要特別小心字節序問題,什么是字節序呢?為什么要搞字節序這個東西呢?我來給你講清楚。
                  我們的計算機內存是以字節為存儲單元的,畫個圖,
                  在這里插入圖片描述
                  我們知道,一個short2個字節,一個int4個字節,現在我問你,假設用0x000000000x00000001這兩個地址對應的2個字節來表示一個short,那么這個short的值是多少?
                  在這里插入圖片描述
                  你可能會回答0x1C09,因為低地址是0x09,高地址是0x1C,組合起來就是0x1C09,轉為十進制就是7177,
                  在這里插入圖片描述

                  但是,為什么不能是0x091C呢?誰規定高地址就是高位,低地址就一定是低位呢?
                  這個,就是字節序問題。
                  如果是高地址放高位,低地址放低位,就是小端字節序,這個符合我們人類的思維習慣。(口訣:高高低低為小端)。
                  反過來就是大端字節序。雖然說小端字節序符合人類的思維習慣,但卻反而不直觀,為什么?比如下面這個二進制文件,我圈出來的4個字節的值你是不是第一反應是0x00000065(大端字節序),如果你真按小端字節序來思考的話,應該是0x65000000,因為0x65的地址是最高的,按小端字節序的話0x65是放在最高位。不過,這里的二進制文件是按大端字節序存儲的,所以答案是0x00000065。
                  在這里插入圖片描述
                  現在問題又來了,我們如果使用BinaryReaderReadInt32()方法一次性讀取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文件

                  NPCpwdaef導入工程目錄中,比如導入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文件ID0,
                  在這里插入圖片描述
                  接下來是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軸朝下的,與Unityy軸方向是相反的,所以讀取像素的時候要使用高度減去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,效果如下,
                  請添加圖片描述
                  我們丟一些其他怪物的pwdaef文件到工程中,生成預設,運行預覽效果如下,
                  請添加圖片描述
                  請添加圖片描述

                  五、工程源碼

                  本文工程我已上傳到GitCode,感興趣的同學可自行下載學習,
                  地址:https://gitcode.net/linxinfa/UnityXunqinOL
                  注:我使用的Unity版本是2021.1.7.f1c1
                  在這里插入圖片描述

                  六、完畢

                  好了,就寫到這里吧。
                  我是新發,https://blog.csdn.net/linxinfa
                  一個在小公司默默奮斗的Unity開發者,希望可以幫助更多想學Unity的人,共勉~

                  隨時隨地學軟件編程-關注百度小程序和微信小程序
                  關于找一找教程網

                  本站文章僅代表作者觀點,不代表本站立場,所有文章非營利性免費分享。
                  本站提供了軟件編程、網站開發技術、服務器運維、人工智能等等IT技術文章,希望廣大程序員努力學習,讓我們用科技改變世界。
                  [【游戲開發實戰】Unity逆向懷舊經典游戲《尋秦OL》,解析二進制動畫文件生成預設并播放(資源逆向 | 二進制 | C#)]http://www.yachtsalesaustralia.com/tech/detail-279708.html

                  贊(0)
                  關注微信小程序
                  程序員編程王-隨時隨地學編程

                  掃描二維碼或查找【程序員編程王】

                  可以隨時隨地學編程啦!

                  技術文章導航 更多>
                  国产在线拍揄自揄视频菠萝

                        1. <dd id="erndk"></dd>