pixilestudios

  • Oct 14, 2024
  • Joined May 2, 2018
  • Ah yeah that'd be an interesting approach, although probably a lot of bytes to send.

  • Nate Ah yeah so I dug a bit deeper to try and capture the full chain and as many variables as possible but it seems I still can't get it to match properly during mix. I also realized I was not applying <empty> animations as well, but fixing that didn't change the outcome. And I did confirm that there was a linked list of mixingFrom going on.
    On the sending client I changed to this:

    foreach (var track in cachedSkeletonAnimationRef.AnimationState.Tracks) {
        if (track != null && track.Animation != null) {
            var animStruct = new AnimationIndexTimeTrack();
            animStruct.trackIndex = (byte) track.TrackIndex;
            animStruct.animName = track.Animation.Name;
            animStruct.animTime = track.TrackTime;
            animStruct.looping = track.Loop;
            animStruct.mixDuration = track.MixDuration;
            animStruct.mixTime = track.MixTime;
            reuseableArrayAnimIndexesHitReg.Add(animStruct);
            var mixingFrom = track.MixingFrom;
            while (mixingFrom != null) {
                var animStruct2 = new AnimationIndexTimeTrack();
                animStruct2.trackIndex = (byte) track.TrackIndex;
                animStruct2.animName = mixingFrom.Animation.Name;
                animStruct2.animTime = mixingFrom.MixTime;
                animStruct2.looping = mixingFrom.Loop;
                animStruct2.isMixingFrom = true;
                animStruct2.mixDuration = mixingFrom.MixDuration;
                reuseableArrayAnimIndexesHitReg.Add(animStruct2);
                mixingFrom = mixingFrom.MixingFrom;
            }
        }
    }

    and on receiving end for each received entry:

    Animation foundAnim;
    if (animationsByName.TryGetValue(animInfo.animName, out foundAnim)) {
        if (animInfo.isMixingFrom) {
            var trackMain = state.GetCurrent(animInfo.trackIndex);
            if (trackMain != null) {
                TrackEntry addMixFromEntry = state.NewTrackEntry(animInfo.trackIndex, foundAnim, animInfo.looping, trackMain);
                addMixFromEntry.mixDuration = animInfo.mixDuration;
                var mixingParent = trackMain;
                while (mixingParent.mixingFrom != null)
                    mixingParent = mixingParent.mixingFrom;
                mixingParent.mixingFrom = addMixFromEntry;
                mixingParent.MixTime = animInfo.animTime;
                addMixFromEntry.mixingTo = mixingParent;
            }
        } else {
            var trackEntry = state.SetAnimation(animInfo.trackIndex, foundAnim, animInfo.looping);
            trackEntry.mixDuration = animInfo.mixDuration;
            trackEntry.TrackTime = animInfo.animTime;
            trackEntry.mixTime = animInfo.mixTime;
        }
    } else if (animInfo.animName == "<empty>") {
        if (animInfo.isMixingFrom) {
            var trackMain = state.GetCurrent(animInfo.trackIndex);
            if (trackMain != null) {
                TrackEntry addMixFromEntry = state.NewTrackEntry(animInfo.trackIndex, AnimationState.EmptyAnimation, false, trackMain);
                addMixFromEntry.mixDuration = animInfo.mixDuration;
                var mixingParent = trackMain;
                while (mixingParent.mixingFrom != null)
                    mixingParent = mixingParent.mixingFrom;
                mixingParent.mixingFrom = addMixFromEntry;
                mixingParent.MixTime = animInfo.animTime;
                addMixFromEntry.mixingTo = mixingParent;
            }
        } else {
            var trackEntry = state.SetEmptyAnimation(animInfo.trackIndex, animInfo.mixDuration);
            trackEntry.TrackTime = animInfo.animTime;
            trackEntry.mixTime = animInfo.mixTime;
        }
    }

    I had to make NewTrackEntry a public method to do this code so probably not the best... I wonder if I missed any variables or if I should try another approach like using SetAnimation several times to simulate the chain of mixing, as opposed to trying to force variables.

  • Harald Hmm well basically the issue is that one client has ongoing character animations in real-time and then when I want to snapshot that to another client, I send the relevant track animations/times. And so far it has been spot on, except if any track was mixing... so really I am just trying to figure out how I can copy/force mixing state I guess.
    The to-be-copied code is quite simple:

    This list of animation info is sent over the network and the other client then uses the code I posted originally to pose. This is for a case where we need it to match for hitbox reasons, not just general animations where we could ignore discrepancies. And so far the only discrepancy I can find is if any track was mixing during this to-be-copied code. It seems I can get mixingFrom and mixTime, but I wasn't able to apply it properly on the receiving end (or make it match I mean).
    Thanks for response!

  • Hello, at the moment when I need to make a spine character match another one (over the network), I send their current track animation names and track times, as well as any IK bone positions. Then the other client takes that info to pose in exactly the same way. It seems to be 100% correct except when mixing is involved, ie if one track was within our 0.1s mix time, then it can actually get quite inaccurate for that portion.
    The code that poses the skeleton basically does this:

    state.Update(0.0f);
    state.Apply(skeleton);
    state.ClearTracks();
    foreach (var animInfo in receivedAnimationsData)
    {
        var trackEntry = state.SetAnimation(animInfo.trackIndex, animationsByName[animInfo.animName], animInfo.looping);
        trackEntry.TrackTime = animInfo.animTime;
    }
    state.Update(0.0f);
    state.Apply(skeleton);
    skeleton.UpdateWorldTransform();

    So I tried to send mixingFrom / mixTime with each track so that can be reproduced/set on the other client... but haven't had any luck making it work. I have tried setting the mixingFrom animation with SetAnimation first and then the actual one after, and I also tried manually overriding the variables themselves. Any help or tips is appreciated!

    • Hello, just wanted to give some feedback regarding event callbacks. There is one case that is harder to deal with at the moment which is if you want to do something every time an animation starts (looping or non looping), you have to use a weird combo of the Start and Complete event to handle these cases. The only other option is to have an event defined in the spine editor at the start of said animation.

    • Ah okay I guess I'll try that... if both seem to work is it better to just do the shallower AddSkin since it would be better on performance?

    • Hello, just wanted to make sure this is correct... we previously used skin utilities method:

      currentCustomSkin = characterSkin.GetClone();

      But since that is gone I am now doing it this way:

      currentCustomSkin = new Spine.Skin(characterSkin.Name + " clone");
      currentCustomSkin.CopySkin(characterSkin);

      Is this the correct way to replace that older method? It appears to work so far but wanted to make sure I'm not missing anything. Thanks!

      (this is used for skin building like mix & match)

    • Hey, I am wondering if I am doing something wrong regarding skin clones. We have a mix and match system that had been working fine in 3.7 ( and is mostly fine in 3.8 ). However it seems with any bones that are using the new skin-bones feature, they get messed up:

      What the arm-flaps should look like:

      All of our characters with outfits are created like so:

      characterSkin = skeletonData.FindSkin(currentCharacter.characterSpineSkinName);
      currentCustomSkin = characterSkin.GetClone();
      [bunch of attachments cloned for outfit]
      [...]
      skeleton.SetSkin(currentCustomSkin);
      skeleton.SetSlotsToSetupPose();
      

      Are we doing something wrong or is there a step I am missing? It all still works fine aside from any places we utilized the new skin bones. And the characters look fine if set without using clones.

      Thanks.

    • Finally upgraded to Spine 3.8! Aside from fixing the changed method/variable names, it went pretty smoothly. Only one issue spotted so far and it's related to Attachment's "GetRemappedClone". On Spine 3.7, region attachments would copy any color tinting, but mesh attachments wouldn't. So I had code like this:

      Spine.Attachment newAttachment = templateAttachment.GetRemappedClone(atlasRegion, true, true, skeletonDataAsset.scale);
       if (newAttachment == null)
           Debug.Log("clone attachment failed for " + originalSkin + " " + regionName + " " + slot + " " + templateAttachmentName);
       if (templateAttachment.GetType() == typeof(Spine.MeshAttachment))
       {
           Spine.MeshAttachment newAttachmentCasted = (Spine.MeshAttachment)newAttachment;
           Spine.MeshAttachment oldAttachment = (Spine.MeshAttachment)templateAttachment;
           newAttachmentCasted.R = oldAttachment.R;
           newAttachmentCasted.G = oldAttachment.G;
           newAttachmentCasted.B = oldAttachment.B;
           newAttachmentCasted.A = oldAttachment.A;
       }
      

      But I noticed in Spine 3.8, a bunch of our attachments were appearing white suddenly. So it turns out we need to also do this check for RegionAttachments now:

      if (templateAttachment.GetType() == typeof(Spine.MeshAttachment))
              {
                  Spine.MeshAttachment newAttachmentCasted = (Spine.MeshAttachment)newAttachment;
                  Spine.MeshAttachment oldAttachment = (Spine.MeshAttachment)templateAttachment;
                  newAttachmentCasted.R = oldAttachment.R;
                  newAttachmentCasted.G = oldAttachment.G;
                  newAttachmentCasted.B = oldAttachment.B;
                  newAttachmentCasted.A = oldAttachment.A;
              }
              else if (templateAttachment.GetType() == typeof(Spine.RegionAttachment))
              {
                  Spine.RegionAttachment newAttachmentCasted = (Spine.RegionAttachment)newAttachment;
                  Spine.RegionAttachment oldAttachment = (Spine.RegionAttachment)templateAttachment;
                  newAttachmentCasted.R = oldAttachment.R;
                  newAttachmentCasted.G = oldAttachment.G;
                  newAttachmentCasted.B = oldAttachment.B;
                  newAttachmentCasted.A = oldAttachment.A;
              }

      I'm not sure if this is intended but it's something I came across.

    • To clarify: these are the methods we are using:

      Spine.Attachment templateAttachment = theSkin.GetAttachment(slotIndex, hatPart.template);
      Spine.AtlasRegion atlasRegion = cachedAtlasRegions[hatPart.region];
      Spine.Attachment newAttachment = templateAttachment.GetRemappedClone(atlasRegion, true, false, skeletonAnimation.SkeletonDataAsset.scale);
      currentCustomSkin.SetAttachment(slotIndex, regionName, newAttachment);

      Like Fainder said, these run-time cloned attachments are automatically correctly tinted for region attachments, but mesh attachments show up untinted.

      Oh and we are using Spine 3.7 for both (Editor is 3.7.91 and Unity is Spine 3.7 2019 March 20th).


      Ok well I fixed it manually on our end for now:

      if (templateAttachment.GetType() == typeof(Spine.MeshAttachment))
      {
          Spine.MeshAttachment newAttachmentCasted = (Spine.MeshAttachment)newAttachment;
          Spine.MeshAttachment oldAttachment = (Spine.MeshAttachment)templateAttachment;
          newAttachmentCasted.R = oldAttachment.R;
          newAttachmentCasted.G = oldAttachment.G;
          newAttachmentCasted.B = oldAttachment.B;
          newAttachmentCasted.A = oldAttachment.A;
      }
    • Harald wrote

      Thanks very much for reporting this issue and sorry for the inconvenience!

      I have just commited a bugfix for the exception Material doesn't have a float or range property '_StraightAlphaInput'. I also removed the warnings in case of non-Spine shaders. So you should be able to enable the warnings again in the Spine Settings and receive neither errors nor warnings.

      It will be included in the next unitypackage, published in the next few days.

      Awesome, thanks for the quick response! Will it be in the 3.7 package or is it something in 3.8 beta?

      Thanks again!

    • So, I followed the upgrade steps exactly, but then had 86 errors in the Unity console... however after some digging around I found out it's just the new texture warning system. It was causing Spine not to compile (and thus not link into Visual Studio scripts). After doing a bunch of reading I figured out I could turn off the texture warnings in preferences, but then after that I had to clear console and then go alter one of my scripts to cause a recompile.

      The warnings looked like this:

      But my materials are using Sprites/Default so I am not using premultiply alpha.. seems like a warning about this stuff is okay, but an error that causes Spine not to compile is intended?

      Thanks.

    • I believe so, it says this spine unity runtime works with 3.6.xx. I can try 3.7 though haven't gotten around to it since everything is fairly stable in our project right now haha.


      Hey I figured out how to use Unity's deep profiler which gave some more information on this...

      Note that this is still on Spine 3.6 export/Unity, but the code for AddSubmesh() appears to be the same in 3.7.

      By debugging, it looks like meshGenerator.BuildMesh(currentInstructions, updateTriangles) is the culprit, but only if a skeleton has clipping in it as well as multiple atlases. If I turn on Single Submesh, or force clipping to off, then there is no GC alloc issues.

      So for now I am going to work with our animator to remove all instances of clipping in the Spine file, and animate in any way possible without it.
      It is also my guess that once we switch to doing run-time attachments/materials, then this problem would also go away...

    • Hey, I just noticed in the Unity Profiler that the large 'menu' version of our character is causing anywhere from 8KB to 25KB of GC alloc per frame. Our small 'in-game' version that fits in one material does not generate garbage. The only difference between the two SkeletonData's is that the large one's atlas uses 3 PNGs/materials.

      This will change depending on which skin I choose, which leads me to believe the GC alloc is happening because it might have some attachments in one material and some in another, though it's unclear why that has to cause so much allocations. I also tested just dragging the SkeletonData into a blank scene, so all that is running is the Spine skeleton, and it was the same.

      Is this expected? If not what can I do? Debug it? Run-time material? I am okay with sharing the file too, privately.

      Thanks.

      P.S. this is on 3.6.

    • Thanks for the detailed responses! With what Fainder mentioned, this is why we were doing it the current way... and are finally now trying to switch to templates / run-time attachments. Things kind of multiplied over time.

      Sounds like we are okay, but that I just need to pull template attachments from the default skin if it's not in the current character skin. And meanwhile Fainder would put a template/placeholder of each costume type in each costume slot. Ie body slot would have a generic template suit/shirt/tshirt/etc attachment that I can clone... and then we would have every actual image we need just sitting in the atlas in a region, ready to be swapped in as needed via my Unity code.