]> git.bts.cx Git - jurassic-tween.git/commitdiff
[FEATURE] Initial Release
authorBen Sherratt <redacted>
Tue, 4 May 2021 15:58:49 +0000 (16:58 +0100)
committerBen Sherratt <redacted>
Tue, 4 May 2021 16:01:21 +0000 (17:01 +0100)
* Created base framework
* Added generic captor for user-created MonoBehavior components
* Added special captor for Transform component

README.md [new file with mode: 0644]
Scripts/Runtime/Internal/ComponentStateCaptor.cs [new file with mode: 0644]
Scripts/Runtime/Internal/Context.cs [new file with mode: 0644]
Scripts/Runtime/Internal/EngineComponentStateCaptor.cs [new file with mode: 0644]
Scripts/Runtime/Internal/GenericComponentStateCaptor.cs [new file with mode: 0644]
Scripts/Runtime/Tween.cs [new file with mode: 0644]
Scripts/Runtime/TweenParameters.cs [new file with mode: 0644]

diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..d86683f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,69 @@
+# JurassicTween
+
+> Your scientists were so preoccupied with whether or not they could (tween like that) they didn’t stop to think if they should.
+
+🚨 This is a preview only. I advise against shipping this. 🚨
+
+## What?
+
+JurassicTween is a Unity3D tweening library designed for:
+
+* Tweening values on `Component` objects...
+* ...in a very simple way...
+* ...favoring ease of prototyping over performance (for now).
+
+## How?
+
+Add JurassicTween to a project. Making a tween is super easy, here's how you would make an object randomly move, rotate and scale onscreen:
+
+```
+using JurassicTween;
+using UnityEngine;
+
+public class MoveMe : MonoBehaviour {
+    void Update() {
+        if (gameObject.IsTweening() == false) {
+            using (transform.Tween()) {
+                transform.position = Random.insideUnitSphere * 10.0f;
+                transform.rotation = Quaternion.Euler(0, 0, Random.Range(0.0f, 360.0f));
+                transform.localScale = Vector3.one * Random.Range(0.1f, 2.0f);
+            }
+            // using (anyOtherComponent.Tween()) {
+                // These chain together, and your own MonoBehavior components should also work...
+            // }
+        }
+    }
+}
+```
+
+Basically, the tween breaks down as follows:
+
+* Mark a game component as requiring a tween using `Component.Tween()`
+* Set the fields on the component that you want to target
+* The tween will then be set up to be from the current values to the target values you set.
+
+The tweens themselves are performed on a component, and you can check if a game object currently has any active tweens.
+
+**VERY IMPORTANT:** Not everything will work correctly, especially Unity's native components. (Transform is an exception, we've fixed that.)
+
+## Roadmap
+
+This is a preview release only. There are bugs. Lots of things won't work.
+
+Currently the following features are supported:
+
+* Some things tween
+* You can give a duration of tween
+* The curve of the tweens can be set, and you can use an AnimationCurve to design your own.
+
+If this was useful to people and there was a sustainable way to develop it, we could add:
+
+* More API features
+* Better animation system and/or better integration into the Unity systems
+* Performance!
+
+If you like this then please let me know on Twitter, find me at http://twitter.com/btsherratt/.
+
+# Licence
+
+Currently this is licenced for evaluation use in your non-commercial projects only, and all rights are currently reserved and retained by my company SKFX Ltd.. If there is interest in taking this forward then this will be changed, but I would like to decide slowly about what makes most sense for licensing here.
\ No newline at end of file
diff --git a/Scripts/Runtime/Internal/ComponentStateCaptor.cs b/Scripts/Runtime/Internal/ComponentStateCaptor.cs
new file mode 100644 (file)
index 0000000..ebf4adb
--- /dev/null
@@ -0,0 +1,77 @@
+//
+// ComponentStateCaptor.cs
+// JurassicTween
+//
+// Created by Benjamin Sherratt on 04/05/2021.
+// Copyright © 2021 SKFX Ltd. All rights reserved.
+//
+
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace JurassicTween.Internal {
+    public struct ComponentState {
+        public float[] values;
+
+        public ComponentState(float[] values) {
+            this.values = values;
+        }
+    }
+    public struct ComponentStateDelta {
+        public struct Range {
+            public float start;
+            public float end;
+
+            public Range(float start, float end) {
+                this.start = start;
+                this.end = end;
+            }
+        }
+
+        public Dictionary<string, Range> rangeByAnimationKey;
+
+        public ComponentStateDelta(Dictionary<string, Range> rangeByAnimationKey) {
+            this.rangeByAnimationKey = rangeByAnimationKey;
+        }
+    }
+
+    public interface IComponentStateGetter {
+        ComponentState GetComponentState(Component component);
+    }
+
+    public interface IComponentStateDeltaGenerator {
+        ComponentStateDelta GenerateComponentStateDelta(ComponentState startState, ComponentState endState);
+    }
+
+    public interface IComponentStateCaptor : IComponentStateGetter, IComponentStateDeltaGenerator {
+    }
+
+    public static class ComponentStateCaptor {
+        static Dictionary<Type, IComponentStateCaptor> stateCaptorByType;
+
+        public static IComponentStateCaptor GetStateCaptor(this Component component) {
+            Type componentType = component.GetType();
+
+            if (stateCaptorByType == null) {
+                stateCaptorByType = new Dictionary<Type, IComponentStateCaptor>();
+                CreateUnityInternalCaptors();
+            }
+
+            IComponentStateCaptor stateCaptor;
+            if (stateCaptorByType.ContainsKey(componentType)) {
+                stateCaptor = stateCaptorByType[componentType];
+            } else {
+                stateCaptor = new GenericComponentStateCaptor(componentType);
+                stateCaptorByType[componentType] = stateCaptor;
+            }
+
+            return stateCaptor;
+        }
+
+        static void CreateUnityInternalCaptors() {
+            // Add the custom captors for Unity internals...
+            stateCaptorByType[typeof(Transform)] = new TransformComponentStateCaptor();
+        }
+    }
+}
diff --git a/Scripts/Runtime/Internal/Context.cs b/Scripts/Runtime/Internal/Context.cs
new file mode 100644 (file)
index 0000000..785a1d5
--- /dev/null
@@ -0,0 +1,85 @@
+//
+// Context.cs
+// JurassicTween
+//
+// Created by Benjamin Sherratt on 04/05/2021.
+// Copyright © 2021 SKFX Ltd. All rights reserved.
+//
+
+using System;
+using UnityEngine;
+
+namespace JurassicTween.Internal {
+    class Context : IDisposable {
+        private Component component;
+        private IComponentStateCaptor componentStateCaptor;
+        private ComponentState startState;
+        private float time;
+        private TweenParameters tweenParameters;
+
+        public Context(Component component, float time, TweenParameters tweenParameters) {
+            this.component = component;
+            componentStateCaptor = component.GetStateCaptor();
+            startState = componentStateCaptor.GetComponentState(component);
+
+            this.time = time;
+            this.tweenParameters = tweenParameters;
+        }
+
+        public void Dispose() {
+            ComponentState endState = componentStateCaptor.GetComponentState(component);
+
+            ComponentStateDelta delta = componentStateCaptor.GenerateComponentStateDelta(startState, endState);
+
+            Animation animation = component.gameObject.GetComponent<Animation>();
+            if (animation == null) {
+                animation = component.gameObject.AddComponent<Animation>();
+            }
+
+            string currentClipName = "jurassicTween";
+            AnimationClip animationClip = animation.GetClip(currentClipName);
+            if (animationClip == null) {
+                animationClip = new AnimationClip();
+                animationClip.legacy = true;
+                animationClip.name = currentClipName;
+                animation.AddClip(animationClip, animationClip.name);
+            }
+
+            foreach (string key in delta.rangeByAnimationKey.Keys) {
+                ComponentStateDelta.Range range = delta.rangeByAnimationKey[key];
+
+                AnimationCurve curve;
+                switch (tweenParameters.interpolation) {
+                case TweenParameters.Interpolation.Linear:
+                    curve = AnimationCurve.Linear(0, range.start, time, range.end);
+                    break;
+
+                case TweenParameters.Interpolation.EaseInOut:
+                    curve = AnimationCurve.EaseInOut(0, range.start, time, range.end);
+                    break;
+
+                case TweenParameters.Interpolation.CustomCurve:
+                    curve = new AnimationCurve();
+                    foreach (Keyframe referenceKeyframe in tweenParameters.customCurve.keys) {
+                        Keyframe keyframe = new Keyframe(
+                            Mathf.Lerp(0.0f, time, referenceKeyframe.time),
+                            Mathf.Lerp(range.start, range.end, referenceKeyframe.value),
+                            referenceKeyframe.inTangent,
+                            referenceKeyframe.outTangent,
+                            referenceKeyframe.inWeight,
+                            referenceKeyframe.outWeight);
+                        curve.AddKey(keyframe);
+                    }
+                    break;
+
+                default:
+                    throw new Exception("We shouldn't be here");
+                }
+
+                animationClip.SetCurve("", component.GetType(), key, curve);
+            }
+
+            animation.Play(animationClip.name);
+        }
+    }
+}
diff --git a/Scripts/Runtime/Internal/EngineComponentStateCaptor.cs b/Scripts/Runtime/Internal/EngineComponentStateCaptor.cs
new file mode 100644 (file)
index 0000000..8beda1f
--- /dev/null
@@ -0,0 +1,59 @@
+//
+// EngineComponentStateCaptor.cs
+// JurassicTween
+//
+// Created by Benjamin Sherratt on 04/05/2021.
+// Copyright © 2021 SKFX Ltd. All rights reserved.
+//
+
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace JurassicTween.Internal {
+    public class TransformComponentStateCaptor : IComponentStateCaptor {
+        static string[] animationKeys = {
+            "localPosition.x",
+            "localPosition.y",
+            "localPosition.z",
+            "localRotation.x",
+            "localRotation.y",
+            "localRotation.z",
+            "localRotation.w",
+            "localScale.x",
+            "localScale.y",
+            "localScale.z",
+        };
+
+        public ComponentState GetComponentState(Component component) {
+            Transform transform = (Transform)component;
+
+            float[] stateValues = new float[10];
+            stateValues[0] = transform.localPosition.x;
+            stateValues[1] = transform.localPosition.y;
+            stateValues[2] = transform.localPosition.z;
+            stateValues[3] = transform.localRotation.x;
+            stateValues[4] = transform.localRotation.y;
+            stateValues[5] = transform.localRotation.z;
+            stateValues[6] = transform.localRotation.w;
+            stateValues[7] = transform.localScale.x;
+            stateValues[8] = transform.localScale.y;
+            stateValues[9] = transform.localScale.z;
+
+            ComponentState state = new ComponentState(stateValues);
+            return state;
+        }
+
+        public ComponentStateDelta GenerateComponentStateDelta(ComponentState startState, ComponentState endState) {
+            Dictionary<string, ComponentStateDelta.Range> rangeByAnimationKey = new Dictionary<string, ComponentStateDelta.Range>();
+
+            for (uint i = 0; i < animationKeys.Length; ++i) {
+                if (startState.values[i] != endState.values[i]) {
+                    rangeByAnimationKey[animationKeys[i]] = new ComponentStateDelta.Range(startState.values[i], endState.values[i]);
+                }
+            }
+
+            ComponentStateDelta delta = new ComponentStateDelta(rangeByAnimationKey);
+            return delta;
+        }
+    }
+}
diff --git a/Scripts/Runtime/Internal/GenericComponentStateCaptor.cs b/Scripts/Runtime/Internal/GenericComponentStateCaptor.cs
new file mode 100644 (file)
index 0000000..9b7a4d6
--- /dev/null
@@ -0,0 +1,103 @@
+//
+// GenericComponentStateCaptor.cs
+// JurassicTween
+//
+// Created by Benjamin Sherratt on 04/05/2021.
+// Copyright © 2021 SKFX Ltd. All rights reserved.
+//
+
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using UnityEngine;
+
+namespace JurassicTween.Internal {
+    public class GenericComponentStateCaptor : IComponentStateCaptor {
+        struct FieldCaptureInfo {
+            public string animationKeyPath;
+            public MemberInfo[] memberInfoHierarchy;
+
+            public FieldCaptureInfo(string animationKeyPath, MemberInfo[] memberInfoHierarchy) {
+                this.animationKeyPath = animationKeyPath;
+                this.memberInfoHierarchy = memberInfoHierarchy;
+            }
+        }
+
+        static FieldCaptureInfo[] QueryCaptureFields(Type type) {
+            List<FieldCaptureInfo> fieldCaptureInfo = new List<FieldCaptureInfo>();
+            List<string> keyComponents = new List<string>();
+            List<MemberInfo> memberInfoHierarchy = new List<MemberInfo>();
+            QueryCaptureFields(fieldCaptureInfo, type, keyComponents, memberInfoHierarchy);
+            return fieldCaptureInfo.ToArray();
+        }
+
+        static void QueryCaptureFields(List<FieldCaptureInfo> fieldCaptureInfo, Type type, List<string> keyComponents, List<MemberInfo> memberInfoHierarchy) {
+            FieldInfo[] allFieldInfo = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+            foreach (FieldInfo fieldInfo in allFieldInfo) {
+                if (fieldInfo.IsPublic || fieldInfo.GetCustomAttribute<SerializeField>() != null) {
+                    keyComponents.Add(fieldInfo.Name);
+                    memberInfoHierarchy.Add(fieldInfo);
+
+                    Type fieldType = fieldInfo.FieldType;
+                    if (fieldType.IsAssignableFrom(typeof(float))) {
+                        string animationKey = string.Join(".", keyComponents);
+                        FieldCaptureInfo info = new FieldCaptureInfo(animationKey, memberInfoHierarchy.ToArray());
+                        fieldCaptureInfo.Add(info);
+                    } else {
+                        QueryCaptureFields(fieldCaptureInfo, fieldType, keyComponents, memberInfoHierarchy);
+                    }
+
+                    memberInfoHierarchy.RemoveAt(memberInfoHierarchy.Count - 1);
+                    keyComponents.RemoveAt(keyComponents.Count - 1);
+                }
+            }
+        }
+
+        Type componentType;
+        FieldCaptureInfo[] fieldCaptureInformation;
+
+        public GenericComponentStateCaptor(Type componentType) {
+            this.componentType = componentType;
+            fieldCaptureInformation = QueryCaptureFields(componentType);
+        }
+
+        public ComponentState GetComponentState(Component component) {
+            float[] stateValues = new float[fieldCaptureInformation.Length];
+
+            for (uint i = 0; i < fieldCaptureInformation.Length; ++i) {
+                ref FieldCaptureInfo fieldCaptureInfo = ref fieldCaptureInformation[i];
+                object instanceScope = component;
+                foreach (MemberInfo memberInfo in fieldCaptureInfo.memberInfoHierarchy) {
+                    if ((memberInfo.MemberType & MemberTypes.Field) > 0) {
+                        instanceScope = ((FieldInfo)memberInfo).GetValue(instanceScope);
+                    } else if ((memberInfo.MemberType & MemberTypes.Property) > 0) {
+                        instanceScope = ((PropertyInfo)memberInfo).GetValue(instanceScope);
+                    } else {
+                        throw new Exception("We shouldn't be here...");
+                    }
+                }
+
+                float value = (float)instanceScope;
+                stateValues[i] = value;
+            }
+
+            ComponentState state = new ComponentState(stateValues);
+            return state;
+        }
+
+        public ComponentStateDelta GenerateComponentStateDelta(ComponentState startState, ComponentState endState) {
+            Dictionary<string, ComponentStateDelta.Range> rangeByAnimationKey = new Dictionary<string, ComponentStateDelta.Range>();
+            for (uint i = 0; i < fieldCaptureInformation.Length; ++i) {
+                float startValue = startState.values[i];
+                float endValue = endState.values[i];
+                if (startValue != endValue) {
+                    ref FieldCaptureInfo fieldCaptureInfo = ref fieldCaptureInformation[i];
+                    rangeByAnimationKey[fieldCaptureInfo.animationKeyPath] = new ComponentStateDelta.Range(startValue, endValue);
+                }
+            }
+
+            ComponentStateDelta delta = new ComponentStateDelta(rangeByAnimationKey);
+            return delta;
+        }
+    }
+}
diff --git a/Scripts/Runtime/Tween.cs b/Scripts/Runtime/Tween.cs
new file mode 100644 (file)
index 0000000..86b288e
--- /dev/null
@@ -0,0 +1,30 @@
+//
+// Tween.cs
+// JurassicTween
+//
+// Created by Benjamin Sherratt on 04/05/2021.
+// Copyright © 2021 SKFX Ltd. All rights reserved.
+//\r
+\r
+using JurassicTween.Internal;\r
+using System;\r
+using UnityEngine;
+
+namespace JurassicTween {
+    public static class JurassicTween {
+        public static IDisposable Tween(this Component component, float time = 1.0f) {
+            return component.Tween(time, TweenParameters.linear);
+        }
+
+        public static IDisposable Tween(this Component component, float time, TweenParameters tweenParameters) {
+            Context context = new Context(component, time, tweenParameters);
+            return context;
+        }
+
+        public static bool IsTweening(this GameObject gameObject) {
+            Animation animation = gameObject.GetComponent<Animation>();
+            bool tweening = ((animation != null) && animation.isPlaying);
+            return tweening;
+        }
+    }
+}
diff --git a/Scripts/Runtime/TweenParameters.cs b/Scripts/Runtime/TweenParameters.cs
new file mode 100644 (file)
index 0000000..9b541d1
--- /dev/null
@@ -0,0 +1,35 @@
+//
+// TweenParameters.cs
+// JurassicTween
+//
+// Created by Benjamin Sherratt on 04/05/2021.
+// Copyright © 2021 SKFX Ltd. All rights reserved.
+//
+
+using UnityEngine;
+
+namespace JurassicTween {
+    public struct TweenParameters {
+        public static readonly TweenParameters linear = new TweenParameters(Interpolation.Linear);
+        public static readonly TweenParameters easeInOut = new TweenParameters(Interpolation.EaseInOut);
+
+        public enum Interpolation {
+            Linear,
+            EaseInOut,
+            CustomCurve,
+        }
+
+        public Interpolation interpolation;
+        public AnimationCurve customCurve;
+
+        public TweenParameters(AnimationCurve customCurve) {
+            interpolation = Interpolation.CustomCurve;
+            this.customCurve = customCurve;
+        }
+
+        TweenParameters(Interpolation interpolation) {
+            this.interpolation = interpolation;
+            this.customCurve = null;
+        }
+    }
+}