What's new in V1
Update page
Most of the details of this update are available on the Update page, and the migration page.
Example code
Skip to the regions below:
- #region What's New? - VRChat - Extension Methods
- #region What's New? - VRChat - VRCAnimatorPlayAudio
- #region What's New? - VRChat - Pretty Parameter Drivers
- #region What's New? - Sub State Machines
- #region What's New? - Blend Trees
#if UNITY_EDITOR
using System;
using AnimatorAsCode.V1;
using UnityEditor.Animations;
using UnityEngine;
#if VRC_SDK_VRCSDK3
using VRC.SDK3.Avatars.Components;
using VRC.SDKBase;
using AnimatorAsCode.V1.VRC;
#endif
namespace DemonstrationAsCode.V1
{
[ExecuteInEditMode]
public class DemonstrationAsCode_WhatsNewInV1 : MonoBehaviour
#if VRC_SDK_VRCSDK3
, IEditorOnly
#endif
{
public RuntimeAnimatorController controller;
private AudioClip[] AudioClips { get; set; }
private AudioSource Source { get; set; }
private void OnEnable()
{
#region Initialize demonstration
if (controller == null) return;
if (Source == null)
{
if (transform.GetComponent<AudioSource>() == null) transform.gameObject.AddComponent<AudioSource>();
Source = transform.GetComponent<AudioSource>();
}
if (AudioClips == null) AudioClips = Array.Empty<AudioClip>();
// This configuration is only for demonstration purposes. It does not create persistent assets in the project.
var aac = AacV1.Create(new AacConfiguration
{
AnimatorRoot = transform,
AssetContainer = null,
ContainerMode = AacConfiguration.Container.Never,
SystemName = "WhatsNew",
DefaultValueRoot = transform,
DefaultsProvider = new AacDefaultsProvider(true)
});
// This is only for demonstration purposes: Reuse an existing animator controller, and edit a layer inside of it
var layer = aac.CreateMainArbitraryControllerLayer((AnimatorController)controller);
#endregion
#if VRC_SDK_VRCSDK3
#region What's New? - VRChat - Extension Methods
{
// Animator As Code V1 no longer requires VRChat (compared to V0).
// VRChat-specific functions have been moved to extension methods.
// If you want to use VRChat Avatars functionality, add the `Animator As Code V1 - VRChat` package, and do the following:
//
// Add the following import:
// using AnimatorAsCode.V1.VRC;
//
// To access VRChat parameters, use the following extension method:
var vrcAv3 = layer.Av3();
// To access VRChat assets, use the following extension method:
var vrcAssets = aac.VrcAssets();
layer.NewState("UsingVRChat")
.WithAnimation(vrcAssets.ProxyForGesture(AacAv3.Av3Gesture.HandOpen, false))
// VRChat State Behaviours are created through extension methods located in namespace `AnimatorAsCode.V1.VRC`
.TrackingAnimates(AacAv3.Av3TrackingElement.RightHand)
.Driving(driver => driver.Sets(layer.BoolParameter("A"), true))
.TransitionsFromEntry()
.When(vrcAv3.GestureRight.IsEqualTo(AacAv3.Av3Gesture.HandOpen));
}
#endregion
#region What's New? - VRChat - VRCAnimatorPlayAudio
{
// Animator As Code V1.1.0 introduces the Audio lambda expression.
// By default, this behaviour is initialized to do nothing. Invoke methods to add more functionality.
AudioSource source = Source; // This can be a string instead.
AudioClip[] clips = AudioClips;
layer.NewState("UsingAudio")
.Audio(source, audio =>
{
// Get the PlayAudio object if there's a need to edit it directly.
VRCAnimatorPlayAudio vrcAnimatorPlayAudio = audio.PlayAudio;
// By default, a PlayAudio created through AAC does nothing (unlike a manually created behaviour)
// so you need to invoke anything that is relevant.
audio
.SelectsClip(VRC_AnimatorPlayAudio.Order.Random, clips)
.SetsLooping()
.RandomizesPitch(0.8f, 1.2f)
.RandomizesVolume(0.5f, 1f)
// "Replays" means Stop and Play.
// "StartsPlaying" means just Play.
// "StopsPlaying" means just Stop.
// To do neither Stop nor Play, don't invoke anything.
.StartsPlayingOnEnter()
.StopsPlayingOnExit();
});
}
#endregion
#region What's New? - VRChat - Pretty Parameter Drivers
{
// Animator As Code V1.1.0 introduces the Driving lambda expression.
// As Parameter Driver has evolved over the years, it has become more complex.
// The Driving lambda expression encompasses the Parameter Driver behaviour,
// and also lets you create more than one Parameter Driver behaviour on a single state:
// One of them could be Local, the other not.
layer.NewState("UsingDrivers").Driving(driver => driver
.Copies(layer.FloatParameter("CopySource"), layer.FloatParameter("CopyDestination"))
.Sets(layer.FloatParameter("Set"), 2.3f)
.Increases(layer.FloatParameter("IncreaseBy2"), 2f)
.Decreases(layer.FloatParameter("DecreaseBy3"), 3f)
.Randomizes(layer.FloatParameter("Randomizes"), 0f, 100f)
.Randomizes(layer.BoolParameter("RandomizesBool"), 0.25f) // 25% chance of being true.
.Remaps(layer.FloatParameter("RemapsSource"), 0f, 2f, layer.FloatParameter("RemapsDestination"), 2f, 4f)
// This creates a second VRCAvatarParameterDriver in the same behaviour.
).Driving(driver => driver
.Sets(layer.FloatParameter("SecondDriver"), 100f)
.Locally() // Only this second VRCAvatarParameterDriver is local.
);
}
#endregion
#endif
#region What's New? - Sub State Machines
{
// Animator As Code V1.1.0 introduces Sub-State Machines, a major contribution by [galister](https://github.com/galister).
// When using Sub-State Machines, the Sub-State Machine will evaluate all transitions until
// it resolves a destination state within one single frame.
// This means it can traverse multiple transition conditions at once, no matter how nested
// the Sub-State Machine is.
//
// This is not doable with just states, so Sub-State Machines have a functional value beyond mere organization.
var a = layer.IntParameter("IntA");
var b = layer.IntParameter("IntB");
var rootSsm = layer.NewSubStateMachine("UsingNestedSubStateMachines");
for (var i = 0; i < 16; i++)
{
// A Sub-State Machine can have other Sub-State Machines created inside them.
// TransitionsFromEntry creates a transition between `subSsm` and the Entry node of the Sub-State Machine it belongs in.
// Exits creates a transition between `subSsm` and the Exit node.
var subSsm = rootSsm.NewSubStateMachine($"A = {i}");
subSsm.TransitionsFromEntry().When(a.IsEqualTo(i));
subSsm.Exits();
for (var j = 0; j < 16; j++)
{
var state = subSsm.NewState($"A = {i} and B = {j}");
state.TransitionsFromEntry().When(b.IsEqualTo(j));
state.Exits()
.When(a.IsNotEqualTo(i))
.Or()
.When(b.IsNotEqualTo(j));
}
}
// This creates a transition between the Sub-State Machine and itself.
// When that Sub-State Machine exits, it will re-enter itself.
rootSsm.Restarts();
}
#endregion
#region What's New? - Blend Trees
{
layer.NewState("BlendTrees").WithAnimation(aac.NewBlendTree()
.FreeformDirectional2D(layer.FloatParameter("X"), layer.FloatParameter("Y"))
.WithAnimation(aac.NewClip("Center"), 0, 0)
.WithAnimation(aac.NewClip("Up"), Vector2.up)
.WithAnimation(aac.NewClip("Right"), 1, 0)
.WithAnimation(aac.NewClip("Down"), 0, -1)
.WithAnimation(aac.NewClip("Left"), -1, 0)
);
var one = layer.FloatParameter("One");
layer.OverrideValue(one, 1f);
layer.NewState("Direct BlendTree").WithAnimation(aac.NewBlendTree()
.Direct()
.WithAnimation(aac.NewClip("DrivenByA"), layer.FloatParameter("A"))
.WithAnimation(aac.NewClip("AlwaysOn"), one)
.WithAnimation(aac.NewBlendTree().Simple1D(layer.FloatParameter("B"))
// In Animator As Code, it is safe to declare points in a Simple1D blend tree in a different order (i.e. 0, 1, -1).
// (In native blend trees, it would not have been safe to do so)
.WithAnimation(aac.NewClip("Zero"), 0)
.WithAnimation(aac.NewClip("Positive"), 1)
.WithAnimation(aac.NewClip("Negative"), -1)
, one)
);
}
#endregion
}
}
}
#endif