Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 37

Unity Animation Triggers for Interactions

Recommended resources

Hello! Let's dive into the next step of building our game's interaction framework.

In our last lesson, you did an excellent job implementing a contextual filtering system. Your radial menu is now intelligent; it only displays interactions that are valid in the current game state. This was a crucial move towards a polished and believable simulation.

Currently, clicking an interaction in the menu only logs a message and changes an object's internal state (like the chair becoming "occupied"). The character itself doesn't react. Today, we will bring your character to life by making them perform the actions they are told to do.

Our learning outcome for this lesson is to trigger the appropriate character animation in Unity based on the selected interaction. When the player selects "Sit," the character will find the chair and play a sitting animation. This visual feedback is fundamental to making the game feel responsive and immersive.


1. The Animator's Role: A Code-First Approach

Unity's animation system is managed by the Animator Component, which uses an Animator Controller asset. An Animator Controller is essentially a state machine that determines which animation clip should be playing at any given time.

You can visually connect these animation states with transitions in the Animator window, but for a complex project like ours, this can quickly become a tangled mess often called a "spaghetti state machine."

Given your background, we will adopt a more robust, code-centric approach. We will add animation clips as states but trigger them directly from our C# scripts. This keeps the logic in one place, making it easier to debug, extend, and eventually connect to our modding framework.

The video below explains the core philosophy of this technique. We will be implementing a simplified version of this concept.

Don't use the Unity Animator, Use Code Instead! - Tutorial

To understand why we're avoiding visual transitions, watch the introduction of this video, "Don't use the Unity Animator, Use Code Instead!" by Small Hedge Games. It makes a strong case for a code-driven animation system.

Watch the section from 01:49 to 02:35. The key takeaway is how all the necessary animation states are added to the Animator window without creating any transitions between them. This is the setup we will use.


2. Setting Up the Animator and Animations

Let's prepare the assets. We need an Animator Controller for our character and at least two animations: one for idling and one for sitting.

  1. Create Animator Controller: In your Project window, right-click, go to Create > Animator Controller, and name it CharacterAnimator. Attach this controller to the Animator component on your character prefab.
  2. Download Animations: Go to Mixamo and find a simple Idle animation and a Sitting animation (one that shows the character sitting down onto a surface). Download them as .fbx for Unity.
  3. Configure Animations:
    • Place the downloaded .fbx files into your project.
    • Select each file and go to the Inspector.
    • In the Rig tab, set Animation Type to Humanoid and for Avatar Definition, select Copy From Other Avatar. Assign your character's main Avatar to the Source field.
    • In the Animation tab, give the animation clip a clear name (e.g., Idle, SitDown). For the Idle animation, ensure Loop Time is checked. For the SitDown animation, ensure it is not looping.
  4. Add States to Controller:
    • Double-click your CharacterAnimator to open the Animator window.
    • Drag your Idle and SitDown animation clips from the Project window into the Animator grid. This will create a state for each one.
    • Right-click the Idle state and select Set as Layer Default State. It will turn orange.

Your Animator window should now look something like this, with states but no connecting arrows.

Unity Animator Setup for Character Animations
This image shows animation clips like 'Idle' and 'Run' being added to the Animator window to create states. We will do the same for 'Idle' and 'SitDown', but we will not create any transition lines between them.

3. Data-Driven Animation Names

To know which animation to play for an interaction, we'll add the animation's name directly to our interaction data.

First, open your interactions.json file. Find the interaction_sit entry and add a new field, animationName:

{
  "id": "interaction_sit",
  "displayName": "Sit",
  "animationName": "SitDown",
  "effects": []
}

Important: The value "SitDown" must exactly match the name of the animation state you created in the Animator window.

Next, update InteractionData.cs to load this new field:

// In InteractionData.cs
[System.Serializable]
public class InteractionData
{
    public string id;
    public string displayName;
    public string animationName; // Add this line
    // ... any other fields you have
}

Your InteractionDataManager will now automatically parse this field when loading the game.


4. Creating the Character Animator Manager

Now, let's create a dedicated script to handle playing animations. This centralizes our animation logic.

Create a new C# script named CharacterAnimatorManager.cs and add it to your player character prefab.

using UnityEngine;
using System.Collections.Generic;

[RequireComponent(typeof(Animator))]
public class CharacterAnimatorManager : MonoBehaviour
{
    private Animator animator;
    private Dictionary<string, int> animationHashes = new Dictionary<string, int>();

    void Awake()
    {
        animator = GetComponent<Animator>();
    }

    /// <summary>
    /// Plays an animation by smoothly cross-fading to it.
    /// </summary>
    /// <param name="animationName">The exact name of the animation state.</param>
    public void PlayAnimation(string animationName)
    {
        if (string.IsNullOrEmpty(animationName))
        {
            // Default to Idle if no animation is specified
            animationName = "Idle";
        }
        
        int stateHash = GetAnimationHash(animationName);
        
        // CrossFade provides a smooth transition between states.
        // 0.2f is the transition duration in seconds.
        animator.CrossFade(stateHash, 0.2f);
    }
    
    /// <summary>
    /// Converts a string name to a hash for faster Animator lookups and caches it.
    /// </summary>
    private int GetAnimationHash(string animationName)
    {
        if (!animationHashes.ContainsKey(animationName))
        {
            animationHashes[animationName] = Animator.StringToHash(animationName);
        }
        return animationHashes[animationName];
    }
}

Key Concepts in this Script:

  • Animator.CrossFade(): This is the core function we use. Instead of instantly snapping to a new animation (like Animator.Play()), it smoothly blends from the current animation to the new one over a short duration. This looks far more natural.
  • Animator.StringToHash(): Comparing strings every frame can be slow. Unity's animation system works much faster with integer IDs (hashes) that represent the string names. We convert the name to a hash once and cache it in a dictionary for performance. This is a professional practice you should always use.

You can read more about these methods in the Unity documentation.

4.1 Introduction to general animation scripting - Unity Learn

The official Unity Learn guide, "Introduction to general animation scripting," provides a concise text-based reference for the methods we are using.

Briefly review the sections on 'Setting Animator Parameter values' to see the StringToHash concept, and 'Using CrossFade and CrossFadeInFixedTime' for an explanation of the function we are using. You don't need to implement the parameter examples, just understand the concepts.


5. Triggering the Animation from the Radial Menu

The final step is to connect our UI to the CharacterAnimatorManager.

Open SelectionManager.cs. In the Update() method, find the callback action you created for the radial menu entries in the last lesson. We need to modify it to call our new animation manager.

// In SelectionManager.cs -> inside the Update() method and the raycast check

// ... existing code ...

// You will need a reference to the player's animator manager.
// A simple way is to find it in the scene. For a larger game, you'd use a singleton or dependency injection.
CharacterAnimatorManager playerAnimatorManager = FindObjectOfType<CharacterAnimatorManager>();

foreach (string id in interactable.availableInteractionIDs)
{
    if (interactable.IsInteractionValid(id))
    {
        InteractionData data = InteractionDataManager.GetInteraction(id);
        if (data != null)
        {
            // --- THIS IS THE UPDATED CALLBACK ---
            System.Action callback = () => {
                Debug.Log($"Executing interaction: {data.displayName}");

                // 1. Trigger the animation
                if (playerAnimatorManager != null && !string.IsNullOrEmpty(data.animationName))
                {
                    playerAnimatorManager.PlayAnimation(data.animationName);
                }

                // 2. Update the object's state (as before)
                if (interactable is ChairInteractable chair)
                {
                    chair.SetOccupied(true);
                }
            };
            // --- END OF UPDATE ---

            _radialMenu.AddEntry(data.displayName, null, callback);
        }
    }
}
// ... rest of the code ...

Now, run the scene. Click on the chair, select "Sit," and your character should play the SitDown animation!


6. Challenge: Returning to Idle

You might notice a problem. After the SitDown animation finishes, the character freezes in the last frame. This is because we told it to play a non-looping animation, but we never told it what to do next.

We need a way to return to the Idle state after a one-shot animation completes. We can achieve this with a clever Unity feature called StateMachineBehaviour. This is a script you can attach directly to an animation state in the Animator window.

  1. Create a new C# script named OnAnimationComplete.cs.
  2. Replace its content with the following:
using UnityEngine;

public class OnAnimationComplete : StateMachineBehaviour
{
    public string animationToPlayOnExit = "Idle";
    
    // This function is called when a transition starts and the state machine starts evaluating this state
    override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
       // We use the animator's gameObject to find our manager script and start a coroutine.
       CharacterAnimatorManager manager = animator.GetComponent<CharacterAnimatorManager>();
       if(manager != null)
       {
           manager.StartCoroutine(WaitForAnimation(stateInfo.length, manager));
       }
    }

    private System.Collections.IEnumerator WaitForAnimation(float length, CharacterAnimatorManager manager)
    {
        yield return new WaitForSeconds(length);
        manager.PlayAnimation(animationToPlayOnExit);
    }
}

Now, go to the Animator window, select the SitDown state, and click Add Behaviour in the Inspector. Choose OnAnimationComplete.

This script will now automatically start a timer when the SitDown animation begins. When the animation clip's duration has passed, it will tell our CharacterAnimatorManager to play the Idle animation, returning the character to a neutral state. This is a powerful, reusable pattern for handling one-shot interaction animations.



Conclusion

Excellent work! You've successfully bridged the gap between a player's choice and the character's physical actions. The visual feedback from animations is now in place, making the simulation far more engaging.

Key Takeaways:

  • Code-Driven Animation: We've implemented a scalable, code-centric animation system using Animator.CrossFade to avoid complex visual state machines.
  • Data-Driven Design: By adding an animationName to our interactions.json, we've made it easy to link any interaction to any animation, which is great for future content and modding.
  • StateMachineBehaviours: You learned how to use StateMachineBehaviour to attach logic directly to an animation state, allowing you to elegantly handle events like an animation finishing.

In the next lesson, Send an interaction command to Rust to update the character's needs and mood upon completion, we will complete the interaction loop. We will take the chosen interaction ID and send it across the FFI to our Rust backend, where it will finally affect the character's underlying simulation values like hunger, energy, and mood.