Skip to main content

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

The code below is delimited in regions; read through, or copy and press CTRL-F to jump to them (those aren't links):

  • #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 state.
).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