519 lines
23 KiB
C#
519 lines
23 KiB
C#
#if UNITY_EDITOR
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Text.RegularExpressions;
|
||
using EventBossFight;
|
||
using FT.Timeline;
|
||
using UnityEditor;
|
||
#if UNITY_2021_2_OR_NEWER
|
||
using PrefabStageUtility = UnityEditor.SceneManagement.PrefabStageUtility;
|
||
using UnityEditor.SceneManagement;
|
||
#else
|
||
using PrefabStageUtility = UnityEditor.Experimental.SceneManagement.PrefabStageUtility;
|
||
#endif
|
||
using UnityEngine;
|
||
using UnityEngine.Playables;
|
||
using UnityEngine.Timeline;
|
||
|
||
namespace EventBossFight.Editor
|
||
{
|
||
/// <summary>
|
||
/// 在选中带 <see cref="PlayableDirector"/> 的 <c>boss</c> 节点上,按子物体 <c>spine0</c>… 批量生成
|
||
/// <c>bossfight_boss01_attack.playable</c> 等 Timeline:一条 <see cref="LeviathanVideoTrack"/> + 一个 <see cref="LeviathanVideoPlayableAsset"/>,
|
||
/// 受击片段:Timeline 文件名为 <c>bossfight_bossXX_attacked01.playable</c>,对应 bytes 为 <c>atlantis_bossXX_hit01</c> / <c>hit02</c>(旧名 <c>attacked01</c>/<c>attacked02</c> 可作 fallback)。
|
||
/// Director 绑定:各片段对应 <c>spine*</c> 下<strong>同级</strong>子物体——<c>idle</c>/<c>death</c>(或 <c>die</c>)/<c>attack</c>/<c>hit</c>/<c>hit_02</c> 上的解码器;
|
||
/// Timeline 文件名仍为 <c>attacked01</c>/<c>attacked02</c>,绑定到 <c>hit</c>/<c>hit_02</c>。缺子物体或无解码器时回退 <c>idle</c>。
|
||
/// <para>输出目录默认为<strong>选中物体所属 Prefab 所在目录下的 <c>timeline</c> 子文件夹</strong>;若不存在则自动创建。无法解析 Prefab 时可选手动指定目录。</para>
|
||
/// </summary>
|
||
public static class EventBossFightAtlantisBossTimelineGenerator
|
||
{
|
||
private static readonly Regex SpineChildRegex = new Regex(@"^spine_?(\d+)$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||
|
||
private const string BytesBossPrefix = "atlantis_boss";
|
||
|
||
/// <summary>
|
||
/// 第 1 项:<c>bossfight_bossXX_{项}.playable</c>;第 2~3 项:仅用于查找 <c>atlantis_bossXX_{第2项}.bytes</c>(受击 Timeline 名仍为 <c>attacked01</c>,bytes 主后缀为 <c>hit01</c>/<c>hit02</c>)。
|
||
/// spine 下子物体供 Director 绑定见 <see cref="ResolveBindingDecoderForTimelineSuffix"/>。
|
||
/// </summary>
|
||
private static readonly (string timelineFileSuffix, string bytesPrimarySuffix, string[] bytesFallback)[] Segments =
|
||
{
|
||
("idle", "idle", null),
|
||
("death", "death", new[] { "die" }),
|
||
("attack", "attack", null),
|
||
("attacked01", "hit01", new[] { "attacked01" }),
|
||
("attacked02", "hit02", new[] { "attacked02" }),
|
||
};
|
||
|
||
private const string MenuPath = "GameObject/EventBossFight/生成 Boss Leviathan Timeline(Leviathan 轨 + atlantis bytes)";
|
||
|
||
private const string TimelineSubFolderName = "timeline";
|
||
|
||
[MenuItem(MenuPath, false, 40)]
|
||
private static void GenerateFromSelectedBoss()
|
||
{
|
||
var boss = Selection.activeGameObject;
|
||
if (boss == null)
|
||
{
|
||
Debug.LogError("请先选中场景或 Prefab 中的 boss 物体(挂有 PlayableDirector)。");
|
||
return;
|
||
}
|
||
|
||
var director = boss.GetComponent<PlayableDirector>();
|
||
if (director == null)
|
||
{
|
||
Debug.LogError("选中物体上没有 PlayableDirector。", boss);
|
||
return;
|
||
}
|
||
|
||
if (!TryGetOrCreateTimelineFolderNearPrefab(boss, out var assetsFolder, out var folderHint))
|
||
{
|
||
if (!EditorUtility.DisplayDialog(
|
||
"生成 Timeline",
|
||
$"{folderHint}\n\n是否改为手动选择输出目录(须位于 Assets 下)?",
|
||
"手动选择",
|
||
"取消"))
|
||
return;
|
||
|
||
var absFolder = EditorUtility.SaveFolderPanel("选择生成 .playable 的目录", Application.dataPath, "");
|
||
if (string.IsNullOrEmpty(absFolder))
|
||
return;
|
||
|
||
assetsFolder = AbsolutePathToAssetsPath(absFolder);
|
||
if (string.IsNullOrEmpty(assetsFolder))
|
||
{
|
||
EditorUtility.DisplayDialog("生成 Timeline", "请选择工程 Assets 目录内的文件夹。", "确定");
|
||
return;
|
||
}
|
||
}
|
||
|
||
Undo.RecordObject(director, "生成 Boss Leviathan Timeline 绑定");
|
||
|
||
var spineChildren = new List<(Transform spineTf, int slot, int bossResourceIndex)>();
|
||
for (var i = 0; i < boss.transform.childCount; i++)
|
||
{
|
||
var ch = boss.transform.GetChild(i);
|
||
var m = SpineChildRegex.Match(ch.name);
|
||
if (!m.Success || !int.TryParse(m.Groups[1].Value, out var slot) || slot < 0)
|
||
continue;
|
||
spineChildren.Add((ch, slot, slot + 1));
|
||
}
|
||
|
||
if (spineChildren.Count == 0)
|
||
{
|
||
Debug.LogError($"在「{boss.name}」下未找到名为 spine0、spine1… 的子物体。", boss);
|
||
return;
|
||
}
|
||
|
||
spineChildren.Sort((a, b) => a.slot.CompareTo(b.slot));
|
||
|
||
var createdPaths = new List<string>();
|
||
var bindingPairs = new List<(TimelineAsset timeline, LeviathanVideoTrack track, LeviathanVideoDecoderBase decoder)>();
|
||
|
||
foreach (var (spineTf, _, bossResourceIndex) in spineChildren)
|
||
{
|
||
var idleDecoder = FindDecoderUnderLayer(spineTf, "idle");
|
||
var deathDecoder = FindDecoderUnderLayer(spineTf, "death") ?? FindDecoderUnderLayer(spineTf, "die");
|
||
var attackDecoder = FindDecoderUnderLayer(spineTf, "attack");
|
||
var hitDecoder = FindDecoderUnderLayer(spineTf, "hit");
|
||
var hit02Decoder = FindDecoderUnderLayer(spineTf, "hit_02");
|
||
if (idleDecoder == null)
|
||
Debug.LogWarning($"「{spineTf.name}」下未在 idle 子树找到 LeviathanVideoDecoderBase,仍将生成 Timeline;缺 idle 时其它片段回退绑定也会失败。", spineTf);
|
||
|
||
foreach (var (fileSuffix, bytesPrimary, bytesFallback) in Segments)
|
||
{
|
||
var bindDecoder = ResolveBindingDecoderForTimelineSuffix(fileSuffix, idleDecoder, deathDecoder,
|
||
attackDecoder, hitDecoder, hit02Decoder);
|
||
if (fileSuffix == "death" && deathDecoder == null)
|
||
Debug.LogWarning($"「{spineTf.name}」下无子物体 death/die 或未挂解码器,bossfight_boss{bossResourceIndex:D2}_death 将回退绑定 idle。", spineTf);
|
||
if (fileSuffix == "attack" && attackDecoder == null)
|
||
Debug.LogWarning($"「{spineTf.name}」下无子物体 attack 或未挂解码器,bossfight_boss{bossResourceIndex:D2}_attack 将回退绑定 idle。", spineTf);
|
||
if (fileSuffix == "attacked01" && hitDecoder == null)
|
||
Debug.LogWarning($"「{spineTf.name}」下无子物体 hit 或未挂解码器,bossfight_boss{bossResourceIndex:D2}_attacked01 将回退绑定 idle。", spineTf);
|
||
if (fileSuffix == "attacked02" && hit02Decoder == null)
|
||
Debug.LogWarning($"「{spineTf.name}」下无子物体 hit_02 或未挂解码器,bossfight_boss{bossResourceIndex:D2}_attacked02 将回退绑定 idle。", spineTf);
|
||
|
||
var fileBase = $"bossfight_boss{bossResourceIndex:D2}_{fileSuffix}";
|
||
var assetPath = $"{assetsFolder.TrimEnd('/')}/{fileBase}.playable".Replace("\\", "/");
|
||
if (AssetExistsAtPath(assetPath))
|
||
{
|
||
Debug.LogWarning($"已存在,跳过生成:{assetPath}");
|
||
var existing = AssetDatabase.LoadAssetAtPath<TimelineAsset>(assetPath);
|
||
if (existing != null && bindDecoder != null)
|
||
TryCollectBindingPair(existing, bindDecoder, bindingPairs);
|
||
continue;
|
||
}
|
||
|
||
var textAsset = FindTextAssetForBossBytes(bossResourceIndex, bytesPrimary, bytesFallback);
|
||
if (textAsset == null)
|
||
{
|
||
Debug.LogWarning($"未找到 TextAsset,跳过:atlantis_boss{bossResourceIndex:D2}_{bytesPrimary}(及 fallback)", spineTf.gameObject);
|
||
continue;
|
||
}
|
||
|
||
if (!TryCreateLeviathanTimeline(assetPath, fileBase, textAsset, out var timeline, out var track))
|
||
continue;
|
||
|
||
createdPaths.Add(assetPath);
|
||
if (bindDecoder != null)
|
||
bindingPairs.Add((timeline, track, bindDecoder));
|
||
}
|
||
|
||
AssignPlayableFieldsToBossCellVideoIfAny(spineTf, assetsFolder, bossResourceIndex);
|
||
}
|
||
|
||
AssetDatabase.SaveAssets();
|
||
|
||
var preferredBossIndex = spineChildren[0].bossResourceIndex;
|
||
ApplyBindingsAndPersistDirector(director, bindingPairs, preferredBossIndex);
|
||
|
||
AssetDatabase.SaveAssets();
|
||
AssetDatabase.Refresh();
|
||
|
||
Debug.Log(
|
||
$"Boss Timeline 生成完成:新建 {createdPaths.Count} 个资源,目录 {assetsFolder};已对 Director 写入 {bindingPairs.Count} 条轨道绑定(spine 下 idle/death|die/attack/hit/hit_02 按片段对应);当前 Director.playableAsset 已设为 bossfight_boss{preferredBossIndex:D2}_idle(若存在)。",
|
||
boss);
|
||
}
|
||
|
||
|
||
|
||
/// <summary>
|
||
/// 为每个 Timeline 写入 Director 绑定。结束后<strong>不要</strong>将 playableAsset 置空,否则 Unity 会丢掉已序列化的 Scene Bindings。
|
||
/// 默认把 playableAsset 设为最小槽位 spine 对应的 idle,便于在 Timeline 窗口里直接预览。
|
||
/// </summary>
|
||
private static void ApplyBindingsAndPersistDirector(
|
||
PlayableDirector director,
|
||
List<(TimelineAsset timeline, LeviathanVideoTrack track, LeviathanVideoDecoderBase decoder)> bindingPairs,
|
||
int preferredBossResourceIndex)
|
||
{
|
||
if (bindingPairs.Count == 0)
|
||
{
|
||
EditorUtility.SetDirty(director);
|
||
PersistDirectorAndScene(director);
|
||
return;
|
||
}
|
||
|
||
var idleName = $"bossfight_boss{preferredBossResourceIndex:D2}_idle";
|
||
TimelineAsset defaultPlayable = null;
|
||
|
||
// SaveAssets 之后用磁盘上的资源再取 Track,避免子资源引用未刷新导致 SetGenericBinding 无效
|
||
foreach (var (timeline, _, decoder) in bindingPairs)
|
||
{
|
||
if (timeline == null || decoder == null)
|
||
continue;
|
||
var path = AssetDatabase.GetAssetPath(timeline);
|
||
var tl = string.IsNullOrEmpty(path)
|
||
? timeline
|
||
: AssetDatabase.LoadAssetAtPath<TimelineAsset>(path);
|
||
if (tl == null)
|
||
continue;
|
||
var tr = tl.GetOutputTracks().OfType<LeviathanVideoTrack>().FirstOrDefault();
|
||
if (tr == null)
|
||
continue;
|
||
|
||
director.playableAsset = tl;
|
||
director.SetGenericBinding(tr, decoder);
|
||
|
||
if (tl.name == idleName)
|
||
defaultPlayable = tl;
|
||
}
|
||
|
||
if (defaultPlayable == null)
|
||
{
|
||
var firstPath = AssetDatabase.GetAssetPath(bindingPairs[0].timeline);
|
||
defaultPlayable = string.IsNullOrEmpty(firstPath)
|
||
? bindingPairs[0].timeline
|
||
: AssetDatabase.LoadAssetAtPath<TimelineAsset>(firstPath);
|
||
}
|
||
|
||
director.playableAsset = defaultPlayable;
|
||
EditorUtility.SetDirty(director);
|
||
PersistDirectorAndScene(director);
|
||
}
|
||
|
||
private static void PersistDirectorAndScene(PlayableDirector director)
|
||
{
|
||
if (director == null)
|
||
return;
|
||
if (PrefabUtility.IsPartOfPrefabInstance(director))
|
||
PrefabUtility.RecordPrefabInstancePropertyModifications(director);
|
||
#if UNITY_2021_2_OR_NEWER
|
||
if (!EditorApplication.isPlaying && director.gameObject != null && director.gameObject.scene.IsValid())
|
||
EditorSceneManager.MarkSceneDirty(director.gameObject.scene);
|
||
#endif
|
||
}
|
||
|
||
/// <summary>
|
||
/// 选中物体所属 Prefab 资源所在目录下的 <c>timeline</c> 文件夹(Assets 路径);不存在则创建。
|
||
/// </summary>
|
||
private static bool TryGetOrCreateTimelineFolderNearPrefab(GameObject boss, out string assetsFolder, out string hint)
|
||
{
|
||
assetsFolder = null;
|
||
hint = null;
|
||
|
||
var go = boss;
|
||
// 与 AutoBindTimelineByFieldNameMenu 一致:实例用 NearestInstanceRoot;Prefab 编辑模式用 PrefabStage.assetPath(无 FirstInstanceRoot API)
|
||
var prefabAssetPath = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(go);
|
||
if (string.IsNullOrEmpty(prefabAssetPath))
|
||
{
|
||
var outer = PrefabUtility.GetOutermostPrefabInstanceRoot(go);
|
||
if (outer != null)
|
||
prefabAssetPath = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(outer);
|
||
}
|
||
|
||
if (string.IsNullOrEmpty(prefabAssetPath))
|
||
{
|
||
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
|
||
if (prefabStage != null)
|
||
prefabAssetPath = prefabStage.assetPath;
|
||
}
|
||
|
||
if (string.IsNullOrEmpty(prefabAssetPath))
|
||
{
|
||
hint = "未解析到 Prefab 资源路径(例如纯场景物体、未保存的临时对象)。";
|
||
return false;
|
||
}
|
||
|
||
var prefabDir = Path.GetDirectoryName(prefabAssetPath)?.Replace("\\", "/");
|
||
if (string.IsNullOrEmpty(prefabDir) || !prefabDir.StartsWith("Assets/", System.StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
hint = $"Prefab 目录无效:{prefabDir}";
|
||
return false;
|
||
}
|
||
|
||
assetsFolder = $"{prefabDir}/{TimelineSubFolderName}".Replace("\\", "/");
|
||
if (AssetDatabase.IsValidFolder(assetsFolder))
|
||
return true;
|
||
|
||
try
|
||
{
|
||
AssetDatabase.CreateFolder(prefabDir, TimelineSubFolderName);
|
||
}
|
||
catch (System.Exception e)
|
||
{
|
||
hint = $"无法在「{prefabDir}」下创建子文件夹「{TimelineSubFolderName}」:{e.Message}";
|
||
return false;
|
||
}
|
||
|
||
if (!AssetDatabase.IsValidFolder(assetsFolder))
|
||
{
|
||
hint = $"创建后仍无法识别文件夹:{assetsFolder}";
|
||
return false;
|
||
}
|
||
|
||
AssetDatabase.Refresh();
|
||
return true;
|
||
}
|
||
|
||
[MenuItem(MenuPath, true)]
|
||
private static bool ValidateGenerateFromSelectedBoss()
|
||
{
|
||
var go = Selection.activeGameObject;
|
||
return go != null && go.GetComponent<PlayableDirector>() != null;
|
||
}
|
||
|
||
private static void AssignPlayableFieldsToBossCellVideoIfAny(Transform spineTf, string assetsFolder, int bossResourceIndex)
|
||
{
|
||
var cell = spineTf.GetComponent<EventBossFightTimelineBossCellVideo>();
|
||
if (cell == null)
|
||
return;
|
||
|
||
var so = new SerializedObject(cell);
|
||
Undo.RecordObject(cell, "写入 Timeline 引用到 BossCellVideo");
|
||
|
||
foreach (var (fieldKey, nameSuffix) in new[]
|
||
{
|
||
("idleTimeline", "idle"),
|
||
("deathTimeline", "death"),
|
||
("attackTimeline", "attack"),
|
||
("attacked1Timeline", "attacked01"),
|
||
("attacked2Timeline", "attacked02"),
|
||
})
|
||
{
|
||
var path = $"{assetsFolder.TrimEnd('/')}/bossfight_boss{bossResourceIndex:D2}_{nameSuffix}.playable".Replace("\\", "/");
|
||
var ta = AssetDatabase.LoadAssetAtPath<TimelineAsset>(path);
|
||
if (ta == null)
|
||
continue;
|
||
var prop = so.FindProperty(fieldKey);
|
||
if (prop != null && prop.propertyType == SerializedPropertyType.ObjectReference)
|
||
prop.objectReferenceValue = ta;
|
||
}
|
||
|
||
so.ApplyModifiedProperties();
|
||
EditorUtility.SetDirty(cell);
|
||
if (PrefabUtility.IsPartOfPrefabInstance(cell))
|
||
PrefabUtility.RecordPrefabInstancePropertyModifications(cell);
|
||
}
|
||
|
||
private static void TryCollectBindingPair(TimelineAsset timeline, LeviathanVideoDecoderBase decoder,
|
||
List<(TimelineAsset timeline, LeviathanVideoTrack track, LeviathanVideoDecoderBase decoder)> bindingPairs)
|
||
{
|
||
var track = timeline.GetOutputTracks().OfType<LeviathanVideoTrack>().FirstOrDefault();
|
||
if (track != null && decoder != null)
|
||
bindingPairs.Add((timeline, track, decoder));
|
||
}
|
||
|
||
private static bool TryCreateLeviathanTimeline(string assetPath, string displayBaseName, TextAsset textAsset,
|
||
out TimelineAsset timeline, out LeviathanVideoTrack track)
|
||
{
|
||
timeline = null;
|
||
track = null;
|
||
|
||
var timelineSo = ScriptableObject.CreateInstance<TimelineAsset>();
|
||
timelineSo.name = displayBaseName;
|
||
AssetDatabase.CreateAsset(timelineSo, assetPath);
|
||
timeline = timelineSo;
|
||
|
||
track = timeline.CreateTrack<LeviathanVideoTrack>(null, "Leviathan Video");
|
||
var timelineClip = track.CreateClip<LeviathanVideoPlayableAsset>();
|
||
timelineClip.start = 0;
|
||
timelineClip.displayName = displayBaseName;
|
||
|
||
var clipAsset = timelineClip.asset as LeviathanVideoPlayableAsset;
|
||
if (clipAsset == null)
|
||
{
|
||
Debug.LogError($"创建 Clip 失败:{assetPath}");
|
||
return false;
|
||
}
|
||
|
||
clipAsset.videoOverride = textAsset;
|
||
if (LeviathanVideoDecoderBase.TryEditorProbeVideoMetaFromBytes(textAsset, out var dur, out _, out _) && dur > 0.0001)
|
||
clipAsset.clipDurationSeconds = dur;
|
||
else
|
||
clipAsset.clipDurationSeconds = 5.0;
|
||
|
||
timelineClip.duration = clipAsset.duration;
|
||
|
||
EditorUtility.SetDirty(clipAsset);
|
||
EditorUtility.SetDirty(track);
|
||
EditorUtility.SetDirty(timeline);
|
||
|
||
return true;
|
||
}
|
||
|
||
private static LeviathanVideoDecoderBase FindDecoderUnderLayer(Transform spineRoot, string layerName)
|
||
{
|
||
var layerTf = FindLayerTransformUnderRoot(spineRoot, layerName);
|
||
if (layerTf == null)
|
||
return null;
|
||
return layerTf.GetComponentInChildren<LeviathanVideoDecoderBase>(true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// <c>spine*</c> 下与片段语义对应的<strong>同级</strong>子物体:<c>idle</c>/<c>death</c>(调用方已合并 <c>die</c>)/<c>attack</c>;<c>attacked01</c>/<c>attacked02</c> → <c>hit</c>/<c>hit_02</c>。缺则回退 <c>idle</c>。
|
||
/// </summary>
|
||
private static LeviathanVideoDecoderBase ResolveBindingDecoderForTimelineSuffix(string timelineFileSuffix,
|
||
LeviathanVideoDecoderBase idleDecoder, LeviathanVideoDecoderBase deathDecoder,
|
||
LeviathanVideoDecoderBase attackDecoder, LeviathanVideoDecoderBase hitDecoder,
|
||
LeviathanVideoDecoderBase hit02Decoder)
|
||
{
|
||
switch (timelineFileSuffix)
|
||
{
|
||
case "idle":
|
||
return idleDecoder;
|
||
case "death":
|
||
return deathDecoder ?? idleDecoder;
|
||
case "attack":
|
||
return attackDecoder ?? idleDecoder;
|
||
case "attacked01":
|
||
return hitDecoder ?? idleDecoder;
|
||
case "attacked02":
|
||
return hit02Decoder ?? idleDecoder;
|
||
default:
|
||
return idleDecoder;
|
||
}
|
||
}
|
||
|
||
private static Transform FindLayerTransformUnderRoot(Transform root, string layerName)
|
||
{
|
||
if (!root)
|
||
return null;
|
||
var found = root.gameObject.FindChildGameObject(layerName);
|
||
if (found)
|
||
return found.transform;
|
||
foreach (var t in root.GetComponentsInChildren<Transform>(true))
|
||
{
|
||
if (t == root)
|
||
continue;
|
||
if (t.name == layerName)
|
||
return t;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static IEnumerable<string> BuildAtlantisBytesBaseNames(int bossIndex, string suffix)
|
||
{
|
||
var d2 = $"{BytesBossPrefix}{bossIndex:D2}_{suffix}";
|
||
var plain = $"{BytesBossPrefix}{bossIndex}_{suffix}";
|
||
if (d2 == plain)
|
||
yield return d2;
|
||
else
|
||
{
|
||
yield return d2;
|
||
yield return plain;
|
||
}
|
||
}
|
||
|
||
private static TextAsset FindTextAssetForBossBytes(int bossIndex, string primarySuffix, string[] fallbackSuffixes)
|
||
{
|
||
foreach (var baseName in BuildAtlantisBytesBaseNames(bossIndex, primarySuffix))
|
||
{
|
||
var ta = FindTextAssetByFileName(baseName);
|
||
if (ta != null)
|
||
return ta;
|
||
}
|
||
|
||
if (fallbackSuffixes == null)
|
||
return null;
|
||
foreach (var suf in fallbackSuffixes)
|
||
{
|
||
if (suf == primarySuffix)
|
||
continue;
|
||
foreach (var baseName in BuildAtlantisBytesBaseNames(bossIndex, suf))
|
||
{
|
||
var ta = FindTextAssetByFileName(baseName);
|
||
if (ta != null)
|
||
return ta;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static TextAsset FindTextAssetByFileName(string fileNameWithoutExtension)
|
||
{
|
||
var guids = AssetDatabase.FindAssets($"{fileNameWithoutExtension} t:TextAsset");
|
||
foreach (var guid in guids)
|
||
{
|
||
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||
if (Path.GetFileNameWithoutExtension(path) != fileNameWithoutExtension)
|
||
continue;
|
||
var asset = AssetDatabase.LoadAssetAtPath<TextAsset>(path);
|
||
if (asset != null)
|
||
return asset;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private static bool AssetExistsAtPath(string assetPath)
|
||
{
|
||
return AssetDatabase.LoadAssetAtPath<Object>(assetPath) != null;
|
||
}
|
||
|
||
private static string AbsolutePathToAssetsPath(string absolutePath)
|
||
{
|
||
if (string.IsNullOrEmpty(absolutePath))
|
||
return null;
|
||
absolutePath = absolutePath.Replace("\\", "/");
|
||
var dataPath = Application.dataPath.Replace("\\", "/");
|
||
if (!absolutePath.StartsWith(dataPath, System.StringComparison.OrdinalIgnoreCase))
|
||
return null;
|
||
var rel = absolutePath.Substring(dataPath.Length).TrimStart('/');
|
||
return string.IsNullOrEmpty(rel) ? "Assets" : "Assets/" + rel;
|
||
}
|
||
}
|
||
}
|
||
#endif
|