• Unity
  • How can I AddComponent<SkeletonAnimation> at runtime?

I'm building my skeleton character programmatically.

I built a Skeleton from SkeletonData, and then I should render this next step.

To render the skeleton, I added a comeponent "SkeletonAnimation"

But the problem is that the component requires a SkeletonDataAsset. I don't have the asset because I build Skeleton programmatically at runtime.

So I tried the following steps.

  1. in SkeletonRenderer.cs, I made the below commented.
    //if (!skeletonDataAsset)
    //{
    //    if (logErrors)
    //       Debug.LogError("Missing SkeletonData asset.", this);
    //
    //    return;
    //}
    
    //SkeletonData skeletonData = skeletonDataAsset.GetSkeletonData(false);
    //if (skeletonData == null)
    //    return;
    
    //skeleton = new Skeleton(skeletonData);
    
    1. in SkeletonAnimation.cs, I made the below commented as well.

      //state = new Spine.AnimationState(skeletonDataAsset.GetAnimationStateData());
      
    2. in my script, I assigned the "skeleton" and "state" directly.

      var anim = gameObject.AddComponent<SkeletonAnimation>();
      anim.skeleton = skeleton;
      anim.state = new Spine.AnimationState(new AnimationStateData(skeletonData));
      anim.enabled = true;
      
    I hit play, but nothing is rendered.

    How can I add the component SkeletonAnimation properly at runtime?
Related Discussions
...
  • Изменено

You programmatically generated a Skeleton object from SkeletonData but without SkeletonDataAsasset? Did you remove stuff from it for it to work?

Adding it with a SkeletonDataAsset works like this:

SkeletonAnimation skeletonAnimation = gameObject.AddComponent<SkeletonAnimation>();

skeletonAnimation.skeletonDataAsset = mySkeletonDataAsset;
skeletonAnimation.Reset();

// Now you can do whatever.
skeletonAnimation.state.SetAnimation(0, "walk", true);

But generating a Skeleton without SkeletonDataAsset isn't typical use so I'm not sure what the solution is yet.
But I assume you still need to call Reset so that it generates all the other objects even if you don't let it instantiate SkeletonData and AnimationState.

Reset is called on Awake.
And Unity Trivia: Awake is called before AddComponent returns.

So you may need to call it yourself after you've given it the right data.
You may also need to call skeleton.UpdateWorldTransform();. Depends on the situation. It may not be necessary since SkeletonAnimation calls that for you too.

The Reset method is likely to be renamed the Initialize method in the near future, since it collides with MonoBehaviour.Reset which isn't its intended use.

Can you describe why you're generating a Skeleton yourself instead of letting SkeletonRenderer do it? Use cases are informative for when we're improving the runtimes.


01 Dec 2015 12:34 am


You may also need to comment out this line: https://github.com/EsotericSoftware/spine-runtimes/blob/master/spine-unity/Assets/spine-unity/SkeletonRenderer.cs#L106
It says

skeleton = null;

I want to implement the following.

  1. My game has rooms.
  2. Many players can enter a room.
  3. A player can equip gears. (eg. a sword, a shield, etc)
  4. I want to merge the textures for gears into one big atlas. (so each character has only 1 draw call.)

So I thought it would be a good idea to link the atlas after building a new Skeleton.

Here's my code.
but I don't have the SkeletonDataAsset.
How to assign SkeletonData into anim.skeletonDataAsset?

public Texture2D[] textures;
public Rect[] rects;


private void Start()
{
   Texture2D bigTexture = new Texture2D(1024, 1024);
   rects = bigTexture.PackTextures(textures, 0, 1024);

   AtlasPage atlasPage = new AtlasPage();
   Material mat = new Material(Shader.Find("Spine/Skeleton"));
   mat.name = "My Mat Name";
   mat.mainTexture = bigTexture;
   atlasPage.rendererObject = mat;

   AtlasRegion weaponAtlasRegion = new AtlasRegion();
   weaponAtlasRegion.name = "weapon";
   weaponAtlasRegion.page = atlasPage;
   weaponAtlasRegion.u = rects[0].x;
   weaponAtlasRegion.v = 0;
   weaponAtlasRegion.u2 = 0.75f;
   weaponAtlasRegion.v2 = 1;
   weaponAtlasRegion.width = 64;
   weaponAtlasRegion.height = 128;
   weaponAtlasRegion.originalWidth = 256;
   weaponAtlasRegion.originalHeight = 128;
   weaponAtlasRegion.offsetX = 0;
   weaponAtlasRegion.offsetY = 0;

   AtlasRegion shieldAtlasRegion = new AtlasRegion();
   shieldAtlasRegion.name = "shield";
   shieldAtlasRegion.page = atlasPage;
   ...

   var pageList = new List<AtlasPage>();
   var regionList = new List<AtlasRegion>();

   pageList.Add(atlasPage);
   regionList.Add(weaponAtlasRegion);
   regionList.Add(shieldAtlasRegion);

   Atlas atlas = new Atlas(pageList, regionList);
   AtlasAttachmentLoader loader = new AtlasAttachmentLoader(atlas);

   SkeletonJson json = new SkeletonJson(loader);
   SkeletonData skeletonData = json.ReadSkeletonData(Application.dataPath + "/skejson.json");

   Skeleton skeleton = new Skeleton(skeletonData);
   skeleton.FindSlot("Weapon").Attachment = loader.NewRegionAttachment(null, "weapon", "weapon");
   skeleton.FindSlot("Shield").Attachment = loader.NewRegionAttachment(null, "shield", "shield");

   var anim = gameObject.AddComponent<SkeletonAnimation>();
   anim.skeletonDataAsset = ?????; // I don't have the SkeletonDataAsset, but only the SkeletonData.
}

So your need revolves around needing a custom Skeleton per object, and a custom Atlas.

SkeletonDataAsset is just a ScriptableObject. It's not necessarily something you need to instantiate in Unity Editor.

What if you modified SkeletonDataAsset to be subclassable,
subclass it (eg, public class CustomSkeletonDataAsset : SkeletonDataAsset { ... } with a version that won't look for all the typical stuff, then put your custom atlas and SkeletonData inside it?

At runtime, you could do ScriptableObject.CreateInstance<CustomSkeletonDataAsset>(); for every player.
And then put your custom SkeletonData and generated Atlas in it per instance. Then feed that to the unmodified SkeletonAnimation object.

This way, you can also allow it to share the same SkeletonData instance across multiple SkeletonDataAssets if you're not modifying the SkeletonData. SkeletonData is just the deserialized version of the json, and it is stateless unless you change something in it.

And each SkeletonRenderer generates its own Skeleton object so this is already what you need.

  • Изменено

It's working OK now. Thanks for helping me. 🙂

I modified "SkeletonDataAsset.cs" like below.

protected SkeletonData skeletonData;

public SkeletonData GetSkeletonData(bool quiet)
{
      //      if (atlasAssets == null)
      //      {
      //         atlasAssets = new AtlasAsset[0];
      //         if (!quiet)
      //            Debug.LogError("Atlas not set for SkeletonData asset: " + name, this);
      //         Reset();
      //         return null;
      //      }

  //      if (skeletonJSON == null)
  //      {
  //         if (!quiet)
  //            Debug.LogError("Skeleton JSON file not set for SkeletonData asset: " + name, this);
  //         Reset();
  //         return null;
  //      }

  //#if !SPINE_TK2D
  //      if (atlasAssets.Length == 0)
  //      {
  //         Reset();
  //         return null;
  //      }
  //#else
  //            if (atlasAssets.Length == 0 && spriteCollection == null) {
  //               Reset();
  //               return null;
  //            }
  //#endif

  //      Atlas[] atlasArr = new Atlas[atlasAssets.Length];
  //      for (int i = 0; i < atlasAssets.Length; i++)
  //      {
  //         if (atlasAssets[i] == null)
  //         {
  //            Reset();
  //            return null;
  //         }
  //         atlasArr[i] = atlasAssets[i].GetAtlas();
  //         if (atlasArr[i] == null)
  //         {
  //            Reset();
  //            return null;
  //         }
  //      }


  //      if (skeletonData != null)
  //         return skeletonData;

  //      AttachmentLoader attachmentLoader;
  //      float skeletonDataScale;

  //#if !SPINE_TK2D
  //      attachmentLoader = new AtlasAttachmentLoader(atlasArr);
  //      skeletonDataScale = scale;
  //#else
  //            if (spriteCollection != null) {
  //               attachmentLoader = new SpriteCollectionAttachmentLoader(spriteCollection);
  //               skeletonDataScale = (1.0f / (spriteCollection.invOrthoSize * spriteCollection.halfTargetHeight) * scale) * 100f;
  //            } else {
  //               if (atlasArr.Length == 0) {
  //                  Reset();
  //                  if (!quiet) Debug.LogError("Atlas not set for SkeletonData asset: " + name, this);
  //                  return null;
  //               }
  //               attachmentLoader = new AtlasAttachmentLoader(atlasArr);
  //               skeletonDataScale = scale;
  //            }
  //#endif

  //      try
  //      {
  //         //var stopwatch = new System.Diagnostics.Stopwatch();
  //         if (skeletonJSON.name.ToLower().Contains(".skel"))
  //         {
  //            var input = new MemoryStream(skeletonJSON.bytes);
  //            var binary = new SkeletonBinary(attachmentLoader);
  //            binary.Scale = skeletonDataScale;
  //            //stopwatch.Start();
  //            skeletonData = binary.ReadSkeletonData(input);
  //         }
  //         else
  //         {
  //            var input = new StringReader(skeletonJSON.text);
  //            var json = new SkeletonJson(attachmentLoader);
  //            json.Scale = skeletonDataScale;
  //            //stopwatch.Start(;)
  //            skeletonData = json.ReadSkeletonData(input);
  //         }
  //         //stopwatch.Stop();
  //         //Debug.Log(stopwatch.Elapsed);
  //      }
  //      catch (Exception ex)
  //      {
  //         if (!quiet)
  //            Debug.LogError("Error reading skeleton JSON file for SkeletonData asset: " + name + "\n" + ex.Message + "\n" + ex.StackTrace, this);
  //         return null;
  //      }

  stateData = new AnimationStateData(skeletonData);
  //FillStateData();
  return skeletonData;
}

and defined "CustomSkeletonDataAsset" class which inherits "SkeletonDataAsset" class.

public class CustomSkeletonDataAsset : SkeletonDataAsset
{
   public void SetSkeletonData(SkeletonData skelData)
   {
      skeletonData = skelData;
   }
}

Now I use "CustomSkeletonDataAsset" like below.

var customSkeletonDataAsset = ScriptableObject.CreateInstance<CustomSkeletonDataAsset>();
customSkeletonDataAsset.SetSkeletonData(skeletonData);

var skelAnim = gameObject.AddComponent<SkeletonAnimation>();
skelAnim.skeletonDataAsset = customSkeletonDataAsset;
skelAnim.Reset();

var weapon = loader.NewRegionAttachment(null, "weapon", "weapon");
weapon.UpdateOffset();
skelAnim.skeleton.FindSlot("Weapon").attachment = weapon;

But I found that the skeleton had't been shown in the scene. I made sure the number of vertices had increased, so supposed the width and height of attachments had NOT been set properly.
So I added 2 lines in "AtlasAttachmentLoader.cs".

public RegionAttachment NewRegionAttachment (Skin skin, String name, String path)
{
   AtlasRegion region = FindRegion(path);
   if (region == null) throw new Exception("Region not found in atlas: " + path + " (region attachment: " + name + ")");
   RegionAttachment attachment = new RegionAttachment(name);
   attachment.RendererObject = region;
   attachment.SetUVs(region.u, region.v, region.u2, region.v2, region.rotate);
   attachment.regionOffsetX = region.offsetX;
   attachment.regionOffsetY = region.offsetY;
   attachment.regionWidth = region.width;
   attachment.regionHeight = region.height;
   attachment.regionOriginalWidth = region.originalWidth;
   attachment.regionOriginalHeight = region.originalHeight;
   attachment.width = region.width;      //added.
   attachment.height = region.height;    //added.
   return attachment;
}

Finally can have 1 draw call.

If you want to keep the original functionality but make it optional, mark the method in the base class virtual.
Then use override in the extending class to change its functionality there.
This way, you can make the base class do mostly what it originally did.

Otherwise, great work!

Thanks very so much. It works perfectly OK now. 🙂

in "SkeletonDataAsset.cs",

protected SkeletonData skeletonData;
protected AnimationStateData stateData;

public virtual SkeletonData GetSkeletonData(bool quiet)
{
      if (atlasAssets == null)
      {
         atlasAssets = new AtlasAsset[0];
         if (!quiet)
            Debug.LogError("Atlas not set for SkeletonData asset: " + name, this);
         Reset();
         return null;
      }
     ...
     ...
     ...
}

in "CustomSkeletonDataAsset" class,

public class CustomSkeletonDataAsset : SkeletonDataAsset
{
   public void SetSkeletonData(SkeletonData skelData)
   {
      skeletonData = skelData;
      stateData = new AnimationStateData(skeletonData);
   }

   public override SkeletonData GetSkeletonData(bool quiet)
   {
      return skeletonData;
   }
}