Compare commits
4
Commits
bd99283f17
...
52b0e682a8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52b0e682a8 | ||
|
|
08b1e3d95a | ||
|
|
d6d120cb61 | ||
|
|
0402ca9b6c |
No files matched your search
+2
-2
@@ -6,7 +6,7 @@ AnimationClip:
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: "Anim_Player_\uACF5\uC911"
|
||||
m_Name: "Anim_Common_\uACF5\uC911"
|
||||
serializedVersion: 7
|
||||
m_Legacy: 0
|
||||
m_Compressed: 0
|
||||
@@ -36709,5 +36709,5 @@ AnimationClip:
|
||||
m_EditorCurves: []
|
||||
m_EulerEditorCurves: []
|
||||
m_HasGenericRootTransform: 0
|
||||
m_HasMotionFloatCurves: 0
|
||||
m_HasMotionFloatCurves: 1
|
||||
m_Events: []
|
||||
File renamed without changes.
+2
-2
@@ -6,7 +6,7 @@ AnimationClip:
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: "Anim_Player_\uC810\uD504"
|
||||
m_Name: "Anim_Common_\uC810\uD504"
|
||||
serializedVersion: 7
|
||||
m_Legacy: 0
|
||||
m_Compressed: 0
|
||||
@@ -17989,5 +17989,5 @@ AnimationClip:
|
||||
m_EditorCurves: []
|
||||
m_EulerEditorCurves: []
|
||||
m_HasGenericRootTransform: 0
|
||||
m_HasMotionFloatCurves: 0
|
||||
m_HasMotionFloatCurves: 1
|
||||
m_Events: []
|
||||
File renamed without changes.
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+2
-2
@@ -6,7 +6,7 @@ AnimationClip:
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: "Anim_Drog_\uC810\uD504"
|
||||
m_Name: "Anim_Drog_\uC810\uD504\uACF5\uC911"
|
||||
serializedVersion: 7
|
||||
m_Legacy: 0
|
||||
m_Compressed: 0
|
||||
@@ -28819,5 +28819,5 @@ AnimationClip:
|
||||
m_EditorCurves: []
|
||||
m_EulerEditorCurves: []
|
||||
m_HasGenericRootTransform: 0
|
||||
m_HasMotionFloatCurves: 0
|
||||
m_HasMotionFloatCurves: 1
|
||||
m_Events: []
|
||||
File renamed without changes.
+2
-2
@@ -6,7 +6,7 @@ AnimationClip:
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: "Anim_Drog_\uC810\uD504 1"
|
||||
m_Name: "Anim_Drog_\uC810\uD504\uC2DC\uC791"
|
||||
serializedVersion: 7
|
||||
m_Legacy: 0
|
||||
m_Compressed: 0
|
||||
@@ -27586,5 +27586,5 @@ AnimationClip:
|
||||
m_EditorCurves: []
|
||||
m_EulerEditorCurves: []
|
||||
m_HasGenericRootTransform: 0
|
||||
m_HasMotionFloatCurves: 0
|
||||
m_HasMotionFloatCurves: 1
|
||||
m_Events: []
|
||||
File renamed without changes.
+1
-1
@@ -6,7 +6,7 @@ AnimationClip:
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: "Anim_Drog_\uC810\uD504 2"
|
||||
m_Name: "Anim_Drog_\uC810\uD504\uCC29\uC9C0"
|
||||
serializedVersion: 7
|
||||
m_Legacy: 0
|
||||
m_Compressed: 0
|
||||
File renamed without changes.
+93983
-64226
File diff suppressed because it is too large.
Load diff
File renamed without changes.
+40304
-35580
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 16f9ef4ddd32aba44a2d0c691957e72f
|
||||
guid: 920ea8a73bbf84849b01d3875ff4e4c3
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a180d15c7b07a64485c8dd4ec7a1fa7
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a8845febff04ecb48b25dac5321c4481
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 190622b0cba48234ba7fc295facac207
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f43438b6095588f4fb4715bd6df16df8
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 38a21eded51c5b24bb70a48d387aa565
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 92a17c8d63463f741a0e9d305a838993
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 79ef70f9bb079cf4799a4f6935b8d984
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+2
-2
@@ -6,7 +6,7 @@ AnimationClip:
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: "Anim_Player_\uAC15\uD0C0"
|
||||
m_Name: "Anim_Player_\uC591\uC190_\uAC15\uD0C0"
|
||||
serializedVersion: 7
|
||||
m_Legacy: 0
|
||||
m_Compressed: 0
|
||||
@@ -147393,5 +147393,5 @@ AnimationClip:
|
||||
m_EditorCurves: []
|
||||
m_EulerEditorCurves: []
|
||||
m_HasGenericRootTransform: 0
|
||||
m_HasMotionFloatCurves: 0
|
||||
m_HasMotionFloatCurves: 1
|
||||
m_Events: []
|
||||
File renamed without changes.
+110490
-134151
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 15519f6d5b8e5ba4194d4b8256ebb60d
|
||||
guid: d6622f6bb6d593c48b903000286de0b6
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
File diff suppressed because it is too large.
Load diff
File renamed without changes.
+1
-1
@@ -6,7 +6,7 @@ AnimationClip:
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: "Anim_Common_\uCC0C\uB974\uAE30"
|
||||
m_Name: "Anim_Player_\uC591\uC190_\uCC0C\uB974\uAE30"
|
||||
serializedVersion: 7
|
||||
m_Legacy: 0
|
||||
m_Compressed: 0
|
||||
File renamed without changes.
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 27f34978bd8e5174cb07562401cea581
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 12bfabc84bb078b41b91dcb0e73034ff
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 47db64c106703ee498e4495d1c434b77
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 665885351f6fc9d4c8b188498edb3d7d
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c2676ac491a6fc94eb042b76a9c3406e
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+34334
-2213
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6e73cca667dfcd4499c84d1b6ba9d531
|
||||
guid: 30b04e3f2e060db4aabfd308ac605a62
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
+41646
-5169
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fee9942923e37e64eb04557cd4e28cdf
|
||||
guid: 9b19fd7edff49ae4681b653285f3a162
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 7400000
|
||||
@@ -2144,15 +2144,16 @@ MonoBehaviour:
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
animator: {fileID: 4019041888965840580}
|
||||
baseController: {fileID: 9100000, guid: 4bd980f1a222c5b468136f7e717925d5, type: 2}
|
||||
baseSkillClip: {fileID: -7717634560727564301, guid: 4005a77aa7d531742b1de1bec27001b1, type: 3}
|
||||
baseSkillClip: {fileID: 7400000, guid: ace5d7e8f405a8846b98bb956f0a9313, type: 2}
|
||||
clipAutoRegisterFilter: _Drog_
|
||||
registeredClips:
|
||||
- {fileID: -242498254790479478, guid: 585e8961b6c6e9f4ba96bdb4ffb2cbc3, type: 3}
|
||||
- {fileID: 3627526391332626453, guid: 39aaec38fc96c4842b972f1e991e5a46, type: 3}
|
||||
- {fileID: -7717634560727564301, guid: 4005a77aa7d531742b1de1bec27001b1, type: 3}
|
||||
- {fileID: -8265974341663887746, guid: d3e4690f866332b43b86ee7005291cd0, type: 3}
|
||||
- {fileID: 712281148059590495, guid: b590c58b50c3b554687b172862fa5d9d, type: 3}
|
||||
- {fileID: 6888780564265376159, guid: 827dfeae95fdf6b41b78698f2e846b5f, type: 3}
|
||||
- {fileID: -8752051743343580635, guid: 5eaeca917bbeb494eb14ad0e0552c42f, type: 3}
|
||||
- {fileID: 7400000, guid: 58847e89d27d1b140b1075bba68445c0, type: 2}
|
||||
- {fileID: 7400000, guid: 6e73cca667dfcd4499c84d1b6ba9d531, type: 2}
|
||||
- {fileID: 7400000, guid: ace5d7e8f405a8846b98bb956f0a9313, type: 2}
|
||||
- {fileID: 7400000, guid: 006b9541ce7dcb3469d79f9d9df3d6df, type: 2}
|
||||
- {fileID: 7400000, guid: 0b70d6464b876144c84f2410c0359a4f, type: 2}
|
||||
- {fileID: 7400000, guid: e35d6eb3ae2c5a146801c9dd399acd52, type: 2}
|
||||
- {fileID: 7400000, guid: 4ff85a68bb491e143a001f3af82639ed, type: 2}
|
||||
- {fileID: 7400000, guid: c8fdea7dee0c6f04bbd27fe565071682, type: 2}
|
||||
debugMode: 1
|
||||
showAreaDebug: 1
|
||||
|
||||
@@ -257,15 +257,14 @@ namespace Colosseum.Combat.Simulation
|
||||
};
|
||||
|
||||
float resolvedAnimationSpeed = loadoutEntry.GetResolvedAnimationSpeed();
|
||||
float mainClipDuration = ResolveClipDuration(skill.SkillClip, resolvedAnimationSpeed);
|
||||
float endClipDuration = ResolveClipDuration(skill.EndClip, 1f);
|
||||
float totalClipDuration = ResolveTotalClipDuration(skill.AnimationClips, resolvedAnimationSpeed);
|
||||
int repeatCount = loadoutEntry.GetResolvedRepeatCount();
|
||||
snapshot.castDuration = Mathf.Max(MinimumActionDuration, (mainClipDuration * repeatCount) + endClipDuration + ruleSet.MovementLossSecondsPerCast);
|
||||
snapshot.castDuration = Mathf.Max(MinimumActionDuration, (totalClipDuration * repeatCount) + ruleSet.MovementLossSecondsPerCast);
|
||||
|
||||
Dictionary<int, List<SkillEffect>> effectMap = new Dictionary<int, List<SkillEffect>>();
|
||||
loadoutEntry.CollectTriggeredEffects(effectMap);
|
||||
|
||||
BuildDamageEvents(snapshot, effectMap, context, weaponDamageMultiplier, ruleSet, mainClipDuration, resolvedAnimationSpeed, repeatCount, warnings);
|
||||
BuildDamageEvents(snapshot, effectMap, context, weaponDamageMultiplier, ruleSet, totalClipDuration, resolvedAnimationSpeed, repeatCount, warnings);
|
||||
snapshots[slotIndex] = snapshot;
|
||||
}
|
||||
|
||||
@@ -278,7 +277,7 @@ namespace Colosseum.Combat.Simulation
|
||||
SimulationContext context,
|
||||
float weaponDamageMultiplier,
|
||||
SimulationRuleSet ruleSet,
|
||||
float mainClipDuration,
|
||||
float totalClipDuration,
|
||||
float resolvedAnimationSpeed,
|
||||
int repeatCount,
|
||||
List<string> warnings)
|
||||
@@ -286,15 +285,30 @@ namespace Colosseum.Combat.Simulation
|
||||
if (snapshot == null || effectMap == null || effectMap.Count == 0)
|
||||
return;
|
||||
|
||||
// 모든 클립에서 OnEffect 이벤트를 수집합니다.
|
||||
List<AnimationEvent> effectEvents = new List<AnimationEvent>();
|
||||
AnimationClip clip = snapshot.skill.SkillClip;
|
||||
if (clip != null)
|
||||
IReadOnlyList<AnimationClip> clips = snapshot.skill.AnimationClips;
|
||||
if (clips != null)
|
||||
{
|
||||
float timeOffset = 0f;
|
||||
for (int clipIndex = 0; clipIndex < clips.Count; clipIndex++)
|
||||
{
|
||||
AnimationClip clip = clips[clipIndex];
|
||||
if (clip == null) continue;
|
||||
|
||||
AnimationEvent[] clipEvents = clip.events;
|
||||
for (int i = 0; i < clipEvents.Length; i++)
|
||||
{
|
||||
if (string.Equals(clipEvents[i].functionName, "OnEffect", StringComparison.Ordinal))
|
||||
effectEvents.Add(clipEvents[i]);
|
||||
{
|
||||
// 이벤트 시간에 이전 클립들의 누적 길이를 더합니다.
|
||||
AnimationEvent offsetEvent = clipEvents[i];
|
||||
offsetEvent.time += timeOffset;
|
||||
effectEvents.Add(offsetEvent);
|
||||
}
|
||||
}
|
||||
|
||||
timeOffset += clip.length;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,7 +317,7 @@ namespace Colosseum.Combat.Simulation
|
||||
|
||||
for (int iteration = 0; iteration < repeatCount; iteration++)
|
||||
{
|
||||
float iterationOffset = mainClipDuration * iteration;
|
||||
float iterationOffset = totalClipDuration * iteration;
|
||||
|
||||
for (int eventIndex = 0; eventIndex < effectEvents.Count; eventIndex++)
|
||||
{
|
||||
@@ -546,6 +560,23 @@ namespace Colosseum.Combat.Simulation
|
||||
return clip.length / Mathf.Max(0.05f, speed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 클립 목록 전체의 재생 시간을 합산합니다.
|
||||
/// </summary>
|
||||
private static float ResolveTotalClipDuration(IReadOnlyList<AnimationClip> clips, float speed)
|
||||
{
|
||||
if (clips == null || clips.Count == 0)
|
||||
return 0f;
|
||||
|
||||
float total = 0f;
|
||||
for (int i = 0; i < clips.Count; i++)
|
||||
{
|
||||
total += ResolveClipDuration(clips[i], speed);
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
private static List<int> CollectValidPrioritySlots(RotationPolicy rotationPolicy, SkillRuntimeSnapshot[] snapshots)
|
||||
{
|
||||
List<int> validSlots = new List<int>();
|
||||
|
||||
@@ -345,13 +345,28 @@ public static class AnimationClipExtractor
|
||||
SerializedObject so = new SerializedObject(skillData);
|
||||
bool modified = false;
|
||||
|
||||
if (TryRemapClip(so.FindProperty("skillClip"), fbxToAnimMap, path, "skillClip"))
|
||||
// animationClips 리스트의 각 요소를 remap합니다.
|
||||
SerializedProperty clipsProp = so.FindProperty("animationClips");
|
||||
if (clipsProp != null)
|
||||
{
|
||||
for (int i = 0; i < clipsProp.arraySize; i++)
|
||||
{
|
||||
if (TryRemapClip(clipsProp.GetArrayElementAtIndex(i), fbxToAnimMap, path, $"animationClips[{i}]"))
|
||||
{
|
||||
modified = true;
|
||||
relinkCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 레거시 필드도 remap (마이그레이션 전 애셋 보호)
|
||||
if (TryRemapClip(so.FindProperty("legacySkillClip"), fbxToAnimMap, path, "legacySkillClip"))
|
||||
{
|
||||
modified = true;
|
||||
relinkCount++;
|
||||
}
|
||||
|
||||
if (TryRemapClip(so.FindProperty("endClip"), fbxToAnimMap, path, "endClip"))
|
||||
if (TryRemapClip(so.FindProperty("legacyEndClip"), fbxToAnimMap, path, "legacyEndClip"))
|
||||
{
|
||||
modified = true;
|
||||
relinkCount++;
|
||||
@@ -567,8 +582,11 @@ public static class AnimationClipExtractor
|
||||
SkillData skillData = AssetDatabase.LoadAssetAtPath<SkillData>(path);
|
||||
if (skillData == null) continue;
|
||||
|
||||
if (skillData.SkillClip != null) clips.Add(skillData.SkillClip);
|
||||
if (skillData.EndClip != null) clips.Add(skillData.EndClip);
|
||||
IReadOnlyList<AnimationClip> skillClips = skillData.AnimationClips;
|
||||
for (int i = 0; i < skillClips.Count; i++)
|
||||
{
|
||||
if (skillClips[i] != null) clips.Add(skillClips[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// ── BossPhaseData ──
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
|
||||
using UnityEditor;
|
||||
|
||||
using Colosseum.Skills;
|
||||
|
||||
namespace Colosseum.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Animations 폴더 내 애니메이션 클립이 변경(생성/수정/삭제/이동)되면
|
||||
/// 관련 SkillData의 애니메이션 클립 목록을 자동 갱신합니다.
|
||||
/// </summary>
|
||||
public class AnimationClipSkillDataMatcher : AssetPostprocessor
|
||||
{
|
||||
private const string AnimationsFolderPath = "Assets/_Game/Animations";
|
||||
|
||||
private static void OnPostprocessAllAssets(
|
||||
string[] importedAssets,
|
||||
string[] deletedAssets,
|
||||
string[] movedAssets,
|
||||
string[] movedFromAssetPaths)
|
||||
{
|
||||
bool hasAnimationChange = false;
|
||||
|
||||
for (int i = 0; i < importedAssets.Length; i++)
|
||||
{
|
||||
if (IsAnimationClipPath(importedAssets[i]))
|
||||
{
|
||||
hasAnimationChange = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasAnimationChange)
|
||||
{
|
||||
for (int i = 0; i < deletedAssets.Length; i++)
|
||||
{
|
||||
if (IsAnimationClipPath(deletedAssets[i]))
|
||||
{
|
||||
hasAnimationChange = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasAnimationChange)
|
||||
{
|
||||
for (int i = 0; i < movedAssets.Length; i++)
|
||||
{
|
||||
if (IsAnimationClipPath(movedAssets[i]) || IsAnimationClipPath(movedFromAssetPaths[i]))
|
||||
{
|
||||
hasAnimationChange = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasAnimationChange)
|
||||
return;
|
||||
|
||||
RefreshAllSkillDataClips();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정한 경로가 Animations 폴더 내의 애니메이션 클립인지 확인합니다.
|
||||
/// </summary>
|
||||
private static bool IsAnimationClipPath(string assetPath)
|
||||
{
|
||||
return assetPath != null
|
||||
&& assetPath.StartsWith(AnimationsFolderPath)
|
||||
&& assetPath.EndsWith(".anim", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 SkillData의 애니메이션 클립 목록을 갱신합니다.
|
||||
/// </summary>
|
||||
private static void RefreshAllSkillDataClips()
|
||||
{
|
||||
string[] skillGuids = AssetDatabase.FindAssets("t:SkillData");
|
||||
int refreshedCount = 0;
|
||||
|
||||
for (int i = 0; i < skillGuids.Length; i++)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(skillGuids[i]);
|
||||
SkillData skillData = AssetDatabase.LoadAssetAtPath<SkillData>(path);
|
||||
if (skillData == null) continue;
|
||||
|
||||
int clipCountBefore = skillData.AnimationClips.Count;
|
||||
skillData.RefreshAnimationClips();
|
||||
|
||||
if (skillData.AnimationClips.Count != clipCountBefore)
|
||||
refreshedCount++;
|
||||
}
|
||||
|
||||
if (refreshedCount > 0)
|
||||
{
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c4524bdb16f89724b96d588200b8a3e8
|
||||
@@ -54,18 +54,28 @@ public class AnimationSectionSpeedEditor : EditorWindow
|
||||
|
||||
// ── 클립 정보 (실제 키프레임 기반) ──
|
||||
float actualLength = GetActualLastKeyframeTime(clip);
|
||||
bool hasBoneCurves = actualLength > 0f;
|
||||
|
||||
// 본 트랜스폼 곡선이 없으면 m_StopTime을 기준으로 사용
|
||||
// (Mixamo export 클립 등 m_FloatCurves만 있는 경우)
|
||||
float effectiveLength = hasBoneCurves ? actualLength : clip.length;
|
||||
|
||||
EditorGUILayout.LabelField("Clip Info", EditorStyles.boldLabel);
|
||||
EditorGUI.indentLevel++;
|
||||
EditorGUILayout.LabelField("Length (keyframes)", $"{actualLength:F3}s");
|
||||
EditorGUILayout.LabelField("m_StopTime", $"{clip.length:F3}s");
|
||||
EditorGUILayout.LabelField("Length", $"{effectiveLength:F3}s");
|
||||
|
||||
if (Mathf.Abs(clip.length - actualLength) > 0.01f)
|
||||
if (hasBoneCurves && Mathf.Abs(clip.length - actualLength) > 0.01f)
|
||||
{
|
||||
EditorGUILayout.LabelField("m_StopTime", $"{clip.length:F3}s");
|
||||
EditorGUILayout.HelpBox($"m_StopTime({clip.length:F3}s)이 실제 키프레임 길이({actualLength:F3}s)와 다릅니다.\n" +
|
||||
"스피드 변경 시 실제 키프레임 길이를 기준으로 계산합니다.", MessageType.Warning);
|
||||
}
|
||||
else if (!hasBoneCurves)
|
||||
{
|
||||
EditorGUILayout.LabelField(" (본 곡선 없음, m_StopTime 기준)", EditorStyles.miniLabel);
|
||||
}
|
||||
|
||||
EditorGUILayout.LabelField("Total Frames ({fps}fps)", $"{Mathf.Max(1, Mathf.FloorToInt(actualLength * fps))}");
|
||||
EditorGUILayout.LabelField("Total Frames ({fps}fps)", $"{Mathf.Max(1, Mathf.FloorToInt(effectiveLength * fps))}");
|
||||
EditorGUI.indentLevel--;
|
||||
|
||||
EditorGUILayout.Space();
|
||||
@@ -74,7 +84,7 @@ public class AnimationSectionSpeedEditor : EditorWindow
|
||||
fps = EditorGUILayout.IntField("FPS", fps);
|
||||
fps = Mathf.Max(1, fps);
|
||||
|
||||
int totalFrames = Mathf.Max(1, Mathf.FloorToInt(actualLength * fps));
|
||||
int totalFrames = Mathf.Max(1, Mathf.FloorToInt(effectiveLength * fps));
|
||||
|
||||
// ── 프레임 범위 ──
|
||||
EditorGUILayout.LabelField("Target Frame Range", EditorStyles.boldLabel);
|
||||
@@ -108,7 +118,7 @@ public class AnimationSectionSpeedEditor : EditorWindow
|
||||
EditorGUILayout.Space();
|
||||
|
||||
// ── 타임라인 시각화 ──
|
||||
DrawTimeline(startTime, endTime, actualLength);
|
||||
DrawTimeline(startTime, endTime, effectiveLength);
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
@@ -133,7 +143,7 @@ public class AnimationSectionSpeedEditor : EditorWindow
|
||||
// ── 미리보기 ──
|
||||
float newDuration = duration / speedMultiplier;
|
||||
float timeDelta = newDuration - duration;
|
||||
float newLength = actualLength + timeDelta;
|
||||
float newLength = effectiveLength + timeDelta;
|
||||
|
||||
EditorGUILayout.LabelField("Preview", EditorStyles.boldLabel);
|
||||
EditorGUI.indentLevel++;
|
||||
@@ -301,6 +311,7 @@ public class AnimationSectionSpeedEditor : EditorWindow
|
||||
float newDuration = duration / speedMultiplier;
|
||||
float timeDelta = newDuration - duration;
|
||||
float actualLength = GetActualLastKeyframeTime(clip);
|
||||
float effectiveLength = actualLength > 0f ? actualLength : clip.length;
|
||||
|
||||
Undo.RegisterCompleteObjectUndo(clip, $"Section Speed {speedMultiplier}x (frames {startFrame}~{endFrame})");
|
||||
|
||||
@@ -392,7 +403,7 @@ public class AnimationSectionSpeedEditor : EditorWindow
|
||||
}
|
||||
|
||||
// ── 5. m_StopTime 갱신 (SerializedObject로) ──
|
||||
float newStopTime = actualLength + timeDelta;
|
||||
float newStopTime = effectiveLength + timeDelta;
|
||||
var serializedClip = new SerializedObject(clip);
|
||||
SerializedProperty stopTimeProp = serializedClip.FindProperty("m_StopTime");
|
||||
if (stopTimeProp != null)
|
||||
@@ -433,7 +444,7 @@ public class AnimationSectionSpeedEditor : EditorWindow
|
||||
|
||||
Debug.Log($"[SectionSpeedEditor] {clip.name}: frames {startFrame}~{endFrame} → {speedMultiplier}x " +
|
||||
$"({duration:F3}s → {newDuration:F3}s) " +
|
||||
$"| clip: {actualLength:F3}s → {newStopTime:F3}s " +
|
||||
$"| clip: {effectiveLength:F3}s → {newStopTime:F3}s " +
|
||||
$"| curves: {modifiedCurves}, keyframes: {modifiedKeyframes}, skipped: {skippedCurves}" +
|
||||
$" | events: {(eventsModified ? "modified" : "none")}");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace Colosseum.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// AnimatorController의 Skill 상태에 base clip을 일괄 할당하는 에디터 도구입니다.
|
||||
/// </summary>
|
||||
public static class BaseSkillClipAssigner
|
||||
{
|
||||
private const string SkillStateName = "Skill";
|
||||
|
||||
/// <summary>
|
||||
/// 선택한 애니메이션 클립을 모든 AnimatorController의 Skill 상태에 할당합니다.
|
||||
/// </summary>
|
||||
[MenuItem("Tools/Animation/Assign Base Skill Clip to All Controllers")]
|
||||
private static void AssignBaseClipToAllControllers()
|
||||
{
|
||||
AnimationClip selectedClip = Selection.activeObject as AnimationClip;
|
||||
if (selectedClip == null)
|
||||
{
|
||||
EditorUtility.DisplayDialog(
|
||||
"Base Skill Clip 할당",
|
||||
"Animations 폴더에서 할당할 AnimationClip을 선택한 후 다시 실행하세요.",
|
||||
"확인");
|
||||
return;
|
||||
}
|
||||
|
||||
string[] controllerGuids = AssetDatabase.FindAssets("t:AnimatorController", new[] { "Assets/_Game" });
|
||||
int assignedCount = 0;
|
||||
|
||||
for (int i = 0; i < controllerGuids.Length; i++)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(controllerGuids[i]);
|
||||
AnimatorController ac = AssetDatabase.LoadAssetAtPath<AnimatorController>(path);
|
||||
if (ac == null) continue;
|
||||
|
||||
if (TryAssignClipToState(ac, SkillStateName, selectedClip))
|
||||
assignedCount++;
|
||||
}
|
||||
|
||||
if (assignedCount > 0)
|
||||
{
|
||||
AssetDatabase.SaveAssets();
|
||||
EditorUtility.DisplayDialog(
|
||||
"Base Skill Clip 할당 완료",
|
||||
$"{selectedClip.name}을(를) {assignedCount}개 컨트롤러의 Skill 상태에 할당했습니다.",
|
||||
"확인");
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorUtility.DisplayDialog(
|
||||
"Base Skill Clip 할당",
|
||||
"Skill 상태를 가진 컨트롤러를 찾지 못했습니다.",
|
||||
"확인");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AnimatorController의 지정한 상태에 클립을 할당합니다.
|
||||
/// </summary>
|
||||
private static bool TryAssignClipToState(AnimatorController ac, string stateName, AnimationClip clip)
|
||||
{
|
||||
for (int i = 0; i < ac.layers.Length; i++)
|
||||
{
|
||||
if (TryAssignClipInStateMachine(ac.layers[i].stateMachine, stateName, clip))
|
||||
{
|
||||
EditorUtility.SetDirty(ac);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// StateMachine을 재귀적으로 탐색하여 지정한 이름의 상태에 클립을 할당합니다.
|
||||
/// </summary>
|
||||
private static bool TryAssignClipInStateMachine(AnimatorStateMachine sm, string stateName, AnimationClip clip)
|
||||
{
|
||||
for (int i = 0; i < sm.states.Length; i++)
|
||||
{
|
||||
if (sm.states[i].state.name == stateName)
|
||||
{
|
||||
sm.states[i].state.motion = clip;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < sm.stateMachines.Length; i++)
|
||||
{
|
||||
if (TryAssignClipInStateMachine(sm.stateMachines[i].stateMachine, stateName, clip))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1a6c131eb50620a47ab0250c9d46d438
|
||||
@@ -178,7 +178,6 @@ namespace Colosseum.Enemy
|
||||
var skillCtrl = GetComponent<Colosseum.Skills.SkillController>();
|
||||
bool needsYMotion = skillCtrl != null
|
||||
&& skillCtrl.IsPlayingAnimation
|
||||
&& !skillCtrl.IsInEndAnimation
|
||||
&& skillCtrl.UsesRootMotion
|
||||
&& !skillCtrl.IgnoreRootMotionY;
|
||||
|
||||
|
||||
@@ -394,10 +394,18 @@ namespace Colosseum.Player
|
||||
|
||||
private float GetSkillDuration(SkillData skill)
|
||||
{
|
||||
if (skill == null || skill.SkillClip == null)
|
||||
if (skill == null || skill.AnimationClips.Count == 0)
|
||||
return settleDelay;
|
||||
|
||||
return Mathf.Max(settleDelay, skill.SkillClip.length / Mathf.Max(0.1f, skill.AnimationSpeed));
|
||||
float totalLength = 0f;
|
||||
var clips = skill.AnimationClips;
|
||||
for (int i = 0; i < clips.Count; i++)
|
||||
{
|
||||
if (clips[i] != null)
|
||||
totalLength += clips[i].length;
|
||||
}
|
||||
|
||||
return Mathf.Max(settleDelay, totalLength / Mathf.Max(0.1f, skill.AnimationSpeed));
|
||||
}
|
||||
|
||||
private SkillData FindCancellableSkill()
|
||||
|
||||
@@ -35,17 +35,18 @@ namespace Colosseum.Skills
|
||||
public class SkillController : NetworkBehaviour
|
||||
{
|
||||
private const string SKILL_STATE_NAME = "Skill";
|
||||
private const string END_STATE_NAME = "SkillEnd";
|
||||
|
||||
[Header("애니메이션")]
|
||||
[SerializeField] private Animator animator;
|
||||
[Tooltip("기본 Animator Controller (스킬 종료 후 복원용)")]
|
||||
[SerializeField] private RuntimeAnimatorController baseController;
|
||||
[Tooltip("Skill 상태에 연결된 기본 클립 (Override용)")]
|
||||
[Tooltip("Skill 상태에 연결된 기본 클립 (Override용). baseController의 Skill state에서 자동 발견됩니다.")]
|
||||
[SerializeField] private AnimationClip baseSkillClip;
|
||||
|
||||
[Header("네트워크 동기화")]
|
||||
[Tooltip("\"_Player_\" 이름이 포함된 클립이 자동 등록됩니다. 서버→클라이언트 클립 동기화에 사용됩니다.")]
|
||||
[Tooltip("이 이름이 포함된 클립이 자동 등록됩니다. 서버→클라이언트 클립 동기화에 사용됩니다.")]
|
||||
[SerializeField] private string clipAutoRegisterFilter = "_Player_";
|
||||
[Tooltip("자동 등록된 클립 목록 (서버→클라이언트 클립 인덱스 동기화용)")]
|
||||
[SerializeField] private List<AnimationClip> registeredClips = new();
|
||||
|
||||
[Header("설정")]
|
||||
@@ -69,7 +70,7 @@ namespace Colosseum.Skills
|
||||
private readonly List<AbnormalityData> currentCastStartAbnormalities = new();
|
||||
private readonly Dictionary<int, List<AbnormalityData>> currentTriggeredAbnormalities = new();
|
||||
private readonly List<GameObject> currentTriggeredTargetsBuffer = new();
|
||||
private bool waitingForEndAnimation; // EndAnimation 종료 대기 중
|
||||
private int currentClipSequenceIndex; // 현재 재생 중인 클립 순서 (animationClips 내 인덱스)
|
||||
private int currentRepeatCount = 1;
|
||||
private int currentIterationIndex = 0;
|
||||
private GameObject currentTargetOverride;
|
||||
@@ -80,7 +81,6 @@ namespace Colosseum.Skills
|
||||
|
||||
public bool IsExecutingSkill => currentSkill != null;
|
||||
public bool IsPlayingAnimation => currentSkill != null;
|
||||
public bool IsInEndAnimation => waitingForEndAnimation;
|
||||
public bool UsesRootMotion => currentSkill != null && currentSkill.UseRootMotion;
|
||||
public bool IgnoreRootMotionY => currentSkill != null && currentSkill.IgnoreRootMotionY;
|
||||
public SkillData CurrentSkill => currentSkill;
|
||||
@@ -110,15 +110,73 @@ namespace Colosseum.Skills
|
||||
#if UNITY_EDITOR
|
||||
private void OnValidate()
|
||||
{
|
||||
AutoRegisterPlayerClips();
|
||||
AutoDiscoverBaseSkillClip();
|
||||
AutoRegisterClips();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// "_Player_"가 포함된 모든 AnimationClip을 registeredClips에 자동 등록합니다.
|
||||
/// baseController의 Skill 상태에 연결된 클립을 baseSkillClip으로 자동 발견합니다.
|
||||
/// </summary>
|
||||
private void AutoDiscoverBaseSkillClip()
|
||||
{
|
||||
if (baseSkillClip != null) return;
|
||||
if (baseController == null) return;
|
||||
|
||||
var ac = baseController as UnityEditor.Animations.AnimatorController;
|
||||
if (ac == null) return;
|
||||
|
||||
AnimationClip foundClip = FindClipInState(ac, SKILL_STATE_NAME);
|
||||
if (foundClip != null)
|
||||
{
|
||||
baseSkillClip = foundClip;
|
||||
UnityEditor.EditorUtility.SetDirty(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AnimatorController의 지정한 상태에 연결된 AnimationClip을 찾습니다.
|
||||
/// </summary>
|
||||
private static AnimationClip FindClipInState(UnityEditor.Animations.AnimatorController ac, string stateName)
|
||||
{
|
||||
for (int i = 0; i < ac.layers.Length; i++)
|
||||
{
|
||||
AnimationClip clip = FindClipInStateMachine(ac.layers[i].stateMachine, stateName);
|
||||
if (clip != null) return clip;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// StateMachine을 재귀적으로 탐색하여 지정한 이름의 상태에서 클립을 찾습니다.
|
||||
/// </summary>
|
||||
private static AnimationClip FindClipInStateMachine(UnityEditor.Animations.AnimatorStateMachine sm, string stateName)
|
||||
{
|
||||
for (int i = 0; i < sm.states.Length; i++)
|
||||
{
|
||||
if (sm.states[i].state.name == stateName && sm.states[i].state.motion is AnimationClip clip)
|
||||
return clip;
|
||||
}
|
||||
|
||||
for (int i = 0; i < sm.stateMachines.Length; i++)
|
||||
{
|
||||
AnimationClip clip = FindClipInStateMachine(sm.stateMachines[i].stateMachine, stateName);
|
||||
if (clip != null) return clip;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// clipAutoRegisterFilter 이름이 포함된 모든 AnimationClip을 registeredClips에 자동 등록합니다.
|
||||
/// 서버→클라이언트 클립 동기화 인덱스의 일관성을 위해 이름순으로 정렬합니다.
|
||||
/// </summary>
|
||||
private void AutoRegisterPlayerClips()
|
||||
private void AutoRegisterClips()
|
||||
{
|
||||
string trimmedFilter = clipAutoRegisterFilter.Trim('_');
|
||||
if (string.IsNullOrEmpty(trimmedFilter))
|
||||
return;
|
||||
|
||||
string[] guids = AssetDatabase.FindAssets("t:AnimationClip", new[] { "Assets/_Game/Animations" });
|
||||
var clips = new List<AnimationClip>();
|
||||
|
||||
@@ -127,7 +185,7 @@ namespace Colosseum.Skills
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
string clipName = Path.GetFileNameWithoutExtension(path);
|
||||
|
||||
if (clipName.IndexOf("_Player_", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
if (clipName.IndexOf(trimmedFilter, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
AnimationClip clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(path);
|
||||
if (clip != null)
|
||||
@@ -155,7 +213,7 @@ namespace Colosseum.Skills
|
||||
{
|
||||
registeredClips.Clear();
|
||||
registeredClips.AddRange(clips);
|
||||
Debug.Log($"[SkillController] 자동 등록: {clips.Count}개 Player 클립", this);
|
||||
Debug.Log($"[SkillController] 자동 등록: {clips.Count}개 클립 (필터: {clipAutoRegisterFilter})", this);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -167,40 +225,23 @@ namespace Colosseum.Skills
|
||||
|
||||
var stateInfo = animator.GetCurrentAnimatorStateInfo(0);
|
||||
|
||||
// EndAnimation 종료 감지
|
||||
if (waitingForEndAnimation)
|
||||
{
|
||||
// 애니메이션 종료 시 처리
|
||||
if (stateInfo.normalizedTime >= 1f)
|
||||
{
|
||||
if (debugMode) Debug.Log($"[Skill] EndAnimation complete: {currentSkill.SkillName}");
|
||||
RestoreBaseController();
|
||||
ClearCurrentSkillState();
|
||||
}
|
||||
// 같은 반복 차수 내에서 다음 클립이 있으면 재생
|
||||
if (TryPlayNextClipInSequence())
|
||||
return;
|
||||
}
|
||||
|
||||
// 애니메이션 종료 시 처리 (OnSkillEnd 여부와 관계없이 애니메이션 끝까지 재생)
|
||||
if (stateInfo.normalizedTime >= 1f)
|
||||
{
|
||||
// 다음 반복 차수가 있으면 시작
|
||||
if (TryStartNextIteration())
|
||||
return;
|
||||
|
||||
if (currentSkill.EndClip != null)
|
||||
{
|
||||
// EndAnimation 재생 후 종료 대기
|
||||
if (debugMode) Debug.Log($"[Skill] SkillAnimation done, playing EndAnimation: {currentSkill.SkillName}");
|
||||
PlayEndClip(currentSkill.EndClip);
|
||||
waitingForEndAnimation = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// EndAnimation 없으면 바로 종료
|
||||
// 모든 클립과 반복이 끝나면 종료
|
||||
if (debugMode) Debug.Log($"[Skill] Animation complete: {currentSkill.SkillName}");
|
||||
RestoreBaseController();
|
||||
ClearCurrentSkillState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 스킬 시전
|
||||
@@ -263,7 +304,6 @@ namespace Colosseum.Skills
|
||||
|
||||
currentLoadoutEntry = loadoutEntry != null ? loadoutEntry.CreateCopy() : SkillLoadoutEntry.CreateTemporary(skill);
|
||||
currentSkill = skill;
|
||||
waitingForEndAnimation = false;
|
||||
lastCancelReason = SkillCancelReason.None;
|
||||
BuildResolvedEffects(currentLoadoutEntry);
|
||||
currentRepeatCount = currentLoadoutEntry.GetResolvedRepeatCount();
|
||||
@@ -379,7 +419,7 @@ namespace Colosseum.Skills
|
||||
return;
|
||||
|
||||
currentIterationIndex++;
|
||||
waitingForEndAnimation = false;
|
||||
currentClipSequenceIndex = 0;
|
||||
|
||||
if (debugMode && currentRepeatCount > 1)
|
||||
{
|
||||
@@ -388,18 +428,41 @@ namespace Colosseum.Skills
|
||||
|
||||
TriggerCastStartEffects();
|
||||
|
||||
if (currentSkill.SkillClip != null && animator != null)
|
||||
if (currentSkill.AnimationClips.Count > 0 && animator != null)
|
||||
{
|
||||
float resolvedAnimationSpeed = currentLoadoutEntry != null
|
||||
? currentLoadoutEntry.GetResolvedAnimationSpeed()
|
||||
: currentSkill.AnimationSpeed;
|
||||
animator.speed = resolvedAnimationSpeed;
|
||||
PlaySkillClip(currentSkill.SkillClip);
|
||||
PlaySkillClip(currentSkill.AnimationClips[0]);
|
||||
}
|
||||
|
||||
TriggerImmediateSelfEffectsIfNeeded();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 시퀀스 내 다음 클립이 있으면 재생합니다.
|
||||
/// </summary>
|
||||
private bool TryPlayNextClipInSequence()
|
||||
{
|
||||
if (currentSkill == null)
|
||||
return false;
|
||||
|
||||
int nextIndex = currentClipSequenceIndex + 1;
|
||||
if (nextIndex >= currentSkill.AnimationClips.Count)
|
||||
return false;
|
||||
|
||||
currentClipSequenceIndex = nextIndex;
|
||||
PlaySkillClip(currentSkill.AnimationClips[currentClipSequenceIndex]);
|
||||
|
||||
if (debugMode)
|
||||
{
|
||||
Debug.Log($"[Skill] Playing clip {currentClipSequenceIndex + 1}/{currentSkill.AnimationClips.Count}: {currentSkill.AnimationClips[currentClipSequenceIndex].name}");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 반복 시전이 남아 있으면 다음 차수를 시작합니다.
|
||||
/// </summary>
|
||||
@@ -445,31 +508,6 @@ namespace Colosseum.Skills
|
||||
PlaySkillClipClientRpc(registeredClips.IndexOf(clip));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 종료 클립 재생
|
||||
/// </summary>
|
||||
private void PlayEndClip(AnimationClip clip)
|
||||
{
|
||||
if (baseSkillClip == null)
|
||||
{
|
||||
Debug.LogError("[SkillController] Base Skill Clip is not assigned!");
|
||||
return;
|
||||
}
|
||||
|
||||
var overrideController = new AnimatorOverrideController(baseController);
|
||||
overrideController[baseSkillClip] = clip;
|
||||
animator.runtimeAnimatorController = overrideController;
|
||||
|
||||
// 애니메이터 완전 리셋 후 재생
|
||||
animator.Rebind();
|
||||
animator.Update(0f);
|
||||
animator.Play(SKILL_STATE_NAME, 0, 0f);
|
||||
|
||||
// 클라이언트에 클립 동기화
|
||||
if (IsServer && IsSpawned)
|
||||
PlaySkillClipClientRpc(registeredClips.IndexOf(clip));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 기본 컨트롤러로 복원
|
||||
/// </summary>
|
||||
@@ -646,7 +684,7 @@ namespace Colosseum.Skills
|
||||
currentTriggeredAbnormalities.Clear();
|
||||
currentTriggeredTargetsBuffer.Clear();
|
||||
currentTargetOverride = null;
|
||||
waitingForEndAnimation = false;
|
||||
currentClipSequenceIndex = 0;
|
||||
currentRepeatCount = 1;
|
||||
currentIterationIndex = 0;
|
||||
}
|
||||
|
||||
@@ -56,6 +56,120 @@ namespace Colosseum.Skills
|
||||
[CreateAssetMenu(fileName = "NewSkill", menuName = "Colosseum/Skill")]
|
||||
public class SkillData : ScriptableObject
|
||||
{
|
||||
private const string SkillAssetPrefix = "Data_Skill_";
|
||||
private const string ClipAssetPrefix = "Anim_";
|
||||
private const string AnimationsSearchPath = "Assets/_Game/Animations";
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>
|
||||
/// 레거시 마이그레이션 및 애니메이션 클립 자동 매칭.
|
||||
/// 에디터에서 애셋 로드/수정 시 자동 실행됩니다.
|
||||
/// </summary>
|
||||
private void OnValidate()
|
||||
{
|
||||
MigrateLegacyClips();
|
||||
RefreshAnimationClips();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 기존 skillClip/endClip 데이터를 animationClips 리스트로 이관합니다.
|
||||
/// </summary>
|
||||
private void MigrateLegacyClips()
|
||||
{
|
||||
if (legacySkillClip == null && legacyEndClip == null)
|
||||
return;
|
||||
|
||||
if (animationClips.Count > 0)
|
||||
return;
|
||||
|
||||
if (legacySkillClip != null)
|
||||
{
|
||||
animationClips.Add(legacySkillClip);
|
||||
legacySkillClip = null;
|
||||
}
|
||||
|
||||
if (legacyEndClip != null)
|
||||
{
|
||||
animationClips.Add(legacyEndClip);
|
||||
legacyEndClip = null;
|
||||
}
|
||||
|
||||
UnityEditor.EditorUtility.SetDirty(this);
|
||||
Debug.Log($"[SkillData] 레거시 클립을 animationClips로 이관 완료: {SkillName ?? name}", this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 애셋 이름 기반으로 매칭되는 애니메이션 클립을 자동 수집합니다.
|
||||
/// SkillData 이름이 'Data_Skill_'으로 시작하면 'Anim_{key}_{순서}' 클립을 찾아 animationClips에 채웁니다.
|
||||
/// </summary>
|
||||
public void RefreshAnimationClips()
|
||||
{
|
||||
if (!name.StartsWith(SkillAssetPrefix))
|
||||
return;
|
||||
|
||||
string key = name.Substring(SkillAssetPrefix.Length);
|
||||
if (string.IsNullOrEmpty(key))
|
||||
return;
|
||||
|
||||
string[] guids = UnityEditor.AssetDatabase.FindAssets("t:AnimationClip", new[] { AnimationsSearchPath });
|
||||
var matchedClips = new List<(AnimationClip clip, int order)>();
|
||||
|
||||
for (int i = 0; i < guids.Length; i++)
|
||||
{
|
||||
string path = UnityEditor.AssetDatabase.GUIDToAssetPath(guids[i]);
|
||||
AnimationClip clip = UnityEditor.AssetDatabase.LoadAssetAtPath<AnimationClip>(path);
|
||||
if (clip == null) continue;
|
||||
|
||||
string clipName = clip.name;
|
||||
if (!clipName.StartsWith(ClipAssetPrefix))
|
||||
continue;
|
||||
|
||||
string remaining = clipName.Substring(ClipAssetPrefix.Length);
|
||||
|
||||
if (remaining == key)
|
||||
{
|
||||
// 정확 매칭 (순서 번호 없음) → 최우선
|
||||
matchedClips.Add((clip, -1));
|
||||
}
|
||||
else if (remaining.StartsWith(key + "_"))
|
||||
{
|
||||
string suffix = remaining.Substring(key.Length + 1);
|
||||
if (int.TryParse(suffix, out int order))
|
||||
{
|
||||
matchedClips.Add((clip, order));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedClips.Count == 0)
|
||||
return;
|
||||
|
||||
// 정렬: 순서 번호 없음(-1) → 순서 번호 오름차순
|
||||
matchedClips.Sort((a, b) => a.order.CompareTo(b.order));
|
||||
|
||||
// 변경이 있는 경우만 갱신 (무한 루프 방지)
|
||||
bool changed = matchedClips.Count != animationClips.Count;
|
||||
if (!changed)
|
||||
{
|
||||
for (int i = 0; i < matchedClips.Count; i++)
|
||||
{
|
||||
if (matchedClips[i].clip != animationClips[i])
|
||||
{
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed) return;
|
||||
|
||||
animationClips.Clear();
|
||||
for (int i = 0; i < matchedClips.Count; i++)
|
||||
animationClips.Add(matchedClips[i].clip);
|
||||
|
||||
UnityEditor.EditorUtility.SetDirty(this);
|
||||
}
|
||||
#endif
|
||||
[Header("기본 정보")]
|
||||
[SerializeField] private string skillName;
|
||||
[TextArea(2, 4)]
|
||||
@@ -73,13 +187,15 @@ namespace Colosseum.Skills
|
||||
[SerializeField] private SkillBaseType baseTypes = SkillBaseType.None;
|
||||
|
||||
[Header("애니메이션")]
|
||||
[Tooltip("기본 Animator Controller의 'Skill' 상태에 덮어씌워질 클립")]
|
||||
[SerializeField] private AnimationClip skillClip;
|
||||
[Tooltip("종료 애니메이션 (선택)")]
|
||||
[SerializeField] private AnimationClip endClip;
|
||||
[Tooltip("순차 재생할 클립 목록. 애셋 이름이 Data_Skill_ 접두사면 Anim_{이름}_{순서} 클립을 자동 수집합니다.")]
|
||||
[SerializeField] private List<AnimationClip> animationClips = new();
|
||||
[Tooltip("애니메이션 재생 속도 (1 = 기본, 2 = 2배속)")]
|
||||
[Min(0.1f)] [SerializeField] private float animationSpeed = 1f;
|
||||
|
||||
// 레거시 마이그레이션 (기존 skillClip/endClip 데이터 보존)
|
||||
[SerializeField, HideInInspector] private AnimationClip legacySkillClip;
|
||||
[SerializeField, HideInInspector] private AnimationClip legacyEndClip;
|
||||
|
||||
[Header("루트 모션")]
|
||||
[Tooltip("애니메이션의 이동/회전 데이터를 캐릭터에 적용")]
|
||||
[SerializeField] private bool useRootMotion = false;
|
||||
@@ -123,8 +239,15 @@ namespace Colosseum.Skills
|
||||
public SkillRoleType SkillRole => skillRole;
|
||||
public SkillActivationType ActivationType => activationType;
|
||||
public SkillBaseType BaseTypes => baseTypes;
|
||||
public AnimationClip SkillClip => skillClip;
|
||||
public AnimationClip EndClip => endClip;
|
||||
/// <summary>
|
||||
/// 순차 재생할 클립 목록입니다.
|
||||
/// </summary>
|
||||
public IReadOnlyList<AnimationClip> AnimationClips => animationClips;
|
||||
|
||||
/// <summary>
|
||||
/// 첫 번째 클립 (레거시 호환성). 기존 코드에서 SkillClip을 참조하는 곳에서 사용됩니다.
|
||||
/// </summary>
|
||||
public AnimationClip SkillClip => animationClips.Count > 0 ? animationClips[0] : null;
|
||||
public float AnimationSpeed => animationSpeed;
|
||||
public float Cooldown => cooldown;
|
||||
public float ManaCost => manaCost;
|
||||
|
||||
Reference in new issue
Block a user