Quellcode durchsuchen

[unity] BoneFollowerGraphic Component.

pharan vor 8 Jahren
Ursprung
Commit
16821f0bf7

+ 196 - 4
spine-unity/Assets/Examples/Getting Started/6 SkeletonGraphic.unity

@@ -135,13 +135,14 @@ RectTransform:
   m_LocalPosition: {x: 0, y: 0, z: 0}
   m_LocalScale: {x: 1, y: 1, z: 1}
   m_Children:
+  - {fileID: 759111375}
   - {fileID: 189134935}
   m_Father: {fileID: 289700665}
-  m_RootOrder: 0
+  m_RootOrder: 1
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
   m_AnchorMin: {x: 0.5, y: 0.5}
   m_AnchorMax: {x: 0.5, y: 0.5}
-  m_AnchoredPosition: {x: -13.605, y: 239}
+  m_AnchoredPosition: {x: -13.604996, y: 239}
   m_SizeDelta: {x: 491, y: 708}
   m_Pivot: {x: 0.5, y: 0.14047737}
 --- !u!114 &57002145
@@ -165,6 +166,8 @@ MonoBehaviour:
       Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
   skeletonDataAsset: {fileID: 11400000, guid: 3c48535ae5679204c950a22a7caaa5a4, type: 2}
   initialSkinName: default
+  initialFlipX: 0
+  initialFlipY: 0
   startingAnimation: main
   startingLoop: 1
   timeScale: 1
@@ -185,6 +188,99 @@ CanvasRenderer:
   m_PrefabParentObject: {fileID: 0}
   m_PrefabInternal: {fileID: 0}
   m_GameObject: {fileID: 57002143}
+--- !u!1 &140863499
+GameObject:
+  m_ObjectHideFlags: 0
+  m_PrefabParentObject: {fileID: 0}
+  m_PrefabInternal: {fileID: 0}
+  serializedVersion: 5
+  m_Component:
+  - component: {fileID: 140863500}
+  - component: {fileID: 140863503}
+  - component: {fileID: 140863502}
+  - component: {fileID: 140863501}
+  m_Layer: 5
+  m_Name: Detached BoneFollowerGraphic
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!224 &140863500
+RectTransform:
+  m_ObjectHideFlags: 0
+  m_PrefabParentObject: {fileID: 0}
+  m_PrefabInternal: {fileID: 0}
+  m_GameObject: {fileID: 140863499}
+  m_LocalRotation: {x: -0, y: -0, z: 0.6421143, w: 0.766609}
+  m_LocalPosition: {x: 0, y: 0, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_Children: []
+  m_Father: {fileID: 289700665}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+  m_AnchorMin: {x: 0.5, y: 0.5}
+  m_AnchorMax: {x: 0.5, y: 0.5}
+  m_AnchoredPosition: {x: -11.670258, y: 578.2387}
+  m_SizeDelta: {x: 100, y: 54.64}
+  m_Pivot: {x: -0.15, y: -2.04}
+--- !u!114 &140863501
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_PrefabParentObject: {fileID: 0}
+  m_PrefabInternal: {fileID: 0}
+  m_GameObject: {fileID: 140863499}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  m_Material: {fileID: 0}
+  m_Color: {r: 1, g: 1, b: 1, a: 1}
+  m_RaycastTarget: 0
+  m_OnCullStateChanged:
+    m_PersistentCalls:
+      m_Calls: []
+    m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
+      Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
+  m_FontData:
+    m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
+    m_FontSize: 50
+    m_FontStyle: 1
+    m_BestFit: 0
+    m_MinSize: 5
+    m_MaxSize: 50
+    m_Alignment: 4
+    m_AlignByGeometry: 0
+    m_RichText: 1
+    m_HorizontalOverflow: 1
+    m_VerticalOverflow: 1
+    m_LineSpacing: 1
+  m_Text: Hello
+--- !u!222 &140863502
+CanvasRenderer:
+  m_ObjectHideFlags: 0
+  m_PrefabParentObject: {fileID: 0}
+  m_PrefabInternal: {fileID: 0}
+  m_GameObject: {fileID: 140863499}
+--- !u!114 &140863503
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_PrefabParentObject: {fileID: 0}
+  m_PrefabInternal: {fileID: 0}
+  m_GameObject: {fileID: 140863499}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: b42a195b47491d34b9bcbc40898bcb29, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  skeletonGraphic: {fileID: 57002145}
+  initializeOnAwake: 1
+  boneName: head
+  followBoneRotation: 1
+  followSkeletonFlip: 1
+  followLocalScale: 0
+  followZPosition: 1
 --- !u!1 &189134934
 GameObject:
   m_ObjectHideFlags: 0
@@ -213,7 +309,7 @@ RectTransform:
   m_LocalScale: {x: 1, y: 1, z: 1}
   m_Children: []
   m_Father: {fileID: 57002144}
-  m_RootOrder: 0
+  m_RootOrder: 1
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
   m_AnchorMin: {x: 0.5, y: 1}
   m_AnchorMax: {x: 0.5, y: 1}
@@ -379,6 +475,7 @@ RectTransform:
   m_LocalPosition: {x: 0, y: 0, z: 0}
   m_LocalScale: {x: 1, y: 1, z: 1}
   m_Children:
+  - {fileID: 140863500}
   - {fileID: 57002144}
   - {fileID: 1384013133}
   m_Father: {fileID: 2133858527}
@@ -478,6 +575,99 @@ Transform:
   m_Father: {fileID: 0}
   m_RootOrder: 0
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &759111374
+GameObject:
+  m_ObjectHideFlags: 0
+  m_PrefabParentObject: {fileID: 0}
+  m_PrefabInternal: {fileID: 0}
+  serializedVersion: 5
+  m_Component:
+  - component: {fileID: 759111375}
+  - component: {fileID: 759111378}
+  - component: {fileID: 759111377}
+  - component: {fileID: 759111376}
+  m_Layer: 5
+  m_Name: Child BoneFollowerGraphic
+  m_TagString: Untagged
+  m_Icon: {fileID: 0}
+  m_NavMeshLayer: 0
+  m_StaticEditorFlags: 0
+  m_IsActive: 1
+--- !u!224 &759111375
+RectTransform:
+  m_ObjectHideFlags: 0
+  m_PrefabParentObject: {fileID: 0}
+  m_PrefabInternal: {fileID: 0}
+  m_GameObject: {fileID: 759111374}
+  m_LocalRotation: {x: 0, y: 0, z: 0.012748675, w: 0.99991876}
+  m_LocalPosition: {x: 0, y: 0, z: 0}
+  m_LocalScale: {x: 1, y: 1, z: 1}
+  m_Children: []
+  m_Father: {fileID: 57002144}
+  m_RootOrder: 0
+  m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+  m_AnchorMin: {x: 0.5, y: 0.5}
+  m_AnchorMax: {x: 0.5, y: 0.5}
+  m_AnchoredPosition: {x: 214.28879, y: 127.327515}
+  m_SizeDelta: {x: 35.7, y: 54.1}
+  m_Pivot: {x: 0.5, y: 0.5}
+--- !u!114 &759111376
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_PrefabParentObject: {fileID: 0}
+  m_PrefabInternal: {fileID: 0}
+  m_GameObject: {fileID: 759111374}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 708705254, guid: f70555f144d8491a825f0804e09c671c, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  m_Material: {fileID: 0}
+  m_Color: {r: 1, g: 1, b: 1, a: 1}
+  m_RaycastTarget: 0
+  m_OnCullStateChanged:
+    m_PersistentCalls:
+      m_Calls: []
+    m_TypeName: UnityEngine.UI.MaskableGraphic+CullStateChangedEvent, UnityEngine.UI,
+      Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
+  m_FontData:
+    m_Font: {fileID: 10102, guid: 0000000000000000e000000000000000, type: 0}
+    m_FontSize: 24
+    m_FontStyle: 1
+    m_BestFit: 0
+    m_MinSize: 2
+    m_MaxSize: 50
+    m_Alignment: 4
+    m_AlignByGeometry: 0
+    m_RichText: 1
+    m_HorizontalOverflow: 1
+    m_VerticalOverflow: 1
+    m_LineSpacing: 1
+  m_Text: World!
+--- !u!222 &759111377
+CanvasRenderer:
+  m_ObjectHideFlags: 0
+  m_PrefabParentObject: {fileID: 0}
+  m_PrefabInternal: {fileID: 0}
+  m_GameObject: {fileID: 759111374}
+--- !u!114 &759111378
+MonoBehaviour:
+  m_ObjectHideFlags: 0
+  m_PrefabParentObject: {fileID: 0}
+  m_PrefabInternal: {fileID: 0}
+  m_GameObject: {fileID: 759111374}
+  m_Enabled: 1
+  m_EditorHideFlags: 0
+  m_Script: {fileID: 11500000, guid: b42a195b47491d34b9bcbc40898bcb29, type: 3}
+  m_Name: 
+  m_EditorClassIdentifier: 
+  skeletonGraphic: {fileID: 57002145}
+  initializeOnAwake: 1
+  boneName: handL
+  followBoneRotation: 1
+  followSkeletonFlip: 1
+  followLocalScale: 0
+  followZPosition: 1
 --- !u!1 &774800193
 GameObject:
   m_ObjectHideFlags: 0
@@ -912,7 +1102,7 @@ RectTransform:
   m_Children:
   - {fileID: 1066372096}
   m_Father: {fileID: 289700665}
-  m_RootOrder: 1
+  m_RootOrder: 2
   m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
   m_AnchorMin: {x: 0.5, y: 0.5}
   m_AnchorMax: {x: 0.5, y: 0.5}
@@ -940,6 +1130,8 @@ MonoBehaviour:
       Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
   skeletonDataAsset: {fileID: 11400000, guid: a467507a4ffb1d542a558739b2fede77, type: 2}
   initialSkinName: base
+  initialFlipX: 0
+  initialFlipY: 0
   startingAnimation: run
   startingLoop: 1
   timeScale: 1

+ 38 - 38
spine-unity/Assets/spine-unity/Editor/SpineEditorUtilities.cs

@@ -1641,21 +1641,21 @@ namespace Spine.Unity.Editor {
 			}
 		}
 
-		public static void DrawBoneNames (Transform transform, Skeleton skeleton) {
+		public static void DrawBoneNames (Transform transform, Skeleton skeleton, float positionScale = 1f) {
 			GUIStyle style = BoneNameStyle;
 			foreach (Bone b in skeleton.Bones) {
-				var pos = new Vector3(b.WorldX, b.WorldY, 0) + (new Vector3(b.A, b.C) * (b.Data.Length * 0.5f));
+				var pos = new Vector3(b.WorldX * positionScale, b.WorldY * positionScale, 0) + (new Vector3(b.A, b.C) * (b.Data.Length * 0.5f));
 				pos = transform.TransformPoint(pos);
 				Handles.Label(pos, b.Data.Name, style);
 			}
 		}
 
-		public static void DrawBones (Transform transform, Skeleton skeleton) {
+		public static void DrawBones (Transform transform, Skeleton skeleton, float positionScale = 1f) {
 			float boneScale = 1.8f; // Draw the root bone largest;
-			DrawCrosshairs2D(skeleton.Bones.Items[0].GetWorldPosition(transform), 0.08f);
+			DrawCrosshairs2D(skeleton.Bones.Items[0].GetWorldPosition(transform), 0.08f, positionScale);
 
 			foreach (Bone b in skeleton.Bones) {
-				DrawBone(transform, b, boneScale);
+				DrawBone(transform, b, boneScale, positionScale);
 				boneScale = 1f;
 			}
 		}
@@ -1668,45 +1668,45 @@ namespace Spine.Unity.Editor {
 			_boneWireBuffer[4] = _boneWireBuffer[0]; // closed polygon.
 			return _boneWireBuffer;
 		}
-		public static void DrawBoneWireframe (Transform transform, Bone b, Color color) {
+		public static void DrawBoneWireframe (Transform transform, Bone b, Color color, float skeletonRenderScale = 1f) {
 			Handles.color = color;
-			var pos = new Vector3(b.WorldX, b.WorldY, 0);
+			var pos = new Vector3(b.WorldX * skeletonRenderScale, b.WorldY * skeletonRenderScale, 0);
 			float length = b.Data.Length;
 
 			if (length > 0) {
 				Quaternion rot = Quaternion.Euler(0, 0, b.WorldRotationX);
-				Vector3 scale = Vector3.one * length * b.WorldScaleX;
+				Vector3 scale = Vector3.one * length * b.WorldScaleX * skeletonRenderScale;
 				const float my = 1.5f;
-				scale.y *= (SpineHandles.handleScale + 1f) * 0.5f;
-				scale.y = Mathf.Clamp(scale.x, -my, my);
+				scale.y *= (SpineHandles.handleScale + 1) * 0.5f;
+				scale.y = Mathf.Clamp(scale.x, -my * skeletonRenderScale, my * skeletonRenderScale);
 				Handles.DrawPolyLine(GetBoneWireBuffer(transform.localToWorldMatrix * Matrix4x4.TRS(pos, rot, scale)));
 				var wp = transform.TransformPoint(pos);
-				DrawBoneCircle(wp, color, transform.forward);
+				DrawBoneCircle(wp, color, transform.forward, skeletonRenderScale);
 			} else {
 				var wp = transform.TransformPoint(pos);
-				DrawBoneCircle(wp, color, transform.forward);
+				DrawBoneCircle(wp, color, transform.forward, skeletonRenderScale);
 			}
 		}
 
-		public static void DrawBone (Transform transform, Bone b, float boneScale) {
-			var pos = new Vector3(b.WorldX, b.WorldY, 0);
+		public static void DrawBone (Transform transform, Bone b, float boneScale, float skeletonRenderScale = 1f) {
+			var pos = new Vector3(b.WorldX * skeletonRenderScale, b.WorldY * skeletonRenderScale, 0);
 			float length = b.Data.Length;
 			if (length > 0) {
 				Quaternion rot = Quaternion.Euler(0, 0, b.WorldRotationX);
-				Vector3 scale = Vector3.one * length * b.WorldScaleX;
+				Vector3 scale = Vector3.one * length * b.WorldScaleX * skeletonRenderScale;
 				const float my = 1.5f;
 				scale.y *= (SpineHandles.handleScale + 1f) * 0.5f;
-				scale.y = Mathf.Clamp(scale.x, -my, my);
+				scale.y = Mathf.Clamp(scale.x, -my * skeletonRenderScale, my * skeletonRenderScale);
 				SpineHandles.GetBoneMaterial().SetPass(0);
 				Graphics.DrawMeshNow(SpineHandles.BoneMesh, transform.localToWorldMatrix * Matrix4x4.TRS(pos, rot, scale));
 			} else {
 				var wp = transform.TransformPoint(pos);
-				DrawBoneCircle(wp, SpineHandles.BoneColor, transform.forward, boneScale);
+				DrawBoneCircle(wp, SpineHandles.BoneColor, transform.forward, boneScale * skeletonRenderScale);
 			}
 		}
 
-		public static void DrawBone (Transform transform, Bone b, float boneScale, Color color) {
-			var pos = new Vector3(b.WorldX, b.WorldY, 0);
+		public static void DrawBone (Transform transform, Bone b, float boneScale, Color color, float skeletonRenderScale = 1f) {
+			var pos = new Vector3(b.WorldX * skeletonRenderScale, b.WorldY * skeletonRenderScale, 0);
 			float length = b.Data.Length;
 			if (length > 0) {
 				Quaternion rot = Quaternion.Euler(0, 0, b.WorldRotationX);
@@ -1718,7 +1718,7 @@ namespace Spine.Unity.Editor {
 				Graphics.DrawMeshNow(SpineHandles.BoneMesh, transform.localToWorldMatrix * Matrix4x4.TRS(pos, rot, scale));
 			} else {
 				var wp = transform.TransformPoint(pos);
-				DrawBoneCircle(wp, color, transform.forward, boneScale);
+				DrawBoneCircle(wp, color, transform.forward, boneScale * skeletonRenderScale);
 			}
 		}
 
@@ -1814,7 +1814,7 @@ namespace Spine.Unity.Editor {
 			Handles.DrawLine(lastVert, firstVert);
 		}
 
-		public static void DrawConstraints (Transform transform, Skeleton skeleton) {
+		public static void DrawConstraints (Transform transform, Skeleton skeleton, float skeletonRenderScale = 1f) {
 			Vector3 targetPos;
 			Vector3 pos;
 			bool active;
@@ -1826,19 +1826,19 @@ namespace Spine.Unity.Editor {
 			handleColor = SpineHandles.TransformContraintColor;
 			foreach (var tc in skeleton.TransformConstraints) {
 				var targetBone = tc.Target;
-				targetPos = targetBone.GetWorldPosition(transform);
+				targetPos = targetBone.GetWorldPosition(transform, skeletonRenderScale);
 
 				if (tc.TranslateMix > 0) {
 					if (tc.TranslateMix != 1f) {
 						Handles.color = handleColor;
 						foreach (var b in tc.Bones) {
-							pos = b.GetWorldPosition(transform);
+							pos = b.GetWorldPosition(transform, skeletonRenderScale);
 							Handles.DrawDottedLine(targetPos, pos, Thickness);
 						}
 					}
-					SpineHandles.DrawBoneCircle(targetPos, handleColor, normal, 1.3f);
+					SpineHandles.DrawBoneCircle(targetPos, handleColor, normal, 1.3f * skeletonRenderScale);
 					Handles.color = handleColor;
-					SpineHandles.DrawCrosshairs(targetPos, 0.2f, targetBone.A, targetBone.B, targetBone.C, targetBone.D, transform);
+					SpineHandles.DrawCrosshairs(targetPos, 0.2f, targetBone.A, targetBone.B, targetBone.C, targetBone.D, transform, skeletonRenderScale);
 				}
 			}
 
@@ -1846,25 +1846,25 @@ namespace Spine.Unity.Editor {
 			handleColor = SpineHandles.IkColor;
 			foreach (var ikc in skeleton.IkConstraints) {
 				Bone targetBone = ikc.Target;
-				targetPos = targetBone.GetWorldPosition(transform);
+				targetPos = targetBone.GetWorldPosition(transform, skeletonRenderScale);
 				var bones = ikc.Bones;
 				active = ikc.Mix > 0;
 				if (active) {
-					pos = bones.Items[0].GetWorldPosition(transform);
+					pos = bones.Items[0].GetWorldPosition(transform, skeletonRenderScale);
 					switch (bones.Count) {
 					case 1: {
 							Handles.color = handleColor;
 							Handles.DrawLine(targetPos, pos);
 							SpineHandles.DrawBoneCircle(targetPos, handleColor, normal);
 							var m = bones.Items[0].GetMatrix4x4();
-							m.m03 = targetBone.WorldX;
-							m.m13 = targetBone.WorldY;
+							m.m03 = targetBone.WorldX * skeletonRenderScale;
+							m.m13 = targetBone.WorldY * skeletonRenderScale;
 							SpineHandles.DrawArrowhead(transform.localToWorldMatrix * m);
 							break;	
 						}
 					case 2: {
 							Bone childBone = bones.Items[1];
-							Vector3 child = childBone.GetWorldPosition(transform);
+							Vector3 child = childBone.GetWorldPosition(transform, skeletonRenderScale);
 							Handles.color = handleColor;
 							Handles.DrawLine(child, pos);
 							Handles.DrawLine(targetPos, child);
@@ -1872,8 +1872,8 @@ namespace Spine.Unity.Editor {
 							SpineHandles.DrawBoneCircle(child, handleColor, normal, 0.5f);
 							SpineHandles.DrawBoneCircle(targetPos, handleColor, normal);
 							var m = childBone.GetMatrix4x4();
-							m.m03 = targetBone.WorldX;
-							m.m13 = targetBone.WorldY;
+							m.m03 = targetBone.WorldX * skeletonRenderScale;
+							m.m13 = targetBone.WorldY * skeletonRenderScale;
 							SpineHandles.DrawArrowhead(transform.localToWorldMatrix * m);
 							break;
 						}
@@ -1888,18 +1888,18 @@ namespace Spine.Unity.Editor {
 				active = pc.TranslateMix > 0;
 				if (active)
 					foreach (var b in pc.Bones)
-						SpineHandles.DrawBoneCircle(b.GetWorldPosition(transform), handleColor, normal, 1f);
+						SpineHandles.DrawBoneCircle(b.GetWorldPosition(transform, skeletonRenderScale), handleColor, normal, 1f * skeletonRenderScale);
 			}
 		}
 
-		static void DrawCrosshairs2D (Vector3 position, float scale) {
-			scale *= SpineHandles.handleScale;
+		static void DrawCrosshairs2D (Vector3 position, float scale, float skeletonRenderScale = 1f) {
+			scale *= SpineHandles.handleScale * skeletonRenderScale;
 			Handles.DrawLine(position + new Vector3(-scale, 0), position + new Vector3(scale, 0));
 			Handles.DrawLine(position + new Vector3(0, -scale), position + new Vector3(0, scale));
 		}
 
-		static void DrawCrosshairs (Vector3 position, float scale, float a, float b, float c, float d, Transform transform) {
-			scale *= SpineHandles.handleScale;
+		static void DrawCrosshairs (Vector3 position, float scale, float a, float b, float c, float d, Transform transform, float skeletonRenderScale = 1f) {
+			scale *= SpineHandles.handleScale * skeletonRenderScale;
 
 			var xOffset = (Vector3)(new Vector2(a, c).normalized * scale);
 			var yOffset = (Vector3)(new Vector2(b, d).normalized * scale);
@@ -1941,7 +1941,7 @@ namespace Spine.Unity.Editor {
 			float firstScale = 0.08f * scale;
 			Handles.DrawSolidDisc(pos, normal, firstScale);
 			const float Thickness = 0.03f;
-			float secondScale = firstScale - (Thickness  * SpineHandles.handleScale);
+			float secondScale = firstScale - (Thickness  * SpineHandles.handleScale * scale);
 
 			if (secondScale > 0f) {
 				Handles.color = new Color(0.3f, 0.3f, 0.3f, 0.5f);

+ 133 - 0
spine-unity/Assets/spine-unity/Modules/SkeletonGraphic/BoneFollowerGraphic.cs

@@ -0,0 +1,133 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace Spine.Unity {
+	[ExecuteInEditMode]
+	[DisallowMultipleComponent]
+	[AddComponentMenu("Spine/UI/BoneFollowerGraphic")]
+	public class BoneFollowerGraphic : MonoBehaviour {
+		public SkeletonGraphic skeletonGraphic;
+		public SkeletonGraphic SkeletonGraphic {
+			get { return skeletonGraphic; }
+			set {
+				skeletonGraphic = value;
+				Initialize();
+			}
+		}
+
+		public bool initializeOnAwake = true;
+
+		/// <summary>If a bone isn't set in code, boneName is used to find the bone at the beginning. For runtime switching by name, use SetBoneByName. You can also set the BoneFollower.bone field directly.</summary>
+		[SpineBone(dataField: "skeletonGraphic")]
+		[SerializeField] public string boneName;
+
+		public bool followBoneRotation = true;
+		[Tooltip("Follows the skeleton's flip state by controlling this Transform's local scale.")]
+		public bool followSkeletonFlip = true;
+		[Tooltip("Follows the target bone's local scale. BoneFollower cannot inherit world/skewed scale because of UnityEngine.Transform property limitations.")]
+		public bool followLocalScale = false;
+		public bool followZPosition = true;
+
+		[System.NonSerialized] public Bone bone;
+
+		Transform skeletonTransform;
+		bool skeletonTransformIsParent;
+
+		[System.NonSerialized] public bool valid;
+
+		/// <summary>
+		/// Sets the target bone by its bone name. Returns false if no bone was found.</summary>
+		public bool SetBone (string name) {
+			bone = skeletonGraphic.Skeleton.FindBone(name);
+			if (bone == null) {
+				Debug.LogError("Bone not found: " + name, this);
+				return false;
+			}
+			boneName = name;
+			return true;
+		}
+
+		public void Awake () {
+			if (initializeOnAwake) Initialize();
+		}
+
+		public void Initialize () {
+			bone = null;
+			valid = skeletonGraphic != null && skeletonGraphic.IsValid;
+			if (!valid) return;
+
+			skeletonTransform = skeletonGraphic.transform;
+//			skeletonGraphic.OnRebuild -= HandleRebuildRenderer;
+//			skeletonGraphic.OnRebuild += HandleRebuildRenderer;
+			skeletonTransformIsParent = Transform.ReferenceEquals(skeletonTransform, transform.parent);
+
+			if (!string.IsNullOrEmpty(boneName))
+				bone = skeletonGraphic.Skeleton.FindBone(boneName);
+
+			#if UNITY_EDITOR
+			if (Application.isEditor)
+				LateUpdate();
+			#endif
+		}
+
+		public void LateUpdate () {
+			if (!valid) {
+				Initialize();
+				return;
+			}
+
+			#if UNITY_EDITOR
+			if (!Application.isPlaying)
+				skeletonTransformIsParent = Transform.ReferenceEquals(skeletonTransform, transform.parent);
+			#endif
+
+			if (bone == null) {
+				if (string.IsNullOrEmpty(boneName)) return;
+				bone = skeletonGraphic.Skeleton.FindBone(boneName);
+				if (!SetBone(boneName)) return;
+			}
+
+			var thisTransform = this.transform as RectTransform;
+			if (thisTransform == null) return;
+
+			float scale = skeletonGraphic.canvas.referencePixelsPerUnit;
+
+			if (skeletonTransformIsParent) {
+				// Recommended setup: Use local transform properties if Spine GameObject is the immediate parent
+				thisTransform.localPosition = new Vector3(bone.worldX * scale, bone.worldY * scale, followZPosition ? 0f : thisTransform.localPosition.z);
+				if (followBoneRotation) thisTransform.localRotation = bone.GetQuaternion();
+			} else {
+				// For special cases: Use transform world properties if transform relationship is complicated
+				Vector3 targetWorldPosition = skeletonTransform.TransformPoint(new Vector3(bone.worldX * scale, bone.worldY * scale, 0f));
+				if (!followZPosition) targetWorldPosition.z = thisTransform.position.z;
+
+				float boneWorldRotation = bone.WorldRotationX;
+
+				Transform transformParent = thisTransform.parent;
+				if (transformParent != null) {
+					Matrix4x4 m = transformParent.localToWorldMatrix;
+					if (m.m00 * m.m11 - m.m01 * m.m10 < 0) // Determinant2D is negative
+						boneWorldRotation = -boneWorldRotation;
+				}
+
+				if (followBoneRotation) {
+					Vector3 worldRotation = skeletonTransform.rotation.eulerAngles;
+					#if UNITY_5_6_OR_NEWER
+					thisTransform.SetPositionAndRotation(targetWorldPosition, Quaternion.Euler(worldRotation.x, worldRotation.y, skeletonTransform.rotation.eulerAngles.z + boneWorldRotation));
+					#else
+					thisTransform.position = targetWorldPosition;
+					thisTransform.rotation = Quaternion.Euler(worldRotation.x, worldRotation.y, skeletonTransform.rotation.eulerAngles.z + bone.WorldRotationX);
+					#endif
+				} else {
+					thisTransform.position = targetWorldPosition;
+				}
+			}
+
+			Vector3 localScale = followLocalScale ? new Vector3(bone.scaleX, bone.scaleY, 1f) : new Vector3(1f, 1f, 1f);
+			if (followSkeletonFlip) localScale.y *= bone.skeleton.flipX ^ bone.skeleton.flipY ? -1f : 1f;
+			thisTransform.localScale = localScale;
+		}
+
+	}
+}

+ 12 - 0
spine-unity/Assets/spine-unity/Modules/SkeletonGraphic/BoneFollowerGraphic.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: b42a195b47491d34b9bcbc40898bcb29
+timeCreated: 1499211965
+licenseType: Free
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 172 - 0
spine-unity/Assets/spine-unity/Modules/SkeletonGraphic/Editor/BoneFollowerGraphicInspector.cs

@@ -0,0 +1,172 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEditor;
+
+using Spine.Unity;
+
+namespace Spine.Unity.Editor {
+
+	using Editor = UnityEditor.Editor;
+	using Event = UnityEngine.Event;
+
+	[CustomEditor(typeof(BoneFollowerGraphic)), CanEditMultipleObjects]
+	public class BoneFollowerGraphicInspector : Editor {
+		
+		SerializedProperty boneName, skeletonGraphic, followZPosition, followBoneRotation, followLocalScale, followSkeletonFlip;
+		BoneFollowerGraphic targetBoneFollower;
+		bool needsReset;
+
+		#region Context Menu Item
+		[MenuItem ("CONTEXT/SkeletonGraphic/Add BoneFollower GameObject")]
+		static void AddBoneFollowerGameObject (MenuCommand cmd) {
+			var skeletonGraphic = cmd.context as SkeletonGraphic;
+			var go = new GameObject("BoneFollower", typeof(RectTransform));
+			var t = go.transform;
+			t.SetParent(skeletonGraphic.transform);
+			t.localPosition = Vector3.zero;
+
+			var f = go.AddComponent<BoneFollowerGraphic>();
+			f.skeletonGraphic = skeletonGraphic;
+			f.SetBone(skeletonGraphic.Skeleton.RootBone.Data.Name);
+
+			EditorGUIUtility.PingObject(t);
+
+			Undo.RegisterCreatedObjectUndo(go, "Add BoneFollowerGraphic");
+		}
+
+		// Validate
+		[MenuItem ("CONTEXT/SkeletonGraphic/Add BoneFollower GameObject", true)]
+		static bool ValidateAddBoneFollowerGameObject (MenuCommand cmd) {
+			var skeletonGraphic = cmd.context as SkeletonGraphic;
+			return skeletonGraphic.IsValid;
+		}
+		#endregion
+
+		void OnEnable () {
+			skeletonGraphic = serializedObject.FindProperty("skeletonGraphic");
+			boneName = serializedObject.FindProperty("boneName");
+			followBoneRotation = serializedObject.FindProperty("followBoneRotation");
+			followZPosition = serializedObject.FindProperty("followZPosition");
+			followLocalScale = serializedObject.FindProperty("followLocalScale");
+			followSkeletonFlip = serializedObject.FindProperty("followSkeletonFlip");
+
+			targetBoneFollower = (BoneFollowerGraphic)target;
+			if (targetBoneFollower.SkeletonGraphic != null)
+				targetBoneFollower.SkeletonGraphic.Initialize(false);
+
+			if (!targetBoneFollower.valid || needsReset) {
+				targetBoneFollower.Initialize();
+				targetBoneFollower.LateUpdate();
+				needsReset = false;
+				SceneView.RepaintAll();
+			}
+		}
+
+		public void OnSceneGUI () {
+			var tbf = target as BoneFollowerGraphic;
+			var skeletonGraphicComponent = tbf.SkeletonGraphic;
+			if (skeletonGraphicComponent == null) return;
+
+			var transform = skeletonGraphicComponent.transform;
+			var skeleton = skeletonGraphicComponent.Skeleton;
+			var canvas = skeletonGraphicComponent.canvas;
+			float positionScale = canvas == null ? 1f : skeletonGraphicComponent.canvas.referencePixelsPerUnit;
+
+			if (string.IsNullOrEmpty(boneName.stringValue)) {
+				SpineHandles.DrawBones(transform, skeleton, positionScale);
+				SpineHandles.DrawBoneNames(transform, skeleton, positionScale);
+				Handles.Label(tbf.transform.position, "No bone selected", EditorStyles.helpBox);
+			} else {
+				var targetBone = tbf.bone;
+				if (targetBone == null) return;
+				
+				SpineHandles.DrawBoneWireframe(transform, targetBone, SpineHandles.TransformContraintColor, positionScale);
+				Handles.Label(targetBone.GetWorldPosition(transform, positionScale), targetBone.Data.Name, SpineHandles.BoneNameStyle);
+			}
+		}
+
+		override public void OnInspectorGUI () {
+			if (serializedObject.isEditingMultipleObjects) {
+				if (needsReset) {
+					needsReset = false;
+					foreach (var o in targets) {
+						var bf = (BoneFollower)o;
+						bf.Initialize();
+						bf.LateUpdate();
+					}
+					SceneView.RepaintAll();
+				}
+
+				EditorGUI.BeginChangeCheck();
+				DrawDefaultInspector();
+				needsReset |= EditorGUI.EndChangeCheck();
+				return;
+			}
+
+			if (needsReset && Event.current.type == EventType.Layout) {
+				targetBoneFollower.Initialize();
+				targetBoneFollower.LateUpdate();
+				needsReset = false;
+				SceneView.RepaintAll();
+			}
+			serializedObject.Update();
+
+			// Find Renderer
+			if (skeletonGraphic.objectReferenceValue == null) {
+				SkeletonGraphic parentRenderer = targetBoneFollower.GetComponentInParent<SkeletonGraphic>();
+				if (parentRenderer != null && parentRenderer.gameObject != targetBoneFollower.gameObject) {
+					skeletonGraphic.objectReferenceValue = parentRenderer;
+					Debug.Log("Inspector automatically assigned BoneFollowerGraphic.SkeletonGraphic");
+				}
+			}
+
+			EditorGUILayout.PropertyField(skeletonGraphic);
+			var skeletonGraphicComponent = skeletonGraphic.objectReferenceValue as SkeletonGraphic;
+			if (skeletonGraphicComponent != null) {
+				if (skeletonGraphicComponent.gameObject == targetBoneFollower.gameObject) {
+					skeletonGraphic.objectReferenceValue = null;
+					EditorUtility.DisplayDialog("Invalid assignment.", "BoneFollowerGraphic can only follow a skeleton on a separate GameObject.\n\nCreate a new GameObject for your BoneFollower, or choose a SkeletonGraphic from a different GameObject.", "Ok");
+				}
+			}
+
+			if (!targetBoneFollower.valid) {
+				needsReset = true;
+			}
+
+			if (targetBoneFollower.valid) {
+				EditorGUI.BeginChangeCheck();
+				EditorGUILayout.PropertyField(boneName);
+				needsReset |= EditorGUI.EndChangeCheck();
+
+				EditorGUILayout.PropertyField(followBoneRotation);
+				EditorGUILayout.PropertyField(followZPosition);
+				EditorGUILayout.PropertyField(followLocalScale);
+				EditorGUILayout.PropertyField(followSkeletonFlip);
+
+				//BoneFollowerInspector.RecommendRigidbodyButton(targetBoneFollower);
+			} else {
+				var boneFollowerSkeletonGraphic = targetBoneFollower.skeletonGraphic;
+				if (boneFollowerSkeletonGraphic == null) {
+					EditorGUILayout.HelpBox("SkeletonGraphic is unassigned. Please assign a SkeletonRenderer (SkeletonAnimation or SkeletonAnimator).", MessageType.Warning);
+				} else {
+					boneFollowerSkeletonGraphic.Initialize(false);
+
+					if (boneFollowerSkeletonGraphic.skeletonDataAsset == null)
+						EditorGUILayout.HelpBox("Assigned SkeletonGraphic does not have SkeletonData assigned to it.", MessageType.Warning);
+
+					if (!boneFollowerSkeletonGraphic.IsValid)
+						EditorGUILayout.HelpBox("Assigned SkeletonGraphic is invalid. Check target SkeletonGraphic, its SkeletonDataAsset or the console for other errors.", MessageType.Warning);
+				}
+			}
+
+			var current = Event.current;
+			bool wasUndo = (current.type == EventType.ValidateCommand && current.commandName == "UndoRedoPerformed");
+			if (wasUndo)
+				targetBoneFollower.Initialize();
+
+			serializedObject.ApplyModifiedProperties();
+		}
+
+	}
+}

+ 12 - 0
spine-unity/Assets/spine-unity/Modules/SkeletonGraphic/Editor/BoneFollowerGraphicInspector.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: da44a8561fd243c43a1f77bda36de0eb
+timeCreated: 1499279157
+licenseType: Free
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 4 - 0
spine-unity/Assets/spine-unity/SkeletonExtensions.cs

@@ -134,6 +134,10 @@ namespace Spine.Unity {
 			return spineGameObjectTransform.TransformPoint(new Vector3(bone.worldX, bone.worldY));
 		}
 
+		public static Vector3 GetWorldPosition (this Bone bone, UnityEngine.Transform spineGameObjectTransform, float positionScale) {
+			return spineGameObjectTransform.TransformPoint(new Vector3(bone.worldX * positionScale, bone.worldY * positionScale));
+		}
+
 		/// <summary>Gets a skeleton space UnityEngine.Quaternion representation of bone.WorldRotationX.</summary>
 		public static Quaternion GetQuaternion (this Bone bone) {
 			var halfRotation = Mathf.Atan2(bone.c, bone.a) * 0.5f;