From 751ee5972cbbde0a4806cd5e51d099b837b4785f Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sat, 5 Jul 2025 04:04:10 +0200 Subject: [PATCH] [cpp] 4.3 porting WIP --- .../spine-cpp/include/spine/AnimationState.h | 96 ++++++++++++++----- spine-cpp/spine-cpp/include/spine/BoneLocal.h | 1 + spine-cpp/spine-cpp/include/spine/Slot.h | 2 + spine-cpp/spine-cpp/include/spine/SlotPose.h | 1 + .../spine-cpp/src/spine/AnimationState.cpp | 36 +++---- 5 files changed, 93 insertions(+), 43 deletions(-) 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) {