[unity] Added skeleton component Inspector parameter Advanced - Animation Update with modes In Update, In FixedUpdate and Manual Update. Added SkeletonRootMotion properties PreviousRigidbodyRootMotion and AdditionalRigidbody2DMovement.

This commit is contained in:
Harald Csaszar 2022-02-09 12:19:44 +01:00
parent 76068cc4f4
commit 3289550eb0
9 changed files with 110 additions and 20 deletions

View File

@ -52,6 +52,8 @@
* `BoneFollower` and `BoneFollowerGraphic` now provide an additional `Follow Parent World Scale` parameter to allow following simple scale of parent bones (rotated/skewed scale can't be supported).
* `SpineAtlasAsset.CreateRuntimeInstance` methods now provide an optional `newCustomTextureLoader` parameter (defaults to `null`) which can be set to e.g. `(a) => new YourCustomTextureLoader(a)` to use your own `TextureLoader` subclass instead of `MaterialsTextureLoader`.
* Improved `Advanced - Fix Prefab Override MeshFilter` property for `SkeletonRenderer` (and subclasses`SkeletonAnimation` and `SkeletonMecanim`), now providing an additional option to use a global value which can be set in `Edit - Preferences - Spine`.
* `SkeletonAnimation`, `SkeletonMecanim` and `SkeletonGraphic` now provide an Inspector parameter `Advanced` - `Animation Update` with modes `In Update` **(previous behaviour, the default)**, `In FixedUpdate` and `Manual Update`. This allows to update animation in `FixedUpdate` when using the `SkeletonRootMotion` component (which is the recommended combination now, issuing a warning otherwise). The reason is that when root motion leads to a collision with a physics collider, it can introduce jittery excess movement when updating animation in `Update` due to more `Update` calls following a single `FixedUpdate` call.
* Added `SkeletonRootMotion` properties `PreviousRigidbodyRootMotion` and `AdditionalRigidbody2DMovement`. Setting or querying these movement vectors can be necessary when multiple scripts call `Rigidbody2D.MovePosition` on the same object where the last call overwrites the effect of preceding ones.
* **Changes of default values**

View File

@ -43,7 +43,7 @@ namespace Spine.Unity.Editor {
const string SeparatorSlotNamesFieldName = "separatorSlotNames";
const string ReloadButtonString = "Reload";
protected GUIContent SkeletonDataAssetLabel;
protected GUIContent SkeletonDataAssetLabel, UpdateTimingLabel;
static GUILayoutOption reloadButtonWidth;
static GUILayoutOption ReloadButtonWidth { get { return reloadButtonWidth = reloadButtonWidth ?? GUILayout.Width(GUI.skin.label.CalcSize(new GUIContent(ReloadButtonString)).x + 20); } }
static GUIStyle ReloadButtonStyle { get { return EditorStyles.miniButton; } }
@ -51,12 +51,15 @@ namespace Spine.Unity.Editor {
SerializedProperty material, color;
SerializedProperty additiveMaterial, multiplyMaterial, screenMaterial;
SerializedProperty skeletonDataAsset, initialSkinName;
SerializedProperty startingAnimation, startingLoop, timeScale, freeze, updateWhenInvisible, unscaledTime, tintBlack;
SerializedProperty startingAnimation, startingLoop, timeScale, freeze,
updateTiming, updateWhenInvisible, unscaledTime, tintBlack;
SerializedProperty initialFlipX, initialFlipY;
SerializedProperty meshGeneratorSettings;
SerializedProperty allowMultipleCanvasRenderers, separatorSlotNames, enableSeparatorSlots, updateSeparatorPartLocation;
SerializedProperty raycastTarget;
SkeletonGraphic thisSkeletonGraphic;
protected bool isInspectingPrefab;
protected bool slotsReapplyRequired = false;
@ -88,6 +91,7 @@ namespace Spine.Unity.Editor {
// Labels
SkeletonDataAssetLabel = new GUIContent("SkeletonData Asset", Icons.spine);
UpdateTimingLabel = new GUIContent("Animation Update", "Whether to update the animation in normal Update (the default), physics step FixedUpdate, or manually via a user call.");
var so = this.serializedObject;
thisSkeletonGraphic = target as SkeletonGraphic;
@ -114,6 +118,7 @@ namespace Spine.Unity.Editor {
timeScale = so.FindProperty("timeScale");
unscaledTime = so.FindProperty("unscaledTime");
freeze = so.FindProperty("freeze");
updateTiming = so.FindProperty("updateTiming");
updateWhenInvisible = so.FindProperty("updateWhenInvisible");
meshGeneratorSettings = so.FindProperty("meshGenerator").FindPropertyRelative("settings");
@ -233,6 +238,7 @@ namespace Spine.Unity.Editor {
}
}
EditorGUILayout.PropertyField(updateTiming, UpdateTimingLabel);
EditorGUILayout.PropertyField(updateWhenInvisible);
// warning box

View File

@ -62,8 +62,8 @@ namespace Spine.Unity.Editor {
protected SerializedProperty skeletonDataAsset, initialSkinName;
protected SerializedProperty initialFlipX, initialFlipY;
protected SerializedProperty updateWhenInvisible, singleSubmesh, separatorSlotNames, clearStateOnDisable,
immutableTriangles, fixDrawOrder, fixPrefabOverrideViaMeshFilter;
protected SerializedProperty updateTiming, updateWhenInvisible, singleSubmesh, separatorSlotNames,
clearStateOnDisable, immutableTriangles, fixDrawOrder, fixPrefabOverrideViaMeshFilter;
protected SerializedProperty normals, tangents, zSpacing, pmaVertexColors, tintBlack; // MeshGenerator settings
protected SerializedProperty maskInteraction;
protected SerializedProperty maskMaterialsNone, maskMaterialsInside, maskMaterialsOutside;
@ -81,7 +81,7 @@ namespace Spine.Unity.Editor {
protected GUIContent SkeletonDataAssetLabel, SkeletonUtilityButtonContent;
protected GUIContent PMAVertexColorsLabel, ClearStateOnDisableLabel, ZSpacingLabel, ImmubleTrianglesLabel,
TintBlackLabel, UpdateWhenInvisibleLabel, SingleSubmeshLabel, FixDrawOrderLabel, FixPrefabOverrideViaMeshFilterLabel;
TintBlackLabel, UpdateTimingLabel, UpdateWhenInvisibleLabel, SingleSubmeshLabel, FixDrawOrderLabel, FixPrefabOverrideViaMeshFilterLabel;
protected GUIContent NormalsLabel, TangentsLabel, MaskInteractionLabel;
protected GUIContent MaskMaterialsHeadingLabel, MaskMaterialsNoneLabel, MaskMaterialsInsideLabel, MaskMaterialsOutsideLabel;
protected GUIContent SetMaterialButtonLabel, ClearMaterialButtonLabel, DeleteMaterialButtonLabel;
@ -127,6 +127,7 @@ namespace Spine.Unity.Editor {
TangentsLabel = new GUIContent("Solve Tangents", "Calculates the tangents per frame. Use this if you are using lit shaders (usually with normal maps) that require vertex tangents.");
TintBlackLabel = new GUIContent("Tint Black (!)", "Adds black tint vertex data to the mesh as UV2 and UV3. Black tinting requires that the shader interpret UV2 and UV3 as black tint colors for this effect to work. You may also use the default [Spine/Skeleton Tint Black] shader.\n\nIf you only need to tint the whole skeleton and not individual parts, the [Spine/Skeleton Tint] shader is recommended for better efficiency and changing/animating the _Black material property via MaterialPropertyBlock.");
SingleSubmeshLabel = new GUIContent("Use Single Submesh", "Simplifies submesh generation by assuming you are only using one Material and need only one submesh. This is will disable multiple materials, render separation, and custom slot materials.");
UpdateTimingLabel = new GUIContent("Animation Update", "Whether to update the animation in normal Update (the default), physics step FixedUpdate, or manually via a user call.");
UpdateWhenInvisibleLabel = new GUIContent("Update When Invisible", "Update mode used when the MeshRenderer becomes invisible. Update mode is automatically reset to UpdateMode.FullUpdate when the mesh becomes visible again.");
FixDrawOrderLabel = new GUIContent("Fix Draw Order", "Applies only when 3+ submeshes are used (2+ materials with alternating order, e.g. \"A B A\"). If true, GPU instancing will be disabled at all materials and MaterialPropertyBlocks are assigned at each material to prevent aggressive batching of submeshes by e.g. the LWRP renderer, leading to incorrect draw order (e.g. \"A1 B A2\" changed to \"A1A2 B\"). You can disable this parameter when everything is drawn correctly to save the additional performance cost. Note: the GPU instancing setting will remain disabled at affected material assets after exiting play mode, you have to enable it manually if you accidentally enabled this parameter.");
FixPrefabOverrideViaMeshFilterLabel = new GUIContent("Fix Prefab Overr. MeshFilter", "Fixes the prefab always being marked as changed (sets the MeshFilter's hide flags to DontSaveInEditor), but at the cost of references to the MeshFilter by other components being lost. For global settings see Edit - Preferences - Spine.");
@ -150,6 +151,7 @@ namespace Spine.Unity.Editor {
pmaVertexColors = so.FindProperty("pmaVertexColors");
clearStateOnDisable = so.FindProperty("clearStateOnDisable");
tintBlack = so.FindProperty("tintBlack");
updateTiming = so.FindProperty("updateTiming");
updateWhenInvisible = so.FindProperty("updateWhenInvisible");
singleSubmesh = so.FindProperty("singleSubmesh");
fixDrawOrder = so.FindProperty("fixDrawOrder");
@ -346,11 +348,12 @@ namespace Spine.Unity.Editor {
wasInitParameterChanged |= EditorGUI.EndChangeCheck(); // Value used in the next update.
EditorGUILayout.Space();
}
EditorGUILayout.Space();
EditorGUILayout.LabelField("Renderer Settings", EditorStyles.boldLabel);
EditorGUILayout.LabelField("Renderer and Update Settings", EditorStyles.boldLabel);
using (new SpineInspectorUtility.LabelWidthScope()) {
// Optimization options
if (updateTiming != null) EditorGUILayout.PropertyField(updateTiming, UpdateTimingLabel);
if (updateWhenInvisible != null) EditorGUILayout.PropertyField(updateWhenInvisible, UpdateWhenInvisibleLabel);
if (singleSubmesh != null) EditorGUILayout.PropertyField(singleSubmesh, SingleSubmeshLabel);

View File

@ -119,6 +119,20 @@ namespace Spine.Unity.Editor {
}
EditorGUILayout.PropertyField(rigidBody, rigidBodyLabel);
DisplayWarnings();
}
protected void DisplayWarnings () {
bool usesRigidbodyPhysics = rigidBody.objectReferenceValue != null || rigidBody2D.objectReferenceValue != null;
if (usesRigidbodyPhysics) {
var rootMotionComponent = (SkeletonRootMotionBase)serializedObject.targetObject;
var skeletonComponent = rootMotionComponent ? rootMotionComponent.TargetSkeletonAnimationComponent : null;
if (skeletonComponent != null && skeletonComponent.UpdateTiming == UpdateTiming.InUpdate) {
string warningMessage = "Skeleton component uses 'Advanced - Animation Update' mode 'In Update'.\n" +
"When using a Rigidbody, 'In FixedUpdate' is recommended instead.";
EditorGUILayout.HelpBox(warningMessage, MessageType.Warning, true);
}
}
}
}
}

View File

@ -61,6 +61,24 @@ namespace Spine.Unity {
public bool UsesRigidbody {
get { return rigidBody != null || rigidBody2D != null; }
}
/// <summary>Root motion translation that has been applied in the preceding <c>FixedUpdate</c> call
/// if a rigidbody is assigned at either <c>rigidbody</c> or <c>rigidbody2D</c>.
/// Returns <c>Vector2.zero</c> when <c>rigidbody</c> and <c>rigidbody2D</c> are null.
/// This can be necessary when multiple scripts call <c>Rigidbody2D.MovePosition</c>,
/// where the last call overwrites the effect of preceding ones.</summary>
public Vector2 PreviousRigidbodyRootMotion {
get { return previousRigidbodyRootMotion; }
}
/// <summary>Additional translation to add to <c>Rigidbody2D.MovePosition</c>
/// called in FixedUpdate. This can be necessary when multiple scripts call
/// <c>MovePosition</c>, where the last call overwrites the effect of preceding ones.
/// Has no effect if <c>rigidBody2D</c> is null.</summary>
public Vector2 AdditionalRigidbody2DMovement {
get { return additionalRigidbody2DMovement; }
set { additionalRigidbody2DMovement = value; }
}
#endregion
protected ISkeletonComponent skeletonComponent;
@ -72,6 +90,8 @@ namespace Spine.Unity {
protected Vector2 initialOffset = Vector2.zero;
protected Vector2 tempSkeletonDisplacement;
protected Vector2 rigidbodyDisplacement;
protected Vector2 previousRigidbodyRootMotion = Vector2.zero;
protected Vector2 additionalRigidbody2DMovement = Vector2.zero;
protected virtual void Reset () {
FindRigidbodyComponent();
@ -107,7 +127,7 @@ namespace Spine.Unity {
}
rigidBody2D.MovePosition(gravityAndVelocityMovement + new Vector2(transform.position.x, transform.position.y)
+ rigidbodyDisplacement);
+ rigidbodyDisplacement + additionalRigidbody2DMovement);
} else if (rigidBody != null) {
rigidBody.MovePosition(transform.position
+ new Vector3(rigidbodyDisplacement.x, rigidbodyDisplacement.y, 0));
@ -116,6 +136,7 @@ namespace Spine.Unity {
Vector2 parentBoneScale;
GetScaleAffectingRootMotion(out parentBoneScale);
ClearEffectiveBoneOffsets(parentBoneScale);
previousRigidbodyRootMotion = rigidbodyDisplacement;
rigidbodyDisplacement = Vector2.zero;
tempSkeletonDisplacement = Vector2.zero;
}
@ -150,6 +171,18 @@ namespace Spine.Unity {
};
abstract public RootMotionInfo GetRootMotionInfo (int trackIndex = 0);
public ISkeletonComponent TargetSkeletonComponent {
get {
if (skeletonComponent == null)
skeletonComponent = GetComponent<ISkeletonComponent>();
return skeletonComponent;
}
}
public ISkeletonAnimation TargetSkeletonAnimationComponent {
get { return TargetSkeletonComponent as ISkeletonAnimation; }
}
public void SetRootMotionBone (string name) {
var skeleton = skeletonComponent.Skeleton;
Bone bone = skeleton.FindBone(name);

View File

@ -90,6 +90,9 @@ namespace Spine.Unity {
/// Use this callback if you want to use bone world space values, but don't intend to modify bone local values.
/// This callback can also be used when setting world position and the bone matrix.</summary>
public event UpdateBonesDelegate UpdateComplete { add { _UpdateComplete += value; } remove { _UpdateComplete -= value; } }
[SerializeField] protected UpdateTiming updateTiming = UpdateTiming.InUpdate;
public UpdateTiming UpdateTiming { get { return updateTiming; } set { updateTiming = value; } }
#endregion
#region Serialized state and Beginner API
@ -185,14 +188,19 @@ namespace Spine.Unity {
}
}
void Update () {
virtual protected void Update () {
#if UNITY_EDITOR
if (!Application.isPlaying) {
Update(0f);
return;
}
#endif
if (updateTiming != UpdateTiming.InUpdate) return;
Update(Time.deltaTime);
}
virtual protected void FixedUpdate () {
if (updateTiming != UpdateTiming.InFixedUpdate) return;
Update(Time.deltaTime);
}

View File

@ -35,7 +35,6 @@
#define HAS_CULL_TRANSPARENT_MESH
#endif
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
@ -102,7 +101,7 @@ namespace Spine.Unity {
#if UNITY_EDITOR
protected override void OnValidate () {
// This handles Scene View preview.
base.OnValidate ();
base.OnValidate();
if (this.IsValid) {
if (skeletonDataAsset == null) {
Clear();
@ -254,8 +253,12 @@ namespace Spine.Unity {
return;
}
#endif
if (freeze || updateTiming != UpdateTiming.InUpdate) return;
Update(unscaledTime ? Time.unscaledDeltaTime : Time.deltaTime);
}
if (freeze) return;
virtual protected void FixedUpdate () {
if (freeze || updateTiming != UpdateTiming.InFixedUpdate) return;
Update(unscaledTime ? Time.unscaledDeltaTime : Time.deltaTime);
}
@ -359,8 +362,7 @@ namespace Spine.Unity {
separatorSlots.Add(slot);
}
#if UNITY_EDITOR
else
{
else {
Debug.LogWarning(slotName + " is not a slot in " + skeletonDataAsset.skeletonJSON.name);
}
#endif
@ -489,6 +491,9 @@ namespace Spine.Unity {
public event UpdateBonesDelegate UpdateWorld;
public event UpdateBonesDelegate UpdateComplete;
[SerializeField] protected UpdateTiming updateTiming = UpdateTiming.InUpdate;
public UpdateTiming UpdateTiming { get { return updateTiming; } set { updateTiming = value; } }
/// <summary> Occurs after the vertex data populated every frame, before the vertices are pushed into the mesh.</summary>
public event Spine.Unity.MeshGeneratorDelegate OnPostProcessVertices;
@ -838,7 +843,7 @@ namespace Spine.Unity {
#if UNITY_EDITOR
if (Application.isEditor && !Application.isPlaying) {
for (int i = separatorParts.Count-1; i >= 0; --i) {
for (int i = separatorParts.Count - 1; i >= 0; --i) {
if (separatorParts[i] == null) {
separatorParts.RemoveAt(i);
}

View File

@ -67,6 +67,9 @@ namespace Spine.Unity {
/// Use this callback if you want to use bone world space values, but don't intend to modify bone local values.
/// This callback can also be used when setting world position and the bone matrix.</summary>
public event UpdateBonesDelegate UpdateComplete { add { _UpdateComplete += value; } remove { _UpdateComplete -= value; } }
[SerializeField] protected UpdateTiming updateTiming = UpdateTiming.InUpdate;
public UpdateTiming UpdateTiming { get { return updateTiming; } set { updateTiming = value; } }
#endregion
public override void Initialize (bool overwrite, bool quiet = false) {
@ -83,13 +86,23 @@ namespace Spine.Unity {
wasUpdatedAfterInit = false;
}
public void Update () {
if (!valid) return;
public virtual void Update () {
if (!valid || updateTiming != UpdateTiming.InUpdate) return;
UpdateAnimation();
}
public virtual void FixedUpdate () {
if (!valid || updateTiming != UpdateTiming.InFixedUpdate) return;
UpdateAnimation();
}
protected void UpdateAnimation () {
wasUpdatedAfterInit = true;
// animation status is kept by Mecanim Animator component
if (updateMode <= UpdateMode.OnlyAnimationStatus)
return;
ApplyAnimation();
}
@ -104,8 +117,7 @@ namespace Spine.Unity {
if (Application.isPlaying) {
translator.Apply(skeleton);
}
else {
} else {
if (translatorAnimator != null && translatorAnimator.isInitialized &&
translatorAnimator.isActiveAndEnabled && translatorAnimator.runtimeAnimatorController != null) {
// Note: Rebind is required to prevent warning "Animator is not playing an AnimatorController" with prefabs
@ -535,7 +547,7 @@ namespace Spine.Unity {
}
#if UNITY_EDITOR
void GetLayerBlendModes() {
void GetLayerBlendModes () {
if (layerBlendModes.Length < animator.layerCount) {
System.Array.Resize<MixBlend>(ref layerBlendModes, animator.layerCount);
}

View File

@ -37,6 +37,12 @@ namespace Spine.Unity {
//Reserved 4 for OnlyEventTimelines
};
public enum UpdateTiming {
ManualUpdate = 0,
InUpdate,
InFixedUpdate
}
public delegate void UpdateBonesDelegate (ISkeletonAnimation animated);
public interface ISpineComponent { }
@ -53,6 +59,7 @@ namespace Spine.Unity {
event UpdateBonesDelegate UpdateWorld;
event UpdateBonesDelegate UpdateComplete;
Skeleton Skeleton { get; }
UpdateTiming UpdateTiming { get; set; }
}
/// <summary>Holds a reference to a SkeletonDataAsset.</summary>