2025-05-09 15:40:34 +08:00

281 lines
12 KiB
C#

// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value.
using System;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_UGUI
using UnityEngine.UI;
#endif
namespace Animancer.Samples.FineControl
{
/// <summary>Tracks user progress through a tutorial and displays appropriate instructions.</summary>
/// <remarks>
/// <strong>Sample:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/samples/fine-control/live-inspector">
/// Live Inspector</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Samples.FineControl/LiveInspectorTutorial
[AddComponentMenu(Strings.SamplesMenuPrefix + "Fine Control - Live Inspector Tutorial")]
[AnimancerHelpUrl(typeof(LiveInspectorTutorial))]
public class LiveInspectorTutorial : MonoBehaviour
{
/************************************************************************************************************************/
#if UNITY_UGUI
/************************************************************************************************************************/
[SerializeField] private AnimancerComponent _Animancer;
[SerializeField] private Text _Text;
/************************************************************************************************************************/
#if !UNITY_EDITOR
/************************************************************************************************************************/
protected virtual void Awake()
{
_Text.text = "This Sample only works in the Unity Editor";
}
/************************************************************************************************************************/
#else
/************************************************************************************************************************/
private class TutorialStep
{
/************************************************************************************************************************/
/// <summary>Instructions to display while waiting for the user to complete this step.</summary>
public readonly string Instructions;
/// <summary>Has the user completed this step?</summary>
public readonly Func<bool> IsComplete;
/************************************************************************************************************************/
public TutorialStep(string instructions, Func<bool> isComplete)
{
Instructions = instructions;
IsComplete = isComplete;
}
/************************************************************************************************************************/
}
/************************************************************************************************************************/
private readonly List<TutorialStep> Steps = new();
private int _CurrentStep;
private bool _WasShowingAddAnimationField;
private const string
AutoNormalizeWeightsFunction = "Display Options/Auto Normalize Weights",
ShowAddAnimationFieldFunction = "Display Options/Show 'Add Animation' Field",
ShowAddAnimationFieldPref = "Animancer/Inspector/" + ShowAddAnimationFieldFunction,
IdleName = "Humanoid-Idle",
WalkName = "Humanoid-WalkForwards",
DragAndDropAnimations =
"\n\nYou can also Drag and Drop animations from the Project window into the preview area to add them.";
/************************************************************************************************************************/
protected virtual void Awake()
{
_WasShowingAddAnimationField = UnityEditor.EditorPrefs.GetBool(ShowAddAnimationFieldPref);
// Select AnimancerComponent.
Steps.Add(new(
$"Select the {_Animancer.name} object in the Hierarchy",
() => UnityEditor.Selection.activeGameObject == _Animancer.gameObject));
// Initialize graph.
Steps.Add(new(
$"Right Click on the header of the {nameof(AnimancerComponent)} in the Inspector" +
$" and select the 'Initialize Graph' function from the context menu.",
() => _Animancer.IsGraphInitialized));
// Show 'Add Animation' field.
Steps.Add(new(
$"Note how the character is now in a hunched over pose with their legs under the ground." +
$" That's the default pose for a Humanoid Rig when it has no animations so let's add some." +
$"\n" +
$"\nMake sure the preview area down the bottom of the Inspector is expanded." +
$"\n" +
$"\nRight Click on the word 'States' in the preview area" +
$" and select the '{ShowAddAnimationFieldFunction}' function from the context menu.",
() => UnityEditor.EditorPrefs.GetBool(ShowAddAnimationFieldPref)));
// Add idle animation.
Steps.Add(new(
$"Use the 'Add Animation' field at the top of the preview area" +
$" to select the '{IdleName}' animation." +
DragAndDropAnimations,
() => IsCurrentState(IdleName)));
// Add walk animation.
Steps.Add(new(
$"Use the 'Add Animation' field at the top of the preview area" +
$" to select the '{WalkName}' animation." +
DragAndDropAnimations,
() => IsCurrentState(WalkName)));
// Play idle animation.
Steps.Add(new(
$"Ctrl + Left Click on the '{IdleName}' animation to play it.",
() => IsCurrentState(IdleName)));
// Play walk animation as well.
Steps.Add(new(
$"With the '{IdleName}' animation still playing," +
$" Left Click on the '{WalkName}' animation to expand its details." +
$"\n" +
$"\nClick the Play button in that state's details.",
() => IsCurrentState(IdleName)
&& TryGetState(WalkName, out AnimancerState walk)
&& walk.Weight == 0
&& walk.IsPlaying));
// Set weight.
Steps.Add(new(
$"Note how the preview now shows both animations playing," +
$" but you can still only see the '{IdleName}' pose in the Game window." +
$" That's because interacting directly with the details of a state only affects those specific details" +
$" and the Weight of '{WalkName}' is still 0." +
$"\n" +
$"\nSet the Weight of '{WalkName}' to 0.4.",
() => IsCurrentState(IdleName)
&& TryGetState(IdleName, out AnimancerState idle)
&& TryGetState(WalkName, out AnimancerState walk)
&& idle.Weight == 0.6f
&& walk.Weight == 0.4f));
// Pause graph.
Steps.Add(new(
$"Now we have a blend of both animations playing at the same time." +
$" This sort of thing is normally achieved using Mixers." +
$"\n" +
$"\nNote how setting the Weight of '{WalkName}' to 0.4" +
$" automatically changed the Weight of '{IdleName}' to 0.6 so that they add up to a total of 1." +
$" If you had set its Weight in code it would only affect that state" +
$" so everything can have whatever values you want," +
$" but that's usually inconvenient when fiddling with them in the Inspector." +
$" This feature can be disabled via '{AutoNormalizeWeightsFunction}' if you don't want it." +
$"\n" +
$"\nClick the Pause button at the top of the preview area to pause the character.",
() => !_Animancer.Graph.IsGraphPlaying));
// Unpause graph.
Steps.Add(new(
$"The character is now paused, but nothing else is." +
$" You can still Right Click in the Game window and drag to move the camera around." +
$"\n" +
$"\nClick the Play button at the top of the preview area to resume playing.",
() => _Animancer.Graph.IsGraphPlaying));
// Set speed.
Steps.Add(new(
$"Click the '1.0x' button at the top of the preview area next to the Play/Pause button" +
$" to show the 'Speed' slider." +
$"\n" +
$"\nUse that slider to set the speed to 0.5.",
() => _Animancer.Graph.Speed == 0.5f));
ApplyCurrentStep();
}
/************************************************************************************************************************/
protected virtual void Update()
{
if (_CurrentStep >= Steps.Count)
return;
if (!Steps[_CurrentStep].IsComplete())
return;
_CurrentStep++;
ApplyCurrentStep();
}
/************************************************************************************************************************/
protected virtual void OnDestroy()
{
if (!_WasShowingAddAnimationField &&
UnityEditor.EditorPrefs.GetBool(ShowAddAnimationFieldPref))
{
UnityEditor.EditorPrefs.SetBool(ShowAddAnimationFieldPref, false);
Debug.Log(
ShowAddAnimationFieldFunction +
" has been disabled as it was before starting this tutorial." +
" Normally it would remember the value you set.");
}
}
/************************************************************************************************************************/
private void ApplyCurrentStep()
{
if (_CurrentStep < Steps.Count)
{
_Text.text = $"{_CurrentStep}/{Steps.Count} {Steps[_CurrentStep].Instructions}";
}
else
{
_Text.text = $"{Steps.Count}/{Steps.Count} Congratulations for completing this tutorial.";
enabled = false;
}
}
/************************************************************************************************************************/
private bool IsCurrentState(string clipName)
{
AnimancerState state = _Animancer.States.Current;
if (state == null)
return false;
AnimationClip clip = state.Clip;
if (clip == null)
return false;
return clip.name == clipName;
}
/************************************************************************************************************************/
private bool TryGetState(string clipName, out AnimancerState state)
{
foreach (AnimancerState otherState in _Animancer.States)
{
if (otherState.Clip.name == clipName)
{
state = otherState;
return true;
}
}
state = null;
return false;
}
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
#else
/************************************************************************************************************************/
protected virtual void Awake()
{
SampleReadMe.LogMissingUnityUIModuleError(this);
}
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
}
}