Unity MR Part 11: Animation
Tags: unity3d, mixedreality, tutorial, beginners
Published: 2024-01-11 08:51:23

👀 Stumbled here on accident? Start with the [introduction](https://dev.to/taikonauten/part-0-introduction-56fl)!

📚 The objective of this article is to animate the door in response to the voice command “open the door”. To achieve this, we will develop an animation specifically for the door and make necessary updates to the `Player` and `OpenDoorConduit` scripts. This integration of voice command functionality with animation will enhance the interactivity and realism of our application.

---

ℹ️ If you find yourself facing any difficulties, remember that you can always refer to or download the code from our accompanying [GitHub repository](https://github.com/taikonauten/unity-mr-article-series/tree/main/11_Animation)

---

Lets start with the animation. Edit our `Door` Prefab by double-clicking it within the `Project` window. Select the `Door` mesh as seen in the next screenshot.


![Selecting the Door mesh on our door Prefab](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nveulq3156oysv07v015.png)
<figcaption>Selecting the Door mesh on our door Prefab</figcaption>
---

Now add the component `Animator` via `Add Component`.


![Adding the Animator component to the Door mesh](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cq4ciqsbm4jf80mvay0e.png)
<figcaption>Adding the Animator component to the Door mesh</figcaption>
---

With the `Door` mesh still selected, navigate to **Window → Animation → Animation** to open the animation window in Unity. Once the animation window is open, click on the `Create` button. You will then be prompted to choose a location for saving the Animation Clip. Save it in a new folder **Assets/Animations** and name it `DoorAnimation`.


![The animation window after clicking Create](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6aldlbofledgbskbjq51.png)
<figcaption>The animation window after clicking Create</figcaption>
---

The next step involves adding a property to animate. As we are crafting an “opening the door” animation, we need to animate the Transform rotation. To do this, click on `Add Property`, and then select **Transform → Rotation**. This action allows us to specifically target and animate the door's rotation, creating a realistic opening effect in our animation sequence.


![Adding the rotation property to the animation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/63fbn8yrmto73qhzd8kw.png)
<figcaption>Adding the rotation property to the animation</figcaption>
---

First move the `Timeline` to `60`. You can either drag and drop the indicator or enter `60` on the left side of the `Timeline`. Then, enter `-120` for the `Rotation.y` value.


![The animation window with the configured Rotation.y property](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eaum4f39pvwiv9nlkjxz.png)
<figcaption>The animation window with the configured Rotation.y property</figcaption>
---

In your `Scene` window the door should now look as follows:


![Door state at the end of the animation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tzcwy9vmemrwtp4m4ccl.png)
<figcaption>Door state at the end of the animation</figcaption>
---

When creating an animation in Unity, the default setting typically causes the animation to loop continuously. However, since our objective is to trigger the door animation just once upon recognizing the correct voice command, we need to adjust the looping behavior in the `Door` `Animation Controller`.

To make this change, navigate to **Assets/Animations** in the Unity Editor and double-click on the `Door` `Animation` controller. This action will open the Animator window with the Door.controller loaded. In the Animator window, we can modify the settings to ensure that the door animation plays only once instead of looping endlessly.


![Door controller in our Animator](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ii51uugfiacdycf58ug9.png)
<figcaption>Door controller in our Animator</figcaption>
---

Right-click on an empty space within the `Animator` window and select **Create State -> Empty**, as shown in the upcoming screenshot. This step will add a new, empty state to the animation controller.


![Creating an empty state in the Animator](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/agkweha4crqwst4bb2np.png)
<figcaption>Creating an empty state in the Animator</figcaption>
---

Right-click on the newly created state, named `New State`, and select `Set as Layer Default State`. This action will designate the `New State` as the default state for that particular layer in the animation controller, ensuring that this is the starting state when the animation sequence begins.


![Setting New State as the default layer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nw8i0z8aih4e7i09lghz.png)
<figcaption>Setting New State as the default layer</figcaption>
---

The result should be as follows: Since the state is empty and no properties have been added to it, this results in the absence of any animation when the GameObject loads. This setup ensures that the door remains static initially, allowing for the animation to be triggered specifically by an event, such as a voice command, rather than starting automatically upon loading.


![New State is the default layer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h9yneco4znmertvhs865.png)
<figcaption>New State is the default layer</figcaption>
---

Now, select the `DoorAnimation` animation clip in your `Project`window. Then, in the inspector, uncheck `Loop Time`.


![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/06kcdu3jh41v8kqujtzm.png)

We are now good to go to trigger the animation in our `MRArticleSeriesController` script.

```csharp
using System.Collections;
using System.Collections.Generic;
using Meta.WitAi;
using Meta.WitAi.Requests;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;
using UnityEngine.XR.Interaction.Toolkit;

namespace Taikonauten.Unity.ArticleSeries
{
    public class MRArticleSeriesController : MonoBehaviour
    {
        [SerializeField] private ARAnchorManager anchorManager;
        [SerializeField] private GameObject door;
        [SerializeField] private GameObject uI;
        [SerializeField] private InputActionReference buttonActionLeft;
        [SerializeField] private InputActionReference buttonActionRight;
        [SerializeField] private VoiceService voiceService;
        [SerializeField] private XRRayInteractor rayInteractor;
        private VoiceServiceRequest voiceServiceRequest;
        private VoiceServiceRequestEvents voiceServiceRequestEvents;
        private GameObject doorInstance;

        void OnEnable()
        {
            Debug.Log("MRArticleSeriesController -> OnEnable()");

            buttonActionRight.action.performed += OnButtonPressedRightAsync;
            buttonActionLeft.action.performed += OnButtonPressedLeft;
        }

        void OnDisable()
        {
            Debug.Log("MRArticleSeriesController -> OnDisable()");
            buttonActionRight.action.performed -= OnButtonPressedRightAsync;
            buttonActionLeft.action.performed -= OnButtonPressedLeft;
        }

        private void ActivateVoiceService()
        {
            Debug.Log("MRArticleSeriesController -> ActivateVoiceService()");

            if (voiceServiceRequestEvents == null)
            {
                voiceServiceRequestEvents = new VoiceServiceRequestEvents();

                voiceServiceRequestEvents.OnInit.AddListener(OnInit);
                voiceServiceRequestEvents.OnComplete.AddListener(OnComplete);
            }

            voiceServiceRequest = voiceService.Activate(voiceServiceRequestEvents);
        }

        private void DeactivateVoiceService()
        {
            Debug.Log("MRArticleSeriesController -> DeactivateVoiceService()");
            voiceServiceRequest.DeactivateAudio();
        }

        private void OnInit(VoiceServiceRequest request)
        {
            uI.SetActive(true);
        }

        private void OnComplete(VoiceServiceRequest request)
        {
            uI.SetActive(false);
            DeactivateVoiceService();
        }

        private async void OnButtonPressedRightAsync(InputAction.CallbackContext context)
        {
            Debug.Log("MRArticleSeriesController -> OnButtonPressedRightAsync()");

            if (doorInstance != null)
            {
                Debug.Log("MRArticleSeriesController -> OnButtonPressedRightAsync(): Door already instantiated");

                return;
            }

            if (rayInteractor.TryGetCurrent3DRaycastHit(out RaycastHit hit))
            {
                Pose pose = new(hit.point, Quaternion.identity);
                Result<ARAnchor> result = await anchorManager.TryAddAnchorAsync(pose);

                result.TryGetResult(out ARAnchor anchor);

                if (anchor != null)
                {
                    // Instantiate the door Prefab
                    doorInstance = Instantiate(door, hit.point, Quaternion.identity);
                    // Unity recommends parenting your content to the anchor.
                    doorInstance.transform.parent = anchor.transform;
                }
            }
        }

        private void OnButtonPressedLeft(InputAction.CallbackContext context)
        {
            Debug.Log("MRArticleSeriesController -> OnButtonPressedLeft()");

            ActivateVoiceService();
        }

        public void OpenDoor()
        {
            Debug.Log("MRArticleSeriesController -> OpenDoor()");

            if (doorInstance == null)
            {
                Debug.Log("MRArticleSeriesController -> OpenDoor(): no door instantiated yet.");

                return;
            }

            Animator animator = doorInstance.GetComponentInChildren<Animator>();
            animator.Play("DoorAnimation", -1, 0);
        }
    }
}

```

Let's go over the modifications made to the `MRArticleSeriesController` script:

1. Added `private GameObject doorInstance;` This variable holds the reference to the instantiated door in the scene.
2. `OnButtonPressedRightAsync`: In this method, we now check if a door has already been instantiated in the scene. If a door is present, the method returns early to prevent another door from being instantiated.
3. `OpenDoor`: This public method will be called from the `OpenDoorConduit`. It triggers the door's animation using `animator.Play`, initiating the opening sequence of the door.
    

These changes enhance the functionality of the Player script, ensuring proper management and control of the door animation in response to user interactions and voice commands.

Now, edit the `OpenDoorConduit` script.

```csharp
using System.Collections;
using System.Collections.Generic;
using Meta.WitAi;
using UnityEngine;

namespace Taikonauten.Unity.ArticleSeries
{
    public class OpenDoorConduit : MonoBehaviour
    {
        [SerializeField] private MRArticleSeriesController mRArticleSeriesController;
        private const string OPEN_DOOR_INTENT = "open_door";

        [MatchIntent(OPEN_DOOR_INTENT)]
        public void OpenDoor(string[] values)
        {
            Debug.Log("OpenDoorConduit -> OpenDoor()");

            string action = values[0];
            string entity = values[1];

            if (!string.IsNullOrEmpty(action) && !string.IsNullOrEmpty(entity))
            {
                if (action == "open" && entity == "door") {
                    mRArticleSeriesController.OpenDoor();
                }
            }
        }
    }
}

```

Let's go over the modifications made to the `OpenDoorConduit` script:

1. `[SerializeField] private MRArticleSeriesController mRArticleSeriesController;` This line declares a private variable and marks it with `[SerializeField]` so it can be assigned via the Unity Editor. This variable holds the reference to our Player component, allowing the script to interact with the Player component's public methods and properties.
2. `OpenDoor`: In this part of the script, we invoke the `OpenDoor` public method of the Player script which starts the door animation.
    

These modifications are essential for enabling communication and interaction between different components and scripts in our Unity project, particularly for handling the door-opening functionality.

Make sure to select the `MRArticleSeriesController` component in the inspector:


![Selecting the MRArticleSeriesController for the Open Door Conduit script](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w37jyk5lyxj7eggawzpm.png)
<figcaption>Selecting the MRArticleSeriesController for the Open Door Conduit script</figcaption>
---

## Testing the app

We are now prepared to test the app. Select `Build and Run`.

1. Press the trigger on the left controller.
2. The label indicated "...Listening...".
3. Speak the word phrase “open the door”.
4. After a brief delay the door animation should now be playing.

{% embed https://player.vimeo.com/video/901565203?h=ea560336f1 %}
<figcaption>Video of the app where the door opens when the phrase “open the door“ is recognized</figcaption>
---

## Next article

In the next article, we will focus on enhancing the visual appeal of our scene. This will include implementing graphic improvements, adding a scene to be displayed behind the door, and disabling the plane material, which was previously used solely for debugging purposes. These refinements are aimed at elevating the aesthetic quality and immersive experience of our application, making it more engaging and visually appealing to users.