diff --git a/spine-cpp/spine-cpp/include/spine/AnimationState.h b/spine-cpp/spine-cpp/include/spine/AnimationState.h
index 0085d2f6f..f5e00975e 100644
--- a/spine-cpp/spine-cpp/include/spine/AnimationState.h
+++ b/spine-cpp/spine-cpp/include/spine/AnimationState.h
@@ -401,28 +401,35 @@ namespace spine {
~AnimationState();
- /// Increments the track entry times, setting queued animations as current if needed
- /// @param delta delta time
+ /// Increments each track entry TrackEntry::getTrackTime(), setting queued animations as current if needed.
void update(float delta);
- /// 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.
+ /// Poses the skeleton using the track entry animations. The animation state is not changed, so can be applied to multiple
+ /// skeletons to pose them identically.
+ /// @return True if any animations were applied.
bool apply(Skeleton &skeleton);
- /// Removes all animations from all tracks, leaving skeletons in their previous pose.
- /// It may be desired to use AnimationState.setEmptyAnimations(float) to mix the skeletons back to the setup pose,
- /// rather than leaving them in their previous pose.
+ /// Removes all animations from all tracks, leaving skeletons in their current pose.
+ ///
+ /// It may be desired to use AnimationState::setEmptyAnimations(float) to mix the skeletons back to the setup pose,
+ /// rather than leaving them in their current pose.
void clearTracks();
- /// Removes all animations from the tracks, leaving skeletons in their previous pose.
- /// It may be desired to use AnimationState.setEmptyAnimations(float) to mix the skeletons back to the setup pose,
- /// rather than leaving them in their previous pose.
+ /// Removes all animations from the track, leaving skeletons in their current pose.
+ ///
+ /// It may be desired to use AnimationState::setEmptyAnimation(int, float) to mix the skeletons back to the setup pose,
+ /// rather than leaving them in their current pose.
void clearTrack(size_t trackIndex);
- /// Sets an animation by name. setAnimation(int, Animation, bool)
+ /// Sets an animation by name.
+ ///
+ /// See setAnimation(int, Animation, bool).
TrackEntry *setAnimation(size_t trackIndex, const String &animationName, bool loop);
/// Sets the current animation for a track, discarding any queued animations.
+ ///
+ /// If the formerly current track entry is for the same animation and 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 TrackEntry.TrackEnd determines when the track is cleared.
@@ -432,11 +439,12 @@ namespace spine {
TrackEntry *setAnimation(size_t trackIndex, Animation *animation, bool loop);
/// Queues an animation by name.
- /// addAnimation(int, Animation, bool, float)
+ ///
+ /// See addAnimation(int, Animation, bool, float).
TrackEntry *addAnimation(size_t trackIndex, const String &animationName, bool loop, float delay);
/// Adds an animation to be played delay seconds after the current or last queued animation
- /// for a track. If the track is empty, it is equivalent to calling setAnimation.
+ /// for a track. If the track has no entries, this is equivalent to calling setAnimation.
/// @param delay
/// Seconds to begin this animation after the start of the previous animation. May be <= 0 to use the animation
/// duration of the previous track minus any mix duration plus the negative delay.
@@ -445,37 +453,67 @@ namespace spine {
/// after AnimationState.Dispose
TrackEntry *addAnimation(size_t trackIndex, Animation *animation, bool loop, float delay);
- /// Sets an empty animation for a track, discarding any queued animations, and mixes to it over the specified mix duration.
+ /// Sets an empty animation for a track, discarding any queued animations, and sets the track entry's
+ /// 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 setEmptyAnimation(int, float),
+ /// setEmptyAnimations(float), or 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
+ /// addAnimation(int, Animation, bool, float) with the desired delay (an empty animation has a duration of 0) and on
+ /// the returned track entry, set the 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.
+ ///
+ /// See Empty animations in the Spine
+ /// Runtimes Guide.
TrackEntry *setEmptyAnimation(size_t trackIndex, float mixDuration);
- /// Adds an empty animation to be played after the current or last queued animation for a track, and mixes to it over the
- /// specified mix duration.
- /// @return
- /// A track entry to allow further customization of animation playback. References to the track entry must not be kept after AnimationState.Dispose.
- ///
- /// @param trackIndex Track number.
- /// @param mixDuration Mix duration.
- /// @param delay Seconds to begin this animation after the start of the previous animation. May be <= 0 to use the animation
- /// duration of the previous track minus any mix duration plus the negative delay.
+ /// Adds an empty animation to be played after the current or last queued animation for a track, and sets the track entry's
+ /// TrackEntry::getMixDuration(). If the track has no entries, it is equivalent to calling
+ /// setEmptyAnimation(int, float).
+ ///
+ /// See setEmptyAnimation(int, float) and
+ /// Empty animations in the Spine
+ /// Runtimes Guide.
+ /// @param delay If > 0, sets 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 AnimationStateListener::dispose(TrackEntry) event occurs.
TrackEntry *addEmptyAnimation(size_t trackIndex, float mixDuration, float delay);
/// Sets an empty animation for every track, discarding any queued animations, and mixes to it over the specified mix duration.
+ ///
+ /// See Empty animations in the Spine
+ /// Runtimes Guide.
void setEmptyAnimations(float mixDuration);
/// @return The track entry for the animation currently playing on the track, or NULL if no animation is currently playing.
TrackEntry *getCurrent(size_t trackIndex);
+ /// The AnimationStateData to look up mix durations.
AnimationStateData *getData();
- /// A list of tracks that have animations, which may contain NULLs.
+ /// The list of tracks that have had animations, which may contain null entries for tracks that currently have no animation.
Vector &getTracks();
+ /// 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 TrackEntry::getTimeScale() for affecting a single animation.
float getTimeScale();
void setTimeScale(float inValue);
+ /// Adds a listener to receive events for all track entries.
void setListener(AnimationStateListener listener);
+ /// Adds a listener to receive events for all track entries.
void setListener(AnimationStateListenerObject *listener);
void disableQueue();
@@ -519,12 +557,18 @@ namespace spine {
static Animation *getEmptyAnimation();
+ /// Applies the rotate timeline, mixing with the current pose while keeping the same rotation direction chosen as the shortest
+ /// the first time the mixing was applied.
static void
applyRotateTimeline(RotateTimeline *rotateTimeline, Skeleton &skeleton, float time, float alpha, MixBlend pose,
Vector &timelinesRotation, size_t i, bool firstFrame);
+ /// Applies the attachment timeline and sets Slot::attachmentState.
+ /// @param attachments False when: 1) the attachment timeline is mixing out, 2) mix < attachmentThreshold, and 3) the timeline
+ /// is not the last timeline to set the slot's attachment. In that case the timeline is applied only so subsequent
+ /// timelines see any deform.
void applyAttachmentTimeline(AttachmentTimeline *attachmentTimeline, Skeleton &skeleton, float animationTime,
- MixBlend pose, bool firstFrame);
+ MixBlend pose, bool attachments);
/// Returns true when all mixing from entries are complete.
bool updateMixingFrom(TrackEntry *to, float delta);
@@ -536,7 +580,7 @@ namespace spine {
/// Sets the active TrackEntry for a given track number.
void setCurrent(size_t index, TrackEntry *current, bool interrupt);
- /// Removes the next entry and all entries after it for the specified entry. */
+ /// Removes the TrackEntry::getNext() next entry and all entries after it for the specified entry.
void clearNext(TrackEntry *entry);
TrackEntry *expandToIndex(size_t index);
diff --git a/spine-cpp/spine-cpp/include/spine/BoneLocal.h b/spine-cpp/spine-cpp/include/spine/BoneLocal.h
index c319d7728..20b821abc 100644
--- a/spine-cpp/spine-cpp/include/spine/BoneLocal.h
+++ b/spine-cpp/spine-cpp/include/spine/BoneLocal.h
@@ -68,6 +68,7 @@ namespace spine {
friend class ToScaleY;
friend class FromShearY;
friend class ToShearY;
+ friend class AnimationState;
protected:
float _x, _y, _rotation, _scaleX, _scaleY, _shearX, _shearY;
diff --git a/spine-cpp/spine-cpp/include/spine/Slot.h b/spine-cpp/spine-cpp/include/spine/Slot.h
index 34850502f..cae449f24 100644
--- a/spine-cpp/spine-cpp/include/spine/Slot.h
+++ b/spine-cpp/spine-cpp/include/spine/Slot.h
@@ -91,6 +91,8 @@ namespace spine {
friend class TwoColorTimeline;
+ friend class AnimationState;
+
public:
Slot(SlotData &data, Skeleton &skeleton);
diff --git a/spine-cpp/spine-cpp/include/spine/SlotPose.h b/spine-cpp/spine-cpp/include/spine/SlotPose.h
index faa55debc..e8cdb3f61 100644
--- a/spine-cpp/spine-cpp/include/spine/SlotPose.h
+++ b/spine-cpp/spine-cpp/include/spine/SlotPose.h
@@ -51,6 +51,7 @@ namespace spine {
friend class RGB2Timeline;
friend class PathConstraint;
friend class SkeletonJson;
+ friend class AnimationState;
protected:
Color _color;
diff --git a/spine-cpp/spine-cpp/src/spine/AnimationState.cpp b/spine-cpp/spine-cpp/src/spine/AnimationState.cpp
index a4cb37648..8d65edbae 100644
--- a/spine-cpp/spine-cpp/src/spine/AnimationState.cpp
+++ b/spine-cpp/spine-cpp/src/spine/AnimationState.cpp
@@ -492,7 +492,7 @@ bool AnimationState::apply(Skeleton &skeleton) {
applyAttachmentTimeline(static_cast(timeline), skeleton, applyTime, blend,
attachments);
else
- timeline->apply(skeleton, animationLast, applyTime, applyEvents, alpha, blend, MixDirection_In);
+ timeline->apply(skeleton, animationLast, applyTime, applyEvents, alpha, blend, MixDirection_In, false);
}
} else {
Vector &timelineMode = current._timelineMode;
@@ -516,7 +516,7 @@ bool AnimationState::apply(Skeleton &skeleton) {
blend, attachments);
else
timeline->apply(skeleton, animationLast, applyTime, applyEvents, alpha, timelineBlend,
- MixDirection_In);
+ MixDirection_In, false);
}
}
@@ -530,9 +530,9 @@ bool AnimationState::apply(Skeleton &skeleton) {
Vector &slots = skeleton.getSlots();
for (int i = 0, n = (int) slots.size(); i < n; i++) {
Slot *slot = slots[i];
- if (slot->getAttachmentState() == setupState) {
+ if (slot->_attachmentState == setupState) {
const String &attachmentName = slot->getData().getAttachmentName();
- slot->setAttachment(attachmentName.isEmpty() ? NULL : skeleton.getAttachment(slot->getData().getIndex(), attachmentName));
+ slot->_pose.setAttachment(attachmentName.isEmpty() ? NULL : skeleton.getAttachment(slot->getData().getIndex(), attachmentName));
}
}
_unkeyedState += 2;
@@ -589,7 +589,7 @@ TrackEntry *AnimationState::setAnimation(size_t trackIndex, Animation *animation
bool interrupt = true;
TrackEntry *current = expandToIndex(trackIndex);
if (current != NULL) {
- if (current->_nextTrackLast == -1) {
+ if (current->_nextTrackLast == -1 && current->_animation == animation) {
// Don't mix from an entry that was never applied.
_tracks[trackIndex] = current->_mixingFrom;
_queue->interrupt(current);
@@ -740,7 +740,7 @@ void AnimationState::applyAttachmentTimeline(AttachmentTimeline *attachmentTimel
}
/* If an attachment wasn't set (ie before the first frame or attachments is false), set the setup attachment later.*/
- if (slot->getAttachmentState() <= _unkeyedState) slot->setAttachmentState(_unkeyedState + Setup);
+ if (slot->_attachmentState <= _unkeyedState) slot->_attachmentState = _unkeyedState + Setup;
}
@@ -749,27 +749,29 @@ void AnimationState::applyRotateTimeline(RotateTimeline *rotateTimeline, Skeleto
if (firstFrame) timelinesRotation[i] = 0;
if (alpha == 1) {
- rotateTimeline->apply(skeleton, 0, time, NULL, 1, blend, MixDirection_In);
+ static_cast(rotateTimeline)->apply(skeleton, 0, time, NULL, 1, blend, MixDirection_In, false);
return;
}
Bone *bone = skeleton._bones[rotateTimeline->_boneIndex];
if (!bone->isActive()) return;
+ BoneLocal &pose = bone->_pose;
+ BoneLocal &setup = bone->_data._setup;
Vector &frames = rotateTimeline->_frames;
float r1, r2;
if (time < frames[0]) {
switch (blend) {
case MixBlend_Setup:
- bone->_rotation = bone->_data._rotation;
+ pose._rotation = setup._rotation;
default:
return;
case MixBlend_First:
- r1 = bone->_rotation;
- r2 = bone->_data._rotation;
+ r1 = pose._rotation;
+ r2 = setup._rotation;
}
} else {
- r1 = blend == MixBlend_Setup ? bone->_data._rotation : bone->_rotation;
- r2 = bone->_data._rotation + rotateTimeline->getCurveValue(time);
+ r1 = blend == MixBlend_Setup ? setup._rotation : pose._rotation;
+ r2 = setup._rotation + rotateTimeline->getCurveValue(time);
}
// Mix between rotations using the direction of the shortest route on the first frame while detecting crosses.
@@ -804,7 +806,7 @@ void AnimationState::applyRotateTimeline(RotateTimeline *rotateTimeline, Skeleto
timelinesRotation[i] = total;
}
timelinesRotation[i + 1] = diff;
- bone->_rotation = r1 + total * alpha;
+ pose._rotation = r1 + total * alpha;
}
bool AnimationState::updateMixingFrom(TrackEntry *to, float delta) {
@@ -868,7 +870,7 @@ float AnimationState::applyMixingFrom(TrackEntry *to, Skeleton &skeleton, MixBle
if (blend == MixBlend_Add) {
for (size_t i = 0; i < timelineCount; i++)
- timelines[i]->apply(skeleton, animationLast, applyTime, events, alphaMix, blend, MixDirection_Out);
+ timelines[i]->apply(skeleton, animationLast, applyTime, events, alphaMix, blend, MixDirection_Out, false);
} else {
Vector &timelineMode = from->_timelineMode;
Vector &timelineHoldMix = from->_timelineHoldMix;
@@ -920,7 +922,7 @@ float AnimationState::applyMixingFrom(TrackEntry *to, Skeleton &skeleton, MixBle
if (drawOrder && timeline->getRTTI().isExactly(DrawOrderTimeline::rtti) &&
timelineBlend == MixBlend_Setup)
direction = MixDirection_In;
- timeline->apply(skeleton, animationLast, applyTime, events, alpha, timelineBlend, direction);
+ timeline->apply(skeleton, animationLast, applyTime, events, alpha, timelineBlend, direction, false);
}
}
}
@@ -937,9 +939,9 @@ float AnimationState::applyMixingFrom(TrackEntry *to, Skeleton &skeleton, MixBle
}
void AnimationState::setAttachment(Skeleton &skeleton, Slot &slot, const String &attachmentName, bool attachments) {
- slot.setAttachment(
+ slot._pose.setAttachment(
attachmentName.isEmpty() ? NULL : skeleton.getAttachment(slot.getData().getIndex(), attachmentName));
- if (attachments) slot.setAttachmentState(_unkeyedState + Current);
+ if (attachments) slot._attachmentState = _unkeyedState + Current;
}
void AnimationState::queueEvents(TrackEntry *entry, float animationTime) {