Unity MR Part 11: Animation
tststs
Posted on January 11, 2024
๐ 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.
Now add the component Animator
via Add Component
.
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 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.
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.
In your Scene
window the door should now look as follows:
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.
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.
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.
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.
Now, select the DoorAnimation
animation clip in your Project
window. 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);
}
}
}
Let's go over the modifications made to the MRArticleSeriesController
script:
- Added
private GameObject doorInstance;
This variable holds the reference to the instantiated door in the scene. -
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. -
OpenDoor
: This public method will be called from theOpenDoorConduit
. It triggers the door's animation usinganimator.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();
}
}
}
}
}
Let's go over the modifications made to the OpenDoorConduit
script:
-
[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. -
OpenDoor
: In this part of the script, we invoke theOpenDoor
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:
Testing the app
We are now prepared to test the app. Select Build and Run
.
- Press the trigger on the left controller.
- The label indicated "...Listening...".
- Speak the word phrase โopen the doorโ.
- After a brief delay the door animation should now be playing.
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.
Posted on January 11, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.