【遊戲開發實戰】Unity逆向懷舊經典遊戲《尋秦OL》,解析二進制動畫文件生成預設並播放(資源逆向 | 二進制 | C#)

林新發 2022-01-08 05:46:07 阅读数:664

unity 逆向 ol 解析 文件

一、前言

嗨,大家夥,我是新發。
有同學私信並給我發了封郵件,內容如下:

郵件內容:
林新發大哥你好,我叫**,是個四川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、二進制讀取: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、字節序問題:大端小端

上面代碼中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的人,共勉~

版权声明:本文为[林新發]所创,转载请带上原文链接,感谢。 https://gsmany.com/2022/01/202201080546063743.html