From 54e463048a6bed88ab183ccd7dd1f540284d4885 Mon Sep 17 00:00:00 2001 From: Harald Csaszar Date: Thu, 29 Aug 2024 17:27:07 +0200 Subject: [PATCH] [unity] SpineVisualElement improvements. Now supports settings reference mesh bounds via a different bounds animation. --- .../Editor/Utility/SpineInspectorUtility.cs | 12 +- spine-unity/Assets/Spine/package.json | 2 +- .../Editor.meta | 8 ++ .../SpineVisualElementAttributeDrawers.cs | 119 +++++++++++++++++ ...SpineVisualElementAttributeDrawers.cs.meta | 2 + .../Runtime/SpineVisualElement.cs | 125 +++++++++++++++--- .../package.json | 8 +- 7 files changed, 251 insertions(+), 25 deletions(-) create mode 100644 spine-unity/Modules/com.esotericsoftware.spine.ui-toolkit/Editor.meta create mode 100644 spine-unity/Modules/com.esotericsoftware.spine.ui-toolkit/Editor/SpineVisualElementAttributeDrawers.cs create mode 100644 spine-unity/Modules/com.esotericsoftware.spine.ui-toolkit/Editor/SpineVisualElementAttributeDrawers.cs.meta diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/SpineInspectorUtility.cs b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/SpineInspectorUtility.cs index c1e4dcf3f..4290fd2fa 100644 --- a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/SpineInspectorUtility.cs +++ b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/SpineInspectorUtility.cs @@ -149,10 +149,20 @@ namespace Spine.Unity.Editor { } } } - newPropertyPath = propertyPath.Remove(propertyPath.Length - localPathLength, localPathLength) + propertyName; relativeProperty = property.serializedObject.FindProperty(newPropertyPath); } + // If this fails as well, try at any base property up the hierarchy + if (relativeProperty == null) { + int dotIndex = propertyPath.Length - property.name.Length - 1; + while (relativeProperty == null) { + dotIndex = propertyPath.LastIndexOf('.', dotIndex - 1); + if (dotIndex < 0) + break; + newPropertyPath = propertyPath.Remove(dotIndex + 1) + propertyName; + relativeProperty = property.serializedObject.FindProperty(newPropertyPath); + } + } } return relativeProperty; diff --git a/spine-unity/Assets/Spine/package.json b/spine-unity/Assets/Spine/package.json index 9a49c9912..a87d36d21 100644 --- a/spine-unity/Assets/Spine/package.json +++ b/spine-unity/Assets/Spine/package.json @@ -2,7 +2,7 @@ "name": "com.esotericsoftware.spine.spine-unity", "displayName": "spine-unity Runtime", "description": "This plugin provides the spine-unity runtime core.", - "version": "4.2.81", + "version": "4.2.82", "unity": "2018.3", "author": { "name": "Esoteric Software", diff --git a/spine-unity/Modules/com.esotericsoftware.spine.ui-toolkit/Editor.meta b/spine-unity/Modules/com.esotericsoftware.spine.ui-toolkit/Editor.meta new file mode 100644 index 000000000..6c2c3ce8a --- /dev/null +++ b/spine-unity/Modules/com.esotericsoftware.spine.ui-toolkit/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 020c4dfc4cd28f8409ea82818e31d040 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/spine-unity/Modules/com.esotericsoftware.spine.ui-toolkit/Editor/SpineVisualElementAttributeDrawers.cs b/spine-unity/Modules/com.esotericsoftware.spine.ui-toolkit/Editor/SpineVisualElementAttributeDrawers.cs new file mode 100644 index 000000000..b8c1681c5 --- /dev/null +++ b/spine-unity/Modules/com.esotericsoftware.spine.ui-toolkit/Editor/SpineVisualElementAttributeDrawers.cs @@ -0,0 +1,119 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated July 28, 2023. Replaces all prior versions. + * + * Copyright (c) 2013-2024, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software or + * otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE + * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +//#define CHANGE_BOUNDS_ON_ANIMATION_CHANGE + +using UnityEditor; +using UnityEditor.UIElements; +using UnityEngine; +using UnityEngine.UIElements; + +namespace Spine.Unity.Editor { + + [CustomPropertyDrawer(typeof(BoundsFromAnimationAttribute))] + public class BoundsFromAnimationAttributeDrawer : PropertyDrawer { + + protected BoundsFromAnimationAttribute TargetAttribute { get { return (BoundsFromAnimationAttribute)attribute; } } + + public override VisualElement CreatePropertyGUI (SerializedProperty boundsProperty) { + var container = new VisualElement(); + PropertyField referenceMeshBounds = new PropertyField(); + referenceMeshBounds.BindProperty(boundsProperty); + + var parentPropertyPath = boundsProperty.propertyPath.Substring(0, boundsProperty.propertyPath.LastIndexOf('.')); + var parent = boundsProperty.serializedObject.FindProperty(parentPropertyPath); + SerializedProperty animationProperty = parent.FindPropertyRelative(TargetAttribute.animationField); + SerializedProperty skeletonDataProperty = parent.FindPropertyRelative(TargetAttribute.dataField); + SerializedProperty skinProperty = parent.FindPropertyRelative(TargetAttribute.skinField); + +#if !CHANGE_BOUNDS_ON_ANIMATION_CHANGE + Button updateBoundsButton = new Button(() => { + UpdateMeshBounds(boundsProperty, animationProperty.stringValue, + (SkeletonDataAsset)skeletonDataProperty.objectReferenceValue, skinProperty.stringValue); + }); + updateBoundsButton.text = "Update Bounds"; + container.Add(updateBoundsButton); +#else + referenceMeshBounds.TrackPropertyValue(animationProperty, prop => { + UpdateMeshBounds(boundsProperty, animationProperty.stringValue, + (SkeletonDataAsset)skeletonDataProperty.objectReferenceValue, skinProperty.stringValue); + }); +#endif + container.Add(referenceMeshBounds); + + container.Bind(boundsProperty.serializedObject); + return container; + } + + protected void UpdateMeshBounds (SerializedProperty boundsProperty, string boundsAnimation, + SkeletonDataAsset skeletonDataAsset, string skin) { + if (!skeletonDataAsset) + return; + + Bounds bounds = CalculateMeshBounds(boundsAnimation, skeletonDataAsset, skin); + if (bounds.extents.x == 0 || bounds.extents.y == 0) { + Debug.LogWarning("Please select different Initial Skin and Bounds Animation. Not setting reference " + + "bounds as current combination (likely no attachments visible) leads to zero Mesh bounds."); + bounds.center = Vector3.zero; + bounds.extents = Vector3.one * 2f; + } + boundsProperty.boundsValue = bounds; + boundsProperty.serializedObject.ApplyModifiedProperties(); + } + + protected Bounds CalculateMeshBounds (string animationName, SkeletonDataAsset skeletonDataAsset, string skin) { + SkeletonData skeletonData = skeletonDataAsset.GetSkeletonData(false); + Skeleton skeleton = new Skeleton(skeletonData); + if (!string.IsNullOrEmpty(skin) && !string.Equals(skin, "default", System.StringComparison.Ordinal)) + skeleton.SetSkin(skin); + skeleton.SetSlotsToSetupPose(); + + Spine.Animation animation = skeletonData.FindAnimation(animationName); + if (animation != null) + animation.Apply(skeleton, -1, 0, false, null, 1.0f, MixBlend.First, MixDirection.In); + + skeleton.Update(0f); + skeleton.UpdateWorldTransform(Skeleton.Physics.Update); + + float x, y, width, height; + SkeletonClipping clipper = new SkeletonClipping(); + float[] vertexBuffer = null; + skeleton.GetBounds(out x, out y, out width, out height, ref vertexBuffer, clipper); + if (x == int.MaxValue) { + return new Bounds(); + } + + Bounds bounds = new Bounds(); + Vector2 halfSize = new Vector2(width * 0.5f, height * 0.5f); + bounds.center = new Vector3(x + halfSize.x, -y - halfSize.y, 0.0f); + bounds.extents = new Vector3(halfSize.x, halfSize.y, 0.0f); + return bounds; + } + } +} diff --git a/spine-unity/Modules/com.esotericsoftware.spine.ui-toolkit/Editor/SpineVisualElementAttributeDrawers.cs.meta b/spine-unity/Modules/com.esotericsoftware.spine.ui-toolkit/Editor/SpineVisualElementAttributeDrawers.cs.meta new file mode 100644 index 000000000..26a1f0bae --- /dev/null +++ b/spine-unity/Modules/com.esotericsoftware.spine.ui-toolkit/Editor/SpineVisualElementAttributeDrawers.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: eb5762c450311694e84304e790546805 \ No newline at end of file diff --git a/spine-unity/Modules/com.esotericsoftware.spine.ui-toolkit/Runtime/SpineVisualElement.cs b/spine-unity/Modules/com.esotericsoftware.spine.ui-toolkit/Runtime/SpineVisualElement.cs index 04116f32a..5ab96d790 100644 --- a/spine-unity/Modules/com.esotericsoftware.spine.ui-toolkit/Runtime/SpineVisualElement.cs +++ b/spine-unity/Modules/com.esotericsoftware.spine.ui-toolkit/Runtime/SpineVisualElement.cs @@ -29,17 +29,42 @@ using System; using Unity.Collections; -using Unity.Jobs; using UnityEngine; -using UnityEngine.Profiling; using UnityEngine.UIElements; using UIVertex = UnityEngine.UIElements.Vertex; namespace Spine.Unity { + public class BoundsFromAnimationAttribute : PropertyAttribute { + + public readonly string animationField; + public readonly string dataField; + public readonly string skinField; + + public BoundsFromAnimationAttribute (string animationField, string skinField, string dataField = "skeletonDataAsset") { + this.animationField = animationField; + this.skinField = skinField; + this.dataField = dataField; + } + } + [UxmlElement] public partial class SpineVisualElement : VisualElement { + [UxmlAttribute] + public SkeletonDataAsset SkeletonDataAsset { + get { return skeletonDataAsset; } + set { + if (skeletonDataAsset == value) return; + skeletonDataAsset = value; +#if UNITY_EDITOR + if (!Application.isPlaying) + Initialize(true); +#endif + } + } + public SkeletonDataAsset skeletonDataAsset; + [SpineAnimation(dataField: "SkeletonDataAsset", avoidGenericMenu: true)] [UxmlAttribute] public string StartingAnimation { @@ -55,7 +80,7 @@ namespace Spine.Unity { } public string startingAnimation = ""; - [SpineSkin(dataField: "SkeletonDataAsset", avoidGenericMenu: true)] + [SpineSkin(dataField: "SkeletonDataAsset", defaultAsEmptyString: true, avoidGenericMenu: true)] [UxmlAttribute] public string InitialSkinName { get { return initialSkinName; } @@ -73,19 +98,42 @@ namespace Spine.Unity { [UxmlAttribute] public bool startingLoop { get; set; } = true; [UxmlAttribute] public float timeScale { get; set; } = 1.0f; + [SpineAnimation(dataField: "SkeletonDataAsset", avoidGenericMenu: true)] [UxmlAttribute] - public SkeletonDataAsset SkeletonDataAsset { - get { return skeletonDataAsset; } + public string BoundsAnimation { + get { return boundsAnimation; } set { - if (skeletonDataAsset == value) return; - skeletonDataAsset = value; + boundsAnimation = value; #if UNITY_EDITOR - if (!Application.isPlaying) - Initialize(true); + if (!Application.isPlaying) { + if (!this.IsValid) + Initialize(true); + else { + UpdateAnimation(); + } + } #endif } } - public SkeletonDataAsset skeletonDataAsset; + public string boundsAnimation = ""; + + [UxmlAttribute] + [BoundsFromAnimation(animationField: "BoundsAnimation", + skinField: "InitialSkinName", dataField: "SkeletonDataAsset")] + public Bounds ReferenceBounds { + get { return referenceMeshBounds; } + set { + if (referenceMeshBounds == value) return; +#if UNITY_EDITOR + if (!Application.isPlaying && (value.size.x == 0 || value.size.y == 0)) return; +#endif + referenceMeshBounds = value; + if (!this.IsValid) return; + + AdjustOffsetScaleToMeshBounds(rendererElement); + } + } + public Bounds referenceMeshBounds; public AnimationState AnimationState { get { @@ -93,21 +141,22 @@ namespace Spine.Unity { return state; } } + [UxmlAttribute] public bool freeze { get; set; } + [UxmlAttribute] public bool unscaledTime { get; set; } /// Update mode to optionally limit updates to e.g. only apply animations but not update the mesh. public UpdateMode UpdateMode { get { return updateMode; } set { updateMode = value; } } protected UpdateMode updateMode = UpdateMode.FullUpdate; - protected AnimationState state; - protected Skeleton skeleton; + protected AnimationState state = null; + protected Skeleton skeleton = null; protected SkeletonRendererInstruction currentInstructions = new();// to match existing code better protected Spine.Unity.MeshGeneratorUIElements meshGenerator = new MeshGeneratorUIElements(); protected VisualElement rendererElement; IVisualElementScheduledItem scheduledItem; - protected Bounds referenceMeshBounds; protected float scale = 100; protected float offsetX, offsetY; @@ -131,6 +180,10 @@ namespace Spine.Unity { } void OnGeometryChanged (GeometryChangedEvent evt) { + if (!this.IsValid) return; + if (referenceMeshBounds.size.x == 0 || referenceMeshBounds.size.y == 0) { + AdjustReferenceMeshBounds(); + } AdjustOffsetScaleToMeshBounds(rendererElement); } @@ -154,7 +207,6 @@ namespace Spine.Unity { return; } #endif - if (freeze) return; Update(unscaledTime ? Time.unscaledDeltaTime : Time.deltaTime); rendererElement.MarkDirtyRepaint(); @@ -212,17 +264,44 @@ namespace Spine.Unity { if (!string.IsNullOrEmpty(initialSkinName)) skeleton.SetSkin(initialSkinName); - if (!string.IsNullOrEmpty(startingAnimation)) { - var animationObject = SkeletonDataAsset.GetSkeletonData(false).FindAnimation(startingAnimation); + string displayedAnimation = Application.isPlaying ? startingAnimation : boundsAnimation; + if (!string.IsNullOrEmpty(displayedAnimation)) { + var animationObject = skeletonData.FindAnimation(displayedAnimation); if (animationObject != null) { state.SetAnimation(0, animationObject, startingLoop); } } + if (referenceMeshBounds.size.x == 0 || referenceMeshBounds.size.y == 0) { + AdjustReferenceMeshBounds(); + AdjustOffsetScaleToMeshBounds(rendererElement); + } - AdjustReferenceMeshBounds(); if (scheduledItem == null) scheduledItem = schedule.Execute(Update).Every(1); + if (!Application.isPlaying) + Update(0.0f); + + rendererElement.MarkDirtyRepaint(); + } + + protected void UpdateAnimation () { + this.state.ClearTracks(); + skeleton.SetToSetupPose(); + + string displayedAnimation = Application.isPlaying ? startingAnimation : boundsAnimation; + if (!string.IsNullOrEmpty(displayedAnimation)) { + var animationObject = SkeletonDataAsset.GetSkeletonData(false).FindAnimation(displayedAnimation); + if (animationObject != null) { + state.SetAnimation(0, animationObject, startingLoop); + } + } + if (referenceMeshBounds.size.x == 0 || referenceMeshBounds.size.y == 0) { + AdjustReferenceMeshBounds(); + AdjustOffsetScaleToMeshBounds(rendererElement); + } + Update(0.0f); + rendererElement.MarkDirtyRepaint(); } @@ -320,6 +399,8 @@ namespace Spine.Unity { } public void AdjustReferenceMeshBounds () { + if (skeleton == null) + return; // Need one update to obtain valid mesh bounds Update(0.0f); @@ -332,12 +413,18 @@ namespace Spine.Unity { var submeshInstructionItem = currentInstructions.submeshInstructions.Items[i]; meshGenerator.AddSubmesh(submeshInstructionItem); } - - referenceMeshBounds = meshGenerator.GetMeshBounds(); + Bounds meshBounds = meshGenerator.GetMeshBounds(); + if (meshBounds.extents.x == 0 || meshBounds.extents.y == 0) { + ReferenceBounds = new Bounds(Vector3.zero, Vector3.one * 2f); + } else { + ReferenceBounds = meshBounds; + } } void AdjustOffsetScaleToMeshBounds (VisualElement visualElement) { Rect targetRect = visualElement.layout; + if (float.IsNaN(targetRect.width)) return; + float xScale = targetRect.width / referenceMeshBounds.size.x; float yScale = targetRect.height / referenceMeshBounds.size.y; this.scale = Math.Min(xScale, yScale); diff --git a/spine-unity/Modules/com.esotericsoftware.spine.ui-toolkit/package.json b/spine-unity/Modules/com.esotericsoftware.spine.ui-toolkit/package.json index c1e6f761b..186be6214 100644 --- a/spine-unity/Modules/com.esotericsoftware.spine.ui-toolkit/package.json +++ b/spine-unity/Modules/com.esotericsoftware.spine.ui-toolkit/package.json @@ -1,9 +1,9 @@ { "name": "com.esotericsoftware.spine.ui-toolkit", "displayName": "Spine UI Toolkit [Experimental]", - "description": "This plugin provides UI Toolkit integration for the spine-unity runtime.\n\nPrerequisites:\nIt requires a working installation of the spine-unity runtime, version 4.2.75 or newer.\n(See http://esotericsoftware.com/git/spine-runtimes/spine-unity)", - "version": "4.2.0-preview.1", - "unity": "2023.2", + "description": "This plugin provides UI Toolkit integration for the spine-unity runtime.\n\nPrerequisites:\nIt requires a working installation of the spine-unity runtime, version 4.2.82 or newer and Unity 6000.0.16 or newer (requires [this bugfix](https://issuetracker.unity3d.com/issues/some-default-uxmlconverters-are-dependent-on-the-current-culture)).\n(See http://esotericsoftware.com/git/spine-runtimes/spine-unity)", + "version": "4.2.0-preview.2", + "unity": "6000.0", "author": { "name": "Esoteric Software", "email": "contact@esotericsoftware.com", @@ -11,7 +11,7 @@ }, "dependencies": { "com.unity.modules.uielements": "1.0.0", - "com.esotericsoftware.spine.spine-unity": "4.2.75" + "com.esotericsoftware.spine.spine-unity": "4.2.82" }, "keywords": [ "spine",