1. 문제점
유니티에서 Runtime에서 AnimationClip은 AnimationCurve를 교체할 수 있지만(AnimationClip.SetCurve()),
AnimationCurve를 읽을 수 있는 API가 없음
결과적으로 특별한 방법이 없으면 특정 AnimationCurve를 "읽어서" 일부분을 수정하고 적용시킬 수 없음
2. 해결 방안
https://stackoverflow.com/questions/57846333/how-can-i-store-or-read-a-animation-clip-data-in-runtime
- 위 링크를 참고하면, 사전에 UnityEditor 네임스페이스의 기능을 이용하여
UnityEditor상에서 AnimationClip을 Serializable한 구조체로 변환하여 파일로 저장하고
게임을 실행하고 그 파일을 읽어들여서 AnimationCurve를 생성하고 필요한 부분을 변경하고
이것을, 복제한(Instantiate()), AnimationClip에 적용시켜 이 AnimationClip을 Animator에 적용시키면 됨
3. 코드
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Object = UnityEngine.Object;
using System.Linq;
namespace AnimationCurveUserType
{
public static class PathString
{
public static readonly string AnimationCurveDataExtensor = ".txt";
public static readonly string AnimationCurveSaveDirName = "AnimationCurveData";
public static readonly string AnimationCurveSaveDirPath = $"{Application.dataPath}/Resources/{AnimationCurveSaveDirName}";
}
[Serializable]
public sealed class ClipInfo
{
public int ClipInstanceID;
public List<CurveInfo> CurveInfos = new List<CurveInfo>();
public ClipInfo() { }
public ClipInfo(Object clip, List<CurveInfo> curveInfos)
{
ClipInstanceID = clip.GetInstanceID();
CurveInfos = curveInfos;
}
public override string ToString()
{
return $"ClipInstanceID : {ClipInstanceID}, Curves Count : {CurveInfos.Count}";
}
}
[Serializable]
public sealed class CurveInfo
{
public string PathKey;
public List<KeyFrameInfo> Keys = new List<KeyFrameInfo>();
public WrapMode PreWrapMode;
public WrapMode PostWrapMode;
public CurveInfo() { }
public CurveInfo(string pathKey, AnimationCurve curve)
{
PathKey = pathKey;
foreach (var keyframe in curve.keys)
{
Keys.Add(new KeyFrameInfo(keyframe));
}
PreWrapMode = curve.preWrapMode;
PostWrapMode = curve.postWrapMode;
}
public override string ToString()
{
return string.Join(" | ", Keys.Select(x => x.ToString()).ToArray());
}
public AnimationCurve ToCurve()
{
Keyframe[] toAddKeys = new Keyframe[Keys.Count];
for (int i = 0; i < Keys.Count; i++)
{
toAddKeys[i] = Keys[i].ToKeyFrame();
}
AnimationCurve curve = new AnimationCurve(toAddKeys);
curve.preWrapMode = PreWrapMode;
curve.postWrapMode = PostWrapMode;
return curve;
}
}
[Serializable]
public sealed class KeyFrameInfo
{
public float Value;
public float InTangent;
public float InWeight;
public float OutTangent;
public float OutWeight;
public float Time;
public WeightedMode WeightedMode;
public KeyFrameInfo() { }
public KeyFrameInfo(Keyframe keyframe)
{
Value = keyframe.value;
InTangent = keyframe.inTangent;
InWeight = keyframe.inWeight;
OutTangent = keyframe.outTangent;
OutWeight = keyframe.outWeight;
Time = keyframe.time;
WeightedMode = keyframe.weightedMode;
}
public override string ToString()
{
return $"Time : {Time}, Value : {Value}";
}
public Keyframe ToKeyFrame()
{
Keyframe keyframe = new Keyframe();
keyframe.value = Value;
keyframe.inTangent = InTangent;
keyframe.inWeight = InWeight;
keyframe.outTangent = OutTangent;
keyframe.outWeight = OutWeight;
keyframe.time = Time;
keyframe.weightedMode = WeightedMode;
return keyframe;
}
}
}
using UnityEditor;
using UnityEngine;
using System.IO;
using AnimationCurveUserType;
using System.Collections.Generic;
using Newtonsoft.Json;
using System;
using Object = UnityEngine.Object;
public class SaveAnimationCurve : MonoBehaviour
{
[MenuItem("Extensions/Save AnimationClip to file")]
static void OnClickedSave()
{
Object[] clips = Selection.GetFiltered(typeof(AnimationClip), SelectionMode.Unfiltered);
Debug.Log("selected AnimationClips: " + clips.Length);
foreach (AnimationClip clip in clips)
{
Debug.Log($"Selected : {clip.name}");
}
if (clips == null)
{
EditorUtility.DisplayDialog(
"Select AnimationClip",
"You Must Select a AnimationClip first!",
"Ok");
return;
}
foreach (AnimationClip clip in clips)
{
Save(clip, PathString.AnimationCurveSaveDirPath);
}
}
public static string CurveKey(string pathToObject, Type type, string propertyName)
{
return $"{pathToObject}:{type.Name}:{propertyName}";
}
public static void Save(AnimationClip clip, string dirPath)
{
var curveInfos = new List<CurveInfo>();
ClipInfo clipCurve = new ClipInfo(clip, curveInfos);
foreach (var binding in AnimationUtility.GetCurveBindings(clip))
{
var key = CurveKey(binding.path, binding.type, binding.propertyName.Replace("m_LocalPosition", "localPosition"));
var curve = AnimationUtility.GetEditorCurve(clip, binding);
curveInfos.Add(new CurveInfo(key, curve));
}
try
{
if (!Directory.Exists(Application.streamingAssetsPath))
{
Directory.CreateDirectory(Application.streamingAssetsPath);
}
}
catch (IOException ex)
{
Debug.LogError(ex.Message);
}
string filePath = $"{dirPath}/{clip.name}{PathString.AnimationCurveDataExtensor}";
var json = JsonConvert.SerializeObject(clipCurve);
if (!File.Exists(filePath))
{
File.WriteAllText(filePath, json);
}
else
{
Debug.Log($"Don't Save. File Exists : {filePath} ");
}
AssetDatabase.Refresh();
}
}
using System.Collections.Generic;
using UnityEngine;
using AnimationCurveUserType;
using Newtonsoft.Json;
using System;
public static class AnimationTool
{
public static ClipInfo Load(string animationClipName)
{
TextAsset mytxtData = (TextAsset)Resources.Load($"{PathString.AnimationCurveSaveDirName}/{animationClipName}");
string txt = mytxtData.text;
return JsonConvert.DeserializeObject<ClipInfo>(txt);
}
public static bool ChangeKeyFrame(AnimationCurve targetCurve, Keyframe keyframe)
{
for (int i = 0; i < targetCurve.keys.Length; i++)
{
if (Mathf.Approximately(targetCurve.keys[i].time, keyframe.time))
{
targetCurve.MoveKey(i, keyframe);
return true;
}
}
return false;
}
public static void ChangeAnimationClip(Animator targetAnimator, string targetClipName, AnimationClip toReplaceClip)
{
AnimatorOverrideController controller = new AnimatorOverrideController(targetAnimator.runtimeAnimatorController);
List<KeyValuePair<AnimationClip, AnimationClip>> clips = new();
controller.GetOverrides(clips);
for (int i = 0; i < clips.Count; i++)
{
Debug.Log($"{clips[i].Key.name}");
if (string.Equals(clips[i].Key.name, targetClipName))
{
Debug.Log($"{clips[i].Key.name} -> {toReplaceClip.name}");
clips[i] = new KeyValuePair<AnimationClip, AnimationClip>(clips[i].Key, toReplaceClip);
break;
}
else if (string.Equals(clips[i].Key.name, $"{targetClipName}(Clone)"))
{
Debug.Log($"{clips[i].Key.name} -> {targetClipName}(Clone)");
clips[i] = new KeyValuePair<AnimationClip, AnimationClip>(clips[i].Key, toReplaceClip);
break;
}
}
controller.ApplyOverrides(clips);
targetAnimator.runtimeAnimatorController = controller;
}
public static (string, Type, string) DeCurveKey(string curveKey)
{
string[] parts = curveKey.Split(":");
if (parts.Length != 3)
throw new NotImplementedException();
string path = parts[0];
Type type;
if (parts[1] == "Transform")
{
type = typeof(Transform);
}
else
{
throw new NotImplementedException();
}
string propertyName = parts[2];
return (path, type, propertyName);
}
}
4. 예제
void ModifyClip()
{
string targetClipName = "Spider_Attack_Copy";
string localPositoinZPath = "jtBn_spiderRoot:Transform:localPosition.z";
ClipInfo attackClipInfo;
AnimationClip[] clips = animator.runtimeAnimatorController.animationClips;
foreach (var clip in clips)
{
if (string.Equals(clip.name, targetClipName))
{
attackClipInfo = AnimationTool.Load(targetClipName);
attackClip = clip;
break;
}
}
AnimationClip newClip = Instantiate(attackClip);
foreach (var curveInfo in attackClipInfo.CurveInfos)
{
if (string.Equals(curveInfo.PathKey, localPositoinZPath))
{
AnimationCurve newCurve = curveInfo.ToCurve();
var (relativePartPath, type, propertyName) = AnimationTool.DeCurveKey(curveInfo.PathKey);
newClip.SetCurve(relativePartPath, type, propertyName, newCurve);
}
}
AnimationTool.ChangeAnimationClip(animator, targetClipName, newClip);
}