DEV Community

Cover image for Unity MR Part 11: Animation
tststs for Taikonauten

Posted on • Updated on • Originally published at Medium

Unity MR Part 11: Animation

๐Ÿ‘€ Stumbled here on accident? Start with the introduction!

๐Ÿ“š 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


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

Selecting the Door mesh on our door Prefab

Now add the component Animator via Add Component.

Adding the Animator component to the Door mesh

Adding the Animator component to the Door mesh

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

The animation window after clicking Create

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

Adding the rotation property to the animation

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

The animation window with the configured Rotation.y property

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

Door state at the end of the animation

Door state at the end of the animation

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

Door controller in our Animator

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

Creating an empty state in the Animator

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

Setting New State as the default layer

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

New State is the default layer

Now, select the DoorAnimation animation clip in your Projectwindow. Then, in the inspector, uncheck Loop Time.

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

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);
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

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.

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();
                }
            }
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

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

Selecting the MRArticleSeriesController for the Open Door Conduit script

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.

Video of the app where the door opens when the phrase โ€œopen the doorโ€œ is recognized

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.

Top comments (0)