All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.esotericsoftware.spine.AnimationState Maven / Gradle / Ivy

/******************************************************************************
 * Spine Runtimes License Agreement
 * Last updated May 1, 2019. Replaces all prior versions.
 *
 * Copyright (c) 2013-2019, Esoteric Software LLC
 *
 * Integration of the Spine Runtimes into software or otherwise creating
 * derivative works of the Spine Runtimes is permitted under the terms and
 * conditions of Section 2 of the Spine Editor License Agreement:
 * http://esotericsoftware.com/spine-editor-license
 *
 * Otherwise, it is permitted to integrate the Spine Runtimes into software
 * or otherwise create derivative works of the Spine Runtimes (collectively,
 * "Products"), provided that each user of the Products must obtain their own
 * Spine Editor license and redistribution of the Products in any form must
 * include this license and copyright notice.
 *
 * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY EXPRESS
 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
 * NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS
 * INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *****************************************************************************/

package com.esotericsoftware.spine;

import static com.esotericsoftware.spine.Animation.RotateTimeline.ENTRIES;
import static com.esotericsoftware.spine.Animation.RotateTimeline.PREV_ROTATION;
import static com.esotericsoftware.spine.Animation.RotateTimeline.PREV_TIME;
import static com.esotericsoftware.spine.Animation.RotateTimeline.ROTATION;

import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.FloatArray;
import com.badlogic.gdx.utils.IntArray;
import com.badlogic.gdx.utils.IntSet;
import com.badlogic.gdx.utils.Pool;
import com.badlogic.gdx.utils.Pool.Poolable;
import com.esotericsoftware.spine.Animation.AttachmentTimeline;
import com.esotericsoftware.spine.Animation.DrawOrderTimeline;
import com.esotericsoftware.spine.Animation.MixBlend;
import com.esotericsoftware.spine.Animation.MixDirection;
import com.esotericsoftware.spine.Animation.RotateTimeline;
import com.esotericsoftware.spine.Animation.Timeline;

/** Applies animations over time, queues animations for later playback, mixes (crossfading) between animations, and applies
 * multiple animations on top of each other (layering).
 * 

* See Applying Animations in the Spine Runtimes Guide. */ public class AnimationState { static private final Animation emptyAnimation = new Animation("", new Array(0), 0); /** 1) A previously applied timeline has set this property.
* Result: Mix from the current pose to the timeline pose. */ static private final int SUBSEQUENT = 0; /** 1) This is the first timeline to set this property.
* 2) The next track entry applied after this one does not have a timeline to set this property.
* Result: Mix from the setup pose to the timeline pose. */ static private final int FIRST = 1; /** 1) This is the first timeline to set this property.
* 2) The next track entry to be applied does have a timeline to set this property.
* 3) The next track entry after that one does not have a timeline to set this property.
* Result: Mix from the setup pose to the timeline pose, but do not mix out. This avoids "dipping" when crossfading animations * that key the same property. A subsequent timeline will set this property using a mix. */ static private final int HOLD = 2; /** 1) This is the first timeline to set this property.
* 2) The next track entry to be applied does have a timeline to set this property.
* 3) The next track entry after that one does have a timeline to set this property.
* 4) timelineHoldMix stores the first subsequent track entry that does not have a timeline to set this property.
* Result: The same as HOLD except the mix percentage from the timelineHoldMix track entry is used. This handles when more than * 2 track entries in a row have a timeline that sets the same property.
* Eg, A -> B -> C -> D where A, B, and C have a timeline setting same property, but D does not. When A is applied, to avoid * "dipping" A is not mixed out, however D (the first entry that doesn't set the property) mixing in is used to mix out A * (which affects B and C). Without using D to mix out, A would be applied fully until mixing completes, then snap into * place. */ static private final int HOLD_MIX = 3; private AnimationStateData data; final Array tracks = new Array(); private final Array events = new Array(); final Array listeners = new Array(); private final EventQueue queue = new EventQueue(); private final IntSet propertyIDs = new IntSet(); boolean animationsChanged; private float timeScale = 1; final Pool trackEntryPool = new Pool() { protected Object newObject () { return new TrackEntry(); } }; /** Creates an uninitialized AnimationState. The animation state data must be set before use. */ public AnimationState () { } public AnimationState (AnimationStateData data) { if (data == null) throw new IllegalArgumentException("data cannot be null."); this.data = data; } /** Increments each track entry {@link TrackEntry#getTrackTime()}, setting queued animations as current if needed. */ public void update (float delta) { delta *= timeScale; for (int i = 0, n = tracks.size; i < n; i++) { TrackEntry current = tracks.get(i); if (current == null) continue; current.animationLast = current.nextAnimationLast; current.trackLast = current.nextTrackLast; float currentDelta = delta * current.timeScale; if (current.delay > 0) { current.delay -= currentDelta; if (current.delay > 0) continue; currentDelta = -current.delay; current.delay = 0; } TrackEntry next = current.next; if (next != null) { // When the next entry's delay is passed, change to the next entry, preserving leftover time. float nextTime = current.trackLast - next.delay; if (nextTime >= 0) { next.delay = 0; next.trackTime = current.timeScale == 0 ? 0 : (nextTime / current.timeScale + delta) * next.timeScale; current.trackTime += currentDelta; setCurrent(i, next, true); while (next.mixingFrom != null) { next.mixTime += delta; next = next.mixingFrom; } continue; } } else if (current.trackLast >= current.trackEnd && current.mixingFrom == null) { // Clear the track when there is no next entry, the track end time is reached, and there is no mixingFrom. tracks.set(i, null); queue.end(current); disposeNext(current); continue; } if (current.mixingFrom != null && updateMixingFrom(current, delta)) { // End mixing from entries once all have completed. TrackEntry from = current.mixingFrom; current.mixingFrom = null; if (from != null) from.mixingTo = null; while (from != null) { queue.end(from); from = from.mixingFrom; } } current.trackTime += currentDelta; } queue.drain(); } /** Returns true when all mixing from entries are complete. */ private boolean updateMixingFrom (TrackEntry to, float delta) { TrackEntry from = to.mixingFrom; if (from == null) return true; boolean finished = updateMixingFrom(from, delta); from.animationLast = from.nextAnimationLast; from.trackLast = from.nextTrackLast; // Require mixTime > 0 to ensure the mixing from entry was applied at least once. if (to.mixTime > 0 && to.mixTime >= to.mixDuration) { // Require totalAlpha == 0 to ensure mixing is complete, unless mixDuration == 0 (the transition is a single frame). if (from.totalAlpha == 0 || to.mixDuration == 0) { to.mixingFrom = from.mixingFrom; if (from.mixingFrom != null) from.mixingFrom.mixingTo = to; to.interruptAlpha = from.interruptAlpha; queue.end(from); } return finished; } from.trackTime += delta * from.timeScale; to.mixTime += delta; return false; } /** Poses the skeleton using the track entry animations. There are no side effects other than invoking listeners, so the * animation state can be applied to multiple skeletons to pose them identically. * @return True if any animations were applied. */ public boolean apply (Skeleton skeleton) { if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null."); if (animationsChanged) animationsChanged(); Array events = this.events; boolean applied = false; for (int i = 0, n = tracks.size; i < n; i++) { TrackEntry current = tracks.get(i); if (current == null || current.delay > 0) continue; applied = true; // Track 0 animations aren't for layering, so do not show the previously applied animations before the first key. MixBlend blend = i == 0 ? MixBlend.first : current.mixBlend; // Apply mixing from entries first. float mix = current.alpha; if (current.mixingFrom != null) mix *= applyMixingFrom(current, skeleton, blend); else if (current.trackTime >= current.trackEnd && current.next == null) // mix = 0; // Set to setup pose the last time the entry will be applied. // Apply current entry. float animationLast = current.animationLast, animationTime = current.getAnimationTime(); int timelineCount = current.animation.timelines.size; Object[] timelines = current.animation.timelines.items; if ((i == 0 && mix == 1) || blend == MixBlend.add) { for (int ii = 0; ii < timelineCount; ii++) ((Timeline)timelines[ii]).apply(skeleton, animationLast, animationTime, events, mix, blend, MixDirection.in); } else { int[] timelineMode = current.timelineMode.items; boolean firstFrame = current.timelinesRotation.size != timelineCount << 1; if (firstFrame) current.timelinesRotation.setSize(timelineCount << 1); float[] timelinesRotation = current.timelinesRotation.items; for (int ii = 0; ii < timelineCount; ii++) { Timeline timeline = (Timeline)timelines[ii]; MixBlend timelineBlend = timelineMode[ii] == SUBSEQUENT ? blend : MixBlend.setup; if (timeline instanceof RotateTimeline) { applyRotateTimeline((RotateTimeline)timeline, skeleton, animationTime, mix, timelineBlend, timelinesRotation, ii << 1, firstFrame); } else timeline.apply(skeleton, animationLast, animationTime, events, mix, timelineBlend, MixDirection.in); } } queueEvents(current, animationTime); events.clear(); current.nextAnimationLast = animationTime; current.nextTrackLast = current.trackTime; } queue.drain(); return applied; } private float applyMixingFrom (TrackEntry to, Skeleton skeleton, MixBlend blend) { TrackEntry from = to.mixingFrom; if (from.mixingFrom != null) applyMixingFrom(from, skeleton, blend); float mix; if (to.mixDuration == 0) { // Single frame mix to undo mixingFrom changes. mix = 1; if (blend == MixBlend.first) blend = MixBlend.setup; // Tracks >0 are transparent and can't reset to setup pose. } else { mix = to.mixTime / to.mixDuration; if (mix > 1) mix = 1; if (blend != MixBlend.first) blend = from.mixBlend; // Track 0 ignores track mix blend. } Array events = mix < from.eventThreshold ? this.events : null; boolean attachments = mix < from.attachmentThreshold, drawOrder = mix < from.drawOrderThreshold; float animationLast = from.animationLast, animationTime = from.getAnimationTime(); int timelineCount = from.animation.timelines.size; Object[] timelines = from.animation.timelines.items; float alphaHold = from.alpha * to.interruptAlpha, alphaMix = alphaHold * (1 - mix); if (blend == MixBlend.add) { for (int i = 0; i < timelineCount; i++) ((Timeline)timelines[i]).apply(skeleton, animationLast, animationTime, events, alphaMix, blend, MixDirection.out); } else { int[] timelineMode = from.timelineMode.items; Object[] timelineHoldMix = from.timelineHoldMix.items; boolean firstFrame = from.timelinesRotation.size != timelineCount << 1; if (firstFrame) from.timelinesRotation.setSize(timelineCount << 1); float[] timelinesRotation = from.timelinesRotation.items; from.totalAlpha = 0; for (int i = 0; i < timelineCount; i++) { Timeline timeline = (Timeline)timelines[i]; MixDirection direction = MixDirection.out; MixBlend timelineBlend; float alpha; switch (timelineMode[i]) { case SUBSEQUENT: if (!attachments && timeline instanceof AttachmentTimeline) continue; if (!drawOrder && timeline instanceof DrawOrderTimeline) continue; timelineBlend = blend; alpha = alphaMix; break; case FIRST: timelineBlend = MixBlend.setup; alpha = alphaMix; break; case HOLD: timelineBlend = MixBlend.setup; alpha = alphaHold; break; default: timelineBlend = MixBlend.setup; TrackEntry holdMix = (TrackEntry)timelineHoldMix[i]; alpha = alphaHold * Math.max(0, 1 - holdMix.mixTime / holdMix.mixDuration); break; } from.totalAlpha += alpha; if (timeline instanceof RotateTimeline) { applyRotateTimeline((RotateTimeline)timeline, skeleton, animationTime, alpha, timelineBlend, timelinesRotation, i << 1, firstFrame); } else { if (timelineBlend == MixBlend.setup) { if (timeline instanceof AttachmentTimeline) { if (attachments) direction = MixDirection.in; } else if (timeline instanceof DrawOrderTimeline) { if (drawOrder) direction = MixDirection.in; } } timeline.apply(skeleton, animationLast, animationTime, events, alpha, timelineBlend, direction); } } } if (to.mixDuration > 0) queueEvents(from, animationTime); this.events.clear(); from.nextAnimationLast = animationTime; from.nextTrackLast = from.trackTime; return mix; } private void applyRotateTimeline (RotateTimeline timeline, Skeleton skeleton, float time, float alpha, MixBlend blend, float[] timelinesRotation, int i, boolean firstFrame) { if (firstFrame) timelinesRotation[i] = 0; if (alpha == 1) { timeline.apply(skeleton, 0, time, null, 1, blend, MixDirection.in); return; } Bone bone = skeleton.bones.get(timeline.boneIndex); float[] frames = timeline.frames; float r1, r2; if (time < frames[0]) { // Time is before first frame. switch (blend) { case setup: bone.rotation = bone.data.rotation; // Fall through. default: return; case first: r1 = bone.rotation; r2 = bone.data.rotation; } } else { r1 = blend == MixBlend.setup ? bone.data.rotation : bone.rotation; if (time >= frames[frames.length - ENTRIES]) // Time is after last frame. r2 = bone.data.rotation + frames[frames.length + PREV_ROTATION]; else { // Interpolate between the previous frame and the current frame. int frame = Animation.binarySearch(frames, time, ENTRIES); float prevRotation = frames[frame + PREV_ROTATION]; float frameTime = frames[frame]; float percent = timeline.getCurvePercent((frame >> 1) - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime)); r2 = frames[frame + ROTATION] - prevRotation; r2 -= (16384 - (int)(16384.499999999996 - r2 / 360)) * 360; r2 = prevRotation + r2 * percent + bone.data.rotation; r2 -= (16384 - (int)(16384.499999999996 - r2 / 360)) * 360; } } // Mix between rotations using the direction of the shortest route on the first frame. float total, diff = r2 - r1; diff -= (16384 - (int)(16384.499999999996 - diff / 360)) * 360; if (diff == 0) total = timelinesRotation[i]; else { float lastTotal, lastDiff; if (firstFrame) { lastTotal = 0; lastDiff = diff; } else { lastTotal = timelinesRotation[i]; // Angle and direction of mix, including loops. lastDiff = timelinesRotation[i + 1]; // Difference between bones. } boolean current = diff > 0, dir = lastTotal >= 0; // Detect cross at 0 (not 180). if (Math.signum(lastDiff) != Math.signum(diff) && Math.abs(lastDiff) <= 90) { // A cross after a 360 rotation is a loop. if (Math.abs(lastTotal) > 180) lastTotal += 360 * Math.signum(lastTotal); dir = current; } total = diff + lastTotal - lastTotal % 360; // Store loops as part of lastTotal. if (dir != current) total += 360 * Math.signum(lastTotal); timelinesRotation[i] = total; } timelinesRotation[i + 1] = diff; r1 += total * alpha; bone.rotation = r1 - (16384 - (int)(16384.499999999996 - r1 / 360)) * 360; } private void queueEvents (TrackEntry entry, float animationTime) { float animationStart = entry.animationStart, animationEnd = entry.animationEnd; float duration = animationEnd - animationStart; float trackLastWrapped = entry.trackLast % duration; // Queue events before complete. Array events = this.events; int i = 0, n = events.size; for (; i < n; i++) { Event event = events.get(i); if (event.time < trackLastWrapped) break; if (event.time > animationEnd) continue; // Discard events outside animation start/end. queue.event(entry, event); } // Queue complete if completed a loop iteration or the animation. boolean complete; if (entry.loop) complete = duration == 0 || trackLastWrapped > entry.trackTime % duration; else complete = animationTime >= animationEnd && entry.animationLast < animationEnd; if (complete) queue.complete(entry); // Queue events after complete. for (; i < n; i++) { Event event = events.get(i); if (event.time < animationStart) continue; // Discard events outside animation start/end. queue.event(entry, events.get(i)); } } /** Removes all animations from all tracks, leaving skeletons in their current pose. *

* It may be desired to use {@link AnimationState#setEmptyAnimations(float)} to mix the skeletons back to the setup pose, * rather than leaving them in their current pose. */ public void clearTracks () { boolean oldDrainDisabled = queue.drainDisabled; queue.drainDisabled = true; for (int i = 0, n = tracks.size; i < n; i++) clearTrack(i); tracks.clear(); queue.drainDisabled = oldDrainDisabled; queue.drain(); } /** Removes all animations from the track, leaving skeletons in their current pose. *

* It may be desired to use {@link AnimationState#setEmptyAnimation(int, float)} to mix the skeletons back to the setup pose, * rather than leaving them in their current pose. */ public void clearTrack (int trackIndex) { if (trackIndex >= tracks.size) return; TrackEntry current = tracks.get(trackIndex); if (current == null) return; queue.end(current); disposeNext(current); TrackEntry entry = current; while (true) { TrackEntry from = entry.mixingFrom; if (from == null) break; queue.end(from); entry.mixingFrom = null; entry.mixingTo = null; entry = from; } tracks.set(current.trackIndex, null); queue.drain(); } private void setCurrent (int index, TrackEntry current, boolean interrupt) { TrackEntry from = expandToIndex(index); tracks.set(index, current); if (from != null) { if (interrupt) queue.interrupt(from); current.mixingFrom = from; from.mixingTo = current; current.mixTime = 0; // Store the interrupted mix percentage. if (from.mixingFrom != null && from.mixDuration > 0) current.interruptAlpha *= Math.min(1, from.mixTime / from.mixDuration); from.timelinesRotation.clear(); // Reset rotation for mixing out, in case entry was mixed in. } queue.start(current); } /** Sets an animation by name. *

* {@link #setAnimation(int, Animation, boolean)}. */ public TrackEntry setAnimation (int trackIndex, String animationName, boolean loop) { Animation animation = data.skeletonData.findAnimation(animationName); if (animation == null) throw new IllegalArgumentException("Animation not found: " + animationName); return setAnimation(trackIndex, animation, loop); } /** Sets the current animation for a track, discarding any queued animations. If the formerly current track entry was never * applied to a skeleton, it is replaced (not mixed from). * @param loop If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its * duration. In either case {@link TrackEntry#getTrackEnd()} determines when the track is cleared. * @return A track entry to allow further customization of animation playback. References to the track entry must not be kept * after the {@link AnimationStateListener#dispose(TrackEntry)} event occurs. */ public TrackEntry setAnimation (int trackIndex, Animation animation, boolean loop) { if (animation == null) throw new IllegalArgumentException("animation cannot be null."); boolean interrupt = true; TrackEntry current = expandToIndex(trackIndex); if (current != null) { if (current.nextTrackLast == -1) { // Don't mix from an entry that was never applied. tracks.set(trackIndex, current.mixingFrom); queue.interrupt(current); queue.end(current); disposeNext(current); current = current.mixingFrom; interrupt = false; // mixingFrom is current again, but don't interrupt it twice. } else disposeNext(current); } TrackEntry entry = trackEntry(trackIndex, animation, loop, current); setCurrent(trackIndex, entry, interrupt); queue.drain(); return entry; } /** Queues an animation by name. *

* See {@link #addAnimation(int, Animation, boolean, float)}. */ public TrackEntry addAnimation (int trackIndex, String animationName, boolean loop, float delay) { Animation animation = data.skeletonData.findAnimation(animationName); if (animation == null) throw new IllegalArgumentException("Animation not found: " + animationName); return addAnimation(trackIndex, animation, loop, delay); } /** Adds an animation to be played after the current or last queued animation for a track. If the track is empty, it is * equivalent to calling {@link #setAnimation(int, Animation, boolean)}. * @param delay If > 0, sets {@link TrackEntry#getDelay()}. If <= 0, the delay set is the duration of the previous track entry * minus any mix duration (from the {@link AnimationStateData}) plus the specified delay (ie the mix * ends at (delay = 0) or before (delay < 0) the previous track entry duration). If the * previous entry is looping, its next loop completion is used instead of its duration. * @return A track entry to allow further customization of animation playback. References to the track entry must not be kept * after the {@link AnimationStateListener#dispose(TrackEntry)} event occurs. */ public TrackEntry addAnimation (int trackIndex, Animation animation, boolean loop, float delay) { if (animation == null) throw new IllegalArgumentException("animation cannot be null."); TrackEntry last = expandToIndex(trackIndex); if (last != null) { while (last.next != null) last = last.next; } TrackEntry entry = trackEntry(trackIndex, animation, loop, last); if (last == null) { setCurrent(trackIndex, entry, true); queue.drain(); } else { last.next = entry; if (delay <= 0) { float duration = last.animationEnd - last.animationStart; if (duration != 0) { if (last.loop) delay += duration * (1 + (int)(last.trackTime / duration)); // Completion of next loop. else delay += Math.max(duration, last.trackTime); // After duration, else next update. delay -= data.getMix(last.animation, animation); } else delay = last.trackTime; // Next update. } } entry.delay = delay; return entry; } /** Sets an empty animation for a track, discarding any queued animations, and sets the track entry's * {@link TrackEntry#getMixDuration()}. An empty animation has no timelines and serves as a placeholder for mixing in or out. *

* Mixing out is done by setting an empty animation with a mix duration using either {@link #setEmptyAnimation(int, float)}, * {@link #setEmptyAnimations(float)}, or {@link #addEmptyAnimation(int, float, float)}. Mixing to an empty animation causes * the previous animation to be applied less and less over the mix duration. Properties keyed in the previous animation * transition to the value from lower tracks or to the setup pose value if no lower tracks key the property. A mix duration of * 0 still mixes out over one frame. *

* Mixing in is done by first setting an empty animation, then adding an animation using * {@link #addAnimation(int, Animation, boolean, float)} and on the returned track entry, set the * {@link TrackEntry#setMixDuration(float)}. Mixing from an empty animation causes the new animation to be applied more and * more over the mix duration. Properties keyed in the new animation transition from the value from lower tracks or from the * setup pose value if no lower tracks key the property to the value keyed in the new animation. */ public TrackEntry setEmptyAnimation (int trackIndex, float mixDuration) { TrackEntry entry = setAnimation(trackIndex, emptyAnimation, false); entry.mixDuration = mixDuration; entry.trackEnd = mixDuration; return entry; } /** Adds an empty animation to be played after the current or last queued animation for a track, and sets the track entry's * {@link TrackEntry#getMixDuration()}. If the track is empty, it is equivalent to calling * {@link #setEmptyAnimation(int, float)}. *

* See {@link #setEmptyAnimation(int, float)}. * @param delay If > 0, sets {@link TrackEntry#getDelay()}. If <= 0, the delay set is the duration of the previous track entry * minus any mix duration plus the specified delay (ie the mix ends at (delay = 0) or * before (delay < 0) the previous track entry duration). If the previous entry is looping, its next * loop completion is used instead of its duration. * @return A track entry to allow further customization of animation playback. References to the track entry must not be kept * after the {@link AnimationStateListener#dispose(TrackEntry)} event occurs. */ public TrackEntry addEmptyAnimation (int trackIndex, float mixDuration, float delay) { if (delay <= 0) delay -= mixDuration; TrackEntry entry = addAnimation(trackIndex, emptyAnimation, false, delay); entry.mixDuration = mixDuration; entry.trackEnd = mixDuration; return entry; } /** Sets an empty animation for every track, discarding any queued animations, and mixes to it over the specified mix * duration. */ public void setEmptyAnimations (float mixDuration) { boolean oldDrainDisabled = queue.drainDisabled; queue.drainDisabled = true; for (int i = 0, n = tracks.size; i < n; i++) { TrackEntry current = tracks.get(i); if (current != null) setEmptyAnimation(current.trackIndex, mixDuration); } queue.drainDisabled = oldDrainDisabled; queue.drain(); } private TrackEntry expandToIndex (int index) { if (index < tracks.size) return tracks.get(index); tracks.ensureCapacity(index - tracks.size + 1); tracks.size = index + 1; return null; } /** @param last May be null. */ private TrackEntry trackEntry (int trackIndex, Animation animation, boolean loop, TrackEntry last) { TrackEntry entry = trackEntryPool.obtain(); entry.trackIndex = trackIndex; entry.animation = animation; entry.loop = loop; entry.holdPrevious = false; entry.eventThreshold = 0; entry.attachmentThreshold = 0; entry.drawOrderThreshold = 0; entry.animationStart = 0; entry.animationEnd = animation.getDuration(); entry.animationLast = -1; entry.nextAnimationLast = -1; entry.delay = 0; entry.trackTime = 0; entry.trackLast = -1; entry.nextTrackLast = -1; entry.trackEnd = Float.MAX_VALUE; entry.timeScale = 1; entry.alpha = 1; entry.interruptAlpha = 1; entry.mixTime = 0; entry.mixDuration = last == null ? 0 : data.getMix(last.animation, animation); return entry; } private void disposeNext (TrackEntry entry) { TrackEntry next = entry.next; while (next != null) { queue.dispose(next); next = next.next; } entry.next = null; } private void animationsChanged () { animationsChanged = false; propertyIDs.clear(2048); for (int i = 0, n = tracks.size; i < n; i++) { TrackEntry entry = tracks.get(i); if (entry == null) continue; // Move to last entry, then iterate in reverse (the order animations are applied). while (entry.mixingFrom != null) entry = entry.mixingFrom; do { if (entry.mixingTo == null || entry.mixBlend != MixBlend.add) setTimelineModes(entry); entry = entry.mixingTo; } while (entry != null); } } private void setTimelineModes (TrackEntry entry) { TrackEntry to = entry.mixingTo; Object[] timelines = entry.animation.timelines.items; int timelinesCount = entry.animation.timelines.size; int[] timelineMode = entry.timelineMode.setSize(timelinesCount); entry.timelineHoldMix.clear(); Object[] timelineHoldMix = entry.timelineHoldMix.setSize(timelinesCount); IntSet propertyIDs = this.propertyIDs; if (to != null && to.holdPrevious) { for (int i = 0; i < timelinesCount; i++) { propertyIDs.add(((Timeline)timelines[i]).getPropertyId()); timelineMode[i] = HOLD; } return; } outer: for (int i = 0; i < timelinesCount; i++) { int id = ((Timeline)timelines[i]).getPropertyId(); if (!propertyIDs.add(id)) timelineMode[i] = SUBSEQUENT; else if (to == null || !hasTimeline(to, id)) timelineMode[i] = FIRST; else { for (TrackEntry next = to.mixingTo; next != null; next = next.mixingTo) { if (hasTimeline(next, id)) continue; if (next.mixDuration > 0) { timelineMode[i] = HOLD_MIX; timelineHoldMix[i] = next; continue outer; } break; } timelineMode[i] = HOLD; } } } private boolean hasTimeline (TrackEntry entry, int id) { Object[] timelines = entry.animation.timelines.items; for (int i = 0, n = entry.animation.timelines.size; i < n; i++) if (((Timeline)timelines[i]).getPropertyId() == id) return true; return false; } /** Returns the track entry for the animation currently playing on the track, or null if no animation is currently playing. */ public TrackEntry getCurrent (int trackIndex) { if (trackIndex >= tracks.size) return null; return tracks.get(trackIndex); } /** Adds a listener to receive events for all track entries. */ public void addListener (AnimationStateListener listener) { if (listener == null) throw new IllegalArgumentException("listener cannot be null."); listeners.add(listener); } /** Removes the listener added with {@link #addListener(AnimationStateListener)}. */ public void removeListener (AnimationStateListener listener) { listeners.removeValue(listener, true); } /** Removes all listeners added with {@link #addListener(AnimationStateListener)}. */ public void clearListeners () { listeners.clear(); } /** Discards all listener notifications that have not yet been delivered. This can be useful to call from an * {@link AnimationStateListener} when it is known that further notifications that may have been already queued for delivery * are not wanted because new animations are being set. */ public void clearListenerNotifications () { queue.clear(); } /** Multiplier for the delta time when the animation state is updated, causing time for all animations and mixes to play slower * or faster. Defaults to 1. *

* See TrackEntry {@link TrackEntry#getTimeScale()} for affecting a single animation. */ public float getTimeScale () { return timeScale; } public void setTimeScale (float timeScale) { this.timeScale = timeScale; } /** The AnimationStateData to look up mix durations. */ public AnimationStateData getData () { return data; } public void setData (AnimationStateData data) { if (data == null) throw new IllegalArgumentException("data cannot be null."); this.data = data; } /** The list of tracks that currently have animations, which may contain null entries. */ public Array getTracks () { return tracks; } public String toString () { StringBuilder buffer = new StringBuilder(64); for (int i = 0, n = tracks.size; i < n; i++) { TrackEntry entry = tracks.get(i); if (entry == null) continue; if (buffer.length() > 0) buffer.append(", "); buffer.append(entry.toString()); } if (buffer.length() == 0) return ""; return buffer.toString(); } /** Stores settings and other state for the playback of an animation on an {@link AnimationState} track. *

* References to a track entry must not be kept after the {@link AnimationStateListener#dispose(TrackEntry)} event occurs. */ static public class TrackEntry implements Poolable { Animation animation; TrackEntry next, mixingFrom, mixingTo; AnimationStateListener listener; int trackIndex; boolean loop, holdPrevious; float eventThreshold, attachmentThreshold, drawOrderThreshold; float animationStart, animationEnd, animationLast, nextAnimationLast; float delay, trackTime, trackLast, nextTrackLast, trackEnd, timeScale; float alpha, mixTime, mixDuration, interruptAlpha, totalAlpha; MixBlend mixBlend = MixBlend.replace; final IntArray timelineMode = new IntArray(); final Array timelineHoldMix = new Array(); final FloatArray timelinesRotation = new FloatArray(); public void reset () { next = null; mixingFrom = null; mixingTo = null; animation = null; listener = null; timelineMode.clear(); timelineHoldMix.clear(); timelinesRotation.clear(); } /** The index of the track where this track entry is either current or queued. *

* See {@link AnimationState#getCurrent(int)}. */ public int getTrackIndex () { return trackIndex; } /** The animation to apply for this track entry. */ public Animation getAnimation () { return animation; } public void setAnimation (Animation animation) { this.animation = animation; } /** If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its * duration. */ public boolean getLoop () { return loop; } public void setLoop (boolean loop) { this.loop = loop; } /** Seconds to postpone playing the animation. When this track entry is the current track entry, delay * postpones incrementing the {@link #getTrackTime()}. When this track entry is queued, delay is the time from * the start of the previous animation to when this track entry will become the current track entry (ie when the previous * track entry {@link TrackEntry#getTrackTime()} >= this track entry's delay). *

* {@link #getTimeScale()} affects the delay. */ public float getDelay () { return delay; } public void setDelay (float delay) { this.delay = delay; } /** Current time in seconds this track entry has been the current track entry. The track time determines * {@link #getAnimationTime()}. The track time can be set to start the animation at a time other than 0, without affecting * looping. */ public float getTrackTime () { return trackTime; } public void setTrackTime (float trackTime) { this.trackTime = trackTime; } /** The track time in seconds when this animation will be removed from the track. Defaults to the highest possible float * value, meaning the animation will be applied until a new animation is set or the track is cleared. If the track end time * is reached, no other animations are queued for playback, and mixing from any previous animations is complete, then the * properties keyed by the animation are set to the setup pose and the track is cleared. *

* It may be desired to use {@link AnimationState#addEmptyAnimation(int, float, float)} rather than have the animation * abruptly cease being applied. */ public float getTrackEnd () { return trackEnd; } public void setTrackEnd (float trackEnd) { this.trackEnd = trackEnd; } /** Seconds when this animation starts, both initially and after looping. Defaults to 0. *

* When changing the animationStart time, it often makes sense to set {@link #getAnimationLast()} to the same * value to prevent timeline keys before the start time from triggering. */ public float getAnimationStart () { return animationStart; } public void setAnimationStart (float animationStart) { this.animationStart = animationStart; } /** Seconds for the last frame of this animation. Non-looping animations won't play past this time. Looping animations will * loop back to {@link #getAnimationStart()} at this time. Defaults to the animation {@link Animation#duration}. */ public float getAnimationEnd () { return animationEnd; } public void setAnimationEnd (float animationEnd) { this.animationEnd = animationEnd; } /** The time in seconds this animation was last applied. Some timelines use this for one-time triggers. Eg, when this * animation is applied, event timelines will fire all events between the animationLast time (exclusive) and * animationTime (inclusive). Defaults to -1 to ensure triggers on frame 0 happen the first time this animation * is applied. */ public float getAnimationLast () { return animationLast; } public void setAnimationLast (float animationLast) { this.animationLast = animationLast; nextAnimationLast = animationLast; } /** Uses {@link #getTrackTime()} to compute the animationTime, which is between {@link #getAnimationStart()} * and {@link #getAnimationEnd()}. When the trackTime is 0, the animationTime is equal to the * animationStart time. */ public float getAnimationTime () { if (loop) { float duration = animationEnd - animationStart; if (duration == 0) return animationStart; return (trackTime % duration) + animationStart; } return Math.min(trackTime + animationStart, animationEnd); } /** Multiplier for the delta time when this track entry is updated, causing time for this animation to pass slower or * faster. Defaults to 1. *

* {@link #getMixTime()} is not affected by track entry time scale, so {@link #getMixDuration()} may need to be adjusted to * match the animation speed. *

* When using {@link AnimationState#addAnimation(int, Animation, boolean, float)} with a delay <= 0, note the * {@link #getDelay()} is set using the mix duration from the {@link AnimationStateData}, assuming time scale to be 1. If * the time scale is not 1, the delay may need to be adjusted. *

* See AnimationState {@link AnimationState#getTimeScale()} for affecting all animations. */ public float getTimeScale () { return timeScale; } public void setTimeScale (float timeScale) { this.timeScale = timeScale; } /** The listener for events generated by this track entry, or null. *

* A track entry returned from {@link AnimationState#setAnimation(int, Animation, boolean)} is already the current animation * for the track, so the track entry listener {@link AnimationStateListener#start(TrackEntry)} will not be called. */ public AnimationStateListener getListener () { return listener; } /** @param listener May be null. */ public void setListener (AnimationStateListener listener) { this.listener = listener; } /** Values < 1 mix this animation with the skeleton's current pose (usually the pose resulting from lower tracks). Defaults * to 1, which overwrites the skeleton's current pose with this animation. *

* Typically track 0 is used to completely pose the skeleton, then alpha is used on higher tracks. It doesn't make sense to * use alpha on track 0 if the skeleton pose is from the last frame render. */ public float getAlpha () { return alpha; } public void setAlpha (float alpha) { this.alpha = alpha; } /** When the mix percentage ({@link #getMixTime()} / {@link #getMixDuration()}) is less than the * eventThreshold, event timelines are applied while this animation is being mixed out. Defaults to 0, so event * timelines are not applied while this animation is being mixed out. */ public float getEventThreshold () { return eventThreshold; } public void setEventThreshold (float eventThreshold) { this.eventThreshold = eventThreshold; } /** When the mix percentage ({@link #getMixTime()} / {@link #getMixDuration()}) is less than the * attachmentThreshold, attachment timelines are applied while this animation is being mixed out. Defaults to * 0, so attachment timelines are not applied while this animation is being mixed out. */ public float getAttachmentThreshold () { return attachmentThreshold; } public void setAttachmentThreshold (float attachmentThreshold) { this.attachmentThreshold = attachmentThreshold; } /** When the mix percentage ({@link #getMixTime()} / {@link #getMixDuration()}) is less than the * drawOrderThreshold, draw order timelines are applied while this animation is being mixed out. Defaults to 0, * so draw order timelines are not applied while this animation is being mixed out. */ public float getDrawOrderThreshold () { return drawOrderThreshold; } public void setDrawOrderThreshold (float drawOrderThreshold) { this.drawOrderThreshold = drawOrderThreshold; } /** The animation queued to start after this animation, or null. next makes up a linked list. */ public TrackEntry getNext () { return next; } /** Returns true if at least one loop has been completed. *

* See {@link AnimationStateListener#complete(TrackEntry)}. */ public boolean isComplete () { return trackTime >= animationEnd - animationStart; } /** Seconds from 0 to the {@link #getMixDuration()} when mixing from the previous animation to this animation. May be * slightly more than mixDuration when the mix is complete. */ public float getMixTime () { return mixTime; } public void setMixTime (float mixTime) { this.mixTime = mixTime; } /** Seconds for mixing from the previous animation to this animation. Defaults to the value provided by AnimationStateData * {@link AnimationStateData#getMix(Animation, Animation)} based on the animation before this animation (if any). *

* The mixDuration can be set manually rather than use the value from * {@link AnimationStateData#getMix(Animation, Animation)}. In that case, the mixDuration can be set for a new * track entry only before {@link AnimationState#update(float)} is first called. *

* When using {@link AnimationState#addAnimation(int, Animation, boolean, float)} with a delay <= 0, note the * {@link #getDelay()} is set using the mix duration from the {@link AnimationStateData}, not a mix duration set * afterward. */ public float getMixDuration () { return mixDuration; } public void setMixDuration (float mixDuration) { this.mixDuration = mixDuration; } /** Controls how properties keyed in the animation are mixed with lower tracks. Defaults to {@link MixBlend#replace}, which * replaces the values from the lower tracks with the animation values. {@link MixBlend#add} adds the animation values to * the values from the lower tracks. *

* The mixBlend can be set for a new track entry only before {@link AnimationState#apply(Skeleton)} is first * called. */ public MixBlend getMixBlend () { return mixBlend; } public void setMixBlend (MixBlend mixBlend) { this.mixBlend = mixBlend; } /** The track entry for the previous animation when mixing from the previous animation to this animation, or null if no * mixing is currently occuring. When mixing from multiple animations, mixingFrom makes up a linked list. */ public TrackEntry getMixingFrom () { return mixingFrom; } /** The track entry for the next animation when mixing from this animation to the next animation, or null if no mixing is * currently occuring. When mixing to multiple animations, mixingTo makes up a linked list. */ public TrackEntry getMixingTo () { return mixingTo; } public void setHoldPrevious (boolean holdPrevious) { this.holdPrevious = holdPrevious; } /** If true, when mixing from the previous animation to this animation, the previous animation is applied as normal instead * of being mixed out. *

* When mixing between animations that key the same property, if a lower track also keys that property then the value will * briefly dip toward the lower track value during the mix. This happens because the first animation mixes from 100% to 0% * while the second animation mixes from 0% to 100%. Setting holdPrevious to true applies the first animation * at 100% during the mix so the lower track value is overwritten. Such dipping does not occur on the lowest track which * keys the property, only when a higher track also keys the property. *

* Snapping will occur if holdPrevious is true and this animation does not key all the same properties as the * previous animation. */ public boolean getHoldPrevious () { return holdPrevious; } /** Resets the rotation directions for mixing this entry's rotate timelines. This can be useful to avoid bones rotating the * long way around when using {@link #alpha} and starting animations on other tracks. *

* Mixing with {@link MixBlend#replace} involves finding a rotation between two others, which has two possible solutions: * the short way or the long way around. The two rotations likely change over time, so which direction is the short or long * way also changes. If the short way was always chosen, bones would flip to the other side when that direction became the * long way. TrackEntry chooses the short way the first time it is applied and remembers that direction. */ public void resetRotationDirections () { timelinesRotation.clear(); } public String toString () { return animation == null ? "" : animation.name; } } class EventQueue { private final Array objects = new Array(); boolean drainDisabled; public void start (TrackEntry entry) { objects.add(EventType.start); objects.add(entry); animationsChanged = true; } public void interrupt (TrackEntry entry) { objects.add(EventType.interrupt); objects.add(entry); } public void end (TrackEntry entry) { objects.add(EventType.end); objects.add(entry); animationsChanged = true; } public void dispose (TrackEntry entry) { objects.add(EventType.dispose); objects.add(entry); } public void complete (TrackEntry entry) { objects.add(EventType.complete); objects.add(entry); } public void event (TrackEntry entry, Event event) { objects.add(EventType.event); objects.add(entry); objects.add(event); } public void drain () { if (drainDisabled) return; // Not reentrant. drainDisabled = true; Array objects = this.objects; Array listeners = AnimationState.this.listeners; for (int i = 0; i < objects.size; i += 2) { EventType type = (EventType)objects.get(i); TrackEntry entry = (TrackEntry)objects.get(i + 1); switch (type) { case start: if (entry.listener != null) entry.listener.start(entry); for (int ii = 0; ii < listeners.size; ii++) listeners.get(ii).start(entry); break; case interrupt: if (entry.listener != null) entry.listener.interrupt(entry); for (int ii = 0; ii < listeners.size; ii++) listeners.get(ii).interrupt(entry); break; case end: if (entry.listener != null) entry.listener.end(entry); for (int ii = 0; ii < listeners.size; ii++) listeners.get(ii).end(entry); // Fall through. case dispose: if (entry.listener != null) entry.listener.dispose(entry); for (int ii = 0; ii < listeners.size; ii++) listeners.get(ii).dispose(entry); trackEntryPool.free(entry); break; case complete: if (entry.listener != null) entry.listener.complete(entry); for (int ii = 0; ii < listeners.size; ii++) listeners.get(ii).complete(entry); break; case event: Event event = (Event)objects.get(i++ + 2); if (entry.listener != null) entry.listener.event(entry, event); for (int ii = 0; ii < listeners.size; ii++) listeners.get(ii).event(entry, event); break; } } clear(); drainDisabled = false; } public void clear () { objects.clear(); } } static private enum EventType { start, interrupt, end, dispose, complete, event } /** The interface to implement for receiving TrackEntry events. It is always safe to call AnimationState methods when receiving * events. *

* See TrackEntry {@link TrackEntry#setListener(AnimationStateListener)} and AnimationState * {@link AnimationState#addListener(AnimationStateListener)}. */ static public interface AnimationStateListener { /** Invoked when this entry has been set as the current entry. */ public void start (TrackEntry entry); /** Invoked when another entry has replaced this entry as the current entry. This entry may continue being applied for * mixing. */ public void interrupt (TrackEntry entry); /** Invoked when this entry is no longer the current entry and will never be applied again. */ public void end (TrackEntry entry); /** Invoked when this entry will be disposed. This may occur without the entry ever being set as the current entry. * References to the entry should not be kept after dispose is called, as it may be destroyed or reused. */ public void dispose (TrackEntry entry); /** Invoked every time this entry's animation completes a loop. Because this event is trigged in * {@link AnimationState#apply(Skeleton)}, any animations set in response to the event won't be applied until the next time * the AnimationState is applied. */ public void complete (TrackEntry entry); /** Invoked when this entry's animation triggers an event. Because this event is trigged in * {@link AnimationState#apply(Skeleton)}, any animations set in response to the event won't be applied until the next time * the AnimationState is applied. */ public void event (TrackEntry entry, Event event); } static public abstract class AnimationStateAdapter implements AnimationStateListener { public void start (TrackEntry entry) { } public void interrupt (TrackEntry entry) { } public void end (TrackEntry entry) { } public void dispose (TrackEntry entry) { } public void complete (TrackEntry entry) { } public void event (TrackEntry entry, Event event) { } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy