Unity复刻尼尔:机械纪元-动作整理分析
申明:本文为笔记,仅供学习交流,严禁用于商业用途。
一、工具准备
- Bayonetta Audio Tools:尼尔.dat文件解包打包软件,后面需要用到再说明,下载地址:https://pan.baidu.com/s/1nL6pDJ5-iKS5fXy944puQg?pwd=4681
二、重新命名文件(十六进制转十进制)
- 我们会发现动作数据mot文件名称是用十六进制命名的,
很难阅读而且在软件中查看也是乱序的,所以我们现在把mot文件重新命名为十进制方便我们后续使用。
- 使用Bayonetta Audio Tools工具,拖拽pl000f.dat文件到Bayonetta Audio Tools工具下的PlatinumDat.exe上面,软件就会解包.dat数据。解包后我们会得到一批mot、bxm文件
- 编写代码对文件名称进行批量转换
[MenuItem("尼尔工具箱/重新命名文件(十六进制转十进制)")]
public static void RenameFile()
{
string _path = EditorUtility.SaveFolderPanel("选择文件夹", "F:/NieRData/pl/", "");
string _newPath = _path + "_new";
if (!Directory.Exists(_newPath))
{
Directory.CreateDirectory(_newPath);
}
string[] _files = System.IO.Directory.GetFiles(_path);
foreach (var _filePath in _files)
{
var _extension = Path.GetExtension(_filePath);
var _fileNameWithoutExtension = Path.GetFileNameWithoutExtension(_filePath);
var _newFileName = _fileNameWithoutExtension;
string[] _tmp = _fileNameWithoutExtension.Split(_);
if (_tmp.Length >= 2)
{
//进制转换
int _id = Convert.ToInt32(_tmp[1], 16);
//保留五位数,列如:00001
var _format = string.Format("{0:d5}", _id);
_tmp[1] = _format;
_newFileName = _tmp[0];
for (int i = 1; i < _tmp.Length; i++)
{
_newFileName += "_" + _tmp[i];
}
var _srcfileName = _newPath + "/" + _newFileName + _extension;
File.Copy(_filePath, _srcfileName);
}
else
{
var _srcfileName = _newPath + "/" + _newFileName + _extension;
File.Copy(_filePath, _srcfileName);
}
}
EditorUtility.DisplayDialog("", "处理完成!", "OK");
}
- 得到我们想要的数据
这些数据继续保留着后面还要使用,再把模型命名后的文件夹pl000f_new拖拽到Bayonetta Audio Tools工具下的PlatinumDat.exe上面,软件会重新把数据打包为.dat文件pl000f_new.dat
三、自动拆分FBX动画片段
- 导入到unity里后我们会发现所有动作在一个Clips里,我们还得手动拆分太麻烦了,上面我们已经得到.dat数据解包后已经重新命名过的xxx.mot文件,.mot就是包含动作数据的,现在我们要解析.mot文件生成我们要的动画数据,最后自动化拆分。
- .mot文件头数据结构
struct {
char id[4]; // "mot\0"
uint32 hash;
uint16 flag;
int16 frameCount;
uint32 recordOffset;
uint32 recordNumber;
uint32 unknown; // usually 0 or 0x003c0000, maybe two uint16
string animName; // found at most 12 bytes with terminating 0
}
- 解析mot文件数据生成动画帧数据文件
[MenuItem("尼尔工具箱/解析mot文件生成数据")]
public static void ReadMotFile()
{
string _path = EditorUtility.SaveFolderPanel("选择文件夹", "F:/NieRData/pl/", "");
var _directoryName = Path.GetFileName(_path);
int _firstFrame = 1;
int _lastFrame = _firstFrame - 1;
List<string> _list = new List<string>();
string[] _files = System.IO.Directory.GetFiles(_path);
foreach (var _filePath in _files)
{
var _extension = Path.GetExtension(_filePath);
var _fileNameWithoutExtension = Path.GetFileNameWithoutExtension(_filePath);
/*
mot文件头结构
struct {
char id[4]; // "mot\0"
uint32 hash;
uint16 flag;
int16 frameCount;
uint32 recordOffset;
uint32 recordNumber;
uint32 unknown; // usually 0 or 0x003c0000, maybe two uint16
string animName; // found at most 12 bytes with terminating 0
}
*/
if (_extension.Equals(".mot"))
{
var _fByte = File.ReadAllBytes(_filePath);
//读取动画片段总帧数
var _frameCount = BitConverter.ToInt16(_fByte, 10);
_lastFrame += _frameCount;
_list.Add($"{_firstFrame}|{_lastFrame}|{_fileNameWithoutExtension}");
_firstFrame += _frameCount;
}
}
File.WriteAllLines(_path + $"/{_directoryName}.txt", _list);
EditorUtility.DisplayDialog("", "解析成功!", "OK");
}
- 读取帧数据自动拆分FBX动画数据
/// <summary>
/// Clip数据
/// </summary>
public class FbxClipData
{
public string mName;
public int mFirstFrame;
public int mLastFrame;
}
[MenuItem("尼尔工具箱/自动拆分FBX动画片段")]
public static void SplitFbxClip()
{
if (EditorUtility.DisplayDialog("自动拆分FBX动画片段", "确认是否已在Project窗口下选中了要处理的FBX", "确定", "取消"))
{
UnityEngine.Object[] _objects = Selection.GetFiltered(typeof(UnityEngine.Object), SelectionMode.DeepAssets);
int _count = 0;
int _objectsLength = _objects.Length;
foreach (var obj in _objects)
{
_count++;
string _assetPath = AssetDatabase.GetAssetPath(obj);
if (Path.GetExtension(_assetPath)?.ToLower() != ".fbx")
{
continue;
}
EditorUtility.DisplayProgressBar("正在处理,请稍后", _assetPath, (float) (_count) / _objectsLength);
List<FbxClipData> _fbxClipDataList = new List<FbxClipData>();
string _extension = Path.ChangeExtension(_assetPath, "txt");
if (!File.Exists(_extension))
{
Debug.LogError(_extension + " 数据文件不存在!");
continue;
}
if (string.IsNullOrEmpty(_extension))
{
continue;
}
StreamReader _reader = new StreamReader(_extension);
string _line;
while ((_line = _reader.ReadLine()) != null)
{
string[] _info = _line.Split(|);
FbxClipData _fbxClipData = new FbxClipData
{
mFirstFrame = int.Parse(_info[0]),
mLastFrame = int.Parse(_info[1]),
mName = _info[2]
};
_fbxClipDataList.Add(_fbxClipData);
}
_reader.Close();
ModelImporter _modelImporter = AssetImporter.GetAtPath(_assetPath) as ModelImporter;
ArrayList _clipsList = new ArrayList();
for (int i = 0; i < _fbxClipDataList.Count; i++)
{
ModelImporterClipAnimation _clipAnimation = new ModelImporterClipAnimation
{
name = _fbxClipDataList[i].mName,
firstFrame = _fbxClipDataList[i].mFirstFrame,
lastFrame = _fbxClipDataList[i].mLastFrame
};
_clipsList.Add(_clipAnimation);
}
if (_modelImporter != null)
{
_modelImporter.clipAnimations =
(ModelImporterClipAnimation[]) _clipsList.ToArray(typeof(ModelImporterClipAnimation));
_modelImporter.SaveAndReimport();
}
}
AssetDatabase.Refresh();
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog("提示", "处理完成", "OK");
}
}
最后就可以得到拆分好的动作片段的数据了
四、数据整理分析
- 尼尔资源格式解析:
- 文件夹:
- pl: 主角模型和动作 wd:场景 wp:武器、 um:NPC、 em:敌人
- 文件类型:
- .dtt:模型和贴图 .dat:动画
- 基本操作:
- 弱攻击、强攻击、冲刺、跳跃、二段跳
- 跳跃+强攻击 = 上挑
- 长按弱|强攻击键 = 特殊攻击
- 冲刺状态下+强攻击 = 冲刺攻击
- 冲刺+弱攻击 = 旋转攻击
- 空中状态+强攻击 = 下劈
- 空中状态+冲刺+强攻击 = 斜向下下劈
- 闪避后攻击+弱攻击|强攻击 = 闪避攻击
角色动作id整理:
在查看武器动画时会发现武器种类是有划分id的,而且人物动画和武器动画id是有匹配关系的
- 通用动作: 动画id:[0~256) 十六进制:[0~99)
- 小型剑000: 动画id:[256~512) 十六进制:[100~200)
- 大型剑200: 动画id:[512~768) 十六进制:[200~300)
- 抢400: 动画id:[768~1024) 十六进制:[300~400)
- 格斗600: 动画id:[1024~1280) 十六进制:[400~500)
- 空手: 动画id:[1280~1536) 十六进制:[500~600)
- 小型剑连招:
- 弱连击:动作id【00256、00257、00258、00259、00260、00261、00262】
- 弱长按:动作id【】
- 强连击:动作id【00272、00275、00278】
- 强长按:动作id【】
- 抢连招:
- 弱连击:动作id【00768、00769、00770、00771、00772、00773】
五、最后把角色动画和武器动画匹配上
参考: