Unity Modify AnimationClip At Runtime

jkjkbk·2023년 5월 4일
0

Unity

목록 보기
7/16

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
    {
        // (TextAsset)Resources.Load로 읽기 위해서 확장자는 .txt, .html, .htm, .xml, .bytes, .json, .csv, .yaml, .fnt
        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;
        }
    }
}
// AnimationClip을 파일로 저장하는 코드
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()
    {
        // 선택된 Asset 중 AnimationClip 필터링
        Object[] clips = Selection.GetFiltered(typeof(AnimationClip), SelectionMode.Unfiltered);
        Debug.Log("selected AnimationClips: " + clips.Length);
        foreach (AnimationClip clip in clips)
        {
            Debug.Log($"Selected : {clip.name}");
        }

        // 선택된 AnimationClip이 없는 경우
        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);

        // Clip의 AnimationCurve를 구조체로 변환하여 저장
        foreach (var binding in AnimationUtility.GetCurveBindings(clip))
        {
            // AnimationClip.SetCurve에 바로 적용시키기 위해 binding.propertyName의 일부분을 미리 변경하여 저장
            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();
    }
}
// 저장된 AnimatioClip 읽기 또는 변경
using System.Collections.Generic;
using UnityEngine;
using AnimationCurveUserType;
using Newtonsoft.Json;
using System;

public static class AnimationTool
{
    public static ClipInfo Load(string animationClipName)
    {
        // Debug.Log(Resources.Load($"{PathString.AnimationCurveSaveDirName}/{animationClipName}"));
        TextAsset mytxtData = (TextAsset)Resources.Load($"{PathString.AnimationCurveSaveDirName}/{animationClipName}");
        string txt = mytxtData.text;
        return JsonConvert.DeserializeObject<ClipInfo>(txt);
    }
	
    // AnimationCurve에서 특정 시간의 Keyframe 변경
    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))
            {
                // Debug.Log($"#{i} time : {keyframe.time}. value : {targetCurve.keys[i].value} -> {keyframe.value}");
                targetCurve.MoveKey(i, keyframe);
                return true;
            }
        }

        return false;
    }
    
    public static void ChangeAnimationClip(Animator targetAnimator, string targetClipName, AnimationClip toReplaceClip)
    {
        // AnimatorOverrideController 객체 생성
        AnimatorOverrideController controller = new AnimatorOverrideController(targetAnimator.runtimeAnimatorController);

        // clips를 받아올 공간 할당
        List<KeyValuePair<AnimationClip, AnimationClip>> clips = new();
        // clips 가져오기
        controller.GetOverrides(clips);

        for (int i = 0; i < clips.Count; i++)
        {
            // Debug.Log("value " + (clips[i].Value == null));
            Debug.Log($"{clips[i].Key.name}");

            // 원하는 AnimationClip으로 교체
            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;
            }
        }

        // clips를 AnimatorOverrideController 객체에 적용
        controller.ApplyOverrides(clips);

        // AnimatorOverrideController 객체를 대입
        targetAnimator.runtimeAnimatorController = controller;
    }
    
    // CurveInfo.PassKey 변환
    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";
    // 수정할 AnimationCurve의 정보
    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))
        {
            // 수정할 AnimationClip의 ClipInfo 구조체 가져오기 및 AnimationClip 복제
            attackClipInfo = AnimationTool.Load(targetClipName);
            attackClip = clip;
            break;
        }
    }
        
    // 수정할 AnimationClip 복제
    AnimationClip newClip = Instantiate(attackClip);

    foreach (var curveInfo in attackClipInfo.CurveInfos)
    {
    	// 수정할 AnimationCurve인지
        if (string.Equals(curveInfo.PathKey, localPositoinZPath))
        {
            AnimationCurve newCurve = curveInfo.ToCurve();
			
            //////////
            // AnimationCurve.keys를 이용
            // newCurve의 Keyframes 원하는대로 수정
            //////////
 
            var (relativePartPath, type, propertyName) = AnimationTool.DeCurveKey(curveInfo.PathKey);
            // AnimationClip에 수정된 AnimationCurve 적용
            newClip.SetCurve(relativePartPath, type, propertyName, newCurve);
        }
    }

    // 수정된 AnimationClip으로 교체
    AnimationTool.ChangeAnimationClip(animator, targetClipName, newClip);
}

0개의 댓글