[libgdx] Added slider mix timeline, JSON/binary fixes.

This commit is contained in:
Nathan Sweet 2025-04-21 17:21:18 -04:00
parent 26df02443f
commit f0c61be159
3 changed files with 229 additions and 132 deletions

View File

@ -182,7 +182,8 @@ public class Animation {
pathConstraintPosition, pathConstraintSpacing, pathConstraintMix, //
physicsConstraintInertia, physicsConstraintStrength, physicsConstraintDamping, physicsConstraintMass, //
physicsConstraintWind, physicsConstraintGravity, physicsConstraintMix, physicsConstraintReset, //
sequence
sequence, //
sliderMix
}
/** The base class for all timelines. */
@ -1711,6 +1712,107 @@ public class Animation {
}
}
/** Changes a slot's {@link SlotPose#getSequenceIndex()} for an attachment's {@link Sequence}. */
static public class SequenceTimeline extends Timeline implements SlotTimeline {
static public final int ENTRIES = 3;
static private final int MODE = 1, DELAY = 2;
final int slotIndex;
final HasTextureRegion attachment;
public SequenceTimeline (int frameCount, int slotIndex, Attachment attachment) {
super(frameCount,
Property.sequence.ordinal() + "|" + slotIndex + "|" + ((HasTextureRegion)attachment).getSequence().getId());
this.slotIndex = slotIndex;
this.attachment = (HasTextureRegion)attachment;
}
public int getFrameEntries () {
return ENTRIES;
}
public int getSlotIndex () {
return slotIndex;
}
public Attachment getAttachment () {
return (Attachment)attachment;
}
/** Sets the time, mode, index, and frame time for the specified frame.
* @param frame Between 0 and <code>frameCount</code>, inclusive.
* @param time Seconds between frames. */
public void setFrame (int frame, float time, SequenceMode mode, int index, float delay) {
frame *= ENTRIES;
frames[frame] = time;
frames[frame + MODE] = mode.ordinal() | (index << 4);
frames[frame + DELAY] = delay;
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction, boolean appliedPose) {
Slot slot = skeleton.slots.items[slotIndex];
if (!slot.bone.active) return;
SlotPose pose = appliedPose ? slot.applied : slot.pose;
Attachment slotAttachment = pose.attachment;
if (slotAttachment != attachment) {
if (!(slotAttachment instanceof VertexAttachment vertexAttachment)
|| vertexAttachment.getTimelineAttachment() != attachment) return;
}
Sequence sequence = ((HasTextureRegion)slotAttachment).getSequence();
if (sequence == null) return;
if (direction == out) {
if (blend == setup) pose.setSequenceIndex(-1);
return;
}
float[] frames = this.frames;
if (time < frames[0]) {
if (blend == setup || blend == first) pose.setSequenceIndex(-1);
return;
}
int i = search(frames, time, ENTRIES);
float before = frames[i];
int modeAndIndex = (int)frames[i + MODE];
float delay = frames[i + DELAY];
int index = modeAndIndex >> 4, count = sequence.getRegions().length;
SequenceMode mode = SequenceMode.values[modeAndIndex & 0xf];
if (mode != SequenceMode.hold) {
index += (time - before) / delay + 0.0001f;
switch (mode) {
case once:
index = Math.min(count - 1, index);
break;
case loop:
index %= count;
break;
case pingpong: {
int n = (count << 1) - 2;
index = n == 0 ? 0 : index % n;
if (index >= count) index = n - index;
break;
}
case onceReverse:
index = Math.max(count - 1 - index, 0);
break;
case loopReverse:
index = count - 1 - (index % count);
break;
case pingpongReverse:
int n = (count << 1) - 2;
index = n == 0 ? 0 : (index + count - 1) % n;
if (index >= count) index = n - index;
}
}
pose.setSequenceIndex(index);
}
}
/** Fires an {@link Event} when specific animation times are reached. */
static public class EventTimeline extends Timeline {
static private final String[] propertyIds = {Integer.toString(Property.event.ordinal())};
@ -1827,6 +1929,7 @@ public class Animation {
}
static public interface ConstraintTimeline {
/** The index of the constraint in {@link Skeleton#getConstraints()} that will be changed when this timeline is applied. */
public int getConstraintIndex ();
}
@ -1848,8 +1951,6 @@ public class Animation {
return ENTRIES;
}
/** The index of the IK constraint in {@link Skeleton#getConstraints()} that will be changed when this timeline is
* applied. */
public int getConstraintIndex () {
return constraintIndex;
}
@ -1960,8 +2061,6 @@ public class Animation {
return ENTRIES;
}
/** The index of the transform constraint in {@link Skeleton#getConstraints()} that will be changed when this timeline is
* applied. */
public int getConstraintIndex () {
return constraintIndex;
}
@ -2075,8 +2174,6 @@ public class Animation {
this.constraintIndex = constraintIndex;
}
/** The index of the path constraint in {@link Skeleton#getConstraints()} that will be changed when this timeline is
* applied. */
public int getConstraintIndex () {
return constraintIndex;
}
@ -2101,8 +2198,6 @@ public class Animation {
this.constraintIndex = constraintIndex;
}
/** The index of the path constraint in {@link Skeleton#getConstraints()} that will be changed when this timeline is
* applied. */
public int getConstraintIndex () {
return constraintIndex;
}
@ -2135,8 +2230,6 @@ public class Animation {
return ENTRIES;
}
/** The index of the path constraint in {@link Skeleton#getConstraints()} that will be changed when this timeline is
* applied. */
public int getConstraintIndex () {
return constraintIndex;
}
@ -2223,8 +2316,8 @@ public class Animation {
this.constraintIndex = constraintIndex;
}
/** The index of the physics constraint in {@link Skeleton#getPhysicsConstraints()} that will be changed when this timeline
* is applied, or -1 if all physics constraints in the skeleton will be changed. */
/** The index of the physics constraint in {@link Skeleton#getConstraints()} that will be changed when this timeline is
* applied, or -1 if all physics constraints in the skeleton will be changed. */
public int getConstraintIndex () {
return constraintIndex;
}
@ -2404,7 +2497,7 @@ public class Animation {
this.constraintIndex = constraintIndex;
}
/** The index of the physics constraint in {@link Skeleton#getPhysicsConstraints()} that will be reset when this timeline is
/** The index of the physics constraint in {@link Skeleton#getConstraints()} that will be reset when this timeline is
* applied, or -1 if all physics constraints in the skeleton will be reset. */
public int getConstraintIndex () {
return constraintIndex;
@ -2453,104 +2546,27 @@ public class Animation {
}
}
/** Changes a slot's {@link SlotPose#getSequenceIndex()} for an attachment's {@link Sequence}. */
static public class SequenceTimeline extends Timeline implements SlotTimeline {
static public final int ENTRIES = 3;
static private final int MODE = 1, DELAY = 2;
/** Changes a slider's {@link SliderPose#getMix()}. */
static public class SliderMixTimeline extends CurveTimeline1 implements ConstraintTimeline {
final int constraintIndex;
final int slotIndex;
final HasTextureRegion attachment;
public SequenceTimeline (int frameCount, int slotIndex, Attachment attachment) {
super(frameCount,
Property.sequence.ordinal() + "|" + slotIndex + "|" + ((HasTextureRegion)attachment).getSequence().getId());
this.slotIndex = slotIndex;
this.attachment = (HasTextureRegion)attachment;
public SliderMixTimeline (int frameCount, int bezierCount, int constraintIndex) {
super(frameCount, bezierCount, Property.sliderMix.ordinal() + "|" + constraintIndex);
this.constraintIndex = constraintIndex;
}
public int getFrameEntries () {
return ENTRIES;
}
public int getSlotIndex () {
return slotIndex;
}
public Attachment getAttachment () {
return (Attachment)attachment;
}
/** Sets the time, mode, index, and frame time for the specified frame.
* @param frame Between 0 and <code>frameCount</code>, inclusive.
* @param time Seconds between frames. */
public void setFrame (int frame, float time, SequenceMode mode, int index, float delay) {
frame *= ENTRIES;
frames[frame] = time;
frames[frame + MODE] = mode.ordinal() | (index << 4);
frames[frame + DELAY] = delay;
public int getConstraintIndex () {
return constraintIndex;
}
public void apply (Skeleton skeleton, float lastTime, float time, @Null Array<Event> events, float alpha, MixBlend blend,
MixDirection direction, boolean appliedPose) {
Slot slot = skeleton.slots.items[slotIndex];
if (!slot.bone.active) return;
SlotPose pose = appliedPose ? slot.applied : slot.pose;
Attachment slotAttachment = pose.attachment;
if (slotAttachment != attachment) {
if (!(slotAttachment instanceof VertexAttachment vertexAttachment)
|| vertexAttachment.getTimelineAttachment() != attachment) return;
var constraint = (Slider)skeleton.constraints.items[constraintIndex];
if (constraint.active) {
SliderPose pose = appliedPose ? constraint.applied : constraint.pose;
pose.mix = getAbsoluteValue(time, alpha, blend, pose.mix, constraint.data.setup.mix);
}
Sequence sequence = ((HasTextureRegion)slotAttachment).getSequence();
if (sequence == null) return;
if (direction == out) {
if (blend == setup) pose.setSequenceIndex(-1);
return;
}
float[] frames = this.frames;
if (time < frames[0]) {
if (blend == setup || blend == first) pose.setSequenceIndex(-1);
return;
}
int i = search(frames, time, ENTRIES);
float before = frames[i];
int modeAndIndex = (int)frames[i + MODE];
float delay = frames[i + DELAY];
int index = modeAndIndex >> 4, count = sequence.getRegions().length;
SequenceMode mode = SequenceMode.values[modeAndIndex & 0xf];
if (mode != SequenceMode.hold) {
index += (time - before) / delay + 0.0001f;
switch (mode) {
case once:
index = Math.min(count - 1, index);
break;
case loop:
index %= count;
break;
case pingpong: {
int n = (count << 1) - 2;
index = n == 0 ? 0 : index % n;
if (index >= count) index = n - index;
break;
}
case onceReverse:
index = Math.max(count - 1 - index, 0);
break;
case loopReverse:
index = count - 1 - (index % count);
break;
case pingpongReverse:
int n = (count << 1) - 2;
index = n == 0 ? 0 : (index + count - 1) % n;
if (index >= count) index = n - index;
}
}
pose.setSequenceIndex(index);
}
}
}

View File

@ -76,6 +76,7 @@ import com.esotericsoftware.spine.Animation.SequenceTimeline;
import com.esotericsoftware.spine.Animation.ShearTimeline;
import com.esotericsoftware.spine.Animation.ShearXTimeline;
import com.esotericsoftware.spine.Animation.ShearYTimeline;
import com.esotericsoftware.spine.Animation.SliderMixTimeline;
import com.esotericsoftware.spine.Animation.Timeline;
import com.esotericsoftware.spine.Animation.TransformConstraintTimeline;
import com.esotericsoftware.spine.Animation.TranslateTimeline;
@ -141,6 +142,7 @@ public class SkeletonBinary extends SkeletonLoader {
static public final int CONSTRAINT_PATH = 1;
static public final int CONSTRAINT_TRANSFORM = 2;
static public final int CONSTRAINT_PHYSICS = 3;
static public final int CONSTRAINT_SLIDER = 4;
static public final int ATTACHMENT_DEFORM = 0;
static public final int ATTACHMENT_SEQUENCE = 1;
@ -158,6 +160,8 @@ public class SkeletonBinary extends SkeletonLoader {
static public final int PHYSICS_MIX = 7;
static public final int PHYSICS_RESET = 8;
static public final int SLIDER_MIX = 0;
static public final int CURVE_LINEAR = 0;
static public final int CURVE_STEPPED = 1;
static public final int CURVE_BEZIER = 2;
@ -259,12 +263,15 @@ public class SkeletonBinary extends SkeletonLoader {
}
// Constraints.
o = skeletonData.constraints.setSize(n = input.readInt(true));
for (int i = 0, nn; i < n; i++) {
int constraintCount = input.readInt(true);
ConstraintData[] constraints = skeletonData.constraints.setSize(constraintCount);
for (int i = 0; i < constraintCount; i++) {
String name = input.readString();
int nn = input.readInt(true);
switch (input.readByte()) {
case CONSTRAINT_IK -> {
var data = new IkConstraintData(input.readString());
BoneData[] constraintBones = data.bones.setSize(nn = input.readInt(true));
var data = new IkConstraintData(name);
BoneData[] constraintBones = data.bones.setSize(nn);
for (int ii = 0; ii < nn; ii++)
constraintBones[ii] = bones[input.readInt(true)];
data.target = bones[input.readInt(true)];
@ -277,11 +284,11 @@ public class SkeletonBinary extends SkeletonLoader {
setup.stretch = (flags & 16) != 0;
if ((flags & 32) != 0) setup.mix = (flags & 64) != 0 ? input.readFloat() : 1;
if ((flags & 128) != 0) setup.softness = input.readFloat() * scale;
o[i] = data;
constraints[i] = data;
}
case CONSTRAINT_TRANSFORM -> {
var data = new TransformConstraintData(input.readString());
BoneData[] constraintBones = data.bones.setSize(nn = input.readInt(true));
var data = new TransformConstraintData(name);
BoneData[] constraintBones = data.bones.setSize(nn);
for (int ii = 0; ii < nn; ii++)
constraintBones[ii] = bones[input.readInt(true)];
data.source = bones[input.readInt(true)];
@ -352,11 +359,11 @@ public class SkeletonBinary extends SkeletonLoader {
if ((flags & 8) != 0) setup.mixScaleX = input.readFloat();
if ((flags & 16) != 0) setup.mixScaleY = input.readFloat();
if ((flags & 32) != 0) setup.mixShearY = input.readFloat();
o[i] = data;
constraints[i] = data;
}
case CONSTRAINT_PATH -> {
var data = new PathConstraintData(input.readString());
BoneData[] constraintBones = data.bones.setSize(nn = input.readInt(true));
var data = new PathConstraintData(name);
BoneData[] constraintBones = data.bones.setSize(nn);
for (int ii = 0; ii < nn; ii++)
constraintBones[ii] = bones[input.readInt(true)];
data.slot = slots[input.readInt(true)];
@ -374,11 +381,11 @@ public class SkeletonBinary extends SkeletonLoader {
setup.mixRotate = input.readFloat();
setup.mixX = input.readFloat();
setup.mixY = input.readFloat();
o[i] = data;
constraints[i] = data;
}
case CONSTRAINT_PHYSICS -> {
var data = new PhysicsConstraintData(input.readString());
data.bone = bones[input.readInt(true)];
var data = new PhysicsConstraintData(name);
data.bone = bones[nn];
int flags = input.read();
data.skinRequired = (flags & 1) != 0;
if ((flags & 2) != 0) data.x = input.readFloat();
@ -404,7 +411,14 @@ public class SkeletonBinary extends SkeletonLoader {
if ((flags & 32) != 0) data.gravityGlobal = true;
if ((flags & 64) != 0) data.mixGlobal = true;
setup.mix = (flags & 128) != 0 ? input.readFloat() : 1;
o[i] = data;
constraints[i] = data;
}
case CONSTRAINT_SLIDER -> {
var data = new SliderData(name);
data.skinRequired = (nn & 1) != 0;
if ((nn & 2) != 0) data.setup.mix = (nn & 4) != 0 ? input.readFloat() : 1;
if ((nn & 8) != 0) data.setup.time = input.readFloat();
constraints[i] = data;
}
}
}
@ -454,10 +468,12 @@ public class SkeletonBinary extends SkeletonLoader {
}
// Animations.
o = skeletonData.animations.setSize(n = input.readInt(true));
Animation[] animations = skeletonData.animations.setSize(n = input.readInt(true));
for (int i = 0; i < n; i++)
o[i] = readAnimation(input, input.readString(), skeletonData);
animations[i] = readAnimation(input, input.readString(), skeletonData);
for (int i = 0; i < constraintCount; i++)
if (constraints[i] instanceof SliderData data) data.animation = animations[input.readInt(true)];
} catch (IOException ex) {
throw new SerializationException("Error reading skeleton file.", ex);
} finally {
@ -1034,6 +1050,32 @@ public class SkeletonBinary extends SkeletonLoader {
}
}
// Slider timelines.
for (int i = 0, n = input.readInt(true); i < n; i++) {
int index = input.readInt(true);
for (int ii = 0, nn = input.readInt(true); ii < nn; ii++) {
int type = input.readByte(), frameCount = input.readInt(true), bezierCount = input.readInt(true);
switch (type) {
case SLIDER_MIX -> {
var timeline = new SliderMixTimeline(frameCount, bezierCount, index);
float time = input.readFloat(), mix = input.readFloat();
for (int frame = 0, bezier = 0, frameLast = timeline.getFrameCount() - 1;; frame++) {
timeline.setFrame(frame, time, mix);
if (frame == frameLast) break;
float time2 = input.readFloat(), mix2 = input.readFloat();
switch (input.readByte()) {
case CURVE_STEPPED -> timeline.setStepped(frame);
case CURVE_BEZIER -> setBezier(input, timeline, bezier++, frame, 0, time, time2, mix, mix2, 1);
}
time = time2;
mix = mix2;
}
timelines.add(timeline);
}
}
}
}
// Attachment timelines.
for (int i = 0, n = input.readInt(true); i < n; i++) {
Skin skin = skeletonData.skins.items[input.readInt(true)];

View File

@ -77,6 +77,7 @@ import com.esotericsoftware.spine.Animation.SequenceTimeline;
import com.esotericsoftware.spine.Animation.ShearTimeline;
import com.esotericsoftware.spine.Animation.ShearXTimeline;
import com.esotericsoftware.spine.Animation.ShearYTimeline;
import com.esotericsoftware.spine.Animation.SliderMixTimeline;
import com.esotericsoftware.spine.Animation.Timeline;
import com.esotericsoftware.spine.Animation.TransformConstraintTimeline;
import com.esotericsoftware.spine.Animation.TranslateTimeline;
@ -217,10 +218,12 @@ public class SkeletonJson extends SkeletonLoader {
// Constraints.
for (JsonValue constraintMap = root.getChild("constraints"); constraintMap != null; constraintMap = constraintMap.next) {
String name = constraintMap.getString("name");
boolean skinRequired = constraintMap.getBoolean("skin", false);
switch (constraintMap.getString("type")) {
case "ik" -> {
var data = new IkConstraintData(constraintMap.getString("name"));
data.skinRequired = constraintMap.getBoolean("skin", false);
var data = new IkConstraintData(name);
data.skinRequired = skinRequired;
for (JsonValue entry = constraintMap.getChild("bones"); entry != null; entry = entry.next) {
BoneData bone = skeletonData.findBone(entry.asString());
@ -243,8 +246,8 @@ public class SkeletonJson extends SkeletonLoader {
skeletonData.constraints.add(data);
}
case "transform" -> {
var data = new TransformConstraintData(constraintMap.getString("name"));
data.skinRequired = constraintMap.getBoolean("skin", false);
var data = new TransformConstraintData(name);
data.skinRequired = skinRequired;
for (JsonValue entry = constraintMap.getChild("bones"); entry != null; entry = entry.next) {
BoneData bone = skeletonData.findBone(entry.asString());
@ -340,8 +343,8 @@ public class SkeletonJson extends SkeletonLoader {
skeletonData.constraints.add(data);
}
case "path" -> {
var data = new PathConstraintData(constraintMap.getString("name"));
data.skinRequired = constraintMap.getBoolean("skin", false);
var data = new PathConstraintData(name);
data.skinRequired = skinRequired;
for (JsonValue entry = constraintMap.getChild("bones"); entry != null; entry = entry.next) {
BoneData bone = skeletonData.findBone(entry.asString());
@ -369,8 +372,8 @@ public class SkeletonJson extends SkeletonLoader {
skeletonData.constraints.add(data);
}
case "physics" -> {
var data = new PhysicsConstraintData(constraintMap.getString("name"));
data.skinRequired = constraintMap.getBoolean("skin", false);
var data = new PhysicsConstraintData(name);
data.skinRequired = skinRequired;
String boneName = constraintMap.getString("bone");
data.bone = skeletonData.findBone(boneName);
@ -384,9 +387,9 @@ public class SkeletonJson extends SkeletonLoader {
data.limit = constraintMap.getFloat("limit", 5000) * scale;
data.step = 1f / constraintMap.getInt("fps", 60);
PhysicsConstraintPose setup = data.setup;
setup.inertia = constraintMap.getFloat("inertia", 1);
setup.inertia = constraintMap.getFloat("inertia", 0.5f);
setup.strength = constraintMap.getFloat("strength", 100);
setup.damping = constraintMap.getFloat("damping", 1);
setup.damping = constraintMap.getFloat("damping", 0.85f);
setup.massInverse = 1 / constraintMap.getFloat("mass", 1);
setup.wind = constraintMap.getFloat("wind", 0);
setup.gravity = constraintMap.getFloat("gravity", 0);
@ -401,7 +404,13 @@ public class SkeletonJson extends SkeletonLoader {
skeletonData.constraints.add(data);
}
// BOZO! - Sliders.
case "slider" -> {
var data = new SliderData(name);
data.skinRequired = skinRequired;
data.setup.time = constraintMap.getFloat("time", 0);
data.setup.mix = constraintMap.getFloat("mix", 1);
skeletonData.constraints.add(data);
}
}
}
@ -492,6 +501,16 @@ public class SkeletonJson extends SkeletonLoader {
}
}
// Slider animations.
for (JsonValue constraintMap = root.getChild("constraints"); constraintMap != null; constraintMap = constraintMap.next) {
if (constraintMap.getString("type").equals("slider")) {
SliderData data = skeletonData.findConstraint(constraintMap.getString("name"), SliderData.class);
String animationName = constraintMap.getString("animation");
data.animation = skeletonData.findAnimation(animationName);
if (data.animation == null) throw new SerializationException("Slider animation not found: " + animationName);
}
}
skeletonData.bones.shrink();
skeletonData.slots.shrink();
skeletonData.skins.shrink();
@ -1032,6 +1051,7 @@ public class SkeletonJson extends SkeletonLoader {
int frames = timelineMap.size;
CurveTimeline1 timeline;
float defaultValue = 0;
switch (timelineMap.name) {
case "reset" -> {
var resetTimeline = new PhysicsConstraintResetTimeline(frames, index);
@ -1046,12 +1066,31 @@ public class SkeletonJson extends SkeletonLoader {
case "mass" -> timeline = new PhysicsConstraintMassTimeline(frames, frames, index);
case "wind" -> timeline = new PhysicsConstraintWindTimeline(frames, frames, index);
case "gravity" -> timeline = new PhysicsConstraintGravityTimeline(frames, frames, index);
case "mix" -> timeline = new PhysicsConstraintMixTimeline(frames, frames, index);
case "mix" -> {
defaultValue = 1;
timeline = new PhysicsConstraintMixTimeline(frames, frames, index);
}
default -> {
continue;
}
}
timelines.add(readTimeline(keyMap, timeline, 0, 1));
timelines.add(readTimeline(keyMap, timeline, defaultValue, 1));
}
}
// Slider timelines.
for (JsonValue constraintMap = map.getChild("slider"); constraintMap != null; constraintMap = constraintMap.next) {
SliderData constraint = skeletonData.findConstraint(constraintMap.name, SliderData.class);
if (constraint == null) throw new SerializationException("Slider not found: " + constraintMap.name);
int index = skeletonData.constraints.indexOf(constraint, true);
for (JsonValue timelineMap = constraintMap.child; timelineMap != null; timelineMap = timelineMap.next) {
JsonValue keyMap = timelineMap.child;
if (keyMap == null) continue;
int frames = timelineMap.size;
switch (timelineMap.name) {
case "mix" -> timelines.add(readTimeline(keyMap, new SliderMixTimeline(frames, frames, index), 1, 1));
}
}
}