diff --git a/spine-c/spine-c/include/spine/AnimationState.h b/spine-c/spine-c/include/spine/AnimationState.h index a2d68500d..7104e0a4a 100644 --- a/spine-c/spine-c/include/spine/AnimationState.h +++ b/spine-c/spine-c/include/spine/AnimationState.h @@ -61,6 +61,8 @@ struct spTrackEntry { float alpha, mixTime, mixDuration, mixAlpha; int* /*boolean*/ timelinesFirst; int timelinesFirstCount; + int* /*boolean*/ timelinesLast; + int timelinesLastCount; float* timelinesRotation; int timelinesRotationCount; void* rendererObject; @@ -79,6 +81,8 @@ struct spTrackEntry { alpha(0), mixTime(0), mixDuration(0), mixAlpha(0), timelinesFirst(0), timelinesFirstCount(0), + timelinesLast(0), + timelinesLastCount(0), timelinesRotation(0), timelinesRotationCount(0) { } @@ -95,6 +99,8 @@ struct spAnimationState { float timeScale; + int /*boolean*/ multipleMixing; + void* rendererObject; #ifdef __cplusplus @@ -103,7 +109,9 @@ struct spAnimationState { tracksCount(0), tracks(0), listener(0), - timeScale(0) { + timeScale(0), + multipleMixing(0), + rendererObject(0) { } #endif }; diff --git a/spine-c/spine-c/src/spine/AnimationState.c b/spine-c/spine-c/src/spine/AnimationState.c index cc3a30713..c2592c3b5 100644 --- a/spine-c/spine-c/src/spine/AnimationState.c +++ b/spine-c/spine-c/src/spine/AnimationState.c @@ -57,7 +57,7 @@ void _spAnimationState_ensureCapacityPropertyIDs(spAnimationState* self, int cap int _spAnimationState_addPropertyID(spAnimationState* self, int id); void _spAnimationState_setTimelinesFirst (spAnimationState* self, spTrackEntry* entry); void _spAnimationState_checkTimelinesFirst (spAnimationState* self, spTrackEntry* entry); -void _spAnimationState_checkTimelinesUsage (spAnimationState* self, spTrackEntry* entry); +void _spAnimationState_checkTimelinesUsage (spAnimationState* self, spTrackEntry* entry, int /*boolean*/ useTimelinesFirst); _spEventQueue* _spEventQueue_create (_spAnimationState* state) { _spEventQueue *self = CALLOC(_spEventQueue, 1); @@ -176,6 +176,7 @@ void _spEventQueue_drain (_spEventQueue* self) { void _spAnimationState_disposeTrackEntry (spTrackEntry* entry) { FREE(entry->timelinesFirst); + FREE(entry->timelinesLast); FREE(entry->timelinesRotation); FREE(entry); } @@ -370,6 +371,9 @@ float _spAnimationState_applyMixingFrom (spAnimationState* self, spTrackEntry* e int timelineCount; spTimeline** timelines; int* timelinesFirst; + int* timelinesLast; + float alphaBase; + float alphaMix; float alpha; int /*boolean*/ firstFrame; float* timelinesRotation; @@ -395,7 +399,9 @@ float _spAnimationState_applyMixingFrom (spAnimationState* self, spTrackEntry* e timelineCount = from->animation->timelinesCount; timelines = from->animation->timelines; timelinesFirst = from->timelinesFirst; - alpha = from->alpha * entry->mixAlpha * (1 - mix); + timelinesLast = self->multipleMixing ? 0 : from->timelinesLast; + alphaBase = from->alpha * entry->mixAlpha; + alphaMix = alphaBase * (1 - mix); firstFrame = from->timelinesRotationCount == 0; if (firstFrame) _spAnimationState_resizeTimelinesRotation(from, timelineCount << 1); @@ -404,6 +410,7 @@ float _spAnimationState_applyMixingFrom (spAnimationState* self, spTrackEntry* e for (i = 0; i < timelineCount; i++) { timeline = timelines[i]; setupPose = timelinesFirst[i]; + alpha = timelinesLast != 0 && setupPose && !timelinesLast[i] ? alphaBase : alphaMix; if (timeline->type == SP_TIMELINE_ROTATE) _spAnimationState_applyRotateTimeline(self, timeline, skeleton, animationTime, alpha, setupPose, timelinesRotation, i << 1, firstFrame); else { @@ -574,6 +581,7 @@ void spAnimationState_clearTrack (spAnimationState* self, int trackIndex) { void _spAnimationState_setCurrent (spAnimationState* self, int index, spTrackEntry* current, int /*boolean*/ interrupt) { _spAnimationState* internal = SUB_CAST(_spAnimationState, self); spTrackEntry* from = _spAnimationState_expandToIndex(self, index); + spTrackEntry* mixingFrom = 0; self->tracks[index] = current; if (from) { @@ -581,10 +589,26 @@ void _spAnimationState_setCurrent (spAnimationState* self, int index, spTrackEnt current->mixingFrom = from; current->mixTime = 0; - from->timelinesRotationCount = 0; + mixingFrom = from->mixingFrom; + if (mixingFrom != 0 && from->mixDuration > 0) { + if (self->multipleMixing && from->mixTime / from->mixDuration < 0.5 && mixingFrom->animation != SP_EMPTY_ANIMATION) { + current->mixingFrom = mixingFrom; + mixingFrom->mixingFrom = from; + mixingFrom->mixTime = from->mixDuration - from->mixTime; + mixingFrom->mixDuration = from->mixDuration; + from->mixingFrom = 0; + from = mixingFrom; + } - /* If not completely mixed in, set mixAlpha so mixing out happens from current mix to zero. */ - if (from->mixingFrom && from->mixDuration > 0) current->mixAlpha *= MIN(from->mixTime / from->mixDuration, 1); + current->mixAlpha *= MIN(from->mixTime / from->mixDuration, 1); + if (!self->multipleMixing) { + from->mixAlpha = 0; + from->mixTime = 0; + from->mixDuration = 0; + } + } + + from->timelinesRotationCount = 0; } _spEventQueue_start(internal->queue, current); @@ -740,8 +764,9 @@ void _spAnimationState_disposeNext (spAnimationState* self, spTrackEntry* entry) void _spAnimationState_animationsChanged (spAnimationState* self) { _spAnimationState* internal = SUB_CAST(_spAnimationState, self); - int i, n; + int i, n, ii, nn, lowestMixingFrom; spTrackEntry* entry; + spTimeline** timelines; internal->animationsChanged = 0; i = 0; n = self->tracksCount; @@ -758,6 +783,31 @@ void _spAnimationState_animationsChanged (spAnimationState* self) { entry = self->tracks[i]; if (entry) _spAnimationState_checkTimelinesFirst(self, entry); } + + if (self->multipleMixing) return; + + internal->propertyIDsCount = 0; + lowestMixingFrom = n; + for (i = 0; i < n; i++) { + entry = self->tracks[i]; + if (entry == 0 || entry->mixingFrom == 0) continue; + lowestMixingFrom = i; + break; + } + for (i = n - 1; i >= lowestMixingFrom; i--) { + entry = self->tracks[i]; + if (entry == 0) continue; + + timelines = entry->animation->timelines; + for (ii = 0, nn = entry->animation->timelinesCount; ii < nn; ii++) + _spAnimationState_addPropertyID(self, spTimeline_getPropertyId(timelines[ii])); + + entry = entry->mixingFrom; + while (entry != 0) { + _spAnimationState_checkTimelinesUsage(self, entry, 0); + entry = entry->mixingFrom; + } + } } float* _spAnimationState_resizeTimelinesRotation(spTrackEntry* entry, int newSize) { @@ -781,6 +831,17 @@ int* _spAnimationState_resizeTimelinesFirst(spTrackEntry* entry, int newSize) { return entry->timelinesFirst; } +int* _spAnimationState_resizeTimelinesLast(spTrackEntry* entry, int newSize) { + if (entry->timelinesLastCount != newSize) { + int* newTimelinesLast = CALLOC(int, newSize); + FREE(entry->timelinesLast); + entry->timelinesLast = newTimelinesLast; + entry->timelinesLastCount = newSize; + } + + return entry->timelinesLast; +} + void _spAnimationState_ensureCapacityPropertyIDs(spAnimationState* self, int capacity) { _spAnimationState* internal = SUB_CAST(_spAnimationState, self); if (internal->propertyIDsCapacity < capacity) { @@ -813,7 +874,7 @@ void _spAnimationState_setTimelinesFirst (spAnimationState* self, spTrackEntry* if (entry->mixingFrom) { _spAnimationState_setTimelinesFirst(self, entry->mixingFrom); - _spAnimationState_checkTimelinesUsage(self, entry); + _spAnimationState_checkTimelinesUsage(self, entry, -1); return; } @@ -828,16 +889,16 @@ void _spAnimationState_setTimelinesFirst (spAnimationState* self, spTrackEntry* void _spAnimationState_checkTimelinesFirst (spAnimationState* self, spTrackEntry* entry) { if (entry->mixingFrom) _spAnimationState_checkTimelinesFirst(self, entry->mixingFrom); - _spAnimationState_checkTimelinesUsage(self, entry); + _spAnimationState_checkTimelinesUsage(self, entry, -1); } -void _spAnimationState_checkTimelinesUsage (spAnimationState* self, spTrackEntry* entry) { +void _spAnimationState_checkTimelinesUsage (spAnimationState* self, spTrackEntry* entry, int /*boolean*/ useTimelinesFirst) { int i, n; int* usage; spTimeline** timelines; n = entry->animation->timelinesCount; timelines = entry->animation->timelines; - usage = _spAnimationState_resizeTimelinesFirst(entry, n); + usage = useTimelinesFirst ? _spAnimationState_resizeTimelinesFirst(entry, n) : _spAnimationState_resizeTimelinesLast(entry, n); for (i = 0; i < n; i++) usage[i] = _spAnimationState_addPropertyID(self, spTimeline_getPropertyId(timelines[i])); } diff --git a/spine-csharp/src/AnimationState.cs b/spine-csharp/src/AnimationState.cs index 8d89ccfd7..5511b89ae 100644 --- a/spine-csharp/src/AnimationState.cs +++ b/spine-csharp/src/AnimationState.cs @@ -40,7 +40,26 @@ namespace Spine { private readonly HashSet propertyIDs = new HashSet(); private readonly ExposedList events = new ExposedList(); private readonly EventQueue queue; + private bool animationsChanged; + private bool multipleMixing; + /// + /// When false, only two animations can be mixed at once. Interrupting a mix by setting a new animation will choose from the + /// two old animations the one that is closest to being fully mixed in and the other is discarded. Discarding an animation in + /// this way may cause keyed values to jump. + /// When true, any number of animations may be mixed at once without causing keyed values to jump. Mixing is done by mixing out + /// one or more animations while mixing in the newest one. When animations key the same value, this may cause "dipping", where + /// the value moves toward the setup pose as the old animation mixes out, then back to the keyed value as the new animation + /// mixes in. + /// Defaults to false. + public bool MultipleMixing { + get { return multipleMixing; } + set { + multipleMixing = value; + animationsChanged = true; + } + } + private float timeScale = 1; Pool trackEntryPool = new Pool(); @@ -214,7 +233,9 @@ namespace Spine { int timelineCount = timelines.Count; var timelinesFirst = from.timelinesFirst; var timelinesFirstItems = timelinesFirst.Items; - float alpha = from.alpha * entry.mixAlpha * (1 - mix); + var timelinesLastItems = multipleMixing ? null : from.timelinesLast.Items; + float alphaBase = from.alpha * entry.mixAlpha; + float alphaMix = alphaBase * (1 - mix); bool firstFrame = entry.timelinesRotation.Count == 0; if (firstFrame) entry.timelinesRotation.EnsureCapacity(timelines.Count << 1); @@ -223,6 +244,7 @@ namespace Spine { for (int i = 0; i < timelineCount; i++) { Timeline timeline = timelinesItems[i]; bool setupPose = timelinesFirstItems[i]; + float alpha = timelinesLastItems != null && setupPose && !timelinesLastItems[i] ? alphaBase : alphaMix; var rotateTimeline = timeline as RotateTimeline; if (rotateTimeline != null) { ApplyRotateTimeline(rotateTimeline, skeleton, animationTime, alpha, setupPose, timelinesRotation, i << 1, firstFrame); @@ -389,10 +411,31 @@ namespace Spine { current.mixingFrom = from; current.mixTime = 0; - from.timelinesRotation.Clear(); + //from.timelinesRotation.Clear(); + var mixingFrom = from.mixingFrom; + float mixProgress = from.mixTime / from.mixDuration; + if (mixingFrom != null && from.mixDuration > 0) { + // A mix was interrupted, mix from the closest animation. + if (!multipleMixing && mixProgress < 0.5f && mixingFrom.animation != AnimationState.EmptyAnimation) { + current.mixingFrom = mixingFrom; + mixingFrom.mixingFrom = from; + mixingFrom.mixTime = from.mixDuration - from.mixTime; + mixingFrom.mixDuration = from.mixDuration; + from.mixingFrom = null; + from = mixingFrom; + } - // If not completely mixed in, set mixAlpha so mixing out happens from current mix to zero. - if (from.mixingFrom != null && from.mixDuration > 0) current.mixAlpha *= Math.Min(from.mixTime / from.mixDuration, 1); + // The interrupted mix will mix out from its current percentage to zero. + if (multipleMixing) current.mixAlpha *= Math.Min(mixProgress, 1); + + if (!multipleMixing) { + from.mixAlpha = 0; + from.mixTime = 0; + from.mixDuration = 0; + } + } + + from.timelinesRotation.Clear(); // Reset rotation for mixing out, in case entry was mixed in. } queue.Start(current); @@ -586,13 +629,41 @@ namespace Spine { TrackEntry entry = tracks.Items[i]; if (entry != null) CheckTimelinesFirst(entry); } + + if (multipleMixing) return; + + // Set timelinesLast for mixingFrom entries, from highest track to lowest that has mixingFrom. + propertyIDs.Clear(); + int lowestMixingFrom = n; + for (i = 0; i < n; i++) { // Find lowest track with a mixingFrom entry. + TrackEntry entry = tracks.Items[i]; + if (entry == null || entry.mixingFrom == null) continue; + lowestMixingFrom = i; + break; + } + for (i = n - 1; i >= lowestMixingFrom; i--) { // Find first non-null entry. + TrackEntry entry = tracks.Items[i]; + if (entry == null) continue; + + // Store properties for non-mixingFrom entry but don't set timelinesLast, which is only used for mixingFrom entries. + var timelines = entry.animation.timelines; + var timelinesItems = timelines.Items; + for (int ii = 0, nn = timelines.Count; ii < nn; ii++) + propertyIDs.Add(timelinesItems[ii].PropertyId); + + entry = entry.mixingFrom; + while (entry != null) { + CheckTimelinesUsage(entry, entry.timelinesLast); + entry = entry.mixingFrom; + } + } } /// From last to first mixingFrom entries, sets timelinesFirst to true on last, calls checkTimelineUsage on rest. private void SetTimelinesFirst (TrackEntry entry) { if (entry.mixingFrom != null) { SetTimelinesFirst(entry.mixingFrom); - CheckTimelinesUsage(entry); + CheckTimelinesUsage(entry, entry.timelinesFirst); return; } var propertyIDs = this.propertyIDs; @@ -610,14 +681,14 @@ namespace Spine { /// From last to first mixingFrom entries, calls checkTimelineUsage. private void CheckTimelinesFirst (TrackEntry entry) { if (entry.mixingFrom != null) CheckTimelinesFirst(entry.mixingFrom); - CheckTimelinesUsage(entry); + CheckTimelinesUsage(entry, entry.timelinesFirst); } - private void CheckTimelinesUsage (TrackEntry entry) { + private void CheckTimelinesUsage (TrackEntry entry, ExposedList usageArray) { var propertyIDs = this.propertyIDs; var timelines = entry.animation.timelines; int n = timelines.Count; - var usageArray = entry.timelinesFirst; + //var usageArray = entry.timelinesFirst; usageArray.EnsureCapacity(n); var usage = usageArray.Items; var timelinesItems = timelines.Items; @@ -662,6 +733,7 @@ namespace Spine { internal float delay, trackTime, trackLast, nextTrackLast, trackEnd, timeScale = 1f; internal float alpha, mixTime, mixDuration, mixAlpha; internal readonly ExposedList timelinesFirst = new ExposedList(); + internal readonly ExposedList timelinesLast = new ExposedList(); internal readonly ExposedList timelinesRotation = new ExposedList(); // IPoolable.Reset() @@ -670,6 +742,7 @@ namespace Spine { mixingFrom = null; animation = null; timelinesFirst.Clear(); + timelinesLast.Clear(); timelinesRotation.Clear(); Start = null; diff --git a/spine-love/main.lua b/spine-love/main.lua index 5db0f8cb1..88cf4c5e8 100644 --- a/spine-love/main.lua +++ b/spine-love/main.lua @@ -53,6 +53,12 @@ function loadSkeleton (jsonFile, atlasFile, animation, skin, scale, x, y) local stateData = spine.AnimationStateData.new(skeletonData) local state = spine.AnimationState.new(stateData) state:setAnimationByName(0, animation, true) + if (jsonFile == "spineboy") then + stateData:setMix("walk", "jump", 0.5) + stateData:setMix("jump", "run", 0.5) + state:addAnimationByName(0, "jump", false, 3) + state:addAnimationByName(0, "run", true, 0) + end -- set some event callbacks state.onStart = function (entry) @@ -82,14 +88,13 @@ end function love.load(arg) if arg[#arg] == "-debug" then require("mobdebug").start() end - table.insert(skeletons, loadSkeleton("test", "test", "animation", nil, 0.5, 400, 300)) - table.insert(skeletons, loadSkeleton("TwoColorTest", "TwoColorTest", "animation", nil, 0.3, 400, 300)) + -- table.insert(skeletons, loadSkeleton("test", "test", "animation", nil, 0.5, 400, 300)) table.insert(skeletons, loadSkeleton("spineboy", "spineboy", "walk", nil, 0.5, 400, 500)) - table.insert(skeletons, loadSkeleton("raptor", "raptor", "walk", nil, 0.3, 400, 500)) +--[[ table.insert(skeletons, loadSkeleton("raptor", "raptor", "walk", nil, 0.3, 400, 500)) table.insert(skeletons, loadSkeleton("goblins-mesh", "goblins", "walk", "goblin", 1, 400, 500)) table.insert(skeletons, loadSkeleton("tank", "tank", "drive", nil, 0.2, 600, 500)) table.insert(skeletons, loadSkeleton("vine", "vine", "animation", nil, 0.3, 400, 500)) - table.insert(skeletons, loadSkeleton("stretchyman", "stretchyman", "sneak", nil, 0.3, 200, 500)) + table.insert(skeletons, loadSkeleton("stretchyman", "stretchyman", "sneak", nil, 0.3, 200, 500))]]-- skeletonRenderer = spine.SkeletonRenderer.new(true) end diff --git a/spine-lua/AnimationState.lua b/spine-lua/AnimationState.lua index a74d67790..d015b5cba 100644 --- a/spine-lua/AnimationState.lua +++ b/spine-lua/AnimationState.lua @@ -169,6 +169,7 @@ function TrackEntry.new () delay = 0, trackTime = 0, trackLast = 0, nextTrackLast = 0, trackEnd = 0, timeScale = 0, alpha = 0, mixTime = 0, mixDuration = 0, mixAlpha = 0, timelinesFirst = {}, + timelinesLast = {}, timelinesRotation = {} } setmetatable(self, TrackEntry) @@ -202,7 +203,8 @@ function AnimationState.new (data) queue = nil, propertyIDs = {}, animationsChanged = false, - timeScale = 1 + timeScale = 1, + mixingMultiple = false } self.queue = EventQueue.new(self) setmetatable(self, AnimationState) @@ -356,8 +358,11 @@ function AnimationState:applyMixingFrom (entry, skeleton) local animationLast = from.animationLast local animationTime = from:getAnimationTime() local timelines = from.animation.timelines - local timelinesFirst = from.timelinesFirst; - local alpha = from.alpha * entry.mixAlpha * (1 - mix) + local timelinesFirst = from.timelinesFirst + local timelinesLast = nil + if (self.multipleMixing == false) then timelinesLast = from.timelinesLast end + local alphaBase = from.alpha * entry.mixAlpha + local alphaMix = alphaBase * (1 - mix) local firstFrame = #from.timelinesRotation == 0 local timelinesRotation = from.timelinesRotation @@ -365,6 +370,12 @@ function AnimationState:applyMixingFrom (entry, skeleton) local skip = false for i,timeline in ipairs(timelines) do local setupPose = timelinesFirst[i] + local alpha = 1; + if (timelinesLast ~= nil and setupPose and not timlinesLast[i]) then + alpha = alphaBase + else + alpha = alphaMix + end if timeline.type == Animation.TimelineType.rotate then self:applyRotateTimeline(timeline, skeleton, animationTime, alpha, setupPose, timelinesRotation, i * 2, firstFrame) -- FIXME passing i * 2, correct indexing? else @@ -541,10 +552,27 @@ function AnimationState:setCurrent (index, current, interrupt) current.mixingFrom = from current.mixTime = 0 - from.timelinesRotation = {}; + local mixingFrom = from.mixingFrom + if (mixingFrom ~= nil and from.mixDuration > 0) then + if (not self.multipleMixing and from.mixTime / from.mixDuration < 0.5 and mixingFrom.animation ~= EMPTY_ANIMATION) then + current.mixingFrom = mixingFrom + mixingFrom.mixingFrom = from + mixingFrom.mixTime = from.mixDuration - from.mixTime + mixingFrom.mixDuration = from.mixDuration + from.mixingFrom = nil + from = mixingFrom + end - -- If not completely mixed in, set mixAlpha so mixing out happens from current mix to zero. - if from.mixingFrom and from.mixDuration > 0 then current.mixAlpha = current.mixAlpha * math_min(from.mixTime / from.mixDuration, 1) end + current.mixAlpha = current.mixAlpha * math_min(from.mixTime / from.mixDuration, 1) + + if (not self.multipleMixing) then + from.mixAlpha = 0; + from.mixTime = 0; + from.mixDuration = 0; + end + end + + from.timelinesRotation = {}; end queue:start(current) @@ -724,6 +752,42 @@ function AnimationState:_animationsChanged () if entry then self:checkTimelinesFirst(entry) end i = i + 1 end + + if (self.multipleMixing) then return end + + self.propertyIDs = {} + local lowestMixingFrom = n + i = 0; + while i < n do + entry = self.tracks[i] + if not (entry == nil or entry.mixingFrom == nil) then + lowestMixingFrom = i + i = n + 1 -- break + end + i = i + 1 + end + i = n - 1 + while i >= lowestMixingFrom do + local entry = self.tracks[i] + if (entry) then + local propertyIDs = self.propertyIDs + local timelines = entry.animation.timelines + local ii = 1 + local nn = #entry.animation.timelines; + while ii <= nn do + local id = "" .. timelines[ii]:getPropertyId() + propertyIDs[id] = id + ii = ii + 1 + end + + entry = entry.mixingFrom + while (entry) do + self:checkTimelinesUsage(entry, entry.timelinesLast) + entry = entry.mixingFrom; + end + end + i = i - 1 + end end function AnimationState:setTimelinesFirst (entry) diff --git a/spine-sfml/example/main.cpp b/spine-sfml/example/main.cpp index ca87d6ccd..a182cc982 100644 --- a/spine-sfml/example/main.cpp +++ b/spine-sfml/example/main.cpp @@ -116,6 +116,7 @@ void spineboy (SkeletonData* skeletonData, Atlas* atlas) { SkeletonDrawable* drawable = new SkeletonDrawable(skeletonData, stateData); drawable->timeScale = 1; + drawable->state->multipleMixing = -1; Skeleton* skeleton = drawable->skeleton; skeleton->flipX = false;