1971 lines
78 KiB
Markdown
1971 lines
78 KiB
Markdown
![]() |
#unity/白日梦/代码缓存
|
|||
|
|
|||
|
```cs
|
|||
|
using System;
|
|||
|
using System.Collections.Generic;
|
|||
|
using UnityEngine;
|
|||
|
using UnityEditor;
|
|||
|
using System.IO;
|
|||
|
using System.Text;
|
|||
|
using System.Linq;
|
|||
|
using System.Text.RegularExpressions;
|
|||
|
using ResourcesModule.LMNA;
|
|||
|
|
|||
|
namespace SkinToolModule.M.Editor
|
|||
|
{
|
|||
|
public struct LODDynamicColliderData
|
|||
|
{
|
|||
|
public Transform colTrans;
|
|||
|
public string name;
|
|||
|
public float radius;
|
|||
|
public string rootName;
|
|||
|
|
|||
|
public LODDynamicColliderData(string n, float r = 0.3f, string rn = "", Transform c = null)
|
|||
|
{
|
|||
|
name = n;
|
|||
|
radius = r;
|
|||
|
rootName = rn;
|
|||
|
colTrans = c;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
public struct LODDynamicBoneParams
|
|||
|
{
|
|||
|
public Transform root;
|
|||
|
public float Damping;
|
|||
|
public float Elasticty;
|
|||
|
public float Stiffness;
|
|||
|
public float Inert;
|
|||
|
public float Friction;
|
|||
|
public float Radius;
|
|||
|
public List<LODDynamicColliderData> colliderDataList;
|
|||
|
public LODDynamicBoneParams(Transform t, float d = 0.02f, float e = 0.08f, float s = 0.06f, float i = 0.8f, float f = 0.6f, float r = 0.0f, List<LODDynamicColliderData> colliders = null)
|
|||
|
{
|
|||
|
root = t;
|
|||
|
Damping = d;
|
|||
|
Elasticty = e;
|
|||
|
Stiffness = s;
|
|||
|
Inert = i;
|
|||
|
Friction = f;
|
|||
|
Radius = r;
|
|||
|
colliderDataList = colliders;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
public class SkinToolEditor : EditorWindow
|
|||
|
{
|
|||
|
private enum MatType
|
|||
|
{
|
|||
|
Character,
|
|||
|
Transparent,
|
|||
|
Fakeskin,
|
|||
|
Fur,
|
|||
|
Error,
|
|||
|
}
|
|||
|
|
|||
|
private static SkinToolEditor m_Window;
|
|||
|
public const string m_SuiteLevelPath = "Assets/Editor/ArtResources/Tool/Config/SuiteLevel.txt";
|
|||
|
private const string m_DefaultMatPath = "Assets/Editor/ArtResources/Tool/Config/DefaultMaterial.mat";
|
|||
|
private const string m_FbxDirIng = "/ResourcesRaw/CommonRegion/InGame/Skins/Characters/";
|
|||
|
private const string m_FbxDirLob = "/ResourcesRaw/CommonRegion/OutsideGame/Skins/Characters/";
|
|||
|
private const string m_PrefabDirIng = "Assets/BundleResources/CommonRegion/InGame/Skins/Characters/";
|
|||
|
private const string m_PrefabDirLob = "Assets/BundleResources/CommonRegion/OutsideGame/Skins/Characters/";
|
|||
|
private const string m_DynamicBoneParamDir = "Assets/Editor/ArtResources/Tool/LODDynamicBoneParams";
|
|||
|
private string m_OldFBXPath = "Assets/ResourcesRaw/CommonRegion/Skins/Characters/Models/";
|
|||
|
|
|||
|
|
|||
|
private bool m_IsLobby;
|
|||
|
private bool m_IsOld; // 是否是重导的减面资源
|
|||
|
private GameObject m_OldPrefab;
|
|||
|
private FileInfo m_FbxFile;
|
|||
|
private FileInfo[] m_TgaFiles;
|
|||
|
private string m_FolderPath = "";
|
|||
|
private string m_FbxPath = "";
|
|||
|
private string m_PrefabPath = "";
|
|||
|
private string LOD_m_PrefabPath = "";
|
|||
|
private string m_DynamicBonePrefabPath;
|
|||
|
|
|||
|
|
|||
|
|
|||
|
private Dictionary<string,eAssetLodLevel> levelDic = new Dictionary<string, eAssetLodLevel>
|
|||
|
{
|
|||
|
["lv0"] = eAssetLodLevel.Lv0,
|
|||
|
["lv1"] = eAssetLodLevel.Lv1,
|
|||
|
["lv2"] = eAssetLodLevel.Lv2,
|
|||
|
["lv3"] = eAssetLodLevel.Lv3,
|
|||
|
["lv4"] = eAssetLodLevel.Lv4
|
|||
|
};
|
|||
|
|
|||
|
private Dictionary<string, eAssetSceneType> sceneDic = new Dictionary<string, eAssetSceneType>
|
|||
|
{
|
|||
|
["lob"] = eAssetSceneType.Lobby,
|
|||
|
["ing"] = eAssetSceneType.InGame,
|
|||
|
};
|
|||
|
|
|||
|
private eAssetSceneType currentAssetSceneType = eAssetSceneType.Unknown;
|
|||
|
private eAssetLodLevel currentAssetLodLevel = eAssetLodLevel.None;
|
|||
|
|
|||
|
private Dictionary<int,FileInfo> mFbxFileList = new Dictionary<int,FileInfo>();
|
|||
|
private Dictionary<int, string> mFolderPathList = new Dictionary<int, string>();
|
|||
|
private Dictionary<int,string> mFbxPathList = new Dictionary<int,string>();
|
|||
|
private Dictionary<int,string> mPrefabPathList = new Dictionary<int,string>();
|
|||
|
|
|||
|
|
|||
|
// 品质相关
|
|||
|
public enum SuiteLevel {
|
|||
|
white = 0,
|
|||
|
blue = 1,
|
|||
|
purple = 2,
|
|||
|
orangee = 3,
|
|||
|
pink = 4,
|
|||
|
};
|
|||
|
private SuiteLevel m_SuiteLevel = SuiteLevel.white;
|
|||
|
private GameObject m_FbxSenceObject;
|
|||
|
|
|||
|
// Dynamic bone相关
|
|||
|
private GameObject m_DynamicBoneSenceObject;
|
|||
|
Dictionary<Transform, string> m_TaggedTransformDict = new Dictionary<Transform, string>(); // 被标记的骨骼节点 - 对应父节点名字
|
|||
|
Dictionary<string, LODDynamicBoneParams> m_TagToDynamicBoneParamDict = new Dictionary<string, LODDynamicBoneParams>(); // 被标记的骨骼节点名字 - 对应DynamicBone参数组
|
|||
|
List<Transform> m_SkinPartTransformList = new List<Transform>(); // 部位
|
|||
|
// 眨眼相关
|
|||
|
string BlinkFaceTag = "head_fx_01";
|
|||
|
Dictionary<string, string[]> m_BlinkFaceDict = new Dictionary<string, string[]> {
|
|||
|
{ "Thief_08", new string[]{"Thief_08_head_OpenEyes_01_height", "Thief_08_head_CloseEyes_01_height"} },
|
|||
|
{ "Police_05", new string[]{ "Police_05_head_OpenEyes_01_height", "Police_05_head_CloseEyes_01_height" } },
|
|||
|
};
|
|||
|
// 界面相关
|
|||
|
Color m_BlueColor = new Color(129 / 255f, 209 / 255f, 248 / 255f, 1);
|
|||
|
Color m_PinkColor = new Color(253 / 255f, 184 / 255f, 225 / 255f, 1);
|
|||
|
Color m_RedColor = new Color(233 / 255f, 74 / 255f, 110 / 255f, 1);
|
|||
|
Color m_PurpColor = new Color(125 / 255f, 128 / 255f, 227 / 255f, 1);
|
|||
|
float m_RowHeight = 20f;
|
|||
|
int m_RowFontSize = 12;
|
|||
|
GUIStyle m_MainHeaderStyle;
|
|||
|
GUIStyle m_SubHeaderStyle;
|
|||
|
GUIStyle m_ButtonStyle;
|
|||
|
GUIStyle m_TipsStyle;
|
|||
|
GUIStyle m_ErrorTipsStyle;
|
|||
|
GUIStyle m_LabelStyle;
|
|||
|
GUIStyle m_TagLabelStyle;
|
|||
|
GUIStyle m_ParamsLabelStyle;
|
|||
|
|
|||
|
private string[] ModelName = new[]
|
|||
|
{
|
|||
|
"clothes",
|
|||
|
"hand",
|
|||
|
"head",
|
|||
|
"shoes",
|
|||
|
"trousers",
|
|||
|
};
|
|||
|
|
|||
|
private string[] DynamicBoneTag = new[]
|
|||
|
{
|
|||
|
"CLOTHES_",
|
|||
|
"HEAD_",
|
|||
|
"HAND_",
|
|||
|
"SHOES_",
|
|||
|
"TROUSERS_"
|
|||
|
};
|
|||
|
|
|||
|
[UnityEditor.MenuItem("美术工具/导入时装资源", false, 1)]
|
|||
|
static void ShowWindow()
|
|||
|
{
|
|||
|
if (m_Window == null)
|
|||
|
{
|
|||
|
m_Window = (SkinToolEditor)EditorWindow.GetWindow(typeof(SkinToolEditor), true, "SkinToolEditor");
|
|||
|
m_Window.Show();
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
m_Window.Close();
|
|||
|
m_Window = null;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
#region GUI
|
|||
|
|
|||
|
void OnGUI()
|
|||
|
{
|
|||
|
InitStyles();
|
|||
|
EditorGUILayout.BeginVertical();
|
|||
|
// Title
|
|||
|
EditorGUILayout.BeginHorizontal();
|
|||
|
EditorGUILayout.LabelField("Bunny Editor", m_MainHeaderStyle, GUILayout.Height(30));
|
|||
|
if (GUILayout.Button("清除", m_ButtonStyle, GUILayout.Width(150), GUILayout.Height(m_RowHeight)))
|
|||
|
{
|
|||
|
if (EditorUtility.DisplayDialog("警告", "确定要清除本次生成的所有资源么?", "删就完事儿了"))
|
|||
|
{
|
|||
|
OnClickClearButton();
|
|||
|
}
|
|||
|
}
|
|||
|
// if (GUILayout.Button(m_IsOld?"当前:老资源合入": "当前:新资源合入", m_ButtonStyle, GUILayout.Width(250), GUILayout.Height(m_RowHeight)))
|
|||
|
// {
|
|||
|
// m_IsOld = !m_IsOld;
|
|||
|
// }
|
|||
|
EditorGUILayout.EndHorizontal();
|
|||
|
EditorGUILayout.Space();
|
|||
|
DrawSplitLine(25);
|
|||
|
|
|||
|
// Load fbx
|
|||
|
if (GUILayout.Button("选fbx所在文件夹", m_ButtonStyle, GUILayout.Width(150), GUILayout.Height(m_RowHeight)))
|
|||
|
{
|
|||
|
OnClickCreateButton(OnClickLoadButton());
|
|||
|
}
|
|||
|
|
|||
|
EditorGUILayout.Space();
|
|||
|
// Show FolderPath
|
|||
|
// if (!string.IsNullOrEmpty(m_FolderPath))
|
|||
|
// {
|
|||
|
// if (!Directory.Exists(m_FolderPath)&&!m_IsOld)
|
|||
|
// {
|
|||
|
// EditorGUILayout.TextArea("地址已存在:\n" + m_FolderPath, m_TipsStyle);
|
|||
|
// }
|
|||
|
// else if(!Directory.Exists(m_FolderPath)&&m_IsOld)
|
|||
|
// {
|
|||
|
// EditorGUILayout.TextArea("导入完毕,资源地址为:\n" + m_OldFBXPath, m_TipsStyle);
|
|||
|
// }
|
|||
|
// else
|
|||
|
// {
|
|||
|
// EditorGUILayout.TextArea("导入完毕,资源地址为:\n" + m_FolderPath, m_TipsStyle);
|
|||
|
// }
|
|||
|
// }
|
|||
|
// else
|
|||
|
// {
|
|||
|
// EditorGUILayout.TextArea("无资源导入...", m_TipsStyle);
|
|||
|
// }
|
|||
|
|
|||
|
EditorGUILayout.Space();
|
|||
|
|
|||
|
// Select Suite Level
|
|||
|
// m_SuiteLevel = (SuiteLevel)EditorGUILayout.EnumPopup("选择时装品质", m_SuiteLevel, GUILayout.Width(300));
|
|||
|
|
|||
|
// EditorGUILayout.Space();
|
|||
|
// Create Prefab without Dynamic bone
|
|||
|
// if (GUILayout.Button("生成模型", m_ButtonStyle, GUILayout.Width(150), GUILayout.Height(m_RowHeight)))
|
|||
|
// {
|
|||
|
// OnClickCreateButton();
|
|||
|
// }
|
|||
|
|
|||
|
EditorGUILayout.Space();
|
|||
|
// show prefab path
|
|||
|
if (!string.IsNullOrEmpty(LOD_m_PrefabPath))
|
|||
|
{
|
|||
|
EditorGUILayout.TextArea("创建Prefab完毕,资源地址为:\n" + LOD_m_PrefabPath, m_TipsStyle);
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
EditorGUILayout.TextArea("无Prefab创建...", m_TipsStyle);
|
|||
|
}
|
|||
|
|
|||
|
EditorGUILayout.Space();
|
|||
|
|
|||
|
// Put prefab in
|
|||
|
// EditorGUILayout.TextArea("部件模型路径", m_LabelStyle);
|
|||
|
// Rect modelRect = EditorGUILayout.GetControlRect(GUILayout.Width(500));
|
|||
|
// m_DynamicBonePrefabPath = EditorGUI.TextField(modelRect, m_DynamicBonePrefabPath);
|
|||
|
// EditorGUILayout.Space();
|
|||
|
// if ((Event.current.type == EventType.DragUpdated)
|
|||
|
// && modelRect.Contains(Event.current.mousePosition))
|
|||
|
// {
|
|||
|
// DragAndDrop.visualMode = DragAndDropVisualMode.Generic;
|
|||
|
// }
|
|||
|
//
|
|||
|
// if ((Event.current.type == EventType.DragPerform) && modelRect.Contains(Event.current.mousePosition))
|
|||
|
// {
|
|||
|
// if (DragAndDrop.paths != null && DragAndDrop.paths.Length > 0)
|
|||
|
// {
|
|||
|
// m_DynamicBonePrefabPath = DragAndDrop.paths[0];
|
|||
|
// }
|
|||
|
// }
|
|||
|
// // Analysis Dynamic bone params
|
|||
|
// if (GUILayout.Button("生成DynamicBone参数", m_ButtonStyle, GUILayout.Width(150),
|
|||
|
// GUILayout.Height(m_RowHeight)))
|
|||
|
// {
|
|||
|
// }
|
|||
|
// OnClickCreateLODDynamicBoneParams();
|
|||
|
|
|||
|
// EditorGUILayout.Space();
|
|||
|
// if (m_TagToDynamicBoneParamDict.Count > 0)
|
|||
|
// {
|
|||
|
// //在 foreach 中修改Dictionary中的值是不允许的,可以将key 先放在List中,foreach 这个list ,找到需要修改的项后,再修改原Dic中的内容
|
|||
|
// OnDrawListView();
|
|||
|
// EditorGUILayout.Space();
|
|||
|
// EditorGUILayout.BeginHorizontal();
|
|||
|
// // Create Prefab with Dynamic bone
|
|||
|
// if (GUILayout.Button("挂DynamicBone脚本", m_ButtonStyle, GUILayout.Width(200),
|
|||
|
// GUILayout.Height(m_RowHeight)))
|
|||
|
// {
|
|||
|
// OnClickDynamicBoneButton();
|
|||
|
// }
|
|||
|
//
|
|||
|
// if (GUILayout.Button("导入DynamicBone文件", m_ButtonStyle, GUILayout.Width(200),
|
|||
|
// GUILayout.Height(m_RowHeight)))
|
|||
|
// {
|
|||
|
// OnClickImportDynamicBoneFile();
|
|||
|
// }
|
|||
|
//
|
|||
|
// if (GUILayout.Button("导出DynamicBone文件", m_ButtonStyle, GUILayout.Width(200),
|
|||
|
// GUILayout.Height(m_RowHeight)))
|
|||
|
// {
|
|||
|
// OnClickExportDynamicBoneFile();
|
|||
|
// }
|
|||
|
//
|
|||
|
// EditorGUILayout.EndHorizontal();
|
|||
|
//
|
|||
|
// }
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// 初始化,清空数据
|
|||
|
/// </summary>
|
|||
|
void ClearData()
|
|||
|
{
|
|||
|
mFbxFileList.Clear();
|
|||
|
mFolderPathList.Clear();
|
|||
|
mFbxFileList.Clear();
|
|||
|
mPrefabPathList.Clear();
|
|||
|
}
|
|||
|
|
|||
|
void OnDestroy()
|
|||
|
{
|
|||
|
ClearScenePrefabs();
|
|||
|
}
|
|||
|
|
|||
|
void InitStyles()
|
|||
|
{
|
|||
|
if (m_MainHeaderStyle == null)
|
|||
|
{
|
|||
|
m_MainHeaderStyle = new GUIStyle(GUI.skin.label);
|
|||
|
m_MainHeaderStyle.fontSize = 20;
|
|||
|
m_MainHeaderStyle.fontStyle = FontStyle.Bold;
|
|||
|
m_MainHeaderStyle.normal.textColor = m_BlueColor;
|
|||
|
}
|
|||
|
|
|||
|
if (m_SubHeaderStyle == null)
|
|||
|
{
|
|||
|
m_SubHeaderStyle = new GUIStyle(GUI.skin.label);
|
|||
|
m_SubHeaderStyle.fontSize = 14;
|
|||
|
m_SubHeaderStyle.fontStyle = FontStyle.Bold;
|
|||
|
m_SubHeaderStyle.normal.textColor = m_BlueColor;
|
|||
|
}
|
|||
|
|
|||
|
if (m_ButtonStyle == null)
|
|||
|
{
|
|||
|
m_ButtonStyle = new GUIStyle(GUI.skin.button);
|
|||
|
m_ButtonStyle.fontSize = 14;
|
|||
|
}
|
|||
|
|
|||
|
if (m_TipsStyle == null)
|
|||
|
{
|
|||
|
m_TipsStyle = new GUIStyle(GUI.skin.label);
|
|||
|
m_TipsStyle.normal.textColor = m_PinkColor;
|
|||
|
m_TipsStyle.fontSize = m_RowFontSize;
|
|||
|
}
|
|||
|
|
|||
|
if (m_ErrorTipsStyle == null)
|
|||
|
{
|
|||
|
m_ErrorTipsStyle = new GUIStyle(GUI.skin.label);
|
|||
|
m_ErrorTipsStyle.normal.textColor = m_RedColor;
|
|||
|
m_ErrorTipsStyle.fontSize = m_RowFontSize;
|
|||
|
}
|
|||
|
|
|||
|
if (m_LabelStyle == null)
|
|||
|
{
|
|||
|
m_LabelStyle = new GUIStyle(GUI.skin.label);
|
|||
|
m_LabelStyle.normal.textColor = m_BlueColor;
|
|||
|
m_LabelStyle.fontSize = 18;
|
|||
|
}
|
|||
|
|
|||
|
if (m_TagLabelStyle == null)
|
|||
|
{
|
|||
|
m_TagLabelStyle = new GUIStyle(GUI.skin.label);
|
|||
|
m_TagLabelStyle.normal.textColor = m_PinkColor;
|
|||
|
m_TagLabelStyle.fontSize = 16;
|
|||
|
}
|
|||
|
|
|||
|
if (m_ParamsLabelStyle == null)
|
|||
|
{
|
|||
|
m_ParamsLabelStyle = new GUIStyle(GUI.skin.label);
|
|||
|
m_ParamsLabelStyle.normal.textColor = m_PurpColor;
|
|||
|
m_ParamsLabelStyle.fontSize = 13;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
void DrawSplitLine(float height)
|
|||
|
{
|
|||
|
GUI.Box(new Rect(0, height, position.width, 1), string.Empty);
|
|||
|
}
|
|||
|
|
|||
|
// 这里是scrollview
|
|||
|
Vector2 _scrollPos;
|
|||
|
|
|||
|
void OnDrawListView()
|
|||
|
{
|
|||
|
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos, false, false, GUILayout.Width(500), GUILayout.Height(400));
|
|||
|
List<string> keyList = new List<string>();
|
|||
|
foreach (KeyValuePair<string, LODDynamicBoneParams> pair in m_TagToDynamicBoneParamDict)
|
|||
|
{
|
|||
|
keyList.Add(pair.Key);
|
|||
|
}
|
|||
|
foreach (string key in keyList)
|
|||
|
{
|
|||
|
EditorGUILayout.TextArea(key, m_TagLabelStyle, GUILayout.Height(20));
|
|||
|
LODDynamicBoneParams inputValue;
|
|||
|
|
|||
|
float Damping = EditorGUILayout.FloatField("Damping", m_TagToDynamicBoneParamDict[key].Damping, m_ParamsLabelStyle);
|
|||
|
float Elasticty = EditorGUILayout.FloatField("Elasticty",
|
|||
|
m_TagToDynamicBoneParamDict[key].Elasticty, m_ParamsLabelStyle);
|
|||
|
float Stiffness = EditorGUILayout.FloatField("Stiffness",
|
|||
|
m_TagToDynamicBoneParamDict[key].Stiffness, m_ParamsLabelStyle);
|
|||
|
float Inert =
|
|||
|
EditorGUILayout.FloatField("Inert", m_TagToDynamicBoneParamDict[key].Inert, m_ParamsLabelStyle);
|
|||
|
float Friction =
|
|||
|
EditorGUILayout.FloatField("Friction", m_TagToDynamicBoneParamDict[key].Friction, m_ParamsLabelStyle);
|
|||
|
float Radius =
|
|||
|
EditorGUILayout.FloatField("Radius", m_TagToDynamicBoneParamDict[key].Friction, m_ParamsLabelStyle);
|
|||
|
EditorGUILayout.BeginHorizontal();
|
|||
|
if (GUILayout.Button("+", m_ButtonStyle, GUILayout.Width(50), GUILayout.Height(m_RowHeight)))
|
|||
|
{
|
|||
|
OnClickAddCollider(key);
|
|||
|
}
|
|||
|
if (GUILayout.Button("-", m_ButtonStyle, GUILayout.Width(50), GUILayout.Height(m_RowHeight)))
|
|||
|
{
|
|||
|
OnClickRemoveCollider(key);
|
|||
|
}
|
|||
|
EditorGUILayout.EndHorizontal();
|
|||
|
|
|||
|
if (m_TagToDynamicBoneParamDict[key].colliderDataList != null)
|
|||
|
{
|
|||
|
for (int i = 0; i < m_TagToDynamicBoneParamDict[key].colliderDataList.Count; i++)
|
|||
|
{
|
|||
|
EditorGUILayout.BeginHorizontal();
|
|||
|
LODDynamicColliderData inputColliderValue;
|
|||
|
// 列出这个标记位下绑到的所有collider信息
|
|||
|
List<LODDynamicColliderData> colliderList = m_TagToDynamicBoneParamDict[key].colliderDataList;
|
|||
|
LODDynamicColliderData collider = colliderList[i];
|
|||
|
EditorGUILayout.LabelField(collider.name, GUILayout.MaxWidth(80));
|
|||
|
float collRadius = EditorGUILayout.FloatField("碰撞体半径:", collider.radius, m_ParamsLabelStyle);
|
|||
|
EditorGUILayout.LabelField("关联骨骼名字:", GUILayout.MaxWidth(80));
|
|||
|
GUI.SetNextControlName("RootNameText");
|
|||
|
string collAttachName = EditorGUILayout.TextField(collider.rootName, m_ParamsLabelStyle,
|
|||
|
GUILayout.MaxWidth(200), GUILayout.Height(m_RowHeight));
|
|||
|
EditorGUILayout.EndHorizontal();
|
|||
|
// 改碰撞体list
|
|||
|
if (collRadius != collider.radius || collAttachName != collider.rootName)
|
|||
|
{
|
|||
|
inputColliderValue = new LODDynamicColliderData(collider.name, collRadius, collAttachName, collider.colTrans);
|
|||
|
colliderList[i] = inputColliderValue;
|
|||
|
inputValue = new LODDynamicBoneParams(null, Damping, Elasticty, Stiffness, // TODO: 这些地方要把null替换掉!
|
|||
|
Inert,
|
|||
|
Friction, Radius, colliderList);
|
|||
|
m_TagToDynamicBoneParamDict[key] = inputValue;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
// 只改Dynamicbone的数字参数,不涉及碰撞体list
|
|||
|
if (Damping != m_TagToDynamicBoneParamDict[key].Damping ||
|
|||
|
Elasticty != m_TagToDynamicBoneParamDict[key].Elasticty ||
|
|||
|
Stiffness != m_TagToDynamicBoneParamDict[key].Stiffness ||
|
|||
|
Inert != m_TagToDynamicBoneParamDict[key].Inert ||
|
|||
|
Friction != m_TagToDynamicBoneParamDict[key].Friction ||
|
|||
|
Radius != m_TagToDynamicBoneParamDict[key].Radius)
|
|||
|
{
|
|||
|
List<LODDynamicColliderData> colliderList = m_TagToDynamicBoneParamDict[key].colliderDataList;
|
|||
|
inputValue = new LODDynamicBoneParams(null, Damping, Elasticty, Stiffness, Inert,// TODO: 这些地方要把null替换掉!
|
|||
|
Friction, Radius, colliderList);
|
|||
|
m_TagToDynamicBoneParamDict[key] = inputValue;
|
|||
|
}
|
|||
|
EditorGUILayout.Space();
|
|||
|
}
|
|||
|
EditorGUILayout.EndScrollView(); // 组结束
|
|||
|
}
|
|||
|
#endregion
|
|||
|
|
|||
|
#region LoadFile
|
|||
|
|
|||
|
|
|||
|
bool OnClickLoadButton()
|
|||
|
{
|
|||
|
ClearData();
|
|||
|
|
|||
|
bool success = true;
|
|||
|
m_OldPrefab = null;
|
|||
|
string defaultFolder = PlayerPrefs.GetString("BunnyflopFilesDefalutFolder", "");
|
|||
|
string folder = EditorUtility.OpenFolderPanel("Select Versions Folder", defaultFolder, "");
|
|||
|
if (string.IsNullOrEmpty(folder))
|
|||
|
{
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
m_SuiteLevel = SuiteLevel.white;
|
|||
|
|
|||
|
PlayerPrefs.SetString("BunnyflopFilesDefalutFolder", folder);
|
|||
|
DirectoryInfo direction = new DirectoryInfo(folder);
|
|||
|
FileInfo[] fbxFiles = direction.GetFiles("*.fbx", SearchOption.AllDirectories);
|
|||
|
if (fbxFiles.Length == 0)
|
|||
|
{
|
|||
|
NamingChecker.PopupErrorMessage("未检测到有fbx导入!");
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
for (int i = 0; i < fbxFiles.Length; i++)
|
|||
|
{
|
|||
|
|
|||
|
m_FbxFile = fbxFiles[i];// 只允许载入一个
|
|||
|
mFbxFileList.Add(i,m_FbxFile);
|
|||
|
// if(m_IsOld)
|
|||
|
// {
|
|||
|
// // 重新导入性能优化使用的低模不检查命名规范,但要找到老的fbx,不然就报错退出
|
|||
|
// m_OldPrefab = FindOldPrefabByFbxFile(m_FbxFile.Name);
|
|||
|
// if(m_OldPrefab==null)
|
|||
|
// {
|
|||
|
// EditorUtility.DisplayDialog("警告", "项目内找不到名为"+m_FbxFile.Name+"的模型文件,请确认减面优化资源命名是否与老资源完全一致!", "我知道了");
|
|||
|
// return;
|
|||
|
// }
|
|||
|
// }
|
|||
|
// else
|
|||
|
// {
|
|||
|
// bool isPassCheck = NamingChecker.PreCheck(m_FbxFile);
|
|||
|
// if (!isPassCheck)
|
|||
|
// {
|
|||
|
// return;
|
|||
|
// }
|
|||
|
// }
|
|||
|
|
|||
|
m_IsLobby = !m_FbxFile.Name.Contains("Low") && !m_FbxFile.Name.Contains("low")&&!m_FbxFile.Name.Contains("&ing");
|
|||
|
success = LoadFbxFile(i);
|
|||
|
|
|||
|
if (success == false)
|
|||
|
{
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return success;
|
|||
|
}
|
|||
|
|
|||
|
GameObject FindOldPrefabByFbxFile(string fileName)
|
|||
|
{
|
|||
|
string fbxName = fileName.Split('.')[0];
|
|||
|
string[] allPath = AssetDatabase.FindAssets("t:Model", new string[] { "Assets/ResourcesRaw/CommonRegion/Skins/Characters/Models" });
|
|||
|
foreach(string singlePath in allPath)
|
|||
|
{
|
|||
|
string assetPath = AssetDatabase.GUIDToAssetPath(singlePath);
|
|||
|
if(assetPath.Contains(fbxName+".FBX"))
|
|||
|
m_OldFBXPath = assetPath.Replace(fbxName+".FBX","");
|
|||
|
if(assetPath.Contains(fbxName+".fbx"))
|
|||
|
m_OldFBXPath = assetPath.Replace(fbxName+".fbx","");
|
|||
|
if(!GetFbxName(assetPath).Equals(fbxName))
|
|||
|
{
|
|||
|
continue;
|
|||
|
}
|
|||
|
// 找到同名的fbx之后再找依赖fbx的prefab
|
|||
|
GameObject fbx = AssetDatabase.LoadAssetAtPath(assetPath, typeof(GameObject)) as GameObject;
|
|||
|
GameObject dependPrefab = GetPrefabDependByThisFbx(singlePath,fbxName);
|
|||
|
return dependPrefab;
|
|||
|
}
|
|||
|
return null;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
GameObject GetPrefabDependByThisFbx(string fbxGuid,string fbxName)
|
|||
|
{
|
|||
|
List<string> prefabExtension = new List<string>() { ".prefab" };
|
|||
|
string[] files = new string[10000];
|
|||
|
if(fbxName.Contains("low")||fbxName.Contains("&ing"))
|
|||
|
files = Directory.GetFiles(Application.dataPath + "/BundleResources/CommonRegion/InGame/Skins/Characters/", "*.*", SearchOption.AllDirectories)
|
|||
|
.Where(s => prefabExtension.Contains(Path.GetExtension(s).ToLower())).ToArray();
|
|||
|
if(fbxName.Contains("&lob")||fbxName.Contains("height"))
|
|||
|
files = Directory.GetFiles(Application.dataPath + "/BundleResources/CommonRegion/OutsideGame/Skins/Characters/", "*.*", SearchOption.AllDirectories)
|
|||
|
.Where(s => prefabExtension.Contains(Path.GetExtension(s).ToLower())).ToArray();
|
|||
|
string prefabFileName = "";
|
|||
|
foreach (string file in files)
|
|||
|
{
|
|||
|
if (Regex.IsMatch(File.ReadAllText(file), fbxGuid))
|
|||
|
{
|
|||
|
prefabFileName = file;
|
|||
|
}
|
|||
|
}
|
|||
|
if (string.IsNullOrEmpty(prefabFileName))
|
|||
|
{
|
|||
|
return null;
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
// 得到的filename是类似【G:/DMM_Art_B/Assets/BundleResources/Prefabs/Characters/SkinParts\Police_03_01_height_skin.prefab】这样的
|
|||
|
string[] words_1 = prefabFileName.Split('/');
|
|||
|
string[] words_2 = words_1[words_1.Length - 1].Split('\\'); ; // SkinParts\Police_03_01_height_skin.prefab
|
|||
|
string prefabName = words_2[words_2.Length-1];
|
|||
|
string prefabPath = "";
|
|||
|
string pathname = CombinePath(prefabName)+"/Prefabs/";
|
|||
|
if(fbxName.Contains("low")||fbxName.Contains("&ing"))
|
|||
|
prefabPath = Application.dataPath + "/BundleResources/CommonRegion/InGame/Skins/Characters/" +pathname+ prefabName;
|
|||
|
if(fbxName.Contains("&lob")||fbxName.Contains("height"))
|
|||
|
prefabPath = Application.dataPath + "/BundleResources/CommonRegion/OutsideGame/Skins/Characters/" +pathname+ prefabName;
|
|||
|
return AssetDatabase.LoadAssetAtPath(NamingChecker.GetRelativePath(prefabPath), typeof(GameObject)) as GameObject;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
string RenameFBX(string fbxName)
|
|||
|
{
|
|||
|
string newName = "";
|
|||
|
|
|||
|
|
|||
|
|
|||
|
return newName;
|
|||
|
|
|||
|
}
|
|||
|
bool LoadFbxFile(int index)
|
|||
|
{
|
|||
|
string fbxName = m_FbxFile.Name;
|
|||
|
// if (fbxName.Contains("height_") && !fbxName.Contains("lob"))
|
|||
|
// {
|
|||
|
// EditorUtility.DisplayDialog("错误", fbxName+"的模型文件命名不正确,缺少前缀lob或者多了height后缀!", "我知道了");
|
|||
|
// return false;
|
|||
|
// // fbxName = fbxName.Replace("height_", "").Replace("&", "&lob");
|
|||
|
// }
|
|||
|
// else if (fbxName.Contains("low_") && !fbxName.Contains("ing"))
|
|||
|
// {
|
|||
|
// EditorUtility.DisplayDialog("错误", fbxName+"的模型文件命名不正确,缺少前缀ing或者多了low后缀!", "我知道了");
|
|||
|
// return false;
|
|||
|
// // fbxName = fbxName.Replace("low_", "").Replace("&", "&ing");
|
|||
|
// }
|
|||
|
// if (!fbxName.Contains("mdl&"))
|
|||
|
// {
|
|||
|
// EditorUtility.DisplayDialog("警告", fbxName+"的模型文件命名不正确缺少前缀!", "我知道了");
|
|||
|
// return;
|
|||
|
// }
|
|||
|
|
|||
|
if(!m_IsOld)
|
|||
|
{
|
|||
|
|
|||
|
m_FolderPath = CreateFolder(fbxName);
|
|||
|
m_FbxPath = m_FolderPath + fbxName;
|
|||
|
|
|||
|
|
|||
|
mFolderPathList.Add(index, m_FolderPath);
|
|||
|
mFbxPathList.Add(index,m_FbxPath);
|
|||
|
|
|||
|
|
|||
|
if (File.Exists(m_FbxPath))
|
|||
|
{
|
|||
|
File.Delete(m_FbxPath);
|
|||
|
}
|
|||
|
|
|||
|
m_FbxFile.CopyTo(m_FbxPath);
|
|||
|
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
m_FolderPath = m_OldFBXPath;
|
|||
|
m_FbxPath = m_FolderPath+fbxName;
|
|||
|
if (File.Exists(m_FbxPath))
|
|||
|
{
|
|||
|
File.Delete(m_FbxPath);
|
|||
|
}
|
|||
|
m_FbxFile.CopyTo(m_FbxPath);
|
|||
|
}
|
|||
|
/*if (!Directory.Exists(m_FolderPath))
|
|||
|
{
|
|||
|
// NamingChecker.PopupErrorMessage("创建文件夹失败:" + m_FolderPath);
|
|||
|
}*/
|
|||
|
AssetDatabase.Refresh();
|
|||
|
// 如果是AnimPrefab则需要reimport一次,否则无法自动挂上Animation脚本(刚导入只能是Animator)
|
|||
|
if (fbxName.Contains(NamingChecker.AnimPattern))
|
|||
|
{
|
|||
|
ModelImporter mi = AssetImporter.GetAtPath(NamingChecker.GetRelativePath(m_FbxPath)) as ModelImporter;
|
|||
|
mi.SaveAndReimport();
|
|||
|
}
|
|||
|
// 这里就可以开始分析skinpart命名是否规范
|
|||
|
// if(m_IsOld)
|
|||
|
// {
|
|||
|
//
|
|||
|
// }
|
|||
|
// else
|
|||
|
// {
|
|||
|
// if (!NamingChecker.SkinpartCheck(GetFbxGameobject()))
|
|||
|
// {
|
|||
|
// Clear();
|
|||
|
// return;
|
|||
|
// }
|
|||
|
// }
|
|||
|
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
string LODCombinePath(string prefabName)
|
|||
|
{
|
|||
|
string realpath = String.Empty;
|
|||
|
string path = "SkinParts";
|
|||
|
string feature = "";
|
|||
|
string config = "";
|
|||
|
string[] splitname = prefabName.Split('_');
|
|||
|
|
|||
|
|
|||
|
if (prefabName.Contains("&ing") || prefabName.Contains("&lob"))
|
|||
|
{
|
|||
|
if (splitname.Length >= 4)
|
|||
|
{
|
|||
|
string[] nameList = splitname[0].Split('&');
|
|||
|
|
|||
|
feature = $"{nameList[1].Replace("lob","").Replace("ing","")}_{splitname[1]}";
|
|||
|
config = splitname[2];
|
|||
|
realpath = $"{feature}/{path}/{config}";
|
|||
|
return realpath;
|
|||
|
}
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
//兼容原版fbx导入
|
|||
|
if (splitname.Length > 4)
|
|||
|
{
|
|||
|
feature = $"{splitname[0].Replace("mdl&","")}_{splitname[1]}";
|
|||
|
config = splitname[2];
|
|||
|
}
|
|||
|
realpath = $"{feature}/{path}/{config}";
|
|||
|
}
|
|||
|
|
|||
|
return realpath;
|
|||
|
}
|
|||
|
|
|||
|
string CombinePath(string prefabName)
|
|||
|
{
|
|||
|
string realpath = String.Empty;
|
|||
|
string path = "SkinParts";
|
|||
|
string feature = "";
|
|||
|
string config = "";
|
|||
|
string[] splitname = prefabName.Split('_');
|
|||
|
if (prefabName.Contains("&ing") || prefabName.Contains("&lob"))
|
|||
|
{
|
|||
|
if (splitname.Length >= 4)
|
|||
|
{
|
|||
|
// feature = $"{splitname[1]}_{splitname[2]}";
|
|||
|
// config = splitname[3];
|
|||
|
|
|||
|
string[] nameList = splitname[0].Split('&');
|
|||
|
string frontDelete = nameList[0] + "&";
|
|||
|
|
|||
|
feature =
|
|||
|
$"{splitname[0].Replace("mdl&", "").Replace("pfb&", "").Replace(frontDelete, "").Replace("lob", "").Replace("ing", "")}_{splitname[1]}";
|
|||
|
config = splitname[2];
|
|||
|
}
|
|||
|
realpath = $"{feature}/{path}/{config}";
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
//兼容原版prefab导入
|
|||
|
if (splitname.Length >= 4)
|
|||
|
{
|
|||
|
feature = $"{splitname[0].Replace("mdl&","").Replace("pfb&","")}_{splitname[1]}";
|
|||
|
config = splitname[2];
|
|||
|
}
|
|||
|
realpath = $"{feature}/{path}/{config}";
|
|||
|
}
|
|||
|
|
|||
|
return realpath;
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
void CreatePrefabFodler(string path)
|
|||
|
{
|
|||
|
string dirPath = Application.dataPath + path.Replace("Assets/", "/");
|
|||
|
if (!Directory.Exists(dirPath))
|
|||
|
{
|
|||
|
Directory.CreateDirectory(dirPath);
|
|||
|
}
|
|||
|
if (!Directory.Exists(m_FolderPath))
|
|||
|
{
|
|||
|
NamingChecker.PopupErrorMessage("创建文件夹失败:" + m_FolderPath);
|
|||
|
}
|
|||
|
}
|
|||
|
string CreateFolder(string fbxName)
|
|||
|
{
|
|||
|
string path = "";
|
|||
|
if (fbxName.Contains("&ing")||fbxName.Contains("low"))
|
|||
|
{
|
|||
|
path = m_FbxDirIng + LODCombinePath(fbxName)+"/Models/";
|
|||
|
}else if (fbxName.Contains("&lob")||fbxName.Contains("height"))
|
|||
|
{
|
|||
|
path = m_FbxDirLob + LODCombinePath(fbxName)+"/Models/";
|
|||
|
}else
|
|||
|
Debug.LogError(fbxName+"FBX命名不规范,未包含前缀lob or ing!");
|
|||
|
string dirPath = Application.dataPath + path;
|
|||
|
if (!Directory.Exists(dirPath))
|
|||
|
{
|
|||
|
Directory.CreateDirectory(dirPath);
|
|||
|
}
|
|||
|
return dirPath;
|
|||
|
}
|
|||
|
|
|||
|
string GetFbxDirectry(string fbxName)
|
|||
|
{
|
|||
|
string rst = "";
|
|||
|
string[] wordList = fbxName.Split('_');
|
|||
|
rst = wordList[0] + wordList[1] + "/SkinParts/" + wordList[2] + "/";
|
|||
|
return rst;
|
|||
|
}
|
|||
|
|
|||
|
string GetFbxName(string assetPath)
|
|||
|
{
|
|||
|
string[] wordList = assetPath.Split('/');
|
|||
|
return wordList[wordList.Length - 1].Split('.')[0];//TODO
|
|||
|
}
|
|||
|
|
|||
|
#endregion
|
|||
|
|
|||
|
#region CreatePrefab
|
|||
|
|
|||
|
void OnClickCreateButton(bool isSuccess = true)
|
|||
|
{
|
|||
|
if (isSuccess == false)
|
|||
|
{
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
m_PrefabPath = "";
|
|||
|
if (m_FolderPath == "")
|
|||
|
{
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
for (int i = 0; i < mFolderPathList.Count; i++)
|
|||
|
{
|
|||
|
|
|||
|
GameObject fbxObject = GetFbxGameobject(mFolderPathList[i],mFbxFileList[i]);
|
|||
|
// WriteSuiteLevel();
|
|||
|
// 性能检查分析
|
|||
|
// if (!DataAnalysisUtility.IsInIgnoreList(fbxObject.name) && !DataAnalysisUtility.ResourcesDataCheck(fbxObject))
|
|||
|
// {
|
|||
|
// //此处先忽略
|
|||
|
// Clear();
|
|||
|
// return;
|
|||
|
// }
|
|||
|
CreatePrefab(ref fbxObject,i);
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
void WriteSuiteLevel()
|
|||
|
{
|
|||
|
string fbxName = m_FbxFile.Name.Replace(".FBX","");
|
|||
|
string writeLine = fbxName + "*" + (int)m_SuiteLevel;
|
|||
|
StreamReader sr = null;
|
|||
|
if (!File.Exists(m_SuiteLevelPath))
|
|||
|
{
|
|||
|
File.Create(m_SuiteLevelPath).Dispose();
|
|||
|
}
|
|||
|
|
|||
|
sr = new StreamReader(m_SuiteLevelPath, Encoding.Default);
|
|||
|
string readStr = "";
|
|||
|
List<string> content = new List<string>();
|
|||
|
// 先缓存下所有除了本次处理fbx信息以外
|
|||
|
while (sr != null && (readStr = sr.ReadLine()) != null)
|
|||
|
{
|
|||
|
if (!readStr.Contains(fbxName))
|
|||
|
{
|
|||
|
content.Add(readStr);
|
|||
|
}
|
|||
|
}
|
|||
|
sr.Close();
|
|||
|
// 重写文件内容
|
|||
|
StreamWriter sw = new StreamWriter(m_SuiteLevelPath, false);
|
|||
|
foreach(string str in content)
|
|||
|
{
|
|||
|
sw.WriteLine(str);
|
|||
|
}
|
|||
|
sw.Close();
|
|||
|
// 末尾补上新改的一段
|
|||
|
FileInfo fi = new FileInfo(m_SuiteLevelPath);
|
|||
|
sw = fi.AppendText();
|
|||
|
sw.WriteLine(writeLine);
|
|||
|
sw.Close();
|
|||
|
sw.Dispose();
|
|||
|
}
|
|||
|
|
|||
|
GameObject GetFbxGameobject(string m_FolderPath,FileInfo m_FbxFile)
|
|||
|
{
|
|||
|
DirectoryInfo direction = new DirectoryInfo(m_FolderPath);
|
|||
|
FileInfo[] fbxFiles = direction.GetFiles("*.fbx", SearchOption.AllDirectories);
|
|||
|
string fbxFileChoosed = "";
|
|||
|
foreach (FileInfo fbxfile in fbxFiles)
|
|||
|
{
|
|||
|
if (fbxfile.Name.Equals(m_FbxFile.Name))
|
|||
|
{
|
|||
|
fbxFileChoosed = fbxfile.FullName;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
string relativePath = NamingChecker.GetRelativePath(fbxFileChoosed);
|
|||
|
Transform fbxTrans = AssetDatabase.LoadAssetAtPath(relativePath, typeof(Transform)) as Transform;
|
|||
|
if (m_FbxSenceObject != null) // 不要重复生成,关闭的时候清不掉
|
|||
|
{
|
|||
|
DestroyImmediate(m_FbxSenceObject.gameObject);
|
|||
|
}
|
|||
|
m_FbxSenceObject = Instantiate(fbxTrans).gameObject;
|
|||
|
return m_FbxSenceObject;
|
|||
|
}
|
|||
|
|
|||
|
void CreatePrefab(ref GameObject fbxGameObject,int index)
|
|||
|
{
|
|||
|
string prefabName = "";
|
|||
|
string realPrefabName = "";
|
|||
|
if (m_IsOld)
|
|||
|
{
|
|||
|
prefabName = m_OldPrefab.name;
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
prefabName = fbxGameObject.name.Split('(')[0];
|
|||
|
realPrefabName = GetPrefabName(prefabName);
|
|||
|
prefabName = DropPostfix(prefabName);
|
|||
|
}
|
|||
|
string pathname = "";
|
|||
|
string instPrefabPath = "";
|
|||
|
pathname = CombinePath(prefabName)+"/Prefabs/";
|
|||
|
if (prefabName.Contains("&ing")||prefabName.Contains("low"))
|
|||
|
{
|
|||
|
instPrefabPath = m_PrefabDirIng + pathname;
|
|||
|
CreatePrefabFodler(instPrefabPath);
|
|||
|
m_PrefabPath = instPrefabPath + realPrefabName + ".prefab";
|
|||
|
LOD_m_PrefabPath = LOD_m_PrefabPath + instPrefabPath + realPrefabName + ".prefab\n";
|
|||
|
mPrefabPathList.Add(index, m_PrefabPath);
|
|||
|
}
|
|||
|
else if(prefabName.Contains("&lob")||prefabName.Contains("height"))
|
|||
|
{
|
|||
|
instPrefabPath = m_PrefabDirLob + pathname;
|
|||
|
CreatePrefabFodler(instPrefabPath);
|
|||
|
m_PrefabPath = instPrefabPath+ realPrefabName + ".prefab";
|
|||
|
LOD_m_PrefabPath = LOD_m_PrefabPath + instPrefabPath + realPrefabName + ".prefab\n";
|
|||
|
mPrefabPathList.Add(index,m_PrefabPath);
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
Debug.LogError(prefabName+"资源命名缺少前缀ing or lob!");
|
|||
|
}
|
|||
|
|
|||
|
// 【注意】不能使用绝对路径!https://blog.csdn.net/MAOMAOXIAOHUO/article/details/51204230
|
|||
|
Material defaultMat = AssetDatabase.LoadAssetAtPath(m_DefaultMatPath, typeof(Material)) as Material;
|
|||
|
// Dictionary<string, string> dict = new Dictionary<string, string>();
|
|||
|
// if (m_IsOld)
|
|||
|
// {
|
|||
|
// // 拿到当前项目里已经存在的prefab中的材质与mesh映射关系
|
|||
|
// dict = GetMaterialRelationFromOldPrefab();
|
|||
|
// }
|
|||
|
|
|||
|
// fbxGameObject.name = fbxGameObject.name.Replace("mdl&", "").Replace("(Clone)", "");
|
|||
|
|
|||
|
|
|||
|
// if (m_IsOld)
|
|||
|
// {
|
|||
|
// // 赋予新生成的go材质
|
|||
|
// foreach (Transform temp in fbxGameObject.transform)
|
|||
|
// {
|
|||
|
// if (temp.GetComponent<SkinnedMeshRenderer>() != null)
|
|||
|
// {
|
|||
|
// string matPath = "";
|
|||
|
// if (dict.TryGetValue(temp.name, out matPath))
|
|||
|
// {
|
|||
|
// temp.GetComponent<SkinnedMeshRenderer>().sharedMaterial = AssetDatabase.LoadAssetAtPath(matPath, typeof(Material)) as Material;
|
|||
|
// }
|
|||
|
// }
|
|||
|
// }
|
|||
|
// }
|
|||
|
// else
|
|||
|
// {
|
|||
|
DMMCharacterRevisionWindow.LODLoadPresetToGameObject(ref fbxGameObject,currentAssetLodLevel,currentAssetSceneType,realPrefabName);
|
|||
|
|
|||
|
// }
|
|||
|
|
|||
|
// 这一段是试图挂blink eye脚本
|
|||
|
foreach (Transform temp in fbxGameObject.transform)
|
|||
|
{
|
|||
|
foreach (string m_name in ModelName)
|
|||
|
{
|
|||
|
if (temp.gameObject.name.Contains(m_name) && temp.GetComponent<SkinnedMeshRenderer>() != null)
|
|||
|
{
|
|||
|
if (m_IsLobby && temp.gameObject.name.Contains(BlinkFaceTag) && temp.gameObject.GetComponent<BlinkController>() == null)
|
|||
|
{
|
|||
|
string characterTag = prefabName.Split('_')[0] + "_" + prefabName.Split('_')[1];
|
|||
|
string[] faceNames;
|
|||
|
bool hasBlinkface = m_BlinkFaceDict.TryGetValue(characterTag, out faceNames);
|
|||
|
if (hasBlinkface)
|
|||
|
{
|
|||
|
// 挂脚本
|
|||
|
temp.GetComponent<SkinnedMeshRenderer>().sharedMaterial = defaultMat;
|
|||
|
BlinkController ctrl = temp.gameObject.AddComponent<BlinkController>();
|
|||
|
ctrl.m_OpenEyesMatName = faceNames[0];
|
|||
|
ctrl.mCloseEyesMatName = faceNames[1];
|
|||
|
}
|
|||
|
}
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
string DynaMicPath = AddDynamicBoneData(Path.GetFileNameWithoutExtension(mPrefabPathList[index]));
|
|||
|
PrefabUtility.SaveAsPrefabAssetAndConnect(fbxGameObject, mPrefabPathList[index], InteractionMode.AutomatedAction);
|
|||
|
// GameObject m_prefabRoot = PrefabUtility.GetCorrespondingObjectFromOriginalSource(fbxGameObject) as GameObject;
|
|||
|
|
|||
|
AssetDatabase.SaveAssets();
|
|||
|
DestroyImmediate(fbxGameObject.gameObject);
|
|||
|
|
|||
|
// 自动生成动态骨骼
|
|||
|
|
|||
|
// 读取prefab
|
|||
|
Transform model = AssetDatabase.LoadAssetAtPath(mPrefabPathList[index], typeof(Transform)) as Transform;
|
|||
|
if (m_DynamicBoneSenceObject != null) // 不要重复生成,关闭的时候清不掉
|
|||
|
{
|
|||
|
DestroyImmediate(m_DynamicBoneSenceObject.gameObject);
|
|||
|
}
|
|||
|
m_DynamicBoneSenceObject = Instantiate(model).gameObject;
|
|||
|
|
|||
|
// 找出标记父节点并初始化一系列成员变量
|
|||
|
ProcessPrefabToInit(ref m_DynamicBoneSenceObject);
|
|||
|
//如果存在配置文件,就按照标准保存
|
|||
|
if (DynaMicPath.Length > 0)
|
|||
|
{
|
|||
|
OnClickImportDynamicBoneFileByName(DynaMicPath);
|
|||
|
}
|
|||
|
|
|||
|
// 根据m_TagToDynamicBoneParamDict动态生成可调节Dynamic Bone参数
|
|||
|
RePaintDynamicBoneParamsPanel();
|
|||
|
|
|||
|
// 在对应部位挂HeadBoneAttach
|
|||
|
AddHeadBoneAttach();
|
|||
|
|
|||
|
|
|||
|
|
|||
|
// 保存prefab
|
|||
|
SaveDynamicBonePrefab();
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// 缓存导入,已有的dynamic配置参数
|
|||
|
/// </summary>
|
|||
|
/// <param name="DynamicBoneName"></param>
|
|||
|
string AddDynamicBoneData(string DynamicBoneName)
|
|||
|
{
|
|||
|
string objectName = DynamicBoneName.Split('(')[0];
|
|||
|
string pathName;
|
|||
|
|
|||
|
string DynamicPath = "";
|
|||
|
|
|||
|
pathName = CombinePath(objectName) + "/Prefabs/";
|
|||
|
string assetPath = "";
|
|||
|
if (objectName.Contains("&ing") || objectName.Contains("low"))
|
|||
|
{
|
|||
|
assetPath = m_PrefabDirIng + pathName + objectName + ".prefab";
|
|||
|
}
|
|||
|
else if (objectName.Contains("&lob") || objectName.Contains("height"))
|
|||
|
{
|
|||
|
assetPath = m_PrefabDirLob + pathName + objectName + ".prefab";
|
|||
|
}
|
|||
|
|
|||
|
Transform model = AssetDatabase.LoadAssetAtPath(assetPath, typeof(Transform)) as Transform;
|
|||
|
|
|||
|
if (model != null)
|
|||
|
{
|
|||
|
GameObject DynamicObj = Instantiate(model).gameObject;
|
|||
|
ProcessPrefabToInit(ref DynamicObj);
|
|||
|
DynamicPath = OnClickExportDynamicBoneFileByName(DynamicObj.name);
|
|||
|
DestroyImmediate(DynamicObj);
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
return DynamicPath;
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
void RePaintDynamicBoneParamsPanel()
|
|||
|
{
|
|||
|
this.Repaint();
|
|||
|
}
|
|||
|
|
|||
|
Dictionary<string, string> GetMaterialRelationFromOldPrefab()
|
|||
|
{
|
|||
|
Dictionary<string, string> rst = new Dictionary<string, string>();
|
|||
|
foreach (Transform temp in m_OldPrefab.transform)
|
|||
|
{
|
|||
|
if (temp.GetComponent<SkinnedMeshRenderer>() != null)
|
|||
|
{
|
|||
|
Material mat = temp.GetComponent<SkinnedMeshRenderer>().sharedMaterial;
|
|||
|
rst.Add(temp.name, AssetDatabase.GetAssetPath(mat));
|
|||
|
}
|
|||
|
}
|
|||
|
return rst;
|
|||
|
}
|
|||
|
|
|||
|
// 去掉_SRN后缀
|
|||
|
string DropPostfix(string name)
|
|||
|
{
|
|||
|
|
|||
|
return name.Replace("mdl", "pfb");
|
|||
|
// string postfix = NamingChecker.GetPostfix(name);
|
|||
|
// bool isValid = NamingChecker.IsValidPostfix(postfix);
|
|||
|
// if (isValid)
|
|||
|
// {
|
|||
|
// return name.Replace("_" + postfix, "").Replace("mdl", "pfb");
|
|||
|
// }
|
|||
|
// else
|
|||
|
// {
|
|||
|
// return name.Replace("mdl", "pfb");
|
|||
|
// }
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// 获取fbx对应的prefab名字
|
|||
|
/// </summary>
|
|||
|
/// <param name="name"></param>
|
|||
|
/// <returns></returns>
|
|||
|
string GetPrefabName(string name)
|
|||
|
{
|
|||
|
string[] nameList = name.Split('&');
|
|||
|
eAssetLodLevel level = eAssetLodLevel.None;
|
|||
|
string finalName = String.Empty;
|
|||
|
|
|||
|
foreach (KeyValuePair<string,eAssetLodLevel> keyValuePair in levelDic)
|
|||
|
{
|
|||
|
if (nameList[0].Contains(keyValuePair.Key))
|
|||
|
{
|
|||
|
level = keyValuePair.Value;
|
|||
|
currentAssetLodLevel = level;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
foreach (KeyValuePair<string,eAssetSceneType> keyValuePair in sceneDic)
|
|||
|
{
|
|||
|
if (nameList[1].Contains(keyValuePair.Key))
|
|||
|
{
|
|||
|
currentAssetSceneType = keyValuePair.Value;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
if (nameList.Length == 2 )
|
|||
|
{
|
|||
|
string[] logicNamelist = nameList[1].Split('_');
|
|||
|
if (logicNamelist.Length >= 4)
|
|||
|
{
|
|||
|
StringBuilder stringBuilder = new StringBuilder();
|
|||
|
stringBuilder.Clear();
|
|||
|
|
|||
|
for (int i = 0; i < 3; i++)
|
|||
|
{
|
|||
|
stringBuilder.Append(logicNamelist[i] + "_");
|
|||
|
}
|
|||
|
|
|||
|
stringBuilder.Append(logicNamelist[3]);
|
|||
|
|
|||
|
string prefabName = stringBuilder.ToString();
|
|||
|
|
|||
|
//识别补充后缀
|
|||
|
if (!prefabName.Contains("_skin"))
|
|||
|
{
|
|||
|
prefabName = prefabName + "_skin";
|
|||
|
}
|
|||
|
|
|||
|
stringBuilder.Clear();
|
|||
|
finalName = LMNAResMapUtil.AssembleAssetName(prefabName, eAssetFunctionType.GameObject,
|
|||
|
level);
|
|||
|
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return finalName;
|
|||
|
}
|
|||
|
|
|||
|
// 处理需要蒙皮的特效部位
|
|||
|
void ProcessEffectPart(ref GameObject prefab)
|
|||
|
{
|
|||
|
foreach(Transform temp in prefab.transform)
|
|||
|
{
|
|||
|
if (!temp.name.Contains(NamingChecker.AttachmentPatterns[2])) continue;
|
|||
|
// 新建一个子go
|
|||
|
GameObject go = new GameObject();
|
|||
|
go.transform.SetParent(temp);
|
|||
|
// 把自己挂着的skinmeshrenderer剪切到子go上
|
|||
|
SkinnedMeshRenderer smr = temp.GetComponent<SkinnedMeshRenderer>();
|
|||
|
UnityEditorInternal.ComponentUtility.CopyComponent(smr);
|
|||
|
UnityEditorInternal.ComponentUtility.PasteComponentAsNew(go);
|
|||
|
// 挂上PlayerEffectController
|
|||
|
PlayerEffectController pec = temp.gameObject.AddComponent<PlayerEffectController>();
|
|||
|
pec.m_Effect = go;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
#endregion
|
|||
|
|
|||
|
#region Clear
|
|||
|
void OnClickClearButton()
|
|||
|
{
|
|||
|
ClearFbxAndTextures(() =>
|
|||
|
{
|
|||
|
NamingChecker.PopupErrorMessage("成功清除" + m_FolderPath + "下新导入的资源!");
|
|||
|
});
|
|||
|
ClearPrefabFolder(() =>
|
|||
|
{
|
|||
|
NamingChecker.PopupErrorMessage("成功清除" + m_PrefabPath + "!");
|
|||
|
});
|
|||
|
AssetDatabase.Refresh();
|
|||
|
this.Repaint();
|
|||
|
}
|
|||
|
|
|||
|
void Clear(System.Action onFbxSucc = null, System.Action onPrefabSucc = null)
|
|||
|
{
|
|||
|
ClearFbxAndTextures(onFbxSucc);
|
|||
|
ClearPrefabFolder(onPrefabSucc);
|
|||
|
m_OldPrefab = null; ;
|
|||
|
AssetDatabase.Refresh();
|
|||
|
this.Repaint();
|
|||
|
}
|
|||
|
|
|||
|
void ClearFbxAndTextures(System.Action onSucc = null)
|
|||
|
{
|
|||
|
if (!Directory.Exists(m_FolderPath))
|
|||
|
return;
|
|||
|
// 删除fbx同目录下所有文件
|
|||
|
string tempMatDir = m_FolderPath;
|
|||
|
DirectoryInfo tempDirection = new DirectoryInfo(tempMatDir);
|
|||
|
FileInfo[] files = tempDirection.GetFiles("*.*", SearchOption.TopDirectoryOnly);
|
|||
|
File.Delete(m_FbxPath);
|
|||
|
m_FbxPath = "";
|
|||
|
m_FolderPath = "";
|
|||
|
if (onSucc != null) onSucc();
|
|||
|
}
|
|||
|
|
|||
|
void ClearPrefabFolder(System.Action onSucc = null)
|
|||
|
{
|
|||
|
if (!File.Exists(m_PrefabPath))
|
|||
|
return;
|
|||
|
File.Delete(m_PrefabPath);
|
|||
|
m_PrefabPath = "";
|
|||
|
if (onSucc != null) onSucc();
|
|||
|
}
|
|||
|
|
|||
|
#endregion
|
|||
|
|
|||
|
#region DynamicBone
|
|||
|
void OnClickCreateLODDynamicBoneParams()
|
|||
|
{
|
|||
|
if (string.IsNullOrEmpty(m_DynamicBonePrefabPath))
|
|||
|
{
|
|||
|
return;
|
|||
|
}
|
|||
|
// 读取prefab
|
|||
|
Transform model = AssetDatabase.LoadAssetAtPath(m_DynamicBonePrefabPath, typeof(Transform)) as Transform;
|
|||
|
if (m_DynamicBoneSenceObject != null) // 不要重复生成,关闭的时候清不掉
|
|||
|
{
|
|||
|
DestroyImmediate(m_DynamicBoneSenceObject.gameObject);
|
|||
|
}
|
|||
|
m_DynamicBoneSenceObject = Instantiate(model).gameObject;
|
|||
|
|
|||
|
// 找出标记父节点并初始化一系列成员变量
|
|||
|
ProcessPrefabToInit(ref m_DynamicBoneSenceObject);
|
|||
|
|
|||
|
// 根据m_TagToDynamicBoneParamDict动态生成可调节Dynamic Bone参数
|
|||
|
RePaintLODDynamicBoneParamsPanel();
|
|||
|
}
|
|||
|
|
|||
|
void OnClickDynamicBoneButton()
|
|||
|
{
|
|||
|
// 在对应部位挂HeadBoneAttach
|
|||
|
AddHeadBoneAttach();
|
|||
|
// 保存prefab
|
|||
|
SaveDynamicBonePrefab();
|
|||
|
}
|
|||
|
|
|||
|
void OnClickAddCollider(string key)
|
|||
|
{
|
|||
|
int idx = 0;
|
|||
|
LODDynamicColliderData newItem;
|
|||
|
List<LODDynamicColliderData> tempList = new List<LODDynamicColliderData>();
|
|||
|
LODDynamicBoneParams tempDictData = m_TagToDynamicBoneParamDict[key]; // 用于给字典赋值
|
|||
|
// 添加碰撞体
|
|||
|
if (m_TagToDynamicBoneParamDict[key].colliderDataList != null)
|
|||
|
{
|
|||
|
tempList = m_TagToDynamicBoneParamDict[key].colliderDataList;
|
|||
|
idx = tempList.Count; // 目前有多少个collider了
|
|||
|
|
|||
|
}
|
|||
|
// 点击添加键时就需要创建并挂上transform
|
|||
|
string name = "Collider_" + idx;
|
|||
|
GameObject collider = new GameObject();
|
|||
|
collider.name = name;
|
|||
|
collider.transform.SetParent(GetTransformByName(key).GetChild(0));
|
|||
|
DynamicBoneCollider dc = collider.AddComponent<DynamicBoneCollider>();
|
|||
|
newItem = new LODDynamicColliderData(name, 0.3f, null, collider.transform);
|
|||
|
dc.m_Radius = newItem.radius;
|
|||
|
tempList.Add(newItem);
|
|||
|
tempDictData = new LODDynamicBoneParams(tempDictData.root, tempDictData.Damping, tempDictData.Elasticty,
|
|||
|
tempDictData.Stiffness, tempDictData.Inert, tempDictData.Friction, tempDictData.Radius,
|
|||
|
tempList);
|
|||
|
m_TagToDynamicBoneParamDict[key] = tempDictData;
|
|||
|
RePaintLODDynamicBoneParamsPanel();
|
|||
|
}
|
|||
|
|
|||
|
void OnClickRemoveCollider(string key)
|
|||
|
{
|
|||
|
if (m_TagToDynamicBoneParamDict[key].colliderDataList == null) return;
|
|||
|
int idx = m_TagToDynamicBoneParamDict[key].colliderDataList.Count;
|
|||
|
List<LODDynamicColliderData> tempList = m_TagToDynamicBoneParamDict[key].colliderDataList;
|
|||
|
LODDynamicBoneParams tempDictData = m_TagToDynamicBoneParamDict[key]; // 用于给字典赋值
|
|||
|
DestroyImmediate(tempList[idx - 1].colTrans.gameObject);
|
|||
|
tempList.Remove(tempList[idx - 1]);
|
|||
|
tempDictData = new LODDynamicBoneParams(tempDictData.root, tempDictData.Damping, tempDictData.Elasticty,
|
|||
|
tempDictData.Stiffness, tempDictData.Inert, tempDictData.Friction, tempDictData.Radius,
|
|||
|
tempList);
|
|||
|
m_TagToDynamicBoneParamDict[key] = tempDictData;
|
|||
|
RePaintLODDynamicBoneParamsPanel();
|
|||
|
}
|
|||
|
|
|||
|
// 导入Dynamicbone配置文件
|
|||
|
void OnClickImportDynamicBoneFile()
|
|||
|
{
|
|||
|
TryReadSBFile();
|
|||
|
}
|
|||
|
|
|||
|
void OnClickImportDynamicBoneFileByName(string path)
|
|||
|
{
|
|||
|
TryReadSBFileByName(path);
|
|||
|
}
|
|||
|
|
|||
|
// 导出Dynamicbone配置文件
|
|||
|
void OnClickExportDynamicBoneFile()
|
|||
|
{
|
|||
|
WriteSBInfo();
|
|||
|
EditorUtility.DisplayDialog("成功", "导出完毕", "确定");
|
|||
|
AssetDatabase.Refresh();
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
string OnClickExportDynamicBoneFileByName(string name)
|
|||
|
{
|
|||
|
string path = WriteSBInfoByName(name);
|
|||
|
// EditorUtility.DisplayDialog("成功", "导出完毕", "确定");
|
|||
|
AssetDatabase.Refresh();
|
|||
|
return path;
|
|||
|
}
|
|||
|
|
|||
|
void SaveDynamicBonePrefab()
|
|||
|
{
|
|||
|
string senceObjectName = m_DynamicBoneSenceObject.name.Split('(')[0];
|
|||
|
string pathname = "";
|
|||
|
pathname = CombinePath(senceObjectName)+"/Prefabs/";
|
|||
|
|
|||
|
// string dirPath = Application.dataPath + m_PrefabDirLob + pathname.Replace("Assets/", "/");
|
|||
|
// if (!Directory.Exists(dirPath))
|
|||
|
// {
|
|||
|
// Directory.CreateDirectory(dirPath);
|
|||
|
// }
|
|||
|
|
|||
|
if (senceObjectName.Contains("&ing")||senceObjectName.Contains("low"))
|
|||
|
PrefabUtility.SaveAsPrefabAsset(m_DynamicBoneSenceObject, m_PrefabDirIng +pathname+ senceObjectName + ".prefab");
|
|||
|
else if(senceObjectName.Contains("&lob")||senceObjectName.Contains("height"))
|
|||
|
PrefabUtility.SaveAsPrefabAsset(m_DynamicBoneSenceObject, m_PrefabDirLob +pathname+ senceObjectName + ".prefab");
|
|||
|
|
|||
|
AssetDatabase.SaveAssets();
|
|||
|
}
|
|||
|
|
|||
|
void ProcessPrefabToInit(ref GameObject senceObject)
|
|||
|
{
|
|||
|
m_SkinPartTransformList.Clear();
|
|||
|
m_TaggedTransformDict.Clear();
|
|||
|
m_TagToDynamicBoneParamDict.Clear();
|
|||
|
|
|||
|
// 初始化m_SkinPartTransformList
|
|||
|
for (int i = 0; i < senceObject.transform.childCount; i++)
|
|||
|
{
|
|||
|
foreach (string part in ModelName)
|
|||
|
{
|
|||
|
if (senceObject.transform.GetChild(i).name.Contains(part) &&
|
|||
|
!senceObject.transform.GetChild(i).name.Contains(part + "_ex") &&
|
|||
|
!senceObject.transform.GetChild(i).name.Contains(part + "_fx"))
|
|||
|
{
|
|||
|
m_SkinPartTransformList.Add(senceObject.transform.GetChild(i));
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 初始化m_TaggedTransformDict和m_TagToDynamicBoneParamDict
|
|||
|
foreach (Transform child in senceObject.GetComponentsInChildren<Transform>())
|
|||
|
{
|
|||
|
|
|||
|
|
|||
|
foreach (string tag in DynamicBoneTag)
|
|||
|
{
|
|||
|
if (child.name.Contains(tag))
|
|||
|
{
|
|||
|
m_TaggedTransformDict.Add(child, child.parent.name);
|
|||
|
LODDynamicBoneParams param = GetLODDynamicBoneParams(child);
|
|||
|
m_TagToDynamicBoneParamDict.Add(child.name, param);
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
void RePaintLODDynamicBoneParamsPanel()
|
|||
|
{
|
|||
|
this.Repaint();
|
|||
|
}
|
|||
|
|
|||
|
void AddHeadBoneAttach()
|
|||
|
{
|
|||
|
foreach (KeyValuePair<Transform, string> part in m_TaggedTransformDict)
|
|||
|
{
|
|||
|
Transform targetTrans = null;
|
|||
|
if (part.Key.name.Contains("CLOTHES_"))
|
|||
|
{
|
|||
|
targetTrans = m_SkinPartTransformList.Find(x => x.name.Contains("clothes"));
|
|||
|
|
|||
|
}
|
|||
|
else if (part.Key.name.Contains("HEAD_"))
|
|||
|
{
|
|||
|
targetTrans = m_SkinPartTransformList.Find(x => x.name.Contains("head"));
|
|||
|
}
|
|||
|
else if (part.Key.name.Contains("HAND_"))
|
|||
|
{
|
|||
|
targetTrans = m_SkinPartTransformList.Find(x => x.name.Contains("hand"));
|
|||
|
}
|
|||
|
else if (part.Key.name.Contains("SHOES_"))
|
|||
|
{
|
|||
|
targetTrans = m_SkinPartTransformList.Find(x => x.name.Contains("shoes"));
|
|||
|
}
|
|||
|
else if (part.Key.name.Contains("TROUSERS_"))
|
|||
|
{
|
|||
|
targetTrans = m_SkinPartTransformList.Find(x => x.name.Contains("trousers"));
|
|||
|
}
|
|||
|
if (targetTrans == null)
|
|||
|
{
|
|||
|
Debug.LogError("在绑DynamicBone时未找到" + part.Key.name + "对应的部位Transform!");
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
bool isShouldAdd = true; // 检查一下是否已经挂了一模一样的attachment脚本
|
|||
|
HeadBoneAttach[] comps = targetTrans.gameObject.GetComponents<HeadBoneAttach>();
|
|||
|
HeadBoneAttach comp;
|
|||
|
Transform attachment = part.Key.GetChild(0);
|
|||
|
for (int i = 0; i < comps.Length; i++)
|
|||
|
{
|
|||
|
if (comps[i].m_Attachment == attachment && comps[i].m_HeadBoneName.Equals(part.Value))
|
|||
|
{
|
|||
|
isShouldAdd = false;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
if (isShouldAdd)
|
|||
|
{
|
|||
|
comp = targetTrans.gameObject.AddComponent<HeadBoneAttach>();
|
|||
|
comp.m_Attachment = attachment;
|
|||
|
comp.m_HeadBoneName = part.Value;
|
|||
|
}
|
|||
|
|
|||
|
part.Key.SetParent(targetTrans);
|
|||
|
// 骨骼串的跟节点挂DynamicBone脚本
|
|||
|
SetLODDynamicBoneParams(ref attachment, m_TagToDynamicBoneParamDict[part.Key.name]);
|
|||
|
|
|||
|
// collider的attachment
|
|||
|
// TODO:我忘了。。DB需不需要和SB一样都要对collider的transform也挂上HeadBoneAttach脚本?先注释掉
|
|||
|
/*
|
|||
|
LODDynamicBoneParams dbParams = m_TagToDynamicBoneParamDict[part.Key.name];
|
|||
|
List<LODDynamicColliderData> collParams = dbParams.colliderDataList;
|
|||
|
HeadBoneAttach collComp;
|
|||
|
if (collParams != null)
|
|||
|
{
|
|||
|
for (int i = 0; i < collParams.Count; i++)
|
|||
|
{
|
|||
|
bool isShouldAddCollAttach = !string.IsNullOrEmpty(collParams[i].rootName);
|
|||
|
for (int j = 0; j < comps.Length; j++)
|
|||
|
{
|
|||
|
// 检查一下是否已经挂了一模一样的attachment脚本
|
|||
|
if (comps[j].m_Attachment == null || string.IsNullOrEmpty(comps[j].m_HeadBoneName))
|
|||
|
continue;
|
|||
|
if (comps[j].m_Attachment.name.Equals(collParams[i].name) &&
|
|||
|
comps[j].m_HeadBoneName.Equals(collParams[i].rootName))
|
|||
|
{
|
|||
|
isShouldAddCollAttach = false;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
if (isShouldAddCollAttach)
|
|||
|
{
|
|||
|
collComp = targetTrans.gameObject.AddComponent<HeadBoneAttach>();
|
|||
|
collComp.m_Attachment = collParams[i].colTrans;
|
|||
|
collComp.m_HeadBoneName = collParams[i].rootName;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
*/
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
Transform GetTransformByName(string name)
|
|||
|
{
|
|||
|
foreach (Transform VARIABLE in m_DynamicBoneSenceObject.transform.GetComponentsInChildren<Transform>())
|
|||
|
{
|
|||
|
if (VARIABLE.name.Equals(name))
|
|||
|
return VARIABLE;
|
|||
|
}
|
|||
|
|
|||
|
return null;
|
|||
|
}
|
|||
|
|
|||
|
LODDynamicBoneParams GetLODDynamicBoneParams(Transform trans)
|
|||
|
{
|
|||
|
LODDynamicBoneParams rst = new LODDynamicBoneParams(trans.GetChild(0), 0.02f, 0.08f, 0.06f, 0.8f, 0.6f, 0.02f, null); // TODO: 这个经验数值应该也用不了
|
|||
|
if (trans.childCount == 0 || trans.GetChild(0).childCount == 0)
|
|||
|
return rst;
|
|||
|
DynamicBone root = trans.GetChild(0).gameObject.GetComponent<DynamicBone>();
|
|||
|
if (root == null)
|
|||
|
{
|
|||
|
root = trans.GetChild(0).gameObject.AddComponent<DynamicBone>();
|
|||
|
}
|
|||
|
rst.root = trans.GetChild(0);
|
|||
|
rst.Damping = root.m_Damping;
|
|||
|
rst.Elasticty = root.m_Elasticity;
|
|||
|
rst.Inert = root.m_Inert;
|
|||
|
rst.Friction = root.m_Friction;
|
|||
|
rst.Stiffness = root.m_Stiffness;
|
|||
|
rst.Radius = root.m_Radius;
|
|||
|
rst.colliderDataList = new List<LODDynamicColliderData>();
|
|||
|
if(root.m_Colliders!=null)
|
|||
|
{
|
|||
|
for (int i = 0; i < root.m_Colliders.Count; i++)
|
|||
|
{
|
|||
|
DynamicBoneCollider dc = root.m_Colliders[i] as DynamicBoneCollider;
|
|||
|
string rootName = GetAttachmentRootName(dc.transform);
|
|||
|
LODDynamicColliderData collData = new LODDynamicColliderData(dc.name, dc.m_Radius, rootName, dc.transform);
|
|||
|
rst.colliderDataList.Add(collData);
|
|||
|
}
|
|||
|
}
|
|||
|
return rst;
|
|||
|
}
|
|||
|
|
|||
|
void SetLODDynamicBoneParams(ref Transform trans, LODDynamicBoneParams param)
|
|||
|
{
|
|||
|
if (trans == null || param.root == null)
|
|||
|
return;
|
|||
|
DynamicBone db = trans.GetComponent<DynamicBone>();
|
|||
|
if (db == null)
|
|||
|
{
|
|||
|
db = trans.gameObject.AddComponent<DynamicBone>();
|
|||
|
}
|
|||
|
db.m_Root = trans;
|
|||
|
db.m_Damping = param.Damping;
|
|||
|
db.m_Elasticity = param.Elasticty;
|
|||
|
db.m_Stiffness = param.Stiffness;
|
|||
|
db.m_Inert = param.Inert;
|
|||
|
db.m_Friction = param.Friction;
|
|||
|
db.m_Radius = param.Radius;
|
|||
|
}
|
|||
|
|
|||
|
string GetAttachmentRootName(Transform collider)
|
|||
|
{
|
|||
|
string name = "";
|
|||
|
HeadBoneAttach[] partAttaches = collider.GetComponentsInParent<HeadBoneAttach>();
|
|||
|
if (partAttaches != null)
|
|||
|
{
|
|||
|
for (int i = 0; i < partAttaches.Length; i++)
|
|||
|
{
|
|||
|
if (partAttaches[i].m_Attachment == collider)
|
|||
|
{
|
|||
|
name = partAttaches[i].m_HeadBoneName;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
return name;
|
|||
|
}
|
|||
|
|
|||
|
Transform GetCollider(Transform parent, string name)
|
|||
|
{
|
|||
|
for (int i = 0; i < parent.childCount; i++)
|
|||
|
{
|
|||
|
if (parent.GetChild(i).name.Equals(name))
|
|||
|
return parent.GetChild(i);
|
|||
|
}
|
|||
|
|
|||
|
return null;
|
|||
|
}
|
|||
|
|
|||
|
void ClearScenePrefabs()
|
|||
|
{
|
|||
|
if (m_FbxSenceObject != null && m_FbxSenceObject.gameObject != null)
|
|||
|
{
|
|||
|
GameObject.DestroyImmediate(m_FbxSenceObject.gameObject);
|
|||
|
}
|
|||
|
if (m_DynamicBoneSenceObject != null && m_DynamicBoneSenceObject.gameObject != null)
|
|||
|
{
|
|||
|
GameObject.DestroyImmediate(m_DynamicBoneSenceObject.gameObject);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
#endregion
|
|||
|
|
|||
|
#region Export/ImportDynamicBoneInfo
|
|||
|
|
|||
|
string GetDynamicBoneInfoFilePath()
|
|||
|
{
|
|||
|
if (m_DynamicBoneSenceObject != null && m_DynamicBoneSenceObject.gameObject != null)
|
|||
|
{
|
|||
|
string fileName = m_DynamicBoneSenceObject.transform.name.Replace("(Clone)", ""); // 就是prefab同名
|
|||
|
return m_DynamicBoneParamDir + fileName + ".txt";
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
EditorUtility.DisplayDialog("警告", "先生成Dynamicbone参数才能正确取到文件名字!", "好吧");
|
|||
|
return "";
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// 通过名字,拿到对应的动态骨骼配置文件
|
|||
|
/// </summary>
|
|||
|
/// <param name="name"></param>
|
|||
|
/// <returns></returns>
|
|||
|
string GetDynamicBoneInfoFilePathByName(string name)
|
|||
|
{
|
|||
|
string fileName = name.Replace("(Clone)", ""); // 就是prefab同名
|
|||
|
return m_DynamicBoneParamDir + fileName + ".txt";
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// 通过名字写入动态骨骼配置文件
|
|||
|
/// </summary>
|
|||
|
/// <param name="name"></param>
|
|||
|
string WriteSBInfoByName(string name)
|
|||
|
{
|
|||
|
string path = GetDynamicBoneInfoFilePathByName(name);
|
|||
|
StreamWriter sw;
|
|||
|
if (!File.Exists(path))
|
|||
|
{
|
|||
|
File.Create(path).Dispose();
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
// 清空
|
|||
|
File.WriteAllText(path, string.Empty);
|
|||
|
}
|
|||
|
FileInfo fi = new FileInfo(path);
|
|||
|
sw = fi.AppendText();
|
|||
|
// 写入m_TagToDynamicBoneParamDict
|
|||
|
WriteSBParamInfo(sw);
|
|||
|
sw.Close();
|
|||
|
sw.Dispose();
|
|||
|
|
|||
|
AssetDatabase.Refresh();
|
|||
|
|
|||
|
return path;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
void WriteSBInfo()
|
|||
|
{
|
|||
|
string path = GetDynamicBoneInfoFilePath();
|
|||
|
StreamWriter sw;
|
|||
|
if (!File.Exists(path))
|
|||
|
{
|
|||
|
File.Create(path).Dispose();
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
// 清空
|
|||
|
File.WriteAllText(path, string.Empty);
|
|||
|
}
|
|||
|
FileInfo fi = new FileInfo(path);
|
|||
|
sw = fi.AppendText();
|
|||
|
// 写入m_TagToDynamicBoneParamDict
|
|||
|
WriteSBParamInfo(sw);
|
|||
|
sw.Close();
|
|||
|
sw.Dispose();
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
void TryReadSBFile()
|
|||
|
{
|
|||
|
string path = GetDynamicBoneInfoFilePath();
|
|||
|
if (!File.Exists(path))
|
|||
|
{
|
|||
|
EditorUtility.DisplayDialog("警告", "找不到对应的DynamicBone参数文件!", "好吧");
|
|||
|
}
|
|||
|
if (EditorUtility.DisplayDialog("警告", "确定要覆盖DynamicBone参数么?", "确定"))
|
|||
|
{
|
|||
|
LoadSBFile();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// 加载动态骨骼配置文件(通过名字)
|
|||
|
/// </summary>
|
|||
|
/// <param name="path"></param>
|
|||
|
void TryReadSBFileByName(string path)
|
|||
|
{
|
|||
|
// string path = GetDynamicBoneInfoFilePathByName(name);
|
|||
|
// if (!File.Exists(path))
|
|||
|
// {
|
|||
|
// EditorUtility.DisplayDialog("警告", "找不到对应的DynamicBone参数文件!", "好吧");
|
|||
|
// }
|
|||
|
// if (EditorUtility.DisplayDialog("警告", "确定要覆盖DynamicBone参数么?", "确定"))
|
|||
|
// {
|
|||
|
// LoadSBFileByName(name);
|
|||
|
// }
|
|||
|
|
|||
|
if (File.Exists(path))
|
|||
|
{
|
|||
|
LoadSBFileByName(path);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
void LoadSBFile()
|
|||
|
{
|
|||
|
string path = GetDynamicBoneInfoFilePath();
|
|||
|
StreamReader sr = new StreamReader(path, Encoding.Default);
|
|||
|
string readStr = "";
|
|||
|
string fileInfoString = ""; // 整个文件的内容都存入
|
|||
|
while (sr != null && (readStr = sr.ReadLine()) != null)
|
|||
|
{
|
|||
|
fileInfoString += readStr;
|
|||
|
}
|
|||
|
sr.Close();
|
|||
|
SetLODDynamicBoneParams(fileInfoString);
|
|||
|
Repaint();
|
|||
|
EditorUtility.DisplayDialog("成功", "导入完毕", "确定");
|
|||
|
}
|
|||
|
|
|||
|
void LoadSBFileByName(string path)
|
|||
|
{
|
|||
|
// string path = GetDynamicBoneInfoFilePathByName(name);
|
|||
|
StreamReader sr = new StreamReader(path, Encoding.Default);
|
|||
|
string readStr = "";
|
|||
|
string fileInfoString = ""; // 整个文件的内容都存入
|
|||
|
while (sr != null && (readStr = sr.ReadLine()) != null)
|
|||
|
{
|
|||
|
fileInfoString += readStr;
|
|||
|
}
|
|||
|
sr.Close();
|
|||
|
SetLODDynamicBoneParams(fileInfoString);
|
|||
|
Repaint();
|
|||
|
// EditorUtility.DisplayDialog("成功", "导入完毕", "确定");
|
|||
|
}
|
|||
|
|
|||
|
void SetLODDynamicBoneParams(string fileInfoString)
|
|||
|
{
|
|||
|
string[] tags = fileInfoString.Split('!');
|
|||
|
string currTagKey;
|
|||
|
LODDynamicBoneParams currDBParam;
|
|||
|
foreach (string tagStr in tags)
|
|||
|
{
|
|||
|
currTagKey = "";
|
|||
|
currDBParam = new LODDynamicBoneParams(null, 0.02f, 0.08f, 0.06f, 0.8f, 0.6f, 0.02f, null);
|
|||
|
if (string.IsNullOrEmpty(tagStr)) continue;
|
|||
|
string[] pairStrs = tagStr.Split('%');
|
|||
|
|
|||
|
foreach (string pairStr in pairStrs)
|
|||
|
{
|
|||
|
if (string.IsNullOrEmpty(pairStr)) continue;
|
|||
|
string[] pair = pairStr.Split(':');
|
|||
|
string key = "";
|
|||
|
string value = "";
|
|||
|
if (pair.Length != 2)
|
|||
|
{
|
|||
|
Debug.LogError("数据对" + pairStr + "解析失败!");
|
|||
|
}
|
|||
|
if (!string.IsNullOrEmpty(pair[0])) key = pair[0];
|
|||
|
if (!string.IsNullOrEmpty(pair[1])) value = pair[1];
|
|||
|
// 简陋的字符匹配
|
|||
|
if (key.Contains("TagName"))
|
|||
|
{
|
|||
|
currTagKey = value;
|
|||
|
if (m_TagToDynamicBoneParamDict.ContainsKey(currTagKey))
|
|||
|
{
|
|||
|
currDBParam = m_TagToDynamicBoneParamDict[currTagKey]; // 应该是最先被赋值的,保证后面参数修改的都是拿到了有值的currDBParam
|
|||
|
}
|
|||
|
}
|
|||
|
else if (key.Contains("DB_Root"))
|
|||
|
{
|
|||
|
currDBParam.root = GetRootFromPath(value);
|
|||
|
}
|
|||
|
else if (key.Contains("DB_Damping"))
|
|||
|
{
|
|||
|
currDBParam.Damping = float.Parse(value);
|
|||
|
}
|
|||
|
else if (key.Contains("DB_Elasticty"))
|
|||
|
{
|
|||
|
currDBParam.Elasticty = float.Parse(value);
|
|||
|
}
|
|||
|
else if (key.Contains("DB_Stiffness"))
|
|||
|
{
|
|||
|
currDBParam.Stiffness = float.Parse(value);
|
|||
|
}
|
|||
|
else if (key.Contains("DB_Inert"))
|
|||
|
{
|
|||
|
currDBParam.Inert = float.Parse(value);
|
|||
|
}
|
|||
|
else if (key.Contains("DB_Friction"))
|
|||
|
{
|
|||
|
currDBParam.Friction = float.Parse(value);
|
|||
|
}
|
|||
|
else if (key.Contains("DB_Radius"))
|
|||
|
{
|
|||
|
currDBParam.Radius = float.Parse(value);
|
|||
|
}
|
|||
|
else if (key.Contains("SBC_List"))
|
|||
|
{
|
|||
|
SetColliders(currTagKey, ref currDBParam.colliderDataList, value);
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
Debug.LogError("读取文件发现异常内容:" + pairStr);
|
|||
|
}
|
|||
|
}
|
|||
|
m_TagToDynamicBoneParamDict[currTagKey] = currDBParam;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
void SetColliders(string tagName, ref List<LODDynamicColliderData> colliderDataList, string colliderInfoStr)
|
|||
|
{
|
|||
|
string[] collInfoStrs = colliderInfoStr.Split('$');
|
|||
|
foreach (string infoStrs in collInfoStrs)
|
|||
|
{
|
|||
|
if (string.IsNullOrEmpty(infoStrs)) continue;
|
|||
|
string[] infoStrArray = infoStrs.Split('@');
|
|||
|
string name = infoStrArray[1];
|
|||
|
float radius = float.Parse(infoStrArray[2]);
|
|||
|
string rootName = infoStrArray[3];
|
|||
|
string[] posArray = infoStrArray[4].Split('#');
|
|||
|
float x, y, z;
|
|||
|
if (posArray.Length != 3)
|
|||
|
{
|
|||
|
Debug.LogError("读取" + name + "collider坐标失败!");
|
|||
|
x = y = z = 0f;
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
x = float.Parse(posArray[0]);
|
|||
|
y = float.Parse(posArray[1]);
|
|||
|
z = float.Parse(posArray[2]);
|
|||
|
}
|
|||
|
// 查找colliderDataList是否存在同名Collider,存在则覆盖,不存在则新建
|
|||
|
bool isNeedAdd = true;
|
|||
|
LODDynamicColliderData scData;
|
|||
|
for (int i = 0; i < colliderDataList.Count; i++)
|
|||
|
{
|
|||
|
scData = colliderDataList[i];
|
|||
|
if (scData.name.Equals(name))
|
|||
|
{
|
|||
|
isNeedAdd = false;
|
|||
|
// 存在,覆盖
|
|||
|
scData.radius = radius;
|
|||
|
scData.rootName = rootName;
|
|||
|
scData.colTrans.localPosition = new Vector3(x, y, z);
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
// 新建并赋值
|
|||
|
if (isNeedAdd)
|
|||
|
{
|
|||
|
GameObject collider = new GameObject();
|
|||
|
collider.name = name;
|
|||
|
collider.transform.SetParent(GetTransformByName(tagName).GetChild(0));
|
|||
|
collider.transform.localPosition = new Vector3(x, y, z);
|
|||
|
DynamicBoneCollider dc = collider.AddComponent<DynamicBoneCollider>();
|
|||
|
dc.m_Radius = radius;
|
|||
|
scData = new LODDynamicColliderData(name, radius, rootName, collider.transform);
|
|||
|
colliderDataList.Add(scData);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
void WriteSBParamInfo(StreamWriter sw)
|
|||
|
{
|
|||
|
Dictionary<string, string> TagInfo = new Dictionary<string, string>();
|
|||
|
foreach (KeyValuePair<string, LODDynamicBoneParams> pair in m_TagToDynamicBoneParamDict)
|
|||
|
{
|
|||
|
LODDynamicBoneParams param = pair.Value;
|
|||
|
TagInfo = new Dictionary<string, string>
|
|||
|
{
|
|||
|
{"TagName:", pair.Key},
|
|||
|
{"DB_Root:", CreateDynamicBoneRoot(pair.Key, param.root==null?"":param.root.name)},
|
|||
|
{"DB_Damping:", param.Damping.ToString()},
|
|||
|
{"DB_Elasticty:", param.Elasticty.ToString()},
|
|||
|
{"DB_Stiffness:", param.Stiffness.ToString()},
|
|||
|
{"DB_Inert:", param.Inert.ToString()},
|
|||
|
{"DB_Friction:", param.Friction.ToString()},
|
|||
|
{"DB_Radius:", param.Radius.ToString()},
|
|||
|
{"SBC_List:", GetColliderListString(pair.Key, param.colliderDataList)}
|
|||
|
};
|
|||
|
sw.WriteLine('!');
|
|||
|
foreach (KeyValuePair<string, string> tag in TagInfo)
|
|||
|
{
|
|||
|
sw.WriteLine('%' + tag.Key + tag.Value);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
string GetColliderListString(string tagName, List<LODDynamicColliderData> colliderDataList)
|
|||
|
{
|
|||
|
string rst = "";
|
|||
|
string singleCollDataStr = "";
|
|||
|
if (colliderDataList == null) return "";
|
|||
|
for (int i = 0; i < colliderDataList.Count; i++)
|
|||
|
{
|
|||
|
singleCollDataStr = "";
|
|||
|
// 普通的参数普通地获取
|
|||
|
singleCollDataStr += "@" + colliderDataList[i].name;
|
|||
|
singleCollDataStr += "@" + colliderDataList[i].radius;
|
|||
|
singleCollDataStr += "@" + colliderDataList[i].rootName;
|
|||
|
// 厉害的参数遍历获取(?)
|
|||
|
foreach (Transform trans in m_DynamicBoneSenceObject.transform.GetComponentsInChildren<Transform>())
|
|||
|
{
|
|||
|
if (!trans.name.Equals(tagName)) continue;
|
|||
|
// 找到标记位下面同名的collider,拿到position
|
|||
|
foreach (DynamicBoneCollider collider in trans.GetComponentsInChildren<DynamicBoneCollider>())
|
|||
|
{
|
|||
|
if (!collider.transform.name.Equals(colliderDataList[i].name)) continue;
|
|||
|
Vector3 pos = collider.transform.localPosition;
|
|||
|
singleCollDataStr += "@" + pos.x + "#" + pos.y + "#" + pos.z; // @-0.21#0#0
|
|||
|
}
|
|||
|
}
|
|||
|
rst += singleCollDataStr + "$"; // collider单条信息以$结尾
|
|||
|
}
|
|||
|
return rst;
|
|||
|
}
|
|||
|
|
|||
|
string CreateDynamicBoneRoot(string parentName, string childName)
|
|||
|
{
|
|||
|
return parentName + "/" + childName;
|
|||
|
}
|
|||
|
|
|||
|
Transform GetRootFromPath(string path)
|
|||
|
{
|
|||
|
DynamicBone[] transList = m_DynamicBoneSenceObject.transform.GetComponentsInChildren<DynamicBone>();
|
|||
|
foreach(DynamicBone trans in transList)
|
|||
|
{
|
|||
|
if(path.Contains(trans.name) &&path.Contains(trans.transform.parent.name))
|
|||
|
{
|
|||
|
return trans.transform;
|
|||
|
}
|
|||
|
}
|
|||
|
return null;
|
|||
|
}
|
|||
|
|
|||
|
#endregion
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
```
|