Adventure Creator Wikia
Advertisement

AC makes it possible to generate lipsyncing animation at runtime by processing lipsync data from various sources (Papagayo, SAPI etc).  However, the best quality lipsync animation often comes from manually animating facial movement for individual lines.

This script makes it possible to play such animations without the need to place them all in a single Animator Controller asset, as well as the option to use different animations for each of your game's language.  It works by using an Animator Override Controller to replace the intended animation at runtime.

To use it:

  1. Create a new sub-layer in your character's Animator Controller, and configure it to allow for facial animation without interfering with the character's regular full-body animation.
  2. Create a new Trigger Animator paramater named e.g. OnSpeak.
  3. Create a new facial animation clip that has no visible effect (e.g. just record the jaw in its default position), but have it last as long as the longest-possible speech line (necessary as overriding animations doesn't affect duration).
  4. Add this clip to the Animator as sub-layer's Default, and have the Enter state transition to it whenever the OnSpeak Trigger parameter is invoked (be sure to check "Can Transition To Self".
  5. Create an Animator Override Controller for the character's Animator Controller, and assign it in your character's Animator component.
  6. Attach the script below to your character and fill in its Inspector.  The "Empty Clip" field should be set to the animation created in step 3.
  7. Create AC speech as normal, and gather it up in the Speech Manager to generate ID values for each line.
  8. Import Unity's Addressbles package, and convert each facial AnimationClip asset to an Addressable, naming its key using the convention "CharacterName_LineID".
  9. (Optional) If the Facial Animation component's "Per Language" option is checked, the convention should instead be "CharacterName_LanguageIndex_LineID".

At runtime, the script will then look for the Addressable that matches the naming convention, and have it override the "Empty Clip" animation.

FacialAnimation.cs:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

namespace AC
{

	public class FacialAnimation : MonoBehaviour
	{

		[SerializeField] private Char character = null;
		[SerializeField] private AnimationClip emptyClip = null;
		[SerializeField] private bool perLanguage = false;
		[SerializeField] private string triggerName = "OnSpeak";
		private AnimatorOverrideController animatorOverrideController;


		private void Start ()
		{
			if (character == null || emptyClip == null || !string.IsNullOrEmpty (triggerName))
			{
				return;
			}

			animatorOverrideController = (AnimatorOverrideController) character.GetAnimator ().runtimeAnimatorController;
			if (animatorOverrideController == null)
			{
				Debug.LogWarning ("Character " + character + "'s AnimatorController must be an Animator Coverride Controller");
				return;
			}

			EventManager.OnStartSpeech += OnStartSpeech;
		}


		private void OnDisable ()
		{
			EventManager.OnStartSpeech -= OnStartSpeech;
		}


		private void OnStartSpeech (Char speakingCharacter, string speechText, int lineID)
		{
			if (speakingCharacter != character)
			{
				return;
			}

			string addressableKey = speakingCharacter.gameObject.name + "_" + lineID;
			if (perLanguage)
			{
				int language = Options.GetLanguage ();
				addressableKey += "_" + language;
			}

			Addressables.LoadAssetAsync<AnimationClip> (addressableKey).Completed += OnLoadAnimationAsset;
		}


		private void OnLoadAnimationAsset (AsyncOperationHandle<AnimationClip> obj)
		{
			var overrides = new List<KeyValuePair<AnimationClip, AnimationClip>> (animatorOverrideController.overridesCount);
			animatorOverrideController.GetOverrides (overrides);
			for (int i = 0; i < overrides.Count; ++i)
			{
				if (overrides[i].Key == emptyClip)
				{
					overrides[i] = new KeyValuePair<AnimationClip, AnimationClip> (overrides[i].Key, emptyClip);
					break;
				}
			}
			animatorOverrideController.ApplyOverrides (overrides);
			character.GetAnimator ().SetTrigger (triggerName);
		}
	}

}
Advertisement