diff --git a/spine-csharp/src/AnimationState.cs b/spine-csharp/src/AnimationState.cs
index e1f05be75..4988d73e3 100644
--- a/spine-csharp/src/AnimationState.cs
+++ b/spine-csharp/src/AnimationState.cs
@@ -40,7 +40,7 @@ namespace Spine {
/// See Applying Animations in the Spine Runtimes Guide.
///
public class AnimationState {
- static readonly Animation EmptyAnimation = new Animation("", new ExposedList(), 0);
+ internal static readonly Animation EmptyAnimation = new Animation("", new ExposedList(), 0);
/// 1) A previously applied timeline has set this property.
/// Result: Mix from the current pose to the timeline pose.
@@ -309,9 +309,11 @@ namespace Spine {
return applied;
}
- /// Version of only applying EventTimelines for lightweight off-screen updates.
+ /// Version of only applying and updating time at
+ /// EventTimelines for lightweight off-screen updates.
+ /// When set to false, only animation times of TrackEntries are updated.
// Note: This method is not part of the libgdx reference implementation.
- public bool ApplyEventTimelinesOnly (Skeleton skeleton) {
+ public bool ApplyEventTimelinesOnly (Skeleton skeleton, bool issueEvents = true) {
if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null.");
ExposedList events = this.events;
@@ -323,24 +325,28 @@ namespace Spine {
applied = true;
// Apply mixing from entries first.
- if (current.mixingFrom != null) ApplyMixingFromEventTimelinesOnly(current, skeleton);
+ if (current.mixingFrom != null) ApplyMixingFromEventTimelinesOnly(current, skeleton, issueEvents);
// Apply current entry.
float animationLast = current.animationLast, animationTime = current.AnimationTime;
- int timelineCount = current.animation.timelines.Count;
- Timeline[] timelines = current.animation.timelines.Items;
- for (int ii = 0; ii < timelineCount; ii++) {
- Timeline timeline = timelines[ii];
- if (timeline is EventTimeline)
- timeline.Apply(skeleton, animationLast, animationTime, events, 1.0f, MixBlend.Setup, MixDirection.In);
+
+ if (issueEvents) {
+ int timelineCount = current.animation.timelines.Count;
+ Timeline[] timelines = current.animation.timelines.Items;
+ for (int ii = 0; ii < timelineCount; ii++) {
+ Timeline timeline = timelines[ii];
+ if (timeline is EventTimeline)
+ timeline.Apply(skeleton, animationLast, animationTime, events, 1.0f, MixBlend.Setup, MixDirection.In);
+ }
+ QueueEvents(current, animationTime);
+ events.Clear(false);
}
- QueueEvents(current, animationTime);
- events.Clear(false);
current.nextAnimationLast = animationTime;
current.nextTrackLast = current.trackTime;
}
- queue.Drain();
+ if (issueEvents)
+ queue.Drain();
return applied;
}
@@ -434,11 +440,14 @@ namespace Spine {
return mix;
}
- /// Version of only applying EventTimelines for lightweight off-screen updates.
+ /// Version of only applying and updating time at
+ /// EventTimelines for lightweight off-screen updates.
+ /// When set to false, only animation times of TrackEntries are updated.
// Note: This method is not part of the libgdx reference implementation.
- private float ApplyMixingFromEventTimelinesOnly (TrackEntry to, Skeleton skeleton) {
+ private float ApplyMixingFromEventTimelinesOnly (TrackEntry to, Skeleton skeleton, bool issueEvents) {
TrackEntry from = to.mixingFrom;
- if (from.mixingFrom != null) ApplyMixingFromEventTimelinesOnly(from, skeleton);
+ if (from.mixingFrom != null) ApplyMixingFromEventTimelinesOnly(from, skeleton, issueEvents);
+
float mix;
if (to.mixDuration == 0) { // Single frame mix to undo mixingFrom changes.
@@ -452,16 +461,18 @@ namespace Spine {
if (eventBuffer == null) return mix;
float animationLast = from.animationLast, animationTime = from.AnimationTime;
- int timelineCount = from.animation.timelines.Count;
- Timeline[] timelines = from.animation.timelines.Items;
- for (int i = 0; i < timelineCount; i++) {
- Timeline timeline = timelines[i];
- if (timeline is EventTimeline)
- timeline.Apply(skeleton, animationLast, animationTime, eventBuffer, 0, MixBlend.Setup, MixDirection.Out);
- }
+ if (issueEvents) {
+ int timelineCount = from.animation.timelines.Count;
+ Timeline[] timelines = from.animation.timelines.Items;
+ for (int i = 0; i < timelineCount; i++) {
+ Timeline timeline = timelines[i];
+ if (timeline is EventTimeline)
+ timeline.Apply(skeleton, animationLast, animationTime, eventBuffer, 0, MixBlend.Setup, MixDirection.Out);
+ }
- if (to.mixDuration > 0) QueueEvents(from, animationTime);
- this.events.Clear(false);
+ if (to.mixDuration > 0) QueueEvents(from, animationTime);
+ this.events.Clear(false);
+ }
from.nextAnimationLast = animationTime;
from.nextTrackLast = from.trackTime;
@@ -1242,6 +1253,10 @@ namespace Spine {
/// If true, the animation will be applied in reverse. Events are not fired when an animation is applied in reverse.
public bool Reverse { get { return reverse; } set { reverse = value; } }
+ /// Returns true if this entry is for the empty animation. See ,
+ /// , and .
+ public bool IsEmptyAnimation { get { return animation == AnimationState.EmptyAnimation; } }
+
///
///
/// Resets the rotation directions for mixing this entry's rotate timelines. This can be useful to avoid bones rotating the
diff --git a/spine-csharp/src/Bone.cs b/spine-csharp/src/Bone.cs
index c6588417a..82e1bd2c5 100644
--- a/spine-csharp/src/Bone.cs
+++ b/spine-csharp/src/Bone.cs
@@ -274,7 +274,7 @@ namespace Spine {
/// Some information is ambiguous in the world transform, such as -1,-1 scale versus 180 rotation. The applied transform after
/// calling this method is equivalent to the local transform used to compute the world transform, but may not be identical.
///
- internal void UpdateAppliedTransform () {
+ public void UpdateAppliedTransform () {
Bone parent = this.parent;
if (parent == null) {
ax = worldX - skeleton.x;
diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SkeletonDataCompatibility.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SkeletonDataCompatibility.cs
index a249321d0..59b2b238b 100644
--- a/spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SkeletonDataCompatibility.cs
+++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Asset Types/SkeletonDataCompatibility.cs
@@ -173,11 +173,16 @@ namespace Spine.Unity {
int i = 0;
if (content.Length >= 3 && content[0] == 0xEF && content[1] == 0xBB && content[2] == 0xBF) // skip potential BOM
i = 3;
+ bool openingBraceFound = false;
for (; i < numCharsToCheck; ++i) {
char c = (char)content[i];
if (char.IsWhiteSpace(c))
continue;
- return c == '{';
+ if (!openingBraceFound) {
+ if (c == '{') openingBraceFound = true;
+ else return false;
+ } else
+ return c == '"';
}
return true;
}
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 01e0915d0..82bf04ef7 100644
--- a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonAnimation.cs
+++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonAnimation.cs
@@ -206,8 +206,10 @@ namespace Spine.Unity {
return;
UpdateAnimationStatus(deltaTime);
- if (updateMode == UpdateMode.OnlyAnimationStatus)
+ if (updateMode == UpdateMode.OnlyAnimationStatus) {
+ state.ApplyEventTimelinesOnly(skeleton, issueEvents: false);
return;
+ }
ApplyAnimation();
}
@@ -224,7 +226,7 @@ namespace Spine.Unity {
if (updateMode != UpdateMode.OnlyEventTimelines)
state.Apply(skeleton);
else
- state.ApplyEventTimelinesOnly(skeleton);
+ state.ApplyEventTimelinesOnly(skeleton, issueEvents: true);
if (_UpdateLocal != null)
_UpdateLocal(this);
@@ -246,6 +248,18 @@ namespace Spine.Unity {
if (!wasUpdatedAfterInit) Update(0);
base.LateUpdate();
}
+
+ public override void OnBecameVisible () {
+ UpdateMode previousUpdateMode = updateMode;
+ updateMode = UpdateMode.FullUpdate;
+
+ // OnBecameVisible is called after LateUpdate()
+ if (previousUpdateMode != UpdateMode.FullUpdate &&
+ previousUpdateMode != UpdateMode.EverythingExceptMesh)
+ Update(0);
+ if (previousUpdateMode != UpdateMode.FullUpdate)
+ LateUpdate();
+ }
}
}
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 731836b64..f09291124 100644
--- a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonGraphic.cs
+++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonGraphic.cs
@@ -265,10 +265,13 @@ namespace Spine.Unity {
wasUpdatedAfterInit = true;
if (updateMode < UpdateMode.OnlyAnimationStatus)
return;
+
UpdateAnimationStatus(deltaTime);
- if (updateMode == UpdateMode.OnlyAnimationStatus)
+ if (updateMode == UpdateMode.OnlyAnimationStatus) {
+ state.ApplyEventTimelinesOnly(skeleton, issueEvents: false);
return;
+ }
ApplyAnimation();
}
@@ -303,7 +306,7 @@ namespace Spine.Unity {
if (updateMode != UpdateMode.OnlyEventTimelines)
state.Apply(skeleton);
else
- state.ApplyEventTimelinesOnly(skeleton);
+ state.ApplyEventTimelinesOnly(skeleton, issueEvents: true);
if (UpdateLocal != null)
UpdateLocal(this);
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 b0b2efe7b..3deb15568 100644
--- a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs
+++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs
@@ -140,6 +140,18 @@ namespace Spine.Unity {
base.LateUpdate();
}
+ public override void OnBecameVisible () {
+ UpdateMode previousUpdateMode = updateMode;
+ updateMode = UpdateMode.FullUpdate;
+
+ // OnBecameVisible is called after LateUpdate()
+ if (previousUpdateMode != UpdateMode.FullUpdate &&
+ previousUpdateMode != UpdateMode.EverythingExceptMesh)
+ Update();
+ if (previousUpdateMode != UpdateMode.FullUpdate)
+ LateUpdate();
+ }
+
[System.Serializable]
public class MecanimTranslator {
diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonRenderer.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonRenderer.cs
index 5a6cd51b4..7bbcf08bc 100644
--- a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonRenderer.cs
+++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonRenderer.cs
@@ -554,11 +554,13 @@ namespace Spine.Unity {
OnMeshAndMaterialsUpdated(this);
}
- public void OnBecameVisible () {
+ public virtual void OnBecameVisible () {
UpdateMode previousUpdateMode = updateMode;
updateMode = UpdateMode.FullUpdate;
+
+ // OnBecameVisible is called after LateUpdate()
if (previousUpdateMode != UpdateMode.FullUpdate)
- LateUpdate(); // OnBecameVisible is called after LateUpdate()
+ LateUpdate();
}
public void OnBecameInvisible () {
diff --git a/spine-unity/Modules/com.esotericsoftware.spine.timeline/Editor/SpineAnimationStateClipInspector.cs b/spine-unity/Modules/com.esotericsoftware.spine.timeline/Editor/SpineAnimationStateClipInspector.cs
index e280ed3de..c160d2d74 100644
--- a/spine-unity/Modules/com.esotericsoftware.spine.timeline/Editor/SpineAnimationStateClipInspector.cs
+++ b/spine-unity/Modules/com.esotericsoftware.spine.timeline/Editor/SpineAnimationStateClipInspector.cs
@@ -79,7 +79,7 @@ namespace Spine.Unity.Editor {
if (timelineClip == null)
return;
- float blendInDur = (float)timelineClip.blendInDuration;
+ float blendInDur = System.Math.Max((float)timelineClip.blendInDuration, (float)timelineClip.easeInDuration);
bool isBlendingNow = blendInDur > 0;
bool wasBlendingBefore = timelineClipInfo.previousBlendInDuration > 0;
diff --git a/spine-unity/Modules/com.esotericsoftware.spine.timeline/Runtime/SpineAnimationState/SpineAnimationStateMixerBehaviour.cs b/spine-unity/Modules/com.esotericsoftware.spine.timeline/Runtime/SpineAnimationState/SpineAnimationStateMixerBehaviour.cs
index c5bdefbae..aa9fa5086 100644
--- a/spine-unity/Modules/com.esotericsoftware.spine.timeline/Runtime/SpineAnimationState/SpineAnimationStateMixerBehaviour.cs
+++ b/spine-unity/Modules/com.esotericsoftware.spine.timeline/Runtime/SpineAnimationState/SpineAnimationStateMixerBehaviour.cs
@@ -38,8 +38,10 @@ namespace Spine.Unity.Playables {
public class SpineAnimationStateMixerBehaviour : PlayableBehaviour {
float[] lastInputWeights;
- bool lastAnyTrackPlaying = false;
+ bool lastAnyClipPlaying = false;
public int trackIndex;
+ ScriptPlayable[] startingClips
+ = new ScriptPlayable[2];
IAnimationStateComponent animationStateComponent;
bool pauseWithDirector = true;
@@ -135,60 +137,81 @@ namespace Spine.Unity.Playables {
this.lastInputWeights[i] = default(float);
}
var lastInputWeights = this.lastInputWeights;
- bool anyTrackPlaying = false;
+ int numStartingClips = 0;
+ bool anyClipPlaying = false;
// Check all clips. If a clip that was weight 0 turned into weight 1, call SetAnimation.
for (int i = 0; i < inputCount; i++) {
float lastInputWeight = lastInputWeights[i];
float inputWeight = playable.GetInputWeight(i);
- bool trackStarted = lastInputWeight == 0 && inputWeight > 0;
+ bool clipStarted = lastInputWeight == 0 && inputWeight > 0;
if (inputWeight > 0)
- anyTrackPlaying = true;
+ anyClipPlaying = true;
lastInputWeights[i] = inputWeight;
- if (trackStarted) {
- ScriptPlayable inputPlayable = (ScriptPlayable)playable.GetInput(i);
- SpineAnimationStateBehaviour clipData = inputPlayable.GetBehaviour();
-
- pauseWithDirector = !clipData.dontPauseWithDirector;
- endAtClipEnd = !clipData.dontEndWithClip;
- endMixOutDuration = clipData.endMixOutDuration;
-
- if (clipData.animationReference == null) {
- float mixDuration = clipData.customDuration ? clipData.mixDuration : state.Data.DefaultMix;
- state.SetEmptyAnimation(trackIndex, mixDuration);
- } else {
- if (clipData.animationReference.Animation != null) {
- Spine.TrackEntry trackEntry = state.SetAnimation(trackIndex, clipData.animationReference.Animation, clipData.loop);
-
- trackEntry.EventThreshold = clipData.eventThreshold;
- trackEntry.DrawOrderThreshold = clipData.drawOrderThreshold;
- trackEntry.TrackTime = (float)inputPlayable.GetTime() * (float)inputPlayable.GetSpeed();
- trackEntry.TimeScale = (float)inputPlayable.GetSpeed();
- trackEntry.AttachmentThreshold = clipData.attachmentThreshold;
- trackEntry.HoldPrevious = clipData.holdPrevious;
-
- if (clipData.customDuration)
- trackEntry.MixDuration = clipData.mixDuration;
-
- timelineStartedTrackEntry = trackEntry;
- }
- //else Debug.LogWarningFormat("Animation named '{0}' not found", clipData.animationName);
- }
-
- // Ensure that the first frame ends with an updated mesh.
- if (skeletonAnimation) {
- skeletonAnimation.Update(0);
- skeletonAnimation.LateUpdate();
- } else if (skeletonGraphic) {
- skeletonGraphic.Update(0);
- skeletonGraphic.LateUpdate();
- }
+ if (clipStarted && numStartingClips < 2) {
+ ScriptPlayable clipPlayable = (ScriptPlayable)playable.GetInput(i);
+ startingClips[numStartingClips++] = clipPlayable;
}
}
- if (lastAnyTrackPlaying && !anyTrackPlaying)
+ // unfortunately order of clips can be wrong when two start at the same time, we have to sort clips
+ if (numStartingClips == 2) {
+ ScriptPlayable clipPlayable0 = startingClips[0];
+ ScriptPlayable clipPlayable1 = startingClips[1];
+ if (clipPlayable0.GetDuration() > clipPlayable1.GetDuration()) { // swap, clip 0 ends after clip 1
+ startingClips[0] = clipPlayable1;
+ startingClips[1] = clipPlayable0;
+ }
+ }
+
+ for (int j = 0; j < numStartingClips; ++j) {
+ ScriptPlayable clipPlayable = startingClips[j];
+ SpineAnimationStateBehaviour clipData = clipPlayable.GetBehaviour();
+ pauseWithDirector = !clipData.dontPauseWithDirector;
+ endAtClipEnd = !clipData.dontEndWithClip;
+ endMixOutDuration = clipData.endMixOutDuration;
+
+ if (clipData.animationReference == null) {
+ float mixDuration = clipData.customDuration ? clipData.mixDuration : state.Data.DefaultMix;
+ state.SetEmptyAnimation(trackIndex, mixDuration);
+ } else {
+ if (clipData.animationReference.Animation != null) {
+ TrackEntry currentEntry = state.GetCurrent(trackIndex);
+ Spine.TrackEntry trackEntry;
+ if (currentEntry == null && (clipData.customDuration && clipData.mixDuration > 0)) {
+ state.SetEmptyAnimation(trackIndex, 0); // ease in requires empty animation
+ trackEntry = state.AddAnimation(trackIndex, clipData.animationReference.Animation, clipData.loop, 0);
+ } else
+ trackEntry = state.SetAnimation(trackIndex, clipData.animationReference.Animation, clipData.loop);
+
+ trackEntry.EventThreshold = clipData.eventThreshold;
+ trackEntry.DrawOrderThreshold = clipData.drawOrderThreshold;
+ trackEntry.TrackTime = (float)clipPlayable.GetTime() * (float)clipPlayable.GetSpeed();
+ trackEntry.TimeScale = (float)clipPlayable.GetSpeed();
+ trackEntry.AttachmentThreshold = clipData.attachmentThreshold;
+ trackEntry.HoldPrevious = clipData.holdPrevious;
+
+ if (clipData.customDuration)
+ trackEntry.MixDuration = clipData.mixDuration;
+
+ timelineStartedTrackEntry = trackEntry;
+ }
+ //else Debug.LogWarningFormat("Animation named '{0}' not found", clipData.animationName);
+ }
+
+ // Ensure that the first frame ends with an updated mesh.
+ if (skeletonAnimation) {
+ skeletonAnimation.Update(0);
+ skeletonAnimation.LateUpdate();
+ } else if (skeletonGraphic) {
+ skeletonGraphic.Update(0);
+ skeletonGraphic.LateUpdate();
+ }
+ }
+ startingClips[0] = startingClips[1] = ScriptPlayable.Null;
+ if (lastAnyClipPlaying && !anyClipPlaying)
HandleClipEnd();
- this.lastAnyTrackPlaying = anyTrackPlaying;
+ this.lastAnyClipPlaying = anyClipPlaying;
}
#if SPINE_EDITMODEPOSE
@@ -251,25 +274,25 @@ namespace Spine.Unity.Playables {
if (fromAnimation != null && mixDuration > 0 && toClipTime < mixDuration) {
dummyAnimationState = dummyAnimationState ?? new AnimationState(skeletonComponent.SkeletonDataAsset.GetAnimationStateData());
- var toTrack = dummyAnimationState.GetCurrent(0);
- var fromTrack = toTrack != null ? toTrack.MixingFrom : null;
- bool isAnimationTransitionMatch = (toTrack != null && toTrack.Animation == toAnimation && fromTrack != null && fromTrack.Animation == fromAnimation);
+ var toEntry = dummyAnimationState.GetCurrent(0);
+ var fromEntry = toEntry != null ? toEntry.MixingFrom : null;
+ bool isAnimationTransitionMatch = (toEntry != null && toEntry.Animation == toAnimation && fromEntry != null && fromEntry.Animation == fromAnimation);
if (!isAnimationTransitionMatch) {
dummyAnimationState.ClearTracks();
- fromTrack = dummyAnimationState.SetAnimation(0, fromAnimation, fromClipLoop);
- fromTrack.AllowImmediateQueue();
+ fromEntry = dummyAnimationState.SetAnimation(0, fromAnimation, fromClipLoop);
+ fromEntry.AllowImmediateQueue();
if (toAnimation != null) {
- toTrack = dummyAnimationState.SetAnimation(0, toAnimation, clipData.loop);
- toTrack.HoldPrevious = clipData.holdPrevious;
+ toEntry = dummyAnimationState.SetAnimation(0, toAnimation, clipData.loop);
+ toEntry.HoldPrevious = clipData.holdPrevious;
}
}
// Update track times.
- fromTrack.TrackTime = fromClipTime;
- if (toTrack != null) {
- toTrack.TrackTime = toClipTime;
- toTrack.MixTime = toClipTime;
+ fromEntry.TrackTime = fromClipTime;
+ if (toEntry != null) {
+ toEntry.TrackTime = toClipTime;
+ toEntry.MixTime = toClipTime;
}
// Apply Pose
diff --git a/spine-unity/Modules/com.esotericsoftware.spine.timeline/package-no-spine-unity-dependency.json b/spine-unity/Modules/com.esotericsoftware.spine.timeline/package-no-spine-unity-dependency.json
index 3d231ddbe..48028d42e 100644
--- a/spine-unity/Modules/com.esotericsoftware.spine.timeline/package-no-spine-unity-dependency.json
+++ b/spine-unity/Modules/com.esotericsoftware.spine.timeline/package-no-spine-unity-dependency.json
@@ -2,7 +2,7 @@
"name": "com.esotericsoftware.spine.timeline",
"displayName": "Spine Timeline Extensions",
"description": "This plugin provides integration of spine-unity for the Unity Timeline.\n\nPrerequisites:\nIt requires a working installation of the spine-unity runtime (via the spine-unity unitypackage), version 4.0.\n(See http://esotericsoftware.com/git/spine-runtimes/spine-unity)",
- "version": "4.0.5",
+ "version": "4.0.6",
"unity": "2018.3",
"author": {
"name": "Esoteric Software",
diff --git a/spine-unity/Modules/com.esotericsoftware.spine.timeline/package.json b/spine-unity/Modules/com.esotericsoftware.spine.timeline/package.json
index f03c692b2..2110d205b 100644
--- a/spine-unity/Modules/com.esotericsoftware.spine.timeline/package.json
+++ b/spine-unity/Modules/com.esotericsoftware.spine.timeline/package.json
@@ -2,7 +2,7 @@
"name": "com.esotericsoftware.spine.timeline",
"displayName": "Spine Timeline Extensions",
"description": "This plugin provides integration of spine-unity for the Unity Timeline.\n\nPrerequisites:\nIt requires a working installation of the spine-unity and spine-csharp runtimes as UPM packages (not as spine-unity unitypackage), version 4.0.\n(See http://esotericsoftware.com/git/spine-runtimes/spine-unity)",
- "version": "4.0.5",
+ "version": "4.0.6",
"unity": "2018.3",
"author": {
"name": "Esoteric Software",