diff --git a/CHANGELOG.md b/CHANGELOG.md
index b8de362c4..6ec808969 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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**
diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonGraphicInspector.cs b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonGraphicInspector.cs
index 961dd1c28..a14c6a711 100644
--- a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonGraphicInspector.cs
+++ b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonGraphicInspector.cs
@@ -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
diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRendererInspector.cs b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRendererInspector.cs
index 118d9001e..1f285cebf 100644
--- a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRendererInspector.cs
+++ b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRendererInspector.cs
@@ -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);
diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRootMotionBaseInspector.cs b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRootMotionBaseInspector.cs
index b61591c98..e5f808a06 100644
--- a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRootMotionBaseInspector.cs
+++ b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRootMotionBaseInspector.cs
@@ -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);
+ }
+ }
}
}
}
diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonRootMotionBase.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonRootMotionBase.cs
index 16107d81a..8e04264c3 100644
--- a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonRootMotionBase.cs
+++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/RootMotion/SkeletonRootMotionBase.cs
@@ -61,6 +61,24 @@ namespace Spine.Unity {
public bool UsesRigidbody {
get { return rigidBody != null || rigidBody2D != null; }
}
+
+ /// Root motion translation that has been applied in the preceding FixedUpdate call
+ /// if a rigidbody is assigned at either rigidbody or rigidbody2D.
+ /// Returns Vector2.zero when rigidbody and rigidbody2D are null.
+ /// This can be necessary when multiple scripts call Rigidbody2D.MovePosition,
+ /// where the last call overwrites the effect of preceding ones.
+ public Vector2 PreviousRigidbodyRootMotion {
+ get { return previousRigidbodyRootMotion; }
+ }
+
+ /// Additional translation to add to Rigidbody2D.MovePosition
+ /// called in FixedUpdate. This can be necessary when multiple scripts call
+ /// MovePosition, where the last call overwrites the effect of preceding ones.
+ /// Has no effect if rigidBody2D is null.
+ 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();
+ return skeletonComponent;
+ }
+ }
+
+ public ISkeletonAnimation TargetSkeletonAnimationComponent {
+ get { return TargetSkeletonComponent as ISkeletonAnimation; }
+ }
+
public void SetRootMotionBone (string name) {
var skeleton = skeletonComponent.Skeleton;
Bone bone = skeleton.FindBone(name);
diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonAnimation.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonAnimation.cs
index bc349b04a..77ff54961 100644
--- a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonAnimation.cs
+++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonAnimation.cs
@@ -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.
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);
}
diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonGraphic.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonGraphic.cs
index c88827a8e..8ad1a7a71 100644
--- a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonGraphic.cs
+++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonGraphic.cs
@@ -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; } }
+
/// Occurs after the vertex data populated every frame, before the vertices are pushed into the mesh.
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);
}
diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs
index 3deb15568..cd56118ff 100644
--- a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs
+++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs
@@ -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.
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(ref layerBlendModes, animator.layerCount);
}
diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/ISkeletonAnimation.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/ISkeletonAnimation.cs
index 7f21b546d..a3d08b3ae 100644
--- a/spine-unity/Assets/Spine/Runtime/spine-unity/ISkeletonAnimation.cs
+++ b/spine-unity/Assets/Spine/Runtime/spine-unity/ISkeletonAnimation.cs
@@ -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; }
}
/// Holds a reference to a SkeletonDataAsset.