shity basic one shot
This commit is contained in:
@@ -0,0 +1,407 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AnimatorAsCode.V1;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AnimatorAsCrab.V1
|
||||
{
|
||||
/// <summary>
|
||||
/// Turns an <see cref="AacCrabGraph"/> into an AnimatorController by driving Animator As Code V1.
|
||||
/// This uses the modification workflow: the controller is cleared and rebuilt, and no layer of the
|
||||
/// controller is touched beforehand.
|
||||
/// </summary>
|
||||
public static class AacCrabGenerator
|
||||
{
|
||||
/// <summary>Clears the AnimatorController and the assets of the same asset key, then rebuilds them.</summary>
|
||||
public static void Generate(AacCrabGraph graph, AacConfiguration configuration, AnimatorController controller)
|
||||
{
|
||||
if (graph == null) throw new ArgumentNullException(nameof(graph));
|
||||
if (controller == null) throw new ArgumentNullException(nameof(controller));
|
||||
if (graph.Controller == null) throw new InvalidOperationException("The graph has no controller.");
|
||||
|
||||
var aac = AacV1.Create(configuration);
|
||||
var modification = aac.Modification();
|
||||
|
||||
aac.ClearPreviousAssets();
|
||||
var aacController = modification.ResetAnimatorController(controller);
|
||||
|
||||
var layers = new Dictionary<string, AacFlLayer>();
|
||||
foreach (var layer in graph.Controller.Layers)
|
||||
{
|
||||
layers[layer.Name] = aacController.NewLayer(layer.Name);
|
||||
}
|
||||
|
||||
var floatingParameters = CreateParameters(graph, layers, controller);
|
||||
|
||||
var clips = new Dictionary<string, AacFlClip>();
|
||||
foreach (var clip in graph.Clips)
|
||||
{
|
||||
clips[clip.Name] = CreateClip(aac, clip);
|
||||
}
|
||||
|
||||
// Blend trees are created in declaration order, so a child motion can only refer to a tree
|
||||
// that was declared before its parent. This also makes cycles impossible.
|
||||
var blendTrees = new Dictionary<string, AacFlBlendTree>();
|
||||
foreach (var blendTree in graph.BlendTrees)
|
||||
{
|
||||
blendTrees[blendTree.Name] = CreateBlendTree(aac, blendTree, clips, blendTrees, floatingParameters);
|
||||
}
|
||||
|
||||
foreach (var layer in graph.Controller.Layers)
|
||||
{
|
||||
BuildLayer(layers[layer.Name], layer, clips, blendTrees);
|
||||
}
|
||||
|
||||
modification.SetDirtyAll();
|
||||
EditorUtility.SetDirty(controller);
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, AacFlFloatParameter> CreateParameters(AacCrabGraph graph, IReadOnlyDictionary<string, AacFlLayer> layers, AnimatorController controller)
|
||||
{
|
||||
var floatingParameters = new Dictionary<string, AacFlFloatParameter>();
|
||||
if (graph.Parameters.Count == 0)
|
||||
{
|
||||
return floatingParameters;
|
||||
}
|
||||
|
||||
if (layers.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("The controller declares parameters but no layer to hold them.");
|
||||
}
|
||||
|
||||
// Parameters are controller-wide, so any layer can create them.
|
||||
var layer = layers.First().Value;
|
||||
foreach (var parameter in graph.Parameters)
|
||||
{
|
||||
switch (parameter.Type)
|
||||
{
|
||||
case AacCrabParameterType.Float:
|
||||
floatingParameters[parameter.Name] = layer.FloatParameter(parameter.Name);
|
||||
break;
|
||||
case AacCrabParameterType.Int:
|
||||
layer.IntParameter(parameter.Name);
|
||||
break;
|
||||
case AacCrabParameterType.Bool:
|
||||
layer.BoolParameter(parameter.Name);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown parameter type {parameter.Type}.");
|
||||
}
|
||||
}
|
||||
|
||||
ApplyParameterDefaults(controller, graph.Parameters);
|
||||
return floatingParameters;
|
||||
}
|
||||
|
||||
// Animator As Code creates parameters with Unity's defaults; the graph's defaults are applied
|
||||
// afterwards by mutating the controller's own parameter list.
|
||||
private static void ApplyParameterDefaults(AnimatorController controller, IEnumerable<AacCrabParameter> parameters)
|
||||
{
|
||||
var wanted = parameters.ToDictionary(parameter => parameter.Name);
|
||||
var current = controller.parameters;
|
||||
foreach (var parameter in current)
|
||||
{
|
||||
if (!wanted.TryGetValue(parameter.name, out var declared))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (declared.Type)
|
||||
{
|
||||
case AacCrabParameterType.Float:
|
||||
parameter.defaultFloat = declared.DefaultFloat;
|
||||
break;
|
||||
case AacCrabParameterType.Int:
|
||||
parameter.defaultInt = declared.DefaultInt;
|
||||
break;
|
||||
case AacCrabParameterType.Bool:
|
||||
parameter.defaultBool = declared.DefaultBool;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
controller.parameters = current;
|
||||
}
|
||||
|
||||
private static AacFlClip CreateClip(AacFlBase aac, AacCrabClip graph)
|
||||
{
|
||||
var clip = aac.NewClip(graph.Name);
|
||||
if (graph.Looping)
|
||||
{
|
||||
clip.Looping();
|
||||
}
|
||||
else
|
||||
{
|
||||
clip.NonLooping();
|
||||
}
|
||||
|
||||
return clip.Animating(edit =>
|
||||
{
|
||||
foreach (var curve in graph.Curves)
|
||||
{
|
||||
var keys = curve.Keys
|
||||
.Select(key => new Keyframe(key.Time, key.Value, key.InTangent, key.OutTangent))
|
||||
.ToArray();
|
||||
edit.Animates(curve.Path, UnityType(curve.Target), curve.Property)
|
||||
.WithAnimationCurve(new AnimationCurve(keys));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static Type UnityType(AacCrabTargetType target)
|
||||
{
|
||||
switch (target)
|
||||
{
|
||||
case AacCrabTargetType.GameObject:
|
||||
return typeof(GameObject);
|
||||
case AacCrabTargetType.SkinnedMeshRenderer:
|
||||
return typeof(SkinnedMeshRenderer);
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown curve target {target}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static AacFlBlendTree CreateBlendTree(
|
||||
AacFlBase aac,
|
||||
AacCrabBlendTree graph,
|
||||
IReadOnlyDictionary<string, AacFlClip> clips,
|
||||
IReadOnlyDictionary<string, AacFlBlendTree> trees,
|
||||
IReadOnlyDictionary<string, AacFlFloatParameter> floatingParameters)
|
||||
{
|
||||
Motion Resolve(AacCrabMotionRef reference) => MotionOf(reference, clips, trees);
|
||||
|
||||
AacFlFloatParameter FloatParameter(string name)
|
||||
{
|
||||
if (name == null || !floatingParameters.TryGetValue(name, out var parameter))
|
||||
{
|
||||
throw new InvalidOperationException($"Blend tree '{graph.Name}' uses '{name}', which is not a Float parameter.");
|
||||
}
|
||||
|
||||
return parameter;
|
||||
}
|
||||
|
||||
var uninitialized = aac.NewBlendTree(graph.Name);
|
||||
switch (graph.BlendType)
|
||||
{
|
||||
case AacCrabBlendType.Simple1D:
|
||||
{
|
||||
var tree = uninitialized.Simple1D(FloatParameter(graph.ParamX));
|
||||
tree.BlendTree.useAutomaticThresholds = graph.UseAutomaticThresholds;
|
||||
foreach (var child in graph.Children)
|
||||
{
|
||||
tree.WithAnimation(Resolve(child.Motion), child.Threshold ?? 0f);
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
case AacCrabBlendType.SimpleDirectional2D:
|
||||
case AacCrabBlendType.FreeformDirectional2D:
|
||||
case AacCrabBlendType.FreeformCartesian2D:
|
||||
{
|
||||
var x = FloatParameter(graph.ParamX);
|
||||
var y = FloatParameter(graph.ParamY);
|
||||
var tree = graph.BlendType == AacCrabBlendType.SimpleDirectional2D
|
||||
? uninitialized.SimpleDirectional2D(x, y)
|
||||
: graph.BlendType == AacCrabBlendType.FreeformDirectional2D
|
||||
? uninitialized.FreeformDirectional2D(x, y)
|
||||
: uninitialized.FreeformCartesian2D(x, y);
|
||||
foreach (var child in graph.Children)
|
||||
{
|
||||
tree.WithAnimation(Resolve(child.Motion), child.Threshold ?? 0f, child.ThresholdY ?? 0f);
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
case AacCrabBlendType.Direct:
|
||||
{
|
||||
var tree = uninitialized.Direct();
|
||||
foreach (var child in graph.Children)
|
||||
{
|
||||
tree.WithAnimation(Resolve(child.Motion), FloatParameter(child.DirectParam));
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown blend type {graph.BlendType}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static Motion MotionOf(
|
||||
AacCrabMotionRef reference,
|
||||
IReadOnlyDictionary<string, AacFlClip> clips,
|
||||
IReadOnlyDictionary<string, AacFlBlendTree> trees)
|
||||
{
|
||||
switch (reference.Type)
|
||||
{
|
||||
case AacCrabMotionType.Clip:
|
||||
if (!clips.TryGetValue(reference.Name, out var clip))
|
||||
{
|
||||
throw new InvalidOperationException($"Clip '{reference.Name}' is not declared.");
|
||||
}
|
||||
|
||||
return clip.Clip;
|
||||
case AacCrabMotionType.BlendTree:
|
||||
// Blend trees are built in declaration order, so a tree can only refer to an earlier one.
|
||||
if (!trees.TryGetValue(reference.Name, out var tree))
|
||||
{
|
||||
throw new InvalidOperationException($"Blend tree '{reference.Name}' is not declared yet; declare it before the motion that uses it.");
|
||||
}
|
||||
|
||||
return tree.BlendTree;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown motion type {reference.Type}.");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class MachineScope
|
||||
{
|
||||
public AacFlStateMachine Machine;
|
||||
public AacCrabStateMachine Graph;
|
||||
}
|
||||
|
||||
private static void BuildLayer(
|
||||
AacFlLayer layer,
|
||||
AacCrabLayer graph,
|
||||
IReadOnlyDictionary<string, AacFlClip> clips,
|
||||
IReadOnlyDictionary<string, AacFlBlendTree> trees)
|
||||
{
|
||||
if (graph.StateMachine == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Layer '{graph.Name}' has no state machine.");
|
||||
}
|
||||
|
||||
Motion Resolve(AacCrabMotionRef reference) => MotionOf(reference, clips, trees);
|
||||
|
||||
// State names are unique within a layer, so a single dictionary resolves every transition,
|
||||
// including the ones that cross state machines.
|
||||
var states = new Dictionary<string, AacFlState>();
|
||||
var scopes = new List<MachineScope>();
|
||||
|
||||
AacFlStateMachine CreateMachine(AacFlStateMachine machine, AacCrabStateMachine machineGraph)
|
||||
{
|
||||
scopes.Add(new MachineScope { Machine = machine, Graph = machineGraph });
|
||||
foreach (var state in machineGraph.States)
|
||||
{
|
||||
var aacState = machine.NewState(state.Name, state.Position.X, state.Position.Y);
|
||||
if (state.Motion != null)
|
||||
{
|
||||
aacState.WithAnimation(Resolve(state.Motion));
|
||||
}
|
||||
|
||||
states[state.Name] = aacState;
|
||||
}
|
||||
|
||||
foreach (var subMachine in machineGraph.SubMachines)
|
||||
{
|
||||
CreateMachine(machine.NewSubStateMachine(subMachine.Name, subMachine.Position.X, subMachine.Position.Y), subMachine);
|
||||
}
|
||||
|
||||
return machine;
|
||||
}
|
||||
|
||||
// Create every state first: transitions may point forward.
|
||||
CreateMachine(layer.StateMachine, graph.StateMachine);
|
||||
|
||||
foreach (var scope in scopes)
|
||||
{
|
||||
foreach (var state in scope.Graph.States)
|
||||
{
|
||||
foreach (var transition in state.Transitions)
|
||||
{
|
||||
ApplyTransition(states[state.Name].TransitionsTo(Destination(states, transition)), transition);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var transition in scope.Graph.AnyStateTransitions)
|
||||
{
|
||||
ApplyTransition(scope.Machine.AnyTransitionsTo(Destination(states, transition)), transition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static AacFlState Destination(IReadOnlyDictionary<string, AacFlState> states, AacCrabTransition transition)
|
||||
{
|
||||
if (!states.TryGetValue(transition.To, out var destination))
|
||||
{
|
||||
throw new InvalidOperationException($"Transition target '{transition.To}' is not a state in this layer.");
|
||||
}
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
private static void ApplyTransition(AacFlTransition transition, AacCrabTransition graph)
|
||||
{
|
||||
transition.WithTransitionDurationSeconds(graph.Duration);
|
||||
if (graph.OrderedInterruption)
|
||||
{
|
||||
transition.WithOrderedInterruption();
|
||||
}
|
||||
else
|
||||
{
|
||||
transition.WithNoOrderedInterruption();
|
||||
}
|
||||
|
||||
if (graph.SourceInterruption)
|
||||
{
|
||||
transition.WithSourceInterruption();
|
||||
}
|
||||
|
||||
if (graph.CanTransitionToSelf)
|
||||
{
|
||||
transition.WithTransitionToSelf();
|
||||
}
|
||||
|
||||
if (graph.HasExitTime)
|
||||
{
|
||||
transition.AfterAnimationIsAtLeastAtNormalized(graph.ExitTime);
|
||||
}
|
||||
|
||||
// Conditions are applied last: Animator As Code forbids configuring a transition afterwards.
|
||||
if (graph.Conditions.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var continuation = transition.When(Condition(graph.Conditions[0]));
|
||||
for (var index = 1; index < graph.Conditions.Count; index++)
|
||||
{
|
||||
continuation = continuation.And(Condition(graph.Conditions[index]));
|
||||
}
|
||||
}
|
||||
|
||||
// Animator As Code only exposes typed comparisons for some parameter types, so conditions are
|
||||
// built the same way the library builds them internally.
|
||||
private static IAacFlCondition Condition(AacCrabCondition condition)
|
||||
{
|
||||
var parameter = condition.Parameter;
|
||||
var mode = UnityConditionMode(condition.Mode);
|
||||
var threshold = condition.Threshold;
|
||||
return AacFlConditionSimple.Just(appender => appender.Add(parameter, mode, threshold));
|
||||
}
|
||||
|
||||
private static AnimatorConditionMode UnityConditionMode(AacCrabCondMode mode)
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case AacCrabCondMode.Greater:
|
||||
return AnimatorConditionMode.Greater;
|
||||
case AacCrabCondMode.Less:
|
||||
return AnimatorConditionMode.Less;
|
||||
case AacCrabCondMode.Equals:
|
||||
return AnimatorConditionMode.Equals;
|
||||
case AacCrabCondMode.NotEqual:
|
||||
return AnimatorConditionMode.NotEqual;
|
||||
case AacCrabCondMode.If:
|
||||
return AnimatorConditionMode.If;
|
||||
case AacCrabCondMode.IfNot:
|
||||
return AnimatorConditionMode.IfNot;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown condition mode {mode}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace AnimatorAsCrab.V1
|
||||
{
|
||||
/// <summary>
|
||||
/// The wire format produced by the Rust core. Keep in sync with rust/src/graph.rs.
|
||||
/// Property names are snake_cased by the serializer settings, so C# names must only differ
|
||||
/// from the wire format by casing; enum values are explicit.
|
||||
/// </summary>
|
||||
public static class AacCrabJson
|
||||
{
|
||||
public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
|
||||
{
|
||||
// A graph the generator does not understand is a bug, not something to ignore.
|
||||
MissingMemberHandling = MissingMemberHandling.Error,
|
||||
ContractResolver = new DefaultContractResolver { NamingStrategy = new SnakeCaseNamingStrategy() },
|
||||
Converters = { new StringEnumConverter() },
|
||||
};
|
||||
|
||||
public static AacCrabGraph Parse(string json)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<AacCrabGraph>(json, Settings);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AacCrabGraph
|
||||
{
|
||||
public string SystemName { get; set; }
|
||||
public string AssetKey { get; set; }
|
||||
public List<AacCrabParameter> Parameters { get; set; }
|
||||
public List<AacCrabClip> Clips { get; set; }
|
||||
public List<AacCrabBlendTree> BlendTrees { get; set; }
|
||||
public AacCrabController Controller { get; set; }
|
||||
}
|
||||
|
||||
public sealed class AacCrabParameter
|
||||
{
|
||||
public AacCrabParameterType Type { get; set; }
|
||||
public string Name { get; set; }
|
||||
public JToken Default { get; set; }
|
||||
|
||||
public float DefaultFloat => Default.Value<float>();
|
||||
public int DefaultInt => Default.Value<int>();
|
||||
public bool DefaultBool => Default.Value<bool>();
|
||||
}
|
||||
|
||||
public enum AacCrabParameterType
|
||||
{
|
||||
[EnumMember(Value = "float")] Float,
|
||||
[EnumMember(Value = "int")] Int,
|
||||
[EnumMember(Value = "bool")] Bool,
|
||||
}
|
||||
|
||||
public sealed class AacCrabClip
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public bool Looping { get; set; }
|
||||
public List<AacCrabCurve> Curves { get; set; }
|
||||
}
|
||||
|
||||
public sealed class AacCrabCurve
|
||||
{
|
||||
public string Path { get; set; }
|
||||
public AacCrabTargetType Target { get; set; }
|
||||
public string Property { get; set; }
|
||||
public List<AacCrabKeyframe> Keys { get; set; }
|
||||
}
|
||||
|
||||
public enum AacCrabTargetType
|
||||
{
|
||||
[EnumMember(Value = "game_object")] GameObject,
|
||||
[EnumMember(Value = "skinned_mesh_renderer")] SkinnedMeshRenderer,
|
||||
}
|
||||
|
||||
public sealed class AacCrabKeyframe
|
||||
{
|
||||
public float Time { get; set; }
|
||||
public float Value { get; set; }
|
||||
public float InTangent { get; set; }
|
||||
public float OutTangent { get; set; }
|
||||
}
|
||||
|
||||
public sealed class AacCrabBlendTree
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public AacCrabBlendType BlendType { get; set; }
|
||||
public string ParamX { get; set; }
|
||||
public string ParamY { get; set; }
|
||||
public List<AacCrabBlendChild> Children { get; set; }
|
||||
public bool UseAutomaticThresholds { get; set; }
|
||||
}
|
||||
|
||||
public enum AacCrabBlendType
|
||||
{
|
||||
[EnumMember(Value = "simple_1d")] Simple1D,
|
||||
[EnumMember(Value = "simple_directional_2d")] SimpleDirectional2D,
|
||||
[EnumMember(Value = "freeform_directional_2d")] FreeformDirectional2D,
|
||||
[EnumMember(Value = "freeform_cartesian_2d")] FreeformCartesian2D,
|
||||
[EnumMember(Value = "direct")] Direct,
|
||||
}
|
||||
|
||||
public sealed class AacCrabBlendChild
|
||||
{
|
||||
public AacCrabMotionRef Motion { get; set; }
|
||||
public float? Threshold { get; set; }
|
||||
public float? ThresholdY { get; set; }
|
||||
public string DirectParam { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>A motion is always a reference: clips and blend trees are declared at the top level.</summary>
|
||||
public sealed class AacCrabMotionRef
|
||||
{
|
||||
public AacCrabMotionType Type { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public enum AacCrabMotionType
|
||||
{
|
||||
[EnumMember(Value = "clip")] Clip,
|
||||
[EnumMember(Value = "blend_tree")] BlendTree,
|
||||
}
|
||||
|
||||
public sealed class AacCrabController
|
||||
{
|
||||
public List<AacCrabLayer> Layers { get; set; }
|
||||
}
|
||||
|
||||
public sealed class AacCrabLayer
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public AacCrabStateMachine StateMachine { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Name is null for the root state machine of a layer.</summary>
|
||||
public sealed class AacCrabStateMachine
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public AacCrabGridPos Position { get; set; }
|
||||
public List<AacCrabState> States { get; set; }
|
||||
public List<AacCrabStateMachine> SubMachines { get; set; }
|
||||
public List<AacCrabTransition> AnyStateTransitions { get; set; }
|
||||
}
|
||||
|
||||
public sealed class AacCrabState
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public AacCrabGridPos Position { get; set; }
|
||||
public AacCrabMotionRef Motion { get; set; }
|
||||
public List<AacCrabTransition> Transitions { get; set; }
|
||||
}
|
||||
|
||||
public sealed class AacCrabTransition
|
||||
{
|
||||
/// <summary>The name of the destination state, within the same layer.</summary>
|
||||
public string To { get; set; }
|
||||
public List<AacCrabCondition> Conditions { get; set; }
|
||||
public bool HasExitTime { get; set; }
|
||||
public float ExitTime { get; set; }
|
||||
public float Duration { get; set; }
|
||||
public bool OrderedInterruption { get; set; }
|
||||
public bool SourceInterruption { get; set; }
|
||||
public bool CanTransitionToSelf { get; set; }
|
||||
}
|
||||
|
||||
public sealed class AacCrabCondition
|
||||
{
|
||||
public string Parameter { get; set; }
|
||||
public AacCrabCondMode Mode { get; set; }
|
||||
public float Threshold { get; set; }
|
||||
}
|
||||
|
||||
public enum AacCrabCondMode
|
||||
{
|
||||
[EnumMember(Value = "greater")] Greater,
|
||||
[EnumMember(Value = "less")] Less,
|
||||
[EnumMember(Value = "equals")] Equals,
|
||||
[EnumMember(Value = "not_equal")] NotEqual,
|
||||
[EnumMember(Value = "if")] If,
|
||||
[EnumMember(Value = "if_not")] IfNot,
|
||||
}
|
||||
|
||||
public struct AacCrabGridPos
|
||||
{
|
||||
public int X { get; set; }
|
||||
public int Y { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace AnimatorAsCrab.V1
|
||||
{
|
||||
/// <summary>
|
||||
/// P/Invoke bindings for `libaac` (see rust/src/lib.rs). The native library is expected to live
|
||||
/// in the project's Assets/Plugins folder; place libaac.so, libaac.dll or libaac.dylib there.
|
||||
/// </summary>
|
||||
public static class AacCrabNative
|
||||
{
|
||||
private const string Library = "libaac";
|
||||
|
||||
[DllImport(Library, CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern IntPtr aac_create();
|
||||
|
||||
[DllImport(Library, CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern int aac_eval_rhai(IntPtr context, byte[] script);
|
||||
|
||||
[DllImport(Library, CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern IntPtr aac_to_json(IntPtr context);
|
||||
|
||||
[DllImport(Library, CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern IntPtr aac_last_error(IntPtr context);
|
||||
|
||||
[DllImport(Library, CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern void aac_free_string(IntPtr value);
|
||||
|
||||
[DllImport(Library, CallingConvention = CallingConvention.Cdecl)]
|
||||
private static extern void aac_destroy(IntPtr context);
|
||||
|
||||
/// <summary>
|
||||
/// Evaluate a Rhai script and return the graph as JSON, or null with a message in
|
||||
/// <paramref name="error"/>. The context lives only for the duration of this call.
|
||||
/// </summary>
|
||||
public static string Evaluate(string script, out string error)
|
||||
{
|
||||
IntPtr context;
|
||||
try
|
||||
{
|
||||
context = aac_create();
|
||||
}
|
||||
catch (DllNotFoundException exception)
|
||||
{
|
||||
error = $"Could not load {Library}: {exception.Message}. Place the native library in Assets/Plugins.";
|
||||
return null;
|
||||
}
|
||||
|
||||
if (context == IntPtr.Zero)
|
||||
{
|
||||
error = "aac_create returned null.";
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var status = aac_eval_rhai(context, Utf8Z(script));
|
||||
if (status != 0)
|
||||
{
|
||||
error = ReadUtf8(aac_last_error(context)) ?? $"The script failed with status {status}.";
|
||||
return null;
|
||||
}
|
||||
|
||||
var json = aac_to_json(context);
|
||||
if (json == IntPtr.Zero)
|
||||
{
|
||||
error = ReadUtf8(aac_last_error(context)) ?? "The graph could not be serialized.";
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
error = null;
|
||||
return ReadUtf8(json);
|
||||
}
|
||||
finally
|
||||
{
|
||||
aac_free_string(json);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
aac_destroy(context);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] Utf8Z(string value)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(value);
|
||||
var terminated = new byte[bytes.Length + 1];
|
||||
Array.Copy(bytes, terminated, bytes.Length);
|
||||
return terminated;
|
||||
}
|
||||
|
||||
private static string ReadUtf8(IntPtr pointer)
|
||||
{
|
||||
if (pointer == IntPtr.Zero)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Marshal.PtrToStringUTF8 is not available on all Unity runtimes, so the bytes are copied by hand.
|
||||
var length = 0;
|
||||
while (Marshal.ReadByte(pointer, length) != 0)
|
||||
{
|
||||
length++;
|
||||
}
|
||||
|
||||
var bytes = new byte[length];
|
||||
Marshal.Copy(pointer, bytes, 0, length);
|
||||
return Encoding.UTF8.GetString(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AnimatorAsCrab.V1
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluates a Rhai script with libaac and generates the Animator Controller through Animator As Code.
|
||||
/// The Unity-side configuration is never guessed: every field below is supplied by the user, and the
|
||||
/// system name and asset key are declared by the script itself.
|
||||
/// </summary>
|
||||
public class AacCrabWindow : EditorWindow
|
||||
{
|
||||
[SerializeField] private string _scriptPath = "";
|
||||
[SerializeField] private bool _generateOnScriptChange = true;
|
||||
[SerializeField] private AnimatorController _controller;
|
||||
[SerializeField] private Transform _animatorRoot;
|
||||
[SerializeField] private UnityEngine.Object _assetContainer;
|
||||
[SerializeField] private AacConfiguration.Container _containerMode = AacConfiguration.Container.Everything;
|
||||
[SerializeField] private bool _writeDefaults;
|
||||
[SerializeField] private long _lastWriteUtcTicks;
|
||||
|
||||
private string _error;
|
||||
private string _status;
|
||||
private bool _scriptChanged;
|
||||
|
||||
[MenuItem("Tools/Animator As Crab")]
|
||||
public static void Open()
|
||||
{
|
||||
var window = GetWindow<AacCrabWindow>();
|
||||
window.titleContent = new GUIContent("Animator As Crab");
|
||||
window.Show();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
EditorApplication.update += OnUpdate;
|
||||
_scriptChanged = _lastWriteUtcTicks != 0 && _lastWriteUtcTicks != LastWriteUtcTicks();
|
||||
_lastWriteUtcTicks = LastWriteUtcTicks();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
EditorApplication.update -= OnUpdate;
|
||||
}
|
||||
|
||||
private void OnUpdate()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_scriptPath) || !File.Exists(_scriptPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var ticks = LastWriteUtcTicks();
|
||||
if (ticks == _lastWriteUtcTicks)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastWriteUtcTicks = ticks;
|
||||
_scriptChanged = true;
|
||||
if (_generateOnScriptChange)
|
||||
{
|
||||
Generate();
|
||||
}
|
||||
else
|
||||
{
|
||||
Repaint();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
EditorGUILayout.LabelField("Rhai script", EditorStyles.boldLabel);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
_scriptPath = EditorGUILayout.TextField(_scriptPath);
|
||||
if (GUILayout.Button("...", GUILayout.Width(28)))
|
||||
{
|
||||
var directory = string.IsNullOrEmpty(_scriptPath) ? null : Path.GetDirectoryName(_scriptPath);
|
||||
var picked = EditorUtility.OpenFilePanel("Rhai script", directory ?? string.Empty, "rhai");
|
||||
if (!string.IsNullOrEmpty(picked))
|
||||
{
|
||||
_scriptPath = picked;
|
||||
_lastWriteUtcTicks = LastWriteUtcTicks();
|
||||
_scriptChanged = false;
|
||||
Generate();
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
_generateOnScriptChange = EditorGUILayout.Toggle("Generate when the script changes", _generateOnScriptChange);
|
||||
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.LabelField("Unity side", EditorStyles.boldLabel);
|
||||
_controller = (AnimatorController)EditorGUILayout.ObjectField("Animator Controller", _controller, typeof(AnimatorController), false);
|
||||
_animatorRoot = (Transform)EditorGUILayout.ObjectField("Animator Root", _animatorRoot, typeof(Transform), true);
|
||||
_assetContainer = EditorGUILayout.ObjectField("Asset Container", _assetContainer, typeof(UnityEngine.Object), false);
|
||||
_containerMode = (AacConfiguration.Container)EditorGUILayout.EnumPopup("Container Mode", _containerMode);
|
||||
_writeDefaults = EditorGUILayout.Toggle("Write Defaults", _writeDefaults);
|
||||
|
||||
EditorGUILayout.Space();
|
||||
if (GUILayout.Button("Generate", GUILayout.Height(24)))
|
||||
{
|
||||
Generate();
|
||||
}
|
||||
|
||||
EditorGUILayout.Space();
|
||||
if (_error != null)
|
||||
{
|
||||
EditorGUILayout.HelpBox(_error, MessageType.Error);
|
||||
}
|
||||
else if (_scriptChanged)
|
||||
{
|
||||
EditorGUILayout.HelpBox("The script changed since the last generation.", MessageType.Warning);
|
||||
}
|
||||
|
||||
if (_status != null)
|
||||
{
|
||||
EditorGUILayout.HelpBox(_status, MessageType.Info);
|
||||
}
|
||||
}
|
||||
|
||||
private void Generate()
|
||||
{
|
||||
_error = null;
|
||||
_status = null;
|
||||
_scriptChanged = false;
|
||||
try
|
||||
{
|
||||
_status = GenerateOrThrow();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_error = exception.Message;
|
||||
Debug.LogException(exception);
|
||||
}
|
||||
|
||||
Repaint();
|
||||
}
|
||||
|
||||
private string GenerateOrThrow()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_scriptPath))
|
||||
{
|
||||
throw new InvalidOperationException("Select a Rhai script.");
|
||||
}
|
||||
|
||||
if (!File.Exists(_scriptPath))
|
||||
{
|
||||
throw new InvalidOperationException($"'{_scriptPath}' does not exist.");
|
||||
}
|
||||
|
||||
if (_controller == null)
|
||||
{
|
||||
throw new InvalidOperationException("Select the Animator Controller to generate into.");
|
||||
}
|
||||
|
||||
if (_animatorRoot == null)
|
||||
{
|
||||
throw new InvalidOperationException("Select the animator root Transform.");
|
||||
}
|
||||
|
||||
if (_assetContainer == null)
|
||||
{
|
||||
throw new InvalidOperationException("Select the asset container that will hold the generated clips and blend trees.");
|
||||
}
|
||||
|
||||
var json = AacCrabNative.Evaluate(File.ReadAllText(_scriptPath), out var nativeError);
|
||||
if (json == null)
|
||||
{
|
||||
throw new InvalidOperationException(nativeError);
|
||||
}
|
||||
|
||||
var graph = AacCrabJson.Parse(json);
|
||||
var configuration = new AacConfiguration
|
||||
{
|
||||
SystemName = graph.SystemName,
|
||||
AssetKey = graph.AssetKey,
|
||||
AnimatorRoot = _animatorRoot,
|
||||
AssetContainer = _assetContainer,
|
||||
ContainerMode = _containerMode,
|
||||
DefaultsProvider = new AacDefaultsProvider(_writeDefaults),
|
||||
};
|
||||
|
||||
AacCrabGenerator.Generate(graph, configuration, _controller);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return $"Generated '{graph.SystemName}' with asset key '{graph.AssetKey}': " +
|
||||
$"{graph.Controller.Layers.Count} layer(s), {graph.Clips.Count} clip(s), {graph.BlendTrees.Count} blend tree(s).";
|
||||
}
|
||||
|
||||
private long LastWriteUtcTicks()
|
||||
{
|
||||
return string.IsNullOrEmpty(_scriptPath) || !File.Exists(_scriptPath)
|
||||
? 0
|
||||
: File.GetLastWriteTimeUtc(_scriptPath).Ticks;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user