From: Ben Sherratt Date: Tue, 4 May 2021 15:58:49 +0000 (+0100) Subject: [FEATURE] Initial Release X-Git-Url: https://git.bts.cx/jurassic-tween.git/commitdiff_plain/23598d04c8c1c67c6b589fb647c67d1d0c82eade?ds=inline [FEATURE] Initial Release * Created base framework * Added generic captor for user-created MonoBehavior components * Added special captor for Transform component --- 23598d04c8c1c67c6b589fb647c67d1d0c82eade diff --git a/README.md b/README.md new file mode 100644 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 index 0000000..ebf4adb --- /dev/null +++ b/Scripts/Runtime/Internal/ComponentStateCaptor.cs @@ -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 rangeByAnimationKey; + + public ComponentStateDelta(Dictionary 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 stateCaptorByType; + + public static IComponentStateCaptor GetStateCaptor(this Component component) { + Type componentType = component.GetType(); + + if (stateCaptorByType == null) { + stateCaptorByType = new Dictionary(); + 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 index 0000000..785a1d5 --- /dev/null +++ b/Scripts/Runtime/Internal/Context.cs @@ -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(); + if (animation == null) { + animation = component.gameObject.AddComponent(); + } + + 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 index 0000000..8beda1f --- /dev/null +++ b/Scripts/Runtime/Internal/EngineComponentStateCaptor.cs @@ -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 rangeByAnimationKey = new Dictionary(); + + 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 index 0000000..9b7a4d6 --- /dev/null +++ b/Scripts/Runtime/Internal/GenericComponentStateCaptor.cs @@ -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 = new List(); + List keyComponents = new List(); + List memberInfoHierarchy = new List(); + QueryCaptureFields(fieldCaptureInfo, type, keyComponents, memberInfoHierarchy); + return fieldCaptureInfo.ToArray(); + } + + static void QueryCaptureFields(List fieldCaptureInfo, Type type, List keyComponents, List memberInfoHierarchy) { + FieldInfo[] allFieldInfo = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + foreach (FieldInfo fieldInfo in allFieldInfo) { + if (fieldInfo.IsPublic || fieldInfo.GetCustomAttribute() != 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 rangeByAnimationKey = new Dictionary(); + 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 index 0000000..86b288e --- /dev/null +++ b/Scripts/Runtime/Tween.cs @@ -0,0 +1,30 @@ +// +// Tween.cs +// JurassicTween +// +// Created by Benjamin Sherratt on 04/05/2021. +// Copyright © 2021 SKFX Ltd. All rights reserved. +// + +using JurassicTween.Internal; +using System; +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(); + bool tweening = ((animation != null) && animation.isPlaying); + return tweening; + } + } +} diff --git a/Scripts/Runtime/TweenParameters.cs b/Scripts/Runtime/TweenParameters.cs new file mode 100644 index 0000000..9b541d1 --- /dev/null +++ b/Scripts/Runtime/TweenParameters.cs @@ -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; + } + } +}