Skip to content

Replacing Valheim Animations

AzumattDev edited this page Dec 2, 2023 · 4 revisions

How to replace Player animations

First of all you need to know that its IMPOSSIBLE to ADD AnimationClip's to Animator in Runtime, which means that your other way of changing animations is to make a copy of Player_animator and put it instead of in-game one. But the problem is that if you do so - you gonna lose all Movement BlendTree values, so your player will always try to run and do some weird stuff.

That leads us to best solution - AnimatorOverrideController

AnimatorOverrideController allows you to OVERRIDE ALL ANIMATIONS in Animator. The only thing you need to rememeber is that OverrideController should contain same amount of AnimationClip's as original RuntimeAnimationController in order to work.

Let's begin!

First of all we need in few fields:

1)

static Dictionary<string, AnimationClip> ExternalAnimations = new Dictionary<string, AnimationClip>();

^ Dictionary where we will add: Our Added Animation Name : Our Loaded AnimationClip

2)

static Dictionary<string, string> ReplacementAnimationList = new Dictionary<string, string>();

^ Dictionary where we will add our Original State Name : Name Of Our Loaded AnimationClip in ExternalAnimations dictionary

3)

private static RuntimeAnimatorController MyNewController;

^ AnimationController that we will create in Runtime in order to change animations

4)

private static RuntimeAnimatorController OriginalPlayerController;

^ Original player controller we will keep just in case we want to reset animations

5)

private static bool FirstInit;

^ Bool to see if we already Initialized our MyNewController in Player.Start method

Code time!

  1. Get Humanoid-type AnimationClip's you need in Unity For out test I will take 3 animations: BerserkerAttack, NinjaJump, NinjaRun Animations

  2. Add all of them into one AssetBundle you will use AssetBundle

  3. Build bundle AssetBundle2 AssetBundle3 AssetBundle4

  4. Add bundle to our Visual Studio project and set it as Embedded Resources. Add Resources

  5. Load our AssetBundle using method:

     public static AssetBundle GetAssetBundle(string filename)
        {
            var execAssembly = Assembly.GetExecutingAssembly();
            var resourceName = execAssembly.GetManifestResourceNames()
                .Single(str => str.EndsWith(filename));
            using (var stream = execAssembly.GetManifestResourceStream(resourceName))
            {
                return AssetBundle.LoadFromStream(stream);
            }
        }

How our code looks now: Code

  1. Now we can start loading our custom AnimationClip's from assetbundle and add them to our dictionary ExternalAnimations It should look like: ExternalAnimations.Add("ANY UNIQUE NAME", asset.Load("clipname")); In my case when I load 3 animations my code looks like that: LoadAnimations

  2. Now we can prepare our ReplacementAnimationList It should look like: ReplacementAnimationList.Add("ORIGINAL PLAYER ANIMATOR STATE NAME" , "OUR EXTERNAL ANIMATION FROM ExternalAnimations dictionary)

In my case code looks like this: enter image description here What we do here is we saying to our future method that we want to replace: 1) player "jump" state animation on our external "JumpAnim" animationclip 2) player "Standart Run" state animation on our external "RunAnim" animationclip 3) player "Sword-Attack-R4" (sword secondary attack state) animation on our external "BerserkerAttackAnim" animationclip

Alright, we done with all preparations, time to Init our OverrideController and apply its override to our Animator

To do that we need to use Harmony and patch Player.Start method. If our field FirstInit == false, then Animator will be "hooked" from Player __instance and FirstInit will be true.

First of all we need in Make Animation Override Controller that will allow us to create our AOC:

     public static RuntimeAnimatorController MakeAOC(RuntimeAnimatorController ORIGINAL, Dictionary<string, string> replacements)
        {
            AnimatorOverrideController aoc = new AnimatorOverrideController(ORIGINAL);
            var anims = new List<KeyValuePair<AnimationClip, AnimationClip>>();
            foreach (var animation in aoc.animationClips)
            {
                string name = animation.name;
                if (replacements.ContainsKey(name))
                {
                    AnimationClip newClip = MonoBehaviour.Instantiate<AnimationClip>(ExternalAnimations[replacements[name]]);
                    anims.Add(new KeyValuePair<AnimationClip, AnimationClip>(animation, newClip));
                }
                else
                {
                    anims.Add(new KeyValuePair<AnimationClip, AnimationClip>(animation, animation));
                }
            }
            aoc.ApplyOverrides(anims);
            return aoc;
        }

This method creates AOC based on our passed ORIGINAL player RuntimeAnimatorController. Then its going to fill our AOC will AnimationClips from ORIGINAL RaC. Here we check if our ReplacementAnimationList contains this AnimationState => we replace its AnimationClip with our ExternalAnimation one

Player.Start patch

Player.Start patch will look like this:

       [HarmonyPatch(typeof(Player), nameof(Player.Start))]
        static class Player_Init_Load_Animator
        {
            static void Postfix(Player __instance)
            {
                if (!FirstInit)
                {
                    FirstInit = true;
                    OriginalPlayerController = MakeAOC(__instance.m_animator.runtimeAnimatorController, new Dictionary<string,string>());
                    MyNewController = MakeAOC(__instance.m_animator.runtimeAnimatorController, ReplacementAnimationList);
                }

                if (Player.m_localPlayer)
                {
                    __instance.m_animator.runtimeAnimatorController = MyNewController;
                }
            }
        } 
  1. When we load in game menu class Player initializing (because we "creating" player in Game Menu screen), that allows us to "hook" Player original Animator.runtimeAnimatorController => pass it to MakeAOC method and get our AnimatorOverrideController, which we pass into our MyNewController = MakeAOC( original player RaC, our Replacement dictionary) Also we set OriginalPlayerController = __instance.m_animator.runtimeAnimatorController in case that we will want to give player its original animations in any moment of time.

If we will start game and Player.m_localPlayer will be initialized, our HarmonyPatch will apply our Animation Override to ANY player in game with: __instance.m_animator.runtimeAnimatorController = MyNewController; if you want to reset animations you should do: __instance.m_animator.runtimeAnimatorController = OriginalPlayerController ;

RESULT

GIF WITH RESULT As you can see RUN, JUMP, Sword secondary (middle mouse attack) were replaced with no problems.

Full code

using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using BepInEx;
using HarmonyLib;
using UnityEngine;

namespace TestMod
{
    [BepInPlugin("AddAnimationsTutorial", "AddAnimationsTutorialnes", "1.1.1")]
    public class Test : BaseUnityPlugin
    {
        private static Dictionary<string, string> ReplacementAnimationList = new Dictionary<string, string>();

        public static Dictionary<string, AnimationClip> ExternalAnimations = new Dictionary<string, AnimationClip>();

        private static RuntimeAnimatorController OriginalPlayerController;

        private static RuntimeAnimatorController MyNewController;

        private static bool FirstInit;

        private static readonly Harmony harm = new Harmony("test");

        void Awake()
        {
            AssetBundle asset = GetAssetBundle("testanimations");

            ExternalAnimations.Add("JumpAnim",asset.LoadAsset<AnimationClip>("NinjaJump"));
            ExternalAnimations.Add("RunAnim", asset.LoadAsset<AnimationClip>("NinjaRun"));
            ExternalAnimations.Add("BerserkerAttackAnim", asset.LoadAsset<AnimationClip>("BerserkerAttack"));

            ReplacementAnimationList.Add("jump", "JumpAnim");
            ReplacementAnimationList.Add("Standard Run", "RunAnim");
            ReplacementAnimationList.Add("Sword-Attack-R4", "BerserkerAttackAnim");

            harm.PatchAll();
        }

        [HarmonyPatch(typeof(Player), nameof(Player.Start))]
        static class Player_Init_Load_Animator
        {
            static void Postfix(Player __instance)
            {
                if (!FirstInit)
                {
                    FirstInit = true;
                    OriginalPlayerController = MakeAOC(__instance.m_animator.runtimeAnimatorController, new Dictionary<string,string>());
                    MyNewController = MakeAOC(__instance.m_animator.runtimeAnimatorController, ReplacementAnimationList);
                }

                if (Player.m_localPlayer)
                {
                    __instance.m_animator.runtimeAnimatorController = MyNewController;
                }
            }
        }

        public static RuntimeAnimatorController MakeAOC(RuntimeAnimatorController ORIGINAL, Dictionary<string, string> replacements)
        {
            AnimatorOverrideController aoc = new AnimatorOverrideController(ORIGINAL);
            var anims = new List<KeyValuePair<AnimationClip, AnimationClip>>();
            foreach (var animation in aoc.animationClips)
            {
                string name = animation.name;
                if (replacements.ContainsKey(name))
                {
                    AnimationClip newClip = MonoBehaviour.Instantiate<AnimationClip>(ExternalAnimations[replacements[name]]);
                    anims.Add(new KeyValuePair<AnimationClip, AnimationClip>(animation, newClip));
                }
                else
                {
                    anims.Add(new KeyValuePair<AnimationClip, AnimationClip>(animation, animation));
                }
            }
            aoc.ApplyOverrides(anims);
            return aoc;
        }

        public static AssetBundle GetAssetBundle(string filename)
        {
            var execAssembly = Assembly.GetExecutingAssembly();

            var resourceName = execAssembly.GetManifestResourceNames()
                .Single(str => str.EndsWith(filename));

            using (var stream = execAssembly.GetManifestResourceStream(resourceName))
            {
                return AssetBundle.LoadFromStream(stream);
            }
        }

    }
}
Clone this wiki locally